Skip to content

Commit 09116d6

Browse files
committed
Added ObjectEach and tests
1 parent 9b126cd commit 09116d6

2 files changed

Lines changed: 269 additions & 0 deletions

File tree

parser.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,113 @@ func ArrayEach(data []byte, cb func(value []byte, dataType ValueType, offset int
540540
return nil
541541
}
542542

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+
543650
// 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.
544651
func GetUnsafeString(data []byte, keys ...string) (val string, err error) {
545652
v, _, _, e := Get(data, keys...)

parser_test.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package jsonparser
22

33
import (
44
"bytes"
5+
"fmt"
56
_ "fmt"
67
"reflect"
78
"testing"
@@ -650,6 +651,167 @@ func TestArrayEach(t *testing.T) {
650651
}, "a", "b")
651652
}
652653

654+
type keyValueEntry struct {
655+
key string
656+
value string
657+
valueType ValueType
658+
}
659+
660+
func (kv keyValueEntry) String() string {
661+
return fmt.Sprintf("[%s: %s (%s)]", kv.key, kv.value, kv.valueType)
662+
}
663+
664+
type ObjectEachTest struct {
665+
desc string
666+
json string
667+
668+
isErr bool
669+
entries []keyValueEntry
670+
}
671+
672+
var objectEachTests = []ObjectEachTest{
673+
{
674+
desc: "empty object",
675+
json: `{}`,
676+
entries: []keyValueEntry{},
677+
},
678+
{
679+
desc: "single key-value object",
680+
json: `{"key": "value"}`,
681+
entries: []keyValueEntry{
682+
{"key", "value", String},
683+
},
684+
},
685+
{
686+
desc: "multiple key-value object with many value types",
687+
json: `{
688+
"key1": null,
689+
"key2": true,
690+
"key3": 1.23,
691+
"key4": "string value",
692+
"key5": [1,2,3],
693+
"key6": {"a":"b"}
694+
}`,
695+
entries: []keyValueEntry{
696+
{"key1", "", Null},
697+
{"key2", "true", Boolean},
698+
{"key3", "1.23", Number},
699+
{"key4", "string value", String},
700+
{"key5", "[1,2,3]", Array},
701+
{"key6", `{"a":"b"}`, Object},
702+
},
703+
},
704+
{
705+
desc: "escaped key",
706+
json: `{"key\"\\\/\b\f\n\r\t\u00B0": "value"}`,
707+
entries: []keyValueEntry{
708+
{"key\"\\/\b\f\n\r\t\u00B0", "value", String},
709+
},
710+
},
711+
// Error cases
712+
{
713+
desc: "no object present",
714+
json: ` \t\n\r`,
715+
isErr: true,
716+
},
717+
{
718+
desc: "unmatched braces 1",
719+
json: `{`,
720+
isErr: true,
721+
},
722+
{
723+
desc: "unmatched braces 2",
724+
json: `}`,
725+
isErr: true,
726+
},
727+
{
728+
desc: "unmatched braces 3",
729+
json: `}{}`,
730+
isErr: true,
731+
},
732+
{
733+
desc: "bad key (number)",
734+
json: `{123: "value"}`,
735+
isErr: true,
736+
},
737+
{
738+
desc: "bad key (unclosed quote)",
739+
json: `{"key: 123}`,
740+
isErr: true,
741+
},
742+
{
743+
desc: "bad value (no value)",
744+
json: `{"key":}`,
745+
isErr: true,
746+
},
747+
{
748+
desc: "bad value (bogus value)",
749+
json: `{"key": notavalue}`,
750+
isErr: true,
751+
},
752+
{
753+
desc: "bad entry (missing colon)",
754+
json: `{"key" "value"}`,
755+
isErr: true,
756+
},
757+
{
758+
desc: "bad entry (no trailing comma)",
759+
json: `{"key": "value" "key2": "value2"}`,
760+
isErr: true,
761+
},
762+
{
763+
desc: "bad entry (two commas)",
764+
json: `{"key": "value",, "key2": "value2"}`,
765+
isErr: true,
766+
},
767+
}
768+
769+
func TestObjectEach(t *testing.T) {
770+
for _, test := range objectEachTests {
771+
if activeTest != "" && test.desc != activeTest {
772+
continue
773+
}
774+
775+
// Execute ObjectEach and capture all of the entries visited, in order
776+
var entries []keyValueEntry
777+
err := ObjectEach([]byte(test.json), func(key, value []byte, valueType ValueType, off int) error {
778+
entries = append(entries, keyValueEntry{
779+
key: string(key),
780+
value: string(value),
781+
valueType: valueType,
782+
})
783+
return nil
784+
})
785+
786+
// Check the correctness of the result
787+
isErr := (err != nil)
788+
if test.isErr != isErr {
789+
// If the call didn't match the error expectation, fail
790+
t.Errorf("ObjectEach test '%s' isErr mismatch: expected %t, obtained %t (err %v)", test.desc, test.isErr, isErr, err)
791+
} else if isErr {
792+
// Else, if there was an expected error, don't fail and don't check anything further
793+
} else if len(test.entries) != len(entries) {
794+
t.Errorf("ObjectEach test '%s' mismatch in number of key-value entries: expected %d, obtained %d (entries found: %s)", test.desc, len(test.entries), len(entries), entries)
795+
} else {
796+
for i, entry := range entries {
797+
expectedEntry := test.entries[i]
798+
if expectedEntry.key != entry.key {
799+
t.Errorf("ObjectEach test '%s' key mismatch at entry %d: expected %s, obtained %s", test.desc, i, expectedEntry.key, entry.key)
800+
break
801+
} else if expectedEntry.value != entry.value {
802+
t.Errorf("ObjectEach test '%s' value mismatch at entry %d: expected %s, obtained %s", test.desc, i, expectedEntry.value, entry.value)
803+
break
804+
} else if expectedEntry.valueType != entry.valueType {
805+
t.Errorf("ObjectEach test '%s' value type mismatch at entry %d: expected %s, obtained %s", test.desc, i, expectedEntry.valueType, entry.valueType)
806+
break
807+
} else {
808+
// Success for this entry
809+
}
810+
}
811+
}
812+
}
813+
}
814+
653815
var testJson = []byte(`{"name": "Name", "order": "Order", "sum": 100, "len": 12, "isPaid": true, "nested": {"a":"test", "b":2, "nested3":{"a":"test3","b":4}, "c": "unknown"}, "nested2": {"a":"test2", "b":3}, "arr": [{"a":"zxc", "b": 1}, {"a":"123", "b":2}], "arrInt": [1,2,3,4], "intPtr": 10}`)
654816

655817
func TestEachKey(t *testing.T) {

0 commit comments

Comments
 (0)