-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfield_matcher.go
222 lines (192 loc) · 6.23 KB
/
field_matcher.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
package conv
import (
"reflect"
"strings"
"sync"
"unicode"
)
/*
Define FieldMatcher and provide some built-in implementation.
*/
// FieldMatcherCreator is used to create FieldMatcher instances when converting from map to struct or
// from struct to struct.
type FieldMatcherCreator interface {
// GetMatcher returns a FieldMatcher for the given type of struct.
// If the type is not Struct, it panics.
GetMatcher(typ reflect.Type) FieldMatcher
}
// FieldMatcher is used to match names when converting from map to struct or from struct to struct.
type FieldMatcher interface {
// MatchField returns the first matched field for the given name;
// if no name can match, returns a zero value and false.
// The field returned must be an exported field.
MatchField(name string) (field reflect.StructField, ok bool)
}
// SimpleMatcherConfig configures SimpleMatcherCreator.
type SimpleMatcherConfig struct {
// Tag specifies the tag name for the fields. When a name is given by the tag, the matcher
// matches the field using the given name; otherwise the raw field name is used.
// The tag works like some commonly used tags such as 'json', 'xml'.
// Tag can be empty.
//
// e.g. When 'conv' is given as the tag name, the matcher returns the OldName field when
// indexing 'NewName'.
// type Target struct {
// OldName int `conv:"NewName"` // 'NewName' can match this field.
// RawName // No tag specified, use 'RawName' for field matching.
// }
//
Tag string
// CaseInsensitive specifies whether the matcher matches field names in a case-insensitive manner.
// If this field is true, CamelSnakeCase is ignored.
//
// If the field is true, 'ab', 'Ab', 'aB', 'AB' are equal.
//
CaseInsensitive bool
// OmitUnderscore specifies whether to omit underscores in field names.
// If this field is true, CamelSnakeCase is ignored.
//
// If the field is true, 'ab', 'a_b', '_ab', 'a__b_' are equal.
//
OmitUnderscore bool
// CamelSnakeCase whether to support camel-case and snake-case name comparing.
// If CaseInsensitive or OmitUnderscore is true, this field is ignored.
//
// When it is set to true, the matcher can match names in camel-case or snake-case form, such as
// 'lowerCaseCamel' or 'UpperCaseCamel' or 'snake_case' or 'Snake_Case' (sometimes called train-case).
//
// The first rune of each word is compared case-insensitively; others are compared in the case-sensitive
// manner. For example:
// - These names are equal: aaBB, AaBb, aa_bb, Aa_Bb, aa_BB
// - These names are not equal: aa_bb, Aabb, aabB, _aaBb, AaBb_, Aa__Bb
//
// Mostly this option can be used to match field names that come from different platform, e.g.,
// 'lowerCaseCamel' from Javascript, 'UpperCaseCamel' from Go, 'snake_case' from Mysql database.
//
CamelSnakeCase bool
}
// SimpleMatcherCreator returns an instance of FieldMatcherCreator.
// It is used as the default value when Conv.Config.FieldMatcher is nil.
type SimpleMatcherCreator struct {
Conf SimpleMatcherConfig
m syncMap
}
// GetMatcher implements FieldMatcherCreator.GetMatcher().
func (c *SimpleMatcherCreator) GetMatcher(typ reflect.Type) FieldMatcher {
v, _ := c.m.LoadOrStore(typ, &simpleMatcher{
conf: c.Conf,
typ: typ,
})
return v.(*simpleMatcher)
}
// simpleMatcher is the FieldMatcher returned by SimpleMatcherCreator.
type simpleMatcher struct {
conf SimpleMatcherConfig // Conf configures the matcher.
typ reflect.Type // The type of the struct.
fs *syncMap // The fields. A thread-safe map[string]fieldInfo.
mu sync.Mutex // Used to initialize fs.
}
func (ix *simpleMatcher) MatchField(name string) (reflect.StructField, bool) {
// Init field mapping with double-lock check.
// mu is used only to initialize fs, fs itself is thread-safe and doesn't need another lock.
if ix.fs == nil {
ix.mu.Lock()
if ix.fs == nil {
ix.initFieldMap()
}
ix.mu.Unlock()
}
name = ix.fixName(name)
if f, ok := ix.fs.Load(name); ok {
return f.(FieldInfo).StructField, ok
}
return reflect.StructField{}, false
}
func (ix *simpleMatcher) initFieldMap() {
m := new(syncMap)
walker := NewFieldWalker(ix.typ, ix.conf.Tag)
walker.WalkFields(func(fi FieldInfo) bool {
// If a tag name is specified, use it; otherwise, use the raw field name.
name := fi.TagValue
if name == "" {
name = fi.Name
}
name = ix.fixName(name)
// As FieldMatcher.IndexName() says, it returns the first matched name,
// When two field named may be transformed to the same name, we keep the first one.
m.LoadOrStore(name, fi)
return true
})
ix.fs = m
}
func (ix *simpleMatcher) fixName(name string) string {
supportCamel := true
if ix.conf.CaseInsensitive {
name = strings.ToLower(name)
supportCamel = false
}
if ix.conf.OmitUnderscore {
name = strings.Replace(name, "_", "", -1)
supportCamel = false
}
if supportCamel && ix.conf.CamelSnakeCase {
name = ix.fixCamelSnakeCaseName([]rune(name))
}
return name
}
// fixCamelSnakeCaseName transforms first runes of each word to '_c' format, 'c' is the rune in lower-case. e.g.:
//
// aaBB -> _aa_b_b
// AaBb -> _aa_bb
// _a_b_ -> __a_b_
//
// c is the first rune of a word if any of:
// - Case 1: The first rune of the name.
// - Case 2: An uppercase rune.
// - Case 3: A rune after a *single* underscore, and the underscore is not the first rune of a word.
func (ix *simpleMatcher) fixCamelSnakeCaseName(name []rune) string {
var b strings.Builder
b.Grow(len(name))
const (
sWordStart byte = 's' // The first rune of a word.
sDelimiter byte = 'd' // A _ as a delimiter.
sNonDelimiter byte = 'n' // A non-delimiter rune.
)
state := sWordStart
for i := 0; i < len(name); i++ {
c := name[i]
// Case 1 & 2.
if i == 0 || unicode.IsUpper(c) {
state = sWordStart
goto ensured
}
// Case 3.
if state == sDelimiter {
state = sWordStart
goto ensured
}
if c != '_' {
state = sNonDelimiter
goto ensured
}
// c is _.
switch state {
case sWordStart:
fallthrough
case sNonDelimiter:
if i < len(name)-1 {
state = sDelimiter
continue
}
state = sNonDelimiter // c is the last rune.
}
ensured:
if state == sWordStart {
b.WriteByte('_')
b.WriteRune(unicode.ToLower(c))
} else {
b.WriteRune(c)
}
}
return b.String()
}