From ebd3a8989ca1eadb7a68e02a93448ecbbab5900c Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 15 Aug 2024 10:50:50 -0700 Subject: [PATCH] Use iter.Seq, added in Go 1.23 (#49) First-class support for custom iterators was added in Go 1.23. Use that as a mechanism for iterating over Pointer tokens. Also, update the workflows to test on Go 1.23 and bump the go.mod language version to 1.23. --- .github/workflows/test.yml | 4 ++-- go.mod | 2 +- jsontext/pointer.go | 23 --------------------- jsontext/pointer_test.go | 42 -------------------------------------- jsontext/state.go | 42 +++++++++++++++++++++++++++----------- jsontext/state_test.go | 22 ++++++++++++++++++++ 6 files changed, 55 insertions(+), 80 deletions(-) delete mode 100644 jsontext/pointer.go delete mode 100644 jsontext/pointer_test.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 646d4d2..b72b012 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: - name: Install Go uses: actions/setup-go@v5 with: - go-version: 1.22.x + go-version: 1.23.x - name: Checkout code uses: actions/checkout@v4 - name: Test @@ -23,7 +23,7 @@ jobs: test-all: strategy: matrix: - go-version: [1.22.x] + go-version: [1.23.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/go.mod b/go.mod index 356025d..7add274 100644 --- a/go.mod +++ b/go.mod @@ -3,4 +3,4 @@ // This package will regularly experience breaking changes. module github.com/go-json-experiment/json -go 1.22 +go 1.23 diff --git a/jsontext/pointer.go b/jsontext/pointer.go deleted file mode 100644 index 90f566f..0000000 --- a/jsontext/pointer.go +++ /dev/null @@ -1,23 +0,0 @@ -// 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 deleted file mode 100644 index bd94370..0000000 --- a/jsontext/pointer_test.go +++ /dev/null @@ -1,42 +0,0 @@ -// 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 2e361e3..aa1b7c9 100644 --- a/jsontext/state.go +++ b/jsontext/state.go @@ -5,6 +5,7 @@ package jsontext import ( + "iter" "math" "strconv" "strings" @@ -51,20 +52,37 @@ func (s *state) reset() { // Pointer is a JSON Pointer (RFC 6901) that references a particular JSON value // relative to the root of the top-level JSON value. +// +// There is exactly one representation of a pointer to a particular value, +// so comparability of Pointer values is equivalent to checking whether +// they both point to the exact same 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 +// 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. +// It is impossible to distinguish between an array index and an object name +// (that happens to be an base-10 encoded integer) without also knowing +// the structure of the top-level JSON value that the pointer refers to. +func (p Pointer) Tokens() iter.Seq[string] { + return func(yield func(string) bool) { + for len(p) > 0 { + 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", "~") + } + if !yield(token) { + return + } + } + } } // appendStackPointer appends a JSON Pointer (RFC 6901) to the current value. diff --git a/jsontext/state_test.go b/jsontext/state_test.go index f396200..c771a06 100644 --- a/jsontext/state_test.go +++ b/jsontext/state_test.go @@ -7,10 +7,32 @@ package jsontext import ( "fmt" "reflect" + "slices" "strings" "testing" ) +func TestPointerTokens(t *testing.T) { + 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 := slices.Collect(tt.in.Tokens()) + if !slices.Equal(got, tt.want) { + t.Errorf("Pointer(%q).Tokens = %q, want %q", tt.in, got, tt.want) + } + } +} + func TestStateMachine(t *testing.T) { // To test a state machine, we pass an ordered sequence of operations and // check whether the current state is as expected.