269 lines
6.3 KiB
Go
269 lines
6.3 KiB
Go
// 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),
|
|
conv: make(map[reflect.Kind]Converter),
|
|
regconv: make(map[reflect.Type]Converter),
|
|
tag: "schema",
|
|
}
|
|
for k, v := range converters {
|
|
c.conv[k] = v
|
|
}
|
|
return &c
|
|
}
|
|
|
|
// cache caches meta-data about a struct.
|
|
type cache struct {
|
|
l sync.RWMutex
|
|
m map[reflect.Type]*structInfo
|
|
conv map[reflect.Kind]Converter
|
|
regconv map[reflect.Type]Converter
|
|
tag string
|
|
}
|
|
|
|
// 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.ss {
|
|
// 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, nil)
|
|
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, info *structInfo) *structInfo {
|
|
if info == nil {
|
|
info = &structInfo{fields: []*fieldInfo{}}
|
|
}
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
if field.Anonymous {
|
|
ft := field.Type
|
|
if ft.Kind() == reflect.Ptr {
|
|
ft = ft.Elem()
|
|
}
|
|
if ft.Kind() == reflect.Struct {
|
|
bef := len(info.fields)
|
|
c.create(ft, info)
|
|
for _, fi := range info.fields[bef:len(info.fields)] {
|
|
// exclude required check because duplicated to embedded field
|
|
fi.required = false
|
|
}
|
|
}
|
|
}
|
|
c.createField(field, info)
|
|
}
|
|
return info
|
|
}
|
|
|
|
// createField creates a fieldInfo for the given field.
|
|
func (c *cache) createField(field reflect.StructField, info *structInfo) {
|
|
alias, options := fieldAlias(field, c.tag)
|
|
if alias == "-" {
|
|
// Ignore this field.
|
|
return
|
|
}
|
|
// 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
|
|
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 conv := c.converter(ft); conv == nil {
|
|
// Type is not supported.
|
|
return
|
|
}
|
|
}
|
|
|
|
info.fields = append(info.fields, &fieldInfo{
|
|
typ: field.Type,
|
|
name: field.Name,
|
|
ss: isSlice && isStruct,
|
|
alias: alias,
|
|
anon: field.Anonymous,
|
|
required: options.Contains("required"),
|
|
})
|
|
}
|
|
|
|
// converter returns the converter for a type.
|
|
func (c *cache) converter(t reflect.Type) Converter {
|
|
conv := c.regconv[t]
|
|
if conv == nil {
|
|
conv = c.conv[t.Kind()]
|
|
}
|
|
return conv
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
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
|
|
}
|
|
|
|
type fieldInfo struct {
|
|
typ reflect.Type
|
|
name string // field name in the struct.
|
|
ss bool // true if this is a slice of structs.
|
|
alias string
|
|
anon bool // is an embedded field
|
|
required bool // tag option
|
|
}
|
|
|
|
type pathPart struct {
|
|
field *fieldInfo
|
|
path []string // path to the field: walks structs using field names.
|
|
index int // struct index in slices of structs.
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// 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
|
|
}
|