From 2ac0e88d7b062233fe44d2d7b2e1f9884bdc0c32 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 10 Oct 2024 11:41:48 -0700 Subject: [PATCH] Add Value.All method to iterate over all values --- .github/workflows/test.yml | 4 +- format.go | 40 +++++++---------- format_test.go | 4 +- go.mod | 2 +- go.work | 2 +- patch.go | 4 +- standard.go | 20 +++++---- types.go | 88 +++++++++++++++++++++++++++++--------- types_test.go | 37 ++++++++++++++++ 9 files changed, 140 insertions(+), 61 deletions(-) create mode 100644 types_test.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1144400..41c4676 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.18.x + go-version: 1.23.x - name: Checkout code uses: actions/checkout@v2 - name: Format @@ -15,7 +15,7 @@ jobs: test-all: strategy: matrix: - go-version: [1.18.x] + go-version: [1.23.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/format.go b/format.go index e2fc006..bf47807 100644 --- a/format.go +++ b/format.go @@ -88,21 +88,6 @@ func (v *Value) Format() { v.UpdateOffsets() } -// Range iterates through a Value in depth-first order and -// calls f for each value (including the root value). -// It stops iteration when f returns false. -func (v *Value) Range(f func(v *Value) bool) bool { - if !f(v) { - return false - } - if comp, ok := v.Value.(composite); ok { - return comp.rangeValues(func(v2 *Value) bool { - return v2.Range(f) - }) - } - return true -} - // normalize performs simple normalization changes. In particular, it: // - normalizes strings, // - normalizes empty objects and arrays as simply {} or [], @@ -129,15 +114,16 @@ func (v *Value) normalize() bool { // If there is only whitespace between the name and colon, // or between the value and comma, then remove the whitespace. - v2.rangeValues(func(v *Value) bool { - if !v.AfterExtra.hasComment() { - v.AfterExtra = nil + for v3 := range v2.allValues() { + if !v3.AfterExtra.hasComment() { + v3.AfterExtra = nil } - return true - }) + } // Normalize all sub-values. - v2.rangeValues((*Value).normalize) + for v3 := range v2.allValues() { + v3.normalize() + } } return true } @@ -583,7 +569,9 @@ func (v *Value) alignObjectValues() bool { // Recursively align all sub-objects. if comp, ok := v.Value.(composite); ok { - comp.rangeValues((*Value).alignObjectValues) + for v2 := range comp.allValues() { + v2.alignObjectValues() + } } return true } @@ -593,9 +581,11 @@ func (v Value) hasNewline(checkTopLevelExtra bool) bool { return true } if comp, ok := v.Value.(composite); ok { - return !comp.rangeValues(func(v *Value) bool { - return !v.hasNewline(true) - }) + for v := range comp.allValues() { + if v.hasNewline(true) { + return true + } + } } return false } diff --git a/format_test.go b/format_test.go index b4e4c43..98770a3 100644 --- a/format_test.go +++ b/format_test.go @@ -67,13 +67,13 @@ var testdataFormat = []struct { in: "[//\r\t\n]", want: "[ //\n]", }, { - in: `{"name" :"value" ,"name":"value"}`, + in: `{"name" :"value" ,"name":"value"}`, want: `{"name": "value", "name": "value"}`, }, { in: `{"name"/**/:"value"/**/,"name":"value"}`, want: `{"name" /**/ : "value" /**/ , "name": "value"}`, }, { - in: `[null ,null]`, + in: `[null ,null]`, want: `[null, null]`, }, { in: `[null/**/,null]`, diff --git a/go.mod b/go.mod index be9562f..e047d29 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/tailscale/hujson -go 1.18 +go 1.23 require github.com/google/go-cmp v0.5.8 diff --git a/go.work b/go.work index d57b4e1..1e29170 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.18 +go 1.23 use ( . diff --git a/patch.go b/patch.go index 876471d..16bcbf6 100644 --- a/patch.go +++ b/patch.go @@ -417,8 +417,8 @@ func (b *Extra) extractTrailingcomments(readonly bool) (trailing Extra) { // classifyComments classifies comments as belonging to the previous element // or belonging to the current element such that: -// * b[:prevEnd] belongs to the previous element, and -// * b[currStart:] belongs to the current element. +// - b[:prevEnd] belongs to the previous element, and +// - b[currStart:] belongs to the current element. // // Invariant: prevEnd <= currStart func (b Extra) classifyComments() (prevEnd, currStart int) { diff --git a/standard.go b/standard.go index aef9cf1..3ab0a3f 100644 --- a/standard.go +++ b/standard.go @@ -14,8 +14,10 @@ func (v *Value) isStandard() bool { return false } if comp, ok := v.Value.(composite); ok { - if !comp.rangeValues((*Value).isStandard) { - return false + for v2 := range comp.allValues() { + if !v2.isStandard() { + return false + } } if hasTrailingComma(comp) || !comp.afterExtra().IsStandard() { return false @@ -41,15 +43,16 @@ func (v *Value) Minimize() { v.minimize() v.UpdateOffsets() } -func (v *Value) minimize() bool { +func (v *Value) minimize() { v.BeforeExtra = nil if v2, ok := v.Value.(composite); ok { - v2.rangeValues((*Value).minimize) + for v3 := range v2.allValues() { + v3.minimize() + } setTrailingComma(v2, false) *v2.afterExtra() = nil } v.AfterExtra = nil - return true } // Standardize strips any features specific to HuJSON from v, @@ -60,10 +63,12 @@ func (v *Value) Standardize() { v.standardize() v.UpdateOffsets() // should be noop if offsets are already correct } -func (v *Value) standardize() bool { +func (v *Value) standardize() { v.BeforeExtra.standardize() if comp, ok := v.Value.(composite); ok { - comp.rangeValues((*Value).standardize) + for v2 := range comp.allValues() { + v2.standardize() + } if last := comp.lastValue(); last != nil && last.AfterExtra != nil { *comp.afterExtra() = append(append(last.AfterExtra, ' '), *comp.afterExtra()...) last.AfterExtra = nil @@ -71,7 +76,6 @@ func (v *Value) standardize() bool { comp.afterExtra().standardize() } v.AfterExtra.standardize() - return true } func (b *Extra) standardize() { for i, c := range *b { diff --git a/types.go b/types.go index 06ced5a..a3d4d6f 100644 --- a/types.go +++ b/types.go @@ -12,8 +12,7 @@ // // See https://nigeltao.github.io/blog/2021/json-with-commas-comments.html // -// -// Functionality +// # Functionality // // The Parse function parses HuJSON input as a Value, // which is a syntax tree exactly representing the input. @@ -32,8 +31,7 @@ // but instead for the HuJSON and standard JSON format. // The Patch method applies a JSON Patch (RFC 6902) to the receiving value. // -// -// Grammar +// # Grammar // // The changes to the JSON grammar are: // @@ -72,8 +70,7 @@ // '000A' ws // '000D' ws // -// -// Use with the Standard Library +// # Use with the Standard Library // // This package operates with HuJSON as an AST. In order to parse HuJSON // into arbitrary Go types, use this package to parse HuJSON input as an AST, @@ -95,6 +92,7 @@ import ( "bytes" "encoding/json" "fmt" + "iter" "math" "strconv" "unicode/utf8" @@ -147,6 +145,39 @@ func (v Value) Clone() Value { return v } +// Range iterates through a Value in depth-first order and +// calls f for each value (including the root value). +// It stops iteration when f returns false. +// +// Deprecated: Use [All] instead. +func (v *Value) Range(f func(v *Value) bool) bool { + for v2 := range v.All() { + if !f(v2) { + return false + } + } + return true +} + +// All returns an iterator over all values in depth-first order, +// starting with v itself. +func (v *Value) All() iter.Seq[*Value] { + return func(yield func(*Value) bool) { + if !yield(v) { + return + } + if comp, ok := v.Value.(composite); ok { + for v2 := range comp.allValues() { + for v3 := range v2.All() { + if !yield(v3) { + return + } + } + } + } + } +} + // ValueTrimmed is a JSON value without surrounding whitespace or comments. // This is a sum type consisting of Literal, *Object, or *Array. type ValueTrimmed interface { @@ -335,6 +366,7 @@ type Object struct { // after the preceding open brace or comma and before the closing brace. AfterExtra Extra } + type ObjectMember struct { Name, Value Value } @@ -342,35 +374,43 @@ type ObjectMember struct { func (obj Object) length() int { return len(obj.Members) } + func (obj Object) firstValue() *Value { if len(obj.Members) > 0 { return &obj.Members[0].Name } return nil } -func (obj Object) rangeValues(f func(*Value) bool) bool { - for i := range obj.Members { - if !f(&obj.Members[i].Name) { - return false - } - if !f(&obj.Members[i].Value) { - return false + +// allValues iterates all members of the object, +// interleaved between the member name and the member value. +func (obj Object) allValues() iter.Seq[*Value] { + return func(yield func(*Value) bool) { + for i := range obj.Members { + if !yield(&obj.Members[i].Name) { + return + } + if !yield(&obj.Members[i].Value) { + return + } } } - return true } + func (obj Object) lastValue() *Value { if len(obj.Members) > 0 { return &obj.Members[len(obj.Members)-1].Value } return nil } + func (obj *Object) beforeExtraAt(i int) *Extra { if i < len(obj.Members) { return &obj.Members[i].Name.BeforeExtra } return &obj.AfterExtra } + func (obj *Object) afterExtra() *Extra { return &obj.AfterExtra } @@ -401,37 +441,45 @@ type Array struct { // after the preceding open bracket or comma and before the closing bracket. AfterExtra Extra } + type ArrayElement = Value func (arr Array) length() int { return len(arr.Elements) } + func (arr Array) firstValue() *Value { if len(arr.Elements) > 0 { return &arr.Elements[0] } return nil } -func (arr Array) rangeValues(f func(*Value) bool) bool { - for i := range arr.Elements { - if !f(&arr.Elements[i]) { - return false + +// allValues iterates all elements of the array. +func (arr Array) allValues() iter.Seq[*Value] { + return func(yield func(*Value) bool) { + for i := range arr.Elements { + if !yield(&arr.Elements[i]) { + return + } } } - return true } + func (arr Array) lastValue() *Value { if len(arr.Elements) > 0 { return &arr.Elements[len(arr.Elements)-1] } return nil } + func (arr *Array) beforeExtraAt(i int) *Extra { if i < len(arr.Elements) { return &arr.Elements[i].BeforeExtra } return &arr.AfterExtra } + func (arr *Array) afterExtra() *Extra { return &arr.AfterExtra } @@ -457,7 +505,7 @@ type composite interface { length() int firstValue() *Value - rangeValues(func(*Value) bool) bool + allValues() iter.Seq[*Value] lastValue() *Value getAt(int) ValueTrimmed diff --git a/types_test.go b/types_test.go new file mode 100644 index 0000000..5caef59 --- /dev/null +++ b/types_test.go @@ -0,0 +1,37 @@ +package hujson + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestAll(t *testing.T) { + v, err := Parse([]byte(`["fizz", {"key": ["value", {"foo": "bar"}]}, [1,2,3], "buzz"]`)) + if err != nil { + t.Fatalf("Parse: %v", err) + } + var got []string + for v2 := range v.All() { + got = append(got, v2.String()) + } + want := []string{ + `["fizz", {"key": ["value", {"foo": "bar"}]}, [1,2,3], "buzz"]`, + `"fizz"`, + ` {"key": ["value", {"foo": "bar"}]}`, + `"key"`, + ` ["value", {"foo": "bar"}]`, + `"value"`, + ` {"foo": "bar"}`, + `"foo"`, + ` "bar"`, + ` [1,2,3]`, + `1`, + `2`, + `3`, + ` "buzz"`, + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } +}