Skip to content

Commit

Permalink
Use a concrete type for JSON pointer (#30)
Browse files Browse the repository at this point in the history
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 ./...
  • Loading branch information
dsnet committed Apr 18, 2024
1 parent adac9ef commit af2d506
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 28 deletions.
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

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

0 comments on commit af2d506

Please sign in to comment.