Files
Davanum Srinivas 0706a3bb89 Update k8s.io/kube-openapi to v0.0.0-20260721132016-d427ff9ee9ad
Picks up:
- kubernetes/kube-openapi#625: update the go-openapi/swag family to v0.27.1
- kubernetes/kube-openapi#622: propagate nullable field between SMD and OpenAPI schemas

Transitive dependency changes:
- github.com/go-openapi/swag (and submodules) v0.25.4 -> v0.27.1
- github.com/go-openapi/jsonpointer v0.22.4 -> v1.0.0
- github.com/go-openapi/jsonreference v0.21.4 -> v1.0.0
- github.com/go-openapi/swag/jsonname removed (package now provided by jsonpointer)
- github.com/go-openapi/swag/pools added

No changes to hack/unwanted-dependencies.json needed.
2026-07-21 09:56:05 -04:00

289 lines
7.0 KiB
Go

// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package jsonname
import (
"reflect"
"strings"
"sync"
)
var _ providerIface = (*GoNameProvider)(nil)
// GoNameProvider resolves json property names to go struct field names following the same rules as
// the standard library's [encoding/json] package.
//
// Contrary to [NameProvider], it considers exported fields without a json tag, and promotes fields
// from anonymous embedded struct types.
//
// Rules (aligned with encoding/json):
//
// - unexported fields are ignored;
// - a field tagged `json:"-"` is ignored;
// - a field tagged `json:"-,"` is kept under the json name "-" (stdlib quirk);
// - a field tagged `json:""` or with no json tag at all keeps its Go name as json name;
// - anonymous struct fields without an explicit json tag have their fields
// promoted into the parent, following breadth-first depth rules:
// a shallower field wins over a deeper one; at equal depth, a conflict
// discards all conflicting fields unless exactly one has an explicit json tag.
//
// This type is safe for concurrent use.
type GoNameProvider struct {
lock sync.Mutex
index map[reflect.Type]nameIndex
}
// NewGoNameProvider creates a new [GoNameProvider].
func NewGoNameProvider() *GoNameProvider {
return &GoNameProvider{
index: make(map[reflect.Type]nameIndex),
}
}
// GetJSONNames gets all the json property names for a type.
func (n *GoNameProvider) GetJSONNames(subject any) []string {
n.lock.Lock()
defer n.lock.Unlock()
tpe := reflect.Indirect(reflect.ValueOf(subject)).Type()
names := n.nameIndexFor(tpe)
res := make([]string, 0, len(names.jsonNames))
for k := range names.jsonNames {
res = append(res, k)
}
return res
}
// GetJSONName gets the json name for a go property name.
func (n *GoNameProvider) GetJSONName(subject any, name string) (string, bool) {
tpe := reflect.Indirect(reflect.ValueOf(subject)).Type()
return n.GetJSONNameForType(tpe, name)
}
// GetJSONNameForType gets the json name for a go property name on a given type.
func (n *GoNameProvider) GetJSONNameForType(tpe reflect.Type, name string) (string, bool) {
n.lock.Lock()
defer n.lock.Unlock()
names := n.nameIndexFor(tpe)
nme, ok := names.goNames[name]
return nme, ok
}
// GetGoName gets the go name for a json property name.
func (n *GoNameProvider) GetGoName(subject any, name string) (string, bool) {
tpe := reflect.Indirect(reflect.ValueOf(subject)).Type()
return n.GetGoNameForType(tpe, name)
}
// GetGoNameForType gets the go name for a given type for a json property name.
func (n *GoNameProvider) GetGoNameForType(tpe reflect.Type, name string) (string, bool) {
n.lock.Lock()
defer n.lock.Unlock()
names := n.nameIndexFor(tpe)
nme, ok := names.jsonNames[name]
return nme, ok
}
func (n *GoNameProvider) nameIndexFor(tpe reflect.Type) nameIndex {
if names, ok := n.index[tpe]; ok {
return names
}
names := buildGoNameIndex(tpe)
n.index[tpe] = names
return names
}
// fieldEntry captures a candidate field discovered while walking a struct along with the
// indirection path from the root type (used to resolve conflicts by depth in the same way
// encoding/json does).
type fieldEntry struct {
goName string
jsonName string
index []int
tagged bool
}
func buildGoNameIndex(tpe reflect.Type) nameIndex {
fields := collectGoFields(tpe)
idx := make(map[string]string, len(fields))
reverseIdx := make(map[string]string, len(fields))
for _, f := range fields {
idx[f.jsonName] = f.goName
reverseIdx[f.goName] = f.jsonName
}
return nameIndex{jsonNames: idx, goNames: reverseIdx}
}
// collectGoFields walks tpe breadth-first along anonymous struct fields,
// reproducing the field selection performed by encoding/json.typeFields.
//
//nolint:gocognit // everything is inlined to help the compiler determine what escapes and what doesn't
func collectGoFields(tpe reflect.Type) []fieldEntry {
if tpe.Kind() != reflect.Struct {
return nil
}
type queued struct {
typ reflect.Type
index []int
}
current := []queued{}
next := []queued{{typ: tpe}}
visited := map[reflect.Type]bool{tpe: true}
var (
candidates []fieldEntry
count = map[string]int{}
nextCount = map[string]int{}
)
for len(next) > 0 {
current, next = next, current[:0]
count, nextCount = nextCount, count
for k := range nextCount {
delete(nextCount, k)
}
for _, q := range current {
for i := range q.typ.NumField() {
sf := q.typ.Field(i)
if sf.Anonymous {
ft := sf.Type
if ft.Kind() == reflect.Pointer {
ft = ft.Elem()
}
if !sf.IsExported() && ft.Kind() != reflect.Struct {
continue
}
} else if !sf.IsExported() {
continue
}
tag := sf.Tag.Get("json")
if tag == "-" {
continue
}
jsonName, _ := parseJSONTag(tag)
tagged := jsonName != ""
ft := sf.Type
if ft.Kind() == reflect.Pointer {
ft = ft.Elem()
}
if sf.Anonymous && ft.Kind() == reflect.Struct && !tagged {
if visited[ft] {
continue
}
visited[ft] = true
index := make([]int, len(q.index)+1)
copy(index, q.index)
index[len(q.index)] = i
next = append(next, queued{typ: ft, index: index})
continue
}
name := jsonName
if name == "" {
name = sf.Name
}
index := make([]int, len(q.index)+1)
copy(index, q.index)
index[len(q.index)] = i
candidates = append(candidates, fieldEntry{
goName: sf.Name,
jsonName: name,
index: index,
tagged: tagged,
})
nextCount[name]++
}
}
}
return dominantFields(candidates)
}
// dominantFields applies the Go encoding/json conflict resolution rules: at each JSON name, the
// shallowest field wins; at equal depth, a uniquely tagged candidate wins; otherwise all candidates
// for that name are dropped.
func dominantFields(candidates []fieldEntry) []fieldEntry {
byName := make(map[string][]fieldEntry, len(candidates))
for _, c := range candidates {
byName[c.jsonName] = append(byName[c.jsonName], c)
}
out := make([]fieldEntry, 0, len(byName))
for _, group := range byName {
if len(group) == 1 {
out = append(out, group[0])
continue
}
minDepth := len(group[0].index)
for _, c := range group[1:] {
if len(c.index) < minDepth {
minDepth = len(c.index)
}
}
var shallow []fieldEntry
for _, c := range group {
if len(c.index) == minDepth {
shallow = append(shallow, c)
}
}
if len(shallow) == 1 {
out = append(out, shallow[0])
continue
}
var tagged []fieldEntry
for _, c := range shallow {
if c.tagged {
tagged = append(tagged, c)
}
}
if len(tagged) == 1 {
out = append(out, tagged[0])
}
}
return out
}
// parseJSONTag returns the name component of a json struct tag and whether it carried any non-name
// option (kept for future-proofing, e.g. "omitempty").
func parseJSONTag(tag string) (string, string) {
if tag == "" {
return "", ""
}
if before, after, ok := strings.Cut(tag, ","); ok {
return before, after
}
return tag, ""
}