├── go.mod ├── .idea ├── vcs.xml ├── modules.xml ├── go-validator.iml └── workspace.xml ├── .codecov.yml ├── _examples ├── gin │ ├── lang │ │ ├── zh_CN │ │ │ └── validation.go │ │ └── en │ │ │ └── validation.go │ ├── gin_validator.go │ └── main.go ├── translations │ ├── lang │ │ ├── zh_CN │ │ │ └── validation.go │ │ └── en │ │ │ └── validation.go │ └── main.go ├── echo │ ├── echo_validator.go │ └── main.go ├── iris │ └── main.go ├── custom │ └── main.go ├── translation │ └── main.go └── simple │ └── main.go ├── validator_int.go ├── validator_unit.go ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── translator.go ├── validator_float.go ├── converter.go ├── .golangci.yml ├── .gitignore ├── converter_test.go ├── error.go ├── validator_string.go ├── lang ├── zh_CN │ └── zh_CN.go ├── zh_HK │ └── zh_HK.go └── en │ └── en.go ├── translator_test.go ├── patterns.go ├── error_test.go ├── message.go ├── required_variants_test.go ├── complex_struct_test.go ├── cache_coverage_test.go ├── coverage_improvement_test.go ├── validation_edge_cases_test.go ├── benchmarks_test.go ├── numeric_validation_test.go ├── CLAUDE.md ├── README.md └── cache.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/syssam/go-validator 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | target: 75% 7 | threshold: 2% 8 | base: auto 9 | patch: 10 | default: 11 | target: 70% 12 | threshold: 5% 13 | base: auto -------------------------------------------------------------------------------- /_examples/gin/lang/zh_CN/validation.go: -------------------------------------------------------------------------------- 1 | package zh_CN 2 | 3 | // AttributeMap is a map of string, that can be used as error message for ValidateStruct function. 4 | var AttributeMap = map[string]string{ 5 | "User.FirstName": "名字", 6 | "User.LastName": "姓氏", 7 | "User.Age": "年龄", 8 | } 9 | -------------------------------------------------------------------------------- /_examples/translations/lang/zh_CN/validation.go: -------------------------------------------------------------------------------- 1 | package zh_CN 2 | 3 | // AttributeMap is a map of string, that can be used as error message for ValidateStruct function. 4 | var AttributeMap = map[string]string{ 5 | "User.FirstName": "名字", 6 | "User.LastName": "姓氏", 7 | "User.Age": "年龄", 8 | } 9 | -------------------------------------------------------------------------------- /_examples/gin/lang/en/validation.go: -------------------------------------------------------------------------------- 1 | package en 2 | 3 | // AttributeMap is a map of string, that can be used as error message for ValidateStruct function. 4 | var AttributeMap = map[string]string{ 5 | "User.FirstName": "First Name", 6 | "User.LastName": "Last Name", 7 | "User.Age": "Age", 8 | } 9 | -------------------------------------------------------------------------------- /_examples/translations/lang/en/validation.go: -------------------------------------------------------------------------------- 1 | package en 2 | 3 | // AttributeMap is a map of string, that can be used as error message for ValidateStruct function. 4 | var AttributeMap = map[string]string{ 5 | "User.FirstName": "First Name", 6 | "User.LastName": "Last Name", 7 | "User.Age": "Age", 8 | } 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/go-validator.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /_examples/echo/echo_validator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | 6 | validator "github.com/syssam/go-validator" 7 | ) 8 | 9 | // DefaultValidator struct 10 | type DefaultValidator struct { 11 | once sync.Once 12 | validate *validator.Validator 13 | } 14 | 15 | // Validate return error 16 | func (v *DefaultValidator) Validate(obj interface{}) error { 17 | v.lazyinit() 18 | if err := validator.ValidateStruct(obj); err != nil { 19 | return error(err) 20 | } 21 | return nil 22 | } 23 | 24 | func (v *DefaultValidator) lazyinit() { 25 | v.once.Do(func() { 26 | v.validate = validator.New() 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /_examples/echo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo" 7 | ) 8 | 9 | type User struct { 10 | Name string `json:"name" valid:"required"` 11 | Email string `json:"email" valid:"required,email"` 12 | } 13 | 14 | func main() { 15 | e := echo.New() 16 | e.Validator = &DefaultValidator{} 17 | e.POST("/users", func(c echo.Context) (err error) { 18 | u := new(User) 19 | if err = c.Bind(u); err != nil { 20 | return 21 | } 22 | if err = c.Validate(u); err != nil { 23 | return c.JSON(http.StatusBadRequest, err) 24 | } 25 | return c.JSON(http.StatusOK, u) 26 | }) 27 | e.Logger.Fatal(e.Start(":8080")) 28 | } 29 | -------------------------------------------------------------------------------- /_examples/iris/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/kataras/iris" 7 | validator "github.com/syssam/go-validator" 8 | ) 9 | 10 | type User struct { 11 | Name string `json:"name" valid:"required"` 12 | Email string `json:"email" valid:"required,email"` 13 | } 14 | 15 | var validate *validator.Validator 16 | 17 | func main() { 18 | app := iris.New() 19 | validate = validator.New() 20 | 21 | app.Post("/user", func(c iris.Context) { 22 | var user User 23 | if err := c.ReadJSON(&user); err != nil { 24 | // Handle error. 25 | } 26 | 27 | if err := validator.ValidateStruct(user); err != nil { 28 | c.StatusCode(http.StatusBadRequest) 29 | c.JSON(err) 30 | return 31 | } 32 | 33 | c.StatusCode(http.StatusOK) 34 | c.JSON(user) 35 | }) 36 | 37 | app.Run(iris.Addr(":8080")) 38 | } 39 | -------------------------------------------------------------------------------- /_examples/gin/gin_validator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/gin-gonic/gin/binding" 7 | validator "github.com/syssam/go-validator" 8 | ) 9 | 10 | // DefaultValidator struct 11 | type DefaultValidator struct { 12 | once sync.Once 13 | validate *validator.Validator 14 | } 15 | 16 | var _ binding.StructValidator = &DefaultValidator{} 17 | 18 | // ValidateStruct return error 19 | func (v *DefaultValidator) ValidateStruct(obj interface{}) error { 20 | v.lazyinit() 21 | if err := validator.ValidateStruct(obj); err != nil { 22 | return error(err) 23 | } 24 | return nil 25 | } 26 | 27 | // Engine return v.validate 28 | func (v *DefaultValidator) Engine() interface{} { 29 | v.lazyinit() 30 | return v.validate 31 | } 32 | 33 | func (v *DefaultValidator) lazyinit() { 34 | v.once.Do(func() { 35 | v.validate = validator.New() 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /validator_int.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import "fmt" 4 | 5 | // ValidateDigitsBetweenInt64 returns true if value lies between left and right border 6 | func ValidateDigitsBetweenInt64(value, left, right int64) bool { 7 | if left > right { 8 | left, right = right, left 9 | } 10 | return value >= left && value <= right 11 | } 12 | 13 | // compareInt64 determine if a comparison passes between the given values. 14 | func compareInt64(first, second int64, operator string) (bool, error) { 15 | switch operator { 16 | case "<": 17 | return first < second, nil 18 | case ">": 19 | return first > second, nil 20 | case "<=": 21 | return first <= second, nil 22 | case ">=": 23 | return first >= second, nil 24 | case "==": 25 | return first == second, nil 26 | default: 27 | return false, fmt.Errorf("validator: compareInt64 unsupported operator %s", operator) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /validator_unit.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import "fmt" 4 | 5 | // ValidateDigitsBetweenUint64 returns true if value lies between left and right border 6 | func ValidateDigitsBetweenUint64(value, left, right uint64) bool { 7 | if left > right { 8 | left, right = right, left 9 | } 10 | return value >= left && value <= right 11 | } 12 | 13 | // compareUint64 determine if a comparison passes between the given values. 14 | func compareUint64(first, second uint64, operator string) (bool, error) { 15 | switch operator { 16 | case "<": 17 | return first < second, nil 18 | case ">": 19 | return first > second, nil 20 | case "<=": 21 | return first <= second, nil 22 | case ">=": 23 | return first >= second, nil 24 | case "==": 25 | return first == second, nil 26 | default: 27 | return false, fmt.Errorf("validator: compareUint64 unsupported operator %s", operator) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 syssam 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 | -------------------------------------------------------------------------------- /_examples/custom/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "regexp" 7 | 8 | validator "github.com/syssam/go-validator" 9 | ) 10 | 11 | // User contains user information 12 | type User struct { 13 | UserName string `valid:"customValidator"` 14 | Password string `valid:"customValidator2"` 15 | } 16 | 17 | func CustomValidator(v reflect.Value, o reflect.Value, validTag *validator.ValidTag) bool { 18 | return false 19 | } 20 | 21 | func main() { 22 | validator.MessageMap["customValidator"] = "customValidator is not valid." 23 | validator.MessageMap["customValidator2"] = "Beginning with a letter, allowing 5-16 bytes, allowing alphanumeric underlining." 24 | validator.CustomTypeRuleMap.Set("customValidator", CustomValidator) 25 | validator.CustomTypeRuleMap.Set("customValidator2", func(v reflect.Value, o reflect.Value, validTag *validator.ValidTag) bool { 26 | switch v.Kind() { 27 | case reflect.String: 28 | return regexp.MustCompile("^[a-zA-Z]\\w{5,17}$").MatchString(v.String()) 29 | } 30 | return false 31 | }) 32 | 33 | user := &User{ 34 | UserName: "Tester", 35 | Password: "12345678", 36 | } 37 | 38 | err := validator.ValidateStruct(user) 39 | if err != nil { 40 | for _, err := range err.(validator.Errors) { 41 | fmt.Println(err) 42 | } 43 | return 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /_examples/translation/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | validator "github.com/syssam/go-validator" 7 | validator_zh_CN "github.com/syssam/go-validator/lang/zh_CN" 8 | ) 9 | 10 | // User contains user information 11 | type User struct { 12 | FirstName string `valid:"required,attribute=名字"` 13 | LastName string `valid:"required,attribute=姓氏"` 14 | Age uint8 `valid:"between=0|30,attribute=年龄"` 15 | Email string `valid:"required,email"` 16 | Addresses []*Address `valid:"required"` // a person can have a home and cottage... 17 | } 18 | 19 | // Address houses a users address information 20 | type Address struct { 21 | Street string `valid:"required"` 22 | City string `valid:"required"` 23 | Planet string `valid:"required"` 24 | Phone string `valid:"required"` 25 | } 26 | 27 | func main() { 28 | translator := validator.NewTranslator() 29 | translator.SetMessage("zh_CN", validator_zh_CN.MessageMap) 30 | validator.Default.Translator = translator 31 | 32 | address := &Address{ 33 | Street: "Eavesdown Docks", 34 | Planet: "Persphone", 35 | Phone: "none", 36 | } 37 | 38 | user := &User{ 39 | FirstName: "Badger", 40 | LastName: "Smith", 41 | Age: 135, 42 | Email: "Badger.Smith@gmail.com", 43 | Addresses: []*Address{address}, 44 | } 45 | 46 | err := validator.ValidateStruct(user) 47 | if err != nil { 48 | errs := validator.Default.Translator.Trans(err.(validator.Errors), "zh_CN") 49 | for _, err := range errs { 50 | fmt.Println(err) 51 | } 52 | return 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master, main ] 6 | pull_request: 7 | branches: [ master, main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | go-version: [1.19.x, 1.20.x, 1.21.x, 1.22.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | 24 | - name: Cache Go modules 25 | uses: actions/cache@v4 26 | with: 27 | path: ~/go/pkg/mod 28 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 29 | restore-keys: | 30 | ${{ runner.os }}-go- 31 | 32 | - name: Install dependencies 33 | run: go mod download 34 | 35 | - name: Run tests 36 | run: | 37 | go test -v ./... 38 | go test -race ./... 39 | 40 | - name: Run benchmarks 41 | run: go test -bench=. -benchmem ./... 42 | 43 | - name: Check coverage 44 | run: | 45 | go test -coverprofile=coverage.out ./... 46 | go tool cover -html=coverage.out -o coverage.html 47 | 48 | - name: Upload coverage reports 49 | uses: codecov/codecov-action@v5 50 | with: 51 | file: ./coverage.out 52 | token: ${{ secrets.CODECOV_TOKEN }} 53 | 54 | 55 | lint: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v4 59 | - uses: actions/setup-go@v4 60 | with: 61 | go-version: 1.22.x 62 | - name: golangci-lint 63 | uses: golangci/golangci-lint-action@v6 64 | with: 65 | version: latest -------------------------------------------------------------------------------- /_examples/translations/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | validator "github.com/syssam/go-validator" 7 | lang_en "github.com/syssam/go-validator/_examples/translations/lang/en" 8 | lang_zh_CN "github.com/syssam/go-validator/_examples/translations/lang/zh_CN" 9 | validator_en "github.com/syssam/go-validator/lang/en" 10 | validator_zh_CN "github.com/syssam/go-validator/lang/zh_CN" 11 | ) 12 | 13 | // User contains user information 14 | type User struct { 15 | FirstName string `valid:"required"` 16 | LastName string `valid:"required"` 17 | Age uint8 `valid:"between=0|30"` 18 | Email string `valid:"required,email"` 19 | Addresses []*Address `valid:"required"` // a person can have a home and cottage... 20 | } 21 | 22 | // Address houses a users address information 23 | type Address struct { 24 | Street string `valid:"required"` 25 | City string `valid:"required"` 26 | Planet string `valid:"required"` 27 | Phone string `valid:"required"` 28 | } 29 | 30 | func main() { 31 | translator := validator.NewTranslator() 32 | translator.SetMessage("en", validator_en.MessageMap) 33 | translator.SetMessage("zh_CN", validator_zh_CN.MessageMap) 34 | translator.SetAttributes("en", lang_en.AttributeMap) 35 | translator.SetAttributes("zh_CN", lang_zh_CN.AttributeMap) 36 | validator.Default.Translator = translator 37 | 38 | address := &Address{ 39 | Street: "Eavesdown Docks", 40 | Planet: "Persphone", 41 | Phone: "none", 42 | } 43 | 44 | user := &User{ 45 | FirstName: "Badger", 46 | LastName: "Smith", 47 | Age: 135, 48 | Email: "Badger.Smith@gmail.com", 49 | Addresses: []*Address{address}, 50 | } 51 | 52 | err := validator.ValidateStruct(user) 53 | if err != nil { 54 | errs := validator.Default.Translator.Trans(err.(validator.Errors), "zh_CN") 55 | for _, err := range errs { 56 | fmt.Println(err) 57 | } 58 | return 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /_examples/simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | validator "github.com/syssam/go-validator" 7 | ) 8 | 9 | // User contains user information with comprehensive validation 10 | type User struct { 11 | FirstName string `valid:"required"` 12 | LastName string `valid:"required"` 13 | Age uint8 `valid:"between=0|120"` 14 | Email string `valid:"required,email"` 15 | Addresses []*Address `valid:"required"` 16 | } 17 | 18 | // Address houses a user's address information 19 | type Address struct { 20 | Street string `valid:"required"` 21 | City string `valid:"required"` 22 | Planet string `valid:"required"` 23 | Phone string `valid:"required"` 24 | } 25 | 26 | func main() { 27 | fmt.Println("🚀 Go Validator - Simple Example with Error Handling") 28 | fmt.Println("==================================================") 29 | 30 | address := &Address{ 31 | Street: "Eavesdown Docks", 32 | Planet: "Persephone", // Fixed spelling 33 | Phone: "none", 34 | // Missing City to demonstrate error handling 35 | } 36 | 37 | user := &User{ 38 | FirstName: "Badger", 39 | LastName: "Smith", 40 | Age: 135, // Invalid age to demonstrate error handling 41 | Email: "Badger.Smith@gmail.com", 42 | Addresses: []*Address{address}, 43 | } 44 | 45 | fmt.Println("📋 Validating user data...") 46 | err := validator.ValidateStruct(user) 47 | if err != nil { 48 | fmt.Println("❌ Validation failed with the following errors:") 49 | 50 | // Cast to Errors type for advanced error handling 51 | if validationErrors, ok := err.(validator.Errors); ok { 52 | // Group errors by field for better organization 53 | groupedErrors := validationErrors.GroupByField() 54 | 55 | for fieldName, fieldErrors := range groupedErrors { 56 | fmt.Printf("\n🔸 Field '%s':\n", fieldName) 57 | for _, fieldError := range fieldErrors { 58 | fmt.Println(fieldError) 59 | } 60 | } 61 | } 62 | return 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /translator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Translate translate type 8 | type Translate map[string]string 9 | 10 | // Translator type 11 | type Translator struct { 12 | customMessage map[string]Translate 13 | messages map[string]Translate 14 | attributes map[string]Translate 15 | } 16 | 17 | // NewTranslator returns a new instance of 'translator' with sane defaults. 18 | func NewTranslator() *Translator { 19 | translator := &Translator{ 20 | messages: make(map[string]Translate), 21 | attributes: make(map[string]Translate), 22 | customMessage: make(map[string]Translate), 23 | } 24 | return translator 25 | } 26 | 27 | // SetMessage set Message 28 | func (t *Translator) SetMessage(langCode string, messages Translate) { 29 | t.messages[langCode] = messages 30 | } 31 | 32 | // LoadMessage load message 33 | func (t *Translator) LoadMessage(langCode string) Translate { 34 | return t.messages[langCode] 35 | } 36 | 37 | // SetAttributes set attributes 38 | func (t *Translator) SetAttributes(langCode string, messages Translate) { 39 | t.attributes[langCode] = messages 40 | } 41 | 42 | // Trans translate errors 43 | func (t *Translator) Trans(errors Errors, language string) Errors { 44 | for i := 0; i < len(errors); i++ { 45 | fieldError, ok := errors[i].(*FieldError) 46 | if !ok { 47 | break 48 | } 49 | 50 | if m, ok := t.customMessage[language][fieldError.Name+"."+fieldError.MessageName]; ok { 51 | errors[i].(*FieldError).SetMessage(m) 52 | break 53 | } 54 | 55 | message, ok := t.messages[language][fieldError.MessageName] 56 | if ok { 57 | attribute := fieldError.Attribute 58 | if customAttribute, ok := t.attributes[language][fieldError.StructName]; ok { 59 | attribute = customAttribute 60 | } else if fieldError.DefaultAttribute != "" { 61 | attribute = fieldError.DefaultAttribute 62 | } 63 | 64 | message = strings.ReplaceAll(message, "{{.Attribute}}", attribute) 65 | 66 | for _, parameter := range fieldError.MessageParameters { 67 | message = strings.ReplaceAll(message, "{{."+parameter.Key+"}}", parameter.Value) 68 | } 69 | 70 | errors[i].(*FieldError).SetMessage(message) 71 | } 72 | } 73 | 74 | return errors 75 | } 76 | -------------------------------------------------------------------------------- /validator_float.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import "fmt" 4 | 5 | // ValidateDigitsBetweenFloat64 returns true if value lies between left and right border 6 | func ValidateDigitsBetweenFloat64(value, left, right float64) bool { 7 | if left > right { 8 | left, right = right, left 9 | } 10 | return value >= left && value <= right 11 | } 12 | 13 | // ValidateMaxFloat64 is the validation function for validating if the current field's value is less than or equal to the param's value. 14 | func ValidateMaxFloat64(v, param float64) bool { 15 | return ValidateLteFloat64(v, param) 16 | } 17 | 18 | // ValidateMinFloat64 is the validation function for validating if the current field's value is greater than or equal to the param's value. 19 | func ValidateMinFloat64(v, param float64) bool { 20 | return ValidateGteFloat64(v, param) 21 | } 22 | 23 | // ValidateLtFloat64 is the validation function for validating if the current field's value is less than the param's value. 24 | func ValidateLtFloat64(v, param float64) bool { 25 | return v < param 26 | } 27 | 28 | // ValidateLteFloat64 is the validation function for validating if the current field's value is less than or equal to the param's value. 29 | func ValidateLteFloat64(v, param float64) bool { 30 | return v <= param 31 | } 32 | 33 | // ValidateGteFloat64 is the validation function for validating if the current field's value is greater than or equal to the param's value. 34 | func ValidateGteFloat64(v, param float64) bool { 35 | return v >= param 36 | } 37 | 38 | // ValidateGtFloat64 is the validation function for validating if the current field's value is greater than to the param's value. 39 | func ValidateGtFloat64(v, param float64) bool { 40 | return v > param 41 | } 42 | 43 | // compareFloat64 determines if a comparison passes between the given values. 44 | func compareFloat64(first, second float64, operator string) (bool, error) { 45 | switch operator { 46 | case "<": 47 | return first < second, nil 48 | case ">": 49 | return first > second, nil 50 | case "<=": 51 | return first <= second, nil 52 | case ">=": 53 | return first >= second, nil 54 | case "==": 55 | return first == second, nil 56 | default: 57 | return false, fmt.Errorf("validator: compareFloat64 unsupported operator %s", operator) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /_examples/gin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/gin-gonic/gin/binding" 9 | validator "github.com/syssam/go-validator" 10 | lang_en "github.com/syssam/go-validator/_examples/translations/lang/en" 11 | lang_zhCN "github.com/syssam/go-validator/_examples/translations/lang/zh_CN" 12 | validator_en "github.com/syssam/go-validator/lang/en" 13 | validator_zhCN "github.com/syssam/go-validator/lang/zh_CN" 14 | ) 15 | 16 | // User contains user information 17 | type User struct { 18 | FirstName string `valid:"required"` 19 | LastName string `valid:"required"` 20 | Age uint8 `valid:"between=0|30"` 21 | Email string `valid:"required,email"` 22 | } 23 | 24 | type appError struct { 25 | Code int `json:"code,omitempty"` 26 | Errors validator.Errors `json:"errors,omitempty"` 27 | Message string `json:"message,omitempty"` 28 | } 29 | 30 | // ErrorResponse return error 31 | func ErrorResponse(c *gin.Context, code int, err error) { 32 | switch v := err.(type) { 33 | case validator.Errors: 34 | locale := c.DefaultQuery("locale", "en") 35 | c.JSON(http.StatusBadRequest, gin.H{"error": &appError{ 36 | Code: http.StatusBadRequest, 37 | Errors: binding.Validator.Engine().(*validator.Validator).Translator.Trans(v, locale), 38 | }}) 39 | default: 40 | c.JSON(http.StatusBadRequest, gin.H{"error": &appError{ 41 | Code: http.StatusBadRequest, 42 | Message: err.Error(), 43 | }}) 44 | } 45 | return 46 | } 47 | 48 | func init() { 49 | 50 | // replace gin default validator 51 | binding.Validator = new(DefaultValidator) 52 | if v, ok := binding.Validator.Engine().(*validator.Validator); ok { 53 | v.Translator = validator.NewTranslator() 54 | v.Translator.SetMessage("en", validator_en.MessageMap) 55 | v.Translator.SetMessage("zh_CN", validator_zhCN.MessageMap) 56 | v.Translator.SetAttributes("en", lang_en.AttributeMap) 57 | v.Translator.SetAttributes("zh_CN", lang_zhCN.AttributeMap) 58 | } 59 | } 60 | 61 | func main() { 62 | r := gin.Default() 63 | r.POST("/", func(c *gin.Context) { 64 | time.Sleep(time.Duration(5) * time.Second) 65 | var form User 66 | if err := c.ShouldBind(&form); err == nil { 67 | c.JSON(http.StatusOK, &form) 68 | } else { 69 | ErrorResponse(c, http.StatusBadRequest, err) 70 | } 71 | }) 72 | r.Run() // listen and serve on 0.0.0.0:8080 73 | } 74 | -------------------------------------------------------------------------------- /converter.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | ) 8 | 9 | // ToString convert the input to a string with optimized fast paths. 10 | func ToString(obj interface{}) string { 11 | // Fast path for common types to avoid fmt.Sprintf overhead 12 | switch v := obj.(type) { 13 | case string: 14 | return v 15 | case int: 16 | return strconv.Itoa(v) 17 | case int64: 18 | return strconv.FormatInt(v, 10) 19 | case uint64: 20 | return strconv.FormatUint(v, 10) 21 | case float64: 22 | return strconv.FormatFloat(v, 'f', -1, 64) 23 | case bool: 24 | return strconv.FormatBool(v) 25 | default: 26 | // Fallback to fmt.Sprintf for complex types 27 | return fmt.Sprintf("%v", obj) 28 | } 29 | } 30 | 31 | // ToFloat convert the input string to a float, or 0.0 if the input is not a float. 32 | func ToFloat(str string) (float64, error) { 33 | res, err := strconv.ParseFloat(str, 64) 34 | if err != nil { 35 | res = 0.0 36 | } 37 | return res, err 38 | } 39 | 40 | // ToBool convert the input string to a bool if the input is not a string. 41 | func ToBool(str string) bool { 42 | if str == "true" || str == "1" { 43 | return true 44 | } 45 | 46 | return false 47 | } 48 | 49 | // ToInt convert the input string or any int type to an integer type 64, or 0 if the input is not an integer. 50 | func ToInt(value interface{}) (res int64, err error) { 51 | switch v := value.(type) { 52 | case int: 53 | return int64(v), nil 54 | case int8: 55 | return int64(v), nil 56 | case int16: 57 | return int64(v), nil 58 | case int32: 59 | return int64(v), nil 60 | case int64: 61 | return v, nil 62 | case uint: 63 | if v > math.MaxInt64 { 64 | return 0, fmt.Errorf("value %v exceeds int64 maximum", v) 65 | } 66 | return int64(v), nil 67 | case uint8: 68 | return int64(v), nil 69 | case uint16: 70 | return int64(v), nil 71 | case uint32: 72 | return int64(v), nil 73 | case uint64: 74 | if v > math.MaxInt64 { 75 | return 0, fmt.Errorf("value %v exceeds int64 maximum", v) 76 | } 77 | return int64(v), nil 78 | case string: 79 | return strconv.ParseInt(v, 10, 64) 80 | default: 81 | return 0, fmt.Errorf("unable to convert %T to int64", value) 82 | } 83 | } 84 | 85 | // ToUint convert the input string if the input is not an unit. 86 | func ToUint(param string) (res uint64, err error) { 87 | i, err := strconv.ParseUint(param, 0, 64) 88 | return i, err 89 | } 90 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 16 | 17 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | true 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # golangci-lint configuration for go-validator 2 | 3 | run: 4 | timeout: 10m 5 | tests: true 6 | 7 | output: 8 | formats: 9 | - format: colored-line-number 10 | print-issued-lines: true 11 | print-linter-name: true 12 | sort-results: true 13 | 14 | linters: 15 | disable-all: true 16 | enable: 17 | - govet 18 | - errcheck 19 | - gosimple 20 | - unused 21 | - gofmt 22 | - goimports 23 | - misspell 24 | - gocyclo 25 | - typecheck 26 | 27 | linters-settings: 28 | gocyclo: 29 | # Higher threshold for validation library complexity 30 | min-complexity: 50 31 | 32 | errcheck: 33 | check-type-assertions: false 34 | check-blank: false 35 | exclude-functions: 36 | - fmt.Print 37 | - fmt.Println 38 | 39 | gocritic: 40 | enabled-tags: 41 | - diagnostic 42 | - style 43 | - performance 44 | disabled-checks: 45 | - rangeValCopy # Allow field struct copying for validation performance 46 | - builtinShadow # Allow min/max parameter names 47 | - regexpSimplify # Keep regex patterns as-is for clarity 48 | - wrapperFunc # Allow explicit string replacement methods 49 | - dynamicFmtString # Allow dynamic error message formatting 50 | - hugeParam # Allow large struct parameters in validation 51 | 52 | revive: 53 | rules: 54 | - name: unused-parameter 55 | disabled: true 56 | - name: cognitive-complexity 57 | arguments: [50] 58 | 59 | issues: 60 | exclude-dirs: 61 | - vendor 62 | - _examples 63 | 64 | exclude-rules: 65 | # Skip complexity and style checks for test files 66 | - path: _test\.go 67 | linters: 68 | - gocyclo 69 | - gocritic 70 | - revive 71 | - cyclop 72 | - funlen 73 | - gosec 74 | 75 | # Skip specific complexity warnings for core validation logic 76 | - text: "cyclomatic complexity.*of func.*is high" 77 | linters: 78 | - gocyclo 79 | 80 | # Skip unused warnings for specific validation helper functions 81 | - text: "is unused" 82 | linters: 83 | - unused 84 | 85 | exclude: 86 | # Global patterns to ignore 87 | - "Error return value is not checked.*_test\\.go" 88 | - "should use.*ReplaceAll" 89 | - "integer overflow conversion" 90 | - "ineffectual assignment to err" 91 | - "possible nil pointer dereference" 92 | - "this check suggests that the pointer can be nil" 93 | - "paramTypeCombine.*could be replaced" 94 | 95 | max-issues-per-linter: 0 96 | max-same-issues: 0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | *.prof 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | # Go workspace file 19 | go.work 20 | go.work.sum 21 | 22 | # Build directories 23 | build/ 24 | dist/ 25 | bin/ 26 | 27 | # IDE/Editor files 28 | .vscode/ 29 | .idea/ 30 | *.swp 31 | *.swo 32 | *~ 33 | 34 | # JetBrains IDEs 35 | .idea/ 36 | *.iml 37 | *.ipr 38 | *.iws 39 | 40 | # Visual Studio Code 41 | .vscode/ 42 | *.code-workspace 43 | 44 | # Vim 45 | *.swp 46 | *.swo 47 | *.tmp 48 | 49 | # Emacs 50 | *~ 51 | \#*\# 52 | /.emacs.desktop 53 | /.emacs.desktop.lock 54 | *.elc 55 | auto-save-list 56 | tramp 57 | .\#* 58 | 59 | # Sublime Text 60 | *.sublime-project 61 | *.sublime-workspace 62 | 63 | # macOS 64 | .DS_Store 65 | .DS_Store? 66 | ._* 67 | .Spotlight-V100 68 | .Trashes 69 | ehthumbs.db 70 | Thumbs.db 71 | 72 | # Windows 73 | Thumbs.db 74 | ehthumbs.db 75 | Desktop.ini 76 | $RECYCLE.BIN/ 77 | *.cab 78 | *.msi 79 | *.msm 80 | *.msp 81 | 82 | # Linux 83 | *~ 84 | .fuse_hidden* 85 | .directory 86 | .Trash-* 87 | .nfs* 88 | 89 | # Testing and Coverage 90 | coverage.out 91 | coverage.html 92 | *.cover 93 | .coverage/ 94 | coverage/ 95 | test-results/ 96 | *.coverprofile 97 | 98 | # Benchmarking 99 | *.bench 100 | benchmarks/ 101 | benchmark-results/ 102 | 103 | # Temporary files 104 | *.tmp 105 | *.temp 106 | .tmp/ 107 | .temp/ 108 | 109 | # Logs 110 | *.log 111 | logs/ 112 | 113 | # Environment variables 114 | .env 115 | .env.local 116 | .env.*.local 117 | 118 | # Configuration files (if they contain secrets) 119 | config.json 120 | config.yml 121 | config.yaml 122 | secrets.json 123 | 124 | # Documentation builds 125 | docs/_build/ 126 | docs/site/ 127 | 128 | # Archives 129 | *.zip 130 | *.tar.gz 131 | *.rar 132 | 133 | # Backup files 134 | *.bak 135 | *.backup 136 | 137 | # Database files 138 | *.db 139 | *.sqlite 140 | *.sqlite3 141 | 142 | # Cache directories 143 | .cache/ 144 | .go-cache/ 145 | 146 | # Generated files 147 | *.pb.go 148 | *.gen.go 149 | 150 | # Protobuf files 151 | *.pb 152 | 153 | # Docker 154 | .dockerignore 155 | Dockerfile.local 156 | 157 | # Kubernetes 158 | *.kubeconfig 159 | 160 | # Certificates and keys 161 | *.pem 162 | *.key 163 | *.crt 164 | *.cert 165 | *.p12 166 | 167 | # Local development 168 | local/ 169 | .local/ 170 | dev/ 171 | 172 | # Ignore CLAUDE.md if you decide not to include it 173 | # CLAUDE.md 174 | 175 | # Security scan results 176 | .gosec/ -------------------------------------------------------------------------------- /converter_test.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestToString(t *testing.T) { 9 | tests := []struct { 10 | input interface{} 11 | expected string 12 | }{ 13 | {"hello", "hello"}, 14 | {123, "123"}, 15 | {int64(456), "456"}, 16 | {uint64(789), "789"}, 17 | {float64(3.14), "3.14"}, 18 | {true, "true"}, 19 | {false, "false"}, 20 | {[]int{1, 2, 3}, "[1 2 3]"}, 21 | {nil, ""}, 22 | {struct{ Name string }{"test"}, "{test}"}, 23 | } 24 | 25 | for _, test := range tests { 26 | result := ToString(test.input) 27 | if result != test.expected { 28 | t.Errorf("ToString(%v) = %s; expected %s", test.input, result, test.expected) 29 | } 30 | } 31 | } 32 | 33 | func TestToFloat(t *testing.T) { 34 | tests := []struct { 35 | input string 36 | expected float64 37 | expectError bool 38 | }{ 39 | {"3.14", 3.14, false}, 40 | {"0", 0.0, false}, 41 | {"-2.5", -2.5, false}, 42 | {"invalid", 0.0, true}, 43 | {"", 0.0, true}, 44 | {"1.23e10", 1.23e10, false}, 45 | } 46 | 47 | for _, test := range tests { 48 | result, err := ToFloat(test.input) 49 | if result != test.expected { 50 | t.Errorf("ToFloat(%s) = %f; expected %f", test.input, result, test.expected) 51 | } 52 | if test.expectError && err == nil { 53 | t.Errorf("ToFloat(%s) expected error but got nil", test.input) 54 | } 55 | if !test.expectError && err != nil { 56 | t.Errorf("ToFloat(%s) unexpected error: %v", test.input, err) 57 | } 58 | } 59 | } 60 | 61 | func TestToBool(t *testing.T) { 62 | tests := []struct { 63 | input string 64 | expected bool 65 | }{ 66 | {"true", true}, 67 | {"1", true}, 68 | {"false", false}, 69 | {"0", false}, 70 | {"", false}, 71 | {"yes", false}, 72 | {"TRUE", false}, 73 | } 74 | 75 | for _, test := range tests { 76 | result := ToBool(test.input) 77 | if result != test.expected { 78 | t.Errorf("ToBool(%s) = %t; expected %t", test.input, result, test.expected) 79 | } 80 | } 81 | } 82 | 83 | func TestToInt(t *testing.T) { 84 | tests := []struct { 85 | input interface{} 86 | expected int64 87 | expectError bool 88 | }{ 89 | {int(123), 123, false}, 90 | {int8(45), 45, false}, 91 | {int16(1234), 1234, false}, 92 | {int32(56789), 56789, false}, 93 | {int64(9876543210), 9876543210, false}, 94 | {uint(123), 123, false}, 95 | {uint8(255), 255, false}, 96 | {uint16(65535), 65535, false}, 97 | {uint32(4294967295), 4294967295, false}, 98 | {uint64(9223372036854775807), 9223372036854775807, false}, 99 | {uint64(math.MaxUint64), 0, true}, // Exceeds int64 max 100 | {uint(math.MaxUint64), 0, true}, // Exceeds int64 max on 64-bit systems 101 | {"123", 123, false}, 102 | {"-456", -456, false}, 103 | {"invalid", 0, true}, 104 | {3.14, 0, true}, // Unsupported type 105 | } 106 | 107 | for _, test := range tests { 108 | result, err := ToInt(test.input) 109 | if result != test.expected { 110 | t.Errorf("ToInt(%v) = %d; expected %d", test.input, result, test.expected) 111 | } 112 | if test.expectError && err == nil { 113 | t.Errorf("ToInt(%v) expected error but got nil", test.input) 114 | } 115 | if !test.expectError && err != nil { 116 | t.Errorf("ToInt(%v) unexpected error: %v", test.input, err) 117 | } 118 | } 119 | } 120 | 121 | func TestToUint(t *testing.T) { 122 | tests := []struct { 123 | input string 124 | expected uint64 125 | expectError bool 126 | }{ 127 | {"123", 123, false}, 128 | {"0", 0, false}, 129 | {"18446744073709551615", math.MaxUint64, false}, 130 | {"-1", 0, true}, 131 | {"invalid", 0, true}, 132 | {"", 0, true}, 133 | {"0x10", 16, false}, // Hex 134 | {"010", 8, false}, // Octal 135 | } 136 | 137 | for _, test := range tests { 138 | result, err := ToUint(test.input) 139 | if result != test.expected { 140 | t.Errorf("ToUint(%s) = %d; expected %d", test.input, result, test.expected) 141 | } 142 | if test.expectError && err == nil { 143 | t.Errorf("ToUint(%s) expected error but got nil", test.input) 144 | } 145 | if !test.expectError && err != nil { 146 | t.Errorf("ToUint(%s) unexpected error: %v", test.input, err) 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | ) 9 | 10 | // Errors is an array of multiple errors and conforms to the error interface. 11 | type Errors []error 12 | 13 | // Error implements the error interface 14 | func (es Errors) Error() string { 15 | if len(es) == 0 { 16 | return "" 17 | } 18 | if len(es) == 1 { 19 | return es[0].Error() 20 | } 21 | 22 | var builder strings.Builder 23 | builder.Grow(len(es) * 50) // Pre-allocate estimated capacity 24 | 25 | for i, e := range es { 26 | if i > 0 { 27 | builder.WriteByte('\n') 28 | } 29 | builder.WriteString(e.Error()) 30 | } 31 | return builder.String() 32 | } 33 | 34 | // Errors returns itself for compatibility 35 | func (es Errors) Errors() []error { 36 | return es 37 | } 38 | 39 | // FieldErrors returns all FieldError instances 40 | func (es Errors) FieldErrors() []*FieldError { 41 | fieldErrors := make([]*FieldError, 0, len(es)) 42 | for _, e := range es { 43 | if fieldErr, ok := e.(*FieldError); ok { 44 | fieldErrors = append(fieldErrors, fieldErr) 45 | } else { 46 | // Convert generic error to FieldError 47 | fieldErrors = append(fieldErrors, &FieldError{ 48 | Message: e.Error(), 49 | }) 50 | } 51 | } 52 | return fieldErrors 53 | } 54 | 55 | // HasFieldError checks if there's an error for the specified field 56 | func (es Errors) HasFieldError(fieldName string) bool { 57 | for _, e := range es { 58 | if fieldErr, ok := e.(*FieldError); ok && fieldErr.Name == fieldName { 59 | return true 60 | } 61 | } 62 | return false 63 | } 64 | 65 | // GetFieldError returns the first error for the specified field 66 | func (es Errors) GetFieldError(fieldName string) *FieldError { 67 | for _, e := range es { 68 | if fieldErr, ok := e.(*FieldError); ok && fieldErr.Name == fieldName { 69 | return fieldErr 70 | } 71 | } 72 | return nil 73 | } 74 | 75 | // GroupByField groups errors by field name 76 | func (es Errors) GroupByField() map[string][]*FieldError { 77 | groups := make(map[string][]*FieldError) 78 | for _, e := range es { 79 | if fieldErr, ok := e.(*FieldError); ok { 80 | groups[fieldErr.Name] = append(groups[fieldErr.Name], fieldErr) 81 | } 82 | } 83 | return groups 84 | } 85 | 86 | type ErrorResponse struct { 87 | Message string `json:"message"` 88 | Parameter string `json:"parameter"` 89 | } 90 | 91 | var errorResponsePool = sync.Pool{ 92 | New: func() interface{} { 93 | slice := make([]ErrorResponse, 0, 10) 94 | return &slice 95 | }, 96 | } 97 | 98 | // MarshalJSON output Json format. 99 | func (es Errors) MarshalJSON() ([]byte, error) { 100 | if len(es) == 0 { 101 | return []byte("[]"), nil 102 | } 103 | 104 | responsesPtr := errorResponsePool.Get().(*[]ErrorResponse) 105 | responses := (*responsesPtr)[:0] 106 | 107 | defer errorResponsePool.Put(responsesPtr) 108 | 109 | for _, e := range es { 110 | if fieldErr, ok := e.(*FieldError); ok { 111 | responses = append(responses, ErrorResponse{ 112 | Message: fieldErr.Message, 113 | Parameter: fieldErr.Name, 114 | }) 115 | } 116 | } 117 | 118 | *responsesPtr = responses 119 | return json.Marshal(responses) 120 | } 121 | 122 | // FieldError encapsulates name, message, and value etc. 123 | type FieldError struct { 124 | Name string `json:"name"` 125 | StructName string `json:"struct_name,omitempty"` 126 | Tag string `json:"tag"` 127 | MessageName string `json:"message_name,omitempty"` 128 | MessageParameters MessageParameters `json:"message_parameters,omitempty"` 129 | Attribute string `json:"attribute,omitempty"` 130 | DefaultAttribute string `json:"default_attribute,omitempty"` 131 | Value string `json:"value,omitempty"` 132 | Message string `json:"message"` 133 | FuncError error `json:"func_error,omitempty"` 134 | } 135 | 136 | // Unwrap implements the errors.Unwrap interface for error chain support 137 | func (fe *FieldError) Unwrap() error { 138 | return fe.FuncError 139 | } 140 | 141 | // Error returns the error message with optional function error details 142 | func (fe *FieldError) Error() string { 143 | if fe.Message != "" { 144 | return fe.Message 145 | } 146 | if fe.FuncError != nil { 147 | return fmt.Sprintf("validation failed for field '%s': %v", fe.Name, fe.FuncError) 148 | } 149 | return fmt.Sprintf("validation failed for field '%s'", fe.Name) 150 | } 151 | 152 | // HasFuncError checks if there's an underlying function error 153 | func (fe *FieldError) HasFuncError() bool { 154 | return fe.FuncError != nil 155 | } 156 | 157 | // SetMessage sets the user-friendly message while preserving function error 158 | func (fe *FieldError) SetMessage(msg string) { 159 | fe.Message = msg 160 | } 161 | -------------------------------------------------------------------------------- /validator_string.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/url" 7 | "strings" 8 | "unicode/utf8" 9 | ) 10 | 11 | // ValidateBetweenString is 12 | func ValidateBetweenString(v string, left, right int64) bool { 13 | return ValidateDigitsBetweenInt64(int64(utf8.RuneCountInString(v)), left, right) 14 | } 15 | 16 | // InString check if string str is a member of the set of strings params 17 | func InString(str string, params []string) bool { 18 | for _, param := range params { 19 | if str == param { 20 | return true 21 | } 22 | } 23 | 24 | return false 25 | } 26 | 27 | // compareString determine if a comparison passes between the given values. 28 | func compareString(first string, second int64, operator string) (bool, error) { 29 | switch operator { 30 | case "<": 31 | return int64(utf8.RuneCountInString(first)) < second, nil 32 | case ">": 33 | return int64(utf8.RuneCountInString(first)) > second, nil 34 | case "<=": 35 | return int64(utf8.RuneCountInString(first)) <= second, nil 36 | case ">=": 37 | return int64(utf8.RuneCountInString(first)) >= second, nil 38 | case "==": 39 | return int64(utf8.RuneCountInString(first)) == second, nil 40 | default: 41 | return false, fmt.Errorf("validator: compareString unsupported operator %s", operator) 42 | } 43 | } 44 | 45 | // IsNumeric check if the string must be numeric. Empty string is valid. 46 | func IsNumeric(str string) bool { 47 | if IsNull(str) { 48 | return true 49 | } 50 | return rxNumeric.MatchString(str) 51 | } 52 | 53 | // IsInt check if the string must be an integer. Empty string is valid. 54 | func IsInt(str string) bool { 55 | if IsNull(str) { 56 | return true 57 | } 58 | return rxInt.MatchString(str) 59 | } 60 | 61 | // IsFloat check if the string must be an float. Empty string is valid. 62 | func IsFloat(str string) bool { 63 | if IsNull(str) { 64 | return true 65 | } 66 | return rxFloat.MatchString(str) 67 | } 68 | 69 | // IsNull check if the string is null. 70 | func IsNull(str string) bool { 71 | return str == "" 72 | } 73 | 74 | // IsEmptyString check if the string is empty. 75 | func IsEmptyString(str string) bool { 76 | return strings.TrimSpace(str) == "" 77 | } 78 | 79 | // ValidateEmail check if the string is an email. 80 | func ValidateEmail(str string) bool { 81 | return rxEmail.MatchString(str) 82 | } 83 | 84 | // ValidateAlpha check if the string may be only contains letters (a-zA-Z). Empty string is valid. 85 | func ValidateAlpha(str string) bool { 86 | if IsNull(str) { 87 | return true 88 | } 89 | return rxAlpha.MatchString(str) 90 | } 91 | 92 | // ValidateAlphaNum check if the string may be only contains letters and numbers. Empty string is valid. 93 | func ValidateAlphaNum(str string) bool { 94 | if IsNull(str) { 95 | return true 96 | } 97 | return rxAlphaNum.MatchString(str) 98 | } 99 | 100 | // ValidateAlphaDash check if the string may be only contains letters, numbers, dashes and underscores. Empty string is valid. 101 | func ValidateAlphaDash(str string) bool { 102 | if IsNull(str) { 103 | return true 104 | } 105 | return rxAlphaDash.MatchString(str) 106 | } 107 | 108 | // ValidateAlphaUnicode check if the string may be only contains letters (a-zA-Z). Empty string is valid. 109 | func ValidateAlphaUnicode(str string) bool { 110 | if IsNull(str) { 111 | return true 112 | } 113 | return rxAlphaUnicode.MatchString(str) 114 | } 115 | 116 | // ValidateAlphaNumUnicode check if the string may be only contains letters and numbers. Empty string is valid. 117 | func ValidateAlphaNumUnicode(str string) bool { 118 | if IsNull(str) { 119 | return true 120 | } 121 | return rxAlphaNumUnicode.MatchString(str) 122 | } 123 | 124 | // ValidateAlphaDashUnicode check if the string may be only contains letters, numbers, dashes and underscores. Empty string is valid. 125 | func ValidateAlphaDashUnicode(str string) bool { 126 | if IsNull(str) { 127 | return true 128 | } 129 | return rxAlphaDashUnicode.MatchString(str) 130 | } 131 | 132 | // ValidateIP check if the string is an ip address. 133 | func ValidateIP(v string) bool { 134 | ip := net.ParseIP(v) 135 | return ip != nil 136 | } 137 | 138 | // ValidateIPv4 check if the string is an ipv4 address. 139 | func ValidateIPv4(v string) bool { 140 | ip := net.ParseIP(v) 141 | return ip != nil && ip.To4() != nil 142 | } 143 | 144 | // ValidateIPv6 check if the string is an ipv6 address. 145 | func ValidateIPv6(v string) bool { 146 | ip := net.ParseIP(v) 147 | return ip != nil && ip.To4() == nil 148 | } 149 | 150 | // ValidateUUID3 check if the string is an uuid3. 151 | func ValidateUUID3(str string) bool { 152 | if IsNull(str) { 153 | return true 154 | } 155 | return rxUUID3.MatchString(str) 156 | } 157 | 158 | // ValidateUUID4 check if the string is an uuid4. 159 | func ValidateUUID4(str string) bool { 160 | if IsNull(str) { 161 | return true 162 | } 163 | return rxUUID4.MatchString(str) 164 | } 165 | 166 | // ValidateUUID5 check if the string is an uuid5. 167 | func ValidateUUID5(str string) bool { 168 | if IsNull(str) { 169 | return true 170 | } 171 | return rxUUID5.MatchString(str) 172 | } 173 | 174 | // ValidateUUID check if the string is an uuid. 175 | func ValidateUUID(str string) bool { 176 | if IsNull(str) { 177 | return true 178 | } 179 | return rxUUID.MatchString(str) 180 | } 181 | 182 | // ValidateURL check if the string is an URL. 183 | func ValidateURL(str string) bool { 184 | var i int 185 | 186 | if IsNull(str) { 187 | return true 188 | } 189 | 190 | if i = strings.Index(str, "#"); i > -1 { 191 | str = str[:i] 192 | } 193 | 194 | url, err := url.ParseRequestURI(str) 195 | if err != nil || url.Scheme == "" { 196 | return false 197 | } 198 | 199 | return true 200 | } 201 | -------------------------------------------------------------------------------- /lang/zh_CN/zh_CN.go: -------------------------------------------------------------------------------- 1 | package zh_CN 2 | 3 | // MessageMap is a map of string, that can be used as error message for ValidateStruct function. 4 | var MessageMap = map[string]string{ 5 | "accepted": "{{.Attribute}} 必须接受.", 6 | "activeUrl": "{{.Attribute}} 必须是一个合法的 URL. ", 7 | "after": "{{.Attribute}} 必须是 {{.Date}} 之後的一个日期.", 8 | "afterOrEqual": "{{.Attribute}} 必须是 {{.Date}} 之後或相同的一个日期.", 9 | "alpha": "{{.Attribute}} 只能包含字母.", 10 | "alphaDash": "{{.Attribute}} 只能包含字母,数字,\"-\",\"_\".", 11 | "alphaNum": "{{.Attribute}} 只能包含字母和数字.", 12 | "alphaUnicode": "{{.Attribute}} 只能包含字母.", 13 | "alphaDashUnicode": "{{.Attribute}} 只能包含字母,数字,\"-\",\"_\".", 14 | "alphaNumUnicode": "{{.Attribute}} 只能包含字母和数字.", 15 | "array": "{{.Attribute}} 必须是一个数组.", 16 | "before": "{{.Attribute}} 必须是 {{.Date}} 之前的一个日期.", 17 | "beforeOrEqual": "{{.Attribute}} 必须是 {{.Date}} 之前或相同的一个日期.", 18 | "between.numeric": "{{.Attribute}} 必须在 {{.Min}} 到 {{.Max}} 之间.", 19 | "between.file": "{{.Attribute}} 必须在 {{.Min}} 到 {{.Max}} KB 之间.", 20 | "between.string": "{{.Attribute}} 必须在 {{.Min}} 到 {{.Max}} 个字符之间.", 21 | "between.array": "{{.Attribute}} 必须在 {{.Min}} 到 {{.Max}} 项之间.", 22 | "boolean": "{{.Attribute}} 项必须是 true 或 false.", 23 | "confirmed": "{{.Attribute}} 的确认不符合.", 24 | "date": "{{.Attribute}} 不是一个有效的日期.", 25 | "dateFormat": "{{.Attribute}} 与 {{.Format}} 不匹配.", 26 | "different": "{{.Attribute}} 和 {{.Other}} 必须不相同.", 27 | "digits": "{{.Attribute}} 必须是 {{.Digits}} 位数.", 28 | "digitsBetween": "{{.Attribute}} 必须在 {{.Min}} 到 {{.Max}} 位数之间.", 29 | "dimensions": "{{.Attribute}} 的图像尺寸无效.", 30 | "distinct": "{{.Attribute}} 项有一个重复的值.", 31 | "email": "{{.Attribute}} 必须是一个合法的电子邮件地址.", 32 | "exists": "选定的 {{.Attribute}} 是无效的.", 33 | "file": "{{.Attribute}} 必须是一个档案.", 34 | "filled": "{{.Attribute}} 项必须输入一个值.", 35 | "gt.numeric": "{{.Attribute}} 必须大於 {{.Value}}.", 36 | "gt.file": "{{.Attribute}} 必须大於 {{.Value}} (千字节).", 37 | "gt.string": "{{.Attribute}} 必须大於 {{.Value}} 字符.", 38 | "gt.array": "{{.Attribute}} 必须大於 {{.Value}} 项.", 39 | "gte.numeric": "{{.Attribute}} 必须大於或等於 {{.Value}}.", 40 | "gte.file": "{{.Attribute}} 必须大於或等於 {{.Value}} (千字节).", 41 | "gte.string": "{{.Attribute}} 必须大於或等於 {{.Value}} 字符.", 42 | "gte.array": "{{.Attribute}} 至少有 {{.Value}} 项.", 43 | "image": "{{.Attribute}} 必须是一个图像.", 44 | "in": "选定的 {{.Attribute}} 是无效的.", 45 | "inArray": "{{.Attribute}} 项不存在於 {{.Other}}.", 46 | "integer": "{{.Attribute}} 必须是一个整数.", 47 | "ip": "{{.Attribute}} 必须是一个有效的 IP 地址.", 48 | "ipv4": "{{.Attribute}} 必须是一个有效的 IPv4 地址.", 49 | "ipv6": "{{.Attribute}} 必须是一个有效的 IPv6 地址.", 50 | "json": "{{.Attribute}} 必须是一个有效的 JSON 字串.", 51 | "lt.numeric": "{{.Attribute}} 必须少於 {{.Value}}.", 52 | "lt.file": "{{.Attribute}} 必须少於 {{.Value}} (千字节).", 53 | "lt.string": "{{.Attribute}} 必须少於 {{.Value}} 字符.", 54 | "lt.array": "{{.Attribute}} 必须少於 {{.Value}} 项.", 55 | "lte.numeric": "{{.Attribute}} 必须少於或等於 {{.Value}}.", 56 | "lte.file": "{{.Attribute}} 必须少於或等於 {{.Value}} (千字节).", 57 | "lte.string": "{{.Attribute}} 必须少於或等於 {{.Value}} 字符.", 58 | "lte.array": "{{.Attribute}} 不能大於 {{.Value}} 项.", 59 | "max.numeric": "{{.Attribute}} 不能大於 {{.Max}}.", 60 | "max.file": "{{.Attribute}} 不能大於 {{.Max}} (千字节).", 61 | "max.string": "{{.Attribute}} 不能大於 {{.Max}} 字符..", 62 | "max.array": "{{.Attribute}} 不能大於 {{.Max}} 项.", 63 | "mimes": "{{.Attribute}} 必须是 {{.Values}} 的档案类型.", 64 | "mimetypes": "{{.Attribute}} 必须是 {{.Values}} 的档案类型.", 65 | "min.numeric": "{{.Attribute}} 的最小长度为 {{.Min}} 位.", 66 | "min.file": "{{.Attribute}} 大小至少为 {{.Min}} KB.", 67 | "min.string": "{{.Attribute}} 的最小长度为 {{.Min}} 字符.", 68 | "min.array": "{{.Attribute}} 至少有 {{.Min}} 项.", 69 | "notIn": "选定的 {{.Attribute}} 是无效的.", 70 | "notRegex": "无效的 {{.Attribute}} 格式.", 71 | "numeric": "{{.Attribute}} 必须是一个数字.", 72 | "int": "{{.Attribute}} 必须是一个整数.", 73 | "float": "{{.Attribute}} 必须是一个浮点数.", 74 | "present": "{{.Attribute}} 必须存在.", 75 | "regex": "无效的 {{.Attribute}} 格式.", 76 | "required": "{{.Attribute}} 字段是必须的.", 77 | "requiredIf": "{{.Attribute}} 字段是必须的当 {{.Other}} 是 {{.Value}}.", 78 | "requiredUnless": "{{.Attribute}} 必须输入,除非 {{.Other}} 存在於 {{.Values}}.", 79 | "requiredWith": "当 {{.Values}} 存在时, {{.Attribute}} 必须输入.", 80 | "requiredWithAll": "当 {{.Values}} 存在时, {{.Attribute}} 必须输入.", 81 | "requiredWithout": "当 {{.Values}} 不存在时, {{.Attribute}} 必须输入.", 82 | "requiredWithoutAll": "当 {{.Values}} 不存在时, {{.Attribute}} 必须输入.", 83 | "same": "{{.Attribute}} 和 {{.Other}} 必须匹配.", 84 | "size.numeric": "{{.Attribute}} 必须是 {{.Size}}.", 85 | "size.file": "{{.Attribute}} 必须是 {{.Size}} (千字节).", 86 | "size.string": "{{.Attribute}} 必须是 {{.Size}} 字符.", 87 | "size.array": "{{.Attribute}} 必须包含 {{.Size}}.", 88 | "string": "{{.Attribute}} 必须是一串字符.", 89 | "timezone": "{{.Attribute}} 必须是一个有效的区域.", 90 | "unique": "{{.Attribute}} 已经被采取.", 91 | "uploaded": "{{.Attribute}} 无法上传.", 92 | "url": "{{.Attribute}} 格式无效.", 93 | "uuid3": "{{.Attribute}} 格式无效.", 94 | "uuid4": "{{.Attribute}} 格式无效.", 95 | "uuid5": "{{.Attribute}} 格式无效.", 96 | "uuid": "{{.Attribute}} 格式无效.", 97 | } 98 | -------------------------------------------------------------------------------- /lang/zh_HK/zh_HK.go: -------------------------------------------------------------------------------- 1 | package zh_HK 2 | 3 | // MessageMap is a map of string, that can be used as error message for ValidateStruct function. 4 | var MessageMap = map[string]string{ 5 | "accepted": "{{.Attribute}} 必須接受.", 6 | "activeUrl": "{{.Attribute}} 必須是一個合法的 URL. ", 7 | "after": "{{.Attribute}} 必須是 {{.Date}} 之後的一個日期.", 8 | "afterOrEqual": "{{.Attribute}} 必須是 {{.Date}} 之後或相同的一個日期.", 9 | "alpha": "{{.Attribute}} 只能包含字母.", 10 | "alphaDash": "{{.Attribute}} 只能包含字母,數字,\"-\",\"_\".", 11 | "alphaNum": "{{.Attribute}} 只能包含字母和數字.", 12 | "alphaUnicode": "{{.Attribute}} 只能包含字母.", 13 | "alphaDashUnicode": "{{.Attribute}} 只能包含字母,數字,\"-\",\"_\".", 14 | "alphaNumUnicode": "{{.Attribute}} 只能包含字母和數字.", 15 | "array": "{{.Attribute}} 必須是一個數組.", 16 | "before": "{{.Attribute}} 必須是 {{.Date}} 之前的一個日期.", 17 | "beforeOrEqual": "{{.Attribute}} 必須是 {{.Date}} 之前或相同的一個日期.", 18 | "between.numeric": "{{.Attribute}} 必須在 {{.Min}} 到 {{.Max}} 之間.", 19 | "between.file": "{{.Attribute}} 必須在 {{.Min}} 到 {{.Max}} KB 之間.", 20 | "between.string": "{{.Attribute}} 必須在 {{.Min}} 到 {{.Max}} 個字符之間.", 21 | "between.array": "{{.Attribute}} 必須在 {{.Min}} 到 {{.Max}} 項之間.", 22 | "boolean": "{{.Attribute}} 項必須是 true 或 false.", 23 | "confirmed": "{{.Attribute}} 的確認不符合.", 24 | "date": "{{.Attribute}} 不是一個有效的日期.", 25 | "dateFormat": "{{.Attribute}} 與 {{.Format}} 不匹配.", 26 | "different": "{{.Attribute}} 和 {{.Other}} 必須不相同.", 27 | "digits": "{{.Attribute}} 必須是 {{.Digits}} 位數.", 28 | "digitsBetween": "{{.Attribute}} 必須在 {{.Min}} 到 {{.Max}} 位數之間.", 29 | "dimensions": "{{.Attribute}} 的圖像尺寸無效.", 30 | "distinct": "{{.Attribute}} 項有一個重復的值.", 31 | "email": "{{.Attribute}} 必須是一個合法的電子郵件地址.", 32 | "exists": "選定的 {{.Attribute}} 是無效的.", 33 | "file": "{{.Attribute}} 必須是一個檔案.", 34 | "filled": "{{.Attribute}} 項必須輸入一個值.", 35 | "gt.numeric": "{{.Attribute}} 必須大於 {{.Value}}.", 36 | "gt.file": "{{.Attribute}} 必須大於 {{.Value}} (千字節).", 37 | "gt.string": "{{.Attribute}} 必須大於 {{.Value}} 字符.", 38 | "gt.array": "{{.Attribute}} 必須大於 {{.Value}} 項.", 39 | "gte.numeric": "{{.Attribute}} 必須大於或等於 {{.Value}}.", 40 | "gte.file": "{{.Attribute}} 必須大於或等於 {{.Value}} (千字節).", 41 | "gte.string": "{{.Attribute}} 必須大於或等於 {{.Value}} 字符.", 42 | "gte.array": "{{.Attribute}} 至少有 {{.Value}} 項.", 43 | "image": "{{.Attribute}} 必須是一個圖像.", 44 | "in": "選定的 {{.Attribute}} 是無效的.", 45 | "inArray": "{{.Attribute}} 項不存在於 {{.Other}}.", 46 | "integer": "{{.Attribute}} 必須是一個整數.", 47 | "ip": "{{.Attribute}} 必須是一個有效的 IP 地址.", 48 | "ipv4": "{{.Attribute}} 必須是一個有效的 IPv4 地址.", 49 | "ipv6": "{{.Attribute}} 必須是一個有效的 IPv6 地址.", 50 | "json": "{{.Attribute}} 必須是一個有效的 JSON 字串.", 51 | "lt.numeric": "{{.Attribute}} 必須少於 {{.Value}}.", 52 | "lt.file": "{{.Attribute}} 必須少於 {{.Value}} (千字節).", 53 | "lt.string": "{{.Attribute}} 必須少於 {{.Value}} 字符.", 54 | "lt.array": "{{.Attribute}} 必須少於 {{.Value}} 項.", 55 | "lte.numeric": "{{.Attribute}} 必須少於或等於 {{.Value}}.", 56 | "lte.file": "{{.Attribute}} 必須少於或等於 {{.Value}} (千字節).", 57 | "lte.string": "{{.Attribute}} 必須少於或等於 {{.Value}} 字符.", 58 | "lte.array": "{{.Attribute}} 不能大於 {{.Value}} 項.", 59 | "max.numeric": "{{.Attribute}} 不能大於 {{.Max}}.", 60 | "max.file": "{{.Attribute}} 不能大於 {{.Max}} (千字節).", 61 | "max.string": "{{.Attribute}} 不能大於 {{.Max}} 字符..", 62 | "max.array": "{{.Attribute}} 不能大於 {{.Max}} 項.", 63 | "mimes": "{{.Attribute}} 必須是 {{.Values}} 的檔案類型.", 64 | "mimetypes": "{{.Attribute}} 必須是 {{.Values}} 的檔案類型.", 65 | "min.numeric": "{{.Attribute}} 的最小長度為 {{.Min}} 位.", 66 | "min.file": "{{.Attribute}} 大小至少為 {{.Min}} KB.", 67 | "min.string": "{{.Attribute}} 的最小長度為 {{.Min}} 字符.", 68 | "min.array": "{{.Attribute}} 至少有 {{.Min}} 項.", 69 | "notIn": "選定的 {{.Attribute}} 是無效的.", 70 | "notRegex": "無效的 {{.Attribute}} 格式.", 71 | "numeric": "{{.Attribute}} 必須是一個數字.", 72 | "int": "{{.Attribute}} 必須是一個整數.", 73 | "float": "{{.Attribute}} 必须是一個浮點數.", 74 | "present": "{{.Attribute}} 必須存在.", 75 | "regex": "無效的 {{.Attribute}} 格式.", 76 | "required": "{{.Attribute}} 字段是必須的.", 77 | "requiredIf": "{{.Attribute}} 字段是必須的當 {{.Other}} 是 {{.Value}}.", 78 | "requiredUnless": "{{.Attribute}} 必須輸入,除非 {{.Other}} 存在於 {{.Values}}.", 79 | "requiredWith": "當 {{.Values}} 存在時, {{.Attribute}} 必須輸入.", 80 | "requiredWithAll": "當 {{.Values}} 存在時, {{.Attribute}} 必須輸入.", 81 | "requiredWithout": "當 {{.Values}} 不存在時, {{.Attribute}} 必須輸入.", 82 | "requiredWithoutAll": "當 {{.Values}} 不存在時, {{.Attribute}} 必須輸入.", 83 | "same": "{{.Attribute}} 和 {{.Other}} 必須匹配.", 84 | "size.numeric": "{{.Attribute}} 必須是 {{.Size}}.", 85 | "size.file": "{{.Attribute}} 必須是 {{.Size}} (千字節).", 86 | "size.string": "{{.Attribute}} 必須是 {{.Size}} 字符.", 87 | "size.array": "{{.Attribute}} 必須包含 {{.Size}}.", 88 | "string": "{{.Attribute}} 必須是一串字符.", 89 | "timezone": "{{.Attribute}} 必須是一個有效的區域.", 90 | "unique": "{{.Attribute}} 已經被采取.", 91 | "uploaded": "{{.Attribute}} 無法上傳.", 92 | "url": "{{.Attribute}} 格式無效.", 93 | "uuid3": "{{.Attribute}} 格式无效.", 94 | "uuid4": "{{.Attribute}} 格式无效.", 95 | "uuid5": "{{.Attribute}} 格式无效.", 96 | "uuid": "{{.Attribute}} 格式无效.", 97 | } 98 | -------------------------------------------------------------------------------- /translator_test.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestNewTranslator(t *testing.T) { 9 | translator := NewTranslator() 10 | if translator == nil { 11 | t.Error("Expected NewTranslator to return a non-nil translator") 12 | return 13 | } 14 | if translator.messages == nil { 15 | t.Error("Expected translator.messages to be initialized") 16 | } 17 | if translator.attributes == nil { 18 | t.Error("Expected translator.attributes to be initialized") 19 | } 20 | if translator.customMessage == nil { 21 | t.Error("Expected translator.customMessage to be initialized") 22 | } 23 | } 24 | 25 | func TestSetMessage(t *testing.T) { 26 | translator := NewTranslator() 27 | messages := Translate{"required": "Field is required"} 28 | translator.SetMessage("en", messages) 29 | 30 | if translator.messages["en"] == nil { 31 | t.Error("Expected messages to be set for 'en' language") 32 | } 33 | if translator.messages["en"]["required"] != "Field is required" { 34 | t.Error("Expected message to be set correctly") 35 | } 36 | } 37 | 38 | func TestLoadMessage(t *testing.T) { 39 | translator := NewTranslator() 40 | messages := Translate{"required": "Field is required"} 41 | translator.SetMessage("en", messages) 42 | 43 | loaded := translator.LoadMessage("en") 44 | if loaded["required"] != "Field is required" { 45 | t.Error("Expected loaded message to match set message") 46 | } 47 | 48 | // Test loading non-existent language 49 | empty := translator.LoadMessage("fr") 50 | if empty != nil { 51 | t.Error("Expected nil for non-existent language") 52 | } 53 | } 54 | 55 | func TestSetAttributes(t *testing.T) { 56 | translator := NewTranslator() 57 | attributes := Translate{"User.Name": "Full Name"} 58 | translator.SetAttributes("en", attributes) 59 | 60 | if translator.attributes["en"] == nil { 61 | t.Error("Expected attributes to be set for 'en' language") 62 | } 63 | if translator.attributes["en"]["User.Name"] != "Full Name" { 64 | t.Error("Expected attribute to be set correctly") 65 | } 66 | } 67 | 68 | func TestTrans(t *testing.T) { 69 | translator := NewTranslator() 70 | 71 | // Set up messages 72 | messages := Translate{ 73 | "required": "The {{.Attribute}} field is required", 74 | "email": "The {{.Attribute}} field must be a valid email", 75 | } 76 | translator.SetMessage("en", messages) 77 | 78 | // Set up attributes 79 | attributes := Translate{"User.Email": "Email Address"} 80 | translator.SetAttributes("en", attributes) 81 | 82 | // Create test field error 83 | fieldError := &FieldError{ 84 | Name: "email", 85 | StructName: "User.Email", 86 | MessageName: "required", 87 | Attribute: "email", 88 | } 89 | 90 | errors := Errors{fieldError} 91 | translatedErrors := translator.Trans(errors, "en") 92 | 93 | if len(translatedErrors) != 1 { 94 | t.Error("Expected one translated error") 95 | } 96 | 97 | translated := translatedErrors[0].(*FieldError) 98 | if translated.Message != "The Email Address field is required" { 99 | t.Errorf("Expected 'The Email Address field is required', got '%s'", translated.Message) 100 | } 101 | } 102 | 103 | func TestTransWithCustomMessage(t *testing.T) { 104 | translator := NewTranslator() 105 | 106 | // Set up custom message 107 | customMessage := Translate{"email.required": "Email is mandatory"} 108 | translator.customMessage["en"] = customMessage 109 | 110 | // Create test field error 111 | fieldError := &FieldError{ 112 | Name: "email", 113 | MessageName: "required", 114 | Attribute: "email", 115 | } 116 | 117 | errors := Errors{fieldError} 118 | translatedErrors := translator.Trans(errors, "en") 119 | 120 | translated := translatedErrors[0].(*FieldError) 121 | if translated.Message != "Email is mandatory" { 122 | t.Errorf("Expected 'Email is mandatory', got '%s'", translated.Message) 123 | } 124 | } 125 | 126 | func TestTransWithMessageParameters(t *testing.T) { 127 | translator := NewTranslator() 128 | 129 | // Set up messages with parameters 130 | messages := Translate{ 131 | "between": "The {{.Attribute}} field must be between {{.Min}} and {{.Max}}", 132 | } 133 | translator.SetMessage("en", messages) 134 | 135 | // Create test field error with parameters 136 | fieldError := &FieldError{ 137 | Name: "age", 138 | MessageName: "between", 139 | Attribute: "age", 140 | MessageParameters: MessageParameters{ 141 | {Key: "Min", Value: "18"}, 142 | {Key: "Max", Value: "65"}, 143 | }, 144 | } 145 | 146 | errors := Errors{fieldError} 147 | translatedErrors := translator.Trans(errors, "en") 148 | 149 | translated := translatedErrors[0].(*FieldError) 150 | expected := "The age field must be between 18 and 65" 151 | if translated.Message != expected { 152 | t.Errorf("Expected '%s', got '%s'", expected, translated.Message) 153 | } 154 | } 155 | 156 | func TestTransWithDefaultAttribute(t *testing.T) { 157 | translator := NewTranslator() 158 | 159 | // Set up messages 160 | messages := Translate{"required": "The {{.Attribute}} field is required"} 161 | translator.SetMessage("en", messages) 162 | 163 | // Create test field error with default attribute 164 | fieldError := &FieldError{ 165 | Name: "user_name", 166 | MessageName: "required", 167 | Attribute: "user_name", 168 | DefaultAttribute: "User Name", 169 | } 170 | 171 | errors := Errors{fieldError} 172 | translatedErrors := translator.Trans(errors, "en") 173 | 174 | translated := translatedErrors[0].(*FieldError) 175 | if translated.Message != "The User Name field is required" { 176 | t.Errorf("Expected 'The User Name field is required', got '%s'", translated.Message) 177 | } 178 | } 179 | 180 | func TestTransWithNonFieldError(t *testing.T) { 181 | translator := NewTranslator() 182 | 183 | // Create error that's not a FieldError 184 | genericError := &struct{ error }{fmt.Errorf("generic error")} 185 | 186 | errors := Errors{genericError} 187 | translatedErrors := translator.Trans(errors, "en") 188 | 189 | // Should not panic and should return original errors 190 | if len(translatedErrors) != 1 { 191 | t.Error("Expected one error") 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /patterns.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import "regexp" 4 | 5 | // Basic regular expressions for validating strings 6 | const ( 7 | Email string = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" 8 | Alpha string = "^[a-zA-Z]+$" 9 | AlphaNum string = "^[a-zA-Z0-9]+$" 10 | AlphaDash string = "^[a-zA-Z_-]+$" 11 | AlphaUnicode string = "^[\\p{L}]+$" 12 | AlphaNumUnicode string = "^[\\p{L}\\p{M}\\p{N}]+$" 13 | AlphaDashUnicode string = "^[\\p{L}\\p{M}\\p{N}_-]+$" 14 | Numeric string = "^\\d+$" 15 | Int string = "^(?:[-+]?(?:0|[1-9][0-9]*))$" 16 | Float string = "^(?:[-+]?(?:[0-9]+))?(?:\\.[0-9]*)?(?:[eE][\\+\\-]?(?:[0-9]+))?$" 17 | Hexadecimal string = "^[0-9a-fA-F]+$" 18 | HexColor string = "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" 19 | RGBColor string = "^rgb\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5]))\\s*\\)$" 20 | RGBAColor string = "^rgba\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:(?:0\\.[0-9]*)|[01]))\\s*\\)$" 21 | HSLColor string = "^hsl\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*\\)$" 22 | HSLAColor string = "^hsla\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0\\.[0-9]*)|[01])\\s*\\)$" 23 | UUID3 string = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$" 24 | UUID4 string = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" 25 | UUID5 string = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" 26 | UUID string = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" 27 | CreditCard string = "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3[0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})$" 28 | ISBN10 string = "^(?:[0-9]{9}X|[0-9]{10})$" 29 | ISBN13 string = "^(?:97[89][0-9]{10})$" 30 | IP string = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))` 31 | URLSchema string = `((ftp|tcp|udp|wss?|https?):\/\/)` 32 | URLUsername string = `(\S+(:\S*)?@)` 33 | URLPath string = `((\/|\?|#)[^\s]*)` 34 | URLPort string = `(:(\d{1,5}))` 35 | URLIP string = `([1-9]\d?|1\d\d|2[01]\d|22[0-3]|24\d|25[0-5])(\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-5]))` 36 | URLSubdomain string = `((www\.)|([a-zA-Z0-9]+([-_\.]?[a-zA-Z0-9])*[a-zA-Z0-9]\.[a-zA-Z0-9]+))` 37 | URL = `^` + URLSchema + `?` + URLUsername + `?` + `((` + URLIP + `|(\[` + IP + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + URLPort + `?` + URLPath + `?$` 38 | ) 39 | 40 | // Used by IsFilePath func 41 | const ( 42 | // Unknown is unresolved OS type 43 | Unknown = iota 44 | // Win is Windows type 45 | Win 46 | // Unix is *nix OS types 47 | Unix 48 | ) 49 | 50 | //nolint:unused // Regex patterns kept for potential future use 51 | var ( 52 | rxEmail = regexp.MustCompile(Email) 53 | rxCreditCard = regexp.MustCompile(CreditCard) 54 | rxISBN10 = regexp.MustCompile(ISBN10) 55 | rxISBN13 = regexp.MustCompile(ISBN13) 56 | rxUUID3 = regexp.MustCompile(UUID3) 57 | rxUUID4 = regexp.MustCompile(UUID4) 58 | rxUUID5 = regexp.MustCompile(UUID5) 59 | rxUUID = regexp.MustCompile(UUID) 60 | rxAlpha = regexp.MustCompile(Alpha) 61 | rxAlphaNum = regexp.MustCompile(AlphaNum) 62 | rxAlphaDash = regexp.MustCompile(AlphaDash) 63 | rxAlphaUnicode = regexp.MustCompile(AlphaUnicode) 64 | rxAlphaNumUnicode = regexp.MustCompile(AlphaNumUnicode) 65 | rxAlphaDashUnicode = regexp.MustCompile(AlphaDashUnicode) 66 | rxNumeric = regexp.MustCompile(Numeric) 67 | rxInt = regexp.MustCompile(Int) 68 | rxFloat = regexp.MustCompile(Float) 69 | rxHexadecimal = regexp.MustCompile(Hexadecimal) 70 | rxHexColor = regexp.MustCompile(HexColor) 71 | rxRGBColor = regexp.MustCompile(RGBColor) 72 | rxRGBAColor = regexp.MustCompile(RGBAColor) 73 | rxHSLColor = regexp.MustCompile(HSLColor) 74 | rxHSLAColor = regexp.MustCompile(HSLAColor) 75 | rxURL = regexp.MustCompile(URL) 76 | ) 77 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "testing" 7 | ) 8 | 9 | func TestErrorsError(t *testing.T) { 10 | // Test empty errors 11 | var errs Errors 12 | if errs.Error() != "" { 13 | t.Errorf("Expected empty string for empty errors, got %s", errs.Error()) 14 | } 15 | 16 | // Test single error 17 | errs = Errors{errors.New("single error")} 18 | if errs.Error() != "single error" { 19 | t.Errorf("Expected 'single error', got %s", errs.Error()) 20 | } 21 | 22 | // Test multiple errors 23 | errs = Errors{ 24 | errors.New("first error"), 25 | errors.New("second error"), 26 | errors.New("third error"), 27 | } 28 | expected := "first error\nsecond error\nthird error" 29 | if errs.Error() != expected { 30 | t.Errorf("Expected '%s', got '%s'", expected, errs.Error()) 31 | } 32 | } 33 | 34 | func TestErrorsErrors(t *testing.T) { 35 | err1 := errors.New("error 1") 36 | err2 := errors.New("error 2") 37 | errs := Errors{err1, err2} 38 | 39 | result := errs.Errors() 40 | if len(result) != 2 { 41 | t.Errorf("Expected 2 errors, got %d", len(result)) 42 | } 43 | if result[0] != err1 || result[1] != err2 { 44 | t.Error("Errors() did not return the original errors") 45 | } 46 | } 47 | 48 | func TestErrorsFieldErrors(t *testing.T) { 49 | fieldErr := &FieldError{Name: "email", Message: "invalid email"} 50 | genericErr := errors.New("generic error") 51 | errs := Errors{fieldErr, genericErr} 52 | 53 | fieldErrors := errs.FieldErrors() 54 | if len(fieldErrors) != 2 { 55 | t.Errorf("Expected 2 field errors, got %d", len(fieldErrors)) 56 | } 57 | 58 | if fieldErrors[0].Name != "email" { 59 | t.Error("First field error should be the original FieldError") 60 | } 61 | 62 | if fieldErrors[1].Message != "generic error" { 63 | t.Error("Second field error should be converted from generic error") 64 | } 65 | } 66 | 67 | func TestErrorsHasFieldError(t *testing.T) { 68 | fieldErr := &FieldError{Name: "email", Message: "invalid email"} 69 | errs := Errors{fieldErr} 70 | 71 | if !errs.HasFieldError("email") { 72 | t.Error("Expected HasFieldError to return true for existing field") 73 | } 74 | 75 | if errs.HasFieldError("name") { 76 | t.Error("Expected HasFieldError to return false for non-existing field") 77 | } 78 | } 79 | 80 | func TestErrorsGetFieldError(t *testing.T) { 81 | fieldErr := &FieldError{Name: "email", Message: "invalid email"} 82 | errs := Errors{fieldErr} 83 | 84 | result := errs.GetFieldError("email") 85 | if result == nil { 86 | t.Error("Expected GetFieldError to return the field error") 87 | } else if result.Name != "email" { 88 | t.Error("Expected field error name to be 'email'") 89 | } 90 | 91 | result = errs.GetFieldError("name") 92 | if result != nil { 93 | t.Error("Expected GetFieldError to return nil for non-existing field") 94 | } 95 | } 96 | 97 | func TestErrorsGroupByField(t *testing.T) { 98 | fieldErr1 := &FieldError{Name: "email", Message: "required"} 99 | fieldErr2 := &FieldError{Name: "email", Message: "invalid format"} 100 | fieldErr3 := &FieldError{Name: "name", Message: "required"} 101 | errs := Errors{fieldErr1, fieldErr2, fieldErr3} 102 | 103 | groups := errs.GroupByField() 104 | if len(groups) != 2 { 105 | t.Errorf("Expected 2 groups, got %d", len(groups)) 106 | } 107 | 108 | if len(groups["email"]) != 2 { 109 | t.Errorf("Expected 2 errors for email field, got %d", len(groups["email"])) 110 | } 111 | 112 | if len(groups["name"]) != 1 { 113 | t.Errorf("Expected 1 error for name field, got %d", len(groups["name"])) 114 | } 115 | } 116 | 117 | func TestErrorsMarshalJSON(t *testing.T) { 118 | // Test empty errors 119 | var errs Errors 120 | data, err := json.Marshal(errs) 121 | if err != nil { 122 | t.Errorf("Unexpected error: %v", err) 123 | } 124 | if string(data) != "[]" { 125 | t.Errorf("Expected '[]', got %s", string(data)) 126 | } 127 | 128 | // Test with field errors 129 | fieldErr := &FieldError{Name: "email", Message: "invalid email"} 130 | errs = Errors{fieldErr} 131 | 132 | data, err = json.Marshal(errs) 133 | if err != nil { 134 | t.Errorf("Unexpected error: %v", err) 135 | } 136 | 137 | var responses []ErrorResponse 138 | err = json.Unmarshal(data, &responses) 139 | if err != nil { 140 | t.Errorf("Failed to unmarshal JSON: %v", err) 141 | } 142 | 143 | if len(responses) != 1 { 144 | t.Errorf("Expected 1 response, got %d", len(responses)) 145 | } 146 | 147 | if responses[0].Message != "invalid email" || responses[0].Parameter != "email" { 148 | t.Error("JSON output does not match expected format") 149 | } 150 | } 151 | 152 | func TestFieldErrorError(t *testing.T) { 153 | // Test with custom message 154 | fe := &FieldError{Name: "email", Message: "Custom error message"} 155 | if fe.Error() != "Custom error message" { 156 | t.Errorf("Expected 'Custom error message', got %s", fe.Error()) 157 | } 158 | 159 | // Test with function error 160 | fe = &FieldError{Name: "email", FuncError: errors.New("function error")} 161 | expected := "validation failed for field 'email': function error" 162 | if fe.Error() != expected { 163 | t.Errorf("Expected '%s', got %s", expected, fe.Error()) 164 | } 165 | 166 | // Test with no message or function error 167 | fe = &FieldError{Name: "email"} 168 | expected = "validation failed for field 'email'" 169 | if fe.Error() != expected { 170 | t.Errorf("Expected '%s', got %s", expected, fe.Error()) 171 | } 172 | } 173 | 174 | func TestFieldErrorUnwrap(t *testing.T) { 175 | originalErr := errors.New("original error") 176 | fe := &FieldError{Name: "email", FuncError: originalErr} 177 | 178 | if fe.Unwrap() != originalErr { 179 | t.Error("Unwrap should return the original function error") 180 | } 181 | 182 | fe = &FieldError{Name: "email"} 183 | if fe.Unwrap() != nil { 184 | t.Error("Unwrap should return nil when no function error") 185 | } 186 | } 187 | 188 | func TestFieldErrorHasFuncError(t *testing.T) { 189 | fe := &FieldError{Name: "email", FuncError: errors.New("error")} 190 | if !fe.HasFuncError() { 191 | t.Error("Expected HasFuncError to return true") 192 | } 193 | 194 | fe = &FieldError{Name: "email"} 195 | if fe.HasFuncError() { 196 | t.Error("Expected HasFuncError to return false") 197 | } 198 | } 199 | 200 | func TestFieldErrorSetMessage(t *testing.T) { 201 | fe := &FieldError{Name: "email"} 202 | fe.SetMessage("New message") 203 | 204 | if fe.Message != "New message" { 205 | t.Errorf("Expected 'New message', got %s", fe.Message) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | // MessageMap is a map of string, that can be used as error message for ValidateStruct function. 4 | var MessageMap = map[string]string{ 5 | "accepted": "The {{.Attribute}} must be accepted.", 6 | "activeUrl": "The {{.Attribute}} is not a valid URL.", 7 | "after": "The {{.Attribute}} must be a date after {{.Date}}.", 8 | "afterOrEqual": "The {{.Attribute}} must be a date after or equal to {{.Date}}.", 9 | "alpha": "The {{.Attribute}} may only contain letters.", 10 | "alphaDash": "The {{.Attribute}} may only contain letters, numbers, dashes and underscores.", 11 | "alphaNum": "The {{.Attribute}} may only contain letters and numbers.", 12 | "array": "The {{.Attribute}} must be an array.", 13 | "before": "The {{.Attribute}} must be a date before {{.Date}}.", 14 | "beforeOrEqual": "The {{.Attribute}} must be a date before or equal to {{.Date}}.", 15 | "between.numeric": "The {{.Attribute}} must be between {{.Min}} and {{.Max}}.", 16 | "between.file": "The {{.Attribute}} must be between {{.Min}} and {{.Max}} kilobytes.", 17 | "between.string": "The {{.Attribute}} must be between {{.Min}} and {{.Max}} characters.", 18 | "between.array": "The {{.Attribute}} must have between {{.Min}} and {{.Max}} items.", 19 | "boolean": "The {{.Attribute}} field must be true or false.", 20 | "confirmed": "The {{.Attribute}} confirmation does not match.", 21 | "date": "The {{.Attribute}} is not a valid date.", 22 | "dateFormat": "The {{.Attribute}} does not match the format {{.Format}}.", 23 | "different": "The {{.Attribute}} and {{.Other}} must be different.", 24 | "digits": "The {{.Attribute}} must be {{.Digits}} digits.", 25 | "digitsBetween": "The {{.Attribute}} must be between {{.Min}} and {{.Max}} digits.", 26 | "dimensions": "The {{.Attribute}} has invalid image dimensions.", 27 | "distinct": "The {{.Attribute}} field has a duplicate value.", 28 | "email": "The {{.Attribute}} must be a valid email address.", 29 | "exists": "The selected {{.Attribute}} is invalid.", 30 | "file": "The {{.Attribute}} must be a file.", 31 | "filled": "The {{.Attribute}} field must have a value.", 32 | "gt.numeric": "The {{.Attribute}} must be greater than {{.Value}}.", 33 | "gt.file": "The {{.Attribute}} must be greater than {{.Value}} kilobytes.", 34 | "gt.string": "The {{.Attribute}} must be greater than {{.Value}} characters.", 35 | "gt.array": "The {{.Attribute}} must have greater than {{.Value}} items.", 36 | "gte.numeric": "The {{.Attribute}} must be greater than or equal {{.Value}}.", 37 | "gte.file": "The {{.Attribute}} must be greater than or equal {{.Value}} kilobytes.", 38 | "gte.string": "The {{.Attribute}} must be greater than or equal {{.Value}} characters.", 39 | "gte.array": "The {{.Attribute}} must have {{.Value}} items or more.", 40 | "image": "The {{.Attribute}} must be an image.", 41 | "in": "The selected {{.Attribute}} is invalid.", 42 | "inArray": "The {{.Attribute}} field does not exist in {{.Other}}.", 43 | "integer": "The {{.Attribute}} must be an integer.", 44 | "ip": "The {{.Attribute}} must be a valid IP address.", 45 | "ipv4": "The {{.Attribute}} must be a valid IPv4 address.", 46 | "ipv6": "The {{.Attribute}} must be a valid IPv6 address.", 47 | "json": "The {{.Attribute}} must be a valid JSON string.", 48 | "lt.numeric": "The {{.Attribute}} must be less than {{.Value}}.", 49 | "lt.file": "The {{.Attribute}} must be less than {{.Value}} kilobytes.", 50 | "lt.string": "The {{.Attribute}} must be less than {{.Value}} characters.", 51 | "lt.array": "The {{.Attribute}} must have less than {{.Value}} items.", 52 | "lte.numeric": "The {{.Attribute}} must be less than or equal {{.Value}}.", 53 | "lte.file": "The {{.Attribute}} must be less than or equal {{.Value}} kilobytes.", 54 | "lte.string": "The {{.Attribute}} must be less than or equal {{.Value}} characters.", 55 | "lte.array": "The {{.Attribute}} must not have more than {{.Value}} items.", 56 | "max.numeric": "The {{.Attribute}} may not be greater than {{.Max}}.", 57 | "max.file": "The {{.Attribute}} may not be greater than {{.Max}} kilobytes.", 58 | "max.string": "The {{.Attribute}} may not be greater than {{.Max}} characters.", 59 | "max.array": "The {{.Attribute}} may not have more than {{.Max}} items.", 60 | "mimes": "The {{.Attribute}} must be a file of type: {{.Values}}.", 61 | "mimetypes": "The {{.Attribute}} must be a file of type: {{.Values}}.", 62 | "min.numeric": "The {{.Attribute}} must be at least {{.Min}}.", 63 | "min.file": "The {{.Attribute}} must be at least {{.Min}} kilobytes.", 64 | "min.string": "The {{.Attribute}} must be at least {{.Min}} characters.", 65 | "min.array": "The {{.Attribute}} must have at least {{.Min}} items.", 66 | "notIn": "The selected {{.Attribute}} is invalid.", 67 | "notRegex": "The {{.Attribute}} format is invalid.", 68 | "numeric": "The {{.Attribute}} must be a number.", 69 | "present": "The {{.Attribute}} field must be present.", 70 | "regex": "The {{.Attribute}} format is invalid.", 71 | "required": "The {{.Attribute}} field is required.", 72 | "requiredIf": "The {{.Attribute}} field is required when {{.Other}} is {{.Value}}.", 73 | "requiredUnless": "The {{.Attribute}} field is required unless {{.Other}} is in {{.Values}}.", 74 | "requiredWith": "The {{.Attribute}} field is required when {{.Values}} is present.", 75 | "requiredWithAll": "The {{.Attribute}} field is required when {{.Values}} is present.", 76 | "requiredWithout": "The {{.Attribute}} field is required when {{.Values}} is not present.", 77 | "requiredWithoutAll": "The {{.Attribute}} field is required when none of {{.Values}} are present.", 78 | "same": "The {{.Attribute}} and {{.Other}} must match.", 79 | "size.numeric": "The {{.Attribute}} must be {{.Size}}.", 80 | "size.file": "The {{.Attribute}} must be {{.Size}} kilobytes.", 81 | "size.string": "The {{.Attribute}} must be {{.Size}} characters.", 82 | "size.array": "The {{.Attribute}} must contain {{.Size}} items.", 83 | "string": "The {{.Attribute}} must be a string.", 84 | "timezone": "The {{.Attribute}} must be a valid zone.", 85 | "unique": "The {{.Attribute}} has already been taken.", 86 | "uploaded": "The {{.Attribute}} failed to upload.", 87 | "uuid3": "The {{.Attribute}} format is invalid.", 88 | "uuid4": "The {{.Attribute}} format is invalid.", 89 | "uuid5": "The {{.Attribute}} format is invalid.", 90 | "uuid": "The {{.Attribute}} format is invalid.", 91 | } 92 | -------------------------------------------------------------------------------- /lang/en/en.go: -------------------------------------------------------------------------------- 1 | package en 2 | 3 | // MessageMap is a map of string, that can be used as error message for ValidateStruct function. 4 | var MessageMap = map[string]string{ 5 | "accepted": "The {{.Attribute}} must be accepted.", 6 | "activeUrl": "The {{.Attribute}} is not a valid URL.", 7 | "after": "The {{.Attribute}} must be a date after {{.Date}}.", 8 | "afterOrEqual": "The {{.Attribute}} must be a date after or equal to {{.Date}}.", 9 | "alpha": "The {{.Attribute}} may only contain letters.", 10 | "alphaDash": "The {{.Attribute}} may only contain letters, numbers, dashes and underscores.", 11 | "alphaNum": "The {{.Attribute}} may only contain letters and numbers.", 12 | "array": "The {{.Attribute}} must be an array.", 13 | "before": "The {{.Attribute}} must be a date before {{.Date}}.", 14 | "beforeOrEqual": "The {{.Attribute}} must be a date before or equal to {{.Date}}.", 15 | "between.numeric": "The {{.Attribute}} must be between {{.Min}} and {{.Max}}.", 16 | "between.file": "The {{.Attribute}} must be between {{.Min}} and {{.Max}} kilobytes.", 17 | "between.string": "The {{.Attribute}} must be between {{.Min}} and {{.Max}} characters.", 18 | "between.array": "The {{.Attribute}} must have between {{.Min}} and {{.Max}} items.", 19 | "boolean": "The {{.Attribute}} field must be true or false.", 20 | "confirmed": "The {{.Attribute}} confirmation does not match.", 21 | "date": "The {{.Attribute}} is not a valid date.", 22 | "dateFormat": "The {{.Attribute}} does not match the format {{.Format}}.", 23 | "different": "The {{.Attribute}} and {{.Other}} must be different.", 24 | "digits": "The {{.Attribute}} must be {{.Digits}} digits.", 25 | "digitsBetween": "The {{.Attribute}} must be between {{.Min}} and {{.Max}} digits.", 26 | "dimensions": "The {{.Attribute}} has invalid image dimensions.", 27 | "distinct": "The {{.Attribute}} field has a duplicate value.", 28 | "email": "The {{.Attribute}} must be a valid email address.", 29 | "exists": "The selected {{.Attribute}} is invalid.", 30 | "file": "The {{.Attribute}} must be a file.", 31 | "filled": "The {{.Attribute}} field must have a value.", 32 | "gt.numeric": "The {{.Attribute}} must be greater than {{.Value}}.", 33 | "gt.file": "The {{.Attribute}} must be greater than {{.Value}} kilobytes.", 34 | "gt.string": "The {{.Attribute}} must be greater than {{.Value}} characters.", 35 | "gt.array": "The {{.Attribute}} must have greater than {{.Value}} items.", 36 | "gte.numeric": "The {{.Attribute}} must be greater than or equal {{.Value}}.", 37 | "gte.file": "The {{.Attribute}} must be greater than or equal {{.Value}} kilobytes.", 38 | "gte.string": "The {{.Attribute}} must be greater than or equal {{.Value}} characters.", 39 | "gte.array": "The {{.Attribute}} must have {{.Value}} items or more.", 40 | "image": "The {{.Attribute}} must be an image.", 41 | "in": "The selected {{.Attribute}} is invalid.", 42 | "inArray": "The {{.Attribute}} field does not exist in {{.Other}}.", 43 | "integer": "The {{.Attribute}} must be an integer.", 44 | "ip": "The {{.Attribute}} must be a valid IP address.", 45 | "ipv4": "The {{.Attribute}} must be a valid IPv4 address.", 46 | "ipv6": "The {{.Attribute}} must be a valid IPv6 address.", 47 | "json": "The {{.Attribute}} must be a valid JSON string.", 48 | "lt.numeric": "The {{.Attribute}} must be less than {{.Value}}.", 49 | "lt.file": "The {{.Attribute}} must be less than {{.Value}} kilobytes.", 50 | "lt.string": "The {{.Attribute}} must be less than {{.Value}} characters.", 51 | "lt.array": "The {{.Attribute}} must have less than {{.Value}} items.", 52 | "lte.numeric": "The {{.Attribute}} must be less than or equal {{.Value}}.", 53 | "lte.file": "The {{.Attribute}} must be less than or equal {{.Value}} kilobytes.", 54 | "lte.string": "The {{.Attribute}} must be less than or equal {{.Value}} characters.", 55 | "lte.array": "The {{.Attribute}} must not have more than {{.Value}} items.", 56 | "max.numeric": "The {{.Attribute}} may not be greater than {{.Max}}.", 57 | "max.file": "The {{.Attribute}} may not be greater than {{.Max}} kilobytes.", 58 | "max.string": "The {{.Attribute}} may not be greater than {{.Max}} characters.", 59 | "max.array": "The {{.Attribute}} may not have more than {{.Max}} items.", 60 | "mimes": "The {{.Attribute}} must be a file of type: {{.Values}}.", 61 | "mimetypes": "The {{.Attribute}} must be a file of type: {{.Values}}.", 62 | "min.numeric": "The {{.Attribute}} must be at least {{.Min}}.", 63 | "min.file": "The {{.Attribute}} must be at least {{.Min}} kilobytes.", 64 | "min.string": "The {{.Attribute}} must be at least {{.Min}} characters.", 65 | "min.array": "The {{.Attribute}} must have at least {{.Min}} items.", 66 | "notIn": "The selected {{.Attribute}} is invalid.", 67 | "notRegex": "The {{.Attribute}} format is invalid.", 68 | "numeric": "The {{.Attribute}} must be a number.", 69 | "present": "The {{.Attribute}} field must be present.", 70 | "regex": "The {{.Attribute}} format is invalid.", 71 | "required": "The {{.Attribute}} field is required.", 72 | "requiredIf": "The {{.Attribute}} field is required when {{.Other}} is {{.Value}}.", 73 | "requiredUnless": "The {{.Attribute}} field is required unless {{.Other}} is in {{.Values}}.", 74 | "requiredWith": "The {{.Attribute}} field is required when {{.Values}} is present.", 75 | "requiredWithAll": "The {{.Attribute}} field is required when {{.Values}} is present.", 76 | "requiredWithout": "The {{.Attribute}} field is required when {{.Values}} is not present.", 77 | "requiredWithoutAll": "The {{.Attribute}} field is required when none of {{.Values}} are present.", 78 | "same": "The {{.Attribute}} and {{.Other}} must match.", 79 | "size.numeric": "The {{.Attribute}} must be {{.Size}}.", 80 | "size.file": "The {{.Attribute}} must be {{.Size}} kilobytes.", 81 | "size.string": "The {{.Attribute}} must be {{.Size}} characters.", 82 | "size.array": "The {{.Attribute}} must contain {{.Size}} items.", 83 | "string": "The {{.Attribute}} must be a string.", 84 | "timezone": "The {{.Attribute}} must be a valid zone.", 85 | "unique": "The {{.Attribute}} has already been taken.", 86 | "uploaded": "The {{.Attribute}} failed to upload.", 87 | "url": "The {{.Attribute}} format is invalid.", 88 | "uuid3": "The {{.Attribute}} format is invalid.", 89 | "uuid4": "The {{.Attribute}} format is invalid.", 90 | "uuid5": "The {{.Attribute}} format is invalid.", 91 | "uuid": "The {{.Attribute}} format is invalid.", 92 | } 93 | -------------------------------------------------------------------------------- /required_variants_test.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // Test struct for RequiredWith validation 8 | type RequiredWithTest struct { 9 | Field1 string `valid:"requiredWith=Field2"` 10 | Field2 string 11 | } 12 | 13 | // Test RequiredWith validation 14 | func TestRequiredWithValidation(t *testing.T) { 15 | tests := []struct { 16 | name string 17 | data RequiredWithTest 18 | expected bool 19 | }{ 20 | {"Both fields present - valid", RequiredWithTest{Field1: "value1", Field2: "value2"}, true}, 21 | {"Field2 present, Field1 missing - invalid", RequiredWithTest{Field2: "value2"}, false}, 22 | {"Field2 absent, Field1 absent - valid", RequiredWithTest{}, true}, 23 | {"Field2 absent, Field1 present - valid", RequiredWithTest{Field1: "value1"}, true}, 24 | } 25 | 26 | for _, test := range tests { 27 | t.Run(test.name, func(t *testing.T) { 28 | err := ValidateStruct(test.data) 29 | actual := err == nil 30 | if actual != test.expected { 31 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 32 | } 33 | }) 34 | } 35 | } 36 | 37 | // Test struct for RequiredWithAll validation 38 | type RequiredWithAllTest struct { 39 | Field1 string `valid:"requiredWithAll=Field2|Field3"` 40 | Field2 string 41 | Field3 string 42 | } 43 | 44 | // Test RequiredWithAll validation 45 | func TestRequiredWithAllValidation(t *testing.T) { 46 | tests := []struct { 47 | name string 48 | data RequiredWithAllTest 49 | expected bool 50 | }{ 51 | {"All fields present - valid", RequiredWithAllTest{Field1: "v1", Field2: "v2", Field3: "v3"}, true}, 52 | {"Field2 and Field3 present, Field1 missing - invalid", RequiredWithAllTest{Field2: "v2", Field3: "v3"}, false}, 53 | {"Only Field2 present - valid", RequiredWithAllTest{Field2: "v2"}, true}, 54 | {"Only Field3 present - valid", RequiredWithAllTest{Field3: "v3"}, true}, 55 | {"No fields present - valid", RequiredWithAllTest{}, true}, 56 | } 57 | 58 | for _, test := range tests { 59 | t.Run(test.name, func(t *testing.T) { 60 | err := ValidateStruct(test.data) 61 | actual := err == nil 62 | if actual != test.expected { 63 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 64 | } 65 | }) 66 | } 67 | } 68 | 69 | // Test struct for RequiredWithout validation 70 | type RequiredWithoutTest struct { 71 | Field1 string `valid:"requiredWithout=Field2"` 72 | Field2 string 73 | } 74 | 75 | // Test RequiredWithout validation 76 | func TestRequiredWithoutValidation(t *testing.T) { 77 | tests := []struct { 78 | name string 79 | data RequiredWithoutTest 80 | expected bool 81 | }{ 82 | {"Field2 absent, Field1 present - valid", RequiredWithoutTest{Field1: "value1"}, true}, 83 | {"Field2 absent, Field1 absent - invalid", RequiredWithoutTest{}, false}, 84 | {"Field2 present, Field1 present - valid", RequiredWithoutTest{Field1: "v1", Field2: "v2"}, true}, 85 | {"Field2 present, Field1 absent - valid", RequiredWithoutTest{Field2: "v2"}, true}, 86 | } 87 | 88 | for _, test := range tests { 89 | t.Run(test.name, func(t *testing.T) { 90 | err := ValidateStruct(test.data) 91 | actual := err == nil 92 | if actual != test.expected { 93 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 94 | } 95 | }) 96 | } 97 | } 98 | 99 | // Test struct for RequiredWithoutAll validation 100 | type RequiredWithoutAllTest struct { 101 | Field1 string `valid:"requiredWithoutAll=Field2|Field3"` 102 | Field2 string 103 | Field3 string 104 | } 105 | 106 | // Test RequiredWithoutAll validation 107 | func TestRequiredWithoutAllValidation(t *testing.T) { 108 | tests := []struct { 109 | name string 110 | data RequiredWithoutAllTest 111 | expected bool 112 | }{ 113 | {"All fields absent, Field1 present - valid", RequiredWithoutAllTest{Field1: "v1"}, true}, 114 | {"All fields absent, Field1 absent - invalid", RequiredWithoutAllTest{}, false}, 115 | {"Field2 present - valid", RequiredWithoutAllTest{Field2: "v2"}, true}, 116 | {"Field3 present - valid", RequiredWithoutAllTest{Field3: "v3"}, true}, 117 | {"Both Field2 and Field3 present - valid", RequiredWithoutAllTest{Field2: "v2", Field3: "v3"}, true}, 118 | } 119 | 120 | for _, test := range tests { 121 | t.Run(test.name, func(t *testing.T) { 122 | err := ValidateStruct(test.data) 123 | actual := err == nil 124 | if actual != test.expected { 125 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 126 | } 127 | }) 128 | } 129 | } 130 | 131 | // Test struct for RequiredUnless validation 132 | type RequiredUnlessTest struct { 133 | Field1 string `valid:"requiredUnless=Field2|exempt"` 134 | Field2 string 135 | } 136 | 137 | // Test RequiredUnless validation 138 | func TestRequiredUnlessValidation(t *testing.T) { 139 | tests := []struct { 140 | name string 141 | data RequiredUnlessTest 142 | expected bool 143 | }{ 144 | {"Field2 is exempt, Field1 absent - valid", RequiredUnlessTest{Field2: "exempt"}, true}, 145 | {"Field2 is exempt, Field1 present - valid", RequiredUnlessTest{Field1: "v1", Field2: "exempt"}, true}, 146 | {"Field2 not exempt, Field1 present - valid", RequiredUnlessTest{Field1: "v1", Field2: "other"}, true}, 147 | {"Field2 not exempt, Field1 absent - invalid", RequiredUnlessTest{Field2: "other"}, false}, 148 | {"Field2 absent, Field1 present - valid", RequiredUnlessTest{Field1: "v1"}, true}, 149 | {"Field2 absent, Field1 absent - invalid", RequiredUnlessTest{}, false}, 150 | } 151 | 152 | for _, test := range tests { 153 | t.Run(test.name, func(t *testing.T) { 154 | err := ValidateStruct(test.data) 155 | actual := err == nil 156 | if actual != test.expected { 157 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 158 | } 159 | }) 160 | } 161 | } 162 | 163 | // Test struct for RequiredIf with multiple conditions 164 | type RequiredIfAdvancedTest struct { 165 | Field1 string `valid:"requiredIf=Field2|active"` 166 | Field2 string 167 | Field3 string 168 | } 169 | 170 | // Test RequiredIf with multiple conditions 171 | func TestRequiredIfAdvancedValidation(t *testing.T) { 172 | tests := []struct { 173 | name string 174 | data RequiredIfAdvancedTest 175 | expected bool 176 | }{ 177 | {"Condition met, field present - valid", RequiredIfAdvancedTest{Field1: "v1", Field2: "active"}, true}, 178 | {"Condition met, field absent - invalid", RequiredIfAdvancedTest{Field2: "active"}, false}, 179 | {"Condition not met, field absent - valid", RequiredIfAdvancedTest{Field2: "inactive"}, true}, 180 | {"Condition not met, field present - valid", RequiredIfAdvancedTest{Field1: "v1", Field2: "inactive"}, true}, 181 | {"Multiple conditions - one met", RequiredIfAdvancedTest{Field1: "v1", Field2: "active", Field3: "disabled"}, true}, 182 | {"Multiple conditions - both met", RequiredIfAdvancedTest{Field1: "v1", Field2: "active", Field3: "enabled"}, true}, 183 | } 184 | 185 | for _, test := range tests { 186 | t.Run(test.name, func(t *testing.T) { 187 | err := ValidateStruct(test.data) 188 | actual := err == nil 189 | if actual != test.expected { 190 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 191 | } 192 | }) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /complex_struct_test.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // Test complex nested struct validation 8 | type ComplexNestedStruct struct { 9 | User UserInfo `valid:"required"` 10 | Config ConfigInfo `valid:"required"` 11 | Items []ItemInfo `valid:"required,distinct"` 12 | Meta interface{} // Interface field 13 | } 14 | 15 | type UserInfo struct { 16 | Name string `valid:"required,min=2"` 17 | Email string `valid:"required,email"` 18 | Age int `valid:"min=18,max=120"` 19 | Settings *UserSettings 20 | } 21 | 22 | type UserSettings struct { 23 | Theme string `valid:"required"` 24 | Language string `valid:"required,size=2"` 25 | } 26 | 27 | type ConfigInfo struct { 28 | Version string `valid:"required"` 29 | Debug bool 30 | Timeout int `valid:"gt=0,lt=3600"` 31 | } 32 | 33 | type ItemInfo struct { 34 | ID string `valid:"required"` 35 | Value string `valid:"min=1"` 36 | } 37 | 38 | // Test complex validation scenarios 39 | func TestComplexStructValidation(t *testing.T) { 40 | tests := []struct { 41 | name string 42 | data ComplexNestedStruct 43 | expected bool 44 | }{ 45 | { 46 | "Valid complex struct", 47 | ComplexNestedStruct{ 48 | User: UserInfo{ 49 | Name: "John Doe", 50 | Email: "john@example.com", 51 | Age: 25, 52 | Settings: &UserSettings{ 53 | Theme: "dark", 54 | Language: "en", 55 | }, 56 | }, 57 | Config: ConfigInfo{ 58 | Version: "1.0.0", 59 | Debug: true, 60 | Timeout: 30, 61 | }, 62 | Items: []ItemInfo{ 63 | {ID: "item1", Value: "value1"}, 64 | {ID: "item2", Value: "value2"}, 65 | }, 66 | Meta: map[string]interface{}{"key": "value"}, 67 | }, 68 | true, 69 | }, 70 | { 71 | "Invalid email", 72 | ComplexNestedStruct{ 73 | User: UserInfo{ 74 | Name: "John Doe", 75 | Email: "invalid-email", 76 | Age: 25, 77 | }, 78 | Config: ConfigInfo{ 79 | Version: "1.0.0", 80 | Timeout: 30, 81 | }, 82 | Items: []ItemInfo{ 83 | {ID: "item1", Value: "value1"}, 84 | }, 85 | }, 86 | false, 87 | }, 88 | { 89 | "Age below minimum", 90 | ComplexNestedStruct{ 91 | User: UserInfo{ 92 | Name: "John Doe", 93 | Email: "john@example.com", 94 | Age: 16, // Below minimum 95 | }, 96 | Config: ConfigInfo{ 97 | Version: "1.0.0", 98 | Timeout: 30, 99 | }, 100 | Items: []ItemInfo{ 101 | {ID: "item1", Value: "value1"}, 102 | }, 103 | }, 104 | false, 105 | }, 106 | { 107 | "Missing required fields", 108 | ComplexNestedStruct{ 109 | User: UserInfo{ 110 | Name: "", // Required field missing 111 | Age: 25, 112 | }, 113 | Config: ConfigInfo{ 114 | Timeout: 30, 115 | }, 116 | Items: []ItemInfo{}, 117 | }, 118 | false, 119 | }, 120 | } 121 | 122 | for _, test := range tests { 123 | t.Run(test.name, func(t *testing.T) { 124 | err := ValidateStruct(test.data) 125 | actual := err == nil 126 | if actual != test.expected { 127 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 128 | } 129 | }) 130 | } 131 | } 132 | 133 | // Test pointer field validation 134 | type PointerStruct struct { 135 | Required *string `valid:"required"` 136 | Optional *string 137 | Number *int `valid:"min=0"` 138 | } 139 | 140 | func TestPointerValidation(t *testing.T) { 141 | validString := "valid" 142 | invalidNumber := -5 143 | validNumber := 10 144 | 145 | tests := []struct { 146 | name string 147 | data PointerStruct 148 | expected bool 149 | }{ 150 | { 151 | "Valid pointers", 152 | PointerStruct{ 153 | Required: &validString, 154 | Number: &validNumber, 155 | }, 156 | true, 157 | }, 158 | { 159 | "Missing required pointer", 160 | PointerStruct{ 161 | Required: nil, 162 | Number: &validNumber, 163 | }, 164 | false, 165 | }, 166 | { 167 | "Invalid number value", 168 | PointerStruct{ 169 | Required: &validString, 170 | Number: &invalidNumber, 171 | }, 172 | false, 173 | }, 174 | } 175 | 176 | for _, test := range tests { 177 | t.Run(test.name, func(t *testing.T) { 178 | err := ValidateStruct(test.data) 179 | actual := err == nil 180 | if actual != test.expected { 181 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 182 | } 183 | }) 184 | } 185 | } 186 | 187 | // Test slice and map validation 188 | type CollectionStruct struct { 189 | StringSlice []string `valid:"required,min=1"` 190 | IntSlice []int `valid:"max=5"` 191 | StringMap map[string]string `valid:"required"` 192 | IntMap map[string]int `valid:"min=1,max=10"` 193 | } 194 | 195 | func TestCollectionValidation(t *testing.T) { 196 | tests := []struct { 197 | name string 198 | data CollectionStruct 199 | expected bool 200 | }{ 201 | { 202 | "Valid collections", 203 | CollectionStruct{ 204 | StringSlice: []string{"a", "b", "c"}, 205 | IntSlice: []int{1, 2, 3}, 206 | StringMap: map[string]string{"key": "value"}, 207 | IntMap: map[string]int{"count": 5}, 208 | }, 209 | true, 210 | }, 211 | { 212 | "Empty required slice", 213 | CollectionStruct{ 214 | StringSlice: []string{}, // Required but empty 215 | StringMap: map[string]string{"key": "value"}, 216 | IntMap: map[string]int{"count": 5}, 217 | }, 218 | false, 219 | }, 220 | { 221 | "Slice too large", 222 | CollectionStruct{ 223 | StringSlice: []string{"a", "b", "c"}, 224 | IntSlice: []int{1, 2, 3, 4, 5, 6}, // Max 5 225 | StringMap: map[string]string{"key": "value"}, 226 | IntMap: map[string]int{"count": 5}, 227 | }, 228 | false, 229 | }, 230 | { 231 | "Map too large", 232 | CollectionStruct{ 233 | StringSlice: []string{"a", "b", "c"}, 234 | IntSlice: []int{1, 2, 3}, 235 | StringMap: map[string]string{"key": "value"}, 236 | IntMap: map[string]int{ 237 | "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, 238 | "6": 6, "7": 7, "8": 8, "9": 9, "10": 10, "11": 11, // Max 10 239 | }, 240 | }, 241 | false, 242 | }, 243 | } 244 | 245 | for _, test := range tests { 246 | t.Run(test.name, func(t *testing.T) { 247 | err := ValidateStruct(test.data) 248 | actual := err == nil 249 | if actual != test.expected { 250 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 251 | } 252 | }) 253 | } 254 | } 255 | 256 | // Test omitempty functionality 257 | type OmitEmptyStruct struct { 258 | Required string `valid:"required"` 259 | Optional string `valid:"omitempty,min=3"` 260 | Number int `valid:"omitempty,gt=0"` 261 | } 262 | 263 | func TestOmitEmptyValidation(t *testing.T) { 264 | tests := []struct { 265 | name string 266 | data OmitEmptyStruct 267 | expected bool 268 | }{ 269 | { 270 | "Valid with optional fields", 271 | OmitEmptyStruct{ 272 | Required: "value", 273 | Optional: "test", 274 | Number: 5, 275 | }, 276 | true, 277 | }, 278 | { 279 | "Valid with empty optional fields", 280 | OmitEmptyStruct{ 281 | Required: "value", 282 | Optional: "", // Empty but omitempty 283 | Number: 0, // Zero but omitempty 284 | }, 285 | true, 286 | }, 287 | { 288 | "Invalid optional field value", 289 | OmitEmptyStruct{ 290 | Required: "value", 291 | Optional: "ab", // Too short when provided 292 | Number: 0, 293 | }, 294 | false, 295 | }, 296 | { 297 | "Missing required field", 298 | OmitEmptyStruct{ 299 | Required: "", // Required field is empty 300 | Optional: "test", 301 | Number: 5, 302 | }, 303 | false, 304 | }, 305 | } 306 | 307 | for _, test := range tests { 308 | t.Run(test.name, func(t *testing.T) { 309 | err := ValidateStruct(test.data) 310 | actual := err == nil 311 | if actual != test.expected { 312 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 313 | } 314 | }) 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /cache_coverage_test.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | // Test shouldSkipField function edge cases 9 | func TestShouldSkipField(t *testing.T) { 10 | type TestStruct struct { 11 | PublicField string 12 | privateField string 13 | } 14 | 15 | structType := reflect.TypeOf(TestStruct{}) 16 | 17 | // Test public field - should not be skipped 18 | publicField := structType.Field(0) 19 | if shouldSkipField(publicField) { 20 | t.Error("Public field should not be skipped") 21 | } 22 | 23 | // Test private field - should be skipped 24 | privateField := structType.Field(1) 25 | if !shouldSkipField(privateField) { 26 | t.Error("Private field should be skipped") 27 | } 28 | } 29 | 30 | // Test isValidAttribute function (currently unused) 31 | func TestIsValidAttribute(t *testing.T) { 32 | f := &field{} 33 | 34 | // Test function exists and handles empty string 35 | result := f.isValidAttribute("") 36 | if result { 37 | t.Error("Expected empty string to be invalid") 38 | } 39 | 40 | // The function rejects most strings due to its character restrictions 41 | result = f.isValidAttribute("validattribute") // no special chars 42 | if !result { 43 | t.Error("Expected simple string without special chars to be valid") 44 | } 45 | } 46 | 47 | // Test parseMessageParameterIntoSlice edge cases 48 | func TestParseMessageParameterIntoSlice(t *testing.T) { 49 | f := &field{} 50 | 51 | tests := []struct { 52 | name string 53 | rule string 54 | params []string 55 | expectError bool 56 | expectNil bool 57 | }{ 58 | { 59 | "requiredUnless with valid params", 60 | "requiredUnless", 61 | []string{"field", "value1", "value2"}, 62 | false, 63 | false, 64 | }, 65 | { 66 | "requiredUnless with insufficient params", 67 | "requiredUnless", 68 | []string{"field"}, 69 | true, 70 | false, 71 | }, 72 | { 73 | "between with valid params", 74 | "between", 75 | []string{"1", "10"}, 76 | false, 77 | false, 78 | }, 79 | { 80 | "between with invalid param count", 81 | "between", 82 | []string{"1"}, 83 | true, 84 | false, 85 | }, 86 | { 87 | "digitsBetween with valid params", 88 | "digitsBetween", 89 | []string{"1", "10"}, 90 | false, 91 | false, 92 | }, 93 | { 94 | "gt with valid param", 95 | "gt", 96 | []string{"5"}, 97 | false, 98 | false, 99 | }, 100 | { 101 | "gt with invalid param count", 102 | "gt", 103 | []string{}, 104 | true, 105 | false, 106 | }, 107 | { 108 | "gte with valid param", 109 | "gte", 110 | []string{"5"}, 111 | false, 112 | false, 113 | }, 114 | { 115 | "lt with valid param", 116 | "lt", 117 | []string{"5"}, 118 | false, 119 | false, 120 | }, 121 | { 122 | "lte with valid param", 123 | "lte", 124 | []string{"5"}, 125 | false, 126 | false, 127 | }, 128 | { 129 | "max with valid param", 130 | "max", 131 | []string{"10"}, 132 | false, 133 | false, 134 | }, 135 | { 136 | "min with valid param", 137 | "min", 138 | []string{"1"}, 139 | false, 140 | false, 141 | }, 142 | { 143 | "size with valid param", 144 | "size", 145 | []string{"5"}, 146 | false, 147 | false, 148 | }, 149 | { 150 | "unknown rule", 151 | "unknown", 152 | []string{}, 153 | false, 154 | true, 155 | }, 156 | } 157 | 158 | for _, test := range tests { 159 | t.Run(test.name, func(t *testing.T) { 160 | result, err := f.parseMessageParameterIntoSlice(test.rule, test.params...) 161 | 162 | if test.expectError && err == nil { 163 | t.Errorf("Expected error for %s", test.name) 164 | } 165 | if !test.expectError && err != nil { 166 | t.Errorf("Unexpected error for %s: %v", test.name, err) 167 | } 168 | if test.expectNil && result != nil { 169 | t.Errorf("Expected nil result for %s", test.name) 170 | } 171 | if !test.expectNil && !test.expectError && result == nil { 172 | t.Errorf("Expected non-nil result for %s", test.name) 173 | } 174 | }) 175 | } 176 | } 177 | 178 | // Test parseMessageName edge cases 179 | func TestParseMessageName(t *testing.T) { 180 | f := &field{} 181 | 182 | tests := []struct { 183 | rule string 184 | fieldType reflect.Type 185 | expected string 186 | }{ 187 | {"between", reflect.TypeOf(0), "between.numeric"}, 188 | {"between", reflect.TypeOf(""), "between.string"}, 189 | {"between", reflect.TypeOf([]int{}), "between.array"}, 190 | {"between", reflect.TypeOf(map[string]int{}), "between.array"}, 191 | {"between", reflect.TypeOf(struct{}{}), "between"}, 192 | {"gt", reflect.TypeOf(int8(0)), "gt.numeric"}, 193 | {"gt", reflect.TypeOf(int16(0)), "gt.numeric"}, 194 | {"gt", reflect.TypeOf(int32(0)), "gt.numeric"}, 195 | {"gt", reflect.TypeOf(int64(0)), "gt.numeric"}, 196 | {"gt", reflect.TypeOf(uint(0)), "gt.numeric"}, 197 | {"gt", reflect.TypeOf(uint8(0)), "gt.numeric"}, 198 | {"gt", reflect.TypeOf(uint16(0)), "gt.numeric"}, 199 | {"gt", reflect.TypeOf(uint32(0)), "gt.numeric"}, 200 | {"gt", reflect.TypeOf(uint64(0)), "gt.numeric"}, 201 | {"gt", reflect.TypeOf(float32(0)), "gt.numeric"}, 202 | {"gt", reflect.TypeOf(float64(0)), "gt.numeric"}, 203 | {"gte", reflect.TypeOf(""), "gte.string"}, 204 | {"lt", reflect.TypeOf([]int{}), "lt.array"}, 205 | {"lte", reflect.TypeOf(&struct{}{}), "lte"}, 206 | {"min", reflect.TypeOf(0), "min.numeric"}, 207 | {"max", reflect.TypeOf(""), "max.string"}, 208 | {"size", reflect.TypeOf([]int{}), "size.array"}, 209 | {"unknown", reflect.TypeOf(0), "unknown"}, 210 | } 211 | 212 | for _, test := range tests { 213 | t.Run(test.rule+"_"+test.fieldType.String(), func(t *testing.T) { 214 | result := f.parseMessageName(test.rule, test.fieldType) 215 | if result != test.expected { 216 | t.Errorf("parseMessageName(%s, %s) = %s; expected %s", 217 | test.rule, test.fieldType, result, test.expected) 218 | } 219 | }) 220 | } 221 | } 222 | 223 | // Test cachedTypefields with complex nested types 224 | func TestCachedTypefieldsComplex(t *testing.T) { 225 | type NestedStruct struct { 226 | Field1 string `valid:"required"` 227 | Field2 int `valid:"min=1"` 228 | } 229 | 230 | type ComplexStruct struct { 231 | Simple string `valid:"email"` 232 | Nested NestedStruct `valid:"required"` 233 | Slice []string `valid:"required"` 234 | Map map[string]int 235 | Pointer *string `valid:"required"` 236 | ignored string `valid:"-"` 237 | } 238 | 239 | // First call should populate cache 240 | fields1 := cachedTypefields(reflect.TypeOf(ComplexStruct{})) 241 | 242 | // Second call should use cache 243 | fields2 := cachedTypefields(reflect.TypeOf(ComplexStruct{})) 244 | 245 | // Should return same results 246 | if len(fields1) != len(fields2) { 247 | t.Errorf("Cache returned different number of fields: %d vs %d", len(fields1), len(fields2)) 248 | } 249 | 250 | // Verify specific fields are present and have correct properties 251 | fieldNames := make(map[string]bool) 252 | for _, field := range fields1 { 253 | fieldNames[field.name] = true 254 | } 255 | 256 | expectedFields := []string{"Simple", "Nested", "Slice", "Pointer"} 257 | for _, expected := range expectedFields { 258 | if !fieldNames[expected] { 259 | t.Errorf("Expected field %s not found in cached results", expected) 260 | } 261 | } 262 | 263 | // Verify ignored field is not present 264 | if fieldNames["ignored"] { 265 | t.Error("Field marked with '-' should be ignored") 266 | } 267 | } 268 | 269 | // Test processStructField with various field configurations 270 | func TestProcessStructField(t *testing.T) { 271 | type TestStruct struct { 272 | ValidField string `valid:"required"` 273 | IgnoredField string `valid:"-"` 274 | EmptyTag string 275 | unexported string `valid:"required"` 276 | } 277 | 278 | structType := reflect.TypeOf(TestStruct{}) 279 | f := &field{typ: structType} 280 | count := make(map[reflect.Type]int) 281 | nextCount := make(map[reflect.Type]int) 282 | var fields []field 283 | var next []field 284 | 285 | // Test each field 286 | for i := 0; i < structType.NumField(); i++ { 287 | sf := structType.Field(i) 288 | processStructField(sf, f, structType, i, count, nextCount, &fields, &next) 289 | } 290 | 291 | // Should have processed valid fields but skipped others 292 | fieldNames := make(map[string]bool) 293 | for _, field := range fields { 294 | fieldNames[field.name] = true 295 | } 296 | 297 | if !fieldNames["ValidField"] { 298 | t.Error("ValidField should be processed") 299 | } 300 | if fieldNames["IgnoredField"] { 301 | t.Error("IgnoredField should be ignored due to '-' tag") 302 | } 303 | if fieldNames["EmptyTag"] { 304 | t.Error("EmptyTag should be ignored due to empty validation tag") 305 | } 306 | if fieldNames["unexported"] { 307 | t.Error("unexported field should be skipped") 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /coverage_improvement_test.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | // Test direct validation functions to improve coverage 9 | func TestCoverageImprovementDirectFunctions(t *testing.T) { 10 | // Test findField function 11 | type TestStruct struct { 12 | Name string 13 | Age int 14 | } 15 | testStruct := TestStruct{Name: "test", Age: 25} 16 | v := reflect.ValueOf(testStruct) 17 | 18 | // Test successful field finding 19 | field, err := findField("Name", v) 20 | if err != nil { 21 | t.Errorf("findField should not error for valid field: %v", err) 22 | } 23 | if field.String() != "test" { 24 | t.Errorf("Expected 'test', got %v", field.String()) 25 | } 26 | 27 | // Test non-existent field (returns zero Value, not error) 28 | field, err = findField("NonExistent", v) 29 | if err != nil { 30 | t.Errorf("findField should not error for non-existent field: %v", err) 31 | } 32 | if field.IsValid() { 33 | t.Error("findField should return invalid Value for non-existent field") 34 | } 35 | 36 | // Test with non-struct 37 | nonStruct := reflect.ValueOf("not a struct") 38 | _, err = findField("field", nonStruct) 39 | if err == nil { 40 | t.Error("findField should error for non-struct value") 41 | } 42 | } 43 | 44 | // Test validateRequiredWith function directly 45 | func TestValidateRequiredWithDirect(t *testing.T) { 46 | type TestStruct struct { 47 | Field1 string 48 | Field2 string 49 | } 50 | 51 | // Case 1: Field2 present, Field1 present - should be valid 52 | testStruct := TestStruct{Field1: "value1", Field2: "value2"} 53 | objValue := reflect.ValueOf(testStruct) 54 | field1Value := reflect.ValueOf("value1") 55 | 56 | result := validateRequiredWith([]string{"Field2"}, field1Value, objValue) 57 | if !result { 58 | t.Error("validateRequiredWith should return true when both fields are present") 59 | } 60 | 61 | // Case 2: Field2 present, Field1 empty - should be invalid 62 | field1EmptyValue := reflect.ValueOf("") 63 | result = validateRequiredWith([]string{"Field2"}, field1EmptyValue, objValue) 64 | if result { 65 | t.Error("validateRequiredWith should return false when Field2 is present but Field1 is empty") 66 | } 67 | 68 | // Case 3: Field2 empty, Field1 empty - should be valid 69 | testStructEmpty := TestStruct{Field1: "", Field2: ""} 70 | objValueEmpty := reflect.ValueOf(testStructEmpty) 71 | result = validateRequiredWith([]string{"Field2"}, field1EmptyValue, objValueEmpty) 72 | if !result { 73 | t.Error("validateRequiredWith should return true when both fields are empty") 74 | } 75 | } 76 | 77 | // Test validateRequiredWithAll function directly 78 | func TestValidateRequiredWithAllDirect(t *testing.T) { 79 | type TestStruct struct { 80 | Field1 string 81 | Field2 string 82 | Field3 string 83 | } 84 | 85 | // Case 1: All fields present - Field1 should be required and present 86 | testStruct := TestStruct{Field1: "value1", Field2: "value2", Field3: "value3"} 87 | objValue := reflect.ValueOf(testStruct) 88 | field1Value := reflect.ValueOf("value1") 89 | 90 | result := validateRequiredWithAll([]string{"Field2", "Field3"}, field1Value, objValue) 91 | if !result { 92 | t.Error("validateRequiredWithAll should return true when all fields are present") 93 | } 94 | 95 | // Case 2: Field2 and Field3 present, Field1 empty - should be invalid 96 | field1EmptyValue := reflect.ValueOf("") 97 | result = validateRequiredWithAll([]string{"Field2", "Field3"}, field1EmptyValue, objValue) 98 | if result { 99 | t.Error("validateRequiredWithAll should return false when Field2 and Field3 are present but Field1 is empty") 100 | } 101 | 102 | // Case 3: Only Field2 present, Field1 empty - should be valid (Field1 not required) 103 | testStructPartial := TestStruct{Field1: "", Field2: "value2", Field3: ""} 104 | objValuePartial := reflect.ValueOf(testStructPartial) 105 | result = validateRequiredWithAll([]string{"Field2", "Field3"}, field1EmptyValue, objValuePartial) 106 | if !result { 107 | t.Error("validateRequiredWithAll should return true when not all required fields are present") 108 | } 109 | } 110 | 111 | // Test validateRequiredWithout function directly 112 | func TestValidateRequiredWithoutDirect(t *testing.T) { 113 | type TestStruct struct { 114 | Field1 string 115 | Field2 string 116 | } 117 | 118 | // Case 1: Field2 absent, Field1 present - should be valid 119 | testStruct := TestStruct{Field1: "value1", Field2: ""} 120 | objValue := reflect.ValueOf(testStruct) 121 | field1Value := reflect.ValueOf("value1") 122 | 123 | result := validateRequiredWithout([]string{"Field2"}, field1Value, objValue) 124 | if !result { 125 | t.Error("validateRequiredWithout should return true when Field2 is absent and Field1 is present") 126 | } 127 | 128 | // Case 2: Field2 absent, Field1 absent - should be invalid 129 | field1EmptyValue := reflect.ValueOf("") 130 | result = validateRequiredWithout([]string{"Field2"}, field1EmptyValue, objValue) 131 | if result { 132 | t.Error("validateRequiredWithout should return false when Field2 is absent and Field1 is also absent") 133 | } 134 | 135 | // Case 3: Field2 present, Field1 absent - should be valid (Field1 not required) 136 | testStructWithField2 := TestStruct{Field1: "", Field2: "value2"} 137 | objValueWithField2 := reflect.ValueOf(testStructWithField2) 138 | result = validateRequiredWithout([]string{"Field2"}, field1EmptyValue, objValueWithField2) 139 | if !result { 140 | t.Error("validateRequiredWithout should return true when Field2 is present (Field1 not required)") 141 | } 142 | } 143 | 144 | // Test validateRequiredWithoutAll function directly 145 | func TestValidateRequiredWithoutAllDirect(t *testing.T) { 146 | type TestStruct struct { 147 | Field1 string 148 | Field2 string 149 | Field3 string 150 | } 151 | 152 | // Case 1: All fields absent, Field1 present - should be valid 153 | testStruct := TestStruct{Field1: "value1", Field2: "", Field3: ""} 154 | objValue := reflect.ValueOf(testStruct) 155 | field1Value := reflect.ValueOf("value1") 156 | 157 | result := validateRequiredWithoutAll([]string{"Field2", "Field3"}, field1Value, objValue) 158 | if !result { 159 | t.Error("validateRequiredWithoutAll should return true when all other fields are absent and Field1 is present") 160 | } 161 | 162 | // Case 2: All fields absent, Field1 absent - should be invalid 163 | field1EmptyValue := reflect.ValueOf("") 164 | result = validateRequiredWithoutAll([]string{"Field2", "Field3"}, field1EmptyValue, objValue) 165 | if result { 166 | t.Error("validateRequiredWithoutAll should return false when all fields including Field1 are absent") 167 | } 168 | 169 | // Case 3: Some field present, Field1 absent - should be valid (Field1 not required) 170 | testStructPartial := TestStruct{Field1: "", Field2: "value2", Field3: ""} 171 | objValuePartial := reflect.ValueOf(testStructPartial) 172 | result = validateRequiredWithoutAll([]string{"Field2", "Field3"}, field1EmptyValue, objValuePartial) 173 | if !result { 174 | t.Error("validateRequiredWithoutAll should return true when some other fields are present (Field1 not required)") 175 | } 176 | } 177 | 178 | // Test parameter-based comparison validators directly 179 | func TestParameterValidatorsDirect(t *testing.T) { 180 | // Test validateGtParam 181 | stringValue := reflect.ValueOf("hello") 182 | result, err := validateGtParam(stringValue, []string{"3"}) 183 | if err != nil || !result { 184 | t.Errorf("validateGtParam should return true for 'hello' > 3 characters: result=%v, err=%v", result, err) 185 | } 186 | 187 | shortString := reflect.ValueOf("hi") 188 | result, err = validateGtParam(shortString, []string{"3"}) 189 | if err != nil || result { 190 | t.Errorf("validateGtParam should return false for 'hi' > 3 characters: result=%v, err=%v", result, err) 191 | } 192 | 193 | // Test validateGteParam 194 | exactString := reflect.ValueOf("test") 195 | result, err = validateGteParam(exactString, []string{"4"}) 196 | if err != nil || !result { 197 | t.Errorf("validateGteParam should return true for 'test' >= 4 characters: result=%v, err=%v", result, err) 198 | } 199 | 200 | // Test validateLtParam 201 | result, err = validateLtParam(shortString, []string{"3"}) 202 | if err != nil || !result { 203 | t.Errorf("validateLtParam should return true for 'hi' < 3 characters: result=%v, err=%v", result, err) 204 | } 205 | 206 | // Test validateLteParam 207 | result, err = validateLteParam(exactString, []string{"4"}) 208 | if err != nil || !result { 209 | t.Errorf("validateLteParam should return true for 'test' <= 4 characters: result=%v, err=%v", result, err) 210 | } 211 | 212 | // Test with integer values 213 | intValue := reflect.ValueOf(5) 214 | result, err = validateGtParam(intValue, []string{"3"}) 215 | if err != nil || !result { 216 | t.Errorf("validateGtParam should return true for 5 > 3: result=%v, err=%v", result, err) 217 | } 218 | 219 | // Test error cases 220 | _, err = validateGtParam(stringValue, []string{}) 221 | if err == nil { 222 | t.Error("validateGtParam should return error for empty params") 223 | } 224 | 225 | _, err = validateGtParam(stringValue, []string{"invalid"}) 226 | if err == nil { 227 | t.Error("validateGtParam should return error for invalid numeric param") 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /validation_edge_cases_test.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | // Test validateSame edge cases 9 | func TestValidateSameEdgeCases(t *testing.T) { 10 | type SameTest struct { 11 | Password string `valid:"required"` 12 | ConfirmPassword string `valid:"same=Password"` 13 | } 14 | 15 | tests := []struct { 16 | name string 17 | data SameTest 18 | expected bool 19 | }{ 20 | {"Passwords match - valid", SameTest{Password: "secret123", ConfirmPassword: "secret123"}, true}, 21 | {"Passwords don't match - invalid", SameTest{Password: "secret123", ConfirmPassword: "different"}, false}, 22 | {"Empty passwords match - invalid (required)", SameTest{Password: "", ConfirmPassword: ""}, false}, 23 | {"One empty, one filled - invalid", SameTest{Password: "secret", ConfirmPassword: ""}, false}, 24 | } 25 | 26 | for _, test := range tests { 27 | t.Run(test.name, func(t *testing.T) { 28 | err := ValidateStruct(test.data) 29 | actual := err == nil 30 | if actual != test.expected { 31 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 32 | } 33 | }) 34 | } 35 | } 36 | 37 | // Test validateLt edge cases 38 | func TestValidateLtEdgeCases(t *testing.T) { 39 | type LtTest struct { 40 | Value string `valid:"lt=10"` 41 | } 42 | 43 | tests := []struct { 44 | name string 45 | data LtTest 46 | expected bool 47 | }{ 48 | {"Short string - valid", LtTest{Value: "test"}, true}, 49 | {"Long string - invalid", LtTest{Value: "this is a very long string"}, false}, 50 | {"Empty string - valid", LtTest{Value: ""}, true}, 51 | {"Exactly 9 chars - valid", LtTest{Value: "123456789"}, true}, 52 | {"Exactly 10 chars - invalid", LtTest{Value: "1234567890"}, false}, 53 | } 54 | 55 | for _, test := range tests { 56 | t.Run(test.name, func(t *testing.T) { 57 | err := ValidateStruct(test.data) 58 | actual := err == nil 59 | if actual != test.expected { 60 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 61 | } 62 | }) 63 | } 64 | } 65 | 66 | // Test validateLte edge cases 67 | func TestValidateLteEdgeCases(t *testing.T) { 68 | type LteTest struct { 69 | Value string `valid:"lte=5"` 70 | } 71 | 72 | tests := []struct { 73 | name string 74 | data LteTest 75 | expected bool 76 | }{ 77 | {"Short string - valid", LteTest{Value: "test"}, true}, 78 | {"Exactly 5 chars - valid", LteTest{Value: "hello"}, true}, 79 | {"6 chars - invalid", LteTest{Value: "hello!"}, false}, 80 | {"Empty string - valid", LteTest{Value: ""}, true}, 81 | } 82 | 83 | for _, test := range tests { 84 | t.Run(test.name, func(t *testing.T) { 85 | err := ValidateStruct(test.data) 86 | actual := err == nil 87 | if actual != test.expected { 88 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 89 | } 90 | }) 91 | } 92 | } 93 | 94 | // Test validateGt edge cases 95 | func TestValidateGtEdgeCases(t *testing.T) { 96 | type GtTest struct { 97 | Value string `valid:"gt=3"` 98 | } 99 | 100 | tests := []struct { 101 | name string 102 | data GtTest 103 | expected bool 104 | }{ 105 | {"4 chars - valid", GtTest{Value: "test"}, true}, 106 | {"Exactly 3 chars - invalid", GtTest{Value: "abc"}, false}, 107 | {"2 chars - invalid", GtTest{Value: "ab"}, false}, 108 | {"Empty string - invalid", GtTest{Value: ""}, false}, 109 | {"10 chars - valid", GtTest{Value: "verylongst"}, true}, 110 | } 111 | 112 | for _, test := range tests { 113 | t.Run(test.name, func(t *testing.T) { 114 | err := ValidateStruct(test.data) 115 | actual := err == nil 116 | if actual != test.expected { 117 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 118 | } 119 | }) 120 | } 121 | } 122 | 123 | // Test validateGte edge cases 124 | func TestValidateGteEdgeCases(t *testing.T) { 125 | type GteTest struct { 126 | Value string `valid:"gte=4"` 127 | } 128 | 129 | tests := []struct { 130 | name string 131 | data GteTest 132 | expected bool 133 | }{ 134 | {"Exactly 4 chars - valid", GteTest{Value: "test"}, true}, 135 | {"5 chars - valid", GteTest{Value: "tests"}, true}, 136 | {"3 chars - invalid", GteTest{Value: "abc"}, false}, 137 | {"Empty string - invalid", GteTest{Value: ""}, false}, 138 | } 139 | 140 | for _, test := range tests { 141 | t.Run(test.name, func(t *testing.T) { 142 | err := ValidateStruct(test.data) 143 | actual := err == nil 144 | if actual != test.expected { 145 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 146 | } 147 | }) 148 | } 149 | } 150 | 151 | // Test validateSize edge cases 152 | func TestValidateSizeEdgeCases(t *testing.T) { 153 | type SizeTest struct { 154 | Value string `valid:"size=5"` 155 | } 156 | 157 | tests := []struct { 158 | name string 159 | data SizeTest 160 | expected bool 161 | }{ 162 | {"Exactly 5 chars - valid", SizeTest{Value: "hello"}, true}, 163 | {"4 chars - invalid", SizeTest{Value: "test"}, false}, 164 | {"6 chars - invalid", SizeTest{Value: "hellos"}, false}, 165 | {"Empty string - invalid", SizeTest{Value: ""}, false}, 166 | } 167 | 168 | for _, test := range tests { 169 | t.Run(test.name, func(t *testing.T) { 170 | err := ValidateStruct(test.data) 171 | actual := err == nil 172 | if actual != test.expected { 173 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 174 | } 175 | }) 176 | } 177 | } 178 | 179 | // Test validateMax edge cases 180 | func TestValidateMaxEdgeCases(t *testing.T) { 181 | type MaxTest struct { 182 | Value string `valid:"max=8"` 183 | } 184 | 185 | tests := []struct { 186 | name string 187 | data MaxTest 188 | expected bool 189 | }{ 190 | {"Under max - valid", MaxTest{Value: "short"}, true}, 191 | {"Exactly max - valid", MaxTest{Value: "exactly8"}, true}, 192 | {"Over max - invalid", MaxTest{Value: "toolongstring"}, false}, 193 | {"Empty string - valid", MaxTest{Value: ""}, true}, 194 | } 195 | 196 | for _, test := range tests { 197 | t.Run(test.name, func(t *testing.T) { 198 | err := ValidateStruct(test.data) 199 | actual := err == nil 200 | if actual != test.expected { 201 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 202 | } 203 | }) 204 | } 205 | } 206 | 207 | // Test validateMin edge cases 208 | func TestValidateMinEdgeCases(t *testing.T) { 209 | type MinTest struct { 210 | Value string `valid:"min=3"` 211 | } 212 | 213 | tests := []struct { 214 | name string 215 | data MinTest 216 | expected bool 217 | }{ 218 | {"Above min - valid", MinTest{Value: "test"}, true}, 219 | {"Exactly min - valid", MinTest{Value: "abc"}, true}, 220 | {"Below min - invalid", MinTest{Value: "ab"}, false}, 221 | {"Empty string - invalid", MinTest{Value: ""}, false}, 222 | } 223 | 224 | for _, test := range tests { 225 | t.Run(test.name, func(t *testing.T) { 226 | err := ValidateStruct(test.data) 227 | actual := err == nil 228 | if actual != test.expected { 229 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 230 | } 231 | }) 232 | } 233 | } 234 | 235 | // Test validateDistinct with different scenarios 236 | func TestValidateDistinctAdvanced(t *testing.T) { 237 | type DistinctTest struct { 238 | Items []string `valid:"distinct"` 239 | } 240 | 241 | tests := []struct { 242 | name string 243 | data DistinctTest 244 | expected bool 245 | }{ 246 | {"All unique - valid", DistinctTest{Items: []string{"a", "b", "c", "d"}}, true}, 247 | {"Has duplicates - invalid", DistinctTest{Items: []string{"a", "b", "c", "a"}}, false}, 248 | {"Empty slice - valid", DistinctTest{Items: []string{}}, true}, 249 | {"Single item - valid", DistinctTest{Items: []string{"a"}}, true}, 250 | {"Two identical - invalid", DistinctTest{Items: []string{"a", "a"}}, false}, 251 | {"Case sensitive - valid", DistinctTest{Items: []string{"A", "a", "B", "b"}}, true}, 252 | } 253 | 254 | for _, test := range tests { 255 | t.Run(test.name, func(t *testing.T) { 256 | err := ValidateStruct(test.data) 257 | actual := err == nil 258 | if actual != test.expected { 259 | t.Errorf("Expected %t for %s, got %t. Error: %v", test.expected, test.name, actual, err) 260 | } 261 | }) 262 | } 263 | } 264 | 265 | // Test direct validation functions to increase coverage 266 | func TestDirectValidationFunctions(t *testing.T) { 267 | // Test validateDistinct function directly 268 | result, err := validateDistinct(reflect.ValueOf([]string{"a", "b", "c"})) 269 | if err != nil || !result { 270 | t.Error("Expected distinct values to validate") 271 | } 272 | 273 | result, err = validateDistinct(reflect.ValueOf([]string{"a", "b", "a"})) 274 | if err != nil || result { 275 | t.Error("Expected duplicate values to fail") 276 | } 277 | 278 | // Test empty slice 279 | result, err = validateDistinct(reflect.ValueOf([]string{})) 280 | if err != nil || !result { 281 | t.Error("Expected empty slice to validate") 282 | } 283 | 284 | // Test single item 285 | result, err = validateDistinct(reflect.ValueOf([]string{"single"})) 286 | if err != nil || !result { 287 | t.Error("Expected single item to validate") 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkFieldsRequired(t *testing.B) { 9 | model := FieldsRequired{Name: "TEST", Email: "test@example.com"} 10 | expected := true 11 | for i := 0; i < t.N; i++ { 12 | err := ValidateStruct(&model) 13 | actual := err == nil 14 | if actual != expected { 15 | t.Errorf("Expected validateateStruct(%q) to be %v, got %v", model, expected, actual) 16 | if err != nil { 17 | t.Errorf("Got Error on validateateStruct(%q): %s", model, err) 18 | } 19 | } 20 | } 21 | } 22 | 23 | // BenchmarkErrorHandling benchmarks the new error handling 24 | func BenchmarkErrorHandling(t *testing.B) { 25 | type TestStruct struct { 26 | Name string `valid:"required"` 27 | Email string `valid:"required,email"` 28 | Age int `valid:"min=18"` 29 | } 30 | 31 | invalidData := TestStruct{Name: "", Email: "invalid", Age: 16} 32 | 33 | t.ResetTimer() 34 | for i := 0; i < t.N; i++ { 35 | _ = ValidateStruct(invalidData) 36 | } 37 | } 38 | 39 | // BenchmarkErrorsUtilityMethods benchmarks the Errors utility methods 40 | func BenchmarkErrorsUtilityMethods(t *testing.B) { 41 | type TestStruct struct { 42 | Name string `valid:"required"` 43 | Email string `valid:"required,email"` 44 | Age int `valid:"min=18"` 45 | } 46 | 47 | invalidData := TestStruct{Name: "", Email: "invalid", Age: 16} 48 | err := ValidateStruct(invalidData) 49 | //nolint:errcheck // Safe type assertion in benchmark context 50 | errors := err.(Errors) 51 | 52 | t.ResetTimer() 53 | for i := 0; i < t.N; i++ { 54 | _ = errors.HasFieldError("Name") 55 | _ = errors.GetFieldError("Email") 56 | _ = errors.GroupByField() 57 | } 58 | } 59 | 60 | // BenchmarkComparisonFunctions benchmarks the comparison functions 61 | func BenchmarkComparisonFunctions(t *testing.B) { 62 | type TestStruct struct { 63 | Value1 int `valid:"gt=Value2"` 64 | Value2 int 65 | } 66 | 67 | data := TestStruct{Value1: 10, Value2: 5} 68 | 69 | t.ResetTimer() 70 | for i := 0; i < t.N; i++ { 71 | _ = ValidateStruct(data) 72 | } 73 | } 74 | 75 | // BenchmarkGo119Performance benchmarks performance with Go 1.19 optimizations 76 | func BenchmarkGo119Performance(t *testing.B) { 77 | type LargeStruct struct { 78 | Field1 string `valid:"required"` 79 | Field2 string `valid:"email"` 80 | Field3 int `valid:"min=1"` 81 | Field4 int `valid:"max=100"` 82 | Field5 string `valid:"between=5|20"` 83 | Field6 string `valid:"alpha"` 84 | Field7 string `valid:"numeric"` 85 | Field8 string `valid:"url"` 86 | Field9 string `valid:"ip"` 87 | Field10 string `valid:"uuid"` 88 | } 89 | 90 | data := LargeStruct{ 91 | Field1: "test", 92 | Field2: "test@example.com", 93 | Field3: 5, 94 | Field4: 50, 95 | Field5: "medium length text", 96 | Field6: "alphabet", 97 | Field7: "123456", 98 | Field8: "https://example.com", 99 | Field9: "192.168.1.1", 100 | Field10: "550e8400-e29b-41d4-a716-446655440000", 101 | } 102 | 103 | t.ResetTimer() 104 | for i := 0; i < t.N; i++ { 105 | _ = ValidateStruct(data) 106 | } 107 | } 108 | 109 | // BenchmarkMemoryAllocation benchmarks memory allocation patterns 110 | func BenchmarkMemoryAllocation(t *testing.B) { 111 | type SimpleStruct struct { 112 | Name string `valid:"required"` 113 | Email string `valid:"email"` 114 | Age int `valid:"min=18,max=120"` 115 | } 116 | 117 | data := SimpleStruct{ 118 | Name: "John Doe", 119 | Email: "john.doe@example.com", 120 | Age: 25, 121 | } 122 | 123 | t.ResetTimer() 124 | t.ReportAllocs() 125 | for i := 0; i < t.N; i++ { 126 | _ = ValidateStruct(data) 127 | } 128 | } 129 | 130 | // BenchmarkStringBuilderOptimization benchmarks the strings.Builder optimization in error handling 131 | func BenchmarkStringBuilderOptimization(t *testing.B) { 132 | type MultiErrorStruct struct { 133 | Field1 string `valid:"required"` 134 | Field2 string `valid:"required"` 135 | Field3 string `valid:"required"` 136 | Field4 string `valid:"required"` 137 | Field5 string `valid:"required"` 138 | } 139 | 140 | // Create data that will generate multiple errors 141 | invalidData := MultiErrorStruct{} // All fields empty, will generate 5 errors 142 | 143 | t.ResetTimer() 144 | t.ReportAllocs() 145 | for i := 0; i < t.N; i++ { 146 | err := ValidateStruct(invalidData) 147 | if err != nil { 148 | // Force error string generation to test strings.Builder optimization 149 | _ = err.Error() 150 | } 151 | } 152 | } 153 | 154 | // BenchmarkErrorGrouping benchmarks the error grouping functionality 155 | func BenchmarkErrorGrouping(t *testing.B) { 156 | type ComplexStruct struct { 157 | Name string `valid:"required"` 158 | Email string `valid:"required,email"` 159 | Age int `valid:"min=18"` 160 | Phone string `valid:"required"` 161 | Address string `valid:"required,min=10"` 162 | } 163 | 164 | // Create invalid data to generate multiple errors 165 | invalidData := ComplexStruct{ 166 | Name: "", 167 | Email: "invalid-email", 168 | Age: 15, 169 | Phone: "", 170 | Address: "short", 171 | } 172 | 173 | err := ValidateStruct(invalidData) 174 | if err == nil { 175 | t.Fatal("Expected validation errors") 176 | } 177 | 178 | //nolint:errcheck // Safe type assertion in benchmark context 179 | errors := err.(Errors) 180 | 181 | t.ResetTimer() 182 | for i := 0; i < t.N; i++ { 183 | _ = errors.GroupByField() 184 | } 185 | } 186 | 187 | // BenchmarkFuncErrorHandling benchmarks the FuncError functionality 188 | func BenchmarkFuncErrorHandling(t *testing.B) { 189 | type ErrorStruct struct { 190 | Complex complex64 `valid:"between=1|10"` // Will trigger FuncError 191 | Valid string `valid:"required"` // Normal validation 192 | } 193 | 194 | data := ErrorStruct{ 195 | Complex: 5 + 5i, // Unsupported type 196 | Valid: "test", 197 | } 198 | 199 | t.ResetTimer() 200 | t.ReportAllocs() 201 | for i := 0; i < t.N; i++ { 202 | err := ValidateStruct(data) 203 | if err != nil { 204 | //nolint:errcheck // Safe type assertion in benchmark context 205 | errors := err.(Errors) 206 | // Benchmark accessing FuncError methods 207 | for _, e := range errors { 208 | if fieldErr, ok := e.(*FieldError); ok { 209 | _ = fieldErr.HasFuncError() 210 | _ = fieldErr.Unwrap() 211 | } 212 | } 213 | } 214 | } 215 | } 216 | 217 | // BenchmarkErrorUnwrapping benchmarks error unwrapping performance 218 | func BenchmarkErrorUnwrapping(t *testing.B) { 219 | // Create a FieldError with FuncError 220 | originalErr := fmt.Errorf("test function error") 221 | fieldError := &FieldError{ 222 | Name: "TestField", 223 | Message: "Test message", 224 | FuncError: originalErr, 225 | } 226 | 227 | t.ResetTimer() 228 | for i := 0; i < t.N; i++ { 229 | _ = fieldError.HasFuncError() 230 | _ = fieldError.Unwrap() 231 | _ = fieldError.Error() 232 | } 233 | } 234 | 235 | // BenchmarkPerformanceOptimizations benchmarks the new performance optimizations 236 | func BenchmarkPerformanceOptimizations(t *testing.B) { 237 | type OptimizedStruct struct { 238 | Name string `valid:"required"` 239 | Email string `valid:"email"` 240 | Age int `valid:"min=18,max=120"` 241 | Score int64 `valid:"between=0|100"` 242 | Active bool `valid:"required"` 243 | } 244 | 245 | data := OptimizedStruct{ 246 | Name: "John Doe", 247 | Email: "john.doe@example.com", 248 | Age: 25, 249 | Score: 85, 250 | Active: true, 251 | } 252 | 253 | t.ResetTimer() 254 | t.ReportAllocs() 255 | for i := 0; i < t.N; i++ { 256 | _ = ValidateStruct(data) 257 | } 258 | } 259 | 260 | // BenchmarkToStringOptimization benchmarks the optimized ToString function 261 | func BenchmarkToStringOptimization(t *testing.B) { 262 | testValues := []interface{}{ 263 | "string value", 264 | 42, 265 | int64(123456789), 266 | uint64(987654321), 267 | 3.14159, 268 | true, 269 | } 270 | 271 | t.ResetTimer() 272 | for i := 0; i < t.N; i++ { 273 | for _, v := range testValues { 274 | _ = ToString(v) 275 | } 276 | } 277 | } 278 | 279 | // BenchmarkObjectPooling benchmarks object pooling performance 280 | func BenchmarkObjectPooling(t *testing.B) { 281 | t.Run("AllocationPattern", func(t *testing.B) { 282 | for i := 0; i < t.N; i++ { 283 | fe := &FieldError{} 284 | fe.Name = "test" 285 | fe.Message = "test message" 286 | // Simulate usage 287 | _ = fe.Error() 288 | } 289 | }) 290 | } 291 | 292 | // BenchmarkComplexValidation tests performance on complex nested structures 293 | func BenchmarkComplexValidation(t *testing.B) { 294 | type Address struct { 295 | Street string `valid:"required"` 296 | City string `valid:"required"` 297 | Zip string `valid:"numeric,size=5"` 298 | Country string `valid:"required,alpha"` 299 | } 300 | 301 | type User struct { 302 | Name string `valid:"required,alpha"` 303 | Email string `valid:"required,email"` 304 | Age int `valid:"min=18,max=120"` 305 | Score float64 `valid:"between=0|100"` 306 | Active bool `valid:"required"` 307 | Addresses []Address `valid:"required"` 308 | } 309 | 310 | data := User{ 311 | Name: "JohnDoe", 312 | Email: "john.doe@example.com", 313 | Age: 30, 314 | Score: 85.5, 315 | Active: true, 316 | Addresses: []Address{ 317 | { 318 | Street: "Main St", 319 | City: "CityName", 320 | Zip: "12345", 321 | Country: "USA", 322 | }, 323 | }, 324 | } 325 | 326 | t.ResetTimer() 327 | t.ReportAllocs() 328 | for i := 0; i < t.N; i++ { 329 | _ = ValidateStruct(data) 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /numeric_validation_test.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // Consolidated numeric validation tests 8 | // Contains tests for integer, float, and uint validation functions 9 | 10 | // Float validation tests 11 | func TestValidateDigitsBetweenFloat64(t *testing.T) { 12 | tests := []struct { 13 | value float64 14 | left float64 15 | right float64 16 | expected bool 17 | }{ 18 | {5.0, 1.0, 10.0, true}, 19 | {0.0, 1.0, 10.0, false}, 20 | {15.0, 1.0, 10.0, false}, 21 | {1.0, 1.0, 10.0, true}, 22 | {10.0, 1.0, 10.0, true}, 23 | {5.5, 5.5, 5.5, true}, 24 | {5.0, 10.0, 1.0, true}, // Test swapping when left > right 25 | {0.5, 10.0, 1.0, false}, 26 | {15.0, 10.0, 1.0, false}, 27 | } 28 | 29 | for _, test := range tests { 30 | result := ValidateDigitsBetweenFloat64(test.value, test.left, test.right) 31 | if result != test.expected { 32 | t.Errorf("ValidateDigitsBetweenFloat64(%f, %f, %f) = %t; expected %t", 33 | test.value, test.left, test.right, result, test.expected) 34 | } 35 | } 36 | } 37 | 38 | func TestValidateLtFloat64(t *testing.T) { 39 | tests := []struct { 40 | value float64 41 | param float64 42 | expected bool 43 | }{ 44 | {5.0, 10.0, true}, 45 | {10.0, 5.0, false}, 46 | {5.5, 5.5, false}, 47 | {-5.0, 0.0, true}, 48 | {0.0, -5.0, false}, 49 | } 50 | 51 | for _, test := range tests { 52 | result := ValidateLtFloat64(test.value, test.param) 53 | if result != test.expected { 54 | t.Errorf("ValidateLtFloat64(%f, %f) = %t; expected %t", 55 | test.value, test.param, result, test.expected) 56 | } 57 | } 58 | } 59 | 60 | func TestValidateLteFloat64(t *testing.T) { 61 | tests := []struct { 62 | value float64 63 | param float64 64 | expected bool 65 | }{ 66 | {5.0, 10.0, true}, 67 | {10.0, 5.0, false}, 68 | {5.5, 5.5, true}, 69 | {-5.0, 0.0, true}, 70 | {0.0, -5.0, false}, 71 | } 72 | 73 | for _, test := range tests { 74 | result := ValidateLteFloat64(test.value, test.param) 75 | if result != test.expected { 76 | t.Errorf("ValidateLteFloat64(%f, %f) = %t; expected %t", 77 | test.value, test.param, result, test.expected) 78 | } 79 | } 80 | } 81 | 82 | func TestValidateGteFloat64(t *testing.T) { 83 | tests := []struct { 84 | value float64 85 | param float64 86 | expected bool 87 | }{ 88 | {10.0, 5.0, true}, 89 | {5.0, 10.0, false}, 90 | {5.5, 5.5, true}, 91 | {0.0, -5.0, true}, 92 | {-5.0, 0.0, false}, 93 | } 94 | 95 | for _, test := range tests { 96 | result := ValidateGteFloat64(test.value, test.param) 97 | if result != test.expected { 98 | t.Errorf("ValidateGteFloat64(%f, %f) = %t; expected %t", 99 | test.value, test.param, result, test.expected) 100 | } 101 | } 102 | } 103 | 104 | func TestValidateGtFloat64(t *testing.T) { 105 | tests := []struct { 106 | value float64 107 | param float64 108 | expected bool 109 | }{ 110 | {10.0, 5.0, true}, 111 | {5.0, 10.0, false}, 112 | {5.5, 5.5, false}, 113 | {0.0, -5.0, true}, 114 | {-5.0, 0.0, false}, 115 | } 116 | 117 | for _, test := range tests { 118 | result := ValidateGtFloat64(test.value, test.param) 119 | if result != test.expected { 120 | t.Errorf("ValidateGtFloat64(%f, %f) = %t; expected %t", 121 | test.value, test.param, result, test.expected) 122 | } 123 | } 124 | } 125 | 126 | func TestCompareFloat64(t *testing.T) { 127 | tests := []struct { 128 | name string 129 | first float64 130 | second float64 131 | operator string 132 | expected bool 133 | expectError bool 134 | }{ 135 | {"Less than true", 5.5, 10.5, "<", true, false}, 136 | {"Less than false", 10.5, 5.5, "<", false, false}, 137 | {"Greater than true", 10.5, 5.5, ">", true, false}, 138 | {"Greater than false", 5.5, 10.5, ">", false, false}, 139 | {"Less than or equal true (less)", 5.5, 10.5, "<=", true, false}, 140 | {"Less than or equal true (equal)", 5.5, 5.5, "<=", true, false}, 141 | {"Less than or equal false", 10.5, 5.5, "<=", false, false}, 142 | {"Greater than or equal true (greater)", 10.5, 5.5, ">=", true, false}, 143 | {"Greater than or equal true (equal)", 5.5, 5.5, ">=", true, false}, 144 | {"Greater than or equal false", 5.5, 10.5, ">=", false, false}, 145 | {"Equal true", 5.5, 5.5, "==", true, false}, 146 | {"Equal false", 5.5, 10.5, "==", false, false}, 147 | {"Invalid operator", 5.5, 10.5, "!=", false, true}, 148 | {"Invalid operator symbol", 5.5, 10.5, "invalid", false, true}, 149 | {"Negative numbers", -10.5, -5.5, "<", true, false}, 150 | {"Zero comparisons", 0.0, 1.5, "<", true, false}, 151 | } 152 | 153 | for _, test := range tests { 154 | t.Run(test.name, func(t *testing.T) { 155 | result, err := compareFloat64(test.first, test.second, test.operator) 156 | if test.expectError && err == nil { 157 | t.Errorf("Expected error for %s", test.name) 158 | } 159 | if !test.expectError && err != nil { 160 | t.Errorf("Unexpected error for %s: %v", test.name, err) 161 | } 162 | if !test.expectError && result != test.expected { 163 | t.Errorf("Expected %t for %s, got %t", test.expected, test.name, result) 164 | } 165 | }) 166 | } 167 | } 168 | 169 | // Integer validation tests 170 | func TestValidateDigitsBetweenInt64EdgeCases(t *testing.T) { 171 | tests := []struct { 172 | name string 173 | value int64 174 | left int64 175 | right int64 176 | expected bool 177 | }{ 178 | {"Value between bounds", 5, 1, 10, true}, 179 | {"Value at left bound", 1, 1, 10, true}, 180 | {"Value at right bound", 10, 1, 10, true}, 181 | {"Value below bounds", 0, 1, 10, false}, 182 | {"Value above bounds", 15, 1, 10, false}, 183 | {"Swapped bounds - value valid", 5, 10, 1, true}, 184 | {"Swapped bounds - value invalid", 15, 10, 1, false}, 185 | {"Negative values", -5, -10, -1, true}, 186 | {"Negative values invalid", -15, -10, -1, false}, 187 | {"Equal bounds", 5, 5, 5, true}, 188 | {"Equal bounds invalid", 4, 5, 5, false}, 189 | } 190 | 191 | for _, test := range tests { 192 | t.Run(test.name, func(t *testing.T) { 193 | result := ValidateDigitsBetweenInt64(test.value, test.left, test.right) 194 | if result != test.expected { 195 | t.Errorf("ValidateDigitsBetweenInt64(%d, %d, %d) = %t; expected %t", 196 | test.value, test.left, test.right, result, test.expected) 197 | } 198 | }) 199 | } 200 | } 201 | 202 | func TestCompareInt64AllOperators(t *testing.T) { 203 | tests := []struct { 204 | name string 205 | first int64 206 | second int64 207 | operator string 208 | expected bool 209 | expectError bool 210 | }{ 211 | {"Less than true", 5, 10, "<", true, false}, 212 | {"Less than false", 10, 5, "<", false, false}, 213 | {"Greater than true", 10, 5, ">", true, false}, 214 | {"Greater than false", 5, 10, ">", false, false}, 215 | {"Less than or equal true (less)", 5, 10, "<=", true, false}, 216 | {"Less than or equal true (equal)", 5, 5, "<=", true, false}, 217 | {"Less than or equal false", 10, 5, "<=", false, false}, 218 | {"Greater than or equal true (greater)", 10, 5, ">=", true, false}, 219 | {"Greater than or equal true (equal)", 5, 5, ">=", true, false}, 220 | {"Greater than or equal false", 5, 10, ">=", false, false}, 221 | {"Equal true", 5, 5, "==", true, false}, 222 | {"Equal false", 5, 10, "==", false, false}, 223 | {"Invalid operator", 5, 10, "!=", false, true}, 224 | {"Invalid operator symbol", 5, 10, "invalid", false, true}, 225 | {"Negative numbers less than", -10, -5, "<", true, false}, 226 | {"Negative numbers greater than", -5, -10, ">", true, false}, 227 | {"Zero comparisons", 0, 1, "<", true, false}, 228 | {"Zero equal", 0, 0, "==", true, false}, 229 | } 230 | 231 | for _, test := range tests { 232 | t.Run(test.name, func(t *testing.T) { 233 | result, err := compareInt64(test.first, test.second, test.operator) 234 | if test.expectError && err == nil { 235 | t.Errorf("Expected error for %s", test.name) 236 | } 237 | if !test.expectError && err != nil { 238 | t.Errorf("Unexpected error for %s: %v", test.name, err) 239 | } 240 | if !test.expectError && result != test.expected { 241 | t.Errorf("Expected %t for %s, got %t", test.expected, test.name, result) 242 | } 243 | }) 244 | } 245 | } 246 | 247 | // Additional comprehensive validation tests using the generic ValidateDigitsBetween function 248 | func TestValidateDigitsBetweenGeneric(t *testing.T) { 249 | tests := []struct { 250 | name string 251 | value interface{} 252 | params []string 253 | expected bool 254 | hasError bool 255 | }{ 256 | // Supported integer types (checks number of digits, not value) 257 | {"int 1 digit valid", 5, []string{"1", "10"}, true, false}, 258 | {"int 2 digits valid", 10, []string{"1", "10"}, true, false}, 259 | {"int 1 digit boundary", 0, []string{"1", "10"}, true, false}, 260 | {"int 3 digits valid", 123, []string{"1", "10"}, true, false}, 261 | {"int64 1 digit", int64(5), []string{"1", "10"}, true, false}, 262 | {"int64 2 digits", int64(12), []string{"1", "10"}, true, false}, 263 | {"int64 too many digits", int64(12345678901), []string{"1", "10"}, false, false}, 264 | 265 | // String digit validation (checks string length, must be numeric) 266 | {"string 1 digit", "5", []string{"1", "10"}, true, false}, 267 | {"string 2 digits", "12", []string{"1", "10"}, true, false}, 268 | {"string too long", "12345678901", []string{"1", "10"}, false, false}, 269 | {"string non-numeric", "ab", []string{"1", "10"}, false, true}, 270 | 271 | // Unsupported types should error 272 | {"uint unsupported", uint(5), []string{"1", "10"}, false, true}, 273 | {"uint64 unsupported", uint64(5), []string{"1", "10"}, false, true}, 274 | {"float32 unsupported", float32(5.5), []string{"1", "10"}, false, true}, 275 | {"float64 unsupported", float64(5.5), []string{"1", "10"}, false, true}, 276 | 277 | // Error cases 278 | {"wrong param count", 5, []string{"1"}, false, true}, 279 | {"invalid param", 5, []string{"invalid", "10"}, false, true}, 280 | {"complex type", complex64(1 + 2i), []string{"1", "10"}, false, true}, 281 | } 282 | 283 | for _, test := range tests { 284 | t.Run(test.name, func(t *testing.T) { 285 | result, err := ValidateDigitsBetween(test.value, test.params) 286 | if test.hasError { 287 | if err == nil { 288 | t.Errorf("Expected error for %s", test.name) 289 | } 290 | return 291 | } 292 | if err != nil { 293 | t.Errorf("Unexpected error for %s: %v", test.name, err) 294 | return 295 | } 296 | if result != test.expected { 297 | t.Errorf("Expected %t for %s, got %t", test.expected, test.name, result) 298 | } 299 | }) 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | This is a Go validation library (`go-validator`) that provides comprehensive validation for structs, strings, and other data types. It's optimized for Go 1.19+ and emphasizes proper error handling without panics. The library is designed for production use with web frameworks like Gin, Echo, and Iris. 8 | 9 | ## Key Architecture 10 | 11 | ### Core Components 12 | 13 | - **Validator (`validator.go`)**: Main validation engine with struct tag-based validation 14 | - **Error Handling (`error.go`)**: Comprehensive error types including `Errors`, `FieldError`, and `UnsupportedTypeError` 15 | - **Types (`types.go`)**: Custom validation function types and thread-safe custom rule mapping 16 | - **Cache (`cache.go`)**: Field metadata structures and validation tag parsing 17 | - **Translator (`translator.go`)**: Internationalization support with language-specific error messages 18 | - **Validation Rules**: Split across multiple files: 19 | - `validator_string.go` - String validation rules (email, alpha, numeric, etc.) 20 | - `validator_int.go` - Integer validation rules 21 | - `validator_float.go` - Float validation rules 22 | - `validator_unit.go` - Unit-specific validations 23 | 24 | ### Error Handling Architecture 25 | 26 | The library uses a sophisticated error handling system: 27 | - `Errors` type: Collection of multiple validation errors 28 | - `FieldError` type: Detailed field-specific errors with optional `FuncError` for internal errors 29 | - Error chaining support with `Unwrap()` method 30 | - Utility methods: `HasFieldError()`, `GetFieldError()`, `GroupByField()` 31 | 32 | ### Validation Flow 33 | 34 | 1. Struct validation uses reflection to process struct tags 35 | 2. Each field is validated against its `valid` tag rules 36 | 3. Custom validation functions can be registered via `CustomTypeRuleMap` 37 | 4. Errors are collected and returned as an `Errors` slice 38 | 39 | ## Development Commands 40 | 41 | ### Testing 42 | ```bash 43 | go test # Run all tests 44 | go test -v # Run tests with verbose output 45 | go test -run TestName # Run specific test 46 | go test -bench=. # Run benchmarks 47 | go test -bench=. -benchmem # Run benchmarks with memory stats 48 | go test -cover # Run tests with coverage 49 | go test -race # Run tests with race detection 50 | ``` 51 | 52 | ### Building & Validation 53 | ```bash 54 | go build # Build the package 55 | go mod tidy # Clean up dependencies 56 | go vet # Run go vet for static analysis 57 | go fmt # Format code 58 | gofmt -s -w . # Format and simplify code 59 | ``` 60 | 61 | ### Performance Testing 62 | ```bash 63 | go test -bench=BenchmarkErrorHandling # Test error handling performance 64 | go test -bench=BenchmarkGo119Performance # Test Go 1.19 optimizations 65 | go test -bench=BenchmarkMemoryAllocation # Test memory allocation patterns 66 | go test -bench=BenchmarkStringBuilderOptimization # Test string building performance 67 | go test -bench=BenchmarkFuncErrorHandling # Test FuncError functionality performance 68 | go test -bench=BenchmarkErrorUnwrapping # Test error unwrapping performance 69 | ``` 70 | 71 | ## Usage Patterns 72 | 73 | ### Basic Struct Validation 74 | ```go 75 | type User struct { 76 | Name string `valid:"required"` 77 | Email string `valid:"required,email"` 78 | Age int `valid:"min=18"` 79 | } 80 | 81 | err := validator.ValidateStruct(user) 82 | if err != nil { 83 | errors := err.(validator.Errors) 84 | // Use utility methods for error handling 85 | if errors.HasFieldError("Email") { 86 | fieldError := errors.GetFieldError("Email") 87 | fmt.Println("Email error:", fieldError.Message) 88 | } 89 | 90 | // Group errors by field for organized display 91 | groupedErrors := errors.GroupByField() 92 | for field, errs := range groupedErrors { 93 | fmt.Printf("Field %s has %d errors\n", field, len(errs)) 94 | } 95 | } 96 | ``` 97 | 98 | ### Custom Validation Rules 99 | ```go 100 | validator.CustomTypeRuleMap.Set("customRule", func(v reflect.Value, o reflect.Value, validTag *validator.ValidTag) bool { 101 | // Custom validation logic 102 | return true 103 | }) 104 | 105 | // Set custom error message 106 | validator.MessageMap["customRule"] = "Custom validation message with {Value}" 107 | ``` 108 | 109 | ### Error Handling with FuncError 110 | ```go 111 | // The library supports error chaining for internal function errors 112 | if fieldError.HasFuncError() { 113 | internalErr := fieldError.Unwrap() 114 | log.Printf("Internal validation error: %v", internalErr) 115 | } 116 | ``` 117 | 118 | ### Available Validation Rules 119 | 120 | **Core Rules**: `required`, `email`, `min`, `max`, `between`, `size`, `alpha`, `numeric`, `ip`, `url`, `uuid` 121 | 122 | **Conditional Rules**: `requiredIf`, `requiredUnless`, `requiredWith`, `requiredWithAll`, `requiredWithout`, `requiredWithoutAll` *(implemented in validator logic)* 123 | 124 | **Comparison Rules**: `gt`, `gte`, `lt`, `lte`, `same`, `distinct` 125 | 126 | **String Rules**: `alphaNum`, `alphaDash`, `alphaUnicode`, `alphaNumUnicode`, `alphaDashUnicode` 127 | 128 | **Network Rules**: `ipv4`, `ipv6`, `uuid3`, `uuid4`, `uuid5` 129 | 130 | **Type Rules**: `int`, `integer`, `float`, `digitsBetween` 131 | 132 | **Note**: All regex patterns are defined in `patterns.go`. Some conditional rules are implemented in the main validation logic but may require specific struct field relationships to function. 133 | 134 | ## Framework Integration Examples 135 | 136 | The `_examples/` directory contains production-ready integration examples: 137 | 138 | ### Web Framework Examples 139 | - **`simple/`**: Basic validation with comprehensive error handling 140 | - **`gin/`**: Gin framework integration with JSON API error responses 141 | - **`echo/`**: Echo framework integration with custom validators 142 | - **`iris/`**: Iris framework integration 143 | - **`translation/`**: Simple internationalization example 144 | - **`translations/`**: Advanced multi-language error messages 145 | - **`custom/`**: Custom validation rule implementation 146 | 147 | ### Key Integration Patterns 148 | ```go 149 | // Gin integration with localized errors 150 | func ValidateJSON(c *gin.Context, obj interface{}) error { 151 | if err := c.ShouldBindJSON(obj); err != nil { 152 | return err 153 | } 154 | 155 | // Apply custom validation 156 | if err := validator.ValidateStruct(obj); err != nil { 157 | // Convert to API-friendly error format 158 | return formatValidationErrors(err.(validator.Errors)) 159 | } 160 | return nil 161 | } 162 | ``` 163 | 164 | ## Testing Strategy 165 | 166 | ### Test Organization 167 | - **`validator_test.go`**: Core validation logic tests (1100+ lines) 168 | - **`benchmarks_test.go`**: Performance benchmarks for all major operations 169 | - **Error handling tests**: Edge cases, Go 1.19 features, FuncError chaining 170 | - **Framework examples**: Real-world integration testing 171 | 172 | ### Test Categories 173 | - **Unit tests**: Individual validation rules and error handling (`validator_test.go` - 1,177 lines) 174 | - **Integration tests**: Struct validation with complex nested types 175 | - **Performance tests**: Memory allocation and execution time benchmarks (`benchmarks_test.go` - 10 benchmark functions) 176 | - **Error chain tests**: FuncError unwrapping and error interface compatibility 177 | - **Framework integration**: Real-world examples in `_examples/` directory 178 | 179 | ## Language Support & Internationalization 180 | 181 | ### Built-in Languages 182 | - **English (`lang/en/`)**: Default language with comprehensive error messages 183 | - **Chinese Simplified (`lang/zh_CN/`)**: Complete translation set 184 | - **Chinese Traditional (`lang/zh_HK/`)**: Regional variant support 185 | 186 | ### Custom Language Implementation 187 | ```go 188 | // Register custom translator 189 | translator := validator.NewTranslator() 190 | translator.SetLanguage("fr") // French 191 | validator.MessageMap["required"] = "Ce champ est requis" 192 | ``` 193 | 194 | ## Performance Characteristics 195 | 196 | ### Go 1.19+ Optimizations 197 | - **String operations**: `strings.Builder` with pre-allocation (`builder.Grow()`) for efficient error message construction 198 | - **Object pooling**: `sync.Pool` for ErrorResponse slices in JSON marshaling to reduce GC pressure 199 | - **Slice pre-allocation**: Strategic capacity pre-sizing to minimize reallocations 200 | - **Runtime benefits**: Leverages Go 1.19+ improved garbage collector and memory management 201 | 202 | ### Benchmark Results Focus Areas 203 | - **Error handling**: Reduced allocations through efficient string building and object reuse 204 | - **JSON marshaling**: Object pooling reduces allocation overhead for API responses 205 | - **String concatenation**: Improved error message building with pre-allocated builders 206 | - **Memory profiling**: Allocation patterns monitored via `go test -benchmem` 207 | 208 | ## Development Best Practices 209 | 210 | ### Code Organization 211 | - Validation rules are logically separated by type (`validator_string.go`, `validator_int.go`, etc.) 212 | - Error types support both simple usage and advanced error chaining 213 | - Custom validation functions should be thread-safe and stateless 214 | - Use the existing patterns for parameter validation and error creation 215 | 216 | ### Performance Guidelines 217 | - Take advantage of object pooling patterns shown in `error.go` for high-frequency operations 218 | - Use benchmark tests when adding new validation rules to measure allocation impact 219 | - Pre-allocate slices with known capacity to reduce reallocations 220 | - Test with `-race` flag for concurrent usage validation 221 | - Consider using `strings.Builder` with `Grow()` for string concatenation in custom validators 222 | 223 | ## Additional Components 224 | 225 | ### Support Files 226 | - **`converter.go`**: Type conversion utilities for validation parameters 227 | - **`message.go`**: Default error message definitions and message mapping 228 | - **`patterns.go`**: Regular expression patterns for string validation rules 229 | - **`LICENSE`**: MIT license for the project 230 | - **`README.md`**: Comprehensive documentation with examples and feature descriptions 231 | 232 | ### Module Information 233 | - **Module**: `github.com/syssam/go-validator` 234 | - **Go Version**: Requires Go 1.19+ 235 | - **Dependencies**: Pure Go implementation with no external dependencies 236 | 237 | ## Quick Start for Development 238 | 239 | 1. **Clone and setup**: 240 | ```bash 241 | git clone https://github.com/syssam/go-validator 242 | cd go-validator 243 | go mod tidy 244 | ``` 245 | 246 | 2. **Run tests to verify setup**: 247 | ```bash 248 | go test -v 249 | go test -bench=. -benchmem 250 | ``` 251 | 252 | 3. **Study examples**: 253 | ```bash 254 | cd _examples/simple && go run main.go 255 | cd ../gin && go run main.go gin_validator.go 256 | ``` 257 | 258 | 4. **Common development workflow**: 259 | - Add new validation rules in appropriate `validator_*.go` files 260 | - Update `patterns.go` for regex-based rules 261 | - Add tests in `validator_test.go` 262 | - Add benchmarks in `benchmarks_test.go` 263 | - Update `message.go` for error messages -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

