package validator import ( "context" "fmt" "reflect" "strconv" ) // per validate construct type validate struct { v *Validate top reflect.Value ns []byte actualNs []byte errs ValidationErrors includeExclude map[string]struct{} // reset only if StructPartial or StructExcept are called, no need otherwise ffn FilterFunc slflParent reflect.Value // StructLevel & FieldLevel slCurrent reflect.Value // StructLevel & FieldLevel flField reflect.Value // StructLevel & FieldLevel cf *cField // StructLevel & FieldLevel ct *cTag // StructLevel & FieldLevel misc []byte // misc reusable str1 string // misc reusable str2 string // misc reusable fldIsPointer bool // StructLevel & FieldLevel isPartial bool hasExcludes bool } // parent and current will be the same the first run of validateStruct func (v *validate) validateStruct(ctx context.Context, parent reflect.Value, current reflect.Value, typ reflect.Type, ns []byte, structNs []byte, ct *cTag) { cs, ok := v.v.structCache.Get(typ) if !ok { cs = v.v.extractStructCache(current, typ.Name()) } if len(ns) == 0 && len(cs.name) != 0 { ns = append(ns, cs.name...) ns = append(ns, '.') structNs = append(structNs, cs.name...) structNs = append(structNs, '.') } // ct is nil on top level struct, and structs as fields that have no tag info // so if nil or if not nil and the structonly tag isn't present if ct == nil || ct.typeof != typeStructOnly { var f *cField for i := 0; i < len(cs.fields); i++ { f = cs.fields[i] if v.isPartial { if v.ffn != nil { // used with StructFiltered if v.ffn(append(structNs, f.name...)) { continue } } else { // used with StructPartial & StructExcept _, ok = v.includeExclude[string(append(structNs, f.name...))] if (ok && v.hasExcludes) || (!ok && !v.hasExcludes) { continue } } } v.traverseField(ctx, parent, current.Field(f.idx), ns, structNs, f, f.cTags) } } // check if any struct level validations, after all field validations already checked. // first iteration will have no info about nostructlevel tag, and is checked prior to // calling the next iteration of validateStruct called from traverseField. if cs.fn != nil { v.slflParent = parent v.slCurrent = current v.ns = ns v.actualNs = structNs cs.fn(ctx, v) } } // traverseField validates any field, be it a struct or single field, ensures it's validity and passes it along to be validated via it's tag options func (v *validate) traverseField(ctx context.Context, parent reflect.Value, current reflect.Value, ns []byte, structNs []byte, cf *cField, ct *cTag) { var typ reflect.Type var kind reflect.Kind current, kind, v.fldIsPointer = v.extractTypeInternal(current, false) var skipValidations, structOnly bool switch kind { case reflect.Ptr, reflect.Interface, reflect.Invalid: if ct == nil { return } if ct.typeof == typeOmitEmpty || ct.typeof == typeIsDefault { return } if ct.hasTag { if kind == reflect.Invalid { v.str1 = string(append(ns, cf.altName...)) if v.v.hasTagNameFunc { v.str2 = string(append(structNs, cf.name...)) } else { v.str2 = v.str1 } v.errs = append(v.errs, &fieldError{ v: v.v, tag: ct.aliasTag, actualTag: ct.tag, ns: v.str1, structNs: v.str2, fieldLen: uint8(len(cf.altName)), structfieldLen: uint8(len(cf.name)), param: ct.param, kind: kind, }, ) return } v.str1 = string(append(ns, cf.altName...)) if v.v.hasTagNameFunc { v.str2 = string(append(structNs, cf.name...)) } else { v.str2 = v.str1 } if !ct.runValidationWhenNil { v.errs = append(v.errs, &fieldError{ v: v.v, tag: ct.aliasTag, actualTag: ct.tag, ns: v.str1, structNs: v.str2, fieldLen: uint8(len(cf.altName)), structfieldLen: uint8(len(cf.name)), value: current.Interface(), param: ct.param, kind: kind, typ: current.Type(), }, ) return } } case reflect.Struct: typ = current.Type() if typ != timeType { for tag := ct; tag != nil; tag = tag.next { switch tag.typeof { case typeNoStructLevel: skipValidations = true case typeStructOnly: structOnly = true } } if !structOnly && !skipValidations { // if len == 0 then validating using 'Var' or 'VarWithValue' // Var - doesn't make much sense to do it that way, should call 'Struct', but no harm... // VarWithField - this allows for validating against each field within the struct against a specific value // pretty handy in certain situations oldNs := ns oldStructNs := structNs if len(cf.name) > 0 { ns = append(append(ns, cf.altName...), '.') structNs = append(append(structNs, cf.name...), '.') } vFldIsPointerOld := v.fldIsPointer previousErrorsCount := len(v.errs) v.validateStruct(ctx, current, current, typ, ns, structNs, ct) v.fldIsPointer = vFldIsPointerOld ns = oldNs structNs = oldStructNs if len(v.errs) > previousErrorsCount { skipValidations = true } } if skipValidations { return } } } if ct != nil && !ct.hasTag { return } typ = current.Type() OUTER: for { if ct == nil { return } switch ct.typeof { case typeOmitEmpty: // set Field Level fields v.slflParent = parent v.flField = current v.cf = cf v.ct = ct if !v.fldIsPointer && !hasValue(v) { return } ct = ct.next continue case typeEndKeys: return case typeDive: ct = ct.next // traverse slice or map here // or panic ;) switch kind { case reflect.Slice, reflect.Array: var i64 int64 reusableCF := &cField{} for i := 0; i < current.Len(); i++ { i64 = int64(i) v.misc = append(v.misc[0:0], cf.name...) v.misc = append(v.misc, '[') v.misc = strconv.AppendInt(v.misc, i64, 10) v.misc = append(v.misc, ']') reusableCF.name = string(v.misc) if cf.namesEqual { reusableCF.altName = reusableCF.name } else { v.misc = append(v.misc[0:0], cf.altName...) v.misc = append(v.misc, '[') v.misc = strconv.AppendInt(v.misc, i64, 10) v.misc = append(v.misc, ']') reusableCF.altName = string(v.misc) } v.traverseField(ctx, parent, current.Index(i), ns, structNs, reusableCF, ct) } case reflect.Map: var pv string reusableCF := &cField{} for _, key := range current.MapKeys() { pv = fmt.Sprintf("%v", key.Interface()) v.misc = append(v.misc[0:0], cf.name...) v.misc = append(v.misc, '[') v.misc = append(v.misc, pv...) v.misc = append(v.misc, ']') reusableCF.name = string(v.misc) if cf.namesEqual { reusableCF.altName = reusableCF.name } else { v.misc = append(v.misc[0:0], cf.altName...) v.misc = append(v.misc, '[') v.misc = append(v.misc, pv...) v.misc = append(v.misc, ']') reusableCF.altName = string(v.misc) } if ct != nil && ct.typeof == typeKeys && ct.keys != nil { v.traverseField(ctx, parent, key, ns, structNs, reusableCF, ct.keys) // can be nil when just keys being validated if ct.next != nil { v.traverseField(ctx, parent, current.MapIndex(key), ns, structNs, reusableCF, ct.next) } } else { v.traverseField(ctx, parent, current.MapIndex(key), ns, structNs, reusableCF, ct) } } default: // throw error, if not a slice or map then should not have gotten here // bad dive tag panic("dive error! can't dive on a non slice or map") } return case typeOr: v.misc = v.misc[0:0] for { // set Field Level fields v.slflParent = parent v.flField = current v.cf = cf v.ct = ct if ct.fn(ctx, v) { // drain rest of the 'or' values, then continue or leave for { ct = ct.next if ct == nil { return } if ct.typeof != typeOr { continue OUTER } } } v.misc = append(v.misc, '|') v.misc = append(v.misc, ct.tag...) if ct.hasParam { v.misc = append(v.misc, '=') v.misc = append(v.misc, ct.param...) } if ct.isBlockEnd || ct.next == nil { // if we get here, no valid 'or' value and no more tags v.str1 = string(append(ns, cf.altName...)) if v.v.hasTagNameFunc { v.str2 = string(append(structNs, cf.name...)) } else { v.str2 = v.str1 } if ct.hasAlias { v.errs = append(v.errs, &fieldError{ v: v.v, tag: ct.aliasTag, actualTag: ct.actualAliasTag, ns: v.str1, structNs: v.str2, fieldLen: uint8(len(cf.altName)), structfieldLen: uint8(len(cf.name)), value: current.Interface(), param: ct.param, kind: kind, typ: typ, }, ) } else { tVal := string(v.misc)[1:] v.errs = append(v.errs, &fieldError{ v: v.v, tag: tVal, actualTag: tVal, ns: v.str1, structNs: v.str2, fieldLen: uint8(len(cf.altName)), structfieldLen: uint8(len(cf.name)), value: current.Interface(), param: ct.param, kind: kind, typ: typ, }, ) } return } ct = ct.next } default: if ct.fn != nil { // set Field Level fields v.slflParent = parent v.flField = current v.cf = cf v.ct = ct v.str1 = string(append(ns, cf.altName...)) if v.v.hasTagNameFunc { v.str2 = string(append(structNs, cf.name...)) } else { v.str2 = v.str1 } if !ct.fn(ctx, v) { v.errs = append(v.errs, &fieldError{ v: v.v, tag: ct.aliasTag, actualTag: ct.tag, ns: v.str1, structNs: v.str2, fieldLen: uint8(len(cf.altName)), structfieldLen: uint8(len(cf.name)), value: current.Interface(), param: ct.param, kind: kind, typ: typ, }, ) return } } ct = ct.next } } }