diff --git a/validation/validation.go b/validation/validation.go index 776f72c86..7964b6abd 100644 --- a/validation/validation.go +++ b/validation/validation.go @@ -2,8 +2,7 @@ package validation import ( "errors" - "reflect" - "time" + "net/url" "github.com/gookit/validate" @@ -29,34 +28,25 @@ func (r *Validation) Make(data any, rules map[string]string, options ...validate return nil, errors.New("rules can't be empty") } - var dataType reflect.Kind - switch data := data.(type) { + var dataFace validate.DataFace + var err error + switch td := data.(type) { + case validate.DataFace: + dataFace = td case map[string]any: - if len(data) == 0 { + if len(td) == 0 { return nil, errors.New("data can't be empty") } - dataType = reflect.Map - } - - val := reflect.ValueOf(data) - indirectVal := reflect.Indirect(val) - typ := indirectVal.Type() - if indirectVal.Kind() == reflect.Struct && typ != reflect.TypeOf(time.Time{}) { - dataType = reflect.Struct - } - - var dataFace validate.DataFace - switch dataType { - case reflect.Map: - dataFace = validate.FromMap(data.(map[string]any)) - case reflect.Struct: - var err error + dataFace = validate.FromMap(td) + case url.Values: + dataFace = validate.FromURLValues(td) + case map[string][]string: + dataFace = validate.FromURLValues(td) + default: dataFace, err = validate.FromStruct(data) if err != nil { - return nil, err + return nil, errors.New("data must be map[string]any or map[string][]string or struct") } - default: - return nil, errors.New("data must be map[string]any or struct") } options = append(options, Rules(rules), CustomRules(r.rules)) @@ -70,7 +60,7 @@ func (r *Validation) Make(data any, rules map[string]string, options ...validate v := dataFace.Create() AppendOptions(v, generateOptions) - return NewValidator(v, dataFace), nil + return NewValidator(v), nil } func (r *Validation) AddRules(rules []validatecontract.Rule) error { diff --git a/validation/validation_test.go b/validation/validation_test.go index efaba85dc..3fd6f1912 100644 --- a/validation/validation_test.go +++ b/validation/validation_test.go @@ -42,10 +42,10 @@ func TestMake(t *testing.T) { expectData: Data{A: "b"}, }, { - description: "error when data isn't map[string]any or struct", + description: "error when data isn't map[string]any or map[string][]string or struct", data: "1", rules: map[string]string{"a": "required"}, - expectErr: errors.New("data must be map[string]any or struct"), + expectErr: errors.New("data must be map[string]any or map[string][]string or struct"), }, { description: "error when data is empty map", @@ -106,7 +106,7 @@ func TestMake(t *testing.T) { }), }, expectValidator: true, - expectData: Data{A: "c"}, + expectData: Data{A: ""}, expectErrors: true, expectErrorMessage: "B can't be empty", }, @@ -146,7 +146,7 @@ func TestMake(t *testing.T) { }), }, expectValidator: true, - expectData: Data{A: "c"}, + expectData: Data{A: ""}, expectErrors: true, expectErrorMessage: "b can't be empty", }, @@ -2676,19 +2676,19 @@ func TestCustomRule(t *testing.T) { type Uppercase struct { } -//Signature The name of the rule. +// Signature The name of the rule. func (receiver *Uppercase) Signature() string { return "uppercase" } -//Passes Determine if the validation rule passes. +// Passes Determine if the validation rule passes. func (receiver *Uppercase) Passes(data httpvalidate.Data, val any, options ...any) bool { name, exist := data.Get("name") return strings.ToUpper(val.(string)) == val.(string) && len(val.(string)) == cast.ToInt(options[0]) && name == val && exist } -//Message Get the validation error message. +// Message Get the validation error message. func (receiver *Uppercase) Message() string { return ":attribute must be upper" } @@ -2696,19 +2696,19 @@ func (receiver *Uppercase) Message() string { type Lowercase struct { } -//Signature The name of the rule. +// Signature The name of the rule. func (receiver *Lowercase) Signature() string { return "lowercase" } -//Passes Determine if the validation rule passes. +// Passes Determine if the validation rule passes. func (receiver *Lowercase) Passes(data httpvalidate.Data, val any, options ...any) bool { address, exist := data.Get("address") return strings.ToLower(val.(string)) == val.(string) && len(val.(string)) == cast.ToInt(options[0]) && address == val && exist } -//Message Get the validation error message. +// Message Get the validation error message. func (receiver *Lowercase) Message() string { return ":attribute must be lower" } @@ -2716,17 +2716,17 @@ func (receiver *Lowercase) Message() string { type Duplicate struct { } -//Signature The name of the rule. +// Signature The name of the rule. func (receiver *Duplicate) Signature() string { return "required" } -//Passes Determine if the validation rule passes. +// Passes Determine if the validation rule passes. func (receiver *Duplicate) Passes(data httpvalidate.Data, val any, options ...any) bool { return true } -//Message Get the validation error message. +// Message Get the validation error message. func (receiver *Duplicate) Message() string { return "" } diff --git a/validation/validator.go b/validation/validator.go index 4b9bb5efc..0149cb843 100644 --- a/validation/validator.go +++ b/validation/validator.go @@ -1,9 +1,11 @@ package validation import ( - "net/url" + "reflect" "github.com/gookit/validate" + "github.com/mitchellh/mapstructure" + "github.com/spf13/cast" httpvalidate "github.com/goravel/framework/contracts/validation" ) @@ -12,48 +14,32 @@ func init() { validate.Config(func(opt *validate.GlobalOption) { opt.StopOnError = false opt.SkipOnEmpty = true + opt.FieldTag = "form" }) } type Validator struct { instance *validate.Validation - data validate.DataFace } -func NewValidator(instance *validate.Validation, data validate.DataFace) *Validator { +func NewValidator(instance *validate.Validation) *Validator { instance.Validate() - return &Validator{instance: instance, data: data} + return &Validator{instance: instance} } func (v *Validator) Bind(ptr any) error { - var data any - if _, ok := v.data.Src().(url.Values); ok { - values := make(map[string]any) - for key, value := range v.data.Src().(url.Values) { - if len(value) > 0 { - values[key] = value[0] - } - } - - formData, ok := v.data.(*validate.FormData) - if ok { - for key, value := range formData.Files { - values[key] = value - } - } - - data = values - } else { - data = v.data.Src() - } - - bts, err := validate.Marshal(data) + data := v.instance.SafeData() + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + TagName: "form", + Result: &ptr, + DecodeHook: v.castValue(), + }) if err != nil { return err } - return validate.Unmarshal(bts, ptr) + return decoder.Decode(data) } func (v *Validator) Errors() httpvalidate.Errors { @@ -67,3 +53,74 @@ func (v *Validator) Errors() httpvalidate.Errors { func (v *Validator) Fails() bool { return v.instance.IsFail() } + +func (v *Validator) castValue() mapstructure.DecodeHookFunc { + return func(from reflect.Value, to reflect.Value) (any, error) { + var castedValue any + var err error + + switch to.Kind() { + case reflect.String: + castedValue = cast.ToString(from.Interface()) + case reflect.Int: + castedValue, err = cast.ToIntE(from.Interface()) + case reflect.Int8: + castedValue, err = cast.ToInt8E(from.Interface()) + case reflect.Int16: + castedValue, err = cast.ToInt16E(from.Interface()) + case reflect.Int32: + castedValue, err = cast.ToInt32E(from.Interface()) + case reflect.Int64: + castedValue, err = cast.ToInt64E(from.Interface()) + case reflect.Uint: + castedValue, err = cast.ToUintE(from.Interface()) + case reflect.Uint8: + castedValue, err = cast.ToUint8E(from.Interface()) + case reflect.Uint16: + castedValue, err = cast.ToUint16E(from.Interface()) + case reflect.Uint32: + castedValue, err = cast.ToUint32E(from.Interface()) + case reflect.Uint64: + castedValue, err = cast.ToUint64E(from.Interface()) + case reflect.Bool: + castedValue, err = cast.ToBoolE(from.Interface()) + case reflect.Float32: + castedValue, err = cast.ToFloat32E(from.Interface()) + case reflect.Float64: + castedValue, err = cast.ToFloat64E(from.Interface()) + case reflect.Slice, reflect.Array: + switch to.Type().Elem().Kind() { + case reflect.String: + castedValue, err = cast.ToStringSliceE(from.Interface()) + case reflect.Int: + castedValue, err = cast.ToIntSliceE(from.Interface()) + case reflect.Bool: + castedValue, err = cast.ToBoolSliceE(from.Interface()) + default: + castedValue, err = cast.ToSliceE(from.Interface()) + } + case reflect.Map: + switch to.Type().Key().Kind() { + case reflect.String: + castedValue, err = cast.ToStringMapStringE(from.Interface()) + case reflect.Bool: + castedValue, err = cast.ToStringMapBoolE(from.Interface()) + case reflect.Int: + castedValue, err = cast.ToStringMapIntE(from.Interface()) + case reflect.Int64: + castedValue, err = cast.ToStringMapInt64E(from.Interface()) + default: + castedValue, err = cast.ToStringMapE(from.Interface()) + } + default: + castedValue = from.Interface() + } + + // Only return casted value if there was no error + if err == nil { + return castedValue, nil + } + + return from.Interface(), nil + } +} diff --git a/validation/validator_test.go b/validation/validator_test.go index d2af9ffdc..d76f82a54 100644 --- a/validation/validator_test.go +++ b/validation/validator_test.go @@ -11,6 +11,8 @@ import ( "github.com/gookit/validate" "github.com/stretchr/testify/assert" + + "github.com/goravel/framework/support/json" ) func TestBind(t *testing.T) { @@ -18,11 +20,10 @@ func TestBind(t *testing.T) { A string B int C string + D *Data File *multipart.FileHeader } - request := buildRequest(t) - tests := []struct { name string data validate.DataFace @@ -38,13 +39,29 @@ func TestBind(t *testing.T) { A: "aa", }, }, + { + name: "success when data is map and key is int", + data: validate.FromMap(map[string]any{"b": 1}), + rules: map[string]string{"b": "required"}, + expectData: Data{ + B: 1, + }, + }, + { + name: "success when data is map and cast key", + data: validate.FromMap(map[string]any{"b": "1"}), + rules: map[string]string{"b": "required"}, + expectData: Data{ + B: 1, + }, + }, { name: "success when data is map, key is lowercase and has errors", data: validate.FromMap(map[string]any{"a": "aa", "c": "cc"}), rules: map[string]string{"a": "required", "b": "required"}, expectData: Data{ - A: "aa", - C: "cc", + A: "", + C: "", }, }, { @@ -87,6 +104,23 @@ func TestBind(t *testing.T) { rules: map[string]string{"a": "required"}, expectData: Data{}, }, + { + name: "empty when data is struct and key is struct", + data: func() validate.DataFace { + data, err := validate.FromStruct(struct { + D *Data + }{ + D: &Data{ + A: "aa", + }, + }) + assert.Nil(t, err) + + return data + }(), + rules: map[string]string{"d.a": "required"}, + expectData: Data{}, + }, { name: "success when data is get request", data: func() validate.DataFace { @@ -97,22 +131,38 @@ func TestBind(t *testing.T) { return data }(), - rules: map[string]string{"A": "required"}, + rules: map[string]string{"a": "required"}, expectData: Data{ A: "aa", }, }, + { + name: "success when data is get request and params is int", + data: func() validate.DataFace { + request, err := http.NewRequest(http.MethodGet, "/?b=1", nil) + assert.Nil(t, err) + data, err := validate.FromRequest(request) + assert.Nil(t, err) + + return data + }(), + rules: map[string]string{"b": "required"}, + expectData: Data{ + B: 1, + }, + }, { name: "success when data is post request", data: func() validate.DataFace { - request, err := http.NewRequest(http.MethodGet, "/?a=aa", nil) + request, err := http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"a":"aa"}`)) + request.Header.Set("Content-Type", "application/json") assert.Nil(t, err) data, err := validate.FromRequest(request) assert.Nil(t, err) return data }(), - rules: map[string]string{"A": "required"}, + rules: map[string]string{"a": "required"}, expectData: Data{ A: "aa", }, @@ -120,13 +170,15 @@ func TestBind(t *testing.T) { { name: "success when data is post request with body", data: func() validate.DataFace { + request := buildRequest(t) data, err := validate.FromRequest(request, 1) assert.Nil(t, err) return data }(), - rules: map[string]string{"A": "required", "File": "required"}, + rules: map[string]string{"a": "required", "file": "required"}, expectData: func() Data { + request := buildRequest(t) _, fileHeader, _ := request.FormFile("file") data := Data{ A: "aa", @@ -138,16 +190,19 @@ func TestBind(t *testing.T) { }, } + validation := NewValidation() for _, test := range tests { t.Run(test.name, func(t *testing.T) { - validator := &Validator{data: test.data} + validator, err := validation.Make(test.data, test.rules) + assert.Nil(t, err) var data Data - err := validator.Bind(&data) + err = validator.Bind(&data) assert.Nil(t, test.expectErr, err) assert.Equal(t, test.expectData.A, data.A) assert.Equal(t, test.expectData.B, data.B) assert.Equal(t, test.expectData.C, data.C) + assert.Equal(t, test.expectData.D, data.D) assert.Equal(t, test.expectData.File == nil, data.File == nil) }) } @@ -185,6 +240,199 @@ func TestFails(t *testing.T) { } } +func TestCastValue(t *testing.T) { + type Data struct { + A string `form:"a" json:"a"` + B int `form:"b" json:"b"` + C int8 `form:"c" json:"c"` + D int16 `form:"d" json:"d"` + E int32 `form:"e" json:"e"` + F int64 `form:"f" json:"f"` + G uint `form:"g" json:"g"` + H uint8 `form:"h" json:"h"` + I uint16 `form:"i" json:"i"` + J uint32 `form:"j" json:"j"` + K uint64 `form:"k" json:"k"` + L bool `form:"l" json:"l"` + M float32 `form:"m" json:"m"` + N float64 `form:"n" json:"n"` + O []string `form:"o" json:"o"` + P map[string]string `form:"p" json:"p"` + } + + tests := []struct { + name string + data validate.DataFace + rules map[string]string + expectData Data + expectErr error + }{ + { + name: "success without cast data", + data: func() validate.DataFace { + body := &Data{ + A: "1", + B: 1, + C: 1, + D: 1, + E: 1, + F: 1, + G: 1, + H: 1, + I: 1, + J: 1, + K: 1, + L: true, + M: 1, + N: 1, + O: []string{"1"}, + P: map[string]string{"a": "aa"}, + } + jsonStr, err := json.Marshal(body) + assert.Nil(t, err) + request, err := http.NewRequest(http.MethodPost, "/", bytes.NewBuffer(jsonStr)) + request.Header.Set("Content-Type", "application/json") + assert.Nil(t, err) + data, err := validate.FromRequest(request) + assert.Nil(t, err) + + return data + }(), + rules: map[string]string{ + "a": "required", + "b": "required", + "c": "required", + "d": "required", + "e": "required", + "f": "required", + "g": "required", + "h": "required", + "i": "required", + "j": "required", + "k": "required", + "l": "required", + "m": "required", + "n": "required", + "o": "required", + "p": "required", + }, + expectData: Data{ + A: "1", + B: 1, + C: 1, + D: 1, + E: 1, + F: 1, + G: 1, + H: 1, + I: 1, + J: 1, + K: 1, + L: true, + M: 1, + N: 1, + O: []string{"1"}, + P: map[string]string{"a": "aa"}, + }, + }, { + name: "success with cast data", + data: func() validate.DataFace { + body := map[string]any{ + "a": 1, + "b": "1", + "c": "1", + "d": "1", + "e": "1", + "f": "1", + "g": "1", + "h": "1", + "i": "1", + "j": "1", + "k": "1", + "l": "true", + "m": "1", + "n": "1", + "o": []int{1}, + "p": map[string]string{"a": "aa"}, + } + jsonStr, err := json.Marshal(body) + assert.Nil(t, err) + request, err := http.NewRequest(http.MethodPost, "/", bytes.NewBuffer(jsonStr)) + request.Header.Set("Content-Type", "application/json") + assert.Nil(t, err) + data, err := validate.FromRequest(request) + assert.Nil(t, err) + + return data + }(), + rules: map[string]string{ + "a": "required", + "b": "required", + "c": "required", + "d": "required", + "e": "required", + "f": "required", + "g": "required", + "h": "required", + "i": "required", + "j": "required", + "k": "required", + "l": "required", + "m": "required", + "n": "required", + "o": "required", + "p": "required", + }, + expectData: Data{ + A: "1", + B: 1, + C: 1, + D: 1, + E: 1, + F: 1, + G: 1, + H: 1, + I: 1, + J: 1, + K: 1, + L: true, + M: 1, + N: 1, + O: []string{"1"}, + P: map[string]string{"a": "aa"}, + }, + }, + } + + validation := NewValidation() + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + validator, err := validation.Make(test.data, test.rules) + assert.Nil(t, err) + + var data Data + err = validator.Bind(&data) + assert.Nil(t, test.expectErr, err) + assert.Equal(t, test.expectData.A, data.A) + assert.Equal(t, test.expectData.B, data.B) + assert.Equal(t, test.expectData.C, data.C) + assert.Equal(t, test.expectData.D, data.D) + assert.Equal(t, test.expectData.E, data.E) + assert.Equal(t, test.expectData.F, data.F) + assert.Equal(t, test.expectData.G, data.G) + assert.Equal(t, test.expectData.H, data.H) + assert.Equal(t, test.expectData.I, data.I) + assert.Equal(t, test.expectData.J, data.J) + assert.Equal(t, test.expectData.K, data.K) + assert.Equal(t, test.expectData.L, data.L) + assert.Equal(t, test.expectData.M, data.M) + assert.Equal(t, test.expectData.N, data.N) + assert.Equal(t, test.expectData.O, data.O) + assert.Equal(t, test.expectData.P, data.P) + }) + } +} + func buildRequest(t *testing.T) *http.Request { payload := &bytes.Buffer{} writer := multipart.NewWriter(payload)