Skip to content

Commit 0c38945

Browse files
committed
Use a concrete type for JSON pointer
WARNING: This commit includes breaking changes. Declare a jsontext.Pointer type as a named string type. This allows us to implement a Tokens method to conveniently iterate over all the reference tokens in the pointer using the upcoming iterators support. The new functionally is currently not tested by CI, but can be manually tested with: GOEXPERIMENT=rangefunc go.tip test ./...
1 parent 2f02d56 commit 0c38945

10 files changed

+112
-28
lines changed

errors.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type SemanticError struct {
2828
ByteOffset int64
2929
// JSONPointer indicates that an error occurred within this JSON value
3030
// as indicated using the JSON Pointer notation (see RFC 6901).
31-
JSONPointer string
31+
JSONPointer jsontext.Pointer
3232

3333
// JSONKind is the JSON kind that could not be handled.
3434
JSONKind jsontext.Kind // may be zero if unknown
@@ -98,7 +98,7 @@ func (e *SemanticError) Error() string {
9898
switch {
9999
case e.JSONPointer != "":
100100
sb.WriteString(" within JSON value at ")
101-
sb.WriteString(strconv.Quote(e.JSONPointer))
101+
sb.WriteString(strconv.Quote(string(e.JSONPointer)))
102102
case e.ByteOffset > 0:
103103
sb.WriteString(" after byte offset ")
104104
sb.WriteString(strconv.FormatInt(e.ByteOffset, 10))

jsontext/coder_test.go

+17-17
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,15 @@ type coderTestdataEntry struct {
3838
outIndented string // outCompacted if empty; uses " " for indent prefix and "\t" for indent
3939
outCanonicalized string // outCompacted if empty
4040
tokens []Token
41-
pointers []string
41+
pointers []Pointer
4242
}
4343

4444
var coderTestdata = []coderTestdataEntry{{
4545
name: jsontest.Name("Null"),
4646
in: ` null `,
4747
outCompacted: `null`,
4848
tokens: []Token{Null},
49-
pointers: []string{""},
49+
pointers: []Pointer{""},
5050
}, {
5151
name: jsontest.Name("False"),
5252
in: ` false `,
@@ -157,15 +157,15 @@ var coderTestdata = []coderTestdataEntry{{
157157
Int(minInt64), Int(maxInt64), Uint(minUint64), Uint(maxUint64),
158158
ArrayEnd,
159159
},
160-
pointers: []string{
160+
pointers: []Pointer{
161161
"", "/0", "/1", "/2", "/3", "/4", "/5", "/6", "/7", "/8", "/9", "/10", "/11", "/12", "/13", "/14", "/15", "/16", "/17", "",
162162
},
163163
}, {
164164
name: jsontest.Name("ObjectN0"),
165165
in: ` { } `,
166166
outCompacted: `{}`,
167167
tokens: []Token{ObjectStart, ObjectEnd},
168-
pointers: []string{"", ""},
168+
pointers: []Pointer{"", ""},
169169
}, {
170170
name: jsontest.Name("ObjectN1"),
171171
in: ` { "0" : 0 } `,
@@ -175,7 +175,7 @@ var coderTestdata = []coderTestdataEntry{{
175175
"0": 0
176176
}`,
177177
tokens: []Token{ObjectStart, String("0"), Uint(0), ObjectEnd},
178-
pointers: []string{"", "/0", "/0", ""},
178+
pointers: []Pointer{"", "/0", "/0", ""},
179179
}, {
180180
name: jsontest.Name("ObjectN2"),
181181
in: ` { "0" : 0 , "1" : 1 } `,
@@ -186,7 +186,7 @@ var coderTestdata = []coderTestdataEntry{{
186186
"1": 1
187187
}`,
188188
tokens: []Token{ObjectStart, String("0"), Uint(0), String("1"), Uint(1), ObjectEnd},
189-
pointers: []string{"", "/0", "/0", "/1", "/1", ""},
189+
pointers: []Pointer{"", "/0", "/0", "/1", "/1", ""},
190190
}, {
191191
name: jsontest.Name("ObjectNested"),
192192
in: ` { "0" : { "1" : { "2" : { "3" : { "4" : { } } } } } } `,
@@ -204,7 +204,7 @@ var coderTestdata = []coderTestdataEntry{{
204204
}
205205
}`,
206206
tokens: []Token{ObjectStart, String("0"), ObjectStart, String("1"), ObjectStart, String("2"), ObjectStart, String("3"), ObjectStart, String("4"), ObjectStart, ObjectEnd, ObjectEnd, ObjectEnd, ObjectEnd, ObjectEnd, ObjectEnd},
207-
pointers: []string{
207+
pointers: []Pointer{
208208
"",
209209
"/0", "/0",
210210
"/0/1", "/0/1",
@@ -268,7 +268,7 @@ var coderTestdata = []coderTestdataEntry{{
268268
ObjectEnd,
269269
ObjectEnd,
270270
},
271-
pointers: []string{
271+
pointers: []Pointer{
272272
"",
273273
"/", "/",
274274
"//44444", "//44444",
@@ -289,7 +289,7 @@ var coderTestdata = []coderTestdataEntry{{
289289
in: ` [ ] `,
290290
outCompacted: `[]`,
291291
tokens: []Token{ArrayStart, ArrayEnd},
292-
pointers: []string{"", ""},
292+
pointers: []Pointer{"", ""},
293293
}, {
294294
name: jsontest.Name("ArrayN1"),
295295
in: ` [ 0 ] `,
@@ -298,7 +298,7 @@ var coderTestdata = []coderTestdataEntry{{
298298
0
299299
]`,
300300
tokens: []Token{ArrayStart, Uint(0), ArrayEnd},
301-
pointers: []string{"", "/0", ""},
301+
pointers: []Pointer{"", "/0", ""},
302302
}, {
303303
name: jsontest.Name("ArrayN2"),
304304
in: ` [ 0 , 1 ] `,
@@ -322,7 +322,7 @@ var coderTestdata = []coderTestdataEntry{{
322322
]
323323
]`,
324324
tokens: []Token{ArrayStart, ArrayStart, ArrayStart, ArrayStart, ArrayStart, ArrayEnd, ArrayEnd, ArrayEnd, ArrayEnd, ArrayEnd},
325-
pointers: []string{
325+
pointers: []Pointer{
326326
"",
327327
"/0",
328328
"/0/0",
@@ -388,7 +388,7 @@ var coderTestdata = []coderTestdataEntry{{
388388
String("objectN2"), ObjectStart, String("0"), Uint(0), String("1"), Uint(1), ObjectEnd,
389389
ObjectEnd,
390390
},
391-
pointers: []string{
391+
pointers: []Pointer{
392392
"",
393393
"/literals", "/literals",
394394
"/literals/0",
@@ -494,8 +494,8 @@ func testCoderInterleaved(t *testing.T, where jsontest.CasePos, modeName string,
494494
func TestCoderStackPointer(t *testing.T) {
495495
tests := []struct {
496496
token Token
497-
wantWithRejectDuplicateNames string
498-
wantWithAllowDuplicateNames string
497+
wantWithRejectDuplicateNames Pointer
498+
wantWithAllowDuplicateNames Pointer
499499
}{
500500
{Null, "", ""},
501501

@@ -549,14 +549,14 @@ func TestCoderStackPointer(t *testing.T) {
549549

550550
for _, allowDupes := range []bool{false, true} {
551551
var name string
552-
var want func(i int) string
552+
var want func(i int) Pointer
553553
switch allowDupes {
554554
case false:
555555
name = "RejectDuplicateNames"
556-
want = func(i int) string { return tests[i].wantWithRejectDuplicateNames }
556+
want = func(i int) Pointer { return tests[i].wantWithRejectDuplicateNames }
557557
case true:
558558
name = "AllowDuplicateNames"
559-
want = func(i int) string { return tests[i].wantWithAllowDuplicateNames }
559+
want = func(i int) Pointer { return tests[i].wantWithAllowDuplicateNames }
560560
}
561561

562562
t.Run(name, func(t *testing.T) {

jsontext/decode.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1052,7 +1052,7 @@ func (d *Decoder) StackIndex(i int) (Kind, int64) {
10521052
// StackPointer returns a JSON Pointer (RFC 6901) to the most recently read value.
10531053
// Object names are only present if [AllowDuplicateNames] is false, otherwise
10541054
// object members are represented using their index within the object.
1055-
func (d *Decoder) StackPointer() string {
1055+
func (d *Decoder) StackPointer() Pointer {
10561056
d.s.Names.copyQuotedBuffer(d.s.buf)
1057-
return string(d.s.appendStackPointer(nil))
1057+
return Pointer(d.s.appendStackPointer(nil))
10581058
}

jsontext/decode_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func testDecoder(t *testing.T, where jsontest.CasePos, typeName string, td coder
4848
switch typeName {
4949
case "Token":
5050
var tokens []Token
51-
var pointers []string
51+
var pointers []Pointer
5252
for {
5353
tok, err := dec.ReadToken()
5454
if err != nil {
@@ -176,7 +176,7 @@ type decoderMethodCall struct {
176176
wantKind Kind
177177
wantOut tokOrVal
178178
wantErr error
179-
wantPointer string
179+
wantPointer Pointer
180180
}
181181

182182
var decoderErrorTestdata = []struct {

jsontext/encode.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,7 @@ func (e *Encoder) StackIndex(i int) (Kind, int64) {
917917
// StackPointer returns a JSON Pointer (RFC 6901) to the most recently written value.
918918
// Object names are only present if [AllowDuplicateNames] is false, otherwise
919919
// object members are represented using their index within the object.
920-
func (e *Encoder) StackPointer() string {
920+
func (e *Encoder) StackPointer() Pointer {
921921
e.s.Names.copyQuotedBuffer(e.s.Buf)
922-
return string(e.s.appendStackPointer(nil))
922+
return Pointer(e.s.appendStackPointer(nil))
923923
}

jsontext/encode_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func testEncoder(t *testing.T, where jsontest.CasePos, formatName, typeName stri
4848

4949
switch typeName {
5050
case "Token":
51-
var pointers []string
51+
var pointers []Pointer
5252
for _, tok := range td.tokens {
5353
if err := enc.WriteToken(tok); err != nil {
5454
t.Fatalf("%s: Encoder.WriteToken error: %v", where, err)
@@ -136,7 +136,7 @@ func testFaultyEncoder(t *testing.T, where jsontest.CasePos, typeName string, td
136136
type encoderMethodCall struct {
137137
in tokOrVal
138138
wantErr error
139-
wantPointer string
139+
wantPointer Pointer
140140
}
141141

142142
var encoderErrorTestdata = []struct {

jsontext/example_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func Example_stringReplace() {
3434
// Using a Decoder and Encoder, we can parse through every token,
3535
// check and modify the token if necessary, and
3636
// write the token to the output.
37-
var replacements []string
37+
var replacements []jsontext.Pointer
3838
in := strings.NewReader(input)
3939
dec := jsontext.NewDecoder(in)
4040
out := new(bytes.Buffer)

jsontext/pointer.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build goexperiment.rangefunc
6+
7+
package jsontext
8+
9+
import "iter"
10+
11+
// Tokens returns an iterator over the reference tokens in the JSON pointer,
12+
// starting from the first token until the last token (unless stopped early).
13+
// A token is either a JSON object name or an index to a JSON array element
14+
// encoded as a base-10 integer value.
15+
func (p Pointer) Tokens() iter.Seq[string] {
16+
return func(yield func(string) bool) {
17+
for len(p) > 0 {
18+
if !yield(p.nextToken()) {
19+
return
20+
}
21+
}
22+
}
23+
}

jsontext/pointer_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build goexperiment.rangefunc
6+
7+
package jsontext
8+
9+
import (
10+
"iter"
11+
"slices"
12+
"testing"
13+
)
14+
15+
func TestPointerTokens(t *testing.T) {
16+
// TODO(https://go.dev/issue/61899): Use slices.Collect.
17+
collect := func(seq iter.Seq[string]) (x []string) {
18+
for v := range seq {
19+
x = append(x, v)
20+
}
21+
return x
22+
}
23+
24+
tests := []struct {
25+
in Pointer
26+
want []string
27+
}{
28+
{in: "", want: nil},
29+
{in: "a", want: []string{"a"}},
30+
{in: "~", want: []string{"~"}},
31+
{in: "/a", want: []string{"a"}},
32+
{in: "/foo/bar", want: []string{"foo", "bar"}},
33+
{in: "///", want: []string{"", "", ""}},
34+
{in: "/~0~1", want: []string{"~/"}},
35+
}
36+
for _, tt := range tests {
37+
got := collect(tt.in.Tokens())
38+
if !slices.Equal(got, tt.want) {
39+
t.Errorf("Pointer(%q).Tokens = %q, want %q", tt.in, got, tt.want)
40+
}
41+
}
42+
}

jsontext/state.go

+19
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package jsontext
77
import (
88
"math"
99
"strconv"
10+
"strings"
1011

1112
"github.com/go-json-experiment/json/internal/jsonwire"
1213
)
@@ -48,6 +49,24 @@ func (s *state) reset() {
4849
s.Namespaces.reset()
4950
}
5051

52+
// Pointer is a JSON Pointer (RFC 6901) that references a particular JSON value
53+
// relative to the root of the top-level JSON value.
54+
type Pointer string
55+
56+
// nextToken returns the next token in the pointer, reducing the length of p.
57+
func (p *Pointer) nextToken() (token string) {
58+
*p = Pointer(strings.TrimPrefix(string(*p), "/"))
59+
i := min(uint(strings.IndexByte(string(*p), '/')), uint(len(*p)))
60+
token = string(*p)[:i]
61+
*p = (*p)[i:]
62+
if strings.Contains(token, "~") {
63+
// Per RFC 6901, section 3, unescape '~' and '/' characters.
64+
token = strings.ReplaceAll(token, "~1", "/")
65+
token = strings.ReplaceAll(token, "~0", "~")
66+
}
67+
return token
68+
}
69+
5170
// appendStackPointer appends a JSON Pointer (RFC 6901) to the current value.
5271
// The returned pointer is only accurate if s.names is populated,
5372
// otherwise it uses the numeric index as the object member name.

0 commit comments

Comments
 (0)