go-validator

2 |

3 | CI Status 4 | Go Report Card 5 | GoDoc 6 | Code Coverage 7 |

8 |

A package of validators and sanitizers for strings, structs and collections.

9 |

features:

10 | 15 |

Installation

16 |

Make sure that Go is installed on your computer. Type the following command in your terminal:

17 |

go get github.com/syssam/go-validator

18 |

Usage and documentation

19 |
Examples:
20 | 29 |

Available Validation Rules

30 | 70 |

omitempty

71 |

The "omitempty" option specifies that the field should be omitted from the encoding if the field has an empty value, defined as false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string.

72 |

required

73 |

The field under validation must be present in the input data and not empty. A field is considered "empty" if one of the following conditions are true:

74 |
75 | 80 |
81 |

requiredIf=anotherfield|value|...

82 |

The field under validation must be present and not empty if the anotherfield field is equal to any value.

83 |

requiredUnless=anotherfield|value|...

84 |

The field under validation must be present and not empty unless the anotherfield field is equal to any value.

85 |

requiredWith=anotherfield|anotherfield|...

86 |

The field under validation must be present and not empty only if any of the other specified fields are present.

87 |

requiredWithAll=anotherfield|anotherfield|...

88 |

The field under validation must be present and not empty only if all of the other specified fields are present.

