Repository navigation
Expand file tree
/
Copy pathget_struct_fields.go
More file actions
155 lines (139 loc) · 4.68 KB
/
Copy pathget_struct_fields.go
File metadata and controls
155 lines (139 loc) · 4.68 KB
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
package structs
import (
"reflect"
"strings"
"unsafe"
)
// GetStructFields reflects over structure (a pointer to a struct) and returns
// its fields as []Field, recursing into named nested structs and building each
// nested field's FQN (field.subfield.subsubfield).
// Embedded (anonymous) struct fields are promoted:
// their fields are returned inline at this level, with no wrapper field and no FQN, matching Go's own field promotion.
// encodingTags selects which tags get their comma options stripped (see DefaultEncodingTags).
// It returns ErrInputPointer or ErrInputPointerStruct when structure is not a pointer to a struct.
func GetStructFields(structure any, parent *Field, encodingTags []string) ([]Field, error) {
val := reflect.ValueOf(structure)
if val.Kind() != reflect.Pointer {
return nil, ErrInputPointer
}
if val.Elem().Kind() != reflect.Struct {
return nil, ErrInputPointerStruct
}
fields := make([]Field, 0)
val = val.Elem()
typ := val.Type()
for i := range typ.NumField() {
field := typ.Field(i)
fieldValue := val.Field(i)
tags := parseTags(string(field.Tag), encodingTags)
// an untagged embedded (anonymous) struct promotes its fields to this
// level, just as Go's own field promotion does: the fields appear inline,
// with no wrapper field and no FQN prefix. a tagged embed (or any named
// struct field) instead groups its fields under a dotted FQN, matching
// encoding/json: a tag on an anonymous field names it rather than
// promoting it.
if field.Anonymous && field.Type.Kind() == reflect.Struct && len(tags) == 0 {
promoted, err := GetStructFields(addrInterface(fieldValue), parent, encodingTags)
if err != nil {
return nil, err
}
fields = append(fields, promoted...)
continue
}
f := NewField(field.Name, field.Type.Kind(), fieldValue, tags, parent)
if field.Type.Kind() == reflect.Struct {
nestedFields, err := GetStructFields(addrInterface(fieldValue), &f, encodingTags)
if err != nil {
return nil, err
}
for j := range nestedFields {
nestedFields[j].Parent = &f
nestedFields[j].FQN = nestedFields[j].buildFQN()
}
f.Fields = nestedFields
fields = append(fields, f)
} else {
fields = append(fields, f)
}
}
return fields, nil
}
// addrInterface returns an interfaceable pointer to v, which must be
// addressable. reflect refuses Addr().Interface() on an unexported field (e.g.
// an embedded struct whose type is unexported), so the pointer is rebuilt via
// unsafe. The field's own exported sub-fields then read and set normally,
// matching Go's field promotion.
func addrInterface(v reflect.Value) any {
if v.CanInterface() {
return v.Addr().Interface()
}
return reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Interface()
}
// parseTags extracts tags and their values from a given line of text
// arg:"cwd" short:"c" help:"Current working directory"
// The function returns a map: {"arg": "cwd", "short": "c", "help": "Current working directory"}
func parseTags(line string, encodingTags []string) map[string]string {
line = strings.TrimSpace(line)
result := make(map[string]string)
inTag := true
lastTagName := ""
inValue := false
lastTagValue := ""
for i, char := range line {
switch char {
case ':':
if inValue {
lastTagValue += string(char)
continue
}
inTag = false
continue
case '"':
// allow escaping quotes
if i > 0 && line[i-1] == '\\' {
lastTagValue = lastTagValue[:len(lastTagValue)-1] + string(char)
continue
}
// entering or exiting a tag value
inValue = !inValue
if inValue {
continue
}
// exiting a tag value
lastTagName = strings.TrimSpace(lastTagName)
lastTagValue = strings.TrimSpace(lastTagValue)
// comma-suffixed options (",omitempty", ",flow", ...) are a
// convention of encoding tags only. strip them for those tags so
// the stored value is just the name (e.g. "filters" not "filters,omitempty").
// Freeform tags (help, default, rules) keep their value verbatim.
if isEncodingTag(lastTagName, encodingTags) {
if idx := strings.IndexByte(lastTagValue, ','); idx >= 0 {
lastTagValue = lastTagValue[:idx]
}
}
result[lastTagName] = lastTagValue
lastTagName = ""
lastTagValue = ""
inTag = true
default:
// collect characters for the tag name or value
if inTag {
lastTagName += string(char)
}
if inValue {
lastTagValue += string(char)
}
}
}
return result
}
// isEncodingTag reports whether name is one of encodingTags, the set of tags
// whose values use comma-separated options. See DefaultEncodingTags.
func isEncodingTag(name string, encodingTags []string) bool {
for _, t := range encodingTags {
if t == name {
return true
}
}
return false
}