5849898283
Unit tests functional and coverage back to 100% Add more routes to dictionary, add more credentials, add default port 5554, rename cameradar logs ENV variable, improve unit test readability, remove tmp file
338 lines
7.2 KiB
Go
338 lines
7.2 KiB
Go
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
|
|
keys *cTag // only populated when using tag's 'keys' and 'endkeys' for map key validation
|
|
next *cTag
|
|
fn FuncCtx
|
|
typeof tagType
|
|
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
|
|
}
|
|
|
|
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 preceded 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
|
|
}
|
|
|
|
func (v *Validate) fetchCacheTag(tag string) *cTag {
|
|
// find cached tag
|
|
ctag, found := v.tagCache.Get(tag)
|
|
if !found {
|
|
v.tagCache.lock.Lock()
|
|
defer v.tagCache.lock.Unlock()
|
|
|
|
// could have been multiple trying to access, but once first is done this ensures tag
|
|
// isn't parsed again.
|
|
ctag, found = v.tagCache.Get(tag)
|
|
if !found {
|
|
ctag, _ = v.parseFieldTagsRecursive(tag, "", "", false)
|
|
v.tagCache.Set(tag, ctag)
|
|
}
|
|
}
|
|
return ctag
|
|
}
|