diff --git a/v2/examples/custom_rule.go b/v2/examples/custom_rule.go new file mode 100644 index 0000000..1e83526 --- /dev/null +++ b/v2/examples/custom_rule.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "log" + + gody "github.com/guiferpa/gody/v2" + "github.com/guiferpa/gody/v2/rule" +) + +// ErrInvalidPalindrome is a custom error to a specific rule implementation +type ErrInvalidPalindrome struct { + Value string +} + +func (e *ErrInvalidPalindrome) Error() string { + return fmt.Sprintf("invalid palindrome: %s", e.Value) +} + +// PalindromeRule is a struct that implements the Rule interface +type PalindromeRule struct{} + +// Name is a func from the Rule contract +func (r *PalindromeRule) Name() string { + return "palindrome" +} + +// Validate is a func from the Rule contract +func (r *PalindromeRule) Validate(f, v, p string) (bool, error) { + // TODO: The algorithm for palindrome validation + return true, &ErrInvalidPalindrome{Value: v} +} + +func CustomRuleValidation() { + b := struct { + Text string `json:"text" validate:"min_bound=5"` + Palindrome string `json:"palindrome" validate:"palindrome"` + }{ + Text: "test-text", + Palindrome: "test-palindrome", + } + + customRules := []gody.Rule{ + &PalindromeRule{}, + } + + valid, err := gody.DefaultValidate(b, customRules) + if err != nil { + if !valid { + log.Println("body do not validated", err) + } + + switch err.(type) { + case *rule.ErrRequired: + log.Println("required error:", err) + + case *ErrInvalidPalindrome: + log.Println("palindrome error:", err) + } + } +} diff --git a/v2/examples/http_api.go b/v2/examples/http_api.go new file mode 100644 index 0000000..ec6749a --- /dev/null +++ b/v2/examples/http_api.go @@ -0,0 +1,107 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" + + gody "github.com/guiferpa/gody/v2" +) + +type ErrIsAdult struct{} + +func (err *ErrIsAdult) Error() string { + return "The client isn't a adult then it isn't allowed buy" +} + +type IsAdultRule struct { + adultAge int +} + +func (r *IsAdultRule) Name() string { + return "is_adult" +} + +func (r *IsAdultRule) Validate(_, value, _ string) (bool, error) { + if value == "" { + return true, &ErrIsAdult{} + } + + clientAge, err := strconv.Atoi(value) + if err != nil { + return false, err + } + + if clientAge < r.adultAge { + return true, &ErrIsAdult{} + } + + return true, nil +} + +type User struct { + Name string `validate:"min_bound=5"` + Age int16 `validate:"min=10 is_adult"` +} + +type Product struct { + Name string `validate:"not_empty"` + Description string `validate:"not_empty"` + Price int +} + +type Cart struct { + Owner User + Products []Product +} + +func HTTPServerAPI() { + validator := gody.NewValidator() + + if err := validator.AddRules(&IsAdultRule{adultAge: 21}); err != nil { + log.Fatalln(err) + } + + port := ":8092" + log.Printf("Example for REST API is running on port %v, now send a POST to /carts and set the claimed Cart struct as request body", port) + http.ListenAndServe(port, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/carts" || r.Method != http.MethodPost { + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "Path or method is wrong: path: %v, method: %v\n", r.URL.Path, r.Method) + return + } + + var body Cart + err := json.NewDecoder(r.Body).Decode(&body) + defer r.Body.Close() + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintln(w, err) + return + } + + if validated, err := validator.Validate(body); err != nil { + if !validated { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Validation for body wasn't processed because of error: %v\n", err) + return + } + + w.WriteHeader(http.StatusUnprocessableEntity) + + if _, ok := err.(*ErrIsAdult); ok { + fmt.Fprintf(w, "The client called %v isn't a adult\n", body.Owner.Name) + return + } + + fmt.Fprintln(w, err) + return + } + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, "The client %v created your cart!\n", body.Owner.Name) + return + })) +} diff --git a/v2/examples/simple.go b/v2/examples/simple.go new file mode 100644 index 0000000..911317c --- /dev/null +++ b/v2/examples/simple.go @@ -0,0 +1,75 @@ +package main + +import ( + "log" + + gody "github.com/guiferpa/gody/v2" + "github.com/guiferpa/gody/v2/rule" +) + +func SimpleDefaultValidation() { + b := struct { + Text string `json:"text" validate:"not_empty"` + }{} + + valid, err := gody.DefaultValidate(b, nil) + if err != nil { + if !valid { + log.Println("body do not validated:", err) + } + + switch err.(type) { + case *rule.ErrNotEmpty: + log.Println("not empty error:", err) + + } + } +} + +func SimplePureValidation() { + b := struct { + Text string `json:"text" validate:"not_empty"` + }{} + + rules := []gody.Rule{ + rule.NotEmpty, + } + valid, err := gody.Validate(b, rules) + if err != nil { + if !valid { + log.Println("body do not validated:", err) + } + + switch err.(type) { + case *rule.ErrNotEmpty: + log.Println("not empty error:", err) + + } + } +} + +func SimpleValidationFromValidator() { + b := struct { + Text string `json:"text" validate:"not_empty"` + }{} + + validator := gody.NewValidator() + + if err := validator.AddRules(rule.NotEmpty); err != nil { + log.Println(err) + return + } + + valid, err := validator.Validate(b) + if err != nil { + if !valid { + log.Println("body do not validated:", err) + } + + switch err.(type) { + case *rule.ErrNotEmpty: + log.Println("not empty error:", err) + + } + } +} diff --git a/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..621a7ea --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,3 @@ +module github.com/guiferpa/gody/v2 + +go 1.13 diff --git a/v2/rule.go b/v2/rule.go new file mode 100644 index 0000000..d6f156d --- /dev/null +++ b/v2/rule.go @@ -0,0 +1,7 @@ +package gody + +// Rule is a interface with the contract to implement a any rule +type Rule interface { + Name() string + Validate(name, value, param string) (bool, error) +} diff --git a/v2/rule/enum.go b/v2/rule/enum.go new file mode 100644 index 0000000..e1ecef2 --- /dev/null +++ b/v2/rule/enum.go @@ -0,0 +1,36 @@ +package rule + +import ( + "fmt" + "strings" +) + +type enum struct{} + +func (r *enum) Name() string { + return "enum" +} + +// ErrEnum is the representation about any error happened inside of the rule Enum +type ErrEnum struct { + Field string + Value string + Enum []string +} + +func (err *ErrEnum) Error() string { + return fmt.Sprintf("the value %v in field %v not contains in %v", err.Value, err.Field, err.Enum) +} + +func (r *enum) Validate(f, v, p string) (bool, error) { + if v == "" { + return true, nil + } + es := strings.Split(p, ",") + for _, e := range es { + if v == e { + return true, nil + } + } + return true, &ErrEnum{f, v, es} +} diff --git a/v2/rule/enum_test.go b/v2/rule/enum_test.go new file mode 100644 index 0000000..1708206 --- /dev/null +++ b/v2/rule/enum_test.go @@ -0,0 +1,53 @@ +package rule + +import ( + "testing" +) + +func TestEnumName(t *testing.T) { + r := Enum + if r.Name() != "enum" { + t.Errorf("unexpected result, result: %v, expected: %v", r.Name(), "enum") + } +} + +func TestEnum(t *testing.T) { + r := Enum + cases := []struct { + value, param string + }{ + {"a", "a,b,c,d"}, + {"b", "a,b,c,d"}, + {"c", "a,b,c,d"}, + {"d", "a,b,c,d"}, + {"", "a,b,c,d"}, + } + for _, test := range cases { + ok, err := r.Validate("", test.value, test.param) + if err != nil { + t.Error(err) + } + if !ok { + t.Errorf("unexpected result, result: %v, expected: %v", ok, true) + } + } +} + +func TestEnumWithInvalidParams(t *testing.T) { + r := Enum + cases := []struct { + value, param string + }{ + {"d", "a,b,c"}, + {"1", "a,b,c"}, + } + for _, test := range cases { + ok, err := r.Validate("", test.value, test.param) + if _, ok := err.(*ErrEnum); !ok { + t.Error(err) + } + if !ok { + t.Errorf("unexpected result, result: %v, expected: %v", ok, true) + } + } +} diff --git a/v2/rule/max.go b/v2/rule/max.go new file mode 100644 index 0000000..46592c5 --- /dev/null +++ b/v2/rule/max.go @@ -0,0 +1,39 @@ +package rule + +import ( + "fmt" + "strconv" + "strings" +) + +type max struct{} + +func (r *max) Name() string { + return "max" +} + +// ErrMax is the representation about any error happened inside of the rule Max +type ErrMax struct { + Field string + Value int + Max int +} + +func (err *ErrMax) Error() string { + return fmt.Sprintf("the value %v in field %v is grater than %v", err.Value, err.Field, err.Max) +} + +func (r *max) Validate(f, v, p string) (bool, error) { + n, err := strconv.Atoi(p) + if err != nil { + return false, err + } + vn, err := strconv.Atoi(v) + if err != nil { + return false, err + } + if vn > n { + return true, &ErrMax{strings.ToLower(f), vn, n} + } + return true, nil +} diff --git a/v2/rule/max_bound.go b/v2/rule/max_bound.go new file mode 100644 index 0000000..4e21496 --- /dev/null +++ b/v2/rule/max_bound.go @@ -0,0 +1,35 @@ +package rule + +import ( + "fmt" + "strconv" + "strings" +) + +type maxBound struct{} + +func (r *maxBound) Name() string { + return "max_bound" +} + +// ErrMaxBound is the representation about any error happened inside of the rule MaxBound +type ErrMaxBound struct { + Field string + Value string + Bound int +} + +func (err *ErrMaxBound) Error() string { + return fmt.Sprintf("the value %v in field %v has character limit greater than %v", err.Value, err.Field, err.Bound) +} + +func (r *maxBound) Validate(f, v, p string) (bool, error) { + n, err := strconv.Atoi(p) + if err != nil { + return false, err + } + if len(v) > n { + return true, &ErrMaxBound{strings.ToLower(f), v, n} + } + return true, nil +} diff --git a/v2/rule/max_bound_test.go b/v2/rule/max_bound_test.go new file mode 100644 index 0000000..dd6786c --- /dev/null +++ b/v2/rule/max_bound_test.go @@ -0,0 +1,72 @@ +package rule + +import ( + "testing" +) + +func TestMaxBoundName(t *testing.T) { + r := MaxBound + if r.Name() != "max_bound" { + t.Errorf("unexpected result, result: %v, expected: %v", r.Name(), "max_bound") + } +} + +func TestMaxBound(t *testing.T) { + r := MaxBound + cases := []struct { + value, param string + }{ + {"", "0"}, + {"", "1"}, + {"a", "2"}, + {"fla", "3"}, + } + for _, test := range cases { + ok, err := r.Validate("", test.value, test.param) + if err != nil { + t.Error(err) + } + if !ok { + t.Errorf("unexpected result, result: %v, expected: %v", ok, true) + } + } +} + +func TestMaxBoundWithoutLimit(t *testing.T) { + r := MaxBound + cases := []struct { + value, param string + }{ + {"fla", "2"}, + {"123", "2"}, + {"1", "0"}, + } + for _, test := range cases { + ok, err := r.Validate("", test.value, test.param) + if _, ok := err.(*ErrMaxBound); !ok { + t.Error(err) + } + if !ok { + t.Errorf("unexpected result, result: %v, expected: %v", ok, true) + } + } +} + +func TestMaxBoundWithInvalidParam(t *testing.T) { + r := MaxBound + cases := []struct { + value, param string + }{ + {"1", "test"}, + {"zico", "true"}, + } + for _, test := range cases { + ok, err := r.Validate("", test.value, test.param) + if err == nil { + t.Error("unexpected no error") + } + if ok { + t.Errorf("unexpected result as okay") + } + } +} diff --git a/v2/rule/max_test.go b/v2/rule/max_test.go new file mode 100644 index 0000000..305f4bb --- /dev/null +++ b/v2/rule/max_test.go @@ -0,0 +1,89 @@ +package rule + +import ( + "testing" +) + +func TestMaxName(t *testing.T) { + r := Max + if r.Name() != "max" { + t.Errorf("unexpected result, result: %v, expected: %v", r.Name(), "max") + } +} + +func TestMax(t *testing.T) { + r := Max + cases := []struct { + value, param string + }{ + {"2", "3"}, + {"500", "2000"}, + {"-1", "0"}, + } + for _, test := range cases { + ok, err := r.Validate("", test.value, test.param) + if err != nil { + t.Error(err) + } + if !ok { + t.Errorf("unexpected result, result: %v, expected: %v", ok, true) + } + } +} + +func TestMaxWithInvalidParam(t *testing.T) { + r := Max + cases := []struct { + value, param string + }{ + {"2", "test"}, + {"500", "true"}, + } + for _, test := range cases { + ok, err := r.Validate("", test.value, test.param) + if err == nil { + t.Error("unexpected no error") + } + if ok { + t.Errorf("unexpected validation result as okay") + } + } +} + +func TestMaxWithInvalidValue(t *testing.T) { + r := Max + cases := []struct { + value, param string + }{ + {"test", "2"}, + {"true", "500"}, + } + for _, test := range cases { + ok, err := r.Validate("", test.value, test.param) + if err == nil { + t.Error("unexpected no error") + } + if ok { + t.Errorf("unexpected validation result as okay") + } + } + +} + +func TestMaxFailure(t *testing.T) { + r := Max + cases := []struct { + value, param string + }{ + {"12", "3"}, + {"5000", "2000"}, + {"0", "-1"}, + {"-20", "-22"}, + } + for _, test := range cases { + _, err := r.Validate("", test.value, test.param) + if _, ok := err.(*ErrMax); !ok { + t.Errorf("unexpected error: %v", err) + } + } +} diff --git a/v2/rule/min.go b/v2/rule/min.go new file mode 100644 index 0000000..8d97615 --- /dev/null +++ b/v2/rule/min.go @@ -0,0 +1,39 @@ +package rule + +import ( + "fmt" + "strconv" + "strings" +) + +type min struct{} + +func (r *min) Name() string { + return "min" +} + +// ErrMin is the representation about any error happened inside of the rule Min +type ErrMin struct { + Field string + Value int + Min int +} + +func (err *ErrMin) Error() string { + return fmt.Sprintf("the value %v in field %v is less than %v", err.Value, err.Field, err.Min) +} + +func (r *min) Validate(f, v, p string) (bool, error) { + n, err := strconv.Atoi(p) + if err != nil { + return false, err + } + vn, err := strconv.Atoi(v) + if err != nil { + return false, err + } + if vn < n { + return true, &ErrMin{strings.ToLower(f), vn, n} + } + return true, nil +} diff --git a/v2/rule/min_bound.go b/v2/rule/min_bound.go new file mode 100644 index 0000000..20b1fa0 --- /dev/null +++ b/v2/rule/min_bound.go @@ -0,0 +1,35 @@ +package rule + +import ( + "fmt" + "strconv" + "strings" +) + +type minBound struct{} + +func (r *minBound) Name() string { + return "min_bound" +} + +// ErrMinBound is the representation about any error happened inside of the rule MinBound +type ErrMinBound struct { + Field string + Value string + Bound int +} + +func (err *ErrMinBound) Error() string { + return fmt.Sprintf("the value %v in field %v has character limit less than %v", err.Value, err.Field, err.Bound) +} + +func (r *minBound) Validate(f, v, p string) (bool, error) { + n, err := strconv.Atoi(p) + if err != nil { + return false, err + } + if len(v) < n { + return true, &ErrMinBound{strings.ToLower(f), v, n} + } + return true, nil +} diff --git a/v2/rule/min_bound_test.go b/v2/rule/min_bound_test.go new file mode 100644 index 0000000..3ec6325 --- /dev/null +++ b/v2/rule/min_bound_test.go @@ -0,0 +1,70 @@ +package rule + +import ( + "testing" +) + +func TestMinBoundName(t *testing.T) { + r := MinBound + if r.Name() != "min_bound" { + t.Errorf("unexpected result, result: %v, expected: %v", r.Name(), "min_bound") + } +} + +func TestMinBound(t *testing.T) { + r := MinBound + cases := []struct { + value, param string + }{ + {"", "0"}, + {"a", "1"}, + {"aaa", "2"}, + } + for _, test := range cases { + ok, err := r.Validate("", test.value, test.param) + if err != nil { + t.Error(err) + } + if !ok { + t.Errorf("unexpected result, result: %v, expected: %v", ok, true) + } + } +} + +func TestMinBoundWithoutLimit(t *testing.T) { + r := MinBound + cases := []struct { + value, param string + }{ + {"fl", "3"}, + {"123", "4"}, + } + for _, test := range cases { + ok, err := r.Validate("", test.value, test.param) + if _, ok := err.(*ErrMinBound); !ok { + t.Error(err) + } + if !ok { + t.Errorf("unexpected result, result: %v, expected: %v", ok, true) + } + } +} + +func TestMinBoundWithInvalidParam(t *testing.T) { + r := MinBound + cases := []struct { + value, param string + }{ + {"2", "test"}, + {"500", "true"}, + } + for _, test := range cases { + ok, err := r.Validate("", test.value, test.param) + if err == nil { + t.Error("unexpected no error") + } + if ok { + t.Errorf("unexpected validation result as okay") + } + } +} diff --git a/v2/rule/min_test.go b/v2/rule/min_test.go new file mode 100644 index 0000000..c93461f --- /dev/null +++ b/v2/rule/min_test.go @@ -0,0 +1,89 @@ +package rule + +import ( + "testing" +) + +func TestMinName(t *testing.T) { + r := Min + if r.Name() != "min" { + t.Errorf("unexpected result, result: %v, expected: %v", r.Name(), "min") + } +} + +func TestMin(t *testing.T) { + r := Min + cases := []struct { + value, param string + }{ + {"3", "2"}, + {"2000", "500"}, + {"0", "-1"}, + } + for _, test := range cases { + ok, err := r.Validate("", test.value, test.param) + if err != nil { + t.Error(err) + } + if !ok { + t.Errorf("unexpected result, result: %v, expected: %v", ok, true) + } + } +} + +func TestMinWithInvalidParam(t *testing.T) { + r := Min + cases := []struct { + value, param string + }{ + {"2", "test"}, + {"500", "true"}, + } + for _, test := range cases { + ok, err := r.Validate("", test.value, test.param) + if err == nil { + t.Error("unexpected no error") + } + if ok { + t.Errorf("unexpected validation result as okay") + } + } +} + +func TestMinWithInvalidValue(t *testing.T) { + r := Min + cases := []struct { + value, param string + }{ + {"test", "2"}, + {"true", "500"}, + } + for _, test := range cases { + ok, err := r.Validate("", test.value, test.param) + if err == nil { + t.Error("unexpected no error") + } + if ok { + t.Errorf("unexpected validation result as okay") + } + } + +} + +func TestMinFailure(t *testing.T) { + r := Min + cases := []struct { + value, param string + }{ + {"12", "30"}, + {"5000", "20000"}, + {"-1", "0"}, + {"-22", "-20"}, + } + for _, test := range cases { + _, err := r.Validate("", test.value, test.param) + if _, ok := err.(*ErrMin); !ok { + t.Errorf("unexpected error: %v", err) + } + } +} diff --git a/v2/rule/not_empty.go b/v2/rule/not_empty.go new file mode 100644 index 0000000..f9c4de8 --- /dev/null +++ b/v2/rule/not_empty.go @@ -0,0 +1,28 @@ +package rule + +import ( + "fmt" + "strings" +) + +type notEmpty struct{} + +func (_ *notEmpty) Name() string { + return "not_empty" +} + +// ErrNotEmpty is the representation about any error happened inside of the rule NotEmpty +type ErrNotEmpty struct { + Field string +} + +func (e *ErrNotEmpty) Error() string { + return fmt.Sprintf("field %v cannot be empty", e.Field) +} + +func (r *notEmpty) Validate(f, v, _ string) (bool, error) { + if v == "" { + return true, &ErrNotEmpty{strings.ToLower(f)} + } + return true, nil +} diff --git a/v2/rule/not_empty_test.go b/v2/rule/not_empty_test.go new file mode 100644 index 0000000..db3f8f6 --- /dev/null +++ b/v2/rule/not_empty_test.go @@ -0,0 +1,43 @@ +package rule + +import ( + "reflect" + "testing" +) + +func TestNotEmptyName(t *testing.T) { + got := NotEmpty.Name() + want := "not_empty" + if got != want { + t.Errorf("NotEmpty.Name(), got: %v, want: %v", got, want) + } +} + +func TestNotEmptyWithSuccessful(t *testing.T) { + valid, err := NotEmpty.Validate("text", "test", "") + if got, want := valid, true; got != want { + t.Errorf(`valid, _ := NotEmpty.Validate("text", "test", ""), got: %v, want: %v`, got, want) + } + if got, want := reflect.TypeOf(err), reflect.TypeOf(nil); got != want { + t.Errorf(`_, err := NotEmpty.Validate("text", "test", ""), got: %v, want: %v`, got, want) + } +} + +func TestNotEmptyWithEmptyValue(t *testing.T) { + valid, err := NotEmpty.Validate("text", "", "") + if got, want := valid, true; got != want { + t.Errorf(`valid, _ := NotEmpty.Validate("text", "", ""), got: %v, want: %v`, got, want) + } + if got, want := reflect.TypeOf(err), reflect.TypeOf(&ErrNotEmpty{}); got != want { + t.Errorf(`_, err := NotEmpty.Validate("text", "", ""), got: %v, want: %v`, got, want) + } +} + +func TestNotEmptyError(t *testing.T) { + err := &ErrNotEmpty{Field: "text"} + got := err.Error() + want := "field text cannot be empty" + if got != want { + t.Errorf(`&ErrNotEmpty{Field: "text"}.Error(), got: %v, want: %v`, got, want) + } +} diff --git a/v2/rule/required.go b/v2/rule/required.go new file mode 100644 index 0000000..23479e0 --- /dev/null +++ b/v2/rule/required.go @@ -0,0 +1,35 @@ +package rule + +import ( + "fmt" + "log" + "strconv" + "strings" +) + +type required struct{} + +func (r *required) Name() string { + return "required" +} + +// ErrRequired is the representation about any error happened inside of the rule Required +type ErrRequired struct { + Field string +} + +func (e *ErrRequired) Error() string { + return fmt.Sprintf("%v is required", e.Field) +} + +func (r *required) Validate(f, v, p string) (bool, error) { + b, err := strconv.ParseBool(p) + if err != nil { + return false, err + } + log.Printf("[guiferpa/gody] :: ATTETION :: required rule is deprecated, please replace to use not_empty.") + if b && v != "" { + return true, nil + } + return true, &ErrRequired{strings.ToLower(f)} +} diff --git a/v2/rule/required_test.go b/v2/rule/required_test.go new file mode 100644 index 0000000..1e81432 --- /dev/null +++ b/v2/rule/required_test.go @@ -0,0 +1,45 @@ +package rule + +import ( + "testing" +) + +func TestRequiredName(t *testing.T) { + r := Required + if r.Name() != "required" { + t.Errorf("unexpected result, result: %v, expected: %v", r.Name(), "required") + } +} + +func TestRequired(t *testing.T) { + r := Required + ok, err := r.Validate("", "test", "true") + if err != nil { + t.Error(err) + } + if !ok { + t.Errorf("unexpected result, result: %v, expected: %v", ok, true) + } +} + +func TestRequiredWithEmptyValue(t *testing.T) { + r := Required + ok, err := r.Validate("", "", "true") + if _, ok := err.(*ErrRequired); !ok { + t.Error(err) + } + if !ok { + t.Errorf("unexpected result, result: %v, expected: %v", ok, true) + } +} + +func TestRequiredWithInvalidParam(t *testing.T) { + r := Required + ok, err := r.Validate("", "", "axl-rose&slash") + if err == nil { + t.Errorf("unexpected error as result") + } + if ok { + t.Errorf("unexpected result, result: %v, expected: %v", ok, false) + } +} diff --git a/v2/rule/rule.go b/v2/rule/rule.go new file mode 100644 index 0000000..e53b675 --- /dev/null +++ b/v2/rule/rule.go @@ -0,0 +1,24 @@ +package rule + +var ( + // NotEmpty is a rule implemented + NotEmpty = ¬Empty{} + + // Required is a rule implemented + Required = &required{} + + // Max is a rule implemented + Max = &max{} + + // Min is a rule implemented + Min = &min{} + + // Enum is a rule implemented + Enum = &enum{} + + // MaxBound is a rule implemented + MaxBound = &maxBound{} + + // MinBound is a rule implemented + MinBound = &minBound{} +) diff --git a/v2/rule/ruletest/rule.go b/v2/rule/ruletest/rule.go new file mode 100644 index 0000000..2f4a884 --- /dev/null +++ b/v2/rule/ruletest/rule.go @@ -0,0 +1,21 @@ +package ruletest + +type Rule struct { + name string + validated bool + err error + ValidateCalled bool +} + +func (r *Rule) Name() string { + return r.name +} + +func (r *Rule) Validate(name, value, param string) (bool, error) { + r.ValidateCalled = true + return r.validated, r.err +} + +func NewRule(name string, validated bool, err error) *Rule { + return &Rule{name, validated, err, false} +} diff --git a/v2/serialize.go b/v2/serialize.go new file mode 100644 index 0000000..e2c5792 --- /dev/null +++ b/v2/serialize.go @@ -0,0 +1,142 @@ +package gody + +import ( + "fmt" + "reflect" + "strings" +) + +// ErrInvalidBody represents all invalid body report +type ErrInvalidBody struct { + Kind reflect.Kind +} + +func (e *ErrInvalidBody) Error() string { + return fmt.Sprintln("invalid body:", e.Kind) +} + +// ErrInvalidTag represents all invalid tag report +type ErrInvalidTag struct { + Format string +} + +func (e *ErrInvalidTag) Error() string { + return fmt.Sprintln("invalid tag:", e.Format) +} + +type ErrEmptyTagName struct{} + +func (e *ErrEmptyTagName) Error() string { + return "tag name is empty" +} + +// Field is a struct to represents the domain about a field inner gody lib +type Field struct { + Name string + Value string + Tags map[string]string +} + +func Serialize(b interface{}) ([]Field, error) { + return RawSerialize(DefaultTagName, b) +} + +// RawSerialize is a func to serialize/parse all content about the struct input +func RawSerialize(tn string, b interface{}) ([]Field, error) { + if tn == "" { + return nil, &ErrEmptyTagName{} + } + + if b == nil { + return nil, &ErrInvalidBody{} + } + + valueOf := reflect.ValueOf(b) + typeOf := reflect.TypeOf(b) + + if kindOfBody := typeOf.Kind(); kindOfBody != reflect.Struct { + return nil, &ErrInvalidBody{Kind: kindOfBody} + } + + fields := make([]Field, 0) + for i := 0; i < typeOf.NumField(); i++ { + field := typeOf.Field(i) + tagString := field.Tag.Get(tn) + if tagString == "" && field.Type.Kind() != reflect.Slice && field.Type.Kind() != reflect.Struct { + continue + } + + tagFormats := strings.Fields(tagString) + tags := make(map[string]string) + for _, tagFormat := range tagFormats { + tagFormatSplitted := strings.Split(tagFormat, "=") + + if len(tagFormatSplitted) == 2 { + tagFormatRule := tagFormatSplitted[0] + tagFormatValue := tagFormatSplitted[1] + if tagFormatValue == "" { + return nil, &ErrInvalidTag{Format: tagFormat} + } + + tags[tagFormatRule] = tagFormatValue + continue + } + + if len(tagFormatSplitted) == 1 { + tagFormatRule := tagFormatSplitted[0] + + tags[tagFormatRule] = "" + continue + } + + return nil, &ErrInvalidTag{Format: tagFormat} + } + + fieldValue := valueOf.FieldByName(field.Name) + fieldNameToLower := strings.ToLower(field.Name) + if kindOfField := field.Type.Kind(); kindOfField == reflect.Struct { + if fieldConverted := fieldValue.Convert(fieldValue.Type()); fieldConverted.CanInterface() { + payload := fieldConverted.Interface() + serialized, err := RawSerialize(tn, payload) + if err != nil { + return nil, err + } + for _, item := range serialized { + fields = append(fields, Field{ + Name: fmt.Sprintf("%s.%s", fieldNameToLower, item.Name), + Value: item.Value, + Tags: item.Tags, + }) + } + } + } else if kindOfField := field.Type.Kind(); kindOfField == reflect.Slice { + j := fieldValue.Len() + for i := 0; i < j; i++ { + sliceFieldValue := fieldValue.Index(i) + if sliceFieldConverted := sliceFieldValue.Convert(sliceFieldValue.Type()); sliceFieldConverted.CanInterface() { + payload := sliceFieldValue.Convert(sliceFieldValue.Type()).Interface() + serialized, err := RawSerialize(tn, payload) + if err != nil { + return nil, err + } + for _, item := range serialized { + fields = append(fields, Field{ + Name: fmt.Sprintf("%s[%v].%s", fieldNameToLower, i, item.Name), + Value: item.Value, + Tags: item.Tags, + }) + } + } + } + } else { + fieldValueString := fmt.Sprintf("%v", fieldValue) + fields = append(fields, Field{ + Name: fieldNameToLower, + Value: fieldValueString, + Tags: tags, + }) + } + } + + return fields, nil +} diff --git a/v2/serialize_test.go b/v2/serialize_test.go new file mode 100644 index 0000000..dbcb048 --- /dev/null +++ b/v2/serialize_test.go @@ -0,0 +1,190 @@ +package gody + +import ( + "fmt" + "testing" +) + +type TestSerializeStructA struct { + a string +} + +type TestSerializeStructB struct { + b string + a TestSerializeStructA +} + +type TestSerializeStructC struct { + c string + B TestSerializeStructB + A TestSerializeStructA +} + +func TestSerializeBodyStruct(t *testing.T) { + cases := []struct { + param interface{} + ok bool + }{ + {map[string]string{"test-key": "test-value"}, false}, + {TestSerializeStructA{a: "a"}, true}, + {TestSerializeStructB{b: "b", a: TestSerializeStructA{a: "a"}}, true}, + {TestSerializeStructC{c: "c", B: TestSerializeStructB{b: "b", a: TestSerializeStructA{a: "a"}}, A: TestSerializeStructA{a: "a"}}, true}, + {10, false}, + {struct{}{}, true}, + {"", false}, + {nil, false}, + } + + for _, c := range cases { + _, err := Serialize(c.param) + if _, ok := err.(*ErrInvalidBody); ok == c.ok { + t.Error(err) + } + } +} + +func TestSerializeBodyTagFormat(t *testing.T) { + cases := []struct { + param interface{} + ok bool + }{ + {struct { + Value string `validate:"required"` + }{"test-value"}, true}, + {struct { + Value string `validate:"required=true"` + }{"test-value"}, true}, + {struct { + Value string `validate:"required="` + }{"test-value"}, false}, + {struct { + Value string `validate:"=required="` + }{"test-value"}, false}, + {struct { + Value string `validate:"="` + }{"test-value"}, false}, + {struct { + Value string + }{"test-value"}, true}, + } + + for _, c := range cases { + _, err := Serialize(c.param) + if _, ok := err.(*ErrInvalidTag); ok == c.ok { + t.Error(err) + } + } +} + +func TestSerialize(t *testing.T) { + body := struct { + A string `validate:"test"` + B int `json:"b"` + C bool `validate:"test test_number=true"` + }{"a-value", 10, true} + + fields, err := Serialize(body) + if err != nil { + t.Error(err) + return + } + + if got, want := len(fields), 2; got != want { + t.Errorf("Length of serialized fields isn't equals: got: %v want: %v", got, want) + return + } + + wantedFields := []Field{ + {Name: "a", Value: "a-value", Tags: map[string]string{"test": ""}}, + {Name: "c", Value: "true", Tags: map[string]string{"test": "", "test_number": "true"}}, + } + if got, want := fmt.Sprint(fields), fmt.Sprint(wantedFields); got != want { + t.Errorf("Serialized fields unexpected: got: %v want: %v", got, want) + return + } +} + +type TestSerializeSliceA struct { + E int `validate:"test-slice"` +} + +func TestSliceSerialize(t *testing.T) { + body := struct { + A string `validate:"test"` + B []TestSerializeSliceA + }{"a-value", []TestSerializeSliceA{{10}, {}}} + + fields, err := Serialize(body) + if err != nil { + t.Error(err) + return + } + + if got, want := len(fields), 3; got != want { + t.Errorf("Length of serialized fields isn't equals: got: %v want: %v", got, want) + return + } + + wantedFields := []Field{ + {Name: "a", Value: "a-value", Tags: map[string]string{"test": ""}}, + {Name: "b[0].e", Value: "10", Tags: map[string]string{"test-slice": ""}}, + {Name: "b[1].e", Value: "0", Tags: map[string]string{"test-slice": ""}}, + } + if got, want := fmt.Sprint(fields), fmt.Sprint(wantedFields); got != want { + t.Errorf("Serialized fields unexpected: got: %v want: %v", got, want) + return + } +} + +type TestSerializeStructE struct { + a string `validate:"test-private-struct-field=300"` +} + +type TestSerializeStructD struct { + J string `validate:"test-struct"` + I TestSerializeStructE +} + +func TestStructSlice(t *testing.T) { + body := struct { + A string `validate:"test"` + B TestSerializeStructD + }{"a-value", TestSerializeStructD{J: "j-test-struct", I: TestSerializeStructE{a: "a-test-private-struct-field"}}} + + fields, err := Serialize(body) + if err != nil { + t.Error(err) + return + } + + wantedFields := []Field{ + {Name: "a", Value: "a-value", Tags: map[string]string{"test": ""}}, + {Name: "b.j", Value: "j-test-struct", Tags: map[string]string{"test-struct": ""}}, + {Name: "b.i.a", Value: "a-test-private-struct-field", Tags: map[string]string{"test-private-struct-field": "300"}}, + } + if got, want := fmt.Sprint(fields), fmt.Sprint(wantedFields); got != want { + t.Errorf("Serialized fields unexpected: got: %v want: %v", got, want) + return + } +} + +func TestRawSerializeWithEmptyTagName(t *testing.T) { + _, err := RawSerialize("", nil) + if err == nil { + t.Error("Unexpected nil value for error") + return + } + + if _, ok := err.(*ErrEmptyTagName); !ok { + t.Error("Unexpected error type, not equal *ErrEmptyTagName") + return + } +} + +func BenchmarkSerializeBodyStruct(b *testing.B) { + b.ResetTimer() + body := map[string]string{"test-key": "test-value"} + for n := 0; n < b.N; n++ { + Serialize(body) + } +} diff --git a/v2/tag.go b/v2/tag.go new file mode 100644 index 0000000..58f3f46 --- /dev/null +++ b/v2/tag.go @@ -0,0 +1,3 @@ +package gody + +const DefaultTagName = "validate" diff --git a/v2/validate.go b/v2/validate.go new file mode 100644 index 0000000..c806cbe --- /dev/null +++ b/v2/validate.go @@ -0,0 +1,51 @@ +package gody + +import "github.com/guiferpa/gody/v2/rule" + +func DefaultValidate(b interface{}, customRules []Rule) (bool, error) { + return RawDefaultValidate(b, DefaultTagName, customRules) +} + +// Validate contains the entrypoint to validation of struct input +func Validate(b interface{}, rules []Rule) (bool, error) { + return RawValidate(b, DefaultTagName, rules) +} + +func RawDefaultValidate(b interface{}, tn string, customRules []Rule) (bool, error) { + defaultRules := []Rule{ + rule.NotEmpty, + rule.Required, + rule.Enum, + rule.Max, + rule.Min, + rule.MaxBound, + rule.MinBound, + } + + return RawValidate(b, tn, append(defaultRules, customRules...)) +} + +func RawValidate(b interface{}, tn string, rules []Rule) (bool, error) { + fields, err := RawSerialize(tn, b) + if err != nil { + return false, err + } + + return ValidateFields(fields, rules) +} + +func ValidateFields(fields []Field, rules []Rule) (bool, error) { + for _, field := range fields { + for _, r := range rules { + val, ok := field.Tags[r.Name()] + if !ok { + continue + } + if ok, err := r.Validate(field.Name, field.Value, val); err != nil { + return ok, err + } + } + } + + return true, nil +} diff --git a/v2/validate_test.go b/v2/validate_test.go new file mode 100644 index 0000000..409eac8 --- /dev/null +++ b/v2/validate_test.go @@ -0,0 +1,107 @@ +package gody + +import ( + "reflect" + "testing" + + "github.com/guiferpa/gody/v2/rule/ruletest" +) + +type StructAForTest struct { + A string `validate:"fake"` + B string +} + +func TestValidateMatchRule(t *testing.T) { + payload := StructAForTest{A: "", B: "test-b"} + rule := ruletest.NewRule("fake", true, nil) + + validated, err := Validate(payload, []Rule{rule}) + if !validated { + t.Error("Validated result is not expected") + return + } + if err != nil { + t.Error("Error result from validate is not expected") + return + } + if !rule.ValidateCalled { + t.Error("The rule validate wasn't call") + return + } +} + +func TestValidateNoMatchRule(t *testing.T) { + payload := StructAForTest{A: "", B: "test-b"} + rule := ruletest.NewRule("mock", true, nil) + + validated, err := Validate(payload, []Rule{rule}) + if !validated { + t.Error("Validated result is not expected") + return + } + if err != nil { + t.Error("Error result from validate is not expected") + return + } + if rule.ValidateCalled { + t.Error("The rule validate was call") + return + } +} + +type StructBForTest struct { + C int `validate:"test"` + D bool +} + +type errStructBForValidation struct{} + +func (_ *errStructBForValidation) Error() string { + return "" +} + +func TestValidateWithRuleError(t *testing.T) { + payload := StructBForTest{C: 10} + rule := ruletest.NewRule("test", true, &errStructBForValidation{}) + + validated, err := Validate(payload, []Rule{rule}) + if !validated { + t.Error("Validated result is not expected") + return + } + if !rule.ValidateCalled { + t.Error("The rule validate was call") + return + } + if _, ok := err.(*errStructBForValidation); !ok { + t.Errorf("Unexpected error type: got: %v", err) + return + } +} + +func TestSetTagName(t *testing.T) { + validator := NewValidator() + if got, want := validator.tagName, DefaultTagName; got != want { + t.Errorf("Unexpected default tag value from validator struct type: got: %v, want: %v", got, want) + return + } + + newTag := "new-tag" + validator.SetTagName(newTag) + if got, want := validator.tagName, newTag; got != want { + t.Errorf("Unexpected default tag value from validator struct type: got: %v, want: %v", got, want) + return + } + + err := validator.SetTagName("") + if err == nil { + t.Errorf("Unexpected error as nil") + return + } + + if ce, ok := err.(*ErrEmptyTagName); !ok { + t.Errorf("Unexpected error type: got: %v, want: %v", reflect.TypeOf(ce), reflect.TypeOf(&ErrEmptyTagName{})) + return + } +} diff --git a/v2/validator.go b/v2/validator.go new file mode 100644 index 0000000..8569819 --- /dev/null +++ b/v2/validator.go @@ -0,0 +1,47 @@ +package gody + +import "fmt" + +type ErrDuplicatedRule struct { + RuleDuplicated Rule +} + +func (err *ErrDuplicatedRule) Error() string { + return fmt.Sprintf("rule %s is duplicated", err.RuleDuplicated.Name()) +} + +type Validator struct { + tagName string + rulesMap map[string]Rule + addedRules []Rule +} + +func (v *Validator) AddRules(rs ...Rule) error { + for _, r := range rs { + if dr, exists := v.rulesMap[r.Name()]; exists { + return &ErrDuplicatedRule{RuleDuplicated: dr} + } + v.rulesMap[r.Name()] = r + } + v.addedRules = append(v.addedRules, rs...) + return nil +} + +func (v *Validator) SetTagName(tn string) error { + if tn == "" { + return &ErrEmptyTagName{} + } + v.tagName = tn + return nil +} + +func (v *Validator) Validate(b interface{}) (bool, error) { + return RawDefaultValidate(b, v.tagName, v.addedRules) +} + +func NewValidator() *Validator { + tagName := DefaultTagName + rulesMap := make(map[string]Rule) + addedRules := make([]Rule, 0) + return &Validator{tagName, rulesMap, addedRules} +} diff --git a/v2/validator_test.go b/v2/validator_test.go new file mode 100644 index 0000000..664cdc4 --- /dev/null +++ b/v2/validator_test.go @@ -0,0 +1,55 @@ +package gody + +import ( + "testing" + + "github.com/guiferpa/gody/v2/rule/ruletest" +) + +func TestValidator(t *testing.T) { + payload := struct { + A int `validate:"test"` + }{10} + + validator := NewValidator() + + rule := ruletest.NewRule("test", true, nil) + if err := validator.AddRules(rule); err != nil { + t.Error("Unexpected error") + return + } + validated, err := validator.Validate(payload) + if !validated { + t.Error("Validated result is not expected") + return + } + if err != nil { + t.Error("Error result from validate is not expected") + return + } + if !rule.ValidateCalled { + t.Error("The rule validate wasn't call") + return + } +} + +func TestDuplicatedRule(t *testing.T) { + validator := NewValidator() + rule := ruletest.NewRule("a", true, nil) + rules := []Rule{ + rule, + ruletest.NewRule("b", true, nil), + ruletest.NewRule("c", true, nil), + rule, + } + err := validator.AddRules(rules...) + if err == nil { + t.Error("Unexpected nil value for duplicated rule error") + return + } + + if _, ok := err.(*ErrDuplicatedRule); !ok { + t.Errorf("Unexpected error type: got: %v", err) + return + } +}