diff --git a/errors.go b/errors.go index 8a67bbd..0b42b41 100644 --- a/errors.go +++ b/errors.go @@ -28,7 +28,7 @@ type SemanticError struct { ByteOffset int64 // JSONPointer indicates that an error occurred within this JSON value // as indicated using the JSON Pointer notation (see RFC 6901). - JSONPointer string + JSONPointer jsontext.Pointer // JSONKind is the JSON kind that could not be handled. JSONKind jsontext.Kind // may be zero if unknown @@ -98,7 +98,7 @@ func (e *SemanticError) Error() string { switch { case e.JSONPointer != "": sb.WriteString(" within JSON value at ") - sb.WriteString(strconv.Quote(e.JSONPointer)) + sb.WriteString(strconv.Quote(string(e.JSONPointer))) case e.ByteOffset > 0: sb.WriteString(" after byte offset ") sb.WriteString(strconv.FormatInt(e.ByteOffset, 10)) diff --git a/jsontext/coder_test.go b/jsontext/coder_test.go index 65f2ff9..ecfe1a5 100644 --- a/jsontext/coder_test.go +++ b/jsontext/coder_test.go @@ -38,7 +38,7 @@ type coderTestdataEntry struct { outIndented string // outCompacted if empty; uses " " for indent prefix and "\t" for indent outCanonicalized string // outCompacted if empty tokens []Token - pointers []string + pointers []Pointer } var coderTestdata = []coderTestdataEntry{{ @@ -46,7 +46,7 @@ var coderTestdata = []coderTestdataEntry{{ in: ` null `, outCompacted: `null`, tokens: []Token{Null}, - pointers: []string{""}, + pointers: []Pointer{""}, }, { name: jsontest.Name("False"), in: ` false `, @@ -157,7 +157,7 @@ var coderTestdata = []coderTestdataEntry{{ Int(minInt64), Int(maxInt64), Uint(minUint64), Uint(maxUint64), ArrayEnd, }, - pointers: []string{ + pointers: []Pointer{ "", "/0", "/1", "/2", "/3", "/4", "/5", "/6", "/7", "/8", "/9", "/10", "/11", "/12", "/13", "/14", "/15", "/16", "/17", "", }, }, { @@ -165,7 +165,7 @@ var coderTestdata = []coderTestdataEntry{{ in: ` { } `, outCompacted: `{}`, tokens: []Token{ObjectStart, ObjectEnd}, - pointers: []string{"", ""}, + pointers: []Pointer{"", ""}, }, { name: jsontest.Name("ObjectN1"), in: ` { "0" : 0 } `, @@ -175,7 +175,7 @@ var coderTestdata = []coderTestdataEntry{{ "0": 0 }`, tokens: []Token{ObjectStart, String("0"), Uint(0), ObjectEnd}, - pointers: []string{"", "/0", "/0", ""}, + pointers: []Pointer{"", "/0", "/0", ""}, }, { name: jsontest.Name("ObjectN2"), in: ` { "0" : 0 , "1" : 1 } `, @@ -186,7 +186,7 @@ var coderTestdata = []coderTestdataEntry{{ "1": 1 }`, tokens: []Token{ObjectStart, String("0"), Uint(0), String("1"), Uint(1), ObjectEnd}, - pointers: []string{"", "/0", "/0", "/1", "/1", ""}, + pointers: []Pointer{"", "/0", "/0", "/1", "/1", ""}, }, { name: jsontest.Name("ObjectNested"), in: ` { "0" : { "1" : { "2" : { "3" : { "4" : { } } } } } } `, @@ -204,7 +204,7 @@ var coderTestdata = []coderTestdataEntry{{ } }`, tokens: []Token{ObjectStart, String("0"), ObjectStart, String("1"), ObjectStart, String("2"), ObjectStart, String("3"), ObjectStart, String("4"), ObjectStart, ObjectEnd, ObjectEnd, ObjectEnd, ObjectEnd, ObjectEnd, ObjectEnd}, - pointers: []string{ + pointers: []Pointer{ "", "/0", "/0", "/0/1", "/0/1", @@ -268,7 +268,7 @@ var coderTestdata = []coderTestdataEntry{{ ObjectEnd, ObjectEnd, }, - pointers: []string{ + pointers: []Pointer{ "", "/", "/", "//44444", "//44444", @@ -289,7 +289,7 @@ var coderTestdata = []coderTestdataEntry{{ in: ` [ ] `, outCompacted: `[]`, tokens: []Token{ArrayStart, ArrayEnd}, - pointers: []string{"", ""}, + pointers: []Pointer{"", ""}, }, { name: jsontest.Name("ArrayN1"), in: ` [ 0 ] `, @@ -298,7 +298,7 @@ var coderTestdata = []coderTestdataEntry{{ 0 ]`, tokens: []Token{ArrayStart, Uint(0), ArrayEnd}, - pointers: []string{"", "/0", ""}, + pointers: []Pointer{"", "/0", ""}, }, { name: jsontest.Name("ArrayN2"), in: ` [ 0 , 1 ] `, @@ -322,7 +322,7 @@ var coderTestdata = []coderTestdataEntry{{ ] ]`, tokens: []Token{ArrayStart, ArrayStart, ArrayStart, ArrayStart, ArrayStart, ArrayEnd, ArrayEnd, ArrayEnd, ArrayEnd, ArrayEnd}, - pointers: []string{ + pointers: []Pointer{ "", "/0", "/0/0", @@ -388,7 +388,7 @@ var coderTestdata = []coderTestdataEntry{{ String("objectN2"), ObjectStart, String("0"), Uint(0), String("1"), Uint(1), ObjectEnd, ObjectEnd, }, - pointers: []string{ + pointers: []Pointer{ "", "/literals", "/literals", "/literals/0", @@ -494,8 +494,8 @@ func testCoderInterleaved(t *testing.T, where jsontest.CasePos, modeName string, func TestCoderStackPointer(t *testing.T) { tests := []struct { token Token - wantWithRejectDuplicateNames string - wantWithAllowDuplicateNames string + wantWithRejectDuplicateNames Pointer + wantWithAllowDuplicateNames Pointer }{ {Null, "", ""}, @@ -549,14 +549,14 @@ func TestCoderStackPointer(t *testing.T) { for _, allowDupes := range []bool{false, true} { var name string - var want func(i int) string + var want func(i int) Pointer switch allowDupes { case false: name = "RejectDuplicateNames" - want = func(i int) string { return tests[i].wantWithRejectDuplicateNames } + want = func(i int) Pointer { return tests[i].wantWithRejectDuplicateNames } case true: name = "AllowDuplicateNames" - want = func(i int) string { return tests[i].wantWithAllowDuplicateNames } + want = func(i int) Pointer { return tests[i].wantWithAllowDuplicateNames } } t.Run(name, func(t *testing.T) { diff --git a/jsontext/decode.go b/jsontext/decode.go index d1ef6ae..e12a37b 100644 --- a/jsontext/decode.go +++ b/jsontext/decode.go @@ -1052,7 +1052,7 @@ func (d *Decoder) StackIndex(i int) (Kind, int64) { // StackPointer returns a JSON Pointer (RFC 6901) to the most recently read value. // Object names are only present if [AllowDuplicateNames] is false, otherwise // object members are represented using their index within the object. -func (d *Decoder) StackPointer() string { +func (d *Decoder) StackPointer() Pointer { d.s.Names.copyQuotedBuffer(d.s.buf) - return string(d.s.appendStackPointer(nil)) + return Pointer(d.s.appendStackPointer(nil)) } diff --git a/jsontext/decode_test.go b/jsontext/decode_test.go index fc156ed..11b25b4 100644 --- a/jsontext/decode_test.go +++ b/jsontext/decode_test.go @@ -48,7 +48,7 @@ func testDecoder(t *testing.T, where jsontest.CasePos, typeName string, td coder switch typeName { case "Token": var tokens []Token - var pointers []string + var pointers []Pointer for { tok, err := dec.ReadToken() if err != nil { @@ -176,7 +176,7 @@ type decoderMethodCall struct { wantKind Kind wantOut tokOrVal wantErr error - wantPointer string + wantPointer Pointer } var decoderErrorTestdata = []struct { diff --git a/jsontext/encode.go b/jsontext/encode.go index e5f3965..1035aa9 100644 --- a/jsontext/encode.go +++ b/jsontext/encode.go @@ -917,7 +917,7 @@ func (e *Encoder) StackIndex(i int) (Kind, int64) { // StackPointer returns a JSON Pointer (RFC 6901) to the most recently written value. // Object names are only present if [AllowDuplicateNames] is false, otherwise // object members are represented using their index within the object. -func (e *Encoder) StackPointer() string { +func (e *Encoder) StackPointer() Pointer { e.s.Names.copyQuotedBuffer(e.s.Buf) - return string(e.s.appendStackPointer(nil)) + return Pointer(e.s.appendStackPointer(nil)) } diff --git a/jsontext/encode_test.go b/jsontext/encode_test.go index 376176a..b7a53c7 100644 --- a/jsontext/encode_test.go +++ b/jsontext/encode_test.go @@ -48,7 +48,7 @@ func testEncoder(t *testing.T, where jsontest.CasePos, formatName, typeName stri switch typeName { case "Token": - var pointers []string + var pointers []Pointer for _, tok := range td.tokens { if err := enc.WriteToken(tok); err != nil { t.Fatalf("%s: Encoder.WriteToken error: %v", where, err) @@ -136,7 +136,7 @@ func testFaultyEncoder(t *testing.T, where jsontest.CasePos, typeName string, td type encoderMethodCall struct { in tokOrVal wantErr error - wantPointer string + wantPointer Pointer } var encoderErrorTestdata = []struct { diff --git a/jsontext/example_test.go b/jsontext/example_test.go index c694b4c..3ab3e2d 100644 --- a/jsontext/example_test.go +++ b/jsontext/example_test.go @@ -34,7 +34,7 @@ func Example_stringReplace() { // Using a Decoder and Encoder, we can parse through every token, // check and modify the token if necessary, and // write the token to the output. - var replacements []string + var replacements []jsontext.Pointer in := strings.NewReader(input) dec := jsontext.NewDecoder(in) out := new(bytes.Buffer) diff --git a/jsontext/pointer.go b/jsontext/pointer.go new file mode 100644 index 0000000..90f566f --- /dev/null +++ b/jsontext/pointer.go @@ -0,0 +1,23 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build goexperiment.rangefunc + +package jsontext + +import "iter" + +// Tokens returns an iterator over the reference tokens in the JSON pointer, +// starting from the first token until the last token (unless stopped early). +// A token is either a JSON object name or an index to a JSON array element +// encoded as a base-10 integer value. +func (p Pointer) Tokens() iter.Seq[string] { + return func(yield func(string) bool) { + for len(p) > 0 { + if !yield(p.nextToken()) { + return + } + } + } +} diff --git a/jsontext/pointer_test.go b/jsontext/pointer_test.go new file mode 100644 index 0000000..bd94370 --- /dev/null +++ b/jsontext/pointer_test.go @@ -0,0 +1,42 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build goexperiment.rangefunc + +package jsontext + +import ( + "iter" + "slices" + "testing" +) + +func TestPointerTokens(t *testing.T) { + // TODO(https://go.dev/issue/61899): Use slices.Collect. + collect := func(seq iter.Seq[string]) (x []string) { + for v := range seq { + x = append(x, v) + } + return x + } + + tests := []struct { + in Pointer + want []string + }{ + {in: "", want: nil}, + {in: "a", want: []string{"a"}}, + {in: "~", want: []string{"~"}}, + {in: "/a", want: []string{"a"}}, + {in: "/foo/bar", want: []string{"foo", "bar"}}, + {in: "///", want: []string{"", "", ""}}, + {in: "/~0~1", want: []string{"~/"}}, + } + for _, tt := range tests { + got := collect(tt.in.Tokens()) + if !slices.Equal(got, tt.want) { + t.Errorf("Pointer(%q).Tokens = %q, want %q", tt.in, got, tt.want) + } + } +} diff --git a/jsontext/state.go b/jsontext/state.go index 5c1a55d..1a15937 100644 --- a/jsontext/state.go +++ b/jsontext/state.go @@ -7,6 +7,7 @@ package jsontext import ( "math" "strconv" + "strings" "github.com/go-json-experiment/json/internal/jsonwire" ) @@ -48,6 +49,24 @@ func (s *state) reset() { s.Namespaces.reset() } +// Pointer is a JSON Pointer (RFC 6901) that references a particular JSON value +// relative to the root of the top-level JSON value. +type Pointer string + +// nextToken returns the next token in the pointer, reducing the length of p. +func (p *Pointer) nextToken() (token string) { + *p = Pointer(strings.TrimPrefix(string(*p), "/")) + i := min(uint(strings.IndexByte(string(*p), '/')), uint(len(*p))) + token = string(*p)[:i] + *p = (*p)[i:] + if strings.Contains(token, "~") { + // Per RFC 6901, section 3, unescape '~' and '/' characters. + token = strings.ReplaceAll(token, "~1", "/") + token = strings.ReplaceAll(token, "~0", "~") + } + return token +} + // appendStackPointer appends a JSON Pointer (RFC 6901) to the current value. // The returned pointer is only accurate if s.names is populated, // otherwise it uses the numeric index as the object member name.