89 |

requiredWithout=anotherfield|anotherfield|...

90 |

The field under validation must be present and not empty only when any of the other specified fields are not present.

91 |

requiredWithoutAll=anotherfield|anotherfield|...

92 |

The field under validation must be present and not empty only when all of the other specified fields are not present.

93 |

between=min|max

94 |

The field under validation must have a size between the given min and max. String, Number, Array, Map are evaluated in the same fashion as the size rule.

95 |

digitsBetween=min|max

96 |

The field under validation must have a length between the given min and max.

97 |

size=value

98 |

The field under validation must have a size matching the given value. For string data, value corresponds to the number of characters. For numeric data, value corresponds to a given integer value. For an array | map | slice, size corresponds to the count of the array | map | slice.

99 |

max=value

100 |

The field under validation must be less than or equal to a maximum value. String, Number, Array, Map are evaluated in the same fashion as the size rule.

101 |

min=value

102 |

The field under validation must be greater than or equal to a minimum value. String, Number, Array, Map are evaluated in the same fashion as the size rule.

103 |

same=anotherfield

104 |

The given field must match the field under validation.

105 |

gt=anotherfield

106 |

The field under validation must be greater than the given field. The two fields must be of the same type. String, Number, Array, Map are evaluated using the same conventions as the size rule.

