From 562b254e8e39d8d1f7b80a1b184d0c7284c33605 Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Wed, 12 Nov 2025 18:36:25 +0100 Subject: [PATCH] chore(lint): reduced disabled linter, addressed a few code quality issues * extended the set of enabled linters * improved readability of the set() method, which was too complex * added systematic checks on type assertion * removed impossible cases (type assertion _then_ same using reflect) * test: removed the use of global variables * test: improved the readability of tests * using t.Run() to label and hierarchize tests * using JSON test document as embedded asset Signed-off-by: Frederic BIDON --- .golangci.yml | 15 +- README.md | 4 +- pointer.go | 147 ++++++------ pointer_test.go | 458 +++++++++++++++++++++--------------- testdata/test_document.json | 34 +++ testdata_test.go | 66 ++++++ 6 files changed, 449 insertions(+), 275 deletions(-) create mode 100644 testdata/test_document.json create mode 100644 testdata_test.go diff --git a/.golangci.yml b/.golangci.yml index 858784d..f4d4518 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,33 +2,22 @@ version: "2" linters: default: all disable: - - cyclop - depguard - - errchkjson - errorlint - exhaustruct - - forcetypeassert - funlen - - gochecknoglobals - - gochecknoinits - - gocognit - godot - godox - gosmopolitan - - inamedparam - ireturn - lll - - musttag - - nestif - nlreturn - nonamedreturns - noinlineerr - paralleltest - recvcheck - testpackage - - thelper - tparallel - - unparam - varnamelen - whitespace - wrapcheck @@ -40,8 +29,10 @@ linters: goconst: min-len: 2 min-occurrences: 3 + cyclop: + max-complexity: 20 gocyclo: - min-complexity: 45 + min-complexity: 20 exclusions: generated: lax presets: diff --git a/README.md b/README.md index 54579cc..1b894ce 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# gojsonpointer +# jsonpointer [![Tests][test-badge]][test-url] [![Coverage][cov-badge]][cov-url] [![CI vuln scan][vuln-scan-badge]][vuln-scan-url] [![CodeQL][codeql-badge]][codeql-url] @@ -14,7 +14,7 @@ --- -An implementation of JSON Pointer - Go language +An implementation of JSON Pointer for golang, which supports go `struct`. ## Status diff --git a/pointer.go b/pointer.go index 76dae26..41d8eca 100644 --- a/pointer.go +++ b/pointer.go @@ -20,21 +20,16 @@ const ( pointerSeparator = `/` ) -var ( - jsonPointableType = reflect.TypeOf(new(JSONPointable)).Elem() - jsonSetableType = reflect.TypeOf(new(JSONSetable)).Elem() -) - // JSONPointable is an interface for structs to implement when they need to customize the // json pointer process type JSONPointable interface { - JSONLookup(string) (any, error) + JSONLookup(key string) (any, error) } // JSONSetable is an interface for structs to implement when they need to customize the // json pointer process type JSONSetable interface { - JSONSet(string, any) error + JSONSet(key string, value any) error } // Pointer is a representation of a json pointer @@ -174,7 +169,7 @@ func (p *Pointer) set(node, data any, nameProvider *jsonname.NameProvider) error nameProvider = jsonname.DefaultJSONNameProvider } - // Full document when empty + // full document when empty if len(p.referenceTokens) == 0 { return nil } @@ -185,81 +180,79 @@ func (p *Pointer) set(node, data any, nameProvider *jsonname.NameProvider) error decodedToken := Unescape(token) if isLastToken { - return setSingleImpl(node, data, decodedToken, nameProvider) } - // Check for nil during traversal - if isNil(node) { - return fmt.Errorf("cannot traverse through nil value at %q: %w", decodedToken, ErrPointer) + next, err := p.resolveNodeForToken(node, decodedToken, nameProvider) + if err != nil { + return err } - rValue := reflect.Indirect(reflect.ValueOf(node)) - kind := rValue.Kind() + node = next + } - if rValue.Type().Implements(jsonPointableType) { - r, err := node.(JSONPointable).JSONLookup(decodedToken) - if err != nil { - return err - } - fld := reflect.ValueOf(r) - if fld.CanAddr() && fld.Kind() != reflect.Interface && fld.Kind() != reflect.Map && fld.Kind() != reflect.Slice && fld.Kind() != reflect.Pointer { - node = fld.Addr().Interface() - continue - } - node = r - continue + return nil +} + +func (p *Pointer) resolveNodeForToken(node any, decodedToken string, nameProvider *jsonname.NameProvider) (next any, err error) { + // check for nil during traversal + if isNil(node) { + return nil, fmt.Errorf("cannot traverse through nil value at %q: %w", decodedToken, ErrPointer) + } + + pointable, ok := node.(JSONPointable) + if ok { + r, err := pointable.JSONLookup(decodedToken) + if err != nil { + return nil, err } - switch kind { //nolint:exhaustive - case reflect.Struct: - nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) - if !ok { - return fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer) - } - fld := rValue.FieldByName(nm) - if fld.CanAddr() && fld.Kind() != reflect.Interface && fld.Kind() != reflect.Map && fld.Kind() != reflect.Slice && fld.Kind() != reflect.Pointer { - node = fld.Addr().Interface() - continue - } - node = fld.Interface() + fld := reflect.ValueOf(r) + if fld.CanAddr() && fld.Kind() != reflect.Interface && fld.Kind() != reflect.Map && fld.Kind() != reflect.Slice && fld.Kind() != reflect.Pointer { + return fld.Addr().Interface(), nil + } - case reflect.Map: - kv := reflect.ValueOf(decodedToken) - mv := rValue.MapIndex(kv) + return r, nil + } - if !mv.IsValid() { - return fmt.Errorf("object has no key %q: %w", decodedToken, ErrPointer) - } - if mv.CanAddr() && mv.Kind() != reflect.Interface && mv.Kind() != reflect.Map && mv.Kind() != reflect.Slice && mv.Kind() != reflect.Pointer { - node = mv.Addr().Interface() - continue - } - node = mv.Interface() + rValue := reflect.Indirect(reflect.ValueOf(node)) + kind := rValue.Kind() - case reflect.Slice: - tokenIndex, err := strconv.Atoi(decodedToken) - if err != nil { - return err - } - sLength := rValue.Len() - if tokenIndex < 0 || tokenIndex >= sLength { - return fmt.Errorf("index out of bounds array[0,%d] index '%d': %w", sLength, tokenIndex, ErrPointer) - } + switch kind { //nolint:exhaustive + case reflect.Struct: + nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) + if !ok { + return nil, fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer) + } - elem := rValue.Index(tokenIndex) - if elem.CanAddr() && elem.Kind() != reflect.Interface && elem.Kind() != reflect.Map && elem.Kind() != reflect.Slice && elem.Kind() != reflect.Pointer { - node = elem.Addr().Interface() - continue - } - node = elem.Interface() + return typeFromValue(rValue.FieldByName(nm)), nil - default: - return fmt.Errorf("invalid token reference %q: %w", decodedToken, ErrPointer) + case reflect.Map: + kv := reflect.ValueOf(decodedToken) + mv := rValue.MapIndex(kv) + + if !mv.IsValid() { + return nil, fmt.Errorf("object has no key %q: %w", decodedToken, ErrPointer) } - } - return nil + return typeFromValue(mv), nil + + case reflect.Slice: + tokenIndex, err := strconv.Atoi(decodedToken) + if err != nil { + return nil, errors.Join(err, ErrPointer) + } + + sLength := rValue.Len() + if tokenIndex < 0 || tokenIndex >= sLength { + return nil, fmt.Errorf("index out of bounds array[0,%d] index '%d': %w", sLength, tokenIndex, ErrPointer) + } + + return typeFromValue(rValue.Index(tokenIndex)), nil + + default: + return nil, fmt.Errorf("invalid token reference %q: %w", decodedToken, ErrPointer) + } } func isNil(input any) bool { @@ -276,6 +269,14 @@ func isNil(input any) bool { } } +func typeFromValue(v reflect.Value) any { + if v.CanAddr() && v.Kind() != reflect.Interface && v.Kind() != reflect.Map && v.Kind() != reflect.Slice && v.Kind() != reflect.Pointer { + return v.Addr().Interface() + } + + return v.Interface() +} + // GetForToken gets a value for a json pointer token 1 level deep func GetForToken(document any, decodedToken string) (any, reflect.Kind, error) { return getSingleImpl(document, decodedToken, jsonname.DefaultJSONNameProvider) @@ -348,14 +349,10 @@ func setSingleImpl(node, data any, decodedToken string, nameProvider *jsonname.N return fmt.Errorf("cannot set field %q on nil value: %w", decodedToken, ErrPointer) } - if ns, ok := node.(JSONSetable); ok { // pointer impl + if ns, ok := node.(JSONSetable); ok { return ns.JSONSet(decodedToken, data) } - if rValue.Type().Implements(jsonSetableType) { - return node.(JSONSetable).JSONSet(decodedToken, data) - } - switch rValue.Kind() { //nolint:exhaustive case reflect.Struct: nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) @@ -499,8 +496,8 @@ const ( ) var ( - encRefTokReplacer = strings.NewReplacer(encRefTok1, decRefTok1, encRefTok0, decRefTok0) - decRefTokReplacer = strings.NewReplacer(decRefTok1, encRefTok1, decRefTok0, encRefTok0) + encRefTokReplacer = strings.NewReplacer(encRefTok1, decRefTok1, encRefTok0, decRefTok0) //nolint:gochecknoglobals // it's okay to declare a replacer as a private global + decRefTokReplacer = strings.NewReplacer(decRefTok1, encRefTok1, decRefTok0, encRefTok0) //nolint:gochecknoglobals // it's okay to declare a replacer as a private global ) // Unescape unescapes a json pointer reference token string to the original representation diff --git a/pointer_test.go b/pointer_test.go index 4b1abae..06ff043 100644 --- a/pointer_test.go +++ b/pointer_test.go @@ -13,158 +13,182 @@ import ( "github.com/go-openapi/testify/v2/require" ) -const ( - TestDocumentNBItems = 11 - TestNodeObjNBItems = 4 - TestDocumentString = `{ -"foo": ["bar", "baz"], -"obj": { "a":1, "b":2, "c":[3,4], "d":[ {"e":9}, {"f":[50,51]} ] }, -"": 0, -"a/b": 1, -"c%d": 2, -"e^f": 3, -"g|h": 4, -"i\\j": 5, -"k\"l": 6, -" ": 7, -"m~n": 8 -}` -) - -var testDocumentJSON any - -type testStructJSON struct { - Foo []string `json:"foo"` - Obj struct { - A int `json:"a"` - B int `json:"b"` - C []int `json:"c"` - D []struct { - E int `json:"e"` - F []int `json:"f"` - } `json:"d"` - } `json:"obj"` -} +func TestEscaping(t *testing.T) { + t.Parallel() + + t.Run("escaped pointer strings against test document", func(t *testing.T) { + ins := []string{`/`, `/`, `/a~1b`, `/a~1b`, `/c%d`, `/e^f`, `/g|h`, `/i\j`, `/k"l`, `/ `, `/m~0n`} + outs := []float64{0, 0, 1, 1, 2, 3, 4, 5, 6, 7, 8} + + for i := range ins { + t.Run("should create a JSONPointer", func(t *testing.T) { + p, err := New(ins[i]) + require.NoError(t, err, "input: %v", ins[i]) + + t.Run("should get JSONPointer from document", func(t *testing.T) { + result, _, err := p.Get(testDocumentJSON(t)) + require.NoError(t, err, "input: %v", ins[i]) + assert.InDeltaf(t, outs[i], result, 1e-6, "input: %v", ins[i]) + }) + }) + } + }) -type aliasedMap map[string]any + t.Run("special escapes", func(t *testing.T) { + t.Parallel() -var testStructJSONDoc testStructJSON -var testStructJSONPtr *testStructJSON + t.Run("with escape then unescape", func(t *testing.T) { + const original = "a/" -func init() { - if err := json.Unmarshal([]byte(TestDocumentString), &testDocumentJSON); err != nil { - panic(err) - } - if err := json.Unmarshal([]byte(TestDocumentString), &testStructJSONDoc); err != nil { - panic(err) - } + t.Run("unescaping an escaped string should yield the original", func(t *testing.T) { + esc := Escape(original) + assert.Equal(t, "a~1", esc) - testStructJSONPtr = &testStructJSONDoc -} + unesc := Unescape(esc) + assert.Equal(t, original, unesc) + }) + }) -func TestEscaping(t *testing.T) { - ins := []string{`/`, `/`, `/a~1b`, `/a~1b`, `/c%d`, `/e^f`, `/g|h`, `/i\j`, `/k"l`, `/ `, `/m~0n`} - outs := []float64{0, 0, 1, 1, 2, 3, 4, 5, 6, 7, 8} + t.Run("with multiple escapes", func(t *testing.T) { + unesc := Unescape("~01") + assert.Equal(t, "~1", unesc) + assert.Equal(t, "~01", Escape(unesc)) - for i := range ins { - p, err := New(ins[i]) - require.NoError(t, err, "input: %v", ins[i]) - result, _, err := p.Get(testDocumentJSON) + const ( + original = "~/" + escaped = "~0~1" + ) - require.NoError(t, err, "input: %v", ins[i]) - assert.InDeltaf(t, outs[i], result, 1e-6, "input: %v", ins[i]) - } + assert.Equal(t, escaped, Escape(original)) + assert.Equal(t, original, Unescape(escaped)) + }) + t.Run("with escaped characters in pointer", func(t *testing.T) { + t.Run("escaped ~", func(t *testing.T) { + s := Escape("m~n") + assert.Equal(t, "m~0n", s) + }) + t.Run("escaped /", func(t *testing.T) { + s := Escape("m/n") + assert.Equal(t, "m~1n", s) + }) + }) + }) } func TestFullDocument(t *testing.T) { - const in = `` + t.Parallel() - p, err := New(in) - require.NoErrorf(t, err, "New(%v) error %v", in, err) + t.Run("with empty pointer", func(t *testing.T) { + const in = `` - result, _, err := p.Get(testDocumentJSON) - require.NoErrorf(t, err, "Get(%v) error %v", in, err) + p, err := New(in) + require.NoErrorf(t, err, "New(%v) error %v", in, err) - asMap, ok := result.(map[string]any) - require.True(t, ok) - require.Lenf(t, asMap, TestDocumentNBItems, "Get(%v) = %v, expect full document", in, result) + t.Run("should resolve full doc", func(t *testing.T) { + result, _, err := p.Get(testDocumentJSON(t)) + require.NoErrorf(t, err, "Get(%v) error %v", in, err) - result, _, err = p.get(testDocumentJSON, nil) - require.NoErrorf(t, err, "Get(%v) error %v", in, err) + asMap, ok := result.(map[string]any) + require.True(t, ok) - asMap, ok = result.(map[string]any) - require.True(t, ok) - require.Lenf(t, asMap, TestDocumentNBItems, "Get(%v) = %v, expect full document", in, result) + require.Lenf(t, asMap, testDocumentNBItems(), "Get(%v) = %v, expect full document", in, result) + }) + + t.Run("should resolve full doc, with nil name provider", func(t *testing.T) { + result, _, err := p.get(testDocumentJSON(t), nil) + require.NoErrorf(t, err, "Get(%v) error %v", in, err) + + asMap, ok := result.(map[string]any) + require.True(t, ok) + require.Lenf(t, asMap, testDocumentNBItems(), "Get(%v) = %v, expect full document", in, result) + }) + }) } func TestDecodedTokens(t *testing.T) { + t.Parallel() + p, err := New("/obj/a~1b") require.NoError(t, err) assert.Equal(t, []string{"obj", "a/b"}, p.DecodedTokens()) } func TestIsEmpty(t *testing.T) { - p, err := New("") - require.NoError(t, err) + t.Parallel() - assert.True(t, p.IsEmpty()) - p, err = New("/obj") - require.NoError(t, err) + t.Run("with empty pointer", func(t *testing.T) { + p, err := New("") + require.NoError(t, err) + + assert.True(t, p.IsEmpty()) + }) - assert.False(t, p.IsEmpty()) + t.Run("with non-empty pointer", func(t *testing.T) { + p, err := New("/obj") + require.NoError(t, err) + + assert.False(t, p.IsEmpty()) + }) } func TestGetSingle(t *testing.T) { - const in = `/obj` + t.Parallel() + + const key = "obj" t.Run("should create a new JSON pointer", func(t *testing.T) { + const in = "/" + key + _, err := New(in) require.NoError(t, err) }) - t.Run(`should find token "obj" in JSON`, func(t *testing.T) { - result, _, err := GetForToken(testDocumentJSON, "obj") + t.Run(fmt.Sprintf("should find token %q in JSON", key), func(t *testing.T) { + result, _, err := GetForToken(testDocumentJSON(t), key) require.NoError(t, err) - assert.Len(t, result, TestNodeObjNBItems) + assert.Len(t, result, testNodeObjNBItems()) }) - t.Run(`should find token "obj" in type alias interface`, func(t *testing.T) { + t.Run(fmt.Sprintf("should find token %q in type alias interface", key), func(t *testing.T) { type alias any - var in alias = testDocumentJSON - result, _, err := GetForToken(in, "obj") + var in alias = testDocumentJSON(t) + + result, _, err := GetForToken(in, key) require.NoError(t, err) - assert.Len(t, result, TestNodeObjNBItems) + assert.Len(t, result, testNodeObjNBItems()) }) - t.Run(`should find token "obj" in pointer to interface`, func(t *testing.T) { - in := &testDocumentJSON - result, _, err := GetForToken(in, "obj") + t.Run(fmt.Sprintf("should find token %q in pointer to interface", key), func(t *testing.T) { + in := testDocumentJSON(t) + + result, _, err := GetForToken(&in, key) require.NoError(t, err) - assert.Len(t, result, TestNodeObjNBItems) + assert.Len(t, result, testNodeObjNBItems()) }) - t.Run(`should not find token "Obj" in struct`, func(t *testing.T) { - result, _, err := GetForToken(testStructJSONDoc, "Obj") + t.Run(`should NOT find token "Obj" in struct`, func(t *testing.T) { + result, _, err := GetForToken(testStructJSONDoc(t), "Obj") require.Error(t, err) assert.Nil(t, result) }) t.Run(`should not find token "Obj2" in struct`, func(t *testing.T) { - result, _, err := GetForToken(testStructJSONDoc, "Obj2") + result, _, err := GetForToken(testStructJSONDoc(t), "Obj2") require.Error(t, err) assert.Nil(t, result) }) - t.Run(`should not find token in nil`, func(t *testing.T) { - result, _, err := GetForToken(nil, "obj") + t.Run("should not find token in nil", func(t *testing.T) { + result, _, err := GetForToken(nil, key) require.Error(t, err) assert.Nil(t, result) }) - t.Run(`should not find token in nil interface`, func(t *testing.T) { + t.Run("should not find token in nil interface", func(t *testing.T) { var in any - result, _, err := GetForToken(in, "obj") + + result, _, err := GetForToken(in, key) require.Error(t, err) assert.Nil(t, result) }) @@ -197,130 +221,192 @@ func (p pointableMap) JSONLookup(token string) (any, error) { } func TestPointableInterface(t *testing.T) { - p := &pointableImpl{"hello"} + t.Parallel() - result, _, err := GetForToken(p, "some") - require.NoError(t, err) - assert.Equal(t, p.a, result) + t.Run("with pointable type", func(t *testing.T) { + p := &pointableImpl{"hello"} + result, _, err := GetForToken(p, "some") + require.NoError(t, err) + assert.Equal(t, p.a, result) - result, _, err = GetForToken(p, "something") - require.Error(t, err) - assert.Nil(t, result) + result, _, err = GetForToken(p, "something") + require.Error(t, err) + assert.Nil(t, result) + }) - pm := pointableMap{"swapped": "hello", "a": "world"} - result, _, err = GetForToken(pm, "swap") - require.NoError(t, err) - assert.Equal(t, pm["swapped"], result) + t.Run("with pointable map", func(t *testing.T) { + p := pointableMap{"swapped": "hello", "a": "world"} + result, _, err := GetForToken(p, "swap") + require.NoError(t, err) + assert.Equal(t, p["swapped"], result) - result, _, err = GetForToken(pm, "a") - require.NoError(t, err) - assert.Equal(t, pm["a"], result) + result, _, err = GetForToken(p, "a") + require.NoError(t, err) + assert.Equal(t, p["a"], result) + }) } func TestGetNode(t *testing.T) { + t.Parallel() + const in = `/obj` - p, err := New(in) - require.NoError(t, err) + t.Run("should build pointer", func(t *testing.T) { + p, err := New(in) + require.NoError(t, err) - result, _, err := p.Get(testDocumentJSON) - require.NoError(t, err) - assert.Len(t, result, TestNodeObjNBItems) + t.Run("should resolve pointer against document", func(t *testing.T) { + result, _, err := p.Get(testDocumentJSON(t)) + require.NoError(t, err) + assert.Len(t, result, testNodeObjNBItems()) + }) - result, _, err = p.Get(aliasedMap(testDocumentJSON.(map[string]any))) - require.NoError(t, err) - assert.Len(t, result, TestNodeObjNBItems) + t.Run("with aliased map", func(t *testing.T) { + asMap, ok := testDocumentJSON(t).(map[string]any) + require.True(t, ok) + alias := aliasedMap(asMap) - result, _, err = p.Get(testStructJSONDoc) - require.NoError(t, err) - assert.Equal(t, testStructJSONDoc.Obj, result) + result, _, err := p.Get(alias) + require.NoError(t, err) + assert.Len(t, result, testNodeObjNBItems()) + }) - result, _, err = p.Get(testStructJSONPtr) - require.NoError(t, err) - assert.Equal(t, testStructJSONDoc.Obj, result) + t.Run("with struct", func(t *testing.T) { + doc := testStructJSONDoc(t) + expected := testStructJSONDoc(t).Obj + + result, _, err := p.Get(doc) + require.NoError(t, err) + assert.Equal(t, expected, result) + }) + + t.Run("with pointer to struct", func(t *testing.T) { + doc := testStructJSONPtr(t) + expected := testStructJSONDoc(t).Obj + + result, _, err := p.Get(doc) + require.NoError(t, err) + assert.Equal(t, expected, result) + }) + }) } func TestArray(t *testing.T) { + t.Parallel() + ins := []string{`/foo/0`, `/foo/0`, `/foo/1`} outs := []string{"bar", "bar", "baz"} - for i := range ins { - p, err := New(ins[i]) - require.NoError(t, err) + for i, pointer := range ins { + expected := outs[i] + + t.Run(fmt.Sprintf("with pointer %q", pointer), func(t *testing.T) { + p, err := New(pointer) + require.NoError(t, err) - result, _, err := p.Get(testStructJSONDoc) + t.Run("should resolve against struct", func(t *testing.T) { + result, _, err := p.Get(testStructJSONDoc(t)) + require.NoError(t, err) + assert.Equal(t, expected, result) + }) + + t.Run("should resolve against pointer to struct", func(t *testing.T) { + result, _, err := p.Get(testStructJSONPtr(t)) + require.NoError(t, err) + assert.Equal(t, expected, result) + }) + + t.Run("should resolve against dynamic JSON map", func(t *testing.T) { + result, _, err := p.Get(testDocumentJSON(t)) + require.NoError(t, err) + assert.Equal(t, expected, result) + }) + }) + } +} + +func TestOtherThings(t *testing.T) { + t.Parallel() + + t.Run("single string pointer should be valid", func(t *testing.T) { + _, err := New("abc") + require.Error(t, err) + }) + + t.Run("empty string pointer should be valid", func(t *testing.T) { + p, err := New("") require.NoError(t, err) - assert.Equal(t, outs[i], result) + assert.Empty(t, p.String()) + }) - result, _, err = p.Get(testStructJSONPtr) + t.Run("string representation of a pointer", func(t *testing.T) { + p, err := New("/obj/a") require.NoError(t, err) - assert.Equal(t, outs[i], result) + assert.Equal(t, "/obj/a", p.String()) + }) - result, _, err = p.Get(testDocumentJSON) + t.Run("out of bound array index should error", func(t *testing.T) { + p, err := New("/foo/3") require.NoError(t, err) - assert.Equal(t, outs[i], result) - } -} -func TestOtherThings(t *testing.T) { - _, err := New("abc") - require.Error(t, err) + _, _, err = p.Get(testDocumentJSON(t)) + require.Error(t, err) + }) - p, err := New("") - require.NoError(t, err) - assert.Empty(t, p.String()) + t.Run("referring to a key in an array should error", func(t *testing.T) { + p, err := New("/foo/a") + require.NoError(t, err) + _, _, err = p.Get(testDocumentJSON(t)) + require.Error(t, err) + }) - p, err = New("/obj/a") - require.NoError(t, err) - assert.Equal(t, "/obj/a", p.String()) + t.Run("referring to a non-existing key in an array should error", func(t *testing.T) { + p, err := New("/notthere") + require.NoError(t, err) + _, _, err = p.Get(testDocumentJSON(t)) + require.Error(t, err) + }) - s := Escape("m~n") - assert.Equal(t, "m~0n", s) - s = Escape("m/n") - assert.Equal(t, "m~1n", s) + t.Run("resolving pointer against an unsupport type (int) should error", func(t *testing.T) { + p, err := New("/invalid") + require.NoError(t, err) + _, _, err = p.Get(1234) + require.Error(t, err) + }) - p, err = New("/foo/3") - require.NoError(t, err) - _, _, err = p.Get(testDocumentJSON) - require.Error(t, err) + t.Run("with pointer to an array index", func(t *testing.T) { + for index := range 2 { + p, err := New(fmt.Sprintf("/foo/%d", index)) + require.NoError(t, err) - p, err = New("/foo/a") - require.NoError(t, err) - _, _, err = p.Get(testDocumentJSON) - require.Error(t, err) + v, _, err := p.Get(testDocumentJSON(t)) + require.NoError(t, err) - p, err = New("/notthere") - require.NoError(t, err) - _, _, err = p.Get(testDocumentJSON) - require.Error(t, err) + expected := extractFooKeyIndex(t, index) + assert.Equal(t, expected, v) + } + }) +} - p, err = New("/invalid") - require.NoError(t, err) - _, _, err = p.Get(1234) - require.Error(t, err) +func extractFooKeyIndex(t *testing.T, index int) any { + t.Helper() - p, err = New("/foo/1") - require.NoError(t, err) - expected := "hello" - bbb := testDocumentJSON.(map[string]any)["foo"] - bbb.([]any)[1] = "hello" + asMap, ok := testDocumentJSON(t).(map[string]any) + require.True(t, ok) - v, _, err := p.Get(testDocumentJSON) - require.NoError(t, err) - assert.Equal(t, expected, v) + // {"foo": [ ... ] } + bbb, ok := asMap["foo"] + require.True(t, ok) - esc := Escape("a/") - assert.Equal(t, "a~1", esc) - unesc := Unescape(esc) - assert.Equal(t, "a/", unesc) + asArray, ok := bbb.([]any) + require.True(t, ok) - unesc = Unescape("~01") - assert.Equal(t, "~1", unesc) - assert.Equal(t, "~0~1", Escape("~/")) - assert.Equal(t, "~/", Unescape("~0~1")) + return asArray[index] } func TestObject(t *testing.T) { + t.Parallel() + ins := []string{`/obj/a`, `/obj/b`, `/obj/c/0`, `/obj/c/1`, `/obj/c/1`, `/obj/d/1/f/0`} outs := []float64{1, 2, 3, 4, 4, 50} @@ -328,26 +414,20 @@ func TestObject(t *testing.T) { p, err := New(ins[i]) require.NoError(t, err) - result, _, err := p.Get(testDocumentJSON) + result, _, err := p.Get(testDocumentJSON(t)) require.NoError(t, err) assert.InDelta(t, outs[i], result, 1e-6) - result, _, err = p.Get(testStructJSONDoc) + result, _, err = p.Get(testStructJSONDoc(t)) require.NoError(t, err) assert.InDelta(t, outs[i], result, 1e-6) - result, _, err = p.Get(testStructJSONPtr) + result, _, err = p.Get(testStructJSONPtr(t)) require.NoError(t, err) assert.InDelta(t, outs[i], result, 1e-6) } } -/* - type setJSONDocEle struct { - B int `json:"b"` - C int `json:"c"` - } -*/ type setJSONDoc struct { A []struct { B int `json:"b"` @@ -487,6 +567,8 @@ func (s *settableInt) UnmarshalJSON(data []byte) error { } func TestSetNode(t *testing.T) { + t.Parallel() + const jsonText = `{"a":[{"b": 1, "c": 2}], "d": 3}` var jsonDocument any @@ -513,7 +595,9 @@ func TestSetNode(t *testing.T) { chNodeVI := changedNode["c"] require.IsType(t, 0, chNodeVI) - changedNodeValue := chNodeVI.(int) + changedNodeValue, ok := chNodeVI.(int) + require.True(t, ok) + require.Equal(t, 999, changedNodeValue) assert.Len(t, sliceNode, 1) }) @@ -713,6 +797,8 @@ func TestSetNode(t *testing.T) { } func TestOffset(t *testing.T) { + t.Parallel() + cases := []struct { name string ptr string diff --git a/testdata/test_document.json b/testdata/test_document.json new file mode 100644 index 0000000..40ea4a2 --- /dev/null +++ b/testdata/test_document.json @@ -0,0 +1,34 @@ +{ + "foo": [ + "bar", + "baz" + ], + "obj": { + "a":1, + "b":2, + "c":[ + 3, + 4 + ], + "d":[ + { + "e":9 + }, + { + "f":[ + 50, + 51 + ] + } + ] + }, + "": 0, + "a/b": 1, + "c%d": 2, + "e^f": 3, + "g|h": 4, + "i\\j": 5, + "k\"l": 6, + " ": 7, + "m~n": 8 +} diff --git a/testdata_test.go b/testdata_test.go new file mode 100644 index 0000000..5e5091f --- /dev/null +++ b/testdata_test.go @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package jsonpointer + +import ( + _ "embed" // initialize embed + "encoding/json" + "testing" + + "github.com/go-openapi/testify/v2/require" +) + +//go:embed testdata/*.json +var testDocumentJSONBytes []byte + +func testDocumentJSON(t *testing.T) any { + t.Helper() + + var document any + require.NoError(t, json.Unmarshal(testDocumentJSONBytes, &document)) + + return document +} + +func testStructJSONDoc(t *testing.T) testStructJSON { + t.Helper() + + var document testStructJSON + require.NoError(t, json.Unmarshal(testDocumentJSONBytes, &document)) + + return document +} + +func testStructJSONPtr(t *testing.T) *testStructJSON { + t.Helper() + + document := testStructJSONDoc(t) + + return &document +} + +// number of items in the test document +func testDocumentNBItems() int { + return 11 +} + +// number of objects nodes in the test document +func testNodeObjNBItems() int { + return 4 +} + +type testStructJSON struct { + Foo []string `json:"foo"` + Obj struct { + A int `json:"a"` + B int `json:"b"` + C []int `json:"c"` + D []struct { + E int `json:"e"` + F []int `json:"f"` + } `json:"d"` + } `json:"obj"` +} + +type aliasedMap map[string]any