You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
320 lines
6.8 KiB
320 lines
6.8 KiB
package validator |
|
|
|
import ( |
|
"fmt" |
|
"reflect" |
|
"strings" |
|
"sync" |
|
"sync/atomic" |
|
) |
|
|
|
type tagType uint8 |
|
|
|
const ( |
|
typeDefault tagType = iota |
|
typeOmitEmpty |
|
typeIsDefault |
|
typeNoStructLevel |
|
typeStructOnly |
|
typeDive |
|
typeOr |
|
typeKeys |
|
typeEndKeys |
|
) |
|
|
|
const ( |
|
invalidValidation = "Invalid validation tag on field '%s'" |
|
undefinedValidation = "Undefined validation function '%s' on field '%s'" |
|
keysTagNotDefined = "'" + endKeysTag + "' tag encountered without a corresponding '" + keysTag + "' tag" |
|
) |
|
|
|
type structCache struct { |
|
lock sync.Mutex |
|
m atomic.Value // map[reflect.Type]*cStruct |
|
} |
|
|
|
func (sc *structCache) Get(key reflect.Type) (c *cStruct, found bool) { |
|
c, found = sc.m.Load().(map[reflect.Type]*cStruct)[key] |
|
return |
|
} |
|
|
|
func (sc *structCache) Set(key reflect.Type, value *cStruct) { |
|
|
|
m := sc.m.Load().(map[reflect.Type]*cStruct) |
|
|
|
nm := make(map[reflect.Type]*cStruct, len(m)+1) |
|
for k, v := range m { |
|
nm[k] = v |
|
} |
|
nm[key] = value |
|
sc.m.Store(nm) |
|
} |
|
|
|
type tagCache struct { |
|
lock sync.Mutex |
|
m atomic.Value // map[string]*cTag |
|
} |
|
|
|
func (tc *tagCache) Get(key string) (c *cTag, found bool) { |
|
c, found = tc.m.Load().(map[string]*cTag)[key] |
|
return |
|
} |
|
|
|
func (tc *tagCache) Set(key string, value *cTag) { |
|
|
|
m := tc.m.Load().(map[string]*cTag) |
|
|
|
nm := make(map[string]*cTag, len(m)+1) |
|
for k, v := range m { |
|
nm[k] = v |
|
} |
|
nm[key] = value |
|
tc.m.Store(nm) |
|
} |
|
|
|
type cStruct struct { |
|
name string |
|
fields []*cField |
|
fn StructLevelFuncCtx |
|
} |
|
|
|
type cField struct { |
|
idx int |
|
name string |
|
altName string |
|
namesEqual bool |
|
cTags *cTag |
|
} |
|
|
|
type cTag struct { |
|
tag string |
|
aliasTag string |
|
actualAliasTag string |
|
param string |
|
typeof tagType |
|
keys *cTag // only populated when using tag's 'keys' and 'endkeys' for map key validation |
|
next *cTag |
|
hasTag bool |
|
hasAlias bool |
|
hasParam bool // true if parameter used eg. eq= where the equal sign has been set |
|
isBlockEnd bool // indicates the current tag represents the last validation in the block |
|
fn FuncCtx |
|
} |
|
|
|
func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStruct { |
|
|
|
v.structCache.lock.Lock() |
|
defer v.structCache.lock.Unlock() // leave as defer! because if inner panics, it will never get unlocked otherwise! |
|
|
|
typ := current.Type() |
|
|
|
// could have been multiple trying to access, but once first is done this ensures struct |
|
// isn't parsed again. |
|
cs, ok := v.structCache.Get(typ) |
|
if ok { |
|
return cs |
|
} |
|
|
|
cs = &cStruct{name: sName, fields: make([]*cField, 0), fn: v.structLevelFuncs[typ]} |
|
|
|
numFields := current.NumField() |
|
|
|
var ctag *cTag |
|
var fld reflect.StructField |
|
var tag string |
|
var customName string |
|
|
|
for i := 0; i < numFields; i++ { |
|
|
|
fld = typ.Field(i) |
|
|
|
if !fld.Anonymous && len(fld.PkgPath) > 0 { |
|
continue |
|
} |
|
|
|
tag = fld.Tag.Get(v.tagName) |
|
|
|
if tag == skipValidationTag { |
|
continue |
|
} |
|
|
|
customName = fld.Name |
|
|
|
if v.hasTagNameFunc { |
|
|
|
name := v.tagNameFunc(fld) |
|
|
|
if len(name) > 0 { |
|
customName = name |
|
} |
|
} |
|
|
|
// NOTE: cannot use shared tag cache, because tags may be equal, but things like alias may be different |
|
// and so only struct level caching can be used instead of combined with Field tag caching |
|
|
|
if len(tag) > 0 { |
|
ctag, _ = v.parseFieldTagsRecursive(tag, fld.Name, "", false) |
|
} else { |
|
// even if field doesn't have validations need cTag for traversing to potential inner/nested |
|
// elements of the field. |
|
ctag = new(cTag) |
|
} |
|
|
|
cs.fields = append(cs.fields, &cField{ |
|
idx: i, |
|
name: fld.Name, |
|
altName: customName, |
|
cTags: ctag, |
|
namesEqual: fld.Name == customName, |
|
}) |
|
} |
|
|
|
v.structCache.Set(typ, cs) |
|
|
|
return cs |
|
} |
|
|
|
func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) { |
|
|
|
var t string |
|
var ok bool |
|
noAlias := len(alias) == 0 |
|
tags := strings.Split(tag, tagSeparator) |
|
|
|
for i := 0; i < len(tags); i++ { |
|
|
|
t = tags[i] |
|
|
|
if noAlias { |
|
alias = t |
|
} |
|
|
|
// check map for alias and process new tags, otherwise process as usual |
|
if tagsVal, found := v.aliases[t]; found { |
|
if i == 0 { |
|
firstCtag, current = v.parseFieldTagsRecursive(tagsVal, fieldName, t, true) |
|
} else { |
|
next, curr := v.parseFieldTagsRecursive(tagsVal, fieldName, t, true) |
|
current.next, current = next, curr |
|
|
|
} |
|
|
|
continue |
|
} |
|
|
|
var prevTag tagType |
|
|
|
if i == 0 { |
|
current = &cTag{aliasTag: alias, hasAlias: hasAlias, hasTag: true} |
|
firstCtag = current |
|
} else { |
|
prevTag = current.typeof |
|
current.next = &cTag{aliasTag: alias, hasAlias: hasAlias, hasTag: true} |
|
current = current.next |
|
} |
|
|
|
switch t { |
|
|
|
case diveTag: |
|
current.typeof = typeDive |
|
continue |
|
|
|
case keysTag: |
|
current.typeof = typeKeys |
|
|
|
if i == 0 || prevTag != typeDive { |
|
panic(fmt.Sprintf("'%s' tag must be immediately preceeded by the '%s' tag", keysTag, diveTag)) |
|
} |
|
|
|
current.typeof = typeKeys |
|
|
|
// need to pass along only keys tag |
|
// need to increment i to skip over the keys tags |
|
b := make([]byte, 0, 64) |
|
|
|
i++ |
|
|
|
for ; i < len(tags); i++ { |
|
|
|
b = append(b, tags[i]...) |
|
b = append(b, ',') |
|
|
|
if tags[i] == endKeysTag { |
|
break |
|
} |
|
} |
|
|
|
current.keys, _ = v.parseFieldTagsRecursive(string(b[:len(b)-1]), fieldName, "", false) |
|
continue |
|
|
|
case endKeysTag: |
|
current.typeof = typeEndKeys |
|
|
|
// if there are more in tags then there was no keysTag defined |
|
// and an error should be thrown |
|
if i != len(tags)-1 { |
|
panic(keysTagNotDefined) |
|
} |
|
return |
|
|
|
case omitempty: |
|
current.typeof = typeOmitEmpty |
|
continue |
|
|
|
case structOnlyTag: |
|
current.typeof = typeStructOnly |
|
continue |
|
|
|
case noStructLevelTag: |
|
current.typeof = typeNoStructLevel |
|
continue |
|
|
|
default: |
|
|
|
if t == isdefault { |
|
current.typeof = typeIsDefault |
|
} |
|
|
|
// if a pipe character is needed within the param you must use the utf8Pipe representation "0x7C" |
|
orVals := strings.Split(t, orSeparator) |
|
|
|
for j := 0; j < len(orVals); j++ { |
|
|
|
vals := strings.SplitN(orVals[j], tagKeySeparator, 2) |
|
|
|
if noAlias { |
|
alias = vals[0] |
|
current.aliasTag = alias |
|
} else { |
|
current.actualAliasTag = t |
|
} |
|
|
|
if j > 0 { |
|
current.next = &cTag{aliasTag: alias, actualAliasTag: current.actualAliasTag, hasAlias: hasAlias, hasTag: true} |
|
current = current.next |
|
} |
|
current.hasParam = len(vals) > 1 |
|
|
|
current.tag = vals[0] |
|
if len(current.tag) == 0 { |
|
panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, fieldName))) |
|
} |
|
|
|
if current.fn, ok = v.validations[current.tag]; !ok { |
|
panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, fieldName))) |
|
} |
|
|
|
if len(orVals) > 1 { |
|
current.typeof = typeOr |
|
} |
|
|
|
if len(vals) > 1 { |
|
current.param = strings.Replace(strings.Replace(vals[1], utf8HexComma, ",", -1), utf8Pipe, "|", -1) |
|
} |
|
} |
|
current.isBlockEnd = true |
|
} |
|
} |
|
|
|
return |
|
}
|
|
|