Skip to content

Use a concrete type for JSON pointer #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
34 changes: 17 additions & 17 deletions jsontext/coder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ 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{{
name: jsontest.Name("Null"),
in: ` null `,
outCompacted: `null`,
tokens: []Token{Null},
pointers: []string{""},
pointers: []Pointer{""},
}, {
name: jsontest.Name("False"),
in: ` false `,
Expand Down Expand Up @@ -157,15 +157,15 @@ 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", "",
},
}, {
name: jsontest.Name("ObjectN0"),
in: ` { } `,
outCompacted: `{}`,
tokens: []Token{ObjectStart, ObjectEnd},
pointers: []string{"", ""},
pointers: []Pointer{"", ""},
}, {
name: jsontest.Name("ObjectN1"),
in: ` { "0" : 0 } `,
Expand All @@ -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 } `,
Expand All @@ -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" : { } } } } } } `,
Expand All @@ -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",
Expand Down Expand Up @@ -268,7 +268,7 @@ var coderTestdata = []coderTestdataEntry{{
ObjectEnd,
ObjectEnd,
},
pointers: []string{
pointers: []Pointer{
"",
"/", "/",
"//44444", "//44444",
Expand All @@ -289,7 +289,7 @@ var coderTestdata = []coderTestdataEntry{{
in: ` [ ] `,
outCompacted: `[]`,
tokens: []Token{ArrayStart, ArrayEnd},
pointers: []string{"", ""},
pointers: []Pointer{"", ""},
}, {
name: jsontest.Name("ArrayN1"),
in: ` [ 0 ] `,
Expand All @@ -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 ] `,
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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, "", ""},

Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions jsontext/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
4 changes: 2 additions & 2 deletions jsontext/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -176,7 +176,7 @@ type decoderMethodCall struct {
wantKind Kind
wantOut tokOrVal
wantErr error
wantPointer string
wantPointer Pointer
}

var decoderErrorTestdata = []struct {
Expand Down
4 changes: 2 additions & 2 deletions jsontext/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
4 changes: 2 additions & 2 deletions jsontext/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion jsontext/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions jsontext/pointer.go
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at this point I would personally use //go:build go1.23 and test with Go master or the first pre-release once it's out, which should be soon. Ideally we would use goexperiment.rangefunc || 1.23, but that's broken because of golang/go#66399. With your build tag as is, any 1.23 users can't see this API, which isn't right.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At present, the iter package has the goexperiment.rangefunc tag, so go1.23 isn't enough. It's not clear to me whether the intent is to have iterators be directly supported in Go 1.23. For now I'll submit it with the current tag and we can adjust it accordingly based on what happens upstream.


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
}
}
}
}
42 changes: 42 additions & 0 deletions jsontext/pointer_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
19 changes: 19 additions & 0 deletions jsontext/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package jsontext
import (
"math"
"strconv"
"strings"

"github.com/go-json-experiment/json/internal/jsonwire"
)
Expand Down Expand Up @@ -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.
Expand Down
Loading