e61da2dcc2878885092923614f4bddc9c421878f — Martin Angers 6 months ago 7e6746b
scan nested objects and arrays
3 files changed, 92 insertions(+), 21 deletions(-)

M zerojson.go
M zerojson_bench_test.go
M zerojson_test.go
M zerojson.go => zerojson.go +85 -21
@@ 144,6 144,8 @@
 	// The type may be:
 	// '{' : start of an object (v is '{')
 	// '[' : start of an array (v is '[')
+	// '}' : end of an object (v is '}')
+	// ']' : end of an array (v is ']')
 	// '"' : a string (v is the full string, with quotes)
 	// '1' : a number (v is the full number)
 	// 't' : true (v is the full literal)


@@ 158,6 160,7 @@
 
 	// parser state
 	cur   byte
+	allow byte // '\x00'=any value, '<'=key, '>'=value, ':'=colon, ','=comma or ']' or '}'
 	pos   int
 	stack stack
 }


@@ 180,30 183,91 @@
 		typ = p.cur
 		p.advance()
 
-		switch typ {
-		case '{':
-			p.stack.push(typ)
-		case '[':
-			p.stack.push(typ)
-		case '"':
-			err = p.scanString()
-		case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
-			first := typ
-			typ = '1'
-			err = p.scanNumber(first)
-		case 't':
-			err = p.scanToken(trueTrail)
-		case 'f':
-			err = p.scanToken(falseTrail)
-		case 'n':
-			err = p.scanToken(nullTrail)
+		switch p.allow {
+		case ':':
+			if typ == ':' {
+				p.allow = '>'
+				continue
+			}
+			err = ErrInvalidCodePoint
+
+		case ',':
+			closing := p.stack.peek() + 2 // turn '[' or '{' to ']' or '}'
+			if typ == ',' {
+				// expect another value in array or anothery key in object
+				if closing == '}' {
+					p.allow = '<'
+				} else {
+					p.allow = '>'
+				}
+				continue
+			}
+
+			if typ == closing {
+				p.stack.pop()
+				peek := p.stack.peek()
+				switch peek {
+				case '{', '[':
+					p.allow = ','
+				default:
+					p.allow = 0
+				}
+			} else {
+				err = ErrInvalidCodePoint
+			}
+
+		case '<':
+			// key expected, must be a string or a closing object
+			switch typ {
+			case '"':
+				err = p.scanString()
+				p.allow = ':'
+			case '}':
+				p.stack.pop()
+				peek := p.stack.peek()
+				switch peek {
+				case '{', '[':
+					p.allow = ','
+				default:
+					p.allow = 0
+				}
+
+			default:
+				err = ErrInvalidCodePoint
+			}
 
 		default:
-			if p.eof() {
-				// TODO: if a token was required, error
-				break loop
+			if p.allow == '>' {
+				p.allow = ','
+			}
+
+			switch typ {
+			case '{':
+				p.stack.push(typ)
+				p.allow = '<'
+			case '[':
+				p.stack.push(typ)
+				p.allow = '>'
+			case '"':
+				err = p.scanString()
+			case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
+				first := typ
+				typ = '1'
+				err = p.scanNumber(first)
+			case 't':
+				err = p.scanToken(trueTrail)
+			case 'f':
+				err = p.scanToken(falseTrail)
+			case 'n':
+				err = p.scanToken(nullTrail)
+
+			default:
+				if p.eof() {
+					// TODO: if a token was required, error
+					break loop
+				}
+				err = ErrInvalidCodePoint
 			}
-			err = ErrInvalidCodePoint
 		}
 
 		if e := p.emit(start, typ, p.input[start:p.pos], err); e != nil {

M zerojson_bench_test.go => zerojson_bench_test.go +4 -0
@@ 26,6 26,10 @@
 	benchmarkInput(b, `123.456e+7890`)
 }
 
+func BenchmarkSmallObject(b *testing.B) {
+	benchmarkInput(b, `{"key": true}`)
+}
+
 func benchmarkInput(b *testing.B, input string) {
 	p := parser{
 		input: []byte(fmt.Sprintf(" %s ", input)),

M zerojson_test.go => zerojson_test.go +3 -0
@@ 95,6 95,9 @@
 		{"-1E+3", "0: 1: -1E+3", nil},
 		{"-1e-3", "0: 1: -1e-3", nil},
 		{"-1E-3", "0: 1: -1E-3", nil},
+
+		{"{}", "0: {: {\n1: }: }", nil},
+		{`{"a" : 1}`, "0: {: {\n1: \": \"a\"\n7: 1: 1\n8: }: }", nil},
 	}
 
 	for _, c := range cases {