mirror of
https://gitee.com/johng/gf
synced 2026-06-06 02:25:47 +08:00
merge feature/improve_struct_validation
This commit is contained in:
@ -85,7 +85,9 @@ func IsEmpty(value interface{}) bool {
|
||||
case map[string]interface{}:
|
||||
return len(value) == 0
|
||||
default:
|
||||
// =========================
|
||||
// Common interfaces checks.
|
||||
// =========================
|
||||
if f, ok := value.(apiTime); ok {
|
||||
if f == nil {
|
||||
return true
|
||||
@ -163,6 +165,126 @@ func IsEmpty(value interface{}) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsEmptyLength checks whether given `value` is empty length.
|
||||
// It returns true if `value` is in: nil, "", len(slice/map/chan) == 0,
|
||||
// or else it returns false.
|
||||
//func IsEmptyLength(value interface{}) bool {
|
||||
// if value == nil {
|
||||
// return true
|
||||
// }
|
||||
// // It firstly checks the variable as common types using assertion to enhance the performance,
|
||||
// // and then using reflection.
|
||||
// switch value := value.(type) {
|
||||
// case
|
||||
// int,
|
||||
// int8,
|
||||
// int16,
|
||||
// int32,
|
||||
// int64,
|
||||
// uint,
|
||||
// uint8,
|
||||
// uint16,
|
||||
// uint32,
|
||||
// uint64,
|
||||
// float32,
|
||||
// float64,
|
||||
// bool:
|
||||
// return false
|
||||
// case string:
|
||||
// return value == ""
|
||||
// case []byte:
|
||||
// return len(value) == 0
|
||||
// case []rune:
|
||||
// return len(value) == 0
|
||||
// case []int:
|
||||
// return len(value) == 0
|
||||
// case []string:
|
||||
// return len(value) == 0
|
||||
// case []float32:
|
||||
// return len(value) == 0
|
||||
// case []float64:
|
||||
// return len(value) == 0
|
||||
// case map[string]interface{}:
|
||||
// return len(value) == 0
|
||||
// default:
|
||||
// // =========================
|
||||
// // Common interfaces checks.
|
||||
// // =========================
|
||||
// if f, ok := value.(apiTime); ok {
|
||||
// if f == nil {
|
||||
// return true
|
||||
// }
|
||||
// return f.IsZero()
|
||||
// }
|
||||
// if f, ok := value.(apiString); ok {
|
||||
// if f == nil {
|
||||
// return true
|
||||
// }
|
||||
// return f.String() == ""
|
||||
// }
|
||||
// if f, ok := value.(apiInterfaces); ok {
|
||||
// if f == nil {
|
||||
// return true
|
||||
// }
|
||||
// return len(f.Interfaces()) == 0
|
||||
// }
|
||||
// if f, ok := value.(apiMapStrAny); ok {
|
||||
// if f == nil {
|
||||
// return true
|
||||
// }
|
||||
// return len(f.MapStrAny()) == 0
|
||||
// }
|
||||
// // Finally using reflect.
|
||||
// var rv reflect.Value
|
||||
// if v, ok := value.(reflect.Value); ok {
|
||||
// rv = v
|
||||
// } else {
|
||||
// rv = reflect.ValueOf(value)
|
||||
// }
|
||||
//
|
||||
// switch rv.Kind() {
|
||||
// case
|
||||
// reflect.Int,
|
||||
// reflect.Int8,
|
||||
// reflect.Int16,
|
||||
// reflect.Int32,
|
||||
// reflect.Int64,
|
||||
// reflect.Uint,
|
||||
// reflect.Uint8,
|
||||
// reflect.Uint16,
|
||||
// reflect.Uint32,
|
||||
// reflect.Uint64,
|
||||
// reflect.Uintptr,
|
||||
// reflect.Float32,
|
||||
// reflect.Float64,
|
||||
// reflect.Bool:
|
||||
// return false
|
||||
// case reflect.String:
|
||||
// return rv.Len() == 0
|
||||
// case reflect.Struct:
|
||||
// for i := 0; i < rv.NumField(); i++ {
|
||||
// if !IsEmpty(rv) {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// return true
|
||||
// case reflect.Chan,
|
||||
// reflect.Map,
|
||||
// reflect.Slice,
|
||||
// reflect.Array:
|
||||
// return rv.Len() == 0
|
||||
// case reflect.Func,
|
||||
// reflect.Ptr,
|
||||
// reflect.Interface,
|
||||
// reflect.UnsafePointer:
|
||||
// if rv.IsNil() {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return false
|
||||
//}
|
||||
|
||||
// IsNil checks whether given `value` is nil.
|
||||
// Parameter `traceSource` is used for tracing to the source variable if given `value` is type
|
||||
// of a pinter that also points to a pointer. It returns nil if the source is nil when `traceSource`
|
||||
|
||||
@ -82,24 +82,27 @@ func (r *Request) doParse(pointer interface{}, requestType int) error {
|
||||
// 1. {"id":1, "name":"john"}
|
||||
// 2. ?id=1&name=john
|
||||
case reflect.Ptr, reflect.Struct:
|
||||
var (
|
||||
err error
|
||||
data map[string]interface{}
|
||||
)
|
||||
// Converting.
|
||||
switch requestType {
|
||||
case parseTypeQuery:
|
||||
if err := r.GetQueryStruct(pointer); err != nil {
|
||||
if data, err = r.doGetQueryStruct(pointer); err != nil {
|
||||
return err
|
||||
}
|
||||
case parseTypeForm:
|
||||
if err := r.GetFormStruct(pointer); err != nil {
|
||||
if data, err = r.doGetFormStruct(pointer); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if err := r.GetStruct(pointer); err != nil {
|
||||
if data, err = r.doGetRequestStruct(pointer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Validation.
|
||||
if err := gvalid.CheckStruct(pointer, nil); err != nil {
|
||||
if err := gvalid.CheckStructWithParamMap(pointer, data, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -107,7 +110,7 @@ func (r *Request) doParse(pointer interface{}, requestType int) error {
|
||||
// [{"id":1, "name":"john"}, {"id":, "name":"smith"}]
|
||||
case reflect.Array, reflect.Slice:
|
||||
// If struct slice conversion, it might post JSON/XML content,
|
||||
// so it uses gjson for the conversion.
|
||||
// so it uses `gjson` for the conversion.
|
||||
j, err := gjson.LoadContent(r.GetBody())
|
||||
if err != nil {
|
||||
return err
|
||||
@ -116,7 +119,11 @@ func (r *Request) doParse(pointer interface{}, requestType int) error {
|
||||
return err
|
||||
}
|
||||
for i := 0; i < reflectVal2.Len(); i++ {
|
||||
if err := gvalid.CheckStruct(reflectVal2.Index(i), nil); err != nil {
|
||||
if err := gvalid.CheckStructWithParamMap(
|
||||
reflectVal2.Index(i),
|
||||
j.GetMap(gconv.String(i)),
|
||||
nil,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,13 +188,18 @@ func (r *Request) GetFormMapStrVar(kvMap ...map[string]interface{}) map[string]*
|
||||
// given struct object. Note that the parameter <pointer> is a pointer to the struct object.
|
||||
// The optional parameter <mapping> is used to specify the key to attribute mapping.
|
||||
func (r *Request) GetFormStruct(pointer interface{}, mapping ...map[string]string) error {
|
||||
_, err := r.doGetFormStruct(pointer, mapping...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Request) doGetFormStruct(pointer interface{}, mapping ...map[string]string) (data map[string]interface{}, err error) {
|
||||
r.parseForm()
|
||||
data := r.formMap
|
||||
data = r.formMap
|
||||
if data == nil {
|
||||
data = map[string]interface{}{}
|
||||
}
|
||||
if err := r.mergeDefaultStructValue(data, pointer); err != nil {
|
||||
return nil
|
||||
return data, nil
|
||||
}
|
||||
return gconv.Struct(data, pointer, mapping...)
|
||||
return data, gconv.Struct(data, pointer, mapping...)
|
||||
}
|
||||
|
||||
@ -196,13 +196,18 @@ func (r *Request) GetQueryMapStrVar(kvMap ...map[string]interface{}) map[string]
|
||||
// to the struct object. The optional parameter <mapping> is used to specify the key to
|
||||
// attribute mapping.
|
||||
func (r *Request) GetQueryStruct(pointer interface{}, mapping ...map[string]string) error {
|
||||
_, err := r.doGetQueryStruct(pointer, mapping...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Request) doGetQueryStruct(pointer interface{}, mapping ...map[string]string) (data map[string]interface{}, err error) {
|
||||
r.parseQuery()
|
||||
data := r.GetQueryMap()
|
||||
data = r.GetQueryMap()
|
||||
if data == nil {
|
||||
data = map[string]interface{}{}
|
||||
}
|
||||
if err := r.mergeDefaultStructValue(data, pointer); err != nil {
|
||||
return nil
|
||||
return data, nil
|
||||
}
|
||||
return gconv.Struct(data, pointer, mapping...)
|
||||
return data, gconv.Struct(data, pointer, mapping...)
|
||||
}
|
||||
|
||||
@ -270,14 +270,19 @@ func (r *Request) GetRequestMapStrVar(kvMap ...map[string]interface{}) map[strin
|
||||
// the parameter <pointer> is a pointer to the struct object.
|
||||
// The optional parameter <mapping> is used to specify the key to attribute mapping.
|
||||
func (r *Request) GetRequestStruct(pointer interface{}, mapping ...map[string]string) error {
|
||||
data := r.GetRequestMap()
|
||||
_, err := r.doGetRequestStruct(pointer, mapping...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Request) doGetRequestStruct(pointer interface{}, mapping ...map[string]string) (data map[string]interface{}, err error) {
|
||||
data = r.GetRequestMap()
|
||||
if data == nil {
|
||||
data = map[string]interface{}{}
|
||||
}
|
||||
if err := r.mergeDefaultStructValue(data, pointer); err != nil {
|
||||
return nil
|
||||
return data, nil
|
||||
}
|
||||
return gconv.Struct(data, pointer, mapping...)
|
||||
return data, gconv.Struct(data, pointer, mapping...)
|
||||
}
|
||||
|
||||
// mergeDefaultStructValue merges the request parameters with default values from struct tag definition.
|
||||
|
||||
@ -23,7 +23,7 @@ func Test_Params_Json_Request(t *testing.T) {
|
||||
Name string
|
||||
Time *time.Time
|
||||
Pass1 string `p:"password1"`
|
||||
Pass2 string `p:"password2" v:"required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致"`
|
||||
Pass2 string `p:"password2" v:"password2@required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致"`
|
||||
}
|
||||
p, _ := ports.PopRand()
|
||||
s := g.Server(p)
|
||||
|
||||
@ -395,7 +395,7 @@ func Test_Params_Struct(t *testing.T) {
|
||||
Name string
|
||||
Time *time.Time
|
||||
Pass1 string `p:"password1"`
|
||||
Pass2 string `p:"password2" v:"passwd1 @required|length:2,20|password3#||密码强度不足"`
|
||||
Pass2 string `p:"password2" v:"password2 @required|length:2,20|password3#||密码强度不足"`
|
||||
}
|
||||
p, _ := ports.PopRand()
|
||||
s := g.Server(p)
|
||||
@ -452,8 +452,8 @@ func Test_Params_Struct(t *testing.T) {
|
||||
t.Assert(client.PostContent("/struct1", `id=1&name=john&password1=123&password2=456`), `1john123456`)
|
||||
t.Assert(client.PostContent("/struct2", `id=1&name=john&password1=123&password2=456`), `1john123456`)
|
||||
t.Assert(client.PostContent("/struct2", ``), ``)
|
||||
t.Assert(client.PostContent("/struct-valid", `id=1&name=john&password1=123&password2=0`), `The passwd1 value length must be between 2 and 20; 密码强度不足`)
|
||||
t.Assert(client.PostContent("/parse", `id=1&name=john&password1=123&password2=0`), `The passwd1 value length must be between 2 and 20; 密码强度不足`)
|
||||
t.Assert(client.PostContent("/struct-valid", `id=1&name=john&password1=123&password2=0`), `The password2 value length must be between 2 and 20; 密码强度不足`)
|
||||
t.Assert(client.PostContent("/parse", `id=1&name=john&password1=123&password2=0`), `The password2 value length must be between 2 and 20; 密码强度不足`)
|
||||
t.Assert(client.PostContent("/parse", `{"id":1,"name":"john","password1":"123Abc!@#","password2":"123Abc!@#"}`), `1john123Abc!@#123Abc!@#`)
|
||||
})
|
||||
}
|
||||
@ -464,7 +464,7 @@ func Test_Params_Structs(t *testing.T) {
|
||||
Name string
|
||||
Time *time.Time
|
||||
Pass1 string `p:"password1"`
|
||||
Pass2 string `p:"password2" v:"passwd1 @required|length:2,20|password3#||密码强度不足"`
|
||||
Pass2 string `p:"password2" v:"password2 @required|length:2,20|password3#||密码强度不足"`
|
||||
}
|
||||
p, _ := ports.PopRand()
|
||||
s := g.Server(p)
|
||||
@ -491,3 +491,39 @@ func Test_Params_Structs(t *testing.T) {
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Params_Struct_Validation(t *testing.T) {
|
||||
type User struct {
|
||||
Id int `v:"required"`
|
||||
Name string `v:"name@required-with:id"`
|
||||
}
|
||||
p, _ := ports.PopRand()
|
||||
s := g.Server(p)
|
||||
s.Group("/", func(group *ghttp.RouterGroup) {
|
||||
group.ALL("/", func(r *ghttp.Request) {
|
||||
var (
|
||||
err error
|
||||
user *User
|
||||
)
|
||||
err = r.Parse(&user)
|
||||
if err != nil {
|
||||
r.Response.WriteExit(err)
|
||||
}
|
||||
r.Response.WriteExit(user.Id, user.Name)
|
||||
})
|
||||
})
|
||||
s.SetPort(p)
|
||||
s.SetDumpRouterMap(false)
|
||||
s.Start()
|
||||
defer s.Shutdown()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
c := g.Client()
|
||||
c.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p))
|
||||
t.Assert(c.GetContent("/", ``), `The Id field is required`)
|
||||
t.Assert(c.GetContent("/", `id=1&name=john`), `1john`)
|
||||
t.Assert(c.PostContent("/", `id=1&name=john&password1=123&password2=456`), `1john`)
|
||||
t.Assert(c.PostContent("/", `id=1`), `The name field is required`)
|
||||
})
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ func Test_Params_Xml_Request(t *testing.T) {
|
||||
Name string
|
||||
Time *time.Time
|
||||
Pass1 string `p:"password1"`
|
||||
Pass2 string `p:"password2" v:"required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致"`
|
||||
Pass2 string `p:"password2" v:"password2@required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致"`
|
||||
}
|
||||
p, _ := ports.PopRand()
|
||||
s := g.Server(p)
|
||||
|
||||
@ -193,6 +193,16 @@ func CheckStruct(object interface{}, rules interface{}, messages ...CustomMsg) *
|
||||
return defaultValidator.CheckStruct(object, rules, messages...)
|
||||
}
|
||||
|
||||
// CheckStructWithParamMap validates struct with given parameter map and returns the error result.
|
||||
//
|
||||
// The parameter `object` should be type of struct/*struct.
|
||||
// The parameter `rules` can be type of []string/map[string]string. It supports sequence in error result
|
||||
// if `rules` is type of []string.
|
||||
// The optional parameter `messages` specifies the custom error messages for specified keys and rules.
|
||||
func CheckStructWithParamMap(object interface{}, paramMap interface{}, rules interface{}, messages ...CustomMsg) *Error {
|
||||
return defaultValidator.CheckStructWithParamMap(object, paramMap, rules, messages...)
|
||||
}
|
||||
|
||||
// parseSequenceTag parses one sequence tag to field, rule and error message.
|
||||
// The sequence tag is like: [alias@]rule[...#msg...]
|
||||
func parseSequenceTag(tag string) (field, rule, msg string) {
|
||||
|
||||
@ -6,9 +6,12 @@
|
||||
|
||||
package gvalid
|
||||
|
||||
import "context"
|
||||
|
||||
// Validator is the validation manager.
|
||||
type Validator struct {
|
||||
i18nLang string // I18n language.
|
||||
i18nLang string // I18n language.
|
||||
ctx context.Context // Context containing custom context variables.
|
||||
}
|
||||
|
||||
// New creates and returns a new Validator.
|
||||
@ -29,3 +32,10 @@ func (v *Validator) I18n(language string) *Validator {
|
||||
newValidator.i18nLang = language
|
||||
return newValidator
|
||||
}
|
||||
|
||||
// Ctx is a chaining operation function which sets the context for next validation.
|
||||
func (v *Validator) Ctx(ctx context.Context) *Validator {
|
||||
newValidator := v.Clone()
|
||||
newValidator.ctx = ctx
|
||||
return newValidator
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
"github.com/gogf/gf/text/gregex"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
"github.com/gogf/gf/util/gutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -34,12 +35,12 @@ type apiTime interface {
|
||||
// string/map/struct/*struct.
|
||||
// The optional parameter `params` specifies the extra validation parameters for some rules
|
||||
// like: required-*、same、different, etc.
|
||||
func (v *Validator) Check(value interface{}, rules string, messages interface{}, params ...interface{}) *Error {
|
||||
return v.doCheck("", value, rules, messages, params...)
|
||||
func (v *Validator) Check(value interface{}, rules string, messages interface{}, paramMap ...interface{}) *Error {
|
||||
return v.doCheck("", value, rules, messages, paramMap...)
|
||||
}
|
||||
|
||||
// doCheck does the really rules validation for single key-value.
|
||||
func (v *Validator) doCheck(key string, value interface{}, rules string, messages interface{}, params ...interface{}) *Error {
|
||||
func (v *Validator) doCheck(key string, value interface{}, rules string, messages interface{}, paramMap ...interface{}) *Error {
|
||||
// If there's no validation rules, it does nothing and returns quickly.
|
||||
if rules == "" {
|
||||
return nil
|
||||
@ -50,8 +51,8 @@ func (v *Validator) doCheck(key string, value interface{}, rules string, message
|
||||
data = make(map[string]interface{})
|
||||
errorMsgArray = make(map[string]string)
|
||||
)
|
||||
if len(params) > 0 {
|
||||
data = gconv.Map(params[0])
|
||||
if len(paramMap) > 0 {
|
||||
data = gconv.Map(paramMap[0])
|
||||
}
|
||||
// Custom error messages handling.
|
||||
var (
|
||||
@ -107,8 +108,8 @@ func (v *Validator) doCheck(key string, value interface{}, rules string, message
|
||||
dataMap map[string]interface{}
|
||||
message = v.getErrorMessageByRule(ruleKey, customMsgMap)
|
||||
)
|
||||
if len(params) > 0 {
|
||||
dataMap = gconv.Map(params[0])
|
||||
if len(paramMap) > 0 {
|
||||
dataMap = gconv.Map(paramMap[0])
|
||||
}
|
||||
if err := f(ruleItems[index], value, message, dataMap); err != nil {
|
||||
match = false
|
||||
@ -232,8 +233,9 @@ func (v *Validator) doCheckBuildInRules(
|
||||
|
||||
// Values of two fields should be equal as string.
|
||||
case "same":
|
||||
if v, ok := dataMap[rulePattern]; ok {
|
||||
if strings.Compare(valueStr, gconv.String(v)) == 0 {
|
||||
_, foundValue := gutil.MapPossibleItemByKey(dataMap, rulePattern)
|
||||
if foundValue != nil {
|
||||
if strings.Compare(valueStr, gconv.String(foundValue)) == 0 {
|
||||
match = true
|
||||
}
|
||||
}
|
||||
@ -247,8 +249,9 @@ func (v *Validator) doCheckBuildInRules(
|
||||
// Values of two fields should not be equal as string.
|
||||
case "different":
|
||||
match = true
|
||||
if v, ok := dataMap[rulePattern]; ok {
|
||||
if strings.Compare(valueStr, gconv.String(v)) == 0 {
|
||||
_, foundValue := gutil.MapPossibleItemByKey(dataMap, rulePattern)
|
||||
if foundValue != nil {
|
||||
if strings.Compare(valueStr, gconv.String(foundValue)) == 0 {
|
||||
match = false
|
||||
}
|
||||
}
|
||||
@ -302,6 +305,7 @@ func (v *Validator) doCheckBuildInRules(
|
||||
// 16x, 19x
|
||||
case "phone":
|
||||
match = gregex.IsMatchString(`^13[\d]{9}$|^14[5,7]{1}\d{8}$|^15[^4]{1}\d{8}$|^16[\d]{9}$|^17[0,2,3,5,6,7,8]{1}\d{8}$|^18[\d]{9}$|^19[\d]{9}$`, valueStr)
|
||||
|
||||
// Loose mobile phone number verification(宽松的手机号验证)
|
||||
// As long as the 11 digit numbers beginning with
|
||||
// 13, 14, 15, 16, 17, 18, 19 can pass the verification (只要满足 13、14、15、16、17、18、19开头的11位数字都可以通过验证)
|
||||
|
||||
@ -9,10 +9,20 @@ package gvalid
|
||||
import (
|
||||
"github.com/gogf/gf/internal/structs"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
"github.com/gogf/gf/util/gutil"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// doCheckStructWithParamMapInput is used for struct validation for internal function.
|
||||
type doCheckStructWithParamMapInput struct {
|
||||
Object interface{} // Can be type of struct/*struct.
|
||||
ParamMap interface{} // Validation parameter map. Note that it acts different according attribute `UseParamMapInsteadOfObjectValue`.
|
||||
UseParamMapInsteadOfObjectValue bool // Using `ParamMap` as its validation source instead of values from `Object`.
|
||||
CustomRules interface{} // Custom validation rules.
|
||||
CustomErrorMessageMap CustomMsg // Custom error message map for validation rules.
|
||||
}
|
||||
|
||||
var (
|
||||
structTagPriority = []string{"gvalid", "valid", "v"} // structTagPriority specifies the validation tag priority array.
|
||||
aliasNameTagPriority = []string{"param", "params", "p"} // aliasNameTagPriority specifies the alias tag priority array.
|
||||
@ -24,18 +34,52 @@ var (
|
||||
// The parameter `rules` can be type of []string/map[string]string. It supports sequence in error result
|
||||
// if `rules` is type of []string.
|
||||
// The optional parameter `messages` specifies the custom error messages for specified keys and rules.
|
||||
func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages ...CustomMsg) *Error {
|
||||
func (v *Validator) CheckStruct(object interface{}, customRules interface{}, customErrorMessageMap ...CustomMsg) *Error {
|
||||
var message CustomMsg
|
||||
if len(customErrorMessageMap) > 0 {
|
||||
message = customErrorMessageMap[0]
|
||||
}
|
||||
return v.doCheckStructWithParamMap(&doCheckStructWithParamMapInput{
|
||||
Object: object,
|
||||
ParamMap: nil,
|
||||
UseParamMapInsteadOfObjectValue: false,
|
||||
CustomRules: customRules,
|
||||
CustomErrorMessageMap: message,
|
||||
})
|
||||
}
|
||||
|
||||
// CheckStructWithParamMap validates struct with given parameter map and returns the error result.
|
||||
//
|
||||
// The parameter `object` should be type of struct/*struct.
|
||||
// The parameter `rules` can be type of []string/map[string]string. It supports sequence in error result
|
||||
// if `rules` is type of []string.
|
||||
// The optional parameter `messages` specifies the custom error messages for specified keys and rules.
|
||||
func (v *Validator) CheckStructWithParamMap(object interface{}, paramMap interface{}, customRules interface{}, customErrorMessageMap ...CustomMsg) *Error {
|
||||
var message CustomMsg
|
||||
if len(customErrorMessageMap) > 0 {
|
||||
message = customErrorMessageMap[0]
|
||||
}
|
||||
return v.doCheckStructWithParamMap(&doCheckStructWithParamMapInput{
|
||||
Object: object,
|
||||
ParamMap: paramMap,
|
||||
UseParamMapInsteadOfObjectValue: true,
|
||||
CustomRules: customRules,
|
||||
CustomErrorMessageMap: message,
|
||||
})
|
||||
}
|
||||
|
||||
func (v *Validator) doCheckStructWithParamMap(input *doCheckStructWithParamMapInput) *Error {
|
||||
var (
|
||||
errorMaps = make(ErrorMap) // Returned error.
|
||||
errorMaps = make(ErrorMap) // Returning error.
|
||||
)
|
||||
mapField, err := structs.FieldMap(object, aliasNameTagPriority, true)
|
||||
fieldMap, err := structs.FieldMap(input.Object, aliasNameTagPriority, true)
|
||||
if err != nil {
|
||||
return newErrorStr("invalid_object", err.Error())
|
||||
}
|
||||
// It checks the struct recursively the its attribute is also a struct.
|
||||
for _, field := range mapField {
|
||||
for _, field := range fieldMap {
|
||||
if field.OriginalKind() == reflect.Struct {
|
||||
if err := v.CheckStruct(field.Value, rules, messages...); err != nil {
|
||||
if err := v.CheckStruct(field.Value, input.CustomRules, input.CustomErrorMessageMap); err != nil {
|
||||
// It merges the errors into single error map.
|
||||
for k, m := range err.errors {
|
||||
errorMaps[k] = m
|
||||
@ -44,22 +88,23 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages
|
||||
}
|
||||
}
|
||||
// It here must use structs.TagFields not structs.FieldMap to ensure error sequence.
|
||||
tagField, err := structs.TagFields(object, structTagPriority)
|
||||
tagField, err := structs.TagFields(input.Object, structTagPriority)
|
||||
if err != nil {
|
||||
return newErrorStr("invalid_object", err.Error())
|
||||
}
|
||||
// If there's no struct tag and validation rules, it does nothing and returns quickly.
|
||||
if len(tagField) == 0 && rules == nil {
|
||||
if len(tagField) == 0 && input.CustomRules == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
params = make(map[string]interface{})
|
||||
inputParamMap map[string]interface{}
|
||||
checkRules = make(map[string]string)
|
||||
customMessage = make(CustomMsg)
|
||||
fieldAliases = make(map[string]string) // Alias names for `messages` overwriting struct tag names.
|
||||
errorRules = make([]string, 0) // Sequence rules.
|
||||
)
|
||||
switch v := rules.(type) {
|
||||
switch v := input.CustomRules.(type) {
|
||||
// Sequence tag: []sequence tag
|
||||
// Sequence has order for error results.
|
||||
case []string:
|
||||
@ -102,11 +147,22 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages
|
||||
if len(tagField) == 0 && len(checkRules) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Checks and extends the parameters map with struct alias tag.
|
||||
for nameOrTag, field := range mapField {
|
||||
params[nameOrTag] = field.Value.Interface()
|
||||
params[field.Name()] = field.Value.Interface()
|
||||
// Input parameter map handling.
|
||||
if input.ParamMap == nil || !input.UseParamMapInsteadOfObjectValue {
|
||||
inputParamMap = make(map[string]interface{})
|
||||
} else {
|
||||
inputParamMap = gconv.Map(input.ParamMap)
|
||||
}
|
||||
// Checks and extends the parameters map with struct alias tag.
|
||||
if !input.UseParamMapInsteadOfObjectValue {
|
||||
for nameOrTag, field := range fieldMap {
|
||||
inputParamMap[nameOrTag] = field.Value.Interface()
|
||||
if nameOrTag != field.Name() {
|
||||
inputParamMap[field.Name()] = field.Value.Interface()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, field := range tagField {
|
||||
fieldName := field.Name()
|
||||
// sequence tag == struct tag
|
||||
@ -118,8 +174,10 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages
|
||||
fieldAliases[fieldName] = name
|
||||
}
|
||||
// It here extends the params map using alias names.
|
||||
if _, ok := params[name]; !ok {
|
||||
params[name] = field.Value.Interface()
|
||||
if _, ok := inputParamMap[name]; !ok {
|
||||
if !input.UseParamMapInsteadOfObjectValue {
|
||||
inputParamMap[name] = field.Value.Interface()
|
||||
}
|
||||
}
|
||||
if _, ok := checkRules[name]; !ok {
|
||||
if _, ok := checkRules[fieldName]; ok {
|
||||
@ -132,7 +190,7 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages
|
||||
}
|
||||
errorRules = append(errorRules, name+"@"+rule)
|
||||
} else {
|
||||
// The passed rules can overwrite the rules in struct tag.
|
||||
// The input rules can overwrite the rules in struct tag.
|
||||
continue
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
@ -160,8 +218,8 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages
|
||||
|
||||
// Custom error messages,
|
||||
// which have the most priority than `rules` and struct tag.
|
||||
if len(messages) > 0 && len(messages[0]) > 0 {
|
||||
for k, v := range messages[0] {
|
||||
if len(input.CustomErrorMessageMap) > 0 {
|
||||
for k, v := range input.CustomErrorMessageMap {
|
||||
if a, ok := fieldAliases[k]; ok {
|
||||
// Overwrite the key of field name.
|
||||
customMessage[a] = v
|
||||
@ -174,12 +232,9 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages
|
||||
// The following logic is the same as some of CheckMap.
|
||||
var value interface{}
|
||||
for key, rule := range checkRules {
|
||||
value = nil
|
||||
if v, ok := params[key]; ok {
|
||||
value = v
|
||||
}
|
||||
_, value = gutil.MapPossibleItemByKey(inputParamMap, key)
|
||||
// It checks each rule and its value in loop.
|
||||
if e := v.doCheck(key, value, rule, customMessage[key], params); e != nil {
|
||||
if e := v.doCheck(key, value, rule, customMessage[key], inputParamMap); e != nil {
|
||||
_, item := e.FirstItem()
|
||||
// ===================================================================
|
||||
// Only in map and struct validations, if value is nil or empty string
|
||||
|
||||
@ -9,6 +9,7 @@ package gvalid
|
||||
import (
|
||||
"github.com/gogf/gf/internal/empty"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
"github.com/gogf/gf/util/gutil"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
@ -26,17 +27,19 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string
|
||||
// Example: required-if: id,1,age,18
|
||||
case "required-if":
|
||||
required = false
|
||||
array := strings.Split(rulePattern, ",")
|
||||
var (
|
||||
array = strings.Split(rulePattern, ",")
|
||||
foundValue interface{}
|
||||
)
|
||||
// It supports multiple field and value pairs.
|
||||
if len(array)%2 == 0 {
|
||||
for i := 0; i < len(array); {
|
||||
tk := array[i]
|
||||
tv := array[i+1]
|
||||
if v, ok := dataMap[tk]; ok {
|
||||
if strings.Compare(tv, gconv.String(v)) == 0 {
|
||||
required = true
|
||||
break
|
||||
}
|
||||
_, foundValue = gutil.MapPossibleItemByKey(dataMap, tk)
|
||||
if strings.Compare(tv, gconv.String(foundValue)) == 0 {
|
||||
required = true
|
||||
break
|
||||
}
|
||||
i += 2
|
||||
}
|
||||
@ -46,18 +49,21 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string
|
||||
// Example: required-unless: id,1,age,18
|
||||
case "required-unless":
|
||||
required = true
|
||||
array := strings.Split(rulePattern, ",")
|
||||
var (
|
||||
array = strings.Split(rulePattern, ",")
|
||||
foundValue interface{}
|
||||
)
|
||||
// It supports multiple field and value pairs.
|
||||
if len(array)%2 == 0 {
|
||||
for i := 0; i < len(array); {
|
||||
tk := array[i]
|
||||
tv := array[i+1]
|
||||
if v, ok := dataMap[tk]; ok {
|
||||
if strings.Compare(tv, gconv.String(v)) == 0 {
|
||||
required = false
|
||||
break
|
||||
}
|
||||
_, foundValue = gutil.MapPossibleItemByKey(dataMap, tk)
|
||||
if strings.Compare(tv, gconv.String(foundValue)) == 0 {
|
||||
required = false
|
||||
break
|
||||
}
|
||||
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
@ -66,9 +72,13 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string
|
||||
// Example: required-with:id,name
|
||||
case "required-with":
|
||||
required = false
|
||||
array := strings.Split(rulePattern, ",")
|
||||
var (
|
||||
array = strings.Split(rulePattern, ",")
|
||||
foundValue interface{}
|
||||
)
|
||||
for i := 0; i < len(array); i++ {
|
||||
if !empty.IsEmpty(dataMap[array[i]]) {
|
||||
_, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i])
|
||||
if !empty.IsEmpty(foundValue) {
|
||||
required = true
|
||||
break
|
||||
}
|
||||
@ -78,9 +88,13 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string
|
||||
// Example: required-with:id,name
|
||||
case "required-with-all":
|
||||
required = true
|
||||
array := strings.Split(rulePattern, ",")
|
||||
var (
|
||||
array = strings.Split(rulePattern, ",")
|
||||
foundValue interface{}
|
||||
)
|
||||
for i := 0; i < len(array); i++ {
|
||||
if empty.IsEmpty(dataMap[array[i]]) {
|
||||
_, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i])
|
||||
if empty.IsEmpty(foundValue) {
|
||||
required = false
|
||||
break
|
||||
}
|
||||
@ -90,9 +104,13 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string
|
||||
// Example: required-with:id,name
|
||||
case "required-without":
|
||||
required = false
|
||||
array := strings.Split(rulePattern, ",")
|
||||
var (
|
||||
array = strings.Split(rulePattern, ",")
|
||||
foundValue interface{}
|
||||
)
|
||||
for i := 0; i < len(array); i++ {
|
||||
if empty.IsEmpty(dataMap[array[i]]) {
|
||||
_, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i])
|
||||
if empty.IsEmpty(foundValue) {
|
||||
required = true
|
||||
break
|
||||
}
|
||||
@ -102,9 +120,13 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string
|
||||
// Example: required-with:id,name
|
||||
case "required-without-all":
|
||||
required = true
|
||||
array := strings.Split(rulePattern, ",")
|
||||
var (
|
||||
array = strings.Split(rulePattern, ",")
|
||||
foundValue interface{}
|
||||
)
|
||||
for i := 0; i < len(array); i++ {
|
||||
if !empty.IsEmpty(dataMap[array[i]]) {
|
||||
_, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i])
|
||||
if !empty.IsEmpty(foundValue) {
|
||||
required = false
|
||||
break
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ package gvalid_test
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/container/gvar"
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/frame/g"
|
||||
@ -352,3 +353,62 @@ func Test_CheckStruct_InvalidRule(t *testing.T) {
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidator_CheckStructWithParamMap(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type UserApiSearch struct {
|
||||
Uid int64 `v:"required"`
|
||||
Nickname string `v:"required-with:uid"`
|
||||
}
|
||||
data := UserApiSearch{
|
||||
Uid: 1,
|
||||
Nickname: "john",
|
||||
}
|
||||
t.Assert(gvalid.CheckStructWithParamMap(data, g.Map{"uid": 1, "nickname": "john"}, nil), nil)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type UserApiSearch struct {
|
||||
Uid int64 `v:"required"`
|
||||
Nickname string `v:"required-with:uid"`
|
||||
}
|
||||
data := UserApiSearch{}
|
||||
t.AssertNE(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type UserApiSearch struct {
|
||||
Uid int64 `json:"uid" v:"required"`
|
||||
Nickname string `json:"nickname" v:"required-with:Uid"`
|
||||
}
|
||||
data := UserApiSearch{
|
||||
Uid: 1,
|
||||
}
|
||||
t.AssertNE(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type UserApiSearch struct {
|
||||
Uid int64 `json:"uid"`
|
||||
Nickname string `json:"nickname" v:"required-with:Uid"`
|
||||
StartTime *gtime.Time `json:"start_time" v:"required-with:EndTime"`
|
||||
EndTime *gtime.Time `json:"end_time" v:"required-with:StartTime"`
|
||||
}
|
||||
data := UserApiSearch{
|
||||
StartTime: nil,
|
||||
EndTime: nil,
|
||||
}
|
||||
t.Assert(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type UserApiSearch struct {
|
||||
Uid int64 `json:"uid"`
|
||||
Nickname string `json:"nickname" v:"required-with:Uid"`
|
||||
StartTime *gtime.Time `json:"start_time" v:"required-with:EndTime"`
|
||||
EndTime *gtime.Time `json:"end_time" v:"required-with:StartTime"`
|
||||
}
|
||||
data := UserApiSearch{
|
||||
StartTime: gtime.Now(),
|
||||
EndTime: nil,
|
||||
}
|
||||
t.AssertNE(gvalid.CheckStructWithParamMap(data, g.Map{"start_time": gtime.Now()}, nil), nil)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user