107 |

gte=anotherfield

108 |

The field under validation must be greater than or equal to the given field. The two fields must be of the same type. String, Number, Array, Map are evaluated using the same conventions as the size rule.

109 |

lt=anotherfield

110 |

The field under validation must be less than the given field. The two fields must be of the same type. String, Number, Array, Map are evaluated using the same conventions as the size rule.

111 |

lte=anotherfield

112 |

The field under validation must be less than or equal to the given field. The two fields must be of the same type. String, Number, Array, Map are evaluated using the same conventions as the size rule.

113 |

distinct

114 |

The field under validation must not have any duplicate values.

115 |

email

116 |

The field under validation must be formatted as an e-mail address.

117 |

alpha

118 |

The field under validation may be only contains letters. Empty string is valid.

119 |

alphaNum

120 |

The field under validation may be only contains letters and numbers. Empty string is valid.

121 |

alphaDash

122 |

The field under validation may be only contains letters, numbers, dashes and underscores. Empty string is valid.

123 |

alphaUnicode

124 |

The field under validation may be only contains letters. Empty string is valid.

125 |

alphaNumUnicode

126 |

The field under validation may be only contains letters and numbers. Empty string is valid.

127 |

alphaDashUnicode

128 |

The field under validation may be only contains letters, numbers, dashes and underscores. Empty string is valid.

129 |

numeric

130 |

The field under validation must be numbers. Empty string is valid.

131 |

int

132 |

The field under validation must be int. Empty string is valid.

133 |

float

134 |

The field under validation must be float. Empty string is valid.

135 |

ip

136 |

The field under validation must be an IP address.

137 |

ipv4

138 |

The field under validation must be an IPv4 address.

139 |

ipv6

140 |

The field under validation must be an IPv6 address.

141 |

uuid3

142 |

The field under validation must be an uuid3.

143 |

uuid4

144 |

The field under validation must be an uuid4.

145 |

uuid5

146 |

The field under validation must be an uuid5.

147 |

uuid

148 |

The field under validation must be an uuid.

149 |

Custom Validation Rules

150 |
151 |
152 |   validator.CustomTypeTagMap.Set("customValidator", func CustomValidator(v reflect.Value, o reflect.Value, validTag *validator.ValidTag) bool {
153 |     return false
154 |   })
155 |   
156 |
157 |

List of functions:

158 |
159 |
160 |     IsNumeric(str string) bool
161 |     IsInt(str string) bool
162 |     IsFloat(str string) bool
163 |     IsNull(str string) bool
164 |     ValidateBetween(i interface{}, params []string) (bool, error)
165 |     ValidateDigitsBetween(i interface{}, params []string) (bool, error)
166 |     ValidateDigitsBetweenInt64(value, left, right int64) bool
167 |     ValidateDigitsBetweenFloat64(value, left, right float64) bool
168 |     ValidateGt(i interface{}, a interface{}) (bool, error)
169 |     ValidateGtFloat64(v, param float64) bool
170 |     ValidateGte(i interface{}, a interface{}) (bool, error)
171 |     ValidateGteFloat64(v, param float64) bool
172 |     ValidateLt(i interface{}, a interface{}) (bool, error)
173 |     ValidateLtFloat64(v, param float64) bool
174 |     ValidateLte(i interface{}, a interface{}) (bool, error)
175 |     ValidateLteFloat64(v, param float64) bool
176 |     ValidateRequired(i interface{}) bool
177 |     ValidateMin(i interface{}, params []string) (bool, error)
178 |     ValidateMinFloat64(v, param float64) bool
179 |     ValidateMax(i interface{}, params []string) (bool, error)
180 |     ValidateMaxFloat64(v, param float64) bool
181 |     ValidateSize(i interface{}, params []string) (bool, error)
182 |     ValidateDistinct(i interface{}) bool
183 |     ValidateEmail(str string) bool
184 |     ValidateAlpha(str string) bool
185 |     ValidateAlphaNum(str string) bool
186 |     ValidateAlphaDash(str string) bool
187 |     ValidateAlphaUnicode(str string) bool
188 |     ValidateAlphaNumUnicode(str string) bool
189 |     ValidateAlphaDashUnicode(str string) bool
190 |     ValidateIP(str string) bool
191 |     ValidateIPv4(str string) bool
192 |     ValidateIPv6(str string) bool
193 |     ValidateUUID3(str string) bool
194 |     ValidateUUID4(str string) bool
195 |     ValidateUUID5(str string) bool
196 |     ValidateUUID(str string) bool
197 |     ValidateURL(str string) bool
198 |   
199 |
200 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "reflect" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | // A field represents a single field found in a struct. 12 | type field struct { 13 | name string 14 | nameBytes []byte // []byte(name) 15 | structName string 16 | structNameBytes []byte // []byte(structName) 17 | attribute string 18 | defaultAttribute string 19 | tag bool 20 | index []int 21 | requiredTags requiredTags 22 | validTags otherValidTags 23 | typ reflect.Type 24 | omitEmpty bool 25 | } 26 | 27 | // A ValidTag represents parse validTag into field struct. 28 | type ValidTag struct { 29 | name string 30 | params []string 31 | messageName string 32 | messageParameters MessageParameters 33 | } 34 | 35 | // A otherValidTags represents parse validTag into field struct when validTag is not required... 36 | type otherValidTags []*ValidTag 37 | 38 | // A requiredTags represents parse validTag into field struct when validTag is required... 39 | type requiredTags []*ValidTag 40 | 41 | var fieldCache sync.Map // map[reflect.Type][]field 42 | 43 | // cachedTypefields is like typefields but uses a cache to avoid repeated work. 44 | func cachedTypefields(t reflect.Type) []field { 45 | if f, ok := fieldCache.Load(t); ok { 46 | if fields, ok := f.([]field); ok { 47 | return fields 48 | } 49 | } 50 | f, _ := fieldCache.LoadOrStore(t, typefields(t)) 51 | if fields, ok := f.([]field); ok { 52 | return fields 53 | } 54 | return []field{} 55 | } 56 | 57 | // shouldSkipField determines if a field should be skipped based on export status 58 | func shouldSkipField(sf reflect.StructField) bool { 59 | isUnexported := sf.PkgPath != "" 60 | if sf.Anonymous { 61 | t := sf.Type 62 | if t.Kind() == reflect.Ptr { 63 | t = t.Elem() 64 | } 65 | if isUnexported && t.Kind() != reflect.Struct { 66 | // Ignore embedded fields of unexported non-struct types. 67 | return true 68 | } 69 | // Do not ignore embedded fields of unexported struct types 70 | // since they may have exported fields. 71 | } else if isUnexported { 72 | // Ignore unexported non-embedded fields. 73 | return true 74 | } 75 | return false 76 | } 77 | 78 | // getFieldName extracts the field name from json tag or struct field name 79 | func getFieldName(sf reflect.StructField, f *field) string { 80 | name := sf.Tag.Get("json") 81 | if !f.isvalidTag(name) { 82 | name = "" 83 | } 84 | if name == "" { 85 | name = sf.Name 86 | } 87 | return name 88 | } 89 | 90 | // createFieldFromStructField creates a field struct from reflect.StructField 91 | func createFieldFromStructField(sf reflect.StructField, f *field, t, ft reflect.Type, index []int, validTag string) field { 92 | name := getFieldName(sf, f) 93 | tagged := sf.Tag.Get("json") != "" && f.isvalidTag(sf.Tag.Get("json")) 94 | requiredTags, otherValidTags, defaultAttribute := f.parseTagIntoSlice(validTag, ft) 95 | 96 | return field{ 97 | name: name, 98 | nameBytes: []byte(name), 99 | structName: t.Name() + "." + sf.Name, 100 | structNameBytes: []byte(t.Name() + "." + sf.Name), 101 | attribute: sf.Name, 102 | defaultAttribute: defaultAttribute, 103 | tag: tagged, 104 | index: index, 105 | requiredTags: requiredTags, 106 | validTags: otherValidTags, 107 | typ: ft, 108 | omitEmpty: strings.Contains(validTag, "omitempty"), 109 | } 110 | } 111 | 112 | // processStructField processes a single struct field and updates fields/next accordingly 113 | func processStructField(sf reflect.StructField, f *field, t reflect.Type, i int, count, nextCount map[reflect.Type]int, fields, next *[]field) { 114 | if shouldSkipField(sf) { 115 | return 116 | } 117 | 118 | validTag := sf.Tag.Get(tagName) 119 | if validTag == "-" { 120 | return 121 | } 122 | 123 | index := make([]int, len(f.index)+1) 124 | copy(index, f.index) 125 | index[len(f.index)] = i 126 | 127 | ft := sf.Type 128 | if validTag == "" && ft.Kind() != reflect.Slice && ft.Kind() != reflect.Array { 129 | return 130 | } 131 | 132 | if ft.Name() == "" && ft.Kind() == reflect.Ptr { 133 | // Follow pointer. 134 | ft = ft.Elem() 135 | } 136 | 137 | name := getFieldName(sf, f) 138 | 139 | // Record found field and index sequence. 140 | if name != sf.Name || !sf.Anonymous || ft.Kind() != reflect.Struct { 141 | count[f.typ]++ 142 | newField := createFieldFromStructField(sf, f, t, ft, index, validTag) 143 | *fields = append(*fields, newField) 144 | 145 | if count[f.typ] > 1 { 146 | // If there were multiple instances, add a second, 147 | // so that the annihilation code will see a duplicate. 148 | // It only cares about the distinction between 1 or 2, 149 | // so don't bother generating any more copies. 150 | *fields = append(*fields, (*fields)[len(*fields)-1]) 151 | } 152 | return 153 | } 154 | 155 | // Record new anonymous struct to explore in next round. 156 | nextCount[ft]++ 157 | if nextCount[ft] == 1 { 158 | newField := createFieldFromStructField(sf, f, t, ft, index, validTag) 159 | *next = append(*next, newField) 160 | } 161 | } 162 | 163 | // typefields returns a list of fields that Validator should recognize for the given type. 164 | // The algorithm is breadth-first search over the set of structs to include - the top struct 165 | // and then any reachable anonymous structs. 166 | func typefields(t reflect.Type) []field { 167 | current := make([]field, 0, t.NumField()) 168 | next := []field{{typ: t}} 169 | 170 | // Count of queued names for current level and the next. 171 | nextCount := map[reflect.Type]int{} 172 | 173 | // Types already visited at an earlier level. 174 | visited := map[reflect.Type]bool{} 175 | 176 | var fields []field 177 | 178 | for len(next) > 0 { 179 | current, next = next, current[:0] 180 | count, nextCount := nextCount, map[reflect.Type]int{} 181 | 182 | for _, f := range current { 183 | if visited[f.typ] { 184 | continue 185 | } 186 | visited[f.typ] = true 187 | for i := 0; i < f.typ.NumField(); i++ { 188 | sf := f.typ.Field(i) 189 | processStructField(sf, &f, t, i, count, nextCount, &fields, &next) 190 | } 191 | } 192 | } 193 | 194 | return fields 195 | } 196 | 197 | func (f *field) parseTagIntoSlice(tag string, ft reflect.Type) (requiredTags, otherValidTags, string) { 198 | options := strings.Split(tag, ",") 199 | var otherValidTags otherValidTags 200 | var requiredTags requiredTags 201 | defaultAttribute := "" 202 | 203 | for _, option := range options { 204 | option = strings.TrimSpace(option) 205 | 206 | tag := strings.Split(option, "=") 207 | var params []string 208 | 209 | if len(tag) == 2 { 210 | params = strings.Split(tag[1], "|") 211 | } 212 | 213 | switch tag[0] { 214 | case "attribute": 215 | if len(tag) == 2 { 216 | defaultAttribute = tag[1] 217 | } 218 | continue 219 | case "required", "requiredIf", "requiredUnless", "requiredWith", "requiredWithAll", "requiredWithout", "requiredWithoutAll": 220 | messageParameters, _ := f.parseMessageParameterIntoSlice(tag[0], params...) 221 | requiredTags = append(requiredTags, &ValidTag{ 222 | name: tag[0], 223 | params: params, 224 | messageName: f.parseMessageName(tag[0], ft), 225 | messageParameters: messageParameters, 226 | }) 227 | continue 228 | } 229 | 230 | messageParameters, _ := f.parseMessageParameterIntoSlice(tag[0], params...) 231 | otherValidTags = append(otherValidTags, &ValidTag{ 232 | name: tag[0], 233 | params: params, 234 | messageName: f.parseMessageName(tag[0], ft), 235 | messageParameters: messageParameters, 236 | }) 237 | } 238 | 239 | return requiredTags, otherValidTags, defaultAttribute 240 | } 241 | 242 | func (f *field) isvalidTag(s string) bool { 243 | if s == "" { 244 | return false 245 | } 246 | for _, c := range s { 247 | if strings.ContainsRune("\\'\"!#$%&()*+-./:<=>?@[]^_{|}~ ", c) { 248 | // Backslash and quote chars are reserved, but 249 | // otherwise anything goes. 250 | return false 251 | } 252 | } 253 | return true 254 | } 255 | 256 | //nolint:unused // Kept for potential future use 257 | func (f *field) isValidAttribute(s string) bool { 258 | if s == "" { 259 | return false 260 | } 261 | for _, c := range s { 262 | if strings.ContainsRune("\\'\"!#$%&()*+-./:<=>?@[]^_{|}~ ", c) { 263 | // Backslash and quote chars are reserved, but 264 | // otherwise anything goes. 265 | return false 266 | } 267 | } 268 | return true 269 | } 270 | 271 | func (f *field) parseMessageName(rule string, ft reflect.Type) string { 272 | messageName := rule 273 | 274 | switch rule { 275 | case "between", "gt", "gte", "lt", "lte", "min", "max", "size": 276 | switch ft.Kind() { 277 | case reflect.Int, reflect.Int8, reflect.Int16, 278 | reflect.Int32, reflect.Int64, 279 | reflect.Uint, reflect.Uint8, reflect.Uint16, 280 | reflect.Uint32, reflect.Uint64, 281 | reflect.Float32, reflect.Float64: 282 | return messageName + ".numeric" 283 | case reflect.String: 284 | return messageName + ".string" 285 | case reflect.Array, reflect.Slice, reflect.Map: 286 | return messageName + ".array" 287 | case reflect.Struct, reflect.Ptr: 288 | return messageName 289 | default: 290 | return messageName 291 | } 292 | default: 293 | return messageName 294 | } 295 | } 296 | 297 | type messageParameter struct { 298 | Key string 299 | Value string 300 | } 301 | 302 | // A MessageParameters represents store message parameter into field struct. 303 | type MessageParameters []messageParameter 304 | 305 | func (f *field) parseMessageParameterIntoSlice(rule string, params ...string) (MessageParameters, error) { 306 | var messageParameters MessageParameters 307 | 308 | switch rule { 309 | case "requiredUnless": 310 | if len(params) < 2 { 311 | return nil, errors.New("validator: " + rule + " format is not valid") 312 | } 313 | 314 | first := true 315 | var buff bytes.Buffer 316 | for _, v := range params[1:] { 317 | if first { 318 | first = false 319 | } else { 320 | buff.WriteByte(' ') 321 | buff.WriteByte(',') 322 | buff.WriteByte(' ') 323 | } 324 | 325 | buff.WriteString(v) 326 | } 327 | 328 | messageParameters = append( 329 | messageParameters, 330 | messageParameter{ 331 | Key: "Values", 332 | Value: buff.String(), 333 | }, 334 | ) 335 | case "between", "digitsBetween": 336 | if len(params) != 2 { 337 | return nil, errors.New("validator: " + rule + " format is not valid") 338 | } 339 | 340 | messageParameters = append( 341 | messageParameters, 342 | messageParameter{ 343 | Key: "Min", 344 | Value: params[0], 345 | }, messageParameter{ 346 | Key: "Max", 347 | Value: params[1], 348 | }, 349 | ) 350 | case "gt", "gte", "lt", "lte": 351 | if len(params) != 1 { 352 | return nil, errors.New("validator: " + rule + " format is not valid") 353 | } 354 | 355 | messageParameters = append( 356 | messageParameters, 357 | messageParameter{ 358 | Key: "Value", 359 | Value: params[0], 360 | }, 361 | ) 362 | case "max": 363 | if len(params) != 1 { 364 | return nil, errors.New("validator: " + rule + " format is not valid") 365 | } 366 | 367 | messageParameters = append( 368 | messageParameters, 369 | messageParameter{ 370 | Key: "Max", 371 | Value: params[0], 372 | }, 373 | ) 374 | case "min": 375 | if len(params) != 1 { 376 | return nil, errors.New("validator: " + rule + " format is not valid") 377 | } 378 | 379 | messageParameters = append( 380 | messageParameters, 381 | messageParameter{ 382 | Key: "Min", 383 | Value: params[0], 384 | }, 385 | ) 386 | case "size": 387 | if len(params) != 1 { 388 | return nil, errors.New("validator: " + rule + " format is not valid") 389 | } 390 | messageParameters = append( 391 | messageParameters, 392 | messageParameter{ 393 | Key: "Size", 394 | Value: params[0], 395 | }, 396 | ) 397 | } 398 | 399 | if len(messageParameters) > 0 { 400 | return messageParameters, nil 401 | } 402 | 403 | return nil, nil 404 | } 405 | --------------------------------------------------------------------------------