-
Notifications
You must be signed in to change notification settings - Fork 70
/
field.go
242 lines (223 loc) · 8.74 KB
/
field.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
// Copyright (c) 2023, Cogent Core. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"log/slog"
"os"
"reflect"
"strings"
"slices"
"cogentcore.org/core/base/ordmap"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/base/strcase"
)
// Field represents a struct field in a configuration struct.
// It is passed around in flag parsing functions, but it should
// not typically be used by end-user code going through the
// standard Run/Config/SetFromArgs API.
type Field struct {
// Field is the reflect struct field object for this field
Field reflect.StructField
// Value is the reflect value of the settable pointer to this field
Value reflect.Value
// Struct is the parent struct that contains this field
Struct reflect.Value
// Name is the fully qualified, nested name of this field (eg: A.B.C).
// It is as it appears in code, and is NOT transformed something like kebab-case.
Name string
// Names contains all of the possible end-user names for this field as a flag.
// It defaults to the name of the field, but custom names can be specified via
// the cli struct tag.
Names []string
}
// Fields is a simple type alias for an ordered map of [Field] objects.
type Fields = ordmap.Map[string, *Field]
// AddAllFields, when passed as the command to [AddFields], indicates
// to add all fields, regardless of their command association.
const AddAllFields = "*"
// AddFields adds to the given fields map all of the fields of the given
// object, in the context of the given command name. A value of [AddAllFields]
// for cmd indicates to add all fields, regardless of their command association.
func AddFields(obj any, allFields *Fields, cmd string) {
AddFieldsImpl(obj, "", "", allFields, map[string]*Field{}, cmd)
}
// AddFieldsImpl is the underlying implementation of [AddFields].
// AddFieldsImpl should almost never be called by end-user code;
// see [AddFields] instead. The path is the current path state,
// the cmdPath is the current path state without command-associated names,
// and usedNames is a map keyed by used CamelCase names with values
// of their associated fields, used to track naming conflicts. The
// [Field.Name]s of the fields are set based on the path, whereas the
// names of the flags are set based on the command path. The difference
// between the two is that the path is always fully qualified, whereas the
// command path omits the names of structs associated with commands via
// the "cmd" struct tag, as the user already knows what command they are
// running, so they do not need that duplicated specificity for every flag.
func AddFieldsImpl(obj any, path string, cmdPath string, allFields *Fields, usedNames map[string]*Field, cmd string) {
if reflectx.AnyIsNil(obj) {
return
}
ov := reflect.ValueOf(obj)
if ov.Kind() == reflect.Pointer && ov.IsNil() {
return
}
val := reflectx.NonPointerValue(ov)
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
fv := val.Field(i)
pval := reflectx.PointerValue(fv)
cmdTag, hct := f.Tag.Lookup("cmd")
cmds := strings.Split(cmdTag, ",")
if hct && !slices.Contains(cmds, cmd) && !slices.Contains(cmds, AddAllFields) { // if we are associated with a different command, skip
continue
}
if reflectx.NonPointerType(f.Type).Kind() == reflect.Struct {
nwPath := f.Name
if path != "" {
nwPath = path + "." + nwPath
}
nwCmdPath := f.Name
// if we have a command tag, we don't scope our command path with our name,
// as we don't need it because we ran that command and know we are in it
if hct {
nwCmdPath = ""
}
if cmdPath != "" {
nwCmdPath = cmdPath + "." + nwCmdPath
}
AddFieldsImpl(reflectx.PointerValue(fv).Interface(), nwPath, nwCmdPath, allFields, usedNames, cmd)
// we still add ourself if we are a struct, so we keep going,
// unless we are associated with a command, in which case there
// is no point in adding ourself
if hct {
continue
}
}
// we first add our unqualified command name, which is the best case scenario
name := f.Name
names := []string{name}
// then, we set our future [Field.Name] to the fully path scoped version (but we don't add it as a command name)
if path != "" {
name = path + "." + name
}
// then, we set add our command path scoped name as a command name
if cmdPath != "" {
names = append(names, cmdPath+"."+f.Name)
}
flagTag, ok := f.Tag.Lookup("flag")
if ok {
names = strings.Split(flagTag, ",")
if len(names) == 0 {
slog.Error("programmer error: expected at least one name in flag struct tag, but got none")
}
}
nf := &Field{
Field: f,
Value: pval,
Struct: ov,
Name: name,
Names: names,
}
for i, name := range nf.Names {
// duplicate deletion can cause us to get out of range
if i >= len(nf.Names) {
break
}
name := strcase.ToCamel(name) // everybody is in camel for naming conflict check
if of, has := usedNames[name]; has { // we have a conflict
// if we have a naming conflict between two fields with the same base
// (in the same parent struct), then there is no nesting and they have
// been directly given conflicting names, so there is a simple programmer error
nbase := ""
nli := strings.LastIndex(nf.Name, ".")
if nli >= 0 {
nbase = nf.Name[:nli]
}
obase := ""
oli := strings.LastIndex(of.Name, ".")
if oli >= 0 {
obase = of.Name[:oli]
}
if nbase == obase {
slog.Error("programmer error: cli: two fields were assigned the same name", "name", name, "field0", of.Name, "field1", nf.Name)
os.Exit(1)
}
// if that isn't the case, they are in different parent structs and
// it is a nesting problem, so we use the nest tags to resolve the conflict.
// the basic rule is that whoever specifies the nest:"-" tag gets to
// be non-nested, and if no one specifies it, everyone is nested.
// if both want to be non-nested, that is a programmer error.
// nest field tag values for new and other
nfns := nf.Field.Tag.Get("nest")
ofns := of.Field.Tag.Get("nest")
// whether new and other get to have non-nested version
nfn := nfns == "-" || nfns == "false"
ofn := ofns == "-" || ofns == "false"
if nfn && ofn {
slog.Error(`programmer error: cli: nest:"-" specified on two config fields with the same name; keep nest:"-" on the field you want to be able to access without nesting and remove it from the other one`, "name", name, "field0", of.Name, "field1", nf.Name, "exampleFlagWithoutNesting", "-"+name, "exampleFlagWithNesting", "-"+strcase.ToKebab(nf.Name))
os.Exit(1)
} else if !nfn && !ofn {
// neither one gets it, so we replace both with fully qualified name
ApplyShortestUniqueName(nf, i, usedNames)
for i, on := range of.Names {
if on == name {
ApplyShortestUniqueName(of, i, usedNames)
}
}
} else if nfn && !ofn {
// we get it, so we keep ours as is and replace them with fully qualified name
for i, on := range of.Names {
if on == name {
ApplyShortestUniqueName(of, i, usedNames)
}
}
// we also need to update the field for our name to us
usedNames[name] = nf
} else if !nfn && ofn {
// they get it, so we replace ours with fully qualified name
ApplyShortestUniqueName(nf, i, usedNames)
}
} else {
// if no conflict, we get the name
usedNames[name] = nf
}
}
allFields.Add(name, nf)
}
}
// ApplyShortestUniqueName uses [ShortestUniqueName] to apply the shortest
// unique name for the given field, in the context of the given
// used names, at the given index. It should not typically be used by
// end-user code.
func ApplyShortestUniqueName(field *Field, idx int, usedNames map[string]*Field) {
nm := ShortestUniqueName(field.Name, usedNames)
// if we already have this name, we don't need to add it, so we just delete this entry
if slices.Contains(field.Names, nm) {
field.Names = slices.Delete(field.Names, idx, idx+1)
} else {
field.Names[idx] = nm
usedNames[nm] = field
}
}
// ShortestUniqueName returns the shortest unique camel-case name for
// the given fully qualified nest name of a field, using the given
// map of used names. It works backwards, so, for example, if given "A.B.C.D",
// it would check "D", then "C.D", then "B.C.D", and finally "A.B.C.D".
// It should not typically be used by end-user code.
func ShortestUniqueName(name string, usedNames map[string]*Field) string {
strs := strings.Split(name, ".")
cur := ""
for i := len(strs) - 1; i >= 0; i-- {
if cur == "" {
cur = strs[i]
} else {
cur = strs[i] + "." + cur
}
if _, has := usedNames[cur]; !has {
return cur
}
}
return cur // TODO: this should never happen, but if it does, we might want to print an error
}