improve struct validation for package gvalid

This commit is contained in:
John Guo
2021-05-12 00:01:52 +08:00
parent a4240bdfb7
commit b06580d343
11 changed files with 168 additions and 73 deletions

View File

@ -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
}
}

View File

@ -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...)
}

View File

@ -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...)
}

View File

@ -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.

View File

@ -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)

View File

@ -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`)
})
}

View File

@ -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)

View File

@ -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"
@ -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位数字都可以通过验证)

View File

@ -9,17 +9,18 @@ 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 `ParamMapStrict`.
ParamMapStrict bool // Strictly using `ParamMap` as its validation source. If false, it ignores `ParamMap` and retrieves values from `Object`.
CustomRules interface{} // Custom validation rules.
CustomErrorMessageMap CustomMsg // Custom error message map for validation rules.
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 (
@ -39,11 +40,11 @@ func (v *Validator) CheckStruct(object interface{}, customRules interface{}, cus
message = customErrorMessageMap[0]
}
return v.doCheckStructWithParamMap(&doCheckStructWithParamMapInput{
Object: object,
ParamMap: nil,
ParamMapStrict: false,
CustomRules: customRules,
CustomErrorMessageMap: message,
Object: object,
ParamMap: nil,
UseParamMapInsteadOfObjectValue: false,
CustomRules: customRules,
CustomErrorMessageMap: message,
})
}
@ -59,11 +60,11 @@ func (v *Validator) CheckStructWithParamMap(object interface{}, paramMap interfa
message = customErrorMessageMap[0]
}
return v.doCheckStructWithParamMap(&doCheckStructWithParamMapInput{
Object: object,
ParamMap: paramMap,
ParamMapStrict: true,
CustomRules: customRules,
CustomErrorMessageMap: message,
Object: object,
ParamMap: paramMap,
UseParamMapInsteadOfObjectValue: true,
CustomRules: customRules,
CustomErrorMessageMap: message,
})
}
@ -147,13 +148,13 @@ func (v *Validator) doCheckStructWithParamMap(input *doCheckStructWithParamMapIn
return nil
}
// Input parameter map handling.
if input.ParamMap == nil || !input.ParamMapStrict {
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.ParamMapStrict {
if !input.UseParamMapInsteadOfObjectValue {
for nameOrTag, field := range fieldMap {
inputParamMap[nameOrTag] = field.Value.Interface()
if nameOrTag != field.Name() {
@ -174,7 +175,9 @@ func (v *Validator) doCheckStructWithParamMap(input *doCheckStructWithParamMapIn
}
// It here extends the params map using alias names.
if _, ok := inputParamMap[name]; !ok {
inputParamMap[name] = field.Value.Interface()
if !input.UseParamMapInsteadOfObjectValue {
inputParamMap[name] = field.Value.Interface()
}
}
if _, ok := checkRules[name]; !ok {
if _, ok := checkRules[fieldName]; ok {
@ -187,7 +190,7 @@ func (v *Validator) doCheckStructWithParamMap(input *doCheckStructWithParamMapIn
}
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 {
@ -229,10 +232,7 @@ func (v *Validator) doCheckStructWithParamMap(input *doCheckStructWithParamMapIn
// The following logic is the same as some of CheckMap.
var value interface{}
for key, rule := range checkRules {
value = nil
if v, ok := inputParamMap[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], inputParamMap); e != nil {
_, item := e.FirstItem()

View File

@ -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
}

View File

@ -357,11 +357,22 @@ func Test_CheckStruct_InvalidRule(t *testing.T) {
func TestValidator_CheckStructWithParamMap(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type UserApiSearch struct {
Uid int64 `json:"uid" v:"required"`
Nickname string `json:"nickname" v:"required-with:Uid"`
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.Assert(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil)
t.AssertNE(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil)
})
gtest.C(t, func(t *gtest.T) {
type UserApiSearch struct {
@ -398,6 +409,6 @@ func TestValidator_CheckStructWithParamMap(t *testing.T) {
StartTime: gtime.Now(),
EndTime: nil,
}
t.AssertNE(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil)
t.AssertNE(gvalid.CheckStructWithParamMap(data, g.Map{"start_time": gtime.Now()}, nil), nil)
})
}