Skip to content

Commit 8c2d6bb

Browse files
authored
Merge pull request buger#58 from pendo-io/objecteach
Add ObjectEach() for iterating over object key-value pairs
2 parents 7e1e8d1 + 03905a4 commit 8c2d6bb

4 files changed

Lines changed: 442 additions & 26 deletions

File tree

benchmark/benchmark_medium_payload_test.go

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"github.com/ugorji/go/codec"
1717
"testing"
1818
// "fmt"
19+
"bytes"
20+
"errors"
1921
)
2022

2123
/*
@@ -35,22 +37,22 @@ func BenchmarkJsonParserMedium(b *testing.B) {
3537
}
3638

3739
func BenchmarkJsonParserEachKeyManualMedium(b *testing.B) {
38-
for i := 0; i < b.N; i++ {
39-
paths := [][]string{
40-
[]string{"person", "name", "fullName"},
41-
[]string{"person", "github", "followers"},
42-
[]string{"company"},
43-
[]string{"person", "gravatar", "avatars"},
44-
}
40+
paths := [][]string{
41+
[]string{"person", "name", "fullName"},
42+
[]string{"person", "github", "followers"},
43+
[]string{"company"},
44+
[]string{"person", "gravatar", "avatars"},
45+
}
4546

46-
jsonparser.EachKey(mediumFixture, func(idx int, value []byte, vt jsonparser.ValueType, err error){
47+
for i := 0; i < b.N; i++ {
48+
jsonparser.EachKey(mediumFixture, func(idx int, value []byte, vt jsonparser.ValueType, err error) {
4749
switch idx {
4850
case 0:
49-
// jsonparser.ParseString(value)
51+
// jsonparser.ParseString(value)
5052
case 1:
5153
jsonparser.ParseInt(value)
5254
case 2:
53-
// jsonparser.ParseString(value)
55+
// jsonparser.ParseString(value)
5456
case 3:
5557
jsonparser.ArrayEach(value, func(avalue []byte, dataType jsonparser.ValueType, offset int, err error) {
5658
jsonparser.Get(avalue, "url")
@@ -60,6 +62,90 @@ func BenchmarkJsonParserEachKeyManualMedium(b *testing.B) {
6062
}
6163
}
6264

65+
func BenchmarkJsonParserEachKeyStructMedium(b *testing.B) {
66+
paths := [][]string{
67+
[]string{"person", "name", "fullName"},
68+
[]string{"person", "github", "followers"},
69+
[]string{"company"},
70+
[]string{"person", "gravatar", "avatars"},
71+
}
72+
73+
for i := 0; i < b.N; i++ {
74+
data := MediumPayload{
75+
Person: &CBPerson{
76+
Name: &CBName{},
77+
Github: &CBGithub{},
78+
Gravatar: &CBGravatar{},
79+
},
80+
}
81+
82+
jsonparser.EachKey(mediumFixture, func(idx int, value []byte, vt jsonparser.ValueType, err error) {
83+
switch idx {
84+
case 0:
85+
data.Person.Name.FullName, _ = jsonparser.ParseString(value)
86+
case 1:
87+
v, _ := jsonparser.ParseInt(value)
88+
data.Person.Github.Followers = int(v)
89+
case 2:
90+
json.Unmarshal(value, &data.Company) // we don't have a JSON -> map[string]interface{} function yet, so use standard encoding/json here
91+
case 3:
92+
var avatars []*CBAvatar
93+
jsonparser.ArrayEach(value, func(avalue []byte, dataType jsonparser.ValueType, offset int, err error) {
94+
url, _ := jsonparser.ParseString(avalue)
95+
avatars = append(avatars, &CBAvatar{Url: url})
96+
})
97+
data.Person.Gravatar.Avatars = avatars
98+
}
99+
}, paths...)
100+
}
101+
}
102+
103+
func BenchmarkJsonParserObjectEachStructMedium(b *testing.B) {
104+
nameKey, githubKey, gravatarKey := []byte("name"), []byte("github"), []byte("gravatar")
105+
errStop := errors.New("stop")
106+
107+
for i := 0; i < b.N; i++ {
108+
data := MediumPayload{
109+
Person: &CBPerson{
110+
Name: &CBName{},
111+
Github: &CBGithub{},
112+
Gravatar: &CBGravatar{},
113+
},
114+
}
115+
116+
missing := 3
117+
118+
jsonparser.ObjectEach(mediumFixture, func(k, v []byte, vt jsonparser.ValueType, o int) error {
119+
switch {
120+
case bytes.Equal(k, nameKey):
121+
data.Person.Name.FullName, _ = jsonparser.GetString(v, "fullName")
122+
missing--
123+
case bytes.Equal(k, githubKey):
124+
x, _ := jsonparser.GetInt(v, "followers")
125+
data.Person.Github.Followers = int(x)
126+
missing--
127+
case bytes.Equal(k, gravatarKey):
128+
var avatars []*CBAvatar
129+
jsonparser.ArrayEach(v, func(avalue []byte, dataType jsonparser.ValueType, offset int, err error) {
130+
url, _ := jsonparser.ParseString(avalue)
131+
avatars = append(avatars, &CBAvatar{Url: url})
132+
}, "avatars")
133+
data.Person.Gravatar.Avatars = avatars
134+
missing--
135+
}
136+
137+
if missing == 0 {
138+
return errStop
139+
} else {
140+
return nil
141+
}
142+
}, "person")
143+
144+
cv, _, _, _ := jsonparser.Get(mediumFixture, "company")
145+
json.Unmarshal(cv, &data.Company)
146+
}
147+
}
148+
63149
/*
64150
encoding/json
65151
*/

benchmark/benchmark_small_payload_test.go

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"github.com/ugorji/go/codec"
1717
"testing"
1818
// "fmt"
19+
"bytes"
20+
"errors"
1921
)
2022

2123
// Just for emulating field access, so it will not throw "evaluated but not used"
@@ -36,15 +38,15 @@ func BenchmarkJsonParserSmall(b *testing.B) {
3638
}
3739

3840
func BenchmarkJsonParserEachKeyManualSmall(b *testing.B) {
41+
paths := [][]string{
42+
[]string{"uuid"},
43+
[]string{"tz"},
44+
[]string{"ua"},
45+
[]string{"st"},
46+
}
47+
3948
for i := 0; i < b.N; i++ {
40-
paths := [][]string{
41-
[]string{"uuid"},
42-
[]string{"tz"},
43-
[]string{"ua"},
44-
[]string{"st"},
45-
}
46-
47-
jsonparser.EachKey(smallFixture, func(idx int, value []byte, vt jsonparser.ValueType, err error){
49+
jsonparser.EachKey(smallFixture, func(idx int, value []byte, vt jsonparser.ValueType, err error) {
4850
switch idx {
4951
case 0:
5052
// jsonparser.ParseString(value)
@@ -59,18 +61,18 @@ func BenchmarkJsonParserEachKeyManualSmall(b *testing.B) {
5961
}
6062
}
6163

62-
6364
func BenchmarkJsonParserEachKeyStructSmall(b *testing.B) {
65+
paths := [][]string{
66+
[]string{"uuid"},
67+
[]string{"tz"},
68+
[]string{"ua"},
69+
[]string{"st"},
70+
}
71+
6472
for i := 0; i < b.N; i++ {
65-
paths := [][]string{
66-
[]string{"uuid"},
67-
[]string{"tz"},
68-
[]string{"ua"},
69-
[]string{"st"},
70-
}
7173
var data SmallPayload
7274

73-
jsonparser.EachKey(smallFixture, func(idx int, value []byte, vt jsonparser.ValueType, err error){
75+
jsonparser.EachKey(smallFixture, func(idx int, value []byte, vt jsonparser.ValueType, err error) {
7476
switch idx {
7577
case 0:
7678
data.Uuid, _ = jsonparser.ParseString(value)
@@ -89,6 +91,44 @@ func BenchmarkJsonParserEachKeyStructSmall(b *testing.B) {
8991
}
9092
}
9193

94+
func BenchmarkJsonParserObjectEachStructSmall(b *testing.B) {
95+
uuidKey, tzKey, uaKey, stKey := []byte("uuid"), []byte("tz"), []byte("ua"), []byte("st")
96+
errStop := errors.New("stop")
97+
98+
for i := 0; i < b.N; i++ {
99+
var data SmallPayload
100+
101+
missing := 4
102+
103+
jsonparser.ObjectEach(smallFixture, func(key, value []byte, vt jsonparser.ValueType, off int) error {
104+
switch {
105+
case bytes.Equal(key, uuidKey):
106+
data.Uuid, _ = jsonparser.ParseString(value)
107+
missing--
108+
case bytes.Equal(key, tzKey):
109+
v, _ := jsonparser.ParseInt(value)
110+
data.Tz = int(v)
111+
missing--
112+
case bytes.Equal(key, uaKey):
113+
data.Ua, _ = jsonparser.ParseString(value)
114+
missing--
115+
case bytes.Equal(key, stKey):
116+
v, _ := jsonparser.ParseInt(value)
117+
data.St = int(v)
118+
missing--
119+
}
120+
121+
if missing == 0 {
122+
return errStop
123+
} else {
124+
return nil
125+
}
126+
})
127+
128+
nothing(data.Uuid, data.Tz, data.Ua, data.St)
129+
}
130+
}
131+
92132
/*
93133
encoding/json
94134
*/

parser.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,27 @@ const (
346346
Unknown
347347
)
348348

349+
func (vt ValueType) String() string {
350+
switch vt {
351+
case NotExist:
352+
return "non-existent"
353+
case String:
354+
return "string"
355+
case Number:
356+
return "number"
357+
case Object:
358+
return "object"
359+
case Array:
360+
return "array"
361+
case Boolean:
362+
return "boolean"
363+
case Null:
364+
return "null"
365+
default:
366+
return "unknown"
367+
}
368+
}
369+
349370
var (
350371
trueLiteral = []byte("true")
351372
falseLiteral = []byte("false")
@@ -519,6 +540,113 @@ func ArrayEach(data []byte, cb func(value []byte, dataType ValueType, offset int
519540
return nil
520541
}
521542

543+
// ObjectEach iterates over the key-value pairs of a JSON object, invoking a given callback for each such entry
544+
func ObjectEach(data []byte, callback func(key []byte, value []byte, dataType ValueType, offset int) error, keys ...string) (err error) {
545+
var stackbuf [unescapeStackBufSize]byte // stack-allocated array for allocation-free unescaping of small strings
546+
offset := 0
547+
548+
// Descend to the desired key, if requested
549+
if len(keys) > 0 {
550+
if off := searchKeys(data, keys...); off == -1 {
551+
return KeyPathNotFoundError
552+
} else {
553+
offset = off
554+
}
555+
}
556+
557+
// Validate and skip past opening brace
558+
if off := nextToken(data[offset:]); off == -1 {
559+
return MalformedObjectError
560+
} else if offset += off; data[offset] != '{' {
561+
return MalformedObjectError
562+
} else {
563+
offset++
564+
}
565+
566+
// Skip to the first token inside the object, or stop if we find the ending brace
567+
if off := nextToken(data[offset:]); off == -1 {
568+
return MalformedJsonError
569+
} else if offset += off; data[offset] == '}' {
570+
return nil
571+
}
572+
573+
// Loop pre-condition: data[offset] points to what should be either the next entry's key, or the closing brace (if it's anything else, the JSON is malformed)
574+
for offset < len(data) {
575+
// Step 1: find the next key
576+
var key []byte
577+
578+
// Check what the the next token is: start of string, end of object, or something else (error)
579+
switch data[offset] {
580+
case '"':
581+
offset++ // accept as string and skip opening quote
582+
case '}':
583+
return nil // we found the end of the object; stop and return success
584+
default:
585+
return MalformedObjectError
586+
}
587+
588+
// Find the end of the key string
589+
var keyEscaped bool
590+
if off, esc := stringEnd(data[offset:]); off == -1 {
591+
return MalformedJsonError
592+
} else {
593+
key, keyEscaped = data[offset:offset+off-1], esc
594+
offset += off
595+
}
596+
597+
// Unescape the string if needed
598+
if keyEscaped {
599+
if keyUnescaped, err := Unescape(key, stackbuf[:]); err != nil {
600+
return MalformedStringEscapeError
601+
} else {
602+
key = keyUnescaped
603+
}
604+
}
605+
606+
// Step 2: skip the colon
607+
if off := nextToken(data[offset:]); off == -1 {
608+
return MalformedJsonError
609+
} else if offset += off; data[offset] != ':' {
610+
return MalformedJsonError
611+
} else {
612+
offset++
613+
}
614+
615+
// Step 3: find the associated value, then invoke the callback
616+
if value, valueType, off, err := Get(data[offset:]); err != nil {
617+
return err
618+
} else if err := callback(key, value, valueType, offset+off); err != nil { // Invoke the callback here!
619+
return err
620+
} else {
621+
offset += off
622+
}
623+
624+
// Step 4: skip over the next comma to the following token, or stop if we hit the ending brace
625+
if off := nextToken(data[offset:]); off == -1 {
626+
return MalformedArrayError
627+
} else {
628+
offset += off
629+
switch data[offset] {
630+
case '}':
631+
return nil // Stop if we hit the close brace
632+
case ',':
633+
offset++ // Ignore the comma
634+
default:
635+
return MalformedObjectError
636+
}
637+
}
638+
639+
// Skip to the next token after the comma
640+
if off := nextToken(data[offset:]); off == -1 {
641+
return MalformedArrayError
642+
} else {
643+
offset += off
644+
}
645+
}
646+
647+
return MalformedObjectError // we shouldn't get here; it's expected that we will return via finding the ending brace
648+
}
649+
522650
// GetUnsafeString returns the value retrieved by `Get`, use creates string without memory allocation by mapping string to slice memory. It does not handle escape symbols.
523651
func GetUnsafeString(data []byte, keys ...string) (val string, err error) {
524652
v, _, _, e := Get(data, keys...)

0 commit comments

Comments
 (0)