diff --git a/.golangci.yml b/.golangci.yml index dc7c960..9d27331 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,7 +4,10 @@ linters: disable: - depguard - funlen + - goconst - godox + - gomodguard + - gomodguard_v2 - exhaustruct - nlreturn - nonamedreturns diff --git a/dash_token_test.go b/dash_token_test.go index 6a18101..bc3bf71 100644 --- a/dash_token_test.go +++ b/dash_token_test.go @@ -11,9 +11,9 @@ import ( "github.com/go-openapi/testify/v2/require" ) -// RFC 6901 §4: the "-" token refers to the (nonexistent) element after the -// last array element. It is always an error on Get/Offset, valid only as -// the terminal token of a Set against a slice (append, per RFC 6902). +// RFC 6901 §4: the "-" token refers to the (nonexistent) element after the last array element. +// It is always an error on Get/Offset, valid only as the terminal token of a Set against a slice +// (append, per RFC 6902). func TestDashToken_GetAlwaysErrors(t *testing.T) { t.Parallel() @@ -56,7 +56,8 @@ func TestDashToken_GetAlwaysErrors(t *testing.T) { }) t.Run("dash on map key is a regular lookup, not an error", func(t *testing.T) { - // "-" is only special for arrays. A literal "-" key in a map is fine. + // "-" is only special for arrays. + // A literal "-" key in a map is fine. doc := map[string]any{"-": 42} p, err := New("/-") require.NoError(t, err) @@ -208,8 +209,8 @@ func (d *dashSetter) JSONSet(key string, value any) error { func TestDashToken_JSONSetableReceivesRawDash(t *testing.T) { t.Parallel() - // When the terminal parent implements JSONSetable, the dash token is - // passed through verbatim. Semantics are the user type's responsibility. + // When the terminal parent implements JSONSetable, the dash token is passed through verbatim. + // Semantics are the user type's responsibility. ds := &dashSetter{} p, err := New("/-") require.NoError(t, err) diff --git a/errors.go b/errors.go index 8813474..2ae6e3c 100644 --- a/errors.go +++ b/errors.go @@ -21,14 +21,15 @@ const ( // ErrUnsupportedValueType indicates that a value of the wrong type is being set. ErrUnsupportedValueType pointerError = "only structs, pointers, maps and slices are supported for setting values" - // ErrDashToken indicates use of the RFC 6901 "-" reference token - // in a context where it cannot be resolved. + // ErrDashToken indicates use of the RFC 6901 "-" reference token in a context where it cannot be + // resolved. // - // Per RFC 6901 §4 the "-" token refers to the (nonexistent) element - // after the last array element. It may only be used as the terminal - // token of a [Pointer.Set] against a slice, where it means "append". - // Any other use (get, offset, intermediate traversal, non-slice target) - // is an error condition that wraps this sentinel. + // Per RFC 6901 §4 the "-" token refers to the (nonexistent) element after the last array element. + // It may only be used as the terminal token of a [Pointer.Set] against a slice, where it means + // "append". + // + // Any other use (get, offset, intermediate traversal, non-slice target) is an error condition that + // wraps this sentinel. ErrDashToken pointerError = `the "-" array token cannot be resolved here` //nolint:gosec // G101 false positive: this is a JSON Pointer reference token, not a credential. ) diff --git a/examples_test.go b/examples_test.go index 0903be8..dbb2b80 100644 --- a/examples_test.go +++ b/examples_test.go @@ -8,7 +8,7 @@ import ( "errors" "fmt" - "github.com/go-openapi/swag/jsonname" + "github.com/go-openapi/jsonpointer/jsonname" ) var ErrExampleStruct = errors.New("example error") @@ -60,7 +60,7 @@ func ExampleNew() { // key contains "/" fmt.Printf("pointer to key %q: %q\n", Unescape("foo~1"), escaped1.String()) - // output: + // Output: // empty pointer: "" // pointer to object key: "/foo" // pointer to array element: "/foo/1" @@ -132,10 +132,10 @@ func ExamplePointer_Set() { // doc: jsonpointer.exampleDocument{Foo:[]string{"bar", "hey my"}} } -// ExamplePointer_Set_append demonstrates the RFC 6901 "-" token as an -// append operation on a slice. On nested slices reached through an -// addressable parent (map entry, pointer to struct, ...), the append is -// performed in place and the returned document is the same reference. +// ExamplePointer_Set_append demonstrates the RFC 6901 "-" token as an append operation on a slice. +// +// On nested slices reached through an addressable parent (map entry, pointer to struct, ...), the +// append is performed in place and the returned document is the same reference. func ExamplePointer_Set_append() { doc := map[string]any{"foo": []any{"bar"}} @@ -154,15 +154,14 @@ func ExamplePointer_Set_append() { fmt.Printf("doc: %v\n", doc["foo"]) - // Output: - // doc: [bar baz] + // Output: doc: [bar baz] } -// ExamplePointer_Set_appendTopLevelSlice shows the one case where the -// returned document is load-bearing: appending to a top-level slice -// passed by value. The library cannot rebind the slice header in the -// caller's variable, so callers must use the returned document (or pass -// *[]T to get in-place rebind). +// ExamplePointer_Set_appendTopLevelSlice shows the one case where the returned document is +// load-bearing: appending to a top-level slice passed by value. +// +// The library cannot rebind the slice header in the caller's variable, so callers must use the +// returned document (or pass *[]T to get in-place rebind). func ExamplePointer_Set_appendTopLevelSlice() { doc := []int{1, 2} @@ -188,8 +187,8 @@ func ExamplePointer_Set_appendTopLevelSlice() { // returned: [1 2 3] } -// ExampleUseGoNameProvider contrasts the two [NameProvider] implementations -// shipped by [github.com/go-openapi/swag/jsonname]: +// ExampleUseGoNameProvider contrasts the two [NameProvider] implementations shipped by +// [github.com/go-openapi/jsonpointer/jsonname]: // // - the default provider requires a `json` struct tag to expose a field; // - the Go-name provider follows encoding/json conventions and accepts diff --git a/go.mod b/go.mod index 3d62937..41a00ff 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,5 @@ module github.com/go-openapi/jsonpointer -require ( - github.com/go-openapi/swag/jsonname v0.26.1 - github.com/go-openapi/testify/v2 v2.6.0 -) +require github.com/go-openapi/testify/v2 v2.6.0 go 1.25.0 diff --git a/go.sum b/go.sum index f3c8589..86512be 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,2 @@ -github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE= -github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc= github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= diff --git a/iface_example_test.go b/iface_example_test.go index 90d6d98..1cf9b2f 100644 --- a/iface_example_test.go +++ b/iface_example_test.go @@ -142,7 +142,7 @@ func Example_iface() { fmt.Printf("updated doc: %v", doc) - // output: + // Output: // propA (string): initial value for a // propB (string): initial value for b // propC: key "extra" not found: example error diff --git a/ifaces.go b/ifaces.go index 1e56ac0..31359c4 100644 --- a/ifaces.go +++ b/ifaces.go @@ -5,39 +5,42 @@ package jsonpointer import "reflect" -// JSONPointable is an interface for structs to implement, -// when they need to customize the json pointer process or want to avoid the use of reflection. +// JSONPointable is an interface for structs to implement, when they need to customize the json +// pointer process or want to avoid the use of reflection. type JSONPointable interface { // JSONLookup returns a value pointed at this (unescaped) key. JSONLookup(key string) (any, error) } -// JSONSetable is an interface for structs to implement, -// when they need to customize the json pointer process or want to avoid the use of reflection. +// JSONSetable is an interface for structs to implement, when they need to customize the json +// pointer process or want to avoid the use of reflection. // // # Handling of the RFC 6901 "-" token // -// When a type implementing JSONSetable is the terminal parent of a [Pointer.Set] -// call, the library passes the raw reference token to JSONSet without -// interpretation. In particular, the RFC 6901 "-" token (which conventionally -// means "append" for arrays, per RFC 6902) is forwarded verbatim as the key -// argument. Implementations that model an array-like container are expected -// to give "-" the append semantics; implementations that do not should return -// an error wrapping [ErrDashToken] (or [ErrPointer]) for clarity. +// When a type implementing JSONSetable is the terminal parent of a [Pointer.Set] call, the library +// passes the raw reference token to JSONSet without interpretation. // -// Implementations are responsible for any in-place mutation: the library does -// not attempt to rebind the result of JSONSet into a parent container. +// In particular, the RFC 6901 "-" token (which conventionally means "append" for arrays, per RFC +// 6902) is forwarded verbatim as the key argument. +// +// Implementations that model an array-like container are expected to give "-" the append semantics; +// implementations that do not should return an error wrapping [ErrDashToken] (or [ErrPointer]) for +// clarity. +// +// Implementations are responsible for any in-place mutation: the library does not attempt to rebind +// the result of JSONSet into a parent container. type JSONSetable interface { // JSONSet sets the value pointed at the (unescaped) key. // - // The key may be the RFC 6901 "-" token when the pointer targets a - // slice-like member; see the interface documentation for details. + // The key may be the RFC 6901 "-" token when the pointer targets a slice-like member; see the + // interface documentation for details. JSONSet(key string, value any) error } // NameProvider knows how to resolve go struct fields into json names. // -// The default provider is brought by [github.com/go-openapi/swag/jsonname.DefaultJSONNameProvider]. +// The default provider is brought by +// [github.com/go-openapi/jsonpointer/jsonname.DefaultJSONNameProvider]. type NameProvider interface { // GetGoName gets the go name for a json property name GetGoName(subject any, name string) (string, bool) diff --git a/jsonname/doc.go b/jsonname/doc.go new file mode 100644 index 0000000..79232ea --- /dev/null +++ b/jsonname/doc.go @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +// Package jsonname is a provider of json property names from go properties. +package jsonname diff --git a/jsonname/go_name_provider.go b/jsonname/go_name_provider.go new file mode 100644 index 0000000..5eec18f --- /dev/null +++ b/jsonname/go_name_provider.go @@ -0,0 +1,288 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package jsonname + +import ( + "reflect" + "strings" + "sync" +) + +var _ providerIface = (*GoNameProvider)(nil) + +// GoNameProvider resolves json property names to go struct field names following the same rules as +// the standard library's [encoding/json] package. +// +// Contrary to [NameProvider], it considers exported fields without a json tag, and promotes fields +// from anonymous embedded struct types. +// +// Rules (aligned with encoding/json): +// +// - unexported fields are ignored; +// - a field tagged `json:"-"` is ignored; +// - a field tagged `json:"-,"` is kept under the json name "-" (stdlib quirk); +// - a field tagged `json:""` or with no json tag at all keeps its Go name as json name; +// - anonymous struct fields without an explicit json tag have their fields +// promoted into the parent, following breadth-first depth rules: +// a shallower field wins over a deeper one; at equal depth, a conflict +// discards all conflicting fields unless exactly one has an explicit json tag. +// +// This type is safe for concurrent use. +type GoNameProvider struct { + lock sync.Mutex + index map[reflect.Type]nameIndex +} + +// NewGoNameProvider creates a new [GoNameProvider]. +func NewGoNameProvider() *GoNameProvider { + return &GoNameProvider{ + index: make(map[reflect.Type]nameIndex), + } +} + +// GetJSONNames gets all the json property names for a type. +func (n *GoNameProvider) GetJSONNames(subject any) []string { + n.lock.Lock() + defer n.lock.Unlock() + + tpe := reflect.Indirect(reflect.ValueOf(subject)).Type() + names := n.nameIndexFor(tpe) + + res := make([]string, 0, len(names.jsonNames)) + for k := range names.jsonNames { + res = append(res, k) + } + + return res +} + +// GetJSONName gets the json name for a go property name. +func (n *GoNameProvider) GetJSONName(subject any, name string) (string, bool) { + tpe := reflect.Indirect(reflect.ValueOf(subject)).Type() + + return n.GetJSONNameForType(tpe, name) +} + +// GetJSONNameForType gets the json name for a go property name on a given type. +func (n *GoNameProvider) GetJSONNameForType(tpe reflect.Type, name string) (string, bool) { + n.lock.Lock() + defer n.lock.Unlock() + + names := n.nameIndexFor(tpe) + nme, ok := names.goNames[name] + + return nme, ok +} + +// GetGoName gets the go name for a json property name. +func (n *GoNameProvider) GetGoName(subject any, name string) (string, bool) { + tpe := reflect.Indirect(reflect.ValueOf(subject)).Type() + + return n.GetGoNameForType(tpe, name) +} + +// GetGoNameForType gets the go name for a given type for a json property name. +func (n *GoNameProvider) GetGoNameForType(tpe reflect.Type, name string) (string, bool) { + n.lock.Lock() + defer n.lock.Unlock() + + names := n.nameIndexFor(tpe) + nme, ok := names.jsonNames[name] + + return nme, ok +} + +func (n *GoNameProvider) nameIndexFor(tpe reflect.Type) nameIndex { + if names, ok := n.index[tpe]; ok { + return names + } + + names := buildGoNameIndex(tpe) + n.index[tpe] = names + + return names +} + +// fieldEntry captures a candidate field discovered while walking a struct along with the +// indirection path from the root type (used to resolve conflicts by depth in the same way +// encoding/json does). +type fieldEntry struct { + goName string + jsonName string + index []int + tagged bool +} + +func buildGoNameIndex(tpe reflect.Type) nameIndex { + fields := collectGoFields(tpe) + + idx := make(map[string]string, len(fields)) + reverseIdx := make(map[string]string, len(fields)) + for _, f := range fields { + idx[f.jsonName] = f.goName + reverseIdx[f.goName] = f.jsonName + } + + return nameIndex{jsonNames: idx, goNames: reverseIdx} +} + +// collectGoFields walks tpe breadth-first along anonymous struct fields, +// reproducing the field selection performed by encoding/json.typeFields. +// +//nolint:gocognit // everything is inlined to help the compiler determine what escapes and what doesn't +func collectGoFields(tpe reflect.Type) []fieldEntry { + if tpe.Kind() != reflect.Struct { + return nil + } + + type queued struct { + typ reflect.Type + index []int + } + + current := []queued{} + next := []queued{{typ: tpe}} + visited := map[reflect.Type]bool{tpe: true} + + var ( + candidates []fieldEntry + count = map[string]int{} + nextCount = map[string]int{} + ) + + for len(next) > 0 { + current, next = next, current[:0] + count, nextCount = nextCount, count + for k := range nextCount { + delete(nextCount, k) + } + + for _, q := range current { + for i := range q.typ.NumField() { + sf := q.typ.Field(i) + + if sf.Anonymous { + ft := sf.Type + if ft.Kind() == reflect.Pointer { + ft = ft.Elem() + } + if !sf.IsExported() && ft.Kind() != reflect.Struct { + continue + } + } else if !sf.IsExported() { + continue + } + + tag := sf.Tag.Get("json") + if tag == "-" { + continue + } + jsonName, _ := parseJSONTag(tag) + tagged := jsonName != "" + + ft := sf.Type + if ft.Kind() == reflect.Pointer { + ft = ft.Elem() + } + + if sf.Anonymous && ft.Kind() == reflect.Struct && !tagged { + if visited[ft] { + continue + } + visited[ft] = true + + index := make([]int, len(q.index)+1) + copy(index, q.index) + index[len(q.index)] = i + next = append(next, queued{typ: ft, index: index}) + + continue + } + + name := jsonName + if name == "" { + name = sf.Name + } + + index := make([]int, len(q.index)+1) + copy(index, q.index) + index[len(q.index)] = i + + candidates = append(candidates, fieldEntry{ + goName: sf.Name, + jsonName: name, + index: index, + tagged: tagged, + }) + nextCount[name]++ + } + } + } + + return dominantFields(candidates) +} + +// dominantFields applies the Go encoding/json conflict resolution rules: at each JSON name, the +// shallowest field wins; at equal depth, a uniquely tagged candidate wins; otherwise all candidates +// for that name are dropped. +func dominantFields(candidates []fieldEntry) []fieldEntry { + byName := make(map[string][]fieldEntry, len(candidates)) + for _, c := range candidates { + byName[c.jsonName] = append(byName[c.jsonName], c) + } + + out := make([]fieldEntry, 0, len(byName)) + for _, group := range byName { + if len(group) == 1 { + out = append(out, group[0]) + + continue + } + + minDepth := len(group[0].index) + for _, c := range group[1:] { + if len(c.index) < minDepth { + minDepth = len(c.index) + } + } + + var shallow []fieldEntry + for _, c := range group { + if len(c.index) == minDepth { + shallow = append(shallow, c) + } + } + + if len(shallow) == 1 { + out = append(out, shallow[0]) + + continue + } + + var tagged []fieldEntry + for _, c := range shallow { + if c.tagged { + tagged = append(tagged, c) + } + } + if len(tagged) == 1 { + out = append(out, tagged[0]) + } + } + + return out +} + +// parseJSONTag returns the name component of a json struct tag and whether it carried any non-name +// option (kept for future-proofing, e.g. "omitempty"). +func parseJSONTag(tag string) (string, string) { + if tag == "" { + return "", "" + } + if before, after, ok := strings.Cut(tag, ","); ok { + return before, after + } + + return tag, "" +} diff --git a/jsonname/go_name_provider_test.go b/jsonname/go_name_provider_test.go new file mode 100644 index 0000000..5c79b91 --- /dev/null +++ b/jsonname/go_name_provider_test.go @@ -0,0 +1,318 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package jsonname + +import ( + "encoding/json" + "reflect" + "sort" + "testing" + + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + +type testAltEmbedded struct { + Nested string `json:"nested"` +} + +type testAltDeep struct { + Deep string `json:"deep"` +} + +type testAltMiddle struct { + testAltDeep + + Middle string `json:"middle"` +} + +// testAltStruct exercises the stdlib-aligned field discovery rules: +// - Name: explicitly tagged +// - NotTheSame: tagged with a different json name +// - Ignored: fully excluded via `json:"-"` +// - DashField: stdlib quirk, literally named "-" in json +// - Untagged: empty name in tag → keeps Go name +// - Optional: options-only tag → keeps Go name +// - NoTag: no tag at all → keeps Go name +// - unexported: excluded +// - testAltEmbedded: fields promoted to the parent +// - testAltMiddle: embedded struct itself embedding another → transitively promoted +type testAltStruct struct { + testAltEmbedded + testAltMiddle + + Name string `json:"name"` + NotTheSame int64 `json:"plain"` + Ignored string `json:"-"` + DashField string `json:"-,"` //nolint:staticcheck // deliberate: exercise stdlib "-," quirk + Untagged string `json:""` //nolint:tagliatelle // that's precisely the point of this test to check the uppercase field + Optional string `json:",omitempty"` //nolint:tagliatelle // that's precisely the point of this test to check the uppercase field + NoTag string + unexported string //nolint:unused // exercised to confirm it is filtered out +} + +// testAltShadow verifies the depth-based conflict resolution: the outer field must win over one +// promoted from an embedded type. +type testAltShadow struct { + testAltEmbedded + + Nested string `json:"nested"` +} + +func TestGoNameProvider(t *testing.T) { + provider := NewGoNameProvider() + obj := testAltStruct{} + tpe := reflect.TypeFor[testAltStruct]() + ptr := &obj + + t.Run("GetGoName resolves tagged fields", func(t *testing.T) { + for _, tc := range []struct { + jsonName string + goName string + }{ + {"name", "Name"}, + {"plain", "NotTheSame"}, + {"-", "DashField"}, // stdlib `json:"-,"` quirk + {"Untagged", "Untagged"}, + {"Optional", "Optional"}, + {"NoTag", "NoTag"}, + {"nested", "Nested"}, + {"middle", "Middle"}, + {"deep", "Deep"}, + } { + nm, ok := provider.GetGoName(obj, tc.jsonName) + assert.TrueT(t, ok, "expected json name %q to resolve", tc.jsonName) + assert.EqualT(t, tc.goName, nm) + } + }) + + t.Run("GetGoName rejects excluded or unknown names", func(t *testing.T) { + for _, bad := range []string{"ignored", "Ignored", "unexported", "doesNotExist"} { + nm, ok := provider.GetGoName(obj, bad) + assert.FalseT(t, ok, "did not expect %q to resolve", bad) + assert.Empty(t, nm) + } + }) + + t.Run("GetGoNameForType mirrors GetGoName", func(t *testing.T) { + nm, ok := provider.GetGoNameForType(tpe, "plain") + assert.TrueT(t, ok) + assert.EqualT(t, "NotTheSame", nm) + + _, ok = provider.GetGoNameForType(tpe, "doesNotExist") + assert.FalseT(t, ok) + }) + + t.Run("GetGoName accepts pointer subjects", func(t *testing.T) { + nm, ok := provider.GetGoName(ptr, "name") + assert.TrueT(t, ok) + assert.EqualT(t, "Name", nm) + + nm, ok = provider.GetGoName(ptr, "nested") + assert.TrueT(t, ok) + assert.EqualT(t, "Nested", nm) + }) + + t.Run("GetJSONName is the inverse mapping", func(t *testing.T) { + for _, tc := range []struct { + goName string + jsonName string + }{ + {"Name", "name"}, + {"NotTheSame", "plain"}, + {"DashField", "-"}, + {"Untagged", "Untagged"}, + {"Optional", "Optional"}, + {"NoTag", "NoTag"}, + {"Nested", "nested"}, + {"Middle", "middle"}, + {"Deep", "deep"}, + } { + nm, ok := provider.GetJSONName(obj, tc.goName) + assert.TrueT(t, ok, "expected go name %q to resolve", tc.goName) + assert.EqualT(t, tc.jsonName, nm) + } + + _, ok := provider.GetJSONName(obj, "Ignored") + assert.FalseT(t, ok) + + _, ok = provider.GetJSONNameForType(tpe, "DoesNotExist") + assert.FalseT(t, ok) + }) + + t.Run("GetJSONNames lists every discoverable field exactly once", func(t *testing.T) { + names := provider.GetJSONNames(ptr) + sort.Strings(names) + assert.Equal(t, []string{ + "-", + "NoTag", + "Optional", + "Untagged", + "deep", + "middle", + "name", + "nested", + "plain", + }, names) + }) + + t.Run("index caches per type", func(t *testing.T) { + // Re-query to confirm no duplicate entries are created on repeat access. + _, _ = provider.GetGoName(obj, "name") + _, _ = provider.GetGoName(ptr, "name") + assert.Len(t, provider.index, 1) + }) +} + +// TestGoNameProvider_ShadowingMatchesStdlib pins our field selection to the behavior of +// encoding/json for shadowed promoted fields. +func TestGoNameProvider_ShadowingMatchesStdlib(t *testing.T) { + provider := NewGoNameProvider() + payload := `{"nested":"outer"}` + + var s testAltShadow + require.NoError(t, json.Unmarshal([]byte(payload), &s)) + assert.Equal(t, "outer", s.Nested) + assert.Empty(t, s.testAltEmbedded.Nested) + + goName, ok := provider.GetGoName(s, "nested") + require.True(t, ok) + // The outer field wins, exactly like encoding/json would pick s.Nested. + assert.Equal(t, "Nested", goName) + + names := provider.GetJSONNames(s) + assert.Len(t, names, 1) +} + +// TestGoNameProvider_ImplementsInterface is a compile-time-ish guard that both providers agree on +// the core lookup shape expected by consumers. +func TestGoNameProvider_ImplementsInterface(t *testing.T) { + var p providerIface = NewGoNameProvider() + _, ok := p.GetGoName(testAltStruct{}, "name") + assert.True(t, ok) +} + +// Fixtures for the embedded-type promotion scenarios. + +type testAltInner struct { + Foo string `json:"foo"` + Bar string +} + +type testAltPromoted struct { + testAltInner + + Baz string `json:"baz"` +} + +type testAltTaggedEmbed struct { + testAltInner `json:"inner"` + + Baz string `json:"baz"` +} + +type testAltPtrEmbed struct { + *testAltInner + + Baz string `json:"baz"` +} + +type testAltUnexportedEmbed struct { + testAltInner // exported type, will still promote + + inner testAltInner //nolint:unused // regular unexported field, must be ignored +} + +// TestGoNameProvider_EmbeddedPromotion validates how the provider resolves fields coming from an +// exported embedded type, mirroring encoding/json. +func TestGoNameProvider_EmbeddedPromotion(t *testing.T) { + t.Run("untagged embedded struct promotes its fields", func(t *testing.T) { + provider := NewGoNameProvider() + obj := testAltPromoted{} + + for _, tc := range []struct { + jsonName string + goName string + }{ + {"foo", "Foo"}, // promoted, tagged on Inner + {"Bar", "Bar"}, // promoted, untagged on Inner -> Go name kept + {"baz", "Baz"}, // declared on Outer + } { + nm, ok := provider.GetGoName(obj, tc.jsonName) + assert.TrueT(t, ok, "expected %q to resolve", tc.jsonName) + assert.EqualT(t, tc.goName, nm) + } + + // "Inner" must NOT appear as its own json name: its fields were promoted. + _, ok := provider.GetJSONName(obj, "testAltInner") + assert.False(t, ok) + + names := provider.GetJSONNames(obj) + sort.Strings(names) + assert.Equal(t, []string{"Bar", "baz", "foo"}, names) + }) + + t.Run("tagged embedded struct is treated as a regular named field", func(t *testing.T) { + provider := NewGoNameProvider() + obj := testAltTaggedEmbed{} + + nm, ok := provider.GetGoName(obj, "inner") + assert.TrueT(t, ok) + assert.EqualT(t, "testAltInner", nm) + + // With the tag in place, Inner's fields are NOT promoted. + _, ok = provider.GetGoName(obj, "foo") + assert.False(t, ok) + _, ok = provider.GetGoName(obj, "Bar") + assert.False(t, ok) + + names := provider.GetJSONNames(obj) + sort.Strings(names) + assert.Equal(t, []string{"baz", "inner"}, names) + }) + + t.Run("pointer-to-struct embedded is promoted like its elem", func(t *testing.T) { + provider := NewGoNameProvider() + obj := testAltPtrEmbed{} + + nm, ok := provider.GetGoName(obj, "foo") + assert.TrueT(t, ok) + assert.EqualT(t, "Foo", nm) + + names := provider.GetJSONNames(obj) + sort.Strings(names) + assert.Equal(t, []string{"Bar", "baz", "foo"}, names) + }) + + t.Run("regular unexported field alongside promotion does not leak", func(t *testing.T) { + provider := NewGoNameProvider() + obj := testAltUnexportedEmbed{} + + // Promotion still works for the exported embedded type. + nm, ok := provider.GetGoName(obj, "foo") + assert.TrueT(t, ok) + assert.EqualT(t, "Foo", nm) + + // The regular unexported "inner" field must be invisible. + _, ok = provider.GetGoName(obj, "inner") + assert.False(t, ok) + }) + + t.Run("agrees with encoding/json on roundtrip", func(t *testing.T) { + provider := NewGoNameProvider() + payload := `{"foo":"f","Bar":"b","baz":"z"}` + + var stdVal testAltPromoted + require.NoError(t, json.Unmarshal([]byte(payload), &stdVal)) + assert.Equal(t, "f", stdVal.Foo) + assert.Equal(t, "b", stdVal.Bar) + assert.Equal(t, "z", stdVal.Baz) + + // For every json key encoding/json accepted, the provider must resolve it too. + for _, key := range []string{"foo", "Bar", "baz"} { + _, ok := provider.GetGoName(stdVal, key) + assert.TrueT(t, ok, "provider should resolve %q like encoding/json", key) + } + }) +} diff --git a/jsonname/ifaces.go b/jsonname/ifaces.go new file mode 100644 index 0000000..64871f0 --- /dev/null +++ b/jsonname/ifaces.go @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package jsonname + +import "reflect" + +// providerIface is an unexported compile-time contract that every name provider in this package is +// expected to satisfy. +// +// It mirrors the interface declared by the main consumer of this module: +// [github.com/go-openapi/jsonpointer.NameProvider]. +type providerIface interface { + GetGoName(subject any, name string) (string, bool) + GetGoNameForType(tpe reflect.Type, name string) (string, bool) +} diff --git a/jsonname/name_provider.go b/jsonname/name_provider.go new file mode 100644 index 0000000..1bec240 --- /dev/null +++ b/jsonname/name_provider.go @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package jsonname + +import ( + "reflect" + "strings" + "sync" +) + +// DefaultJSONNameProvider is the default cache for types. +var DefaultJSONNameProvider = NewNameProvider() //nolint:gochecknoglobals // default settings, for backward compatible package-level settings + +var _ providerIface = (*NameProvider)(nil) + +// NameProvider represents an object capable of translating from go property names to json property +// names. +// +// This type is thread-safe. +// +// See [github.com/go-openapi/jsonpointer.Pointer] for an example. +type NameProvider struct { + lock *sync.Mutex + index map[reflect.Type]nameIndex +} + +type nameIndex struct { + jsonNames map[string]string + goNames map[string]string +} + +// NewNameProvider creates a new name provider. +func NewNameProvider() *NameProvider { + return &NameProvider{ + lock: &sync.Mutex{}, + index: make(map[reflect.Type]nameIndex), + } +} + +func buildnameIndex(tpe reflect.Type, idx, reverseIdx map[string]string) { + for i := range tpe.NumField() { + targetDes := tpe.Field(i) + + if targetDes.PkgPath != "" { // unexported + continue + } + + if targetDes.Anonymous { // walk embedded structures tree down first + buildnameIndex(targetDes.Type, idx, reverseIdx) + continue + } + + if tag := targetDes.Tag.Get("json"); tag != "" { + + parts := strings.Split(tag, ",") + if len(parts) == 0 { + continue + } + + nm := parts[0] + if nm == "-" { + continue + } + if nm == "" { // empty string means we want to use the Go name + nm = targetDes.Name + } + + idx[nm] = targetDes.Name + reverseIdx[targetDes.Name] = nm + } + } +} + +func newNameIndex(tpe reflect.Type) nameIndex { + idx := make(map[string]string, tpe.NumField()) + reverseIdx := make(map[string]string, tpe.NumField()) + + buildnameIndex(tpe, idx, reverseIdx) + return nameIndex{jsonNames: idx, goNames: reverseIdx} +} + +// GetJSONNames gets all the json property names for a type. +func (n *NameProvider) GetJSONNames(subject any) []string { + n.lock.Lock() + defer n.lock.Unlock() + tpe := reflect.Indirect(reflect.ValueOf(subject)).Type() + names, ok := n.index[tpe] + if !ok { + names = n.makeNameIndex(tpe) + } + + res := make([]string, 0, len(names.jsonNames)) + for k := range names.jsonNames { + res = append(res, k) + } + return res +} + +// GetJSONName gets the json name for a go property name. +func (n *NameProvider) GetJSONName(subject any, name string) (string, bool) { + tpe := reflect.Indirect(reflect.ValueOf(subject)).Type() + return n.GetJSONNameForType(tpe, name) +} + +// GetJSONNameForType gets the json name for a go property name on a given type. +func (n *NameProvider) GetJSONNameForType(tpe reflect.Type, name string) (string, bool) { + n.lock.Lock() + defer n.lock.Unlock() + names, ok := n.index[tpe] + if !ok { + names = n.makeNameIndex(tpe) + } + nme, ok := names.goNames[name] + return nme, ok +} + +// GetGoName gets the go name for a json property name. +func (n *NameProvider) GetGoName(subject any, name string) (string, bool) { + tpe := reflect.Indirect(reflect.ValueOf(subject)).Type() + return n.GetGoNameForType(tpe, name) +} + +// GetGoNameForType gets the go name for a given type for a json property name. +func (n *NameProvider) GetGoNameForType(tpe reflect.Type, name string) (string, bool) { + n.lock.Lock() + defer n.lock.Unlock() + names, ok := n.index[tpe] + if !ok { + names = n.makeNameIndex(tpe) + } + nme, ok := names.jsonNames[name] + return nme, ok +} + +func (n *NameProvider) makeNameIndex(tpe reflect.Type) nameIndex { + names := newNameIndex(tpe) + n.index[tpe] = names + return names +} diff --git a/jsonname/name_provider_test.go b/jsonname/name_provider_test.go new file mode 100644 index 0000000..8951049 --- /dev/null +++ b/jsonname/name_provider_test.go @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package jsonname + +import ( + "reflect" + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + +type testNameStruct struct { + Name string `json:"name"` + NotTheSame int64 `json:"plain"` + Ignored string `json:"-"` +} + +func TestNameProvider(t *testing.T) { + provider := NewNameProvider() + + obj := testNameStruct{} + + nm, ok := provider.GetGoName(obj, "name") + assert.TrueT(t, ok) + assert.EqualT(t, "Name", nm) + + nm, ok = provider.GetGoName(obj, "plain") + assert.TrueT(t, ok) + assert.EqualT(t, "NotTheSame", nm) + + nm, ok = provider.GetGoName(obj, "doesNotExist") + assert.FalseT(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetGoName(obj, "ignored") + assert.FalseT(t, ok) + assert.Empty(t, nm) + + tpe := reflect.TypeFor[testNameStruct]() + nm, ok = provider.GetGoNameForType(tpe, "name") + assert.TrueT(t, ok) + assert.EqualT(t, "Name", nm) + + nm, ok = provider.GetGoNameForType(tpe, "plain") + assert.TrueT(t, ok) + assert.EqualT(t, "NotTheSame", nm) + + nm, ok = provider.GetGoNameForType(tpe, "doesNotExist") + assert.FalseT(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetGoNameForType(tpe, "ignored") + assert.FalseT(t, ok) + assert.Empty(t, nm) + + ptr := &obj + nm, ok = provider.GetGoName(ptr, "name") + assert.TrueT(t, ok) + assert.EqualT(t, "Name", nm) + + nm, ok = provider.GetGoName(ptr, "plain") + assert.TrueT(t, ok) + assert.EqualT(t, "NotTheSame", nm) + + nm, ok = provider.GetGoName(ptr, "doesNotExist") + assert.FalseT(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetGoName(ptr, "ignored") + assert.FalseT(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetJSONName(obj, "Name") + assert.TrueT(t, ok) + assert.EqualT(t, "name", nm) + + nm, ok = provider.GetJSONName(obj, "NotTheSame") + assert.TrueT(t, ok) + assert.EqualT(t, "plain", nm) + + nm, ok = provider.GetJSONName(obj, "DoesNotExist") + assert.FalseT(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetJSONName(obj, "Ignored") + assert.FalseT(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetJSONNameForType(tpe, "Name") + assert.TrueT(t, ok) + assert.EqualT(t, "name", nm) + + nm, ok = provider.GetJSONNameForType(tpe, "NotTheSame") + assert.TrueT(t, ok) + assert.EqualT(t, "plain", nm) + + nm, ok = provider.GetJSONNameForType(tpe, "doesNotExist") + assert.FalseT(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetJSONNameForType(tpe, "Ignored") + assert.FalseT(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetJSONName(ptr, "Name") + assert.TrueT(t, ok) + assert.EqualT(t, "name", nm) + + nm, ok = provider.GetJSONName(ptr, "NotTheSame") + assert.TrueT(t, ok) + assert.EqualT(t, "plain", nm) + + nm, ok = provider.GetJSONName(ptr, "doesNotExist") + assert.FalseT(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetJSONName(ptr, "Ignored") + assert.FalseT(t, ok) + assert.Empty(t, nm) + + nms := provider.GetJSONNames(ptr) + assert.Len(t, nms, 2) + + assert.Len(t, provider.index, 1) +} diff --git a/options.go b/options.go index d52caab..223c1e5 100644 --- a/options.go +++ b/options.go @@ -6,7 +6,7 @@ package jsonpointer import ( "sync" - "github.com/go-openapi/swag/jsonname" + "github.com/go-openapi/jsonpointer/jsonname" ) // Option to tune the behavior of a JSON [Pointer]. @@ -25,9 +25,9 @@ var ( // // By default, the default provider is [jsonname.DefaultJSONNameProvider]. // -// It is safe to call concurrently with [Pointer.Get], [Pointer.Set], -// [GetForToken] and [SetForToken]. The typical usage is to call it once -// at initialization time. +// It is safe to call concurrently with [Pointer.Get], [Pointer.Set], [GetForToken] and +// [SetForToken]. +// The typical usage is to call it once at initialization time. // // A nil provider is ignored. func SetDefaultNameProvider(provider NameProvider) { @@ -41,16 +41,15 @@ func SetDefaultNameProvider(provider NameProvider) { defaultOptions.provider = provider } -// UseGoNameProvider sets the [NameProvider] as a package-level default -// to the alternative provider [jsonname.GoNameProvider], that covers a few areas -// not supported by the default name provider. +// UseGoNameProvider sets the [NameProvider] as a package-level default to the alternative provider +// [jsonname.GoNameProvider], that covers a few areas not supported by the default name provider. // // This implementation supports untagged exported fields and embedded types in go struct. // It follows strictly the behavior of the JSON standard library regarding field naming conventions. // -// It is safe to call concurrently with [Pointer.Get], [Pointer.Set], -// [GetForToken] and [SetForToken]. The typical usage is to call it once -// at initialization time. +// It is safe to call concurrently with [Pointer.Get], [Pointer.Set], [GetForToken] and +// [SetForToken]. +// The typical usage is to call it once at initialization time. func UseGoNameProvider() { SetDefaultNameProvider(jsonname.NewGoNameProvider()) } diff --git a/options_test.go b/options_test.go index 4b397fb..72b3f5b 100644 --- a/options_test.go +++ b/options_test.go @@ -12,9 +12,10 @@ import ( "github.com/go-openapi/testify/v2/require" ) -// stubNameProvider is a NameProvider that maps JSON names to Go field names -// via a fixed dictionary. It lets tests observe which provider was used by -// the resolver without relying on the default reflection-based behavior. +// stubNameProvider is a NameProvider that maps JSON names to Go field names via a fixed dictionary. +// +// It lets tests observe which provider was used by the resolver without relying on the default +// reflection-based behavior. type stubNameProvider struct { mu sync.Mutex mapping map[string]string @@ -46,8 +47,8 @@ func (s *stubNameProvider) record(name string, forType bool) { } type optionStruct struct { - // intentional: the JSON name "renamed" is deliberately not a valid - // struct tag so that only a custom provider can resolve it. + // intentional: the JSON name "renamed" is deliberately not a valid struct tag so that only a + // custom provider can resolve it. Field string } @@ -116,8 +117,8 @@ func TestUseGoNameProvider_resolvesUntaggedFields(t *testing.T) { original := DefaultNameProvider() t.Cleanup(func() { SetDefaultNameProvider(original) }) - // optionStruct.Field has no json tag; the default provider can't resolve it, - // but the Go-name provider follows encoding/json conventions and can. + // optionStruct.Field has no json tag; the default provider can't resolve it, but the Go-name + // provider follows encoding/json conventions and can. doc := optionStruct{Field: "hello"} p, err := New("/Field") diff --git a/pointer.go b/pointer.go index 2369c18..05fc863 100644 --- a/pointer.go +++ b/pointer.go @@ -34,7 +34,8 @@ const ( // // For struct s resolved by reflection, key mappings honor the conventional struct tag `json`. // -// Fields that do not specify a `json` tag, or specify an empty one, or are tagged as `json:"-"` are ignored. +// Fields that do not specify a `json` tag, or specify an empty one, or are tagged as `json:"-"` are +// ignored. // // # Limitations // @@ -61,23 +62,24 @@ func (p *Pointer) Get(document any, opts ...Option) (any, reflect.Kind, error) { return p.get(document, o.provider) } -// Set uses the pointer to set a value from a data type -// that represent a JSON document. +// Set uses the pointer to set a value from a data type that represent a JSON document. // // # Mutation contract // -// Set mutates the provided document in place whenever Go's type system allows -// it: when document is a map, a pointer, or when the targeted value is reached -// through an addressable ancestor (e.g. a struct field traversed via a pointer, -// a slice element). Callers that rely on this in-place behavior may continue -// to ignore the returned document. +// Set mutates the provided document in place whenever Go's type system allows it: when document is +// a map, a pointer, or when the targeted value is reached through an addressable ancestor (e.g. a +// struct field traversed via a pointer, a slice element). +// +// Callers that rely on this in-place behavior may continue to ignore the returned document. // // The returned document is only load-bearing when Set cannot mutate in place. -// This happens in one specific case: appending to a top-level slice passed by -// value (e.g. document of type []T rather than *[]T) via the RFC 6901 "-" -// terminal token. reflect.Append produces a new slice header that the library -// cannot rebind into the caller's variable; the updated document is returned -// instead. Pass *[]T if you want in-place rebind for that case as well. +// +// This happens in one specific case: appending to a top-level slice passed by value (e.g. document +// of type []T rather than *[]T) via the RFC 6901 "-" terminal token. reflect.Append produces a new +// slice header that the library cannot rebind into the caller's variable; the updated document is +// returned instead. +// +// Pass *[]T if you want in-place rebind for that case as well. // // See [ErrDashToken] for the semantics of the "-" token. func (p *Pointer) Set(document any, value any, opts ...Option) (any, error) { @@ -112,23 +114,23 @@ func (p *Pointer) String() string { return pointerSeparator + strings.Join(p.referenceTokens, pointerSeparator) } -// Offset returns the byte offset, in the raw JSON text of document, of the -// location referenced by this pointer's terminal token. +// Offset returns the byte offset, in the raw JSON text of document, of the location referenced by +// this pointer's terminal token. +// +// Unlike [Pointer.Get] and [Pointer.Set], which operate on a decoded Go value, Offset operates +// directly on the textual JSON source. // -// Unlike [Pointer.Get] and [Pointer.Set], which operate on a decoded Go value, -// Offset operates directly on the textual JSON source. It drives an -// [encoding/json.Decoder] over the string and stops at the terminal token, -// returning the position at which the decoder was about to read that token. +// It drives an [encoding/json.Decoder] over the string and stops at the terminal token, returning +// the position at which the decoder was about to read that token. // -// It is primarily intended for tooling that needs to map a pointer back to a -// region of the original source: reporting line/column for validation or -// parse diagnostics, extracting a sub-document by slicing the raw bytes, or -// highlighting the referenced span in an editor. +// It is primarily intended for tooling that needs to map a pointer back to a region of the original +// source: reporting line/column for validation or parse diagnostics, extracting a sub-document by +// slicing the raw bytes, or highlighting the referenced span in an editor. // // # Offset semantics // -// The meaning of the returned offset depends on whether the terminal token -// addresses an object property or an array element: +// The meaning of the returned offset depends on whether the terminal token addresses an object +// property or an array element: // // - Object property: the offset points to the first byte of the key (its // opening quote character), not to the associated value. For example, @@ -183,16 +185,15 @@ func (p *Pointer) Offset(document string) (int64, error) { return skipJSONSeparator(document, offset), nil } -// skipJSONSeparator advances offset past trailing JSON whitespace and at most -// one value separator (comma) in document, so the result points at the first -// byte of the next JSON token. +// skipJSONSeparator advances offset past trailing JSON whitespace and at most one value separator +// (comma) in document, so the result points at the first byte of the next JSON token. // -// The streaming decoder's InputOffset sits right after the most recently -// consumed token, which between values is the comma (or whitespace) — not -// the following token. Normalizing here keeps Offset's contract uniform: -// for both object keys and array elements, and regardless of position within -// the parent container, the returned offset always points at the first byte -// of the addressed token. +// The streaming decoder's InputOffset sits right after the most recently consumed token, which +// between values is the comma (or whitespace) — not the following token. +// +// Normalizing here keeps Offset's contract uniform: for both object keys and array elements, and +// regardless of position within the parent container, the returned offset always points at the +// first byte of the addressed token. func skipJSONSeparator(document string, offset int64) int64 { n := int64(len(document)) for offset < n && isJSONWhitespace(document[offset]) { @@ -279,14 +280,13 @@ func (p *Pointer) set(node, data any, nameProvider NameProvider) (any, error) { return p.setAt(node, p.referenceTokens, data, nameProvider) } -// setAt recursively walks the token list, setting the data at the terminal -// token and rebinding any new child reference (e.g. a slice header returned -// by an "-" append) into its parent on the way back up. +// setAt recursively walks the token list, setting the data at the terminal token and rebinding any +// new child reference (e.g. a slice header returned by an "-" append) into its parent on the way +// back up. // -// Returning the (possibly new) node at each level is what makes append work -// at any depth without requiring the caller to pass a pointer to the -// containing slice: the new slice header propagates up and each parent -// rebinds it via the appropriate kind-specific setter. +// Returning the (possibly new) node at each level is what makes append work at any depth without +// requiring the caller to pass a pointer to the containing slice: the new slice header propagates +// up and each parent rebinds it via the appropriate kind-specific setter. func (p *Pointer) setAt(node any, tokens []string, data any, nameProvider NameProvider) (any, error) { decodedToken := Unescape(tokens[0]) @@ -309,15 +309,14 @@ func (p *Pointer) setAt(node any, tokens []string, data any, nameProvider NamePr // rebindChild writes newChild back into node at decodedToken. // -// For cases where the child was already mutated in place (pointer aliasing, -// addressable slice elements) the rebind is a safe no-op. For cases where -// the child was returned by value (map entries holding a slice, slices -// reached through a non-addressable ancestor), the rebind propagates the -// new value into the parent. +// For cases where the child was already mutated in place (pointer aliasing, addressable slice +// elements) the rebind is a safe no-op. +// +// For cases where the child was returned by value (map entries holding a slice, slices reached +// through a non-addressable ancestor), the rebind propagates the new value into the parent. // -// Parents implementing [JSONPointable] are left alone: they took ownership -// of the child via JSONLookup and did not opt into a JSONSet-based rebind -// on intermediate tokens. +// Parents implementing [JSONPointable] are left alone: they took ownership of the child via +// JSONLookup and did not opt into a JSONSet-based rebind on intermediate tokens. func rebindChild(node any, decodedToken string, newChild any, nameProvider NameProvider) (any, error) { if _, ok := node.(JSONPointable); ok { return node, nil @@ -362,9 +361,9 @@ func rebindChild(node any, decodedToken string, newChild any, nameProvider NameP } } -// assignReflectValue assigns src into dst, unwrapping a pointer when dst -// expects the pointee type. This tolerates the pointer-wrapping performed -// by [typeFromValue] for addressable fields. +// assignReflectValue assigns src into dst, unwrapping a pointer when dst expects the pointee type. +// +// This tolerates the pointer-wrapping performed by [typeFromValue] for addressable fields. func assignReflectValue(dst reflect.Value, src any) { nv := reflect.ValueOf(src) if !nv.IsValid() { @@ -474,8 +473,8 @@ func GetForToken(document any, decodedToken string, opts ...Option) (any, reflec // SetForToken sets a value for a json pointer token 1 level deep. // -// See [Pointer.Set] for the mutation contract, in particular the handling of -// the RFC 6901 "-" token on slices. +// See [Pointer.Set] for the mutation contract, in particular the handling of the RFC 6901 "-" token +// on slices. func SetForToken(document any, decodedToken string, value any, opts ...Option) (any, error) { o := optionsWithDefaults(opts) @@ -586,10 +585,10 @@ func setSingleImpl(node, data any, decodedToken string, nameProvider NameProvide case reflect.Slice: if decodedToken == dashToken { - // RFC 6901 §4 / RFC 6902 append semantics: terminal "-" appends - // the value to the slice. We rebind in place when the slice is - // reachable via an addressable ancestor; otherwise we return the - // new slice header for the parent (or the public Set) to rebind. + // RFC 6901 §4 / RFC 6902 append semantics: terminal "-" appends the value to the slice. + // + // We rebind in place when the slice is reachable via an addressable ancestor; otherwise we + // return the new slice header for the parent (or the public Set) to rebind. value := reflect.ValueOf(data) elemType := rValue.Type().Elem() if !value.Type().AssignableTo(elemType) { @@ -650,8 +649,8 @@ func offsetSingleObject(dec *json.Decoder, decodedToken string) (int64, error) { return offset, nil } - // Consume the associated value. Scalars are fully read by a single - // Token() call; composite values must be drained. + // Consume the associated value. + // Scalars are fully read by a single Token() call; composite values must be drained. tk, err = dec.Token() if err != nil { return 0, err @@ -736,10 +735,7 @@ func drainSingle(dec *json.Decoder) error { return nil } -// JSON pointer encoding: -// ~0 => ~ -// ~1 => / -// ... and vice versa +// JSON pointer encoding: ~0 => ~ ~1 => / ... and vice versa. const ( encRefTok0 = `~0` diff --git a/pointer_test.go b/pointer_test.go index 758a4ad..cf39d86 100644 --- a/pointer_test.go +++ b/pointer_test.go @@ -10,7 +10,7 @@ import ( "strconv" "testing" - "github.com/go-openapi/swag/jsonname" + "github.com/go-openapi/jsonpointer/jsonname" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) @@ -837,8 +837,8 @@ func TestSetNode(t *testing.T) { }) t.Run("with nil traversal panic", func(t *testing.T) { - // This test exposes the panic that occurs when trying to set a value - // through a path that contains nil intermediate values + // This test exposes the panic that occurs when trying to set a value through a path that contains + // nil intermediate values. data := map[string]any{ "level1": map[string]any{ "level2": map[string]any{ @@ -853,8 +853,7 @@ func TestSetNode(t *testing.T) { // This should return an error, not panic _, err = ptr.Set(data, "test-value") - // The library should handle this gracefully and return an error - // instead of panicking + // The library should handle this gracefully and return an error instead of panicking. require.Error(t, err, "Setting value through nil intermediate path should return an error, not panic") }) @@ -896,19 +895,19 @@ func TestSetNode(t *testing.T) { }) t.Run("with path creation through nil intermediate", func(t *testing.T) { - // Test case that simulates path creation functions encountering nil - // This happens when tools try to create missing paths but encounter nil intermediate values + // Test case that simulates path creation functions encountering nil This happens when tools try + // to create missing paths but encounter nil intermediate values. data := map[string]any{ "spec": map[string]any{ "template": nil, // This blocks path creation attempts }, } - // Attempting to create a path like /spec/template/metadata/labels should fail gracefully + // Attempting to create a path like /spec/template/metadata/labels should fail gracefully. ptr, err := New("/spec/template/metadata") require.NoError(t, err) - // Should return error when trying to set on nil intermediate during path creation + // Should return error when trying to set on nil intermediate during path creation. _, err = ptr.Set(data, map[string]any{"labels": map[string]any{}}) require.Error(t, err, "Setting on nil intermediate during path creation should return error") }) diff --git a/struct_example_test.go b/struct_example_test.go index 665531e..11c00fd 100644 --- a/struct_example_test.go +++ b/struct_example_test.go @@ -106,7 +106,7 @@ func Example_struct() { fmt.Printf("untagged: %v\n", err) } - // output: + // Output: // a: a // b: promoted // c: c