git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/schema/cache.go (about) 1 // Copyright 2012 The Gorilla Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package schema 6 7 import ( 8 "errors" 9 "reflect" 10 "strconv" 11 "strings" 12 "sync" 13 ) 14 15 var errInvalidPath = errors.New("schema: invalid path") 16 17 // newCache returns a new cache. 18 func newCache() *cache { 19 c := cache{ 20 m: make(map[reflect.Type]*structInfo), 21 regconv: make(map[reflect.Type]Converter), 22 tag: "schema", 23 } 24 return &c 25 } 26 27 // cache caches meta-data about a struct. 28 type cache struct { 29 l sync.RWMutex 30 m map[reflect.Type]*structInfo 31 regconv map[reflect.Type]Converter 32 tag string 33 } 34 35 // registerConverter registers a converter function for a custom type. 36 func (c *cache) registerConverter(value interface{}, converterFunc Converter) { 37 c.regconv[reflect.TypeOf(value)] = converterFunc 38 } 39 40 // parsePath parses a path in dotted notation verifying that it is a valid 41 // path to a struct field. 42 // 43 // It returns "path parts" which contain indices to fields to be used by 44 // reflect.Value.FieldByString(). Multiple parts are required for slices of 45 // structs. 46 func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) { 47 var struc *structInfo 48 var field *fieldInfo 49 var index64 int64 50 var err error 51 parts := make([]pathPart, 0) 52 path := make([]string, 0) 53 keys := strings.Split(p, ".") 54 for i := 0; i < len(keys); i++ { 55 if t.Kind() != reflect.Struct { 56 return nil, errInvalidPath 57 } 58 if struc = c.get(t); struc == nil { 59 return nil, errInvalidPath 60 } 61 if field = struc.get(keys[i]); field == nil { 62 return nil, errInvalidPath 63 } 64 // Valid field. Append index. 65 path = append(path, field.name) 66 if field.isSliceOfStructs && (!field.unmarshalerInfo.IsValid || (field.unmarshalerInfo.IsValid && field.unmarshalerInfo.IsSliceElement)) { 67 // Parse a special case: slices of structs. 68 // i+1 must be the slice index. 69 // 70 // Now that struct can implements TextUnmarshaler interface, 71 // we don't need to force the struct's fields to appear in the path. 72 // So checking i+2 is not necessary anymore. 73 i++ 74 if i+1 > len(keys) { 75 return nil, errInvalidPath 76 } 77 if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil { 78 return nil, errInvalidPath 79 } 80 parts = append(parts, pathPart{ 81 path: path, 82 field: field, 83 index: int(index64), 84 }) 85 path = make([]string, 0) 86 87 // Get the next struct type, dropping ptrs. 88 if field.typ.Kind() == reflect.Ptr { 89 t = field.typ.Elem() 90 } else { 91 t = field.typ 92 } 93 if t.Kind() == reflect.Slice { 94 t = t.Elem() 95 if t.Kind() == reflect.Ptr { 96 t = t.Elem() 97 } 98 } 99 } else if field.typ.Kind() == reflect.Ptr { 100 t = field.typ.Elem() 101 } else { 102 t = field.typ 103 } 104 } 105 // Add the remaining. 106 parts = append(parts, pathPart{ 107 path: path, 108 field: field, 109 index: -1, 110 }) 111 return parts, nil 112 } 113 114 // get returns a cached structInfo, creating it if necessary. 115 func (c *cache) get(t reflect.Type) *structInfo { 116 c.l.RLock() 117 info := c.m[t] 118 c.l.RUnlock() 119 if info == nil { 120 info = c.create(t, "") 121 c.l.Lock() 122 c.m[t] = info 123 c.l.Unlock() 124 } 125 return info 126 } 127 128 // create creates a structInfo with meta-data about a struct. 129 func (c *cache) create(t reflect.Type, parentAlias string) *structInfo { 130 info := &structInfo{} 131 var anonymousInfos []*structInfo 132 for i := 0; i < t.NumField(); i++ { 133 if f := c.createField(t.Field(i), parentAlias); f != nil { 134 info.fields = append(info.fields, f) 135 if ft := indirectType(f.typ); ft.Kind() == reflect.Struct && f.isAnonymous { 136 anonymousInfos = append(anonymousInfos, c.create(ft, f.canonicalAlias)) 137 } 138 } 139 } 140 for i, a := range anonymousInfos { 141 others := []*structInfo{info} 142 others = append(others, anonymousInfos[:i]...) 143 others = append(others, anonymousInfos[i+1:]...) 144 for _, f := range a.fields { 145 if !containsAlias(others, f.alias) { 146 info.fields = append(info.fields, f) 147 } 148 } 149 } 150 return info 151 } 152 153 // createField creates a fieldInfo for the given field. 154 func (c *cache) createField(field reflect.StructField, parentAlias string) *fieldInfo { 155 alias, options := fieldAlias(field, c.tag) 156 if alias == "-" { 157 // Ignore this field. 158 return nil 159 } 160 canonicalAlias := alias 161 if parentAlias != "" { 162 canonicalAlias = parentAlias + "." + alias 163 } 164 // Check if the type is supported and don't cache it if not. 165 // First let's get the basic type. 166 isSlice, isStruct := false, false 167 ft := field.Type 168 m := isTextUnmarshaler(reflect.Zero(ft)) 169 if ft.Kind() == reflect.Ptr { 170 ft = ft.Elem() 171 } 172 if isSlice = ft.Kind() == reflect.Slice; isSlice { 173 ft = ft.Elem() 174 if ft.Kind() == reflect.Ptr { 175 ft = ft.Elem() 176 } 177 } 178 if ft.Kind() == reflect.Array { 179 ft = ft.Elem() 180 if ft.Kind() == reflect.Ptr { 181 ft = ft.Elem() 182 } 183 } 184 if isStruct = ft.Kind() == reflect.Struct; !isStruct { 185 if c.converter(ft) == nil && builtinConverters[ft.Kind()] == nil { 186 // Type is not supported. 187 return nil 188 } 189 } 190 191 return &fieldInfo{ 192 typ: field.Type, 193 name: field.Name, 194 alias: alias, 195 canonicalAlias: canonicalAlias, 196 unmarshalerInfo: m, 197 isSliceOfStructs: isSlice && isStruct, 198 isAnonymous: field.Anonymous, 199 isRequired: options.Contains("required"), 200 } 201 } 202 203 // converter returns the converter for a type. 204 func (c *cache) converter(t reflect.Type) Converter { 205 return c.regconv[t] 206 } 207 208 // ---------------------------------------------------------------------------- 209 210 type structInfo struct { 211 fields []*fieldInfo 212 } 213 214 func (i *structInfo) get(alias string) *fieldInfo { 215 for _, field := range i.fields { 216 if strings.EqualFold(field.alias, alias) { 217 return field 218 } 219 } 220 return nil 221 } 222 223 func containsAlias(infos []*structInfo, alias string) bool { 224 for _, info := range infos { 225 if info.get(alias) != nil { 226 return true 227 } 228 } 229 return false 230 } 231 232 type fieldInfo struct { 233 typ reflect.Type 234 // name is the field name in the struct. 235 name string 236 alias string 237 // canonicalAlias is almost the same as the alias, but is prefixed with 238 // an embedded struct field alias in dotted notation if this field is 239 // promoted from the struct. 240 // For instance, if the alias is "N" and this field is an embedded field 241 // in a struct "X", canonicalAlias will be "X.N". 242 canonicalAlias string 243 // unmarshalerInfo contains information regarding the 244 // encoding.TextUnmarshaler implementation of the field type. 245 unmarshalerInfo unmarshaler 246 // isSliceOfStructs indicates if the field type is a slice of structs. 247 isSliceOfStructs bool 248 // isAnonymous indicates whether the field is embedded in the struct. 249 isAnonymous bool 250 isRequired bool 251 } 252 253 func (f *fieldInfo) paths(prefix string) []string { 254 if f.alias == f.canonicalAlias { 255 return []string{prefix + f.alias} 256 } 257 return []string{prefix + f.alias, prefix + f.canonicalAlias} 258 } 259 260 type pathPart struct { 261 field *fieldInfo 262 path []string // path to the field: walks structs using field names. 263 index int // struct index in slices of structs. 264 } 265 266 // ---------------------------------------------------------------------------- 267 268 func indirectType(typ reflect.Type) reflect.Type { 269 if typ.Kind() == reflect.Ptr { 270 return typ.Elem() 271 } 272 return typ 273 } 274 275 // fieldAlias parses a field tag to get a field alias. 276 func fieldAlias(field reflect.StructField, tagName string) (alias string, options tagOptions) { 277 if tag := field.Tag.Get(tagName); tag != "" { 278 alias, options = parseTag(tag) 279 } 280 if alias == "" { 281 alias = field.Name 282 } 283 return alias, options 284 } 285 286 // tagOptions is the string following a comma in a struct field's tag, or 287 // the empty string. It does not include the leading comma. 288 type tagOptions []string 289 290 // parseTag splits a struct field's url tag into its name and comma-separated 291 // options. 292 func parseTag(tag string) (string, tagOptions) { 293 s := strings.Split(tag, ",") 294 return s[0], s[1:] 295 } 296 297 // Contains checks whether the tagOptions contains the specified option. 298 func (o tagOptions) Contains(option string) bool { 299 for _, s := range o { 300 if s == option { 301 return true 302 } 303 } 304 return false 305 }