├── .github └── workflows │ └── pipeline.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── examples ├── custom_rule.go ├── http_api.go └── simple.go ├── go.mod ├── rule.go ├── rule ├── enum.go ├── enum_test.go ├── is_bool.go ├── is_bool_test.go ├── max.go ├── max_bound.go ├── max_bound_test.go ├── max_test.go ├── min.go ├── min_bound.go ├── min_bound_test.go ├── min_test.go ├── not_empty.go ├── not_empty_test.go ├── required.go ├── required_test.go ├── rule.go └── ruletest │ └── rule.go ├── serialize.go ├── serialize_test.go ├── tag.go ├── validate.go ├── validate_test.go ├── validator.go └── validator_test.go /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Pipeline 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-24.04 10 | strategy: 11 | matrix: 12 | go: ['1.21', '1.22', '1.23'] 13 | name: Go ${{ matrix.go }} 14 | steps: 15 | - name: Checkout source code 16 | uses: actions/checkout@v2 17 | 18 | - name: Setup Go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: ${{ matrix.go }} 22 | 23 | - name: Install dependencies 24 | run: go mod download 25 | 26 | - name: Running test 27 | run: go test $(go list ./... | grep -v examples | grep -v ruletest) -race -covermode atomic -coverprofile=covprofile 28 | 29 | - name: Send coverage 30 | uses: shogo82148/actions-goveralls@v1 31 | with: 32 | path-to-profile: covprofile 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | tags 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Guilherme Paixão 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/sh 2 | GOPATH ?= $(shell go env GOPATH) 3 | PKGS = $(shell go list ./... | grep -v examples | grep -v ruletest) 4 | LINTER = $(GOPATH)/bin/golangci-lint 5 | ACT_BIN = $(GOPATH)/bin/act 6 | TPARSE_BIN = $(GOPATH)/bin/tparse 7 | 8 | help: 9 | @cat $(MAKEFILE_LIST) | docker run --rm --platform linux/amd64 -i xanders/make-help 10 | 11 | # Execute all meaningful jobs from Makefile to release the project's binary 12 | all: test lint 13 | 14 | # Run tests 15 | test: $(TPARSE_BIN) 16 | @go test $(PKGS) -v -json -race -buildvcs -cover -coverprofile=coverage.out | $(TPARSE_BIN) -pass 17 | 18 | # Run benchmarks of source code 19 | bench: 20 | @go test $(PKGS) -v -race -buildvcs -bench=. -benchmem -cpu=1,2,4,12 21 | 22 | # Run lint 23 | lint: $(LINTER) 24 | @$(LINTER) run ./... --timeout 10m 25 | 26 | $(LINTER): 27 | @echo "==> Installing linter..." 28 | @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/d302a302c93198df24e842a29f6ddebb5f4cb3dd/install.sh | sh -s -- -b ${GOPATH}/bin v1.60.3 29 | 30 | # This jobs is to simulate github ci environment for tests github action workflows 31 | act: $(ACT_BIN) 32 | $(ACT_BIN) --container-architecture linux/amd64 --platform ubuntu-latest=node:buster --rm 33 | 34 | $(ACT_BIN): 35 | @echo "==> Installing act..." 36 | @curl -sSfL https://raw.githubusercontent.com/nektos/act/38e43bd51f66493057857f6d743153c874a7178f/install.sh | sh -s -- -b ${GOPATH}/bin 37 | 38 | # It's a great job to take a look to source code coverage using a friendly view 39 | cover-html: test 40 | @go tool cover -html=coverage.out 41 | 42 | $(TPARSE_BIN): 43 | @echo "==> Installing tparse..." 44 | @go install github.com/mfridman/tparse@latest 45 | 46 | .PHONY: all build test bench lint act cover-html help 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gody 2 | 3 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) 4 | [![Last commit](https://img.shields.io/github/last-commit/guiferpa/gody)](https://img.shields.io/github/last-commit/guiferpa/gody) 5 | [![GoDoc](https://godoc.org/github.com/guiferpa/gody?status.svg)](https://godoc.org/github.com/guiferpa/gody) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/guiferpa/gody)](https://goreportcard.com/report/github.com/guiferpa/gody) 7 | ![Pipeline workflow](https://github.com/guiferpa/gody/actions/workflows/pipeline.yml/badge.svg) 8 | [![Coverage Status](https://coveralls.io/repos/github/guiferpa/gody/badge.svg?branch=main)](https://coveralls.io/github/guiferpa/gody?branch=main) 9 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/guiferpa/gody?color=purple&label=latest)](https://github.com/guiferpa/gody/releases/latest) 10 | 11 | ### [Go versions supported](https://github.com/guiferpa/gody/blob/main/.github/workflows/pipeline.yml#L12) 12 | 13 | ### Installation 14 | ```bash 15 | go get github.com/guiferpa/gody/v2 16 | ``` 17 | 18 | ### Usage 19 | 20 | ```go 21 | package main 22 | 23 | import ( 24 | "encoding/json" 25 | "fmt" 26 | "net/http" 27 | 28 | gody "github.com/guiferpa/gody/v2" 29 | "github.com/guiferpa/gody/v2/rule" 30 | ) 31 | 32 | type RequestBody struct { 33 | Name string `json:"name" validate:"not_empty"` 34 | Age int `json:"age" validate:"min=21"` 35 | } 36 | 37 | func HTTPHandler(v *gody.Validator) http.HandlerFunc { 38 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 39 | var body RequestBody 40 | if err := json.NewDecoder(r.Body).Decode(&body); err != nil { 41 | // ... 42 | } 43 | defer r.Body.Close() 44 | 45 | if isValidated, err := v.Validate(body); err != nil { 46 | // ... 47 | } 48 | }) 49 | } 50 | 51 | func main() { 52 | validator := gody.NewValidator() 53 | 54 | validator.AddRules(rule.NotEmpty, rule.Min) 55 | 56 | port := ":3000" 57 | http.ListenAndServe(port, HTTPHandler(validator)) 58 | } 59 | ``` 60 | 61 | ### Others ways for validation 62 | 63 | There are others ways to valid a struct, take a look on functions below: 64 | 65 | - **RawValidate** - It's a function that make validate with no rule, it's necessary put the struct for validation, some rule(s) and tag name. 66 | 67 | ```go 68 | gody.RawValidate(interface{}, string, []gody.Rule) (bool, error) 69 | ``` 70 | 71 | - **Validate** - It's a function that make validate with no rule, it's necessary put the struct for validation and some rule(s). 72 | ```go 73 | gody.Validate(interface{}, []gody.Rule) (bool, error) 74 | ``` 75 | 76 | - **RawDefaultValidate** - It's a function that already have [built-in rules](https://github.com/guiferpa/gody/blob/72ce1caecc5fdacf40ee282716ec1b5abe6f7adf/validate.go#L15-L23) configured, it's necessary put the struct for validation, tag name and optional custom rule(s). 77 | ```go 78 | gody.RawDefaultValidate(interface{}, string, []gody.Rule) (bool, error) 79 | ``` 80 | 81 | - **DefaultValidate** - It's a function that already have [built-in rules](https://github.com/guiferpa/gody/blob/72ce1caecc5fdacf40ee282716ec1b5abe6f7adf/validate.go#L15-L23) configured, it's necessary put the struct for validation and optional custom rule(s). 82 | ```go 83 | gody.DefaultValidate(interface{}, []gody.Rule) (bool, error) 84 | ``` 85 | 86 | ### Dynamic Enum Validation (No Duplication) 87 | 88 | You can avoid duplicating enum values in struct tags by using dynamic parameters: 89 | 90 | ```go 91 | const ( 92 | StatusCreated = "__CREATED__" 93 | StatusPending = "__PENDING__" 94 | StatusDoing = "__DOING__" 95 | StatusDone = "__DONE__" 96 | ) 97 | 98 | type Task struct { 99 | Name string `json:"name"` 100 | Status string `json:"status" validate:"enum={status}"` 101 | } 102 | 103 | validator := gody.NewValidator() 104 | validator.AddRuleParameters(map[string]string{ 105 | "status": fmt.Sprintf("%s,%s,%s,%s", StatusCreated, StatusPending, StatusDoing, StatusDone), 106 | }) 107 | validator.AddRules(rule.Enum) 108 | 109 | // Now you can validate Task structs without duplicating enum values in the tag! 110 | ``` 111 | 112 | ### Contribution policies 113 | 114 | 1. At this time the only policy is don't create a Pull Request directly, it's necessary some discussions for some implementation then open an issue before to dicussion anything about the project. 115 | -------------------------------------------------------------------------------- /examples/custom_rule.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | gody "github.com/guiferpa/gody/v2" 8 | "github.com/guiferpa/gody/v2/rule" 9 | ) 10 | 11 | // ErrInvalidPalindrome is a custom error to a specific rule implementation 12 | type ErrInvalidPalindrome struct { 13 | Value string 14 | } 15 | 16 | func (e *ErrInvalidPalindrome) Error() string { 17 | return fmt.Sprintf("invalid palindrome: %s", e.Value) 18 | } 19 | 20 | // PalindromeRule is a struct that implements the Rule interface 21 | type PalindromeRule struct{} 22 | 23 | // Name is a func from the Rule contract 24 | func (r *PalindromeRule) Name() string { 25 | return "palindrome" 26 | } 27 | 28 | // Validate is a func from the Rule contract 29 | func (r *PalindromeRule) Validate(f, v, p string) (bool, error) { 30 | // TODO: The algorithm for palindrome validation 31 | return true, &ErrInvalidPalindrome{Value: v} 32 | } 33 | 34 | func CustomRuleValidation() { 35 | b := struct { 36 | Text string `json:"text" validate:"min_bound=5"` 37 | Palindrome string `json:"palindrome" validate:"palindrome"` 38 | }{ 39 | Text: "test-text", 40 | Palindrome: "test-palindrome", 41 | } 42 | 43 | customRules := []gody.Rule{ 44 | &PalindromeRule{}, 45 | } 46 | 47 | valid, err := gody.DefaultValidate(b, customRules) 48 | if err != nil { 49 | if !valid { 50 | log.Println("body do not validated", err) 51 | } 52 | 53 | switch err.(type) { 54 | case *rule.ErrRequired: 55 | log.Println("required error:", err) 56 | 57 | case *ErrInvalidPalindrome: 58 | log.Println("palindrome error:", err) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/http_api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "strconv" 9 | 10 | gody "github.com/guiferpa/gody/v2" 11 | ) 12 | 13 | type ErrIsAdult struct{} 14 | 15 | func (err *ErrIsAdult) Error() string { 16 | return "The client isn't a adult then it isn't allowed buy" 17 | } 18 | 19 | type IsAdultRule struct { 20 | adultAge int 21 | } 22 | 23 | func (r *IsAdultRule) Name() string { 24 | return "is_adult" 25 | } 26 | 27 | func (r *IsAdultRule) Validate(_, value, _ string) (bool, error) { 28 | if value == "" { 29 | return true, &ErrIsAdult{} 30 | } 31 | 32 | clientAge, err := strconv.Atoi(value) 33 | if err != nil { 34 | return false, err 35 | } 36 | 37 | if clientAge < r.adultAge { 38 | return true, &ErrIsAdult{} 39 | } 40 | 41 | return true, nil 42 | } 43 | 44 | type User struct { 45 | Name string `validate:"min_bound=5"` 46 | Age int16 `validate:"min=10 is_adult"` 47 | } 48 | 49 | type Product struct { 50 | Name string `validate:"not_empty"` 51 | Description string `validate:"not_empty"` 52 | Price int 53 | } 54 | 55 | type Cart struct { 56 | Owner User 57 | Products []Product 58 | } 59 | 60 | func HTTPServerAPI() { 61 | validator := gody.NewValidator() 62 | 63 | if err := validator.AddRules(&IsAdultRule{adultAge: 21}); err != nil { 64 | log.Fatalln(err) 65 | } 66 | 67 | port := ":8092" 68 | 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) 69 | http.ListenAndServe(port, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 70 | if r.URL.Path != "/carts" || r.Method != http.MethodPost { 71 | w.WriteHeader(http.StatusNotFound) 72 | fmt.Fprintf(w, "Path or method is wrong: path: %v, method: %v\n", r.URL.Path, r.Method) 73 | return 74 | } 75 | 76 | var body Cart 77 | err := json.NewDecoder(r.Body).Decode(&body) 78 | defer r.Body.Close() 79 | if err != nil { 80 | w.WriteHeader(http.StatusBadRequest) 81 | fmt.Fprintln(w, err) 82 | return 83 | } 84 | 85 | if validated, err := validator.Validate(body); err != nil { 86 | if !validated { 87 | w.WriteHeader(http.StatusInternalServerError) 88 | fmt.Fprintf(w, "Validation for body wasn't processed because of error: %v\n", err) 89 | return 90 | } 91 | 92 | w.WriteHeader(http.StatusUnprocessableEntity) 93 | 94 | if _, ok := err.(*ErrIsAdult); ok { 95 | fmt.Fprintf(w, "The client called %v isn't a adult\n", body.Owner.Name) 96 | return 97 | } 98 | 99 | fmt.Fprintln(w, err) 100 | return 101 | } 102 | 103 | w.WriteHeader(http.StatusCreated) 104 | fmt.Fprintf(w, "The client %v created your cart!\n", body.Owner.Name) 105 | return 106 | })) 107 | } 108 | -------------------------------------------------------------------------------- /examples/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | gody "github.com/guiferpa/gody/v2" 7 | "github.com/guiferpa/gody/v2/rule" 8 | ) 9 | 10 | func SimpleDefaultValidation() { 11 | b := struct { 12 | Text string `json:"text" validate:"not_empty"` 13 | }{} 14 | 15 | valid, err := gody.DefaultValidate(b, nil) 16 | if err != nil { 17 | if !valid { 18 | log.Println("body do not validated:", err) 19 | } 20 | 21 | switch err.(type) { 22 | case *rule.ErrNotEmpty: 23 | log.Println("not empty error:", err) 24 | 25 | } 26 | } 27 | } 28 | 29 | func SimplePureValidation() { 30 | b := struct { 31 | Text string `json:"text" validate:"not_empty"` 32 | }{} 33 | 34 | rules := []gody.Rule{ 35 | rule.NotEmpty, 36 | } 37 | valid, err := gody.Validate(b, rules) 38 | if err != nil { 39 | if !valid { 40 | log.Println("body do not validated:", err) 41 | } 42 | 43 | switch err.(type) { 44 | case *rule.ErrNotEmpty: 45 | log.Println("not empty error:", err) 46 | 47 | } 48 | } 49 | } 50 | 51 | func SimpleValidationFromValidator() { 52 | b := struct { 53 | Text string `json:"text" validate:"not_empty"` 54 | }{} 55 | 56 | validator := gody.NewValidator() 57 | 58 | if err := validator.AddRules(rule.NotEmpty); err != nil { 59 | log.Println(err) 60 | return 61 | } 62 | 63 | valid, err := validator.Validate(b) 64 | if err != nil { 65 | if !valid { 66 | log.Println("body do not validated:", err) 67 | } 68 | 69 | switch err.(type) { 70 | case *rule.ErrNotEmpty: 71 | log.Println("not empty error:", err) 72 | 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/guiferpa/gody/v2 2 | 3 | go 1.23.5 4 | -------------------------------------------------------------------------------- /rule.go: -------------------------------------------------------------------------------- 1 | package gody 2 | 3 | // Rule is a interface with the contract to implement a any rule 4 | type Rule interface { 5 | Name() string 6 | Validate(name, value, param string) (bool, error) 7 | } 8 | -------------------------------------------------------------------------------- /rule/enum.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type enum struct{} 9 | 10 | func (r *enum) Name() string { 11 | return "enum" 12 | } 13 | 14 | // ErrEnum is the representation about any error happened inside of the rule Enum 15 | type ErrEnum struct { 16 | Field string 17 | Value string 18 | Enum []string 19 | } 20 | 21 | func (err *ErrEnum) Error() string { 22 | return fmt.Sprintf("the value %v in field %v not contains in %v", err.Value, err.Field, err.Enum) 23 | } 24 | 25 | func (r *enum) Validate(f, v, p string) (bool, error) { 26 | if v == "" { 27 | return true, nil 28 | } 29 | es := strings.Split(p, ",") 30 | for _, e := range es { 31 | if v == e { 32 | return true, nil 33 | } 34 | } 35 | return true, &ErrEnum{f, v, es} 36 | } 37 | -------------------------------------------------------------------------------- /rule/enum_test.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestEnumName(t *testing.T) { 8 | r := Enum 9 | if r.Name() != "enum" { 10 | t.Errorf("unexpected result, result: %v, expected: %v", r.Name(), "enum") 11 | } 12 | } 13 | 14 | func TestEnum(t *testing.T) { 15 | r := Enum 16 | cases := []struct { 17 | value, param string 18 | }{ 19 | {"a", "a,b,c,d"}, 20 | {"b", "a,b,c,d"}, 21 | {"c", "a,b,c,d"}, 22 | {"d", "a,b,c,d"}, 23 | {"", "a,b,c,d"}, 24 | } 25 | for _, test := range cases { 26 | ok, err := r.Validate("", test.value, test.param) 27 | if err != nil { 28 | t.Error(err) 29 | } 30 | if !ok { 31 | t.Errorf("unexpected result, result: %v, expected: %v", ok, true) 32 | } 33 | } 34 | } 35 | 36 | func TestEnumWithInvalidParams(t *testing.T) { 37 | r := Enum 38 | cases := []struct { 39 | value, param string 40 | }{ 41 | {"d", "a,b,c"}, 42 | {"1", "a,b,c"}, 43 | } 44 | for _, test := range cases { 45 | ok, err := r.Validate("", test.value, test.param) 46 | if _, isErrEnum := err.(*ErrEnum); !isErrEnum { 47 | t.Error(err) 48 | } 49 | if !ok { 50 | t.Errorf("unexpected result, result: %v, expected: %v", ok, true) 51 | } 52 | } 53 | } 54 | 55 | func TestEnumError(t *testing.T) { 56 | err := &ErrEnum{Field: "text", Value: "2", Enum: []string{"A", "B"}} 57 | got := err.Error() 58 | want := "the value 2 in field text not contains in [A B]" 59 | if got != want { 60 | t.Errorf(`&ErrEnum{Field: "text", Value: "2", Enum: []string{"A", "B"}}.Error(), got: %v, want: %v`, got, want) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /rule/is_bool.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | type isBool struct{} 9 | 10 | func (r *isBool) Name() string { 11 | return "is_bool" 12 | } 13 | 14 | // ErrIsBool is the representation about any error happened inside of the rule IsBool 15 | type ErrIsBool struct { 16 | Field string 17 | } 18 | 19 | func (err *ErrIsBool) Error() string { 20 | return fmt.Sprintf("field %v must be 'true' or 'false'", err.Field) 21 | } 22 | 23 | func (r *isBool) Validate(f, v, p string) (bool, error) { 24 | if _, err := strconv.ParseBool(v); err != nil { 25 | return true, &ErrIsBool{f} 26 | } 27 | return true, nil 28 | } 29 | -------------------------------------------------------------------------------- /rule/is_bool_test.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestIsBoolName(t *testing.T) { 9 | got := IsBool.Name() 10 | want := "is_bool" 11 | if got != want { 12 | t.Errorf("IsBool.Name(), got: %v, want: %v", got, want) 13 | } 14 | } 15 | 16 | func TestIsBoolWithSuccessful(t *testing.T) { 17 | valid, err := IsBool.Validate("text", "false", "") 18 | if got, want := valid, true; got != want { 19 | t.Errorf(`valid, _ := IsBool.Validate("text", "false", ""), got: %v, want: %v`, got, want) 20 | } 21 | if got, want := reflect.TypeOf(err), reflect.TypeOf(nil); got != want { 22 | t.Errorf(`_, err := IsBool.Validate("text", "false", ""), got: %v, want: %v`, got, want) 23 | } 24 | 25 | valid, err = IsBool.Validate("text", "true", "") 26 | if got, want := valid, true; got != want { 27 | t.Errorf(`valid, _ := IsBool.Validate("text", "true", ""), got: %v, want: %v`, got, want) 28 | } 29 | if got, want := reflect.TypeOf(err), reflect.TypeOf(nil); got != want { 30 | t.Errorf(`_, err := IsBool.Validate("text", "true", ""), got: %v, want: %v`, got, want) 31 | } 32 | } 33 | 34 | func TestIsBoolWithEmptyValue(t *testing.T) { 35 | valid, err := IsBool.Validate("text", "", "") 36 | if got, want := valid, true; got != want { 37 | t.Errorf(`valid, _ := IsBool.Validate("text", "", ""), got: %v, want: %v`, got, want) 38 | } 39 | if got, want := reflect.TypeOf(err), reflect.TypeOf(&ErrIsBool{}); got != want { 40 | t.Errorf(`_, err := IsBool.Validate("text", "", ""), got: %v, want: %v`, got, want) 41 | } 42 | } 43 | 44 | func TestIsBoolError(t *testing.T) { 45 | err := &ErrIsBool{Field: "text"} 46 | got := err.Error() 47 | want := "field text must be 'true' or 'false'" 48 | if got != want { 49 | t.Errorf(`&ErrIsBool{Field: "text"}.Error(), got: %v, want: %v`, got, want) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rule/max.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type max struct{} 10 | 11 | func (r *max) Name() string { 12 | return "max" 13 | } 14 | 15 | // ErrMax is the representation about any error happened inside of the rule Max 16 | type ErrMax struct { 17 | Field string 18 | Value int 19 | Max int 20 | } 21 | 22 | func (err *ErrMax) Error() string { 23 | return fmt.Sprintf("the value %v in field %v is grater than %v", err.Value, err.Field, err.Max) 24 | } 25 | 26 | func (r *max) Validate(f, v, p string) (bool, error) { 27 | n, err := strconv.Atoi(p) 28 | if err != nil { 29 | return false, err 30 | } 31 | vn, err := strconv.Atoi(v) 32 | if err != nil { 33 | return false, err 34 | } 35 | if vn > n { 36 | return true, &ErrMax{strings.ToLower(f), vn, n} 37 | } 38 | return true, nil 39 | } 40 | -------------------------------------------------------------------------------- /rule/max_bound.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type maxBound struct{} 10 | 11 | func (r *maxBound) Name() string { 12 | return "max_bound" 13 | } 14 | 15 | // ErrMaxBound is the representation about any error happened inside of the rule MaxBound 16 | type ErrMaxBound struct { 17 | Field string 18 | Value string 19 | Bound int 20 | } 21 | 22 | func (err *ErrMaxBound) Error() string { 23 | return fmt.Sprintf("the value %v in field %v has character limit greater than %v", err.Value, err.Field, err.Bound) 24 | } 25 | 26 | func (r *maxBound) Validate(f, v, p string) (bool, error) { 27 | n, err := strconv.Atoi(p) 28 | if err != nil { 29 | return false, err 30 | } 31 | if len(v) > n { 32 | return true, &ErrMaxBound{strings.ToLower(f), v, n} 33 | } 34 | return true, nil 35 | } 36 | -------------------------------------------------------------------------------- /rule/max_bound_test.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMaxBoundName(t *testing.T) { 8 | r := MaxBound 9 | if r.Name() != "max_bound" { 10 | t.Errorf("unexpected result, result: %v, expected: %v", r.Name(), "max_bound") 11 | } 12 | } 13 | 14 | func TestMaxBound(t *testing.T) { 15 | r := MaxBound 16 | cases := []struct { 17 | value, param string 18 | }{ 19 | {"", "0"}, 20 | {"", "1"}, 21 | {"a", "2"}, 22 | {"fla", "3"}, 23 | } 24 | for _, test := range cases { 25 | ok, err := r.Validate("", test.value, test.param) 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | if !ok { 30 | t.Errorf("unexpected result, result: %v, expected: %v", ok, true) 31 | } 32 | } 33 | } 34 | 35 | func TestMaxBoundWithoutLimit(t *testing.T) { 36 | r := MaxBound 37 | cases := []struct { 38 | value, param string 39 | }{ 40 | {"fla", "2"}, 41 | {"123", "2"}, 42 | {"1", "0"}, 43 | } 44 | for _, test := range cases { 45 | ok, err := r.Validate("", test.value, test.param) 46 | if _, ok := err.(*ErrMaxBound); !ok { 47 | t.Error(err) 48 | } 49 | if !ok { 50 | t.Errorf("unexpected result, result: %v, expected: %v", ok, true) 51 | } 52 | } 53 | } 54 | 55 | func TestMaxBoundWithInvalidParam(t *testing.T) { 56 | r := MaxBound 57 | cases := []struct { 58 | value, param string 59 | }{ 60 | {"1", "test"}, 61 | {"zico", "true"}, 62 | } 63 | for _, test := range cases { 64 | ok, err := r.Validate("", test.value, test.param) 65 | if err == nil { 66 | t.Error("unexpected no error") 67 | } 68 | if ok { 69 | t.Errorf("unexpected result as okay") 70 | } 71 | } 72 | } 73 | 74 | func TestMaxBoundError(t *testing.T) { 75 | err := &ErrMaxBound{Field: "text", Value: "2", Bound: 1} 76 | got := err.Error() 77 | want := "the value 2 in field text has character limit greater than 1" 78 | if got != want { 79 | t.Errorf(`&ErrMaxBound{Field: "text", Value: "2", Bound: 1}.Error(), got: %v, want: %v`, got, want) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /rule/max_test.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMaxName(t *testing.T) { 8 | r := Max 9 | if r.Name() != "max" { 10 | t.Errorf("unexpected result, result: %v, expected: %v", r.Name(), "max") 11 | } 12 | } 13 | 14 | func TestMax(t *testing.T) { 15 | r := Max 16 | cases := []struct { 17 | value, param string 18 | }{ 19 | {"2", "3"}, 20 | {"500", "2000"}, 21 | {"-1", "0"}, 22 | } 23 | for _, test := range cases { 24 | ok, err := r.Validate("", test.value, test.param) 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | if !ok { 29 | t.Errorf("unexpected result, result: %v, expected: %v", ok, true) 30 | } 31 | } 32 | } 33 | 34 | func TestMaxWithInvalidParam(t *testing.T) { 35 | r := Max 36 | cases := []struct { 37 | value, param string 38 | }{ 39 | {"2", "test"}, 40 | {"500", "true"}, 41 | } 42 | for _, test := range cases { 43 | ok, err := r.Validate("", test.value, test.param) 44 | if err == nil { 45 | t.Error("unexpected no error") 46 | } 47 | if ok { 48 | t.Errorf("unexpected validation result as okay") 49 | } 50 | } 51 | } 52 | 53 | func TestMaxWithInvalidValue(t *testing.T) { 54 | r := Max 55 | cases := []struct { 56 | value, param string 57 | }{ 58 | {"test", "2"}, 59 | {"true", "500"}, 60 | } 61 | for _, test := range cases { 62 | ok, err := r.Validate("", test.value, test.param) 63 | if err == nil { 64 | t.Error("unexpected no error") 65 | } 66 | if ok { 67 | t.Errorf("unexpected validation result as okay") 68 | } 69 | } 70 | } 71 | 72 | func TestMaxFailure(t *testing.T) { 73 | r := Max 74 | cases := []struct { 75 | value, param string 76 | }{ 77 | {"12", "3"}, 78 | {"5000", "2000"}, 79 | {"0", "-1"}, 80 | {"-20", "-22"}, 81 | } 82 | for _, test := range cases { 83 | _, err := r.Validate("", test.value, test.param) 84 | if _, ok := err.(*ErrMax); !ok { 85 | t.Errorf("unexpected error: %v", err) 86 | } 87 | } 88 | } 89 | 90 | func TestMaxError(t *testing.T) { 91 | err := &ErrMax{Field: "text", Value: 3, Max: 1} 92 | got := err.Error() 93 | want := "the value 3 in field text is grater than 1" 94 | if got != want { 95 | t.Errorf(`&ErrMax{Field: "text", Value: 3, Max: 1}.Error(), got: %v, want: %v`, got, want) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /rule/min.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type min struct{} 10 | 11 | func (r *min) Name() string { 12 | return "min" 13 | } 14 | 15 | // ErrMin is the representation about any error happened inside of the rule Min 16 | type ErrMin struct { 17 | Field string 18 | Value int 19 | Min int 20 | } 21 | 22 | func (err *ErrMin) Error() string { 23 | return fmt.Sprintf("the value %v in field %v is less than %v", err.Value, err.Field, err.Min) 24 | } 25 | 26 | func (r *min) Validate(f, v, p string) (bool, error) { 27 | n, err := strconv.Atoi(p) 28 | if err != nil { 29 | return false, err 30 | } 31 | vn, err := strconv.Atoi(v) 32 | if err != nil { 33 | return false, err 34 | } 35 | if vn < n { 36 | return true, &ErrMin{strings.ToLower(f), vn, n} 37 | } 38 | return true, nil 39 | } 40 | -------------------------------------------------------------------------------- /rule/min_bound.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type minBound struct{} 10 | 11 | func (r *minBound) Name() string { 12 | return "min_bound" 13 | } 14 | 15 | // ErrMinBound is the representation about any error happened inside of the rule MinBound 16 | type ErrMinBound struct { 17 | Field string 18 | Value string 19 | Bound int 20 | } 21 | 22 | func (err *ErrMinBound) Error() string { 23 | return fmt.Sprintf("the value %v in field %v has character limit less than %v", err.Value, err.Field, err.Bound) 24 | } 25 | 26 | func (r *minBound) Validate(f, v, p string) (bool, error) { 27 | n, err := strconv.Atoi(p) 28 | if err != nil { 29 | return false, err 30 | } 31 | if len(v) < n { 32 | return true, &ErrMinBound{strings.ToLower(f), v, n} 33 | } 34 | return true, nil 35 | } 36 | -------------------------------------------------------------------------------- /rule/min_bound_test.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMinBoundName(t *testing.T) { 8 | r := MinBound 9 | if r.Name() != "min_bound" { 10 | t.Errorf("unexpected result, result: %v, expected: %v", r.Name(), "min_bound") 11 | } 12 | } 13 | 14 | func TestMinBound(t *testing.T) { 15 | r := MinBound 16 | cases := []struct { 17 | value, param string 18 | }{ 19 | {"", "0"}, 20 | {"a", "1"}, 21 | {"aaa", "2"}, 22 | } 23 | for _, test := range cases { 24 | ok, err := r.Validate("", test.value, test.param) 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | if !ok { 29 | t.Errorf("unexpected result, result: %v, expected: %v", ok, true) 30 | } 31 | } 32 | } 33 | 34 | func TestMinBoundWithoutLimit(t *testing.T) { 35 | r := MinBound 36 | cases := []struct { 37 | value, param string 38 | }{ 39 | {"fl", "3"}, 40 | {"123", "4"}, 41 | } 42 | for _, test := range cases { 43 | ok, err := r.Validate("", test.value, test.param) 44 | if _, ok := err.(*ErrMinBound); !ok { 45 | t.Error(err) 46 | } 47 | if !ok { 48 | t.Errorf("unexpected result, result: %v, expected: %v", ok, true) 49 | } 50 | } 51 | } 52 | 53 | func TestMinBoundWithInvalidParam(t *testing.T) { 54 | r := MinBound 55 | cases := []struct { 56 | value, param string 57 | }{ 58 | {"2", "test"}, 59 | {"500", "true"}, 60 | } 61 | for _, test := range cases { 62 | ok, err := r.Validate("", test.value, test.param) 63 | if err == nil { 64 | t.Error("unexpected no error") 65 | } 66 | if ok { 67 | t.Errorf("unexpected validation result as okay") 68 | } 69 | } 70 | } 71 | 72 | func TestMinBoundError(t *testing.T) { 73 | err := &ErrMinBound{Field: "text", Value: "2", Bound: 1} 74 | got := err.Error() 75 | want := "the value 2 in field text has character limit less than 1" 76 | if got != want { 77 | t.Errorf(`&ErrMinBound{Field: "text", Value: "2", Bound: 1}.Error(), got: %v, want: %v`, got, want) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /rule/min_test.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMinName(t *testing.T) { 8 | r := Min 9 | if r.Name() != "min" { 10 | t.Errorf("unexpected result, result: %v, expected: %v", r.Name(), "min") 11 | } 12 | } 13 | 14 | func TestMin(t *testing.T) { 15 | r := Min 16 | cases := []struct { 17 | value, param string 18 | }{ 19 | {"3", "2"}, 20 | {"2000", "500"}, 21 | {"0", "-1"}, 22 | } 23 | for _, test := range cases { 24 | ok, err := r.Validate("", test.value, test.param) 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | if !ok { 29 | t.Errorf("unexpected result, result: %v, expected: %v", ok, true) 30 | } 31 | } 32 | } 33 | 34 | func TestMinWithInvalidParam(t *testing.T) { 35 | r := Min 36 | cases := []struct { 37 | value, param string 38 | }{ 39 | {"2", "test"}, 40 | {"500", "true"}, 41 | } 42 | for _, test := range cases { 43 | ok, err := r.Validate("", test.value, test.param) 44 | if err == nil { 45 | t.Error("unexpected no error") 46 | } 47 | if ok { 48 | t.Errorf("unexpected validation result as okay") 49 | } 50 | } 51 | } 52 | 53 | func TestMinWithInvalidValue(t *testing.T) { 54 | r := Min 55 | cases := []struct { 56 | value, param string 57 | }{ 58 | {"test", "2"}, 59 | {"true", "500"}, 60 | } 61 | for _, test := range cases { 62 | ok, err := r.Validate("", test.value, test.param) 63 | if err == nil { 64 | t.Error("unexpected no error") 65 | } 66 | if ok { 67 | t.Errorf("unexpected validation result as okay") 68 | } 69 | } 70 | 71 | } 72 | 73 | func TestMinFailure(t *testing.T) { 74 | r := Min 75 | cases := []struct { 76 | value, param string 77 | }{ 78 | {"12", "30"}, 79 | {"5000", "20000"}, 80 | {"-1", "0"}, 81 | {"-22", "-20"}, 82 | } 83 | for _, test := range cases { 84 | _, err := r.Validate("", test.value, test.param) 85 | if _, ok := err.(*ErrMin); !ok { 86 | t.Errorf("unexpected error: %v", err) 87 | } 88 | } 89 | } 90 | 91 | func TestMinError(t *testing.T) { 92 | err := &ErrMin{Field: "text", Value: 1, Min: 3} 93 | got := err.Error() 94 | want := "the value 1 in field text is less than 3" 95 | if got != want { 96 | t.Errorf(`&ErrMin{Field: "text", Value: 1, Min: 3}.Error(), got: %v, want: %v`, got, want) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /rule/not_empty.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type notEmpty struct{} 9 | 10 | func (_ *notEmpty) Name() string { 11 | return "not_empty" 12 | } 13 | 14 | // ErrNotEmpty is the representation about any error happened inside of the rule NotEmpty 15 | type ErrNotEmpty struct { 16 | Field string 17 | } 18 | 19 | func (e *ErrNotEmpty) Error() string { 20 | return fmt.Sprintf("field %v cannot be empty", e.Field) 21 | } 22 | 23 | func (r *notEmpty) Validate(f, v, _ string) (bool, error) { 24 | if v == "" { 25 | return true, &ErrNotEmpty{strings.ToLower(f)} 26 | } 27 | return true, nil 28 | } 29 | -------------------------------------------------------------------------------- /rule/not_empty_test.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestNotEmptyName(t *testing.T) { 9 | got := NotEmpty.Name() 10 | want := "not_empty" 11 | if got != want { 12 | t.Errorf("NotEmpty.Name(), got: %v, want: %v", got, want) 13 | } 14 | } 15 | 16 | func TestNotEmptyWithSuccessful(t *testing.T) { 17 | valid, err := NotEmpty.Validate("text", "test", "") 18 | if got, want := valid, true; got != want { 19 | t.Errorf(`valid, _ := NotEmpty.Validate("text", "test", ""), got: %v, want: %v`, got, want) 20 | } 21 | if got, want := reflect.TypeOf(err), reflect.TypeOf(nil); got != want { 22 | t.Errorf(`_, err := NotEmpty.Validate("text", "test", ""), got: %v, want: %v`, got, want) 23 | } 24 | } 25 | 26 | func TestNotEmptyWithEmptyValue(t *testing.T) { 27 | valid, err := NotEmpty.Validate("text", "", "") 28 | if got, want := valid, true; got != want { 29 | t.Errorf(`valid, _ := NotEmpty.Validate("text", "", ""), got: %v, want: %v`, got, want) 30 | } 31 | if got, want := reflect.TypeOf(err), reflect.TypeOf(&ErrNotEmpty{}); got != want { 32 | t.Errorf(`_, err := NotEmpty.Validate("text", "", ""), got: %v, want: %v`, got, want) 33 | } 34 | } 35 | 36 | func TestNotEmptyError(t *testing.T) { 37 | err := &ErrNotEmpty{Field: "text"} 38 | got := err.Error() 39 | want := "field text cannot be empty" 40 | if got != want { 41 | t.Errorf(`&ErrNotEmpty{Field: "text"}.Error(), got: %v, want: %v`, got, want) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /rule/required.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type required struct{} 11 | 12 | func (r *required) Name() string { 13 | return "required" 14 | } 15 | 16 | // ErrRequired is the representation about any error happened inside of the rule Required 17 | type ErrRequired struct { 18 | Field string 19 | } 20 | 21 | func (e *ErrRequired) Error() string { 22 | return fmt.Sprintf("%v is required", e.Field) 23 | } 24 | 25 | func (r *required) Validate(f, v, p string) (bool, error) { 26 | b, err := strconv.ParseBool(p) 27 | if err != nil { 28 | return false, err 29 | } 30 | log.Printf("[guiferpa/gody] :: ATTETION :: required rule is deprecated, please replace to use not_empty.") 31 | if b && v != "" { 32 | return true, nil 33 | } 34 | return true, &ErrRequired{strings.ToLower(f)} 35 | } 36 | -------------------------------------------------------------------------------- /rule/required_test.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRequiredName(t *testing.T) { 8 | r := Required 9 | if r.Name() != "required" { 10 | t.Errorf("unexpected result, result: %v, expected: %v", r.Name(), "required") 11 | } 12 | } 13 | 14 | func TestRequired(t *testing.T) { 15 | r := Required 16 | ok, err := r.Validate("", "test", "true") 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | if !ok { 21 | t.Errorf("unexpected result, result: %v, expected: %v", ok, true) 22 | } 23 | } 24 | 25 | func TestRequiredWithEmptyValue(t *testing.T) { 26 | r := Required 27 | ok, err := r.Validate("", "", "true") 28 | if _, ok := err.(*ErrRequired); !ok { 29 | t.Error(err) 30 | } 31 | if !ok { 32 | t.Errorf("unexpected result, result: %v, expected: %v", ok, true) 33 | } 34 | } 35 | 36 | func TestRequiredWithInvalidParam(t *testing.T) { 37 | r := Required 38 | ok, err := r.Validate("", "", "axl-rose&slash") 39 | if err == nil { 40 | t.Errorf("unexpected error as result") 41 | } 42 | if ok { 43 | t.Errorf("unexpected result, result: %v, expected: %v", ok, false) 44 | } 45 | } 46 | 47 | func TestRequiredError(t *testing.T) { 48 | err := &ErrRequired{Field: "text"} 49 | got := err.Error() 50 | want := "text is required" 51 | if got != want { 52 | t.Errorf(`&ErrRequired{Field: "text"}.Error(), got: %v, want: %v`, got, want) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rule/rule.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | var ( 4 | // NotEmpty is a rule implemented 5 | NotEmpty = ¬Empty{} 6 | 7 | // Required is a rule implemented 8 | Required = &required{} 9 | 10 | // Max is a rule implemented 11 | Max = &max{} 12 | 13 | // Min is a rule implemented 14 | Min = &min{} 15 | 16 | // Enum is a rule implemented 17 | Enum = &enum{} 18 | 19 | // MaxBound is a rule implemented 20 | MaxBound = &maxBound{} 21 | 22 | // MinBound is a rule implemented 23 | MinBound = &minBound{} 24 | 25 | // IsBool is a rule implemented 26 | IsBool = &isBool{} 27 | ) 28 | -------------------------------------------------------------------------------- /rule/ruletest/rule.go: -------------------------------------------------------------------------------- 1 | package ruletest 2 | 3 | type Rule struct { 4 | name string 5 | validated bool 6 | err error 7 | ValidateCalled bool 8 | } 9 | 10 | func (r *Rule) Name() string { 11 | return r.name 12 | } 13 | 14 | func (r *Rule) Validate(name, value, param string) (bool, error) { 15 | r.ValidateCalled = true 16 | return r.validated, r.err 17 | } 18 | 19 | func NewRule(name string, validated bool, err error) *Rule { 20 | return &Rule{name, validated, err, false} 21 | } 22 | -------------------------------------------------------------------------------- /serialize.go: -------------------------------------------------------------------------------- 1 | package gody 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | // ErrInvalidBody represents all invalid body report 10 | type ErrInvalidBody struct { 11 | Kind reflect.Kind 12 | } 13 | 14 | func (e *ErrInvalidBody) Error() string { 15 | return fmt.Sprintln("invalid body:", e.Kind) 16 | } 17 | 18 | // ErrInvalidTag represents all invalid tag report 19 | type ErrInvalidTag struct { 20 | Format string 21 | } 22 | 23 | func (e *ErrInvalidTag) Error() string { 24 | return fmt.Sprintln("invalid tag:", e.Format) 25 | } 26 | 27 | type ErrEmptyTagName struct{} 28 | 29 | func (e *ErrEmptyTagName) Error() string { 30 | return "tag name is empty" 31 | } 32 | 33 | // Field is a struct to represents the domain about a field inner gody lib 34 | type Field struct { 35 | Name string 36 | Value string 37 | Tags map[string]string 38 | } 39 | 40 | func Serialize(b any) ([]Field, error) { 41 | return RawSerialize(DefaultTagName, b) 42 | } 43 | 44 | // RawSerialize is a func to serialize/parse all content about the struct input 45 | func RawSerialize(tn string, b any) ([]Field, error) { 46 | if tn == "" { 47 | return nil, &ErrEmptyTagName{} 48 | } 49 | 50 | if b == nil { 51 | return nil, &ErrInvalidBody{} 52 | } 53 | 54 | valueOf := reflect.ValueOf(b) 55 | typeOf := reflect.TypeOf(b) 56 | 57 | if kindOfBody := typeOf.Kind(); kindOfBody != reflect.Struct { 58 | return nil, &ErrInvalidBody{Kind: kindOfBody} 59 | } 60 | 61 | fields := make([]Field, 0) 62 | for i := 0; i < typeOf.NumField(); i++ { 63 | field := typeOf.Field(i) 64 | tagString := field.Tag.Get(tn) 65 | if tagString == "" && field.Type.Kind() != reflect.Slice && field.Type.Kind() != reflect.Struct { 66 | continue 67 | } 68 | 69 | tagFormats := strings.Fields(tagString) 70 | tags := make(map[string]string) 71 | for _, tagFormat := range tagFormats { 72 | tagFormatSplitted := strings.Split(tagFormat, "=") 73 | 74 | if len(tagFormatSplitted) == 2 { 75 | tagFormatRule := tagFormatSplitted[0] 76 | tagFormatValue := tagFormatSplitted[1] 77 | if tagFormatValue == "" { 78 | return nil, &ErrInvalidTag{Format: tagFormat} 79 | } 80 | 81 | tags[tagFormatRule] = tagFormatValue 82 | continue 83 | } 84 | 85 | if len(tagFormatSplitted) == 1 { 86 | tagFormatRule := tagFormatSplitted[0] 87 | 88 | tags[tagFormatRule] = "" 89 | continue 90 | } 91 | 92 | return nil, &ErrInvalidTag{Format: tagFormat} 93 | } 94 | 95 | fieldValue := valueOf.FieldByName(field.Name) 96 | fieldNameToLower := strings.ToLower(field.Name) 97 | if fieldNameFromJSONTag := field.Tag.Get("json"); fieldNameFromJSONTag != "" { 98 | fieldNameToLower = fieldNameFromJSONTag 99 | } 100 | if kindOfField := field.Type.Kind(); kindOfField == reflect.Struct { 101 | if fieldConverted := fieldValue.Convert(fieldValue.Type()); fieldConverted.CanInterface() { 102 | payload := fieldConverted.Interface() 103 | serialized, err := RawSerialize(tn, payload) 104 | if err != nil { 105 | return nil, err 106 | } 107 | for _, item := range serialized { 108 | fields = append(fields, Field{ 109 | Name: fmt.Sprintf("%s.%s", fieldNameToLower, item.Name), 110 | Value: item.Value, 111 | Tags: item.Tags, 112 | }) 113 | } 114 | } 115 | } else if kindOfField := field.Type.Kind(); kindOfField == reflect.Slice { 116 | j := fieldValue.Len() 117 | for i := 0; i < j; i++ { 118 | sliceFieldValue := fieldValue.Index(i) 119 | if sliceFieldConverted := sliceFieldValue.Convert(sliceFieldValue.Type()); sliceFieldConverted.CanInterface() { 120 | payload := sliceFieldValue.Convert(sliceFieldValue.Type()).Interface() 121 | serialized, err := RawSerialize(tn, payload) 122 | if err != nil { 123 | return nil, err 124 | } 125 | for _, item := range serialized { 126 | fields = append(fields, Field{ 127 | Name: fmt.Sprintf("%s[%v].%s", fieldNameToLower, i, item.Name), 128 | Value: item.Value, 129 | Tags: item.Tags, 130 | }) 131 | } 132 | } 133 | } 134 | } else { 135 | fieldValueString := fmt.Sprintf("%v", fieldValue) 136 | fields = append(fields, Field{ 137 | Name: fieldNameToLower, 138 | Value: fieldValueString, 139 | Tags: tags, 140 | }) 141 | } 142 | } 143 | 144 | return fields, nil 145 | } 146 | -------------------------------------------------------------------------------- /serialize_test.go: -------------------------------------------------------------------------------- 1 | package gody 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type TestSerializeStructA struct { 9 | a string 10 | } 11 | 12 | type TestSerializeStructB struct { 13 | b string 14 | a TestSerializeStructA 15 | } 16 | 17 | type TestSerializeStructC struct { 18 | c string 19 | B TestSerializeStructB 20 | A TestSerializeStructA 21 | } 22 | 23 | func TestSerializeBodyStruct(t *testing.T) { 24 | cases := []struct { 25 | param any 26 | ok bool 27 | }{ 28 | {map[string]string{"test-key": "test-value"}, false}, 29 | {TestSerializeStructA{a: "a"}, true}, 30 | {TestSerializeStructB{b: "b", a: TestSerializeStructA{a: "a"}}, true}, 31 | {TestSerializeStructC{c: "c", B: TestSerializeStructB{b: "b", a: TestSerializeStructA{a: "a"}}, A: TestSerializeStructA{a: "a"}}, true}, 32 | {10, false}, 33 | {struct{}{}, true}, 34 | {"", false}, 35 | {nil, false}, 36 | } 37 | 38 | for _, c := range cases { 39 | _, err := Serialize(c.param) 40 | if _, ok := err.(*ErrInvalidBody); ok == c.ok { 41 | t.Error(err) 42 | } 43 | } 44 | } 45 | 46 | func TestSerializeBodyTagFormat(t *testing.T) { 47 | cases := []struct { 48 | param any 49 | ok bool 50 | }{ 51 | {struct { 52 | Value string `validate:"required"` 53 | }{"test-value"}, true}, 54 | {struct { 55 | Value string `validate:"required=true"` 56 | }{"test-value"}, true}, 57 | {struct { 58 | Value string `validate:"required="` 59 | }{"test-value"}, false}, 60 | {struct { 61 | Value string `validate:"=required="` 62 | }{"test-value"}, false}, 63 | {struct { 64 | Value string `validate:"="` 65 | }{"test-value"}, false}, 66 | {struct { 67 | Value string 68 | }{"test-value"}, true}, 69 | } 70 | 71 | for _, c := range cases { 72 | _, err := Serialize(c.param) 73 | if _, ok := err.(*ErrInvalidTag); ok == c.ok { 74 | t.Error(err) 75 | } 76 | } 77 | } 78 | 79 | func TestSerialize(t *testing.T) { 80 | body := struct { 81 | A string `validate:"test"` 82 | B int `json:"b"` 83 | C bool `validate:"test test_number=true"` 84 | }{"a-value", 10, true} 85 | 86 | fields, err := Serialize(body) 87 | if err != nil { 88 | t.Error(err) 89 | return 90 | } 91 | 92 | if got, want := len(fields), 2; got != want { 93 | t.Errorf("Length of serialized fields isn't equals: got: %v want: %v", got, want) 94 | return 95 | } 96 | 97 | wantedFields := []Field{ 98 | {Name: "a", Value: "a-value", Tags: map[string]string{"test": ""}}, 99 | {Name: "c", Value: "true", Tags: map[string]string{"test": "", "test_number": "true"}}, 100 | } 101 | if got, want := fmt.Sprint(fields), fmt.Sprint(wantedFields); got != want { 102 | t.Errorf("Serialized fields unexpected: got: %v want: %v", got, want) 103 | return 104 | } 105 | } 106 | 107 | type TestSerializeSliceA struct { 108 | E int `validate:"test-slice"` 109 | } 110 | 111 | func TestSliceSerialize(t *testing.T) { 112 | body := struct { 113 | A string `validate:"test"` 114 | B []TestSerializeSliceA 115 | }{"a-value", []TestSerializeSliceA{{10}, {}}} 116 | 117 | fields, err := Serialize(body) 118 | if err != nil { 119 | t.Error(err) 120 | return 121 | } 122 | 123 | if got, want := len(fields), 3; got != want { 124 | t.Errorf("Length of serialized fields isn't equals: got: %v want: %v", got, want) 125 | return 126 | } 127 | 128 | wantedFields := []Field{ 129 | {Name: "a", Value: "a-value", Tags: map[string]string{"test": ""}}, 130 | {Name: "b[0].e", Value: "10", Tags: map[string]string{"test-slice": ""}}, 131 | {Name: "b[1].e", Value: "0", Tags: map[string]string{"test-slice": ""}}, 132 | } 133 | if got, want := fmt.Sprint(fields), fmt.Sprint(wantedFields); got != want { 134 | t.Errorf("Serialized fields unexpected: got: %v want: %v", got, want) 135 | return 136 | } 137 | } 138 | 139 | type TestSerializeStructE struct { 140 | a string `validate:"test-private-struct-field=300"` 141 | } 142 | 143 | type TestSerializeStructD struct { 144 | J string `validate:"test-struct"` 145 | I TestSerializeStructE 146 | } 147 | 148 | func TestStructSlice(t *testing.T) { 149 | body := struct { 150 | A string `validate:"test"` 151 | B TestSerializeStructD 152 | }{"a-value", TestSerializeStructD{J: "j-test-struct", I: TestSerializeStructE{a: "a-test-private-struct-field"}}} 153 | 154 | fields, err := Serialize(body) 155 | if err != nil { 156 | t.Error(err) 157 | return 158 | } 159 | 160 | wantedFields := []Field{ 161 | {Name: "a", Value: "a-value", Tags: map[string]string{"test": ""}}, 162 | {Name: "b.j", Value: "j-test-struct", Tags: map[string]string{"test-struct": ""}}, 163 | {Name: "b.i.a", Value: "a-test-private-struct-field", Tags: map[string]string{"test-private-struct-field": "300"}}, 164 | } 165 | if got, want := fmt.Sprint(fields), fmt.Sprint(wantedFields); got != want { 166 | t.Errorf("Serialized fields unexpected: got: %v want: %v", got, want) 167 | return 168 | } 169 | } 170 | 171 | func TestRawSerializeWithEmptyTagName(t *testing.T) { 172 | _, err := RawSerialize("", nil) 173 | if err == nil { 174 | t.Error("Unexpected nil value for error") 175 | return 176 | } 177 | 178 | if _, ok := err.(*ErrEmptyTagName); !ok { 179 | t.Error("Unexpected error type, not equal *ErrEmptyTagName") 180 | return 181 | } 182 | } 183 | 184 | func TestRawSerializeWithJSONTagName(t *testing.T) { 185 | body := struct { 186 | A string `json:"b" validate:"not_empty"` 187 | }{} 188 | 189 | fields, err := RawSerialize("validate", body) 190 | if err != nil { 191 | t.Error(err) 192 | return 193 | } 194 | 195 | field := fields[0] 196 | 197 | if got, want := field.Name, "b"; got != want { 198 | t.Errorf("Unexpected field name, got: %s, want: %s", got, want) 199 | return 200 | } 201 | } 202 | 203 | func BenchmarkSerializeBodyStruct(b *testing.B) { 204 | b.ResetTimer() 205 | body := map[string]string{"test-key": "test-value"} 206 | for n := 0; n < b.N; n++ { 207 | _, _ = Serialize(body) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /tag.go: -------------------------------------------------------------------------------- 1 | package gody 2 | 3 | const DefaultTagName = "validate" 4 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | package gody 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/guiferpa/gody/v2/rule" 7 | ) 8 | 9 | func DefaultValidate(b any, customRules []Rule) (bool, error) { 10 | return RawDefaultValidate(b, DefaultTagName, customRules) 11 | } 12 | 13 | // Validate contains the entrypoint to validation of struct input 14 | func Validate(b any, rules []Rule) (bool, error) { 15 | return RawValidate(b, DefaultTagName, rules) 16 | } 17 | 18 | func RawDefaultValidate(b any, tn string, customRules []Rule) (bool, error) { 19 | defaultRules := []Rule{ 20 | rule.NotEmpty, 21 | rule.Required, 22 | rule.Enum, 23 | rule.Max, 24 | rule.Min, 25 | rule.MaxBound, 26 | rule.MinBound, 27 | rule.IsBool, 28 | } 29 | 30 | return RawValidate(b, tn, append(defaultRules, customRules...)) 31 | } 32 | 33 | func RawValidate(b any, tn string, rules []Rule) (bool, error) { 34 | fields, err := RawSerialize(tn, b) 35 | if err != nil { 36 | return false, err 37 | } 38 | 39 | return ValidateFields(fields, rules) 40 | } 41 | 42 | func ValidateFields(fields []Field, rules []Rule) (bool, error) { 43 | for _, field := range fields { 44 | for _, r := range rules { 45 | val, ok := field.Tags[r.Name()] 46 | if !ok { 47 | continue 48 | } 49 | if ok, err := r.Validate(field.Name, field.Value, val); err != nil { 50 | return ok, err 51 | } 52 | } 53 | } 54 | 55 | return true, nil 56 | } 57 | 58 | func RawDefaultValidateWithParams(b any, tn string, customRules []Rule, params map[string]string) (bool, error) { 59 | defaultRules := []Rule{ 60 | rule.NotEmpty, 61 | rule.Required, 62 | rule.Enum, 63 | rule.Max, 64 | rule.Min, 65 | rule.MaxBound, 66 | rule.MinBound, 67 | rule.IsBool, 68 | } 69 | return RawValidateWithParams(b, tn, append(defaultRules, customRules...), params) 70 | } 71 | 72 | func RawValidateWithParams(b any, tn string, rules []Rule, params map[string]string) (bool, error) { 73 | fields, err := RawSerialize(tn, b) 74 | if err != nil { 75 | return false, err 76 | } 77 | return ValidateFieldsWithParams(fields, rules, params) 78 | } 79 | 80 | func ValidateFieldsWithParams(fields []Field, rules []Rule, params map[string]string) (bool, error) { 81 | for _, field := range fields { 82 | for _, r := range rules { 83 | val, ok := field.Tags[r.Name()] 84 | if !ok { 85 | continue 86 | } 87 | 88 | val = substituteParams(val, params) 89 | if ok, err := r.Validate(field.Name, field.Value, val); err != nil { 90 | return ok, err 91 | } 92 | } 93 | } 94 | return true, nil 95 | } 96 | 97 | func substituteParams(s string, params map[string]string) string { 98 | for k, v := range params { 99 | needle := "{" + k + "}" 100 | s = strings.ReplaceAll(s, needle, v) 101 | } 102 | return s 103 | } 104 | -------------------------------------------------------------------------------- /validate_test.go: -------------------------------------------------------------------------------- 1 | package gody 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/guiferpa/gody/v2/rule" 8 | "github.com/guiferpa/gody/v2/rule/ruletest" 9 | ) 10 | 11 | type StructAForTest struct { 12 | A string `validate:"fake"` 13 | B string 14 | } 15 | 16 | func TestValidateMatchRule(t *testing.T) { 17 | payload := StructAForTest{A: "", B: "test-b"} 18 | rule := ruletest.NewRule("fake", true, nil) 19 | 20 | validated, err := Validate(payload, []Rule{rule}) 21 | if !validated { 22 | t.Error("Validated result is not expected") 23 | return 24 | } 25 | if err != nil { 26 | t.Error("Error result from validate is not expected") 27 | return 28 | } 29 | if !rule.ValidateCalled { 30 | t.Error("The rule validate wasn't call") 31 | return 32 | } 33 | } 34 | 35 | func TestValidateNoMatchRule(t *testing.T) { 36 | payload := StructAForTest{A: "", B: "test-b"} 37 | rule := ruletest.NewRule("mock", true, nil) 38 | 39 | validated, err := Validate(payload, []Rule{rule}) 40 | if !validated { 41 | t.Error("Validated result is not expected") 42 | return 43 | } 44 | if err != nil { 45 | t.Error("Error result from validate is not expected") 46 | return 47 | } 48 | if rule.ValidateCalled { 49 | t.Error("The rule validate was call") 50 | return 51 | } 52 | } 53 | 54 | type StructBForTest struct { 55 | C int `validate:"test"` 56 | D bool 57 | } 58 | 59 | type errStructBForValidation struct{} 60 | 61 | func (_ *errStructBForValidation) Error() string { 62 | return "" 63 | } 64 | 65 | func TestValidateWithRuleError(t *testing.T) { 66 | payload := StructBForTest{C: 10} 67 | rule := ruletest.NewRule("test", true, &errStructBForValidation{}) 68 | 69 | validated, err := Validate(payload, []Rule{rule}) 70 | if !validated { 71 | t.Error("Validated result is not expected") 72 | return 73 | } 74 | if !rule.ValidateCalled { 75 | t.Error("The rule validate was call") 76 | return 77 | } 78 | if _, ok := err.(*errStructBForValidation); !ok { 79 | t.Errorf("Unexpected error type: got: %v", err) 80 | return 81 | } 82 | } 83 | 84 | func TestSetTagName(t *testing.T) { 85 | validator := NewValidator() 86 | if got, want := validator.tagName, DefaultTagName; got != want { 87 | t.Errorf("Unexpected default tag value from validator struct type: got: %v, want: %v", got, want) 88 | return 89 | } 90 | 91 | newTag := "new-tag" 92 | if err := validator.SetTagName(newTag); err != nil { 93 | t.Error(err) 94 | return 95 | } 96 | if got, want := validator.tagName, newTag; got != want { 97 | t.Errorf("Unexpected default tag value from validator struct type: got: %v, want: %v", got, want) 98 | return 99 | } 100 | 101 | err := validator.SetTagName("") 102 | if err == nil { 103 | t.Errorf("Unexpected error as nil") 104 | return 105 | } 106 | 107 | if ce, ok := err.(*ErrEmptyTagName); !ok { 108 | t.Errorf("Unexpected error type: got: %v, want: %v", reflect.TypeOf(ce), reflect.TypeOf(&ErrEmptyTagName{})) 109 | return 110 | } 111 | } 112 | 113 | func TestDynamicEnumParameterValidation(t *testing.T) { 114 | type Status string 115 | const ( 116 | StatusCreated Status = "__CREATED__" 117 | StatusPending Status = "__PENDING__" 118 | StatusDoing Status = "__DOING__" 119 | StatusDone Status = "__DONE__" 120 | ) 121 | type Task struct { 122 | Name string 123 | Status Status `validate:"enum={status}"` 124 | } 125 | validator := NewValidator() 126 | validator.AddRuleParameters(map[string]string{ 127 | "status": string(StatusCreated) + "," + string(StatusPending) + "," + string(StatusDoing) + "," + string(StatusDone), 128 | }) 129 | validator.AddRules(rule.Enum) 130 | 131 | task := Task{Name: "Test", Status: StatusCreated} 132 | ok, err := validator.Validate(task) 133 | if !ok || err != nil { 134 | t.Errorf("expected valid enum, got ok=%v, err=%v", ok, err) 135 | } 136 | 137 | task.Status = "__INVALID__" 138 | ok, err = validator.Validate(task) 139 | if !ok || err == nil { 140 | t.Errorf("expected invalid enum, got ok=%v, err=%v", ok, err) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /validator.go: -------------------------------------------------------------------------------- 1 | package gody 2 | 3 | import "fmt" 4 | 5 | type ErrDuplicatedRule struct { 6 | RuleDuplicated Rule 7 | } 8 | 9 | func (err *ErrDuplicatedRule) Error() string { 10 | return fmt.Sprintf("rule %s is duplicated", err.RuleDuplicated.Name()) 11 | } 12 | 13 | type Validator struct { 14 | tagName string 15 | rulesMap map[string]Rule 16 | addedRules []Rule 17 | params map[string]string 18 | } 19 | 20 | func (v *Validator) AddRules(rs ...Rule) error { 21 | for _, r := range rs { 22 | if dr, exists := v.rulesMap[r.Name()]; exists { 23 | return &ErrDuplicatedRule{RuleDuplicated: dr} 24 | } 25 | v.rulesMap[r.Name()] = r 26 | } 27 | v.addedRules = append(v.addedRules, rs...) 28 | return nil 29 | } 30 | 31 | func (v *Validator) SetTagName(tn string) error { 32 | if tn == "" { 33 | return &ErrEmptyTagName{} 34 | } 35 | v.tagName = tn 36 | return nil 37 | } 38 | 39 | func (v *Validator) AddRuleParameters(params map[string]string) { 40 | for k, val := range params { 41 | v.params[k] = val 42 | } 43 | } 44 | 45 | func (v *Validator) Validate(b any) (bool, error) { 46 | return RawDefaultValidateWithParams(b, v.tagName, v.addedRules, v.params) 47 | } 48 | 49 | func NewValidator() *Validator { 50 | tagName := DefaultTagName 51 | rulesMap := make(map[string]Rule) 52 | addedRules := make([]Rule, 0) 53 | params := make(map[string]string) 54 | return &Validator{tagName, rulesMap, addedRules, params} 55 | } 56 | -------------------------------------------------------------------------------- /validator_test.go: -------------------------------------------------------------------------------- 1 | package gody 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/guiferpa/gody/v2/rule/ruletest" 8 | ) 9 | 10 | func TestValidator(t *testing.T) { 11 | payload := struct { 12 | A int `validate:"test"` 13 | }{10} 14 | 15 | validator := NewValidator() 16 | 17 | rule := ruletest.NewRule("test", true, nil) 18 | if err := validator.AddRules(rule); err != nil { 19 | t.Error("Unexpected error") 20 | return 21 | } 22 | validated, err := validator.Validate(payload) 23 | if !validated { 24 | t.Error("Validated result is not expected") 25 | return 26 | } 27 | if err != nil { 28 | t.Error("Error result from validate is not expected") 29 | return 30 | } 31 | if !rule.ValidateCalled { 32 | t.Error("The rule validate wasn't call") 33 | return 34 | } 35 | } 36 | 37 | func TestDuplicatedRule(t *testing.T) { 38 | validator := NewValidator() 39 | rule := ruletest.NewRule("a", true, nil) 40 | rules := []Rule{ 41 | rule, 42 | ruletest.NewRule("b", true, nil), 43 | ruletest.NewRule("c", true, nil), 44 | rule, 45 | } 46 | err := validator.AddRules(rules...) 47 | if err == nil { 48 | t.Error("Unexpected nil value for duplicated rule error") 49 | return 50 | } 51 | 52 | if _, ok := err.(*ErrDuplicatedRule); !ok { 53 | t.Errorf("Unexpected error type: got: %v", err) 54 | return 55 | } 56 | } 57 | 58 | func TestDuplicatedRuleError(t *testing.T) { 59 | r := ruletest.NewRule("mock", true, errors.New("mock error")) 60 | err := &ErrDuplicatedRule{RuleDuplicated: r} 61 | got := err.Error() 62 | want := "rule mock is duplicated" 63 | if got != want { 64 | t.Errorf(`&ErrDuplicatedRule{RuleDuplicated: r}.Error(), got: %v, want: %v`, got, want) 65 | } 66 | } 67 | --------------------------------------------------------------------------------