Skip to content

Commit bb14bb6

Browse files
authored
Merge pull request #102 from JoshKCarroll/add-set-impl
Add initial Set implementation #61
2 parents ee11858 + 0da1997 commit bb14bb6

File tree

4 files changed

+419
-20
lines changed

4 files changed

+419
-20
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,20 @@ jsonparser.EachKey(smallFixture, func(idx int, value []byte, vt jsonparser.Value
201201
}, paths...)
202202
```
203203

204+
### **`Set`**
205+
```go
206+
func Set(data []byte, setValue []byte, keys ...string) (value []byte, err error)
207+
```
208+
Receives existing data structure, key path to set, and value to set at that key. *This functionality is experimental.*
209+
210+
Returns:
211+
* `value` - Pointer to original data structure with updated or added key value.
212+
* `err` - If any parsing issue, it should return error.
213+
214+
Accepts multiple keys to specify path to JSON value (in case of updating or creating nested structures).
215+
216+
Note that keys can be an array indexes: `jsonparser.Set(data, []byte("http://github.com"), "person", "avatars", "[0]", "url")`
217+
204218

205219
## What makes it so fast?
206220
* It does not rely on `encoding/json`, `reflection` or `interface{}`, the only real package dependency is `bytes`.

benchmark/benchmark_small_payload_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,17 @@ func BenchmarkJsonParserObjectEachStructSmall(b *testing.B) {
130130
}
131131
}
132132

133+
func BenchmarkJsonParserSetSmall(b *testing.B) {
134+
for i := 0; i < b.N; i++ {
135+
jsonparser.Set(smallFixture, []byte(`"c90927dd-1588-4fe7-a14f-8a8950cfcbd8"`), "uuid")
136+
jsonparser.Set(smallFixture, []byte("-3"), "tz")
137+
jsonparser.Set(smallFixture, []byte(`"server_agent"`), "ua")
138+
jsonparser.Set(smallFixture, []byte("3"), "st")
139+
140+
nothing()
141+
}
142+
}
143+
133144
/*
134145
encoding/json
135146
*/
@@ -186,6 +197,19 @@ func BenchmarkGoSimplejsonSmall(b *testing.B) {
186197
}
187198
}
188199

200+
func BenchmarkGoSimplejsonSetSmall(b *testing.B) {
201+
for i := 0; i < b.N; i++ {
202+
json, _ := simplejson.NewJson(smallFixture)
203+
204+
json.SetPath([]string{"uuid"}, "c90927dd-1588-4fe7-a14f-8a8950cfcbd8")
205+
json.SetPath([]string{"tz"}, -3)
206+
json.SetPath([]string{"ua"}, "server_agent")
207+
json.SetPath([]string{"st"}, 3)
208+
209+
nothing()
210+
}
211+
}
212+
189213
/*
190214
github.com/pquerna/ffjson
191215
*/

parser.go

Lines changed: 151 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"math"
88
"strconv"
9+
"strings"
910
)
1011

1112
// Errors
@@ -49,6 +50,20 @@ func nextToken(data []byte) int {
4950
return -1
5051
}
5152

53+
// Find position of last character which is not whitespace
54+
func lastToken(data []byte) int {
55+
for i := len(data) - 1; i >= 0; i-- {
56+
switch data[i] {
57+
case ' ', '\n', '\r', '\t':
58+
continue
59+
default:
60+
return i
61+
}
62+
}
63+
64+
return -1
65+
}
66+
5267
// Tries to find the end of string
5368
// Support if string contains escaped quote symbols.
5469
func stringEnd(data []byte) (int, bool) {
@@ -460,34 +475,114 @@ var (
460475
nullLiteral = []byte("null")
461476
)
462477

478+
func createInsertComponent(keys []string, setValue []byte, comma, object bool) []byte {
479+
var buffer bytes.Buffer
480+
if comma {
481+
buffer.WriteString(",")
482+
}
483+
if object {
484+
buffer.WriteString("{")
485+
}
486+
buffer.WriteString("\"")
487+
buffer.WriteString(keys[0])
488+
buffer.WriteString("\":")
489+
for i := 1; i < len(keys); i++ {
490+
buffer.WriteString("{\"")
491+
buffer.WriteString(keys[i])
492+
buffer.WriteString("\":")
493+
}
494+
buffer.Write(setValue)
495+
buffer.WriteString(strings.Repeat("}", len(keys)-1))
496+
if object {
497+
buffer.WriteString("}")
498+
}
499+
return buffer.Bytes()
500+
}
501+
463502
/*
464-
Get - Receives data structure, and key path to extract value from.
503+
504+
Set - Receives existing data structure, path to set, and data to set at that key.
465505
466506
Returns:
467-
`value` - Pointer to original data structure containing key value, or just empty slice if nothing found or error
468-
`dataType` - Can be: `NotExist`, `String`, `Number`, `Object`, `Array`, `Boolean` or `Null`
469-
`offset` - Offset from provided data structure where key value ends. Used mostly internally, for example for `ArrayEach` helper.
470-
`err` - If key not found or any other parsing issue it should return error. If key not found it also sets `dataType` to `NotExist`
507+
`value` - modified byte array
508+
`err` - On any parsing error
471509
472-
Accept multiple keys to specify path to JSON value (in case of quering nested structures).
473-
If no keys provided it will try to extract closest JSON value (simple ones or object/array), useful for reading streams or arrays, see `ArrayEach` implementation.
474510
*/
475-
func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset int, err error) {
476-
if len(keys) > 0 {
477-
if offset = searchKeys(data, keys...); offset == -1 {
478-
return []byte{}, NotExist, -1, KeyPathNotFoundError
511+
func Set(data []byte, setValue []byte, keys ...string) (value []byte, err error) {
512+
// ensure keys are set
513+
if len(keys) == 0 {
514+
return nil, KeyPathNotFoundError
515+
}
516+
517+
_, _, startOffset, endOffset, err := internalGet(data, keys...)
518+
if err != nil {
519+
if err != KeyPathNotFoundError {
520+
// problem parsing the data
521+
return []byte{}, err
522+
}
523+
// full path doesnt exist
524+
// does any subpath exist?
525+
var depth int
526+
for i := range keys {
527+
_, _, start, end, sErr := internalGet(data, keys[:i+1]...)
528+
if sErr != nil {
529+
break
530+
} else {
531+
endOffset = end
532+
startOffset = start
533+
depth++
534+
}
479535
}
480-
}
536+
comma := true
537+
object := false
538+
if endOffset == -1 {
539+
firstToken := nextToken(data)
540+
// We can't set a top-level key if data isn't an object
541+
if len(data) == 0 || data[firstToken] != '{' {
542+
return nil, KeyPathNotFoundError
543+
}
544+
// Don't need a comma if the input is an empty object
545+
secondToken := firstToken + 1 + nextToken(data[firstToken+1:])
546+
if data[secondToken] == '}' {
547+
comma = false
548+
}
549+
// Set the top level key at the end (accounting for any trailing whitespace)
550+
// This assumes last token is valid like '}', could check and return error
551+
endOffset = lastToken(data)
552+
}
553+
depthOffset := endOffset
554+
if depth != 0 {
555+
// if subpath is a non-empty object, add to it
556+
if data[startOffset] == '{' && data[startOffset+1+nextToken(data[startOffset+1:])]!='}' {
557+
depthOffset--
558+
startOffset = depthOffset
559+
// otherwise, over-write it with a new object
560+
} else {
561+
comma = false
562+
object = true
563+
}
564+
} else {
565+
startOffset = depthOffset
566+
}
567+
value = append(data[:startOffset], append(createInsertComponent(keys[depth:], setValue, comma, object), data[depthOffset:]...)...)
568+
} else {
569+
// path currently exists
570+
startComponent := data[:startOffset]
571+
endComponent := data[endOffset:]
481572

482-
// Go to closest value
483-
nO := nextToken(data[offset:])
484-
if nO == -1 {
485-
return []byte{}, NotExist, -1, MalformedJsonError
573+
value = make([]byte, len(startComponent)+len(endComponent)+len(setValue))
574+
newEndOffset := startOffset + len(setValue)
575+
copy(value[0:startOffset], startComponent)
576+
copy(value[startOffset:newEndOffset], setValue)
577+
copy(value[newEndOffset:], endComponent)
486578
}
579+
return value, nil
580+
}
487581

488-
offset += nO
489-
582+
func getType(data []byte, offset int) ([]byte, ValueType, int, error) {
583+
var dataType ValueType
490584
endOffset := offset
585+
491586
// if string value
492587
if data[offset] == '"' {
493588
dataType = String
@@ -547,15 +642,51 @@ func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset
547642

548643
endOffset += end
549644
}
645+
return data[offset:endOffset], dataType, endOffset, nil
646+
}
647+
648+
/*
649+
Get - Receives data structure, and key path to extract value from.
650+
651+
Returns:
652+
`value` - Pointer to original data structure containing key value, or just empty slice if nothing found or error
653+
`dataType` - Can be: `NotExist`, `String`, `Number`, `Object`, `Array`, `Boolean` or `Null`
654+
`offset` - Offset from provided data structure where key value ends. Used mostly internally, for example for `ArrayEach` helper.
655+
`err` - If key not found or any other parsing issue it should return error. If key not found it also sets `dataType` to `NotExist`
550656
551-
value = data[offset:endOffset]
657+
Accept multiple keys to specify path to JSON value (in case of quering nested structures).
658+
If no keys provided it will try to extract closest JSON value (simple ones or object/array), useful for reading streams or arrays, see `ArrayEach` implementation.
659+
*/
660+
func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset int, err error) {
661+
a, b, _, d, e := internalGet(data, keys...)
662+
return a, b, d, e
663+
}
664+
665+
func internalGet(data []byte, keys ...string) (value []byte, dataType ValueType, offset, endOffset int, err error) {
666+
if len(keys) > 0 {
667+
if offset = searchKeys(data, keys...); offset == -1 {
668+
return []byte{}, NotExist, -1, -1, KeyPathNotFoundError
669+
}
670+
}
671+
672+
// Go to closest value
673+
nO := nextToken(data[offset:])
674+
if nO == -1 {
675+
return []byte{}, NotExist, offset, -1, MalformedJsonError
676+
}
677+
678+
offset += nO
679+
value, dataType, endOffset, err = getType(data, offset)
680+
if err != nil {
681+
return value, dataType, offset, endOffset, err
682+
}
552683

553684
// Strip quotes from string values
554685
if dataType == String {
555686
value = value[1 : len(value)-1]
556687
}
557688

558-
return value, dataType, endOffset, nil
689+
return value, dataType, offset, endOffset, nil
559690
}
560691

561692
// ArrayEach is used when iterating arrays, accepts a callback function with the same return arguments as `Get`.

0 commit comments

Comments
 (0)