mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-08-06 05:40:15 +00:00
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.
289 lines
7.0 KiB
Go
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, ""
|
|
}
|