From 7a320447240b7e0118db1cc9b1336248dc83b733 Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Tue, 25 Mar 2025 18:39:10 +0100 Subject: [PATCH 01/15] refact loading, jsonutils, yamlutils utililities Follow-up on refactoring the swag API as more granular packages, and deprecating the vast API currently exposed by swag. All existing methods are maintained and work. Existing configuration using global vars will continue to work at the "swag" level: sub-packages no longer expose global variables. Smaller packages will help maintain this very diverse set of features. This API separates: * loading : file loading utilities * jsonutils, yamlutils: JSON & YAML document conversions Signed-off-by: Frederic BIDON --- jsonname/name_provider.go | 146 +++++++++++++++++++++++++++++++++ jsonname/name_provider_test.go | 137 +++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 jsonname/name_provider.go create mode 100644 jsonname/name_provider_test.go diff --git a/jsonname/name_provider.go b/jsonname/name_provider.go new file mode 100644 index 0000000..aab1529 --- /dev/null +++ b/jsonname/name_provider.go @@ -0,0 +1,146 @@ +// Copyright 2015 go-swagger maintainers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package swag + +import ( + "reflect" + "strings" + "sync" +) + +// DefaultJSONNameProvider the default cache for types +var DefaultJSONNameProvider = NewNameProvider() + +// NameProvider represents an object capable of translating from go property names +// to json property names +// This type is thread-safe. +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 := 0; i < tpe.NumField(); i++ { + 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 { + var idx = make(map[string]string, tpe.NumField()) + var 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 interface{}) []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 interface{}, 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 +} + +func (n *NameProvider) makeNameIndex(tpe reflect.Type) nameIndex { + names := newNameIndex(tpe) + n.index[tpe] = names + return names +} + +// GetGoName gets the go name for a json property name +func (n *NameProvider) GetGoName(subject interface{}, 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 +} diff --git a/jsonname/name_provider_test.go b/jsonname/name_provider_test.go new file mode 100644 index 0000000..0114260 --- /dev/null +++ b/jsonname/name_provider_test.go @@ -0,0 +1,137 @@ +// Copyright 2015 go-swagger maintainers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package swag + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +type testNameStruct struct { + Name string `json:"name"` + NotTheSame int64 `json:"plain"` + Ignored string `json:"-"` +} + +func TestNameProvider(t *testing.T) { + provider := NewNameProvider() + + var obj = testNameStruct{} + + nm, ok := provider.GetGoName(obj, "name") + assert.True(t, ok) + assert.Equal(t, "Name", nm) + + nm, ok = provider.GetGoName(obj, "plain") + assert.True(t, ok) + assert.Equal(t, "NotTheSame", nm) + + nm, ok = provider.GetGoName(obj, "doesNotExist") + assert.False(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetGoName(obj, "ignored") + assert.False(t, ok) + assert.Empty(t, nm) + + tpe := reflect.TypeOf(obj) + nm, ok = provider.GetGoNameForType(tpe, "name") + assert.True(t, ok) + assert.Equal(t, "Name", nm) + + nm, ok = provider.GetGoNameForType(tpe, "plain") + assert.True(t, ok) + assert.Equal(t, "NotTheSame", nm) + + nm, ok = provider.GetGoNameForType(tpe, "doesNotExist") + assert.False(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetGoNameForType(tpe, "ignored") + assert.False(t, ok) + assert.Empty(t, nm) + + ptr := &obj + nm, ok = provider.GetGoName(ptr, "name") + assert.True(t, ok) + assert.Equal(t, "Name", nm) + + nm, ok = provider.GetGoName(ptr, "plain") + assert.True(t, ok) + assert.Equal(t, "NotTheSame", nm) + + nm, ok = provider.GetGoName(ptr, "doesNotExist") + assert.False(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetGoName(ptr, "ignored") + assert.False(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetJSONName(obj, "Name") + assert.True(t, ok) + assert.Equal(t, "name", nm) + + nm, ok = provider.GetJSONName(obj, "NotTheSame") + assert.True(t, ok) + assert.Equal(t, "plain", nm) + + nm, ok = provider.GetJSONName(obj, "DoesNotExist") + assert.False(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetJSONName(obj, "Ignored") + assert.False(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetJSONNameForType(tpe, "Name") + assert.True(t, ok) + assert.Equal(t, "name", nm) + + nm, ok = provider.GetJSONNameForType(tpe, "NotTheSame") + assert.True(t, ok) + assert.Equal(t, "plain", nm) + + nm, ok = provider.GetJSONNameForType(tpe, "doesNotExist") + assert.False(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetJSONNameForType(tpe, "Ignored") + assert.False(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetJSONName(ptr, "Name") + assert.True(t, ok) + assert.Equal(t, "name", nm) + + nm, ok = provider.GetJSONName(ptr, "NotTheSame") + assert.True(t, ok) + assert.Equal(t, "plain", nm) + + nm, ok = provider.GetJSONName(ptr, "doesNotExist") + assert.False(t, ok) + assert.Empty(t, nm) + + nm, ok = provider.GetJSONName(ptr, "Ignored") + assert.False(t, ok) + assert.Empty(t, nm) + + nms := provider.GetJSONNames(ptr) + assert.Len(t, nms, 2) + + assert.Len(t, provider.index, 1) +} From 1e343ce33ba0403b1f5b99cde0c9f54b188d28ca Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Sat, 15 Mar 2025 08:07:00 +0100 Subject: [PATCH 02/15] refact: refactored the package into multiple specialized sub-packages This clarifies the dependencies between the various utilities exposed by this package. Ultimately (not in this PR), we may convert some of the sub-packages into go modules, so as to isolate dependencies and allow users of some of the features not to be polluted by cross-dependencies. * refactored mangling to remove package state Signed-off-by: Frederic BIDON --- jsonname/doc.go | 16 ++++++++++++++++ jsonname/name_provider.go | 9 ++++++--- jsonname/name_provider_test.go | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 jsonname/doc.go diff --git a/jsonname/doc.go b/jsonname/doc.go new file mode 100644 index 0000000..b2e0c80 --- /dev/null +++ b/jsonname/doc.go @@ -0,0 +1,16 @@ +// Copyright 2015 go-swagger maintainers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package jsonname is a provider of json property names from go properties. +package jsonname diff --git a/jsonname/name_provider.go b/jsonname/name_provider.go index aab1529..6f2277a 100644 --- a/jsonname/name_provider.go +++ b/jsonname/name_provider.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package swag +package jsonname import ( "reflect" @@ -20,12 +20,15 @@ import ( "sync" ) -// DefaultJSONNameProvider the default cache for types +// DefaultJSONNameProvider is the default cache for types. var DefaultJSONNameProvider = NewNameProvider() // NameProvider represents an object capable of translating from go property names -// to json 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 diff --git a/jsonname/name_provider_test.go b/jsonname/name_provider_test.go index 0114260..6a93c09 100644 --- a/jsonname/name_provider_test.go +++ b/jsonname/name_provider_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package swag +package jsonname import ( "reflect" From c8ef8a26093a2a803e80680fdd0f36aa472991d1 Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Mon, 17 Mar 2025 17:28:50 +0100 Subject: [PATCH 03/15] split packages as modules This PR transforms the "swag" package into a mono repo, which exposes a collection of independant go modules. The objective is to reduce the footprint of required dependencies. To remain fully backward-compatible, the swag module imports all the newly created (sub) modules, for programs that consume its deprecated API. ci: ci automation, test coverage reporting and dependabot configuration have been adapted to support this new mono-repo structure. Signed-off-by: Frederic BIDON --- jsonname/go.mod | 13 +++++++++++++ jsonname/go.sum | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 jsonname/go.mod create mode 100644 jsonname/go.sum diff --git a/jsonname/go.mod b/jsonname/go.mod new file mode 100644 index 0000000..d1455c7 --- /dev/null +++ b/jsonname/go.mod @@ -0,0 +1,13 @@ +module github.com/go-openapi/swag/jsonname + +require github.com/stretchr/testify v1.10.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +go 1.20.0 diff --git a/jsonname/go.sum b/jsonname/go.sum new file mode 100644 index 0000000..dc6c0f3 --- /dev/null +++ b/jsonname/go.sum @@ -0,0 +1,18 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From e7dc0174671c8cebd6456a7e2f0f611f3db324a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20BIDON?= Date: Sun, 10 Aug 2025 08:39:43 +0200 Subject: [PATCH 04/15] chore(ci): setup monorepo linting and fixed automerge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linting ------- * lint: updated linter config * lint; golangci-lint no longer digs into sub-modules: we have to run the action explicitly with "working-directory" * setup a matrix of submodules to lint all modules in this repo * lint: relinted code Auto-merging ------- * ci(auto-merge): the issue with golang.org updates auto-merge rule is caused by the name of the group "golang.org-dependencies" is internally rewritten as a slug name "golang-org-dependencies" (with a dash, not a dot) so this no longer matches the triggering rules. * ci(auto-merge): fixed the group name to replace "." by "-" (note: this fix should be propagated to all go-openapi repositories). Mono-repo settings ------- * hack: modified the tagging utility to support future modules declared at an arbitrary level in the hierarchy of folders. * ci: introduced a new job to identify all sub-modules and produces the corresponding matrix for linting & testing each one independently. * ci: introduced rendez-vous jobs for linting and testing so we can use only these two in branch protection rule requirements Signed-off-by: Frédéric BIDON --- jsonname/name_provider.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/jsonname/name_provider.go b/jsonname/name_provider.go index 6f2277a..e87aac2 100644 --- a/jsonname/name_provider.go +++ b/jsonname/name_provider.go @@ -124,12 +124,6 @@ func (n *NameProvider) GetJSONNameForType(tpe reflect.Type, name string) (string return nme, ok } -func (n *NameProvider) makeNameIndex(tpe reflect.Type) nameIndex { - names := newNameIndex(tpe) - n.index[tpe] = names - return names -} - // GetGoName gets the go name for a json property name func (n *NameProvider) GetGoName(subject interface{}, name string) (string, bool) { tpe := reflect.Indirect(reflect.ValueOf(subject)).Type() @@ -147,3 +141,9 @@ func (n *NameProvider) GetGoNameForType(tpe reflect.Type, name string) (string, 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 +} From 3807d868afa9a6aa7ee6260cb9d352e3a7430a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20BIDON?= Date: Sun, 7 Sep 2025 22:54:31 +0200 Subject: [PATCH 05/15] Remove dependency to mailru/easyjson by default We want to remove this dependency by default, while still providing support to users who want to use easyjson. * fixes #68 This PR introduces an adapter to use ReadJSON and WriteJSON with different supporting libraries. By default only the standard library is enabled. A simple runtime registration allows the adapter to support easyjson. When import swag or swag/jsonutils or swag/yamlutils, easyjson is no longer a dependency: JSON serialization only requires the standard library. Supporting easyjson interfaces (e.g. for faster serialization) remains possible, by calling an independent module: swag/jsonutils/adapters/easyjson/json.Register(). 1. ordered map w/stdlib 2. registry with stdlib as default 3. fixed int vs float64 rendering 4. beefed up tests, including integration tests 5. honed doc 6. more tests TODO: * [x] hone test coverage * benchmarks * profiling Signed-off-by: Frederic BIDON --- jsonname/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonname/go.mod b/jsonname/go.mod index d1455c7..c7eee5f 100644 --- a/jsonname/go.mod +++ b/jsonname/go.mod @@ -10,4 +10,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -go 1.20.0 +go 1.24.0 From 3eee415f1a82ace1cf0ef85c9d3f23c85631217c Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Sat, 20 Sep 2025 17:27:46 +0200 Subject: [PATCH 06/15] chore: improved mono-repo management This PR prepares the forthcoming release. We need to tidy up a little bit the way the mono-repo setup is handled. This starts by embracing `go.work`. Content ======= * added go.work declaration * ci: adapted go tests to go.work. This should dramatically reduce the number of test jobs. At this moment, we still lint go modules in independent jobs. * ci: fixed issue with go mod caching * fix: dependabot not being able to update nested modules * updated stretchr/testify dependency (because dependabot didn't succeed doing so previously) * removed replace directives from top-level module. We still to keep the replace directives in sub-modules. These will disappear when we cut the next release. * doc (updated TODO items list) adapter tagger script to go.work Signed-off-by: Frederic BIDON --- jsonname/go.mod | 2 +- jsonname/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jsonname/go.mod b/jsonname/go.mod index c7eee5f..81e1207 100644 --- a/jsonname/go.mod +++ b/jsonname/go.mod @@ -1,6 +1,6 @@ module github.com/go-openapi/swag/jsonname -require github.com/stretchr/testify v1.10.0 +require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/jsonname/go.sum b/jsonname/go.sum index dc6c0f3..bdfb3f7 100644 --- a/jsonname/go.sum +++ b/jsonname/go.sum @@ -9,8 +9,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 7653690adc2538e0f952ef54dd6013cc72252ae1 Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Fri, 3 Oct 2025 10:48:58 +0200 Subject: [PATCH 07/15] chore: removed most remaining external dependencies * switch to forked testify (go-openapi/testify/v2) which no longer imports external dependencies * all test dependencies now boil down to go-openapi/testify/v2 Signed-off-by: Frederic BIDON --- jsonname/go.mod | 10 +--------- jsonname/go.sum | 20 ++------------------ jsonname/name_provider_test.go | 2 +- 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/jsonname/go.mod b/jsonname/go.mod index 81e1207..6af4fbd 100644 --- a/jsonname/go.mod +++ b/jsonname/go.mod @@ -1,13 +1,5 @@ module github.com/go-openapi/swag/jsonname -require github.com/stretchr/testify v1.11.1 - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) +require github.com/go-openapi/testify/v2 v2.0.2 go 1.24.0 diff --git a/jsonname/go.sum b/jsonname/go.sum index bdfb3f7..1876434 100644 --- a/jsonname/go.sum +++ b/jsonname/go.sum @@ -1,18 +1,2 @@ -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= diff --git a/jsonname/name_provider_test.go b/jsonname/name_provider_test.go index 6a93c09..0490ec1 100644 --- a/jsonname/name_provider_test.go +++ b/jsonname/name_provider_test.go @@ -18,7 +18,7 @@ import ( "reflect" "testing" - "github.com/stretchr/testify/assert" + "github.com/go-openapi/testify/v2/assert" ) type testNameStruct struct { From 763de45c907e3d9fed9c67a67d4e7ef793d83132 Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Sat, 4 Oct 2025 10:10:57 +0200 Subject: [PATCH 08/15] licensing: modernized the licensing of source code. This commit replaces the full license terms in each go source file by the more compact SPDX annotations (i.e. 2 lines instead of 13). All go files now embed these summarized licensing terms. Also: * license is now cited in README Aside: * fixed sample code in README on how to register an adapter Signed-off-by: Frederic BIDON --- jsonname/doc.go | 15 ++------------- jsonname/name_provider.go | 15 ++------------- jsonname/name_provider_test.go | 15 ++------------- 3 files changed, 6 insertions(+), 39 deletions(-) diff --git a/jsonname/doc.go b/jsonname/doc.go index b2e0c80..79232ea 100644 --- a/jsonname/doc.go +++ b/jsonname/doc.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// 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/name_provider.go b/jsonname/name_provider.go index e87aac2..bd0cd40 100644 --- a/jsonname/name_provider.go +++ b/jsonname/name_provider.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package jsonname diff --git a/jsonname/name_provider_test.go b/jsonname/name_provider_test.go index 0490ec1..92d1a20 100644 --- a/jsonname/name_provider_test.go +++ b/jsonname/name_provider_test.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package jsonname From 9d139a2ed6e2246c6b675d50864b047c7dbb8f5f Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Tue, 11 Nov 2025 21:15:42 +0200 Subject: [PATCH 09/15] lint: fix modernize issues Signed-off-by: Oleksandr Redko --- jsonname/name_provider.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jsonname/name_provider.go b/jsonname/name_provider.go index bd0cd40..8eaf1be 100644 --- a/jsonname/name_provider.go +++ b/jsonname/name_provider.go @@ -79,7 +79,7 @@ func newNameIndex(tpe reflect.Type) nameIndex { } // GetJSONNames gets all the json property names for a type -func (n *NameProvider) GetJSONNames(subject interface{}) []string { +func (n *NameProvider) GetJSONNames(subject any) []string { n.lock.Lock() defer n.lock.Unlock() tpe := reflect.Indirect(reflect.ValueOf(subject)).Type() @@ -96,7 +96,7 @@ func (n *NameProvider) GetJSONNames(subject interface{}) []string { } // GetJSONName gets the json name for a go property name -func (n *NameProvider) GetJSONName(subject interface{}, name string) (string, bool) { +func (n *NameProvider) GetJSONName(subject any, name string) (string, bool) { tpe := reflect.Indirect(reflect.ValueOf(subject)).Type() return n.GetJSONNameForType(tpe, name) } @@ -114,7 +114,7 @@ func (n *NameProvider) GetJSONNameForType(tpe reflect.Type, name string) (string } // GetGoName gets the go name for a json property name -func (n *NameProvider) GetGoName(subject interface{}, name string) (string, bool) { +func (n *NameProvider) GetGoName(subject any, name string) (string, bool) { tpe := reflect.Indirect(reflect.ValueOf(subject)).Type() return n.GetGoNameForType(tpe, name) } From c86da874347fca0528a6cbf568eafd991a3d0ae4 Mon Sep 17 00:00:00 2001 From: fredbi Date: Mon, 9 Feb 2026 23:05:07 +0100 Subject: [PATCH 10/15] test: upgraded to go-openapi/testify@v2.3.0 (#175) Signed-off-by: Frederic BIDON --- jsonname/go.mod | 2 +- jsonname/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jsonname/go.mod b/jsonname/go.mod index 6af4fbd..60030af 100644 --- a/jsonname/go.mod +++ b/jsonname/go.mod @@ -1,5 +1,5 @@ module github.com/go-openapi/swag/jsonname -require github.com/go-openapi/testify/v2 v2.0.2 +require github.com/go-openapi/testify/v2 v2.3.0 go 1.24.0 diff --git a/jsonname/go.sum b/jsonname/go.sum index 1876434..5ce360f 100644 --- a/jsonname/go.sum +++ b/jsonname/go.sum @@ -1,2 +1,2 @@ -github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= -github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/testify/v2 v2.3.0 h1:cZFOKhatfyVejoFNd8jqnHFosegN5vJZiPOTnkyT9hA= +github.com/go-openapi/testify/v2 v2.3.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= From 1106664433dcdced788737e017c2d4326c2b5062 Mon Sep 17 00:00:00 2001 From: fredbi Date: Wed, 11 Feb 2026 14:02:44 +0100 Subject: [PATCH 11/15] test: upgraded tests to use generics (#176) Signed-off-by: Frederic BIDON --- jsonname/name_provider_test.go | 72 +++++++++++++++++----------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/jsonname/name_provider_test.go b/jsonname/name_provider_test.go index 92d1a20..b1b5ce8 100644 --- a/jsonname/name_provider_test.go +++ b/jsonname/name_provider_test.go @@ -22,101 +22,101 @@ func TestNameProvider(t *testing.T) { var obj = testNameStruct{} nm, ok := provider.GetGoName(obj, "name") - assert.True(t, ok) - assert.Equal(t, "Name", nm) + assert.TrueT(t, ok) + assert.EqualT(t, "Name", nm) nm, ok = provider.GetGoName(obj, "plain") - assert.True(t, ok) - assert.Equal(t, "NotTheSame", nm) + assert.TrueT(t, ok) + assert.EqualT(t, "NotTheSame", nm) nm, ok = provider.GetGoName(obj, "doesNotExist") - assert.False(t, ok) + assert.FalseT(t, ok) assert.Empty(t, nm) nm, ok = provider.GetGoName(obj, "ignored") - assert.False(t, ok) + assert.FalseT(t, ok) assert.Empty(t, nm) tpe := reflect.TypeOf(obj) nm, ok = provider.GetGoNameForType(tpe, "name") - assert.True(t, ok) - assert.Equal(t, "Name", nm) + assert.TrueT(t, ok) + assert.EqualT(t, "Name", nm) nm, ok = provider.GetGoNameForType(tpe, "plain") - assert.True(t, ok) - assert.Equal(t, "NotTheSame", nm) + assert.TrueT(t, ok) + assert.EqualT(t, "NotTheSame", nm) nm, ok = provider.GetGoNameForType(tpe, "doesNotExist") - assert.False(t, ok) + assert.FalseT(t, ok) assert.Empty(t, nm) nm, ok = provider.GetGoNameForType(tpe, "ignored") - assert.False(t, ok) + assert.FalseT(t, ok) assert.Empty(t, nm) ptr := &obj nm, ok = provider.GetGoName(ptr, "name") - assert.True(t, ok) - assert.Equal(t, "Name", nm) + assert.TrueT(t, ok) + assert.EqualT(t, "Name", nm) nm, ok = provider.GetGoName(ptr, "plain") - assert.True(t, ok) - assert.Equal(t, "NotTheSame", nm) + assert.TrueT(t, ok) + assert.EqualT(t, "NotTheSame", nm) nm, ok = provider.GetGoName(ptr, "doesNotExist") - assert.False(t, ok) + assert.FalseT(t, ok) assert.Empty(t, nm) nm, ok = provider.GetGoName(ptr, "ignored") - assert.False(t, ok) + assert.FalseT(t, ok) assert.Empty(t, nm) nm, ok = provider.GetJSONName(obj, "Name") - assert.True(t, ok) - assert.Equal(t, "name", nm) + assert.TrueT(t, ok) + assert.EqualT(t, "name", nm) nm, ok = provider.GetJSONName(obj, "NotTheSame") - assert.True(t, ok) - assert.Equal(t, "plain", nm) + assert.TrueT(t, ok) + assert.EqualT(t, "plain", nm) nm, ok = provider.GetJSONName(obj, "DoesNotExist") - assert.False(t, ok) + assert.FalseT(t, ok) assert.Empty(t, nm) nm, ok = provider.GetJSONName(obj, "Ignored") - assert.False(t, ok) + assert.FalseT(t, ok) assert.Empty(t, nm) nm, ok = provider.GetJSONNameForType(tpe, "Name") - assert.True(t, ok) - assert.Equal(t, "name", nm) + assert.TrueT(t, ok) + assert.EqualT(t, "name", nm) nm, ok = provider.GetJSONNameForType(tpe, "NotTheSame") - assert.True(t, ok) - assert.Equal(t, "plain", nm) + assert.TrueT(t, ok) + assert.EqualT(t, "plain", nm) nm, ok = provider.GetJSONNameForType(tpe, "doesNotExist") - assert.False(t, ok) + assert.FalseT(t, ok) assert.Empty(t, nm) nm, ok = provider.GetJSONNameForType(tpe, "Ignored") - assert.False(t, ok) + assert.FalseT(t, ok) assert.Empty(t, nm) nm, ok = provider.GetJSONName(ptr, "Name") - assert.True(t, ok) - assert.Equal(t, "name", nm) + assert.TrueT(t, ok) + assert.EqualT(t, "name", nm) nm, ok = provider.GetJSONName(ptr, "NotTheSame") - assert.True(t, ok) - assert.Equal(t, "plain", nm) + assert.TrueT(t, ok) + assert.EqualT(t, "plain", nm) nm, ok = provider.GetJSONName(ptr, "doesNotExist") - assert.False(t, ok) + assert.FalseT(t, ok) assert.Empty(t, nm) nm, ok = provider.GetJSONName(ptr, "Ignored") - assert.False(t, ok) + assert.FalseT(t, ok) assert.Empty(t, nm) nms := provider.GetJSONNames(ptr) From e8f76b3a38913d6c142738ad3d01a3a21256b52f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:25:45 +0000 Subject: [PATCH 12/15] build(deps): bump the go-openapi-dependencies group across 15 directories with 2 updates Signed-off-by: dependabot[bot] --- jsonname/go.mod | 4 ++-- jsonname/go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jsonname/go.mod b/jsonname/go.mod index 60030af..95cad65 100644 --- a/jsonname/go.mod +++ b/jsonname/go.mod @@ -1,5 +1,5 @@ module github.com/go-openapi/swag/jsonname -require github.com/go-openapi/testify/v2 v2.3.0 +require github.com/go-openapi/testify/v2 v2.4.2 -go 1.24.0 +go 1.25.0 diff --git a/jsonname/go.sum b/jsonname/go.sum index 5ce360f..d679ea5 100644 --- a/jsonname/go.sum +++ b/jsonname/go.sum @@ -1,2 +1,2 @@ -github.com/go-openapi/testify/v2 v2.3.0 h1:cZFOKhatfyVejoFNd8jqnHFosegN5vJZiPOTnkyT9hA= -github.com/go-openapi/testify/v2 v2.3.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= +github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= From d138cf8b6230466655dfb587b0d6d36da37870a0 Mon Sep 17 00:00:00 2001 From: fredbi Date: Wed, 15 Apr 2026 17:38:16 +0200 Subject: [PATCH 13/15] feat(jsonname): added new json name provider more respectful of go conventions for JSON (#195) Signed-off-by: Frederic BIDON --- jsonname/go_name_provider.go | 286 +++++++++++++++++++++++++++ jsonname/go_name_provider_test.go | 318 ++++++++++++++++++++++++++++++ jsonname/ifaces.go | 14 ++ jsonname/name_provider.go | 2 + 4 files changed, 620 insertions(+) create mode 100644 jsonname/go_name_provider.go create mode 100644 jsonname/go_name_provider_test.go create mode 100644 jsonname/ifaces.go diff --git a/jsonname/go_name_provider.go b/jsonname/go_name_provider.go new file mode 100644 index 0000000..adc4426 --- /dev/null +++ b/jsonname/go_name_provider.go @@ -0,0 +1,286 @@ +// 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. +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 := 0; i < q.typ.NumField(); i++ { + sf := q.typ.Field(i) + + if sf.Anonymous { + ft := sf.Type + if ft.Kind() == reflect.Ptr { + 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.Ptr { + 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 idx := strings.IndexByte(tag, ','); idx >= 0 { + return tag[:idx], tag[idx+1:] + } + + return tag, "" +} diff --git a/jsonname/go_name_provider_test.go b/jsonname/go_name_provider_test.go new file mode 100644 index 0000000..eb01533 --- /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:""` + Optional string `json:",omitempty"` + 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.TypeOf(obj) + 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..812ace5 --- /dev/null +++ b/jsonname/ifaces.go @@ -0,0 +1,14 @@ +// 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 index 8eaf1be..9f5da7a 100644 --- a/jsonname/name_provider.go +++ b/jsonname/name_provider.go @@ -12,6 +12,8 @@ import ( // DefaultJSONNameProvider is the default cache for types. var DefaultJSONNameProvider = NewNameProvider() +var _ providerIface = (*NameProvider)(nil) + // NameProvider represents an object capable of translating from go property names // to json property names. // From 5667aac8effbbc79c1ddcc51e7d598959eb2d53b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 18:32:06 +0000 Subject: [PATCH 14/15] build(deps): bump the go-openapi-dependencies group across 15 directories with 2 updates Signed-off-by: dependabot[bot] --- jsonname/go.mod | 2 +- jsonname/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jsonname/go.mod b/jsonname/go.mod index 95cad65..6e0dbc3 100644 --- a/jsonname/go.mod +++ b/jsonname/go.mod @@ -1,5 +1,5 @@ module github.com/go-openapi/swag/jsonname -require github.com/go-openapi/testify/v2 v2.4.2 +require github.com/go-openapi/testify/v2 v2.6.0 go 1.25.0 diff --git a/jsonname/go.sum b/jsonname/go.sum index d679ea5..86512be 100644 --- a/jsonname/go.sum +++ b/jsonname/go.sum @@ -1,2 +1,2 @@ -github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= -github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +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= From ae73433d4932b066554b9056aa0078cc2bcf478e Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Mon, 29 Jun 2026 18:34:11 +0200 Subject: [PATCH 15/15] chore: wired the local jsonname package & relinted Signed-off-by: Frederic BIDON --- .golangci.yml | 3 + dash_token_test.go | 13 ++-- errors.go | 15 ++-- examples_test.go | 29 ++++--- go.mod | 5 +- go.sum | 2 - iface_example_test.go | 2 +- ifaces.go | 35 +++++---- jsonname/go.mod | 5 -- jsonname/go.sum | 2 - jsonname/go_name_provider.go | 36 +++++---- jsonname/go_name_provider_test.go | 24 +++--- jsonname/ifaces.go | 8 +- jsonname/name_provider.go | 24 +++--- jsonname/name_provider_test.go | 4 +- options.go | 19 +++-- options_test.go | 15 ++-- pointer.go | 124 +++++++++++++++--------------- pointer_test.go | 17 ++-- struct_example_test.go | 2 +- 20 files changed, 189 insertions(+), 195 deletions(-) delete mode 100644 jsonname/go.mod delete mode 100644 jsonname/go.sum 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/go.mod b/jsonname/go.mod deleted file mode 100644 index 6e0dbc3..0000000 --- a/jsonname/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/go-openapi/swag/jsonname - -require github.com/go-openapi/testify/v2 v2.6.0 - -go 1.25.0 diff --git a/jsonname/go.sum b/jsonname/go.sum deleted file mode 100644 index 86512be..0000000 --- a/jsonname/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -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/jsonname/go_name_provider.go b/jsonname/go_name_provider.go index adc4426..5eec18f 100644 --- a/jsonname/go_name_provider.go +++ b/jsonname/go_name_provider.go @@ -11,11 +11,11 @@ import ( 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. +// 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. +// Contrary to [NameProvider], it considers exported fields without a json tag, and promotes fields +// from anonymous embedded struct types. // // Rules (aligned with encoding/json): // @@ -104,9 +104,9 @@ func (n *GoNameProvider) nameIndexFor(tpe reflect.Type) nameIndex { 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). +// 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 @@ -129,6 +129,8 @@ func buildGoNameIndex(tpe reflect.Type) nameIndex { // 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 @@ -157,12 +159,12 @@ func collectGoFields(tpe reflect.Type) []fieldEntry { } for _, q := range current { - for i := 0; i < q.typ.NumField(); i++ { + for i := range q.typ.NumField() { sf := q.typ.Field(i) if sf.Anonymous { ft := sf.Type - if ft.Kind() == reflect.Ptr { + if ft.Kind() == reflect.Pointer { ft = ft.Elem() } if !sf.IsExported() && ft.Kind() != reflect.Struct { @@ -180,7 +182,7 @@ func collectGoFields(tpe reflect.Type) []fieldEntry { tagged := jsonName != "" ft := sf.Type - if ft.Kind() == reflect.Ptr { + if ft.Kind() == reflect.Pointer { ft = ft.Elem() } @@ -221,9 +223,9 @@ func collectGoFields(tpe reflect.Type) []fieldEntry { 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. +// 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 { @@ -272,14 +274,14 @@ func dominantFields(candidates []fieldEntry) []fieldEntry { 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"). +// 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 idx := strings.IndexByte(tag, ','); idx >= 0 { - return tag[:idx], tag[idx+1:] + 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 index eb01533..5c79b91 100644 --- a/jsonname/go_name_provider_test.go +++ b/jsonname/go_name_provider_test.go @@ -45,15 +45,15 @@ type testAltStruct struct { Name string `json:"name"` NotTheSame int64 `json:"plain"` Ignored string `json:"-"` - DashField string `json:"-,"` //nolint:staticcheck // deliberate: exercise stdlib "-," quirk - Untagged string `json:""` - Optional string `json:",omitempty"` + 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. +// testAltShadow verifies the depth-based conflict resolution: the outer field must win over one +// promoted from an embedded type. type testAltShadow struct { testAltEmbedded @@ -63,7 +63,7 @@ type testAltShadow struct { func TestGoNameProvider(t *testing.T) { provider := NewGoNameProvider() obj := testAltStruct{} - tpe := reflect.TypeOf(obj) + tpe := reflect.TypeFor[testAltStruct]() ptr := &obj t.Run("GetGoName resolves tagged fields", func(t *testing.T) { @@ -165,8 +165,8 @@ func TestGoNameProvider(t *testing.T) { }) } -// TestGoNameProvider_ShadowingMatchesStdlib pins our field selection to the -// behavior of encoding/json for shadowed promoted fields. +// 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"}` @@ -185,8 +185,8 @@ func TestGoNameProvider_ShadowingMatchesStdlib(t *testing.T) { 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. +// 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") @@ -224,8 +224,8 @@ type testAltUnexportedEmbed struct { 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. +// 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() diff --git a/jsonname/ifaces.go b/jsonname/ifaces.go index 812ace5..64871f0 100644 --- a/jsonname/ifaces.go +++ b/jsonname/ifaces.go @@ -5,9 +5,11 @@ 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]. +// 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 index 9f5da7a..1bec240 100644 --- a/jsonname/name_provider.go +++ b/jsonname/name_provider.go @@ -10,12 +10,12 @@ import ( ) // DefaultJSONNameProvider is the default cache for types. -var DefaultJSONNameProvider = NewNameProvider() +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. +// NameProvider represents an object capable of translating from go property names to json property +// names. // // This type is thread-safe. // @@ -30,7 +30,7 @@ type nameIndex struct { goNames map[string]string } -// NewNameProvider creates a new name provider +// NewNameProvider creates a new name provider. func NewNameProvider() *NameProvider { return &NameProvider{ lock: &sync.Mutex{}, @@ -39,7 +39,7 @@ func NewNameProvider() *NameProvider { } func buildnameIndex(tpe reflect.Type, idx, reverseIdx map[string]string) { - for i := 0; i < tpe.NumField(); i++ { + for i := range tpe.NumField() { targetDes := tpe.Field(i) if targetDes.PkgPath != "" { // unexported @@ -73,14 +73,14 @@ func buildnameIndex(tpe reflect.Type, idx, reverseIdx map[string]string) { } func newNameIndex(tpe reflect.Type) nameIndex { - var idx = make(map[string]string, tpe.NumField()) - var reverseIdx = make(map[string]string, tpe.NumField()) + 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 +// GetJSONNames gets all the json property names for a type. func (n *NameProvider) GetJSONNames(subject any) []string { n.lock.Lock() defer n.lock.Unlock() @@ -97,13 +97,13 @@ func (n *NameProvider) GetJSONNames(subject any) []string { return res } -// GetJSONName gets the json name for a go property name +// 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 +// 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() @@ -115,13 +115,13 @@ func (n *NameProvider) GetJSONNameForType(tpe reflect.Type, name string) (string return nme, ok } -// GetGoName gets the go name for a json property name +// 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 +// 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() diff --git a/jsonname/name_provider_test.go b/jsonname/name_provider_test.go index b1b5ce8..8951049 100644 --- a/jsonname/name_provider_test.go +++ b/jsonname/name_provider_test.go @@ -19,7 +19,7 @@ type testNameStruct struct { func TestNameProvider(t *testing.T) { provider := NewNameProvider() - var obj = testNameStruct{} + obj := testNameStruct{} nm, ok := provider.GetGoName(obj, "name") assert.TrueT(t, ok) @@ -37,7 +37,7 @@ func TestNameProvider(t *testing.T) { assert.FalseT(t, ok) assert.Empty(t, nm) - tpe := reflect.TypeOf(obj) + tpe := reflect.TypeFor[testNameStruct]() nm, ok = provider.GetGoNameForType(tpe, "name") assert.TrueT(t, ok) assert.EqualT(t, "Name", nm) 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