Permalink
// Copyright 2012 The Gorilla Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
package schema | |
import ( | |
"errors" | |
"reflect" | |
"strconv" | |
"strings" | |
"sync" | |
) | |
var invalidPath = errors.New("schema: invalid path") | |
// newCache returns a new cache. | |
func newCache() *cache { | |
c := cache{ | |
m: make(map[reflect.Type]*structInfo), | |
regconv: make(map[reflect.Type]Converter), | |
tag: "schema", | |
} | |
return &c | |
} | |
// cache caches meta-data about a struct. | |
type cache struct { | |
l sync.RWMutex | |
m map[reflect.Type]*structInfo | |
regconv map[reflect.Type]Converter | |
tag string | |
} | |
// registerConverter registers a converter function for a custom type. | |
func (c *cache) registerConverter(value interface{}, converterFunc Converter) { | |
c.regconv[reflect.TypeOf(value)] = converterFunc | |
} | |
// parsePath parses a path in dotted notation verifying that it is a valid | |
// path to a struct field. | |
// | |
// It returns "path parts" which contain indices to fields to be used by | |
// reflect.Value.FieldByString(). Multiple parts are required for slices of | |
// structs. | |
func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) { | |
var struc *structInfo | |
var field *fieldInfo | |
var index64 int64 | |
var err error | |
parts := make([]pathPart, 0) | |
path := make([]string, 0) | |
keys := strings.Split(p, ".") | |
for i := 0; i < len(keys); i++ { | |
if t.Kind() != reflect.Struct { | |
return nil, invalidPath | |
} | |
if struc = c.get(t); struc == nil { | |
return nil, invalidPath | |
} | |
if field = struc.get(keys[i]); field == nil { | |
return nil, invalidPath | |
} | |
// Valid field. Append index. | |
path = append(path, field.name) | |
if field.isSliceOfStructs && (!field.unmarshalerInfo.IsValid || (field.unmarshalerInfo.IsValid && field.unmarshalerInfo.IsSliceElement)) { | |
// Parse a special case: slices of structs. | |
// i+1 must be the slice index. | |
// | |
// Now that struct can implements TextUnmarshaler interface, | |
// we don't need to force the struct's fields to appear in the path. | |
// So checking i+2 is not necessary anymore. | |
i++ | |
if i+1 > len(keys) { | |
return nil, invalidPath | |
} | |
if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil { | |
return nil, invalidPath | |
} | |
parts = append(parts, pathPart{ | |
path: path, | |
field: field, | |
index: int(index64), | |
}) | |
path = make([]string, 0) | |
// Get the next struct type, dropping ptrs. | |
if field.typ.Kind() == reflect.Ptr { | |
t = field.typ.Elem() | |
} else { | |
t = field.typ | |
} | |
if t.Kind() == reflect.Slice { | |
t = t.Elem() | |
if t.Kind() == reflect.Ptr { | |
t = t.Elem() | |
} | |
} | |
} else if field.typ.Kind() == reflect.Ptr { | |
t = field.typ.Elem() | |
} else { | |
t = field.typ | |
} | |
} | |
// Add the remaining. | |
parts = append(parts, pathPart{ | |
path: path, | |
field: field, | |
index: -1, | |
}) | |
return parts, nil | |
} | |
// get returns a cached structInfo, creating it if necessary. | |
func (c *cache) get(t reflect.Type) *structInfo { | |
c.l.RLock() | |
info := c.m[t] | |
c.l.RUnlock() | |
if info == nil { | |
info = c.create(t, "") | |
c.l.Lock() | |
c.m[t] = info | |
c.l.Unlock() | |
} | |
return info | |
} | |
// create creates a structInfo with meta-data about a struct. | |
func (c *cache) create(t reflect.Type, parentAlias string) *structInfo { | |
info := &structInfo{} | |
var anonymousInfos []*structInfo | |
for i := 0; i < t.NumField(); i++ { | |
if f := c.createField(t.Field(i), parentAlias); f != nil { | |
info.fields = append(info.fields, f) | |
if ft := indirectType(f.typ); ft.Kind() == reflect.Struct && f.isAnonymous { | |
anonymousInfos = append(anonymousInfos, c.create(ft, f.canonicalAlias)) | |
} | |
} | |
} | |
for i, a := range anonymousInfos { | |
others := []*structInfo{info} | |
others = append(others, anonymousInfos[:i]...) | |
others = append(others, anonymousInfos[i+1:]...) | |
for _, f := range a.fields { | |
if !containsAlias(others, f.alias) { | |
info.fields = append(info.fields, f) | |
} | |
} | |
} | |
return info | |
} | |
// createField creates a fieldInfo for the given field. | |
func (c *cache) createField(field reflect.StructField, parentAlias string) *fieldInfo { | |
alias, options := fieldAlias(field, c.tag) | |
if alias == "-" { | |
// Ignore this field. | |
return nil | |
} | |
canonicalAlias := alias | |
if parentAlias != "" { | |
canonicalAlias = parentAlias + "." + alias | |
} | |
// Check if the type is supported and don't cache it if not. | |
// First let's get the basic type. | |
isSlice, isStruct := false, false | |
ft := field.Type | |
m := isTextUnmarshaler(reflect.Zero(ft)) | |
if ft.Kind() == reflect.Ptr { | |
ft = ft.Elem() | |
} | |
if isSlice = ft.Kind() == reflect.Slice; isSlice { | |
ft = ft.Elem() | |
if ft.Kind() == reflect.Ptr { | |
ft = ft.Elem() | |
} | |
} | |
if ft.Kind() == reflect.Array { | |
ft = ft.Elem() | |
if ft.Kind() == reflect.Ptr { | |
ft = ft.Elem() | |
} | |
} | |
if isStruct = ft.Kind() == reflect.Struct; !isStruct { | |
if c.converter(ft) == nil && builtinConverters[ft.Kind()] == nil { | |
// Type is not supported. | |
return nil | |
} | |
} | |
return &fieldInfo{ | |
typ: field.Type, | |
name: field.Name, | |
alias: alias, | |
canonicalAlias: canonicalAlias, | |
unmarshalerInfo: m, | |
isSliceOfStructs: isSlice && isStruct, | |
isAnonymous: field.Anonymous, | |
isRequired: options.Contains("required"), | |
} | |
} | |
// converter returns the converter for a type. | |
func (c *cache) converter(t reflect.Type) Converter { | |
return c.regconv[t] | |
} | |
// ---------------------------------------------------------------------------- | |
type structInfo struct { | |
fields []*fieldInfo | |
} | |
func (i *structInfo) get(alias string) *fieldInfo { | |
for _, field := range i.fields { | |
if strings.EqualFold(field.alias, alias) { | |
return field | |
} | |
} | |
return nil | |
} | |
func containsAlias(infos []*structInfo, alias string) bool { | |
for _, info := range infos { | |
if info.get(alias) != nil { | |
return true | |
} | |
} | |
return false | |
} | |
type fieldInfo struct { | |
typ reflect.Type | |
// name is the field name in the struct. | |
name string | |
alias string | |
// canonicalAlias is almost the same as the alias, but is prefixed with | |
// an embedded struct field alias in dotted notation if this field is | |
// promoted from the struct. | |
// For instance, if the alias is "N" and this field is an embedded field | |
// in a struct "X", canonicalAlias will be "X.N". | |
canonicalAlias string | |
// unmarshalerInfo contains information regarding the | |
// encoding.TextUnmarshaler implementation of the field type. | |
unmarshalerInfo unmarshaler | |
// isSliceOfStructs indicates if the field type is a slice of structs. | |
isSliceOfStructs bool | |
// isAnonymous indicates whether the field is embedded in the struct. | |
isAnonymous bool | |
isRequired bool | |
} | |
func (f *fieldInfo) paths(prefix string) []string { | |
if f.alias == f.canonicalAlias { | |
return []string{prefix + f.alias} | |
} | |
return []string{prefix + f.alias, prefix + f.canonicalAlias} | |
} | |
type pathPart struct { | |
field *fieldInfo | |
path []string // path to the field: walks structs using field names. | |
index int // struct index in slices of structs. | |
} | |
// ---------------------------------------------------------------------------- | |
func indirectType(typ reflect.Type) reflect.Type { | |
if typ.Kind() == reflect.Ptr { | |
return typ.Elem() | |
} | |
return typ | |
} | |
// fieldAlias parses a field tag to get a field alias. | |
func fieldAlias(field reflect.StructField, tagName string) (alias string, options tagOptions) { | |
if tag := field.Tag.Get(tagName); tag != "" { | |
alias, options = parseTag(tag) | |
} | |
if alias == "" { | |
alias = field.Name | |
} | |
return alias, options | |
} | |
// tagOptions is the string following a comma in a struct field's tag, or | |
// the empty string. It does not include the leading comma. | |
type tagOptions []string | |
// parseTag splits a struct field's url tag into its name and comma-separated | |
// options. | |
func parseTag(tag string) (string, tagOptions) { | |
s := strings.Split(tag, ",") | |
return s[0], s[1:] | |
} | |
// Contains checks whether the tagOptions contains the specified option. | |
func (o tagOptions) Contains(option string) bool { | |
for _, s := range o { | |
if s == option { | |
return true | |
} | |
} | |
return false | |
} |