"},
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 |
4 |
5 |
6 |
7 |
8 | A package of validators and sanitizers for strings, structs and collections.
9 | features:
10 |
11 | - Customizable Attributes.
12 | - Customizable error messages.
13 | - Support i18n messages
14 |
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 |
76 | - The value is
nil.
77 | - The value is an empty string.
78 | - The value is an empty array | map
79 |
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 |
--------------------------------------------------------------------------------