-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathmapper.go
357 lines (333 loc) · 11.7 KB
/
mapper.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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
package set
import (
"fmt"
"reflect"
"sort"
"strings"
"sync"
"time"
"github.com/nofeaturesonlybugs/set/path"
)
var mapperTreatAsScalar = map[reflect.Type]struct{}{
reflect.TypeOf(time.Time{}): {},
reflect.TypeOf(&time.Time{}): {},
}
// A Mapping is the result of traversing a struct hierarchy to map pathways from
// the origin (i.e. top-level struct) to fields within the hierarchy.
//
// Mappings are created by calling Mapper.Map(s) where s is the struct you wish
// to map.
type Mapping struct {
// Keys contains the names generated by the Mapper that created this Mapping.
//
// See Mapper documentation for information on controlling how and what names
// are generated.
Keys []string
// Indeces contains each mapped field's index as an int slice ([]int) such as
// would be appropriate for passing to reflect.Value.FieldByIndex([]int).
//
// However bear in mind reflect.Value.FieldByIndex([]int) essentially requires that
// none of the intermediate fields described by the index are pointers or nil.
//
// The Value type in this package also has a FieldByIndex([]int) method. Value.FieldByIndex([]int)
// will traverse and instantiate pointers and pointer chains where as
// reflect.Value.FieldByIndex([]int) may panic.
Indeces map[string][]int
// StructFields can be used to look up the reflect.StructField by a generated
// key name.
//
// This field is provided as a convenience so you can map a struct and inspect
// field tags without having to use the reflect package yourself.
StructFields map[string]reflect.StructField
// ReflectPaths can be used to retrieve a path.ReflectPath by mapped name.
//
// A path.ReflectPath is slightly different and slightly more informative representation
// for a path than a plain []int.
ReflectPaths map[string]path.ReflectPath
// HasPointers will be true if any of the pathways traverse a field that is a pointer.
HasPointers bool
}
// Mapper creates Mapping instances from structs and struct hierarchies.
//
// Mapper allows you to take any struct or struct hierarchy and flatten it
// to a set of key or column names that index into the hierarchy.
//
// Each of the public fields on Mapper controls an aspect of its behavior in order
// to provide fine grained control over how key names are generated or how types
// unknown to the set package are treated.
//
// Instantiate mappers as pointers:
// myMapper := &set.Mapper{}
type Mapper struct {
// If the types you wish to map contain embedded structs or interfaces you do not
// want to map to string names include those types in the Ignored member.
//
// See also NewTypeList().
Ignored TypeList
// Struct fields that are also structs or embedded structs will have their name
// as part of the generated name unless it is included in the Elevated member.
//
// See also NewTypeList().
Elevated TypeList
// Types in this list are treated as scalars when generating mappings; in other words
// their exported fields are not mapped and the mapping created targets the type as
// a whole. This is useful when you want to create mappings for types such as sql.NullString
// without traversing within the sql.NullString itself.
TreatAsScalar TypeList
// A list of struct tags that will be used for name generation in order of preference.
// An example would be using this feature for both JSON and DB field name specification.
// If most of your db and json names match but you occasionally want to override the json
// struct tag value with the db struct tag value you could set this member to:
// []string{ "db", "json" } // struct tag `db` used before struct tag `json`
Tags []string
// When TaggedFieldsOnly is true the Map() method only maps struct fields that have tags
// matching a value in the Tags field. In other words exported tag-less struct fields are not
// mapped.
TaggedFieldsOnly bool
// Join specifies the string used to join generated names as nesting increases.
Join string
// If set this function is called when the struct field name is being used as
// the generated name. This function can perform string alteration to force all
// names to lowercase, string replace, etc.
Transform func(string) string
//
// NB sync.Map outperformed map+RWMutex in benchmarks.
known sync.Map
}
// DefaultMapper joins names by "_" but performs no other modifications.
var DefaultMapper = &Mapper{
Join: "_",
}
// Bind creates a BoundMapping that is initially bound to I.
//
// BoundMappings are provided for performance critical code that needs to
// read or mutate many instances of the same type repeatedly without constraint
// on field access between instances.
//
// See documentation for BoundMapping for more details.
//
// I must be an addressable type.
func (me *Mapper) Bind(I interface{}) (BoundMapping, error) {
var value reflect.Value
var typ reflect.Type
var writable bool
switch sw := I.(type) {
case reflect.Value:
typ = sw.Type()
value, writable = Writable(sw)
default:
typ = reflect.TypeOf(I)
value, writable = Writable(reflect.ValueOf(I))
}
if !writable {
typeStr := typ.String()
err := pkgerr{
Err: ErrReadOnly,
CallSite: "Mapper.Bind",
Hint: "call to Mapper.Bind(" + typeStr + ") should have been Mapper.Bind(*" + typeStr + ")",
}
return BoundMapping{err: err}, err
}
mapping := me.Map(I)
//
rv := BoundMapping{
top: typ,
value: value,
paths: mapping.ReflectPaths,
}
return rv, nil
}
// Map adds T to the Mapper's list of known and recognized types.
//
// Map is goroutine safe. Multiple goroutines can call Map() simultaneously and the returned
// Mappings will behave identically. However if multiple goroutines simultaneously call
// Map(T) for the same type each goroutine may receiver a Mapping with its own underlying memory. If you
// require returned Mappings to use shared memory for the slice and map members then you should
// call Map(T) from a high level goroutine to build up the cache before calling it from
// other goroutines.
//
// Mappings that are returned are shared resources and should not be altered in any way. If this is your
// use-case then create a copy of the Mapping with Mapping.Copy.
func (me *Mapper) Map(T interface{}) Mapping {
var typeInfo TypeInfo
switch tt := T.(type) {
case reflect.Type:
typeInfo = TypeCache.StatType(tt)
case reflect.Value:
typeInfo = TypeCache.StatType(tt.Type())
default:
typeInfo = TypeCache.Stat(T)
}
//
if rv, ok := me.known.Load(typeInfo.Type); ok {
return *(rv.(*Mapping))
}
//
// Create a more formal mapping from subpackage path.
paths := path.Stat(typeInfo.Type)
//
rv := &Mapping{
Keys: []string{},
Indeces: map[string][]int{},
StructFields: map[string]reflect.StructField{},
ReflectPaths: map[string]path.ReflectPath{},
}
//
// add adds an entry to rv.
add := func(name string, index []int, field reflect.StructField, path path.Path) {
rv.Keys = append(rv.Keys, name)
rv.Indeces[name] = index
rv.StructFields[name] = field
rv.ReflectPaths[name] = path.ReflectPath()
if len(path.PathwayIndex) > 1 {
rv.HasPointers = true
}
}
//
// NB fullpath is used to refer back into paths to obtain a path.Path for a mapped pathway.
// Practically this is a bit slower than coalescing the logic of path.Stat into
// this scan() function here. Conceptually it's much easier to understand, support
// and maintain.
var scan func(typeInfo TypeInfo, indeces []int, prefix string, fullpath string)
scan = func(typeInfo TypeInfo, indeces []int, prefix string, fullpath string) {
// Separate fullpath with underscore.
if fullpath != "" {
fullpath = fullpath + "."
}
for k, field := range typeInfo.StructFields {
if field.PkgPath != "" {
continue
}
fieldTypeInfo := TypeCache.StatType(field.Type)
if me.Ignored.Has(fieldTypeInfo.Type) {
continue
}
//
name, hadTag := "", false
if !me.Elevated.Has(fieldTypeInfo.Type) {
for _, tagName := range append(me.Tags, "") {
if tagValue, ok := field.Tag.Lookup(tagName); ok {
name, hadTag = tagValue, true
break
} else if tagName == "" {
name = field.Name
if me.Transform != nil {
name = me.Transform(name)
}
break
}
}
}
// When tagged fields are required but name was not set via tag.
if me.TaggedFieldsOnly && !hadTag {
continue
}
//
if prefix != "" && name != "" {
name = prefix + me.Join + name
} else if prefix != "" {
name = prefix
}
nameIndeces := append(indeces, k)
if _, ok := mapperTreatAsScalar[fieldTypeInfo.Type]; ok {
add(name, nameIndeces, field, paths.Leaves[fullpath+field.Name])
} else if _, ok = me.TreatAsScalar[fieldTypeInfo.Type]; ok {
// When the type is treated as scalar first search Branches and then Leaves
// for the associated path information.
if p, ok := paths.Branches[fullpath+field.Name]; ok {
add(name, nameIndeces, field, p)
} else if p, ok = paths.Leaves[fullpath+field.Name]; ok {
add(name, nameIndeces, field, p)
}
} else if fieldTypeInfo.IsStruct {
scan(fieldTypeInfo, nameIndeces, name, fullpath+field.Name)
} else if fieldTypeInfo.IsScalar {
add(name, nameIndeces, field, paths.Leaves[fullpath+field.Name])
}
}
}
// Scan and assign the result to our known types.
scan(typeInfo, []int(nil), "", "")
me.known.Store(typeInfo.Type, rv)
//
return *rv
}
// Prepare creates a PreparedMapping that is initially bound to I.
//
// PreparedMappings are provided for performance critical code that needs to
// read or mutate many instances of the same type repeatedly and for every
// instance the same fields will be accessed in the same order.
//
// See documentation for PreparedMapping for more details.
//
// I must be an addressable type.
func (me *Mapper) Prepare(I interface{}) (PreparedMapping, error) {
var value reflect.Value
var typ reflect.Type
var writable bool
switch sw := I.(type) {
case reflect.Value:
typ = sw.Type()
value, writable = Writable(sw)
default:
value, writable = Writable(reflect.ValueOf(I))
typ = reflect.TypeOf(I)
}
if !writable {
typeStr := typ.String()
err := pkgerr{
Err: ErrReadOnly,
CallSite: "Mapper.Prepare",
Hint: "call to Mapper.Prepare(" + typeStr + ") should have been Mapper.Prepare(*" + typeStr + ")",
}
return PreparedMapping{err: err}, err
}
//
mapping := me.Map(I)
//
rv := PreparedMapping{
top: typ,
value: value,
// Set err to ErrNoPlan; previously it was set to a new instance of pkgerr{Err:ErrNoPlay,Hint:"a hint"+typ.String()}
// but this creates many allocations and hurts performance.
err: ErrNoPlan,
paths: mapping.ReflectPaths,
}
return rv, nil
}
// Copy creates a copy of the Mapping.
func (me Mapping) Copy() Mapping {
rv := Mapping{
Keys: append([]string(nil), me.Keys...),
Indeces: map[string][]int{},
StructFields: map[string]reflect.StructField{},
ReflectPaths: map[string]path.ReflectPath{},
}
for _, key := range me.Keys {
rv.Indeces[key] = append([]int(nil), me.Indeces[key]...)
rv.StructFields[key] = me.StructFields[key]
rv.ReflectPaths[key] = me.ReflectPaths[key]
}
return rv
}
// Get returns the indeces associated with key in the mapping. If no such key
// is found a nil slice is returned.
func (me Mapping) Get(key string) []int {
v, _ := me.Lookup(key)
return v
}
// Lookup returns the value associated with key in the mapping. If no such key is
// found a nil slice is returned and ok is false; otherwise ok is true.
func (me Mapping) Lookup(key string) (indeces []int, ok bool) {
indeces, ok = me.Indeces[key]
return indeces, ok
}
// String returns the Mapping as a string value.
func (me Mapping) String() string {
parts := []string{}
for str, indeces := range me.Indeces {
parts = append(parts, fmt.Sprintf("%v\t\t%v", indeces, str))
}
sort.Strings(parts)
return strings.Join(parts, "\n")
}