Skip to content

Commit e5f9c2e

Browse files
author
Ubuntu
committed
Add initial Set implementation
1 parent 016ea00 commit e5f9c2e

File tree

4 files changed

+426
-27
lines changed

4 files changed

+426
-27
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
@@ -129,6 +129,17 @@ func BenchmarkJsonParserObjectEachStructSmall(b *testing.B) {
129129
}
130130
}
131131

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

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

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) {
@@ -445,34 +460,114 @@ var (
445460
nullLiteral = []byte("null")
446461
)
447462

463+
func createInsertComponent(keys []string, setValue []byte, comma, object bool) []byte {
464+
var buffer bytes.Buffer
465+
if comma {
466+
buffer.WriteString(",")
467+
}
468+
if object {
469+
buffer.WriteString("{")
470+
}
471+
buffer.WriteString("\"")
472+
buffer.WriteString(keys[0])
473+
buffer.WriteString("\":")
474+
for i := 1; i < len(keys); i++ {
475+
buffer.WriteString("{\"")
476+
buffer.WriteString(keys[i])
477+
buffer.WriteString("\":")
478+
}
479+
buffer.Write(setValue)
480+
buffer.WriteString(strings.Repeat("}", len(keys)-1))
481+
if object {
482+
buffer.WriteString("}")
483+
}
484+
return buffer.Bytes()
485+
}
486+
448487
/*
449-
Get - Receives data structure, and key path to extract value from.
488+
489+
Set - Receives existing data structure, path to set, and data to set at that key.
450490
451491
Returns:
452-
`value` - Pointer to original data structure containing key value, or just empty slice if nothing found or error
453-
`dataType` - Can be: `NotExist`, `String`, `Number`, `Object`, `Array`, `Boolean` or `Null`
454-
`offset` - Offset from provided data structure where key value ends. Used mostly internally, for example for `ArrayEach` helper.
455-
`err` - If key not found or any other parsing issue it should return error. If key not found it also sets `dataType` to `NotExist`
492+
`value` - modified byte array
493+
`err` - On any parsing error
456494
457-
Accept multiple keys to specify path to JSON value (in case of quering nested structures).
458-
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.
459495
*/
460-
func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset int, err error) {
461-
if len(keys) > 0 {
462-
if offset = searchKeys(data, keys...); offset == -1 {
463-
return []byte{}, NotExist, -1, KeyPathNotFoundError
496+
func Set(data []byte, setValue []byte, keys ...string) (value []byte, err error) {
497+
// ensure keys are set
498+
if len(keys) == 0 {
499+
return nil, KeyPathNotFoundError
500+
}
501+
502+
_, _, startOffset, endOffset, err := internalGet(data, keys...)
503+
if err != nil {
504+
if err != KeyPathNotFoundError {
505+
// problem parsing the data
506+
return []byte{}, err
507+
}
508+
// full path doesnt exist
509+
// does any subpath exist?
510+
var depth int
511+
for i := range keys {
512+
_, _, start, end, sErr := internalGet(data, keys[:i+1]...)
513+
if sErr != nil {
514+
break
515+
} else {
516+
endOffset = end
517+
startOffset = start
518+
depth++
519+
}
464520
}
465-
}
521+
comma := true
522+
object := false
523+
if endOffset == -1 {
524+
firstToken := nextToken(data)
525+
// We can't set a top-level key if data isn't an object
526+
if len(data) == 0 || data[firstToken] != '{' {
527+
return nil, KeyPathNotFoundError
528+
}
529+
// Don't need a comma if the input is an empty object
530+
secondToken := firstToken + 1 + nextToken(data[firstToken+1:])
531+
if data[secondToken] == '}' {
532+
comma = false
533+
}
534+
// Set the top level key at the end (accounting for any trailing whitespace)
535+
// This assumes last token is valid like '}', could check and return error
536+
endOffset = lastToken(data)
537+
}
538+
depthOffset := endOffset
539+
if depth != 0 {
540+
// if subpath is a non-empty object, add to it
541+
if data[startOffset] == '{' && data[startOffset+1+nextToken(data[startOffset+1:])]!='}' {
542+
depthOffset--
543+
startOffset = depthOffset
544+
// otherwise, over-write it with a new object
545+
} else {
546+
comma = false
547+
object = true
548+
}
549+
} else {
550+
startOffset = depthOffset
551+
}
552+
value = append(data[:startOffset], append(createInsertComponent(keys[depth:], setValue, comma, object), data[depthOffset:]...)...)
553+
} else {
554+
// path currently exists
555+
startComponent := data[:startOffset]
556+
endComponent := data[endOffset:]
466557

467-
// Go to closest value
468-
nO := nextToken(data[offset:])
469-
if nO == -1 {
470-
return []byte{}, NotExist, -1, MalformedJsonError
558+
value = make([]byte, len(startComponent)+len(endComponent)+len(setValue))
559+
newEndOffset := startOffset + len(setValue)
560+
copy(value[0:startOffset], startComponent)
561+
copy(value[startOffset:newEndOffset], setValue)
562+
copy(value[newEndOffset:], endComponent)
471563
}
564+
return value, nil
565+
}
472566

473-
offset += nO
474-
567+
func getType(data []byte, offset int) ([]byte, ValueType, int, error) {
568+
var dataType ValueType
475569
endOffset := offset
570+
476571
// if string value
477572
if data[offset] == '"' {
478573
dataType = String
@@ -532,8 +627,44 @@ func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset
532627

533628
endOffset += end
534629
}
630+
return data[offset:endOffset], dataType, endOffset, nil
631+
}
632+
633+
/*
634+
Get - Receives data structure, and key path to extract value from.
635+
636+
Returns:
637+
`value` - Pointer to original data structure containing key value, or just empty slice if nothing found or error
638+
`dataType` - Can be: `NotExist`, `String`, `Number`, `Object`, `Array`, `Boolean` or `Null`
639+
`offset` - Offset from provided data structure where key value ends. Used mostly internally, for example for `ArrayEach` helper.
640+
`err` - If key not found or any other parsing issue it should return error. If key not found it also sets `dataType` to `NotExist`
535641
536-
value = data[offset:endOffset]
642+
Accept multiple keys to specify path to JSON value (in case of quering nested structures).
643+
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.
644+
*/
645+
func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset int, err error) {
646+
a, b, _, d, e := internalGet(data, keys...)
647+
return a, b, d, e
648+
}
649+
650+
func internalGet(data []byte, keys ...string) (value []byte, dataType ValueType, offset, endOffset int, err error) {
651+
if len(keys) > 0 {
652+
if offset = searchKeys(data, keys...); offset == -1 {
653+
return []byte{}, NotExist, -1, -1, KeyPathNotFoundError
654+
}
655+
}
656+
657+
// Go to closest value
658+
nO := nextToken(data[offset:])
659+
if nO == -1 {
660+
return []byte{}, NotExist, offset, -1, MalformedJsonError
661+
}
662+
663+
offset += nO
664+
value, dataType, endOffset, err = getType(data, offset)
665+
if err != nil {
666+
return value, dataType, offset, endOffset, err
667+
}
537668

538669
// Strip quotes from string values
539670
if dataType == String {
@@ -544,7 +675,7 @@ func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset
544675
value = []byte{}
545676
}
546677

547-
return value, dataType, endOffset, nil
678+
return value, dataType, offset, endOffset, nil
548679
}
549680

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

0 commit comments

Comments
 (0)