├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── constants.go ├── custom └── custom_validator_test.go ├── error.go ├── error_test.go ├── factory.go ├── factory_test.go ├── generator ├── main.go ├── validator_number.gen.go.tpl ├── validator_number_p.gen.go.tpl ├── validator_number_p_test.gen.go.tpl └── validator_number_test.gen.go.tpl ├── go.mod ├── go.sum ├── locale.go ├── locale_de.go ├── locale_en.go ├── locale_es.go ├── locale_test.go ├── util.go ├── valgo.go ├── valgo_test.go ├── validation.go ├── validation_test.go ├── validator.go ├── validator_any.go ├── validator_any_test.go ├── validator_boolean.go ├── validator_boolean_p.go ├── validator_boolean_p_test.go ├── validator_boolean_test.go ├── validator_context.go ├── validator_number.gen.go ├── validator_number.go ├── validator_number_p.gen.go ├── validator_number_p.go ├── validator_number_p_test.gen.go ├── validator_number_p_test.go ├── validator_number_test.gen.go ├── validator_number_test.go ├── validator_string.go ├── validator_string_p.go ├── validator_string_p_test.go ├── validator_string_test.go ├── validator_time.go ├── validator_time_p.go ├── validator_time_p_test.go └── validator_time_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: daily -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | - main 6 | paths: 7 | - "**" 8 | - "!docs/**" 9 | - "!**.md" 10 | pull_request: 11 | paths: 12 | - "**" 13 | - "!docs/**" 14 | - "!**.md" 15 | 16 | name: Tests 17 | jobs: 18 | Build: 19 | strategy: 20 | matrix: 21 | go-version: [1.19.x, 1.20.x, 1.21.x, 1.22.x] 22 | platform: [ubuntu-latest] 23 | runs-on: ${{ matrix.platform }} 24 | steps: 25 | - name: Fetch Git Repository 26 | uses: actions/checkout@v4 27 | 28 | - name: Install Go 29 | uses: actions/setup-go@v5 30 | with: 31 | go-version: ${{ matrix.go-version }} 32 | 33 | - name: Install gotestsum 34 | run: go install gotest.tools/gotestsum@v1.11.0 35 | 36 | - name: Run Tests 37 | run: gotestsum -f testname -- ./... -race -count=1 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | vendor 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | *.prof 27 | 28 | # Output of the go coverage tool, specifically when used with LiteIDE 29 | *.out 30 | 31 | # external packages folder 32 | vendor/ 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2020 Carlos Forero 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | const ( 4 | ErrorKeyAfter = "after" 5 | ErrorKeyNotAfter = "not_after" 6 | 7 | ErrorKeyAfterOrEqualTo = "after_equal_to" 8 | ErrorKeyNotAfterOrEqualTo = "not_after_equal_to" 9 | 10 | ErrorKeyBefore = "before" 11 | ErrorKeyNotBefore = "not_before" 12 | 13 | ErrorKeyBeforeOrEqualTo = "before_equal_to" 14 | ErrorKeyNotBeforeOrEqualTo = "not_before_equal_to" 15 | 16 | ErrorKeyBetween = "between" 17 | ErrorKeyNotBetween = "not_between" 18 | 19 | ErrorKeyBlank = "blank" 20 | ErrorKeyNotBlank = "not_blank" 21 | 22 | ErrorKeyEmpty = "empty" 23 | ErrorKeyNotEmpty = "not_empty" 24 | 25 | ErrorKeyEqualTo = "equal_to" 26 | ErrorKeyNotEqualTo = "not_equal_to" 27 | 28 | ErrorKeyFalse = "false" 29 | ErrorKeyNotFalse = "not_false" 30 | 31 | ErrorKeyGreaterOrEqualTo = "greater_equal_to" 32 | ErrorKeyNotGreaterOrEqualTo = "not_greater_equal_to" 33 | 34 | ErrorKeyGreaterThan = "greater_than" 35 | ErrorKeyNotGreaterThan = "not_greater_than" 36 | 37 | ErrorKeyInSlice = "in_slice" 38 | ErrorKeyNotInSlice = "not_in_slice" 39 | 40 | ErrorKeyLength = "length" 41 | ErrorKeyNotLength = "not_length" 42 | 43 | ErrorKeyLengthBetween = "length_between" 44 | ErrorKeyNotLengthBetween = "not_length_between" 45 | 46 | ErrorKeyLessOrEqualTo = "less_or_equal_to" 47 | ErrorKeyNotLessOrEqualTo = "not_less_or_equal_to" 48 | 49 | ErrorKeyLessThan = "less_than" 50 | ErrorKeyNotLessThan = "not_less_than" 51 | 52 | ErrorKeyMatchingTo = "matching_to" 53 | ErrorKeyNotMatchingTo = "not_matching_to" 54 | 55 | ErrorKeyMaxLength = "max_length" 56 | ErrorKeyNotMaxLength = "not_max_length" 57 | 58 | ErrorKeyMinLength = "min_length" 59 | ErrorKeyNotMinLength = "not_min_length" 60 | 61 | ErrorKeyNil = "nil" 62 | ErrorKeyNotNil = "not_nil" 63 | 64 | ErrorKeyPassing = "passing" 65 | ErrorKeyNotPassing = "not_passing" 66 | 67 | ErrorKeyTrue = "true" 68 | ErrorKeyNotTrue = "not_true" 69 | 70 | ErrorKeyZero = "zero" 71 | ErrorKeyNotZero = "not_zero" 72 | ) 73 | -------------------------------------------------------------------------------- /custom/custom_validator_test.go: -------------------------------------------------------------------------------- 1 | package custom 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cohesivestack/valgo" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCustomValidator(t *testing.T) { 11 | 12 | locale := &valgo.Locale{ 13 | "not_valid_secret": "{{title}} is invalid.", 14 | } 15 | 16 | v := valgo.New(valgo.Options{Locale: locale}). 17 | Is(SecretWord("loose", "secret"). 18 | Correct()) 19 | 20 | assert.False(t, v.Valid()) 21 | assert.Len(t, v.Errors(), 1) 22 | assert.Len(t, v.Errors()["secret"].Messages(), 1) 23 | assert.Contains(t, v.Errors()["secret"].Messages(), "Secret is invalid.") 24 | 25 | v = valgo.Is(SecretWord("cohesive").Correct()) 26 | assert.True(t, v.Valid()) 27 | assert.Len(t, v.Errors(), 0) 28 | 29 | } 30 | 31 | type ValidatorSecretWord struct { 32 | context *valgo.ValidatorContext 33 | } 34 | 35 | func (validator *ValidatorSecretWord) Correct(template ...string) *ValidatorSecretWord { 36 | validator.context.Add( 37 | func() bool { 38 | return validator.context.Value().(string) == "cohesive" || 39 | validator.context.Value().(string) == "stack" 40 | }, 41 | "not_valid_secret", template...) 42 | 43 | return validator 44 | } 45 | 46 | func (validator *ValidatorSecretWord) Context() *valgo.ValidatorContext { 47 | return validator.context 48 | } 49 | 50 | func SecretWord(value string, nameAndTitle ...string) *ValidatorSecretWord { 51 | return &ValidatorSecretWord{context: valgo.NewContext(value, nameAndTitle...)} 52 | } 53 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/valyala/fasttemplate" 8 | ) 9 | 10 | // Implementation of the Go error interface in Valgo. The [Validation.Error()] 11 | // method returns a value of this type. 12 | // 13 | // There is a function in this type, [Errors()], that returns a list of errors 14 | // in a [Validation] session. 15 | type Error struct { 16 | errors map[string]*valueError 17 | marshalJsonFunc func(e *Error) ([]byte, error) 18 | } 19 | 20 | type errorTemplate struct { 21 | key string 22 | template *string 23 | params map[string]interface{} 24 | } 25 | 26 | // Contains information about each invalid field value returned by the 27 | // [Validation] session. 28 | type valueError struct { 29 | name *string 30 | title *string 31 | errorTemplates map[string]*errorTemplate 32 | errorMessages []string 33 | messages []string 34 | dirty bool 35 | validator *Validation 36 | } 37 | 38 | // The title of the invalid field value. 39 | func (ve *valueError) Title() string { 40 | // Lazy load the title 41 | if ve.title == nil { 42 | return humanizeName(*ve.name) 43 | } 44 | return *ve.title 45 | } 46 | 47 | // The name of the invalid field value. 48 | func (ve *valueError) Name() string { 49 | return *ve.name 50 | } 51 | 52 | // Error messages related to an invalid field value. 53 | func (ve *valueError) Messages() []string { 54 | if ve.dirty { 55 | ve.messages = []string{} 56 | for _, et := range ve.errorTemplates { 57 | ve.messages = append(ve.messages, ve.buildMessageFromTemplate(et)) 58 | } 59 | 60 | ve.messages = append(ve.messages, ve.errorMessages...) 61 | 62 | ve.dirty = false 63 | } 64 | 65 | return ve.messages 66 | } 67 | 68 | func (ve *valueError) buildMessageFromTemplate(et *errorTemplate) string { 69 | 70 | var ts string 71 | if et.template != nil { 72 | ts = *et.template 73 | } else if _ts, ok := (*ve.validator._locale)[et.key]; ok { 74 | ts = _ts 75 | } else { 76 | ts = concatString("ERROR: THERE IS NOT A MESSAGE WITH THE KEY: ", et.key) 77 | } 78 | 79 | var title string 80 | if ve.title == nil { 81 | title = humanizeName(*ve.name) 82 | } else { 83 | title = *ve.title 84 | } 85 | 86 | et.params["name"] = *ve.name 87 | et.params["title"] = title 88 | 89 | t := fasttemplate.New(ts, "{{", "}}") 90 | 91 | // Ensure interface{} values are string in order to be handle by fasttemplate 92 | for k, v := range et.params { 93 | if k != "name" && k != "title" { 94 | et.params[k] = fmt.Sprintf("%v", v) 95 | } 96 | } 97 | 98 | return t.ExecuteString(et.params) 99 | } 100 | 101 | // Return the error message associated with a Valgo error. 102 | func (e *Error) Error() string { 103 | count := len(e.errors) 104 | if count == 1 { 105 | return fmt.Sprintf("There is 1 error") 106 | } else { 107 | return fmt.Sprintf("There are %v errors", count) 108 | } 109 | } 110 | 111 | // Return a map with each Invalid value error. 112 | func (e *Error) Errors() map[string]*valueError { 113 | return e.errors 114 | } 115 | 116 | // Returns the JSON encoding of the validation error messages. 117 | // 118 | // A custom function can be set either by passing it as a parameter to 119 | // [validation.Error()] or through [FactoryOptions]. 120 | func (e *Error) MarshalJSON() ([]byte, error) { 121 | if e.marshalJsonFunc != nil { 122 | return e.marshalJsonFunc(e) 123 | } else { 124 | errors := map[string]interface{}{} 125 | 126 | for k, v := range e.errors { 127 | errors[k] = v.Messages() 128 | } 129 | 130 | return json.Marshal(errors) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "encoding/json" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNotError(t *testing.T) { 12 | for _, value := range []string{"", " "} { 13 | v := Is(String(value).Blank()) 14 | assert.True(t, v.Valid()) 15 | assert.NoError(t, v.Error()) 16 | } 17 | } 18 | 19 | func TestError(t *testing.T) { 20 | v := Is(String("Vitalik Buterin").Blank()) 21 | assert.False(t, v.Valid()) 22 | assert.Error(t, v.Error()) 23 | } 24 | 25 | func TestAddErrorMessageFromValidator(t *testing.T) { 26 | 27 | v := Is(String("Vitalik Buterin", "name").Blank()) 28 | 29 | assert.False(t, v.Valid()) 30 | assert.Len(t, v.Errors(), 1) 31 | assert.Len(t, v.Errors()["name"].Messages(), 1) 32 | assert.Contains(t, v.Errors()["name"].Messages(), "Name must be blank") 33 | 34 | v.AddErrorMessage("email", "Email is invalid") 35 | 36 | assert.Len(t, v.Errors(), 2) 37 | assert.False(t, v.Valid()) 38 | assert.Len(t, v.Errors()["name"].Messages(), 1) 39 | assert.Len(t, v.Errors()["email"].Messages(), 1) 40 | assert.Contains(t, v.Errors()["name"].Messages(), "Name must be blank") 41 | assert.Contains(t, v.Errors()["email"].Messages(), "Email is invalid") 42 | } 43 | 44 | func TestAddErrorMessageFromValgo(t *testing.T) { 45 | 46 | v := AddErrorMessage("email", "Email is invalid") 47 | 48 | assert.False(t, v.Valid()) 49 | assert.Len(t, v.Errors(), 1) 50 | assert.Len(t, v.Errors()["email"].Messages(), 1) 51 | assert.Contains(t, v.Errors()["email"].Messages(), "Email is invalid") 52 | 53 | v.Is(String("Vitalik Buterin", "name").Blank()) 54 | 55 | assert.Len(t, v.Errors(), 2) 56 | assert.False(t, v.Valid()) 57 | assert.Len(t, v.Errors()["email"].Messages(), 1) 58 | assert.Len(t, v.Errors()["name"].Messages(), 1) 59 | assert.Contains(t, v.Errors()["email"].Messages(), "Email is invalid") 60 | assert.Contains(t, v.Errors()["name"].Messages(), "Name must be blank") 61 | } 62 | 63 | func TestMultipleErrorsInOneFieldWithIs(t *testing.T) { 64 | r, _ := regexp.Compile("a") 65 | v := Is(String("", "email").Not().Blank().MatchingTo(r)) 66 | 67 | assert.Len(t, v.Errors(), 1) 68 | assert.False(t, v.Valid()) 69 | assert.Len(t, v.Errors()["email"].Messages(), 1) 70 | assert.Contains(t, v.Errors()["email"].Messages(), "Email can't be blank") 71 | } 72 | 73 | func TestMultipleErrorsInOneFieldWithCheck(t *testing.T) { 74 | r, _ := regexp.Compile("a") 75 | v := Check(String("", "email").Not().Blank().MatchingTo(r)) 76 | 77 | assert.Len(t, v.Errors(), 1) 78 | assert.False(t, v.Valid()) 79 | assert.Len(t, v.Errors()["email"].Messages(), 2) 80 | assert.Contains(t, v.Errors()["email"].Messages(), "Email can't be blank") 81 | assert.Contains(t, v.Errors()["email"].Messages(), "Email must match to \"a\"") 82 | } 83 | 84 | func TestErrorMarshallJSONWithIs(t *testing.T) { 85 | r, _ := regexp.Compile("a") 86 | v := Is(String("", "email").Not().Blank().MatchingTo(r)). 87 | Is(String("", "name").Not().Blank()) 88 | 89 | jsonByte, err := json.Marshal(v.Error()) 90 | assert.NoError(t, err) 91 | 92 | jsonMap := map[string][]interface{}{} 93 | err = json.Unmarshal(jsonByte, &jsonMap) 94 | assert.NoError(t, err) 95 | 96 | assert.Equal(t, "Name can't be blank", jsonMap["name"][0]) 97 | assert.Equal(t, "Email can't be blank", jsonMap["email"][0]) 98 | 99 | } 100 | 101 | func TestErrorMarshallJSONWithCheck(t *testing.T) { 102 | r, _ := regexp.Compile("a") 103 | v := Check(String("", "email").Not().Blank().MatchingTo(r)). 104 | Check(String("", "name").Not().Blank()) 105 | 106 | jsonByte, err := json.Marshal(v.Error()) 107 | assert.NoError(t, err) 108 | 109 | jsonMap := map[string][]interface{}{} 110 | err = json.Unmarshal(jsonByte, &jsonMap) 111 | assert.NoError(t, err) 112 | 113 | assert.Equal(t, "Name can't be blank", jsonMap["name"][0]) 114 | assert.Contains(t, jsonMap["email"], "Email can't be blank") 115 | assert.Contains(t, jsonMap["email"], "Email must match to \"a\"") 116 | } 117 | 118 | func TestIsValidByName(t *testing.T) { 119 | v := Is(String("Steve", "firstName").Not().Blank()). 120 | Is(String("", "lastName").Not().Blank()) 121 | 122 | assert.True(t, v.IsValid("firstName")) 123 | assert.False(t, v.IsValid("lastName")) 124 | } 125 | 126 | func TestCustomErrorMarshallJSON(t *testing.T) { 127 | 128 | customFunc := func(e *Error) ([]byte, error) { 129 | 130 | errors := map[string]interface{}{} 131 | 132 | for k, v := range e.errors { 133 | if len(v.Messages()) == 1 { 134 | errors[k] = v.Messages()[0] 135 | } else { 136 | errors[k] = v.Messages() 137 | } 138 | } 139 | 140 | // Add root level errors to customize errors interface 141 | return json.Marshal(map[string]map[string]interface{}{"errors": errors}) 142 | } 143 | 144 | r, _ := regexp.Compile("a") 145 | v := New(Options{MarshalJsonFunc: customFunc}). 146 | Check(String("", "email").Not().Blank().MatchingTo(r)). 147 | Check(String("", "name").Not().Blank()) 148 | 149 | jsonByte, err := json.Marshal(v.Error()) 150 | assert.NoError(t, err) 151 | 152 | jsonMap := map[string]map[string]interface{}{} 153 | err = json.Unmarshal(jsonByte, &jsonMap) 154 | assert.NoError(t, err) 155 | 156 | assert.Equal(t, "Name can't be blank", jsonMap["errors"]["name"]) 157 | emailErrors := jsonMap["errors"]["email"].([]interface{}) 158 | assert.Contains(t, emailErrors, "Email can't be blank") 159 | assert.Contains(t, emailErrors, "Email must match to \"a\"") 160 | } 161 | 162 | func TestCustomErrorMarshallJSONParameter(t *testing.T) { 163 | 164 | customFunc := func(e *Error) ([]byte, error) { 165 | 166 | errors := map[string]interface{}{} 167 | 168 | for k, v := range e.errors { 169 | if len(v.Messages()) == 1 { 170 | errors[k] = v.Messages()[0] 171 | } else { 172 | errors[k] = v.Messages() 173 | } 174 | } 175 | 176 | // Add root level errors to customize errors interface 177 | return json.Marshal(map[string]map[string]interface{}{"errors": errors}) 178 | } 179 | 180 | r, _ := regexp.Compile("a") 181 | v := Check( 182 | String("", "email").Not().Blank().MatchingTo(r), 183 | String("", "name").Not().Blank()) 184 | 185 | jsonByte, err := json.Marshal(v.Error(customFunc)) 186 | assert.NoError(t, err) 187 | 188 | jsonMap := map[string]map[string]interface{}{} 189 | err = json.Unmarshal(jsonByte, &jsonMap) 190 | assert.NoError(t, err) 191 | 192 | assert.Equal(t, "Name can't be blank", jsonMap["errors"]["name"]) 193 | emailErrors := jsonMap["errors"]["email"].([]interface{}) 194 | assert.Contains(t, emailErrors, "Email can't be blank") 195 | assert.Contains(t, emailErrors, "Email must match to \"a\"") 196 | } 197 | -------------------------------------------------------------------------------- /factory.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | // FactoryOptions is a struct in Go that is used to pass options to a [Factory()] 4 | type FactoryOptions struct { 5 | // A string field that represents the default locale code to use by the 6 | // factory if a specific locale code is not provided when a Validation is 7 | // created 8 | LocaleCodeDefault string 9 | // A map field that allows to modify the current or add new locales 10 | Locales map[string]*Locale 11 | // A function field that allows to set a custom JSON marshaler for [Error] 12 | MarshalJsonFunc func(e *Error) ([]byte, error) 13 | } 14 | 15 | // ValidationFactory is a struct provided by Valgo that enables the creation of 16 | // Validation sessions with preset options. This allows for more flexibility and 17 | // easier management of options when creating [Validation] sessions, as it avoids 18 | // having to pass options each time a new [Validation] is created. 19 | // 20 | // The [Factory()] function is used to create a ValidationFactory instance, and 21 | // it takes a [FactoryOptions] struct as a parameter. This allows customization 22 | // of the default locale code, addition of new locales, and setting a custom 23 | // JSON marshaler for errors. 24 | // 25 | // A ValidationFactory instance offers all the functions for creating 26 | // Validations available at the package level ([Is()], [In()], [Check()], [New()]), 27 | // which create new Validation sessions with the preset options defined in the 28 | // factory. 29 | 30 | type ValidationFactory struct { 31 | localeCodeDefault string 32 | locales map[string]*Locale 33 | marshalJsonFunc func(e *Error) ([]byte, error) 34 | } 35 | 36 | // This New function allows you to create, through a factory, a new Validation 37 | // session without a Validator. This is useful for conditional validation or 38 | // reusing validation logic. 39 | // 40 | // The function is similar to the [New()] function, but it uses a factory. 41 | // For more information see the [New()] function. 42 | func (_factory *ValidationFactory) New(options ...Options) *Validation { 43 | 44 | var _options *Options 45 | finalOptions := Options{ 46 | localeCodeDefaultFromFactory: _factory.localeCodeDefault, 47 | } 48 | 49 | if _factory.locales != nil { 50 | finalOptions.localesFromFactory = _factory.locales 51 | } 52 | 53 | if len(options) > 0 { 54 | _options = &options[0] 55 | } 56 | 57 | if _options != nil && _options.LocaleCode != "" { 58 | finalOptions.LocaleCode = _options.LocaleCode 59 | } 60 | 61 | if _options != nil && _options.MarshalJsonFunc != nil { 62 | finalOptions.MarshalJsonFunc = _options.MarshalJsonFunc 63 | } else if _factory.marshalJsonFunc != nil { 64 | finalOptions.MarshalJsonFunc = _factory.marshalJsonFunc 65 | } 66 | 67 | return newValidation(finalOptions) 68 | } 69 | 70 | // The Is function allows you to pass, through a factory, a [Validator] 71 | // with the value and the rules for validating it. At the same time, create a 72 | // [Validation] session, which lets you add more Validators in order to verify 73 | // more values. 74 | // 75 | // The function is similar to the [Is()] function, but it uses a factory. 76 | // For more information see the [Is()] function. 77 | func (_factory *ValidationFactory) Is(v Validator) *Validation { 78 | return _factory.New().Is(v) 79 | } 80 | 81 | // The In function executes, through a factory, one or more validators in a 82 | // namespace, so the value names in the error result are prefixed with this 83 | // namespace. This is useful for validating nested structures. 84 | // 85 | // The function is similar to the [In()] function, but it uses a factory. 86 | // For more information see the [In()] function. 87 | func (_factory *ValidationFactory) In(name string, v *Validation) *Validation { 88 | return _factory.New().In(name, v) 89 | } 90 | 91 | // The InRow function executes, through a factory, one or more validators in a 92 | // namespace similar to the [In](...) function, but with indexed namespace. So,\ 93 | // the value names in the error result are prefixed with this indexed namespace. 94 | // It is useful for validating nested lists in structures. 95 | // 96 | // The function is similar to the [InRow()] function, but it uses a factory. 97 | // For more information see the [InRow()] function. 98 | func (_factory *ValidationFactory) InRow(name string, index int, v *Validation) *Validation { 99 | return _factory.New().InRow(name, index, v) 100 | } 101 | 102 | // The Check function, through a factory, is similar to the Is function, however 103 | // with Check the Rules of the [Validator] parameter are not short-circuited, 104 | // which means that regardless of whether a previous rule was valid, all rules 105 | // are checked. 106 | // 107 | // The function is similar to the [Check()] function, but it uses a factory. 108 | // For more information see the [Check()] function. 109 | func (_factory *ValidationFactory) Check(v Validator) *Validation { 110 | return _factory.New().Check(v) 111 | } 112 | 113 | // Create a new [Validation] session, through a factory, and add an error 114 | // message to it without executing a field validator. By adding this error 115 | // message, the [Validation] session will be marked as invalid. 116 | func (_factory *ValidationFactory) AddErrorMessage(name string, message string) *Validation { 117 | return _factory.New().AddErrorMessage(name, message) 118 | } 119 | -------------------------------------------------------------------------------- /factory_test.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "encoding/json" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestFactoryUseLocaleCodeDefault(t *testing.T) { 12 | 13 | factory := Factory(FactoryOptions{ 14 | LocaleCodeDefault: LocaleCodeEs, 15 | }) 16 | 17 | v := factory.New().Is(String(" ").Not().Blank()) 18 | assert.Contains(t, v.Errors()["value_0"].Messages(), "Value 0 no puede estar en blanco") 19 | 20 | // Default localization must be persistent in the same validation 21 | v = v.Is(String(" ").Empty()) 22 | assert.Contains(t, v.Errors()["value_1"].Messages(), "Value 1 debe estar vacío") 23 | 24 | // Default localization must be persistent in the other validations 25 | v = factory.New().Is(String(" ").Not().Blank()) 26 | assert.Contains(t, v.Errors()["value_0"].Messages(), "Value 0 no puede estar en blanco") 27 | 28 | // Default localization must be possible to change at Validation level 29 | v = factory.New(Options{LocaleCode: LocaleCodeEn}).Is(String(" ").Not().Blank()) 30 | assert.Contains(t, v.Errors()["value_0"].Messages(), "Value 0 can't be blank") 31 | } 32 | 33 | func TestFactoryChangeLocaleEntries(t *testing.T) { 34 | 35 | originalErrorMessage0 := (*getLocaleEn())[ErrorKeyNotBlank] 36 | modifiedErrorMessage0 := "{{title}} should not be blank" 37 | 38 | originalErrorMessage1 := (*getLocaleEn())[ErrorKeyBlank] 39 | modifiedErrorMessage1 := "{{title}} should be blank" 40 | 41 | assert.NotEqual(t, originalErrorMessage0, modifiedErrorMessage0) 42 | assert.NotEqual(t, originalErrorMessage1, modifiedErrorMessage1) 43 | 44 | locale := &Locale{ 45 | ErrorKeyNotBlank: modifiedErrorMessage0, 46 | ErrorKeyBlank: modifiedErrorMessage1, 47 | } 48 | 49 | factory := Factory(FactoryOptions{ 50 | Locales: map[string]*Locale{ 51 | localeCodeDefault: locale, 52 | }, 53 | }) 54 | 55 | v := factory.New().Is(String(" ").Not().Blank()) 56 | assert.Contains(t, v.Errors()["value_0"].Messages(), "Value 0 should not be blank") 57 | 58 | v = v.Is(String("a").Blank()) 59 | assert.Contains(t, v.Errors()["value_1"].Messages(), "Value 1 should be blank") 60 | 61 | // Other entries should not be modified 62 | v = v.Is(String("").Not().Empty()) 63 | assert.Contains(t, v.Errors()["value_2"].Messages(), "Value 2 can't be empty") 64 | 65 | v = v.Is(String(" ").Empty()) 66 | assert.Contains(t, v.Errors()["value_3"].Messages(), "Value 3 must be empty") 67 | } 68 | 69 | func TestFactoryUseOtherLocaleAndChangeLocaleEntries(t *testing.T) { 70 | 71 | originalErrorMessage0 := (*getLocaleEs())[ErrorKeyNotBlank] 72 | modifiedErrorMessage0 := "{{title}} no debería estar en blanco" 73 | 74 | originalErrorMessage1 := (*getLocaleEs())[ErrorKeyBlank] 75 | modifiedErrorMessage1 := "{{title}} debería estar en blanco" 76 | 77 | assert.NotEqual(t, originalErrorMessage0, modifiedErrorMessage0) 78 | assert.NotEqual(t, originalErrorMessage1, modifiedErrorMessage1) 79 | 80 | locale := &Locale{ 81 | ErrorKeyNotBlank: modifiedErrorMessage0, 82 | ErrorKeyBlank: modifiedErrorMessage1, 83 | } 84 | 85 | factory := Factory(FactoryOptions{ 86 | Locales: map[string]*Locale{ 87 | LocaleCodeEs: locale, 88 | }, 89 | }) 90 | 91 | v := factory.New(Options{LocaleCode: LocaleCodeEs}).Is(String(" ").Not().Blank()) 92 | assert.Contains(t, v.Errors()["value_0"].Messages(), "Value 0 no debería estar en blanco") 93 | 94 | v = v.Is(String("a").Blank()) 95 | assert.Contains(t, v.Errors()["value_1"].Messages(), "Value 1 debería estar en blanco") 96 | 97 | // Other entries should not be modified 98 | v = v.Is(String("").Not().Empty()) 99 | assert.Contains(t, v.Errors()["value_2"].Messages(), "Value 2 no puede estar vacío") 100 | 101 | v = v.Is(String(" ").Empty()) 102 | assert.Contains(t, v.Errors()["value_3"].Messages(), "Value 3 debe estar vacío") 103 | } 104 | 105 | func TestFactoryAddNewLocaleEntries(t *testing.T) { 106 | 107 | locale := &Locale{ 108 | ErrorKeyNotBlank: "{{title}} can't be blank (XX)", 109 | ErrorKeyBlank: "{{title}} must be blank (XX)", 110 | } 111 | 112 | factory := Factory(FactoryOptions{ 113 | Locales: map[string]*Locale{ 114 | "xx": locale, 115 | }, 116 | }) 117 | 118 | v := factory.New(Options{LocaleCode: "xx"}).Is(String(" ").Not().Blank()) 119 | assert.Contains(t, v.Errors()["value_0"].Messages(), "Value 0 can't be blank (XX)") 120 | 121 | v = v.Is(String("a").Blank()) 122 | assert.Contains(t, v.Errors()["value_1"].Messages(), "Value 1 must be blank (XX)") 123 | 124 | // For the unexisting keys, then should use the default language 125 | v = v.Is(String("").Not().Empty()) 126 | assert.Contains(t, v.Errors()["value_2"].Messages(), "Value 2 can't be empty") 127 | 128 | v = v.Is(String(" ").Empty()) 129 | assert.Contains(t, v.Errors()["value_3"].Messages(), "Value 3 must be empty") 130 | 131 | // Use new locale Entries but changing the default in the Factory 132 | factory = Factory(FactoryOptions{ 133 | LocaleCodeDefault: LocaleCodeEs, 134 | Locales: map[string]*Locale{ 135 | "xx": locale, 136 | }, 137 | }) 138 | 139 | v = factory.New().Is(String(" ").Not().Blank()) 140 | assert.Contains(t, v.Errors()["value_0"].Messages(), "Value 0 can't be blank (XX)") 141 | 142 | v = v.Is(String("a").Blank()) 143 | assert.Contains(t, v.Errors()["value_1"].Messages(), "Value 1 must be blank (XX)") 144 | 145 | // For the unexisting keys, then should use the default language 146 | v = v.Is(String("").Not().Empty()) 147 | assert.Contains(t, v.Errors()["value_2"].Messages(), "Value 2 no puede estar vacío") 148 | 149 | v = v.Is(String(" ").Empty()) 150 | assert.Contains(t, v.Errors()["value_3"].Messages(), "Value 3 debe estar vacío") 151 | 152 | // Use new locale Entries but changing the default in the Factory to be the 153 | // same new unexisting locale. That will use the Valgo default locale ("en") 154 | factory = Factory(FactoryOptions{ 155 | LocaleCodeDefault: "xx", 156 | Locales: map[string]*Locale{ 157 | "xx": locale, 158 | }, 159 | }) 160 | 161 | v = factory.New().Is(String(" ").Not().Blank()) 162 | assert.Contains(t, v.Errors()["value_0"].Messages(), "Value 0 can't be blank (XX)") 163 | 164 | v = v.Is(String("a").Blank()) 165 | assert.Contains(t, v.Errors()["value_1"].Messages(), "Value 1 must be blank (XX)") 166 | 167 | // For the unexisting keys, then should use the default language 168 | v = v.Is(String("").Not().Empty()) 169 | assert.Contains(t, v.Errors()["value_2"].Messages(), "Value 2 can't be empty") 170 | 171 | v = v.Is(String(" ").Empty()) 172 | assert.Contains(t, v.Errors()["value_3"].Messages(), "Value 3 must be empty") 173 | } 174 | 175 | func TestFactoryCustomErrorMarshallJSON(t *testing.T) { 176 | 177 | customFunc := func(e *Error) ([]byte, error) { 178 | 179 | errors := map[string]interface{}{} 180 | 181 | for k, v := range e.errors { 182 | if len(v.Messages()) == 1 { 183 | errors[k] = v.Messages()[0] 184 | } else { 185 | errors[k] = v.Messages() 186 | } 187 | } 188 | 189 | // Add root level errors to customize errors interface 190 | return json.Marshal(map[string]map[string]interface{}{"errors": errors}) 191 | } 192 | 193 | factory := Factory(FactoryOptions{ 194 | MarshalJsonFunc: customFunc, 195 | }) 196 | 197 | r, _ := regexp.Compile("a") 198 | v := factory.New(). 199 | Check(String("", "email").Not().Blank().MatchingTo(r)). 200 | Check(String("", "name").Not().Blank()) 201 | 202 | jsonByte, err := json.Marshal(v.Error()) 203 | assert.NoError(t, err) 204 | 205 | jsonMap := map[string]map[string]interface{}{} 206 | err = json.Unmarshal(jsonByte, &jsonMap) 207 | assert.NoError(t, err) 208 | 209 | assert.Equal(t, "Name can't be blank", jsonMap["errors"]["name"]) 210 | emailErrors := jsonMap["errors"]["email"].([]interface{}) 211 | assert.Contains(t, emailErrors, "Email can't be blank") 212 | assert.Contains(t, emailErrors, "Email must match to \"a\"") 213 | 214 | customFuncAtValidationLevel := func(e *Error) ([]byte, error) { 215 | 216 | errors := map[string]string{"errors": "overridden"} 217 | 218 | // Add root level errors to customize errors interface 219 | return json.Marshal(errors) 220 | } 221 | 222 | // Marshal JSON should be overridden at Validation level 223 | v = factory.New(Options{MarshalJsonFunc: customFuncAtValidationLevel}). 224 | Check(String("", "email").Not().Blank().MatchingTo(r)). 225 | Check(String("", "name").Not().Blank()) 226 | 227 | jsonByte, err = json.Marshal(v.Error()) 228 | assert.NoError(t, err) 229 | 230 | jsonMapAtValidationLevel := map[string]string{} 231 | err = json.Unmarshal(jsonByte, &jsonMapAtValidationLevel) 232 | assert.NoError(t, err) 233 | 234 | assert.Equal(t, "overridden", jsonMapAtValidationLevel["errors"]) 235 | } 236 | -------------------------------------------------------------------------------- /generator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "text/template" 8 | 9 | "golang.org/x/text/cases" 10 | "golang.org/x/text/language" 11 | ) 12 | 13 | func main() { 14 | 15 | types := []string{ 16 | "uint8", 17 | "uint16", 18 | "uint32", 19 | "uint64", 20 | "int", 21 | "int8", 22 | "int16", 23 | "int32", 24 | "int64", 25 | "float32", 26 | "float64", 27 | "byte", 28 | "rune", 29 | } 30 | 31 | type dataType struct { 32 | Name string 33 | Type string 34 | } 35 | 36 | dataTypes := []dataType{} 37 | 38 | for _, t := range types { 39 | dataTypes = append(dataTypes, dataType{ 40 | Name: cases.Title(language.Und, cases.NoLower).String(t), 41 | Type: t, 42 | }) 43 | } 44 | 45 | tmpl, err := template.ParseGlob(filepath.Join("generator", "*.tpl")) 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | for _, fileName := range []string{ 51 | "validator_number.gen.go", 52 | "validator_number_test.gen.go", 53 | "validator_number_p.gen.go", 54 | "validator_number_p_test.gen.go"} { 55 | 56 | templateName := fmt.Sprintf("%s.tpl", fileName) 57 | 58 | output, err := os.Create(fileName) 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | err = tmpl.ExecuteTemplate(output, templateName, dataTypes) 64 | if err != nil { 65 | panic(err) 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /generator/validator_number.gen.go.tpl: -------------------------------------------------------------------------------- 1 | // Code generated by Valgo; DO NOT EDIT. 2 | package valgo 3 | 4 | {{ range . -}} 5 | func is{{ .Name }}EqualTo[T ~{{ .Type }}](v0 T, v1 T) bool { 6 | return v0 == v1 7 | } 8 | func is{{ .Name }}GreaterThan[T ~{{ .Type }}](v0 T, v1 T) bool { 9 | return v0 > v1 10 | } 11 | func is{{ .Name }}GreaterOrEqualTo[T ~{{ .Type }}](v0 T, v1 T) bool { 12 | return v0 >= v1 13 | } 14 | func is{{ .Name }}LessThan[T ~{{ .Type }}](v0 T, v1 T) bool { 15 | return v0 < v1 16 | } 17 | func is{{ .Name }}LessOrEqualTo[T ~{{ .Type }}](v0 T, v1 T) bool { 18 | return v0 <= v1 19 | } 20 | func is{{ .Name }}Between[T ~{{ .Type }}](v T, min T, max T) bool { 21 | return v >= min && v <= max 22 | } 23 | func is{{ .Name }}Zero[T ~{{ .Type }}](v T) bool { 24 | return v == 0 25 | } 26 | func is{{ .Name }}InSlice[T ~{{ .Type }}](v T, slice []T) bool { 27 | for _, _v := range slice { 28 | if v == _v { 29 | return true 30 | } 31 | } 32 | return false 33 | } 34 | 35 | // The {{ .Type }} validator type that keeps its validator context. 36 | type Validator{{ .Name }}[T ~{{ .Type }}] struct { 37 | context *ValidatorContext 38 | } 39 | 40 | // Receives the {{ .Type }} value to validate. 41 | // 42 | // The value also can be a custom {{ .Type }} type such as `type Level {{ .Type }};` 43 | // 44 | // Optionally, the function can receive a name and title, in that order, 45 | // to be displayed in the error messages. A `value_%N`` pattern is used as a name in 46 | // error messages if a name and title are not supplied; for example: value_0. 47 | // When the name is provided but not the title, then the name is humanized to be 48 | // used as the title as well; for example the name `phone_number` will be 49 | // humanized as `Phone {{ .Name }}` 50 | 51 | func {{ .Name }}[T ~{{ .Type }}](value T, nameAndTitle ...string) *Validator{{ .Name }}[T] { 52 | return &Validator{{ .Name }}[T]{context: NewContext(value, nameAndTitle...)} 53 | } 54 | 55 | // Return the context of the validator. The context is useful to create a custom 56 | // validator by extending this validator. 57 | func (validator *Validator{{ .Name }}[T]) Context() *ValidatorContext { 58 | return validator.context 59 | } 60 | 61 | // Invert the logical value associated with the next validator function. 62 | // For example: 63 | // 64 | // // It will return false because Not() inverts the boolean value associated with the Zero() function 65 | // Is(v.{{ .Name }}({{ .Type }}(0)).Not().Zero()).Valid() 66 | func (validator *Validator{{ .Name }}[T]) Not() *Validator{{ .Name }}[T] { 67 | validator.context.Not() 68 | 69 | return validator 70 | } 71 | 72 | // Introduces a logical OR in the chain of validation conditions, affecting the 73 | // evaluation order and priority of subsequent validators. A value passes the 74 | // validation if it meets any one condition following the Or() call, adhering to 75 | // a left-to-right evaluation. This mechanism allows for validating against 76 | // multiple criteria where satisfying any single criterion is sufficient. 77 | // Example: 78 | // 79 | // // This validator will pass because the input is Zero. 80 | // input := {{ .Type }}(0) 81 | // isValid := v.Is(v.{{ .Name }}(&input).GreaterThan(5).Or().Zero()).Valid() 82 | func (validator *Validator{{ .Name }}[T]) Or() *Validator{{ .Name }}[T] { 83 | validator.context.Or() 84 | 85 | return validator 86 | } 87 | 88 | // Validate if the {{ .Type }} value is equal to another. This function internally uses 89 | // the golang `==` operator. 90 | // For example: 91 | // 92 | // quantity := {{ .Type }}(2) 93 | // Is(v.{{ .Name }}(quantity).Equal({{ .Type }}(2))) 94 | func (validator *Validator{{ .Name }}[T]) EqualTo(value T, template ...string) *Validator{{ .Name }}[T] { 95 | validator.context.AddWithValue( 96 | func() bool { 97 | return is{{ .Name }}EqualTo(validator.context.Value().(T), value) 98 | }, 99 | ErrorKeyEqualTo, value, template...) 100 | 101 | return validator 102 | } 103 | 104 | // Validate if the {{ .Type }} value is greater than another. This function internally 105 | // uses the golang `>` operator. 106 | // For example: 107 | // 108 | // quantity := {{ .Type }}(3) 109 | // Is(v.{{ .Name }}(quantity).GreaterThan({{ .Type }}(2))) 110 | func (validator *Validator{{ .Name }}[T]) GreaterThan(value T, template ...string) *Validator{{ .Name }}[T] { 111 | validator.context.AddWithValue( 112 | func() bool { 113 | return is{{ .Name }}GreaterThan(validator.context.Value().(T), value) 114 | }, 115 | ErrorKeyGreaterThan, value, template...) 116 | 117 | return validator 118 | } 119 | 120 | // Validate if the {{ .Type }} value is greater than or equal to another. This function 121 | // internally uses the golang `>=` operator. 122 | // For example: 123 | // 124 | // quantity := {{ .Type }}(3) 125 | // Is(v.{{ .Name }}(quantity).GreaterOrEqualTo({{ .Type }}(3))) 126 | func (validator *Validator{{ .Name }}[T]) GreaterOrEqualTo(value T, template ...string) *Validator{{ .Name }}[T] { 127 | validator.context.AddWithValue( 128 | func() bool { 129 | return is{{ .Name }}GreaterOrEqualTo(validator.context.Value().(T), value) 130 | }, 131 | ErrorKeyGreaterOrEqualTo, value, template...) 132 | 133 | return validator 134 | } 135 | 136 | // Validate if the {{ .Type }} value is less than another. This function internally 137 | // uses the golang `<` operator. 138 | // For example: 139 | // 140 | // quantity := {{ .Type }}(2) 141 | // Is(v.{{ .Name }}(quantity).LessThan({{ .Type }}(3))) 142 | func (validator *Validator{{ .Name }}[T]) LessThan(value T, template ...string) *Validator{{ .Name }}[T] { 143 | validator.context.AddWithValue( 144 | func() bool { 145 | return is{{ .Name }}LessThan(validator.context.Value().(T), value) 146 | }, 147 | ErrorKeyLessThan, value, template...) 148 | 149 | return validator 150 | } 151 | 152 | // Validate if the {{ .Type }} value is less than or equal to another. This function 153 | // internally uses the golang `<=` operator. 154 | // For example: 155 | // 156 | // quantity := {{ .Type }}(2) 157 | // Is(v.{{ .Name }}(quantity).LessOrEqualTo({{ .Type }}(2))) 158 | func (validator *Validator{{ .Name }}[T]) LessOrEqualTo(value T, template ...string) *Validator{{ .Name }}[T] { 159 | validator.context.AddWithValue( 160 | func() bool { 161 | return is{{ .Name }}LessOrEqualTo(validator.context.Value().(T), value) 162 | }, 163 | ErrorKeyLessOrEqualTo, value, template...) 164 | 165 | return validator 166 | } 167 | 168 | // Validate if the {{ .Type }} is within a range (inclusive). 169 | // For example: 170 | // 171 | // Is(v.{{ .Name }}({{ .Type }}(3)).Between({{ .Type }}(2),{{ .Type }}(6))) 172 | func (validator *Validator{{ .Name }}[T]) Between(min T, max T, template ...string) *Validator{{ .Name }}[T] { 173 | validator.context.AddWithParams( 174 | func() bool { 175 | return is{{ .Name }}Between(validator.context.Value().(T), min, max) 176 | }, 177 | ErrorKeyBetween, 178 | map[string]any{"title": validator.context.title, "min": min, "max": max}, 179 | template...) 180 | 181 | return validator 182 | } 183 | 184 | // Validate if the {{ .Type }} value is zero. 185 | // 186 | // For example: 187 | // 188 | // Is(v.{{ .Name }}({{ .Type }}(0)).Zero()) 189 | func (validator *Validator{{ .Name }}[T]) Zero(template ...string) *Validator{{ .Name }}[T] { 190 | validator.context.Add( 191 | func() bool { 192 | return is{{ .Name }}Zero(validator.context.Value().(T)) 193 | }, 194 | ErrorKeyZero, template...) 195 | 196 | return validator 197 | } 198 | 199 | // Validate if the {{ .Type }} value passes a custom function. 200 | // For example: 201 | // 202 | // quantity := {{ .Type }}(2) 203 | // Is(v.{{ .Name }}(quantity).Passing((v {{ .Type }}) bool { 204 | // return v == getAllowedQuantity() 205 | // }) 206 | func (validator *Validator{{ .Name }}[T]) Passing(function func(v T) bool, template ...string) *Validator{{ .Name }}[T] { 207 | validator.context.Add( 208 | func() bool { 209 | return function(validator.context.Value().(T)) 210 | }, 211 | ErrorKeyPassing, template...) 212 | 213 | return validator 214 | } 215 | 216 | // Validate if the {{ .Type }} value is present in the {{ .Type }} slice. 217 | // For example: 218 | // 219 | // quantity := {{ .Type }}(3) 220 | // validQuantities := []{{ .Type }}{1,3,5} 221 | // Is(v.{{ .Name }}(quantity).InSlice(validQuantities)) 222 | func (validator *Validator{{ .Name }}[T]) InSlice(slice []T, template ...string) *Validator{{ .Name }}[T] { 223 | validator.context.AddWithValue( 224 | func() bool { 225 | return is{{ .Name }}InSlice(validator.context.Value().(T), slice) 226 | }, 227 | ErrorKeyInSlice, validator.context.Value(), template...) 228 | 229 | return validator 230 | } 231 | {{ end }} -------------------------------------------------------------------------------- /generator/validator_number_p.gen.go.tpl: -------------------------------------------------------------------------------- 1 | // Code generated by Valgo; DO NOT EDIT. 2 | package valgo 3 | {{ range . }} 4 | // The {{ .Type }} pointer validator type that keeps its validator context. 5 | type Validator{{ .Name }}P[T ~{{ .Type }}] struct { 6 | context *ValidatorContext 7 | } 8 | 9 | // Receives the {{ .Type }} pointer to validate. 10 | // 11 | // The value also can be a custom {{ .Type }} type such as `type Level *{{ .Type }};` 12 | // 13 | // Optionally, the function can receive a name and title, in that order, 14 | // to be used in the error messages. A `value_%N“ pattern is used as a name in 15 | // error messages if a name and title are not supplied; for example: value_0. 16 | // When the name is provided but not the title, then the name is humanized to be 17 | // used as the title as well; for example the name `phone_number` will be 18 | // humanized as `Phone Number` 19 | func {{ .Name }}P[T ~{{ .Type }}](value *T, nameAndTitle ...string) *Validator{{ .Name }}P[T] { 20 | return &Validator{{ .Name }}P[T]{context: NewContext(value, nameAndTitle...)} 21 | } 22 | 23 | // Return the context of the validator. The context is useful to create a custom 24 | // validator by extending this validator. 25 | func (validator *Validator{{ .Name }}P[T]) Context() *ValidatorContext { 26 | return validator.context 27 | } 28 | 29 | // Invert the boolean value associated with the next validator function. 30 | // For example: 31 | // 32 | // // It will return false because Not() inverts the boolean value associated with the Zero() function 33 | // n := {{ .Type }}(0) 34 | // Is(v.{{ .Name }}P(&n).Not().Zero()).Valid() 35 | func (validator *Validator{{ .Name }}P[T]) Not() *Validator{{ .Name }}P[T] { 36 | validator.context.Not() 37 | 38 | return validator 39 | } 40 | 41 | // Introduces a logical OR in the chain of validation conditions, affecting the 42 | // evaluation order and priority of subsequent validators. A value passes the 43 | // validation if it meets any one condition following the Or() call, adhering to 44 | // a left-to-right evaluation. This mechanism allows for validating against 45 | // multiple criteria where satisfying any single criterion is sufficient. 46 | // Example: 47 | // 48 | // // This validator will pass because the input is Zero. 49 | // input := {{ .Type }}(0) 50 | // isValid := v.Is(v.{{ .Name }}P(&input).GreaterThan(5).Or().Zero()).Valid() 51 | func (validator *Validator{{ .Name }}P[T]) Or() *Validator{{ .Name }}P[T] { 52 | validator.context.Or() 53 | 54 | return validator 55 | } 56 | 57 | // Validate if the {{ .Type }} pointer value is equal to another value. This function internally uses 58 | // the golang `==` operator. 59 | // For example: 60 | // 61 | // quantity := {{ .Type }}(2) 62 | // Is(v.{{ .Name }}P(quantity).Equal({{ .Type }}(2))) 63 | func (validator *Validator{{ .Name }}P[T]) EqualTo(value T, template ...string) *Validator{{ .Name }}P[T] { 64 | validator.context.AddWithValue( 65 | func() bool { 66 | return validator.context.Value().(*T) != nil && is{{ .Name }}EqualTo(*(validator.context.Value().(*T)), value) 67 | }, 68 | ErrorKeyEqualTo, value, template...) 69 | 70 | return validator 71 | } 72 | 73 | // Validate if the {{ .Type }} pointer value is greater than another value. This function internally 74 | // uses the golang `>` operator. 75 | // For example: 76 | // 77 | // quantity := {{ .Type }}(3) 78 | // Is(v.{{ .Name }}P(&quantity).GreaterThan({{ .Type }}(2))) 79 | func (validator *Validator{{ .Name }}P[T]) GreaterThan(value T, template ...string) *Validator{{ .Name }}P[T] { 80 | validator.context.AddWithValue( 81 | func() bool { 82 | return validator.context.Value().(*T) != nil && is{{ .Name }}GreaterThan(*(validator.context.Value().(*T)), value) 83 | }, 84 | ErrorKeyGreaterThan, value, template...) 85 | 86 | return validator 87 | } 88 | 89 | // Validate if the {{ .Type }} pointer value is greater than or equal to another value. This function 90 | // internally uses the golang `>=` operator. 91 | // For example: 92 | // 93 | // quantity := {{ .Type }}(3) 94 | // Is(v.{{ .Name }}P(&quantity).GreaterOrEqualTo({{ .Type }}(3))) 95 | func (validator *Validator{{ .Name }}P[T]) GreaterOrEqualTo(value T, template ...string) *Validator{{ .Name }}P[T] { 96 | validator.context.AddWithValue( 97 | func() bool { 98 | return validator.context.Value().(*T) != nil && is{{ .Name }}GreaterOrEqualTo(*(validator.context.Value().(*T)), value) 99 | }, 100 | ErrorKeyGreaterOrEqualTo, value, template...) 101 | 102 | return validator 103 | } 104 | 105 | // Validate if the {{ .Type }} pointer value is less than another value. This function internally 106 | // uses the golang `<` operator. 107 | // For example: 108 | // 109 | // quantity := {{ .Type }}(2) 110 | // Is(v.{{ .Name }}P(&quantity).LessThan({{ .Type }}(3))) 111 | func (validator *Validator{{ .Name }}P[T]) LessThan(value T, template ...string) *Validator{{ .Name }}P[T] { 112 | validator.context.AddWithValue( 113 | func() bool { 114 | return validator.context.Value().(*T) != nil && is{{ .Name }}LessThan(*(validator.context.Value().(*T)), value) 115 | }, 116 | ErrorKeyLessThan, value, template...) 117 | 118 | return validator 119 | } 120 | 121 | // Validate if the {{ .Type }} pointer value is less than or equal to another value. This function 122 | // internally uses the golang `<=` operator. 123 | // For example: 124 | // 125 | // quantity := {{ .Type }}(2) 126 | // Is(v.{{ .Name }}P(&quantity).LessOrEqualTo({{ .Type }}(2))) 127 | func (validator *Validator{{ .Name }}P[T]) LessOrEqualTo(value T, template ...string) *Validator{{ .Name }}P[T] { 128 | validator.context.AddWithValue( 129 | func() bool { 130 | return validator.context.Value().(*T) != nil && is{{ .Name }}LessOrEqualTo(*(validator.context.Value().(*T)), value) 131 | }, 132 | ErrorKeyLessOrEqualTo, value, template...) 133 | 134 | return validator 135 | } 136 | 137 | // Validate if the value of the {{ .Type }} pointer is within a range (inclusive). 138 | // For example: 139 | // 140 | // n := {{ .Type }}(3) 141 | // Is(v.{{ .Name }}P(&n).Between({{ .Type }}(2),{{ .Type }}(6))) 142 | func (validator *Validator{{ .Name }}P[T]) Between(min T, max T, template ...string) *Validator{{ .Name }}P[T] { 143 | validator.context.AddWithParams( 144 | func() bool { 145 | return validator.context.Value().(*T) != nil && is{{ .Name }}Between(*(validator.context.Value().(*T)), min, max) 146 | }, 147 | ErrorKeyBetween, 148 | map[string]any{"title": validator.context.title, "min": min, "max": max}, 149 | template...) 150 | 151 | return validator 152 | } 153 | 154 | // Validate if the {{ .Type }} pointer value is zero. 155 | // 156 | // For example: 157 | // 158 | // n := {{ .Type }}(0) 159 | // Is(v.{{ .Name }}P(&n).Zero()) 160 | func (validator *Validator{{ .Name }}P[T]) Zero(template ...string) *Validator{{ .Name }}P[T] { 161 | validator.context.Add( 162 | func() bool { 163 | return validator.context.Value().(*T) != nil && is{{ .Name }}Zero(*(validator.context.Value().(*T))) 164 | }, 165 | ErrorKeyZero, template...) 166 | 167 | return validator 168 | } 169 | 170 | // Validate if the {{ .Type }} pointer value is zero or nil. 171 | // 172 | // For example: 173 | // 174 | // var _quantity *{{ .Type }} 175 | // Is(v.{{ .Name }}P(_quantity).ZeroOrNil()) // Will be true 176 | func (validator *Validator{{ .Name }}P[T]) ZeroOrNil(template ...string) *Validator{{ .Name }}P[T] { 177 | validator.context.Add( 178 | func() bool { 179 | return validator.context.Value().(*T) == nil || is{{ .Name }}Zero(*(validator.context.Value().(*T))) 180 | }, 181 | ErrorKeyZero, template...) 182 | 183 | return validator 184 | } 185 | 186 | // Validate if the {{ .Type }} pointer value is nil. 187 | // 188 | // For example: 189 | // 190 | // var quantity *{{ .Type }} 191 | // Is(v.{{ .Name }}P(quantity).Nil()) // Will be true 192 | func (validator *Validator{{ .Name }}P[T]) Nil(template ...string) *Validator{{ .Name }}P[T] { 193 | validator.context.Add( 194 | func() bool { 195 | return validator.context.Value().(*T) == nil 196 | }, 197 | ErrorKeyNil, template...) 198 | 199 | return validator 200 | } 201 | 202 | // Validate if the {{ .Type }} pointer value passes a custom function. 203 | // For example: 204 | // 205 | // quantity := {{ .Type }}(2) 206 | // Is(v.{{ .Name }}P(&quantity).Passing((v *{{ .Type }}) bool { 207 | // return *v == getAllowedQuantity() 208 | // }) 209 | func (validator *Validator{{ .Name }}P[T]) Passing(function func(v *T) bool, template ...string) *Validator{{ .Name }}P[T] { 210 | validator.context.Add( 211 | func() bool { 212 | return function(validator.context.Value().(*T)) 213 | }, 214 | ErrorKeyPassing, template...) 215 | 216 | return validator 217 | } 218 | 219 | // Validate if the {{ .Type }} pointer value is present in a numeric slice. 220 | // For example: 221 | // 222 | // quantity := {{ .Type }}(3) 223 | // validQuantities := []{{ .Type }}{1,3,5} 224 | // Is(v.{{ .Name }}P(&quantity).InSlice(validQuantities)) 225 | func (validator *Validator{{ .Name }}P[T]) InSlice(slice []T, template ...string) *Validator{{ .Name }}P[T] { 226 | validator.context.AddWithValue( 227 | func() bool { 228 | return validator.context.Value().(*T) != nil && is{{ .Name }}InSlice(*(validator.context.Value().(*T)), slice) 229 | }, 230 | ErrorKeyInSlice, validator.context.Value(), template...) 231 | 232 | return validator 233 | } 234 | {{ end }} -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cohesivestack/valgo 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/stretchr/testify v1.9.0 7 | github.com/valyala/fasttemplate v1.2.2 8 | golang.org/x/text v0.14.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/pmezard/go-difflib v1.0.0 // indirect 14 | github.com/valyala/bytebufferpool v1.0.0 // indirect 15 | gopkg.in/yaml.v3 v3.0.1 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 6 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 8 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 9 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 10 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 11 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 12 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 16 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 17 | -------------------------------------------------------------------------------- /locale.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | const ( 4 | LocaleCodeEn = "en" 5 | LocaleCodeEs = "es" 6 | LocaleCodeDe = "de" 7 | ) 8 | 9 | const localeCodeDefault = LocaleCodeEn 10 | 11 | // Locale is a type alias that represents a map of locale entries. 12 | // The keys in the map are strings that represent the entry's identifier, and 13 | // the values are strings that contain the corresponding localized text 14 | // for that entry 15 | type Locale map[string]string 16 | 17 | func getLocaleWithSkipDefaultOption(code string, skipDefault bool, factoryLocales ...map[string]*Locale) *Locale { 18 | 19 | if len(factoryLocales) > 0 && factoryLocales[0] != nil { 20 | 21 | if locale, exists := factoryLocales[0][code]; exists { 22 | return locale 23 | } 24 | if skipDefault { 25 | return nil 26 | } 27 | return getLocaleEn() 28 | 29 | } else { 30 | 31 | switch code { 32 | case LocaleCodeEs: 33 | return getLocaleEs() 34 | case LocaleCodeEn: 35 | return getLocaleEn() 36 | case LocaleCodeDe: 37 | return getLocaleDe() 38 | default: 39 | if skipDefault { 40 | return nil 41 | } 42 | return getLocaleEn() 43 | } 44 | 45 | } 46 | } 47 | 48 | func getLocaleAndSkipDefaultOption(code string, factoryLocales ...map[string]*Locale) *Locale { 49 | return getLocaleWithSkipDefaultOption(code, true, factoryLocales...) 50 | } 51 | 52 | func getLocale(code string, factoryLocales ...map[string]*Locale) *Locale { 53 | return getLocaleWithSkipDefaultOption(code, false, factoryLocales...) 54 | } 55 | 56 | func (_locale *Locale) merge(locale *Locale) *Locale { 57 | if locale != nil { 58 | for k, v := range *locale { 59 | (*_locale)[k] = v 60 | } 61 | } 62 | 63 | return _locale 64 | } 65 | -------------------------------------------------------------------------------- /locale_de.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | func getLocaleDe() *Locale { 4 | return &Locale{ 5 | ErrorKeyAfter: "{{title}} muss nach \"{{value}}\" sein", 6 | ErrorKeyNotAfter: "{{title}} darf nicht nach \"{{value}}\" sein", 7 | 8 | ErrorKeyAfterOrEqualTo: "{{title}} muss nach oder gleich \"{{value}}\" sein", 9 | ErrorKeyNotAfterOrEqualTo: "{{title}} darf nicht nach oder gleich \"{{value}}\" sein", 10 | 11 | ErrorKeyBefore: "{{title}} muss vor \"{{value}}\" sein", 12 | ErrorKeyNotBefore: "{{title}} darf nicht vor \"{{value}}\" sein", 13 | 14 | ErrorKeyBeforeOrEqualTo: "{{title}} muss vor oder gleich \"{{value}}\" sein", 15 | ErrorKeyNotBeforeOrEqualTo: "{{title}} darf nicht vor oder gleich \"{{value}}\" sein", 16 | 17 | ErrorKeyBetween: "{{title}} muss zwischen \"{{min}}\" und \"{{max}}\" sein", 18 | ErrorKeyNotBetween: "{{title}} darf nicht zwischen \"{{min}}\" und \"{{max}}\" sein", 19 | 20 | ErrorKeyBlank: "{{title}} darf nicht ausgefüllt sein", 21 | ErrorKeyNotBlank: "{{title}} muss ausgefüllt sein", 22 | 23 | ErrorKeyEmpty: "{{title}} muss leer sein", 24 | ErrorKeyNotEmpty: "{{title}} darf nicht leer sein", 25 | 26 | ErrorKeyEqualTo: "{{title}} muss identisch zu \"{{value}}\" sein", 27 | ErrorKeyNotEqualTo: "{{title}} darf nicht identisch zu \"{{value}}\" sein", 28 | 29 | ErrorKeyFalse: "{{title}} muss \"false\" sein", 30 | ErrorKeyNotFalse: "{{title}} darf nicht \"false\" sein", 31 | 32 | ErrorKeyGreaterOrEqualTo: "{{title}} muss größer oder gleich als \"{{value}}\" sein", 33 | ErrorKeyNotGreaterOrEqualTo: "{{title}} darf nicht größer oder gleich als \"{{value}}\" sein", 34 | 35 | ErrorKeyGreaterThan: "{{title}} muss größer als \"{{value}}\" sein", 36 | ErrorKeyNotGreaterThan: "{{title}} darf nicht größer als \"{{value}}\" sein", 37 | 38 | ErrorKeyInSlice: "{{title}} ist nicht gültig", 39 | ErrorKeyNotInSlice: "{{title}} ist nicht gültig", 40 | 41 | ErrorKeyLength: "{{title}} muss exakt \"{{length}}\" Zeichen lang sein", 42 | ErrorKeyNotLength: "{{title}} darf nicht \"{{length}}\" Zeichen lang sein", 43 | 44 | ErrorKeyLengthBetween: "{{title}} muss zwischen \"{{min}}\" und \"{{max}}\" Zeichen lang sein", 45 | ErrorKeyNotLengthBetween: "{{title}} darf nicht zwischen \"{{min}}\" und \"{{max}}\" Zeichen lang sein", 46 | 47 | ErrorKeyLessOrEqualTo: "{{title}} muss kleiner oder gleich als \"{{value}}\" sein", 48 | ErrorKeyNotLessOrEqualTo: "{{title}} darf nicht kleiner oder gleich als \"{{value}}\" sein", 49 | 50 | ErrorKeyLessThan: "{{title}} muss weniger als \"{{value}}\" sein", 51 | ErrorKeyNotLessThan: "{{title}} darf nicht weniger als \"{{value}}\" sein", 52 | 53 | ErrorKeyMatchingTo: "{{title}} muss \"{{regexp}}\" entsprechen", 54 | ErrorKeyNotMatchingTo: "{{title}} darf nicht \"{{regexp}}\" entsprechen", 55 | 56 | ErrorKeyMaxLength: "{{title}} darf nicht länger als \"{{length}}\" sein", 57 | ErrorKeyNotMaxLength: "{{title}} muss länger als \"{{length}}\" sein", 58 | 59 | ErrorKeyMinLength: "{{title}} darf nicht kürzer als \"{{length}}\" sein", 60 | ErrorKeyNotMinLength: "{{title}} muss kürzer als \"{{length}}\" sein", 61 | 62 | ErrorKeyNil: "{{title}} muss \"nil\" sein", 63 | ErrorKeyNotNil: "{{title}} darf nicht \"nil\" sein", 64 | 65 | ErrorKeyPassing: "{{title}} ist nicht gültig", 66 | ErrorKeyNotPassing: "{{title}} ist nicht gültig", 67 | 68 | ErrorKeyTrue: "{{title}} muss \"true\" sein", 69 | ErrorKeyNotTrue: "{{title}} darf nicht \"true\" sein", 70 | 71 | ErrorKeyZero: "{{title}} muss 0 sein", 72 | ErrorKeyNotZero: "{{title}} darf nicht 0 sein", 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /locale_en.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | func getLocaleEn() *Locale { 4 | return &Locale{ 5 | ErrorKeyAfter: "{{title}} must be after \"{{value}}\"", 6 | ErrorKeyNotAfter: "{{title}} can't be after \"{{value}}\"", 7 | 8 | ErrorKeyAfterOrEqualTo: "{{title}} must be after or equal to \"{{value}}\"", 9 | ErrorKeyNotAfterOrEqualTo: "{{title}} can't be after or equal to \"{{value}}\"", 10 | 11 | ErrorKeyBefore: "{{title}} must be before \"{{value}}\"", 12 | ErrorKeyNotBefore: "{{title}} can't be before \"{{value}}\"", 13 | 14 | ErrorKeyBeforeOrEqualTo: "{{title}} must be before or equal to \"{{value}}\"", 15 | ErrorKeyNotBeforeOrEqualTo: "{{title}} can't be before or equal to \"{{value}}\"", 16 | 17 | ErrorKeyBetween: "{{title}} must be between \"{{min}}\" and \"{{max}}\"", 18 | ErrorKeyNotBetween: "{{title}} can't be a value between \"{{min}}\" and \"{{max}}\"", 19 | 20 | ErrorKeyBlank: "{{title}} must be blank", 21 | ErrorKeyNotBlank: "{{title}} can't be blank", 22 | 23 | ErrorKeyEmpty: "{{title}} must be empty", 24 | ErrorKeyNotEmpty: "{{title}} can't be empty", 25 | 26 | ErrorKeyEqualTo: "{{title}} must be equal to \"{{value}}\"", 27 | ErrorKeyNotEqualTo: "{{title}} can't be equal to \"{{value}}\"", 28 | 29 | ErrorKeyFalse: "{{title}} must be false", 30 | ErrorKeyNotFalse: "{{title}} must not be false", 31 | 32 | ErrorKeyGreaterOrEqualTo: "{{title}} must be greater than or equal to \"{{value}}\"", 33 | ErrorKeyNotGreaterOrEqualTo: "{{title}} can't be greater than or equal to \"{{value}}\"", 34 | 35 | ErrorKeyGreaterThan: "{{title}} must be greater than \"{{value}}\"", 36 | ErrorKeyNotGreaterThan: "{{title}} can't be greater than \"{{value}}\"", 37 | 38 | ErrorKeyInSlice: "{{title}} is not valid", 39 | ErrorKeyNotInSlice: "{{title}} is not valid", 40 | 41 | ErrorKeyLength: "{{title}} must have a length equal to \"{{length}}\"", 42 | ErrorKeyNotLength: "{{title}} must not have a length equal to \"{{length}}\"", 43 | 44 | ErrorKeyLengthBetween: "{{title}} must have a length between \"{{min}}\" and \"{{max}}\"", 45 | ErrorKeyNotLengthBetween: "{{title}} must not have a length between \"{{min}}\" and \"{{max}}\"", 46 | 47 | ErrorKeyLessOrEqualTo: "{{title}} must be less than or equal to \"{{value}}\"", 48 | ErrorKeyNotLessOrEqualTo: "{{title}} must not be less than or equal to \"{{value}}\"", 49 | 50 | ErrorKeyLessThan: "{{title}} must be less than \"{{value}}\"", 51 | ErrorKeyNotLessThan: "{{title}} can't be less than \"{{value}}\"", 52 | 53 | ErrorKeyMatchingTo: "{{title}} must match to \"{{regexp}}\"", 54 | ErrorKeyNotMatchingTo: "{{title}} can't match to \"{{regexp}}\"", 55 | 56 | ErrorKeyMaxLength: "{{title}} must not have a length longer than \"{{length}}\"", 57 | ErrorKeyNotMaxLength: "{{title}} must not have a length shorter than or equal to \"{{length}}\"", 58 | 59 | ErrorKeyMinLength: "{{title}} must not have a length shorter than \"{{length}}\"", 60 | ErrorKeyNotMinLength: "{{title}} must not have a length longer than or equal to \"{{length}}\"", 61 | 62 | ErrorKeyNil: "{{title}} must be nil", 63 | ErrorKeyNotNil: "{{title}} must not be nil", 64 | 65 | ErrorKeyPassing: "{{title}} is not valid", 66 | ErrorKeyNotPassing: "{{title}} is not valid", 67 | 68 | ErrorKeyTrue: "{{title}} must be true", 69 | ErrorKeyNotTrue: "{{title}} must not be true", 70 | 71 | ErrorKeyZero: "{{title}} must be zero", 72 | ErrorKeyNotZero: "{{title}} must not be zero", 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /locale_es.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | func getLocaleEs() *Locale { 4 | return &Locale{ 5 | ErrorKeyAfter: "{{title}} debe ser después \"{{value}}\"", 6 | ErrorKeyNotAfter: "{{title}} no puede ser después \"{{value}}\"", 7 | 8 | ErrorKeyAfterOrEqualTo: "{{title}} debe ser después o igual a \"{{value}}\"", 9 | ErrorKeyNotAfterOrEqualTo: "{{title}} no puede ser después o igual a \"{{value}}\"", 10 | 11 | ErrorKeyBefore: "{{title}} debe ser antes \"{{value}}\"", 12 | ErrorKeyNotBefore: "{{title}} no puede ser antes \"{{value}}\"", 13 | 14 | ErrorKeyBeforeOrEqualTo: "{{title}} debe ser antes o igual a \"{{value}}\"", 15 | ErrorKeyNotBeforeOrEqualTo: "{{title}} no puede ser antes o igual a \"{{value}}\"", 16 | 17 | ErrorKeyBetween: "{{title}} debe estar entre \"{{min}}\" y \"{{max}}\"", 18 | ErrorKeyNotBetween: "{{title}} no puede ser un valor entre \"{{min}}\" y \"{{max}}\"", 19 | 20 | ErrorKeyBlank: "{{title}} debe estar en blanco", 21 | ErrorKeyNotBlank: "{{title}} no puede estar en blanco", 22 | 23 | ErrorKeyEmpty: "{{title}} debe estar vacío", 24 | ErrorKeyNotEmpty: "{{title}} no puede estar vacío", 25 | 26 | ErrorKeyEqualTo: "{{title}} debe ser igual a \"{{value}}\"", 27 | ErrorKeyNotEqualTo: "{{title}} no puede ser igual a \"{{value}}\"", 28 | 29 | ErrorKeyFalse: "{{title}} debe ser falso", 30 | ErrorKeyNotFalse: "{{title}} no debe ser falso", 31 | 32 | ErrorKeyGreaterOrEqualTo: "{{title}} debe ser mayor o igual a \"{{value}}\"", 33 | ErrorKeyNotGreaterOrEqualTo: "{{title}} no puede ser mayor o igual a \"{{value}}\"", 34 | 35 | ErrorKeyGreaterThan: "{{title}} debe ser mayor que \"{{value}}\"", 36 | ErrorKeyNotGreaterThan: "{{title}} no puede ser mayor que \"{{value}}\"", 37 | 38 | ErrorKeyInSlice: "{{title}} no es válido", 39 | ErrorKeyNotInSlice: "{{title}} no es válido", 40 | 41 | ErrorKeyLength: "{{title}} debe tener una longitud igual a \"{{length}}\"", 42 | ErrorKeyNotLength: "{{title}} no debe tener una longitud igual a \"{{length}}\"", 43 | 44 | ErrorKeyLengthBetween: "{{title}} debe tener una longitud entre \"{{min}}\" and \"{{max}}\"", 45 | ErrorKeyNotLengthBetween: "{{title}} no debe tener una longitud entre \"{{min}}\" and \"{{max}}\"", 46 | 47 | ErrorKeyLessOrEqualTo: "{{title}} debe ser menor o igual a \"{{value}}\"", 48 | ErrorKeyNotLessOrEqualTo: "{{title}} no debe ser menor o igual a \"{{value}}\"", 49 | 50 | ErrorKeyLessThan: "{{title}} debe ser menor que \"{{value}}\"", 51 | ErrorKeyNotLessThan: "{{title}} no puede ser menor que \"{{value}}\"", 52 | 53 | ErrorKeyMatchingTo: "{{title}} debe coincidir con \"{{regexp}}\"", 54 | ErrorKeyNotMatchingTo: "{{title}} no puede coincidir con \"{{regexp}}\"", 55 | 56 | ErrorKeyMaxLength: "{{title}} no debe tener una longitud mayor a \"{{length}}\"", 57 | ErrorKeyNotMaxLength: "{{title}} no debe tener una longitud menor o igual a \"{{length}}\"", 58 | 59 | ErrorKeyMinLength: "{{title}} no debe tener una longitud menor a \"{{length}}\"", 60 | ErrorKeyNotMinLength: "{{title}} no debe tener una longitud mayor o igual a \"{{length}}\"", 61 | 62 | ErrorKeyNil: "{{title}} debe ser nulo", 63 | ErrorKeyNotNil: "{{title}} no debe ser nulo", 64 | 65 | ErrorKeyPassing: "{{title}} no es válido", 66 | ErrorKeyNotPassing: "{{title}} no es válido", 67 | 68 | ErrorKeyTrue: "{{title}} debe ser verdadero", 69 | ErrorKeyNotTrue: "{{title}} no debe ser verdadero", 70 | 71 | ErrorKeyZero: "{{title}} debe ser cero", 72 | ErrorKeyNotZero: "{{title}} no debe ser cero", 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /locale_test.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestUseOtherLocale(t *testing.T) { 10 | 11 | vEs := New(Options{LocaleCode: LocaleCodeEs}).Is(String(" ").Not().Blank()) 12 | assert.Contains(t, vEs.Errors()["value_0"].Messages(), "Value 0 no puede estar en blanco") 13 | 14 | // Default localization must be persistent in the same validation 15 | vEs = vEs.Is(String(" ").Empty()) 16 | assert.Contains(t, vEs.Errors()["value_1"].Messages(), "Value 1 debe estar vacío") 17 | 18 | vDe := New(Options{LocaleCode: LocaleCodeDe}).Is(String(" ").Not().Blank()) 19 | assert.Contains(t, vDe.Errors()["value_0"].Messages(), "Value 0 muss ausgefüllt sein") 20 | 21 | // Default localization must be persistent in the same validation 22 | vDe = vDe.Is(String(" ").Empty()) 23 | assert.Contains(t, vDe.Errors()["value_1"].Messages(), "Value 1 muss leer sein") 24 | } 25 | 26 | func TestChangeLocaleEntries(t *testing.T) { 27 | 28 | originalErrorMessage0 := (*getLocaleEn())[ErrorKeyNotBlank] 29 | modifiedErrorMessage0 := "{{title}} should not be blank" 30 | 31 | originalErrorMessage1 := (*getLocaleEn())[ErrorKeyBlank] 32 | modifiedErrorMessage1 := "{{title}} should be blank" 33 | 34 | assert.NotEqual(t, originalErrorMessage0, modifiedErrorMessage0) 35 | assert.NotEqual(t, originalErrorMessage1, modifiedErrorMessage1) 36 | 37 | locale := &Locale{ 38 | ErrorKeyNotBlank: modifiedErrorMessage0, 39 | ErrorKeyBlank: modifiedErrorMessage1, 40 | } 41 | 42 | v := New(Options{Locale: locale}).Is(String(" ").Not().Blank()) 43 | assert.Contains(t, v.Errors()["value_0"].Messages(), "Value 0 should not be blank") 44 | 45 | v = v.Is(String("a").Blank()) 46 | assert.Contains(t, v.Errors()["value_1"].Messages(), "Value 1 should be blank") 47 | 48 | // Other entries should not be modified 49 | v = v.Is(String("").Not().Empty()) 50 | assert.Contains(t, v.Errors()["value_2"].Messages(), "Value 2 can't be empty") 51 | 52 | v = v.Is(String(" ").Empty()) 53 | assert.Contains(t, v.Errors()["value_3"].Messages(), "Value 3 must be empty") 54 | } 55 | 56 | func TestUseOtherLocaleAndChangeLocaleEntries(t *testing.T) { 57 | 58 | originalErrorMessage0 := (*getLocaleEs())[ErrorKeyNotBlank] 59 | modifiedErrorMessage0 := "{{title}} no debería estar en blanco" 60 | 61 | originalErrorMessage1 := (*getLocaleEs())[ErrorKeyBlank] 62 | modifiedErrorMessage1 := "{{title}} debería estar en blanco" 63 | 64 | assert.NotEqual(t, originalErrorMessage0, modifiedErrorMessage0) 65 | assert.NotEqual(t, originalErrorMessage1, modifiedErrorMessage1) 66 | 67 | locale := &Locale{ 68 | ErrorKeyNotBlank: modifiedErrorMessage0, 69 | ErrorKeyBlank: modifiedErrorMessage1, 70 | } 71 | 72 | v := New(Options{LocaleCode: LocaleCodeEs, Locale: locale}).Is(String(" ").Not().Blank()) 73 | assert.Contains(t, v.Errors()["value_0"].Messages(), "Value 0 no debería estar en blanco") 74 | 75 | v = v.Is(String("a").Blank()) 76 | assert.Contains(t, v.Errors()["value_1"].Messages(), "Value 1 debería estar en blanco") 77 | 78 | // Other entries should not be modified 79 | v = v.Is(String("").Not().Empty()) 80 | assert.Contains(t, v.Errors()["value_2"].Messages(), "Value 2 no puede estar vacío") 81 | 82 | v = v.Is(String(" ").Empty()) 83 | assert.Contains(t, v.Errors()["value_3"].Messages(), "Value 3 debe estar vacío") 84 | } 85 | 86 | func TestAddNewLocaleEntries(t *testing.T) { 87 | 88 | locale := &Locale{ 89 | ErrorKeyNotBlank: "{{title}} can't be blank (XX)", 90 | ErrorKeyBlank: "{{title}} must be blank (XX)", 91 | } 92 | 93 | v := New(Options{LocaleCode: "xx", Locale: locale}).Is(String(" ").Not().Blank()) 94 | assert.Contains(t, v.Errors()["value_0"].Messages(), "Value 0 can't be blank (XX)") 95 | 96 | v = v.Is(String("a").Blank()) 97 | assert.Contains(t, v.Errors()["value_1"].Messages(), "Value 1 must be blank (XX)") 98 | 99 | // For the unexisting keys, then should use the default language 100 | v = v.Is(String("").Not().Empty()) 101 | assert.Contains(t, v.Errors()["value_2"].Messages(), "Value 2 can't be empty") 102 | 103 | v = v.Is(String(" ").Empty()) 104 | assert.Contains(t, v.Errors()["value_3"].Messages(), "Value 3 must be empty") 105 | } 106 | 107 | func TestLocalesIsInValidationScope(t *testing.T) { 108 | originalErrorMessage0 := (*getLocaleEn())[ErrorKeyNotBlank] 109 | modifiedErrorMessage0 := "{{title}} should not be blank" 110 | 111 | assert.NotEqual(t, originalErrorMessage0, modifiedErrorMessage0) 112 | 113 | locale := &Locale{ 114 | ErrorKeyNotBlank: modifiedErrorMessage0, 115 | } 116 | 117 | v := New(Options{Locale: locale}).Is(String(" ").Not().Blank()) 118 | assert.Contains(t, v.Errors()["value_0"].Messages(), "Value 0 should not be blank") 119 | 120 | // New validation 121 | v = New().Is(String(" ").Not().Blank()) 122 | assert.Contains(t, v.Errors()["value_0"].Messages(), "Value 0 can't be blank") 123 | } 124 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "encoding/json" 5 | "sort" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | func concatString(stringA string, stringB string) string { 11 | strBuilder := strings.Builder{} 12 | strBuilder.WriteString(stringA) 13 | strBuilder.WriteString(stringB) 14 | return strBuilder.String() 15 | } 16 | 17 | func humanizeName(name string) string { 18 | in := []rune(strings.TrimSpace(name)) 19 | space := []rune(" ")[0] 20 | lastIndex := len(in) - 1 21 | 22 | out := strings.Builder{} 23 | 24 | for i, c := range in { 25 | if i == 0 { 26 | if unicode.IsLower(c) { 27 | out.WriteRune(unicode.ToUpper(c)) 28 | } else { 29 | out.WriteRune(c) 30 | } 31 | } else { 32 | cb := in[i-1] 33 | if !unicode.IsLetter(c) && !unicode.IsNumber(c) { 34 | if !unicode.IsLetter(cb) && !unicode.IsNumber(cb) { 35 | continue 36 | } else { 37 | out.WriteRune(space) 38 | } 39 | } else if unicode.IsUpper(c) { 40 | isLast := i == lastIndex 41 | var cn rune 42 | if !isLast { 43 | cn = in[i+1] 44 | } 45 | if unicode.IsUpper(cb) && (isLast || (unicode.IsUpper(cn) || !unicode.IsLetter(cn))) { 46 | out.WriteRune(c) 47 | } else { 48 | if unicode.IsLetter(cb) || unicode.IsNumber(cb) { 49 | out.WriteRune(space) 50 | } 51 | if !unicode.IsUpper(cb) && (!isLast && unicode.IsUpper(cn)) { 52 | out.WriteRune(c) 53 | } else { 54 | out.WriteRune(unicode.ToLower(c)) 55 | } 56 | } 57 | } else if unicode.IsNumber(c) { 58 | if unicode.IsLetter(cb) { 59 | out.WriteRune(space) 60 | } 61 | out.WriteRune(c) 62 | } else { 63 | out.WriteRune(unicode.ToLower(c)) 64 | } 65 | } 66 | } 67 | 68 | return out.String() 69 | } 70 | 71 | // serializes the error messages into sorted JSON for consistency in 72 | // documentation examples. 73 | func sortedErrorMarshalForDocs(e *Error) ([]byte, error) { 74 | // Create a slice to hold the keys from the map, so we can sort them. 75 | keys := make([]string, 0, len(e.errors)) 76 | for k := range e.errors { 77 | keys = append(keys, k) 78 | } 79 | sort.Strings(keys) // Sort the keys alphabetically. 80 | 81 | // Create a map to hold the sorted key-value pairs. 82 | sortedErrors := make(map[string]interface{}, len(keys)) 83 | for _, k := range keys { 84 | messages := make([]string, len(e.errors[k].Messages())) 85 | copy(messages, e.errors[k].Messages()) 86 | sort.Strings(messages) 87 | sortedErrors[k] = messages 88 | } 89 | 90 | // Marshal the sorted map to JSON. 91 | return json.Marshal(sortedErrors) 92 | } 93 | -------------------------------------------------------------------------------- /valgo.go: -------------------------------------------------------------------------------- 1 | // Valgo is a type-safe, expressive, and extensible validator library for 2 | // Golang. Valgo is built with generics, so Go 1.18 or higher is required. 3 | // 4 | // Valgo differs from other Golang validation libraries in that the rules are 5 | // written in functions and not in struct tags. This allows greater flexibility 6 | // and freedom when it comes to where and how data is validated. 7 | // 8 | // Additionally, Valgo supports customizing and localizing validation messages. 9 | package valgo 10 | 11 | // Factory is a function used to create a Valgo factory. 12 | // 13 | // With a Valgo factory, you can create Validation sessions with preset options, 14 | // avoiding having to pass options each time when a Validation session is 15 | // created. 16 | // 17 | // This allows for more flexibility and easier management of options. 18 | // 19 | // The Factory function accepts an options parameter of type [FactoryOptions] 20 | // struct, which allows you to specify options such as the default locale code, 21 | // available locales and a custom JSON marshaler for errors. 22 | func Factory(options FactoryOptions) *ValidationFactory { 23 | 24 | factory := &ValidationFactory{ 25 | localeCodeDefault: localeCodeDefault, 26 | marshalJsonFunc: options.MarshalJsonFunc, 27 | } 28 | 29 | if options.LocaleCodeDefault != "" { 30 | factory.localeCodeDefault = options.LocaleCodeDefault 31 | } 32 | 33 | // Create factory locales for the case when locales was specified 34 | if len(options.Locales) > 0 { 35 | factory.locales = map[string]*Locale{ 36 | LocaleCodeEn: getLocaleEn().merge(options.Locales[LocaleCodeEn]), 37 | LocaleCodeEs: getLocaleEs().merge(options.Locales[LocaleCodeEs]), 38 | } 39 | 40 | // Add unexisting locales 41 | 42 | // Determine what is the default locale, since an unexisting locale, 43 | // can't be created with an unexisting default locale. In that case use 44 | // the Valgo default locale as fallback 45 | _localeCodeDefault := factory.localeCodeDefault 46 | if _, exists := factory.locales[_localeCodeDefault]; !exists { 47 | _localeCodeDefault = localeCodeDefault 48 | } 49 | 50 | for k, l := range options.Locales { 51 | if _, exists := factory.locales[k]; !exists { 52 | factory.locales[k] = factory.locales[_localeCodeDefault].merge(l) 53 | } 54 | } 55 | } 56 | 57 | return factory 58 | } 59 | 60 | // This function allows you to create a new [Validation] session without a 61 | // Validator. This is useful for conditional validation, reusing validation 62 | // logic or just to pass optional parameters to the [Validation] session. 63 | // 64 | // The function accepts an optional parameter of type [Options] struct, which 65 | // allows you to specify options such as the specific locale code and locale 66 | // to use, and a custom JSON marshaler for errors. 67 | // 68 | // The following example conditionally adds a validator rule for the month_day 69 | // value. 70 | func New(options ...Options) *Validation { 71 | 72 | return newValidation(options...) 73 | } 74 | 75 | // The [Is](...) function allows you to pass a [Validator] with the value and 76 | // the rules for validating it. At the same time, create a [Validation] session, 77 | // which lets you add more Validators in order to verify more values. 78 | // 79 | // As shown in the following example, we are passing to the function [Is](...) 80 | // the [Validator] for the full_name value. The function returns a [Validation] 81 | // session that allows us to add more Validators to validate more values; in the 82 | // example case the values age and status: 83 | func Is(validators ...Validator) *Validation { 84 | return New().Is(validators...) 85 | } 86 | 87 | // The [In](...) function executes one or more validators in a namespace, so the 88 | // value names in the error result are prefixed with this namespace. This is 89 | // useful for validating nested structures. 90 | // 91 | // In the following example we are validating the Person and the nested 92 | // Address structure. We can distinguish the errors of the nested Address 93 | // structure in the error results. 94 | func In(name string, v *Validation) *Validation { 95 | return New().In(name, v) 96 | } 97 | 98 | // The [InRow](...) function executes one or more validators in a namespace 99 | // similar to the [In](...) function, but with indexed namespace. So, the value 100 | // names in the error result are prefixed with this indexed namespace. It is 101 | // useful for validating nested lists in structures. 102 | // 103 | // In the following example we validate the Person and the nested list 104 | // Addresses. The error results can distinguish the errors of the nested list 105 | // Addresses. 106 | func InRow(name string, index int, v *Validation) *Validation { 107 | return New().InRow(name, index, v) 108 | } 109 | 110 | // The [Check](...) function is similar to the [Is](...) function, however with 111 | // [Check](...)` the Rules of the [Validator] parameter are not short-circuited, 112 | // which means that regardless of whether a previous rule was valid, all rules 113 | // are checked. 114 | // 115 | // This example shows two rules that fail due to the empty value in the full_name 116 | // [Validator], and since the [Validator] is not short-circuited, both error 117 | // messages are added to the error result. 118 | func Check(validators ...Validator) *Validation { 119 | return New().Check(validators...) 120 | } 121 | 122 | // Create a new [Validation] session and add an error message to it without 123 | // executing a field validator. By adding this error message, the [Validation] 124 | // session will be marked as invalid. 125 | func AddErrorMessage(name string, message string) *Validation { 126 | return New().AddErrorMessage(name, message) 127 | } 128 | -------------------------------------------------------------------------------- /valgo_test.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | func Example() { 9 | val := Is(String("Bob", "full_name").Not().Blank().OfLengthBetween(4, 20)). 10 | Is(Number(17, "age").GreaterThan(18)) 11 | 12 | if !val.Valid() { 13 | // NOTE: sortedErrorMarshalForDocs is an optional parameter used here for 14 | // documentation purposes to ensure the order of keys in the JSON output. 15 | out, _ := json.MarshalIndent(val.Error(sortedErrorMarshalForDocs), "", " ") 16 | fmt.Println(string(out)) 17 | } 18 | // Output: { 19 | // "age": [ 20 | // "Age must be greater than \"18\"" 21 | // ], 22 | // "full_name": [ 23 | // "Full name must have a length between \"4\" and \"20\"" 24 | // ] 25 | // } 26 | } 27 | 28 | func ExampleIs() { 29 | 30 | val := Is(String("Bob", "full_name").Not().Blank().OfLengthBetween(4, 20)). 31 | Is(Number(17, "age").GreaterThan(18)). 32 | Is(String("singl", "status").InSlice([]string{"married", "single"})) 33 | 34 | if !val.Valid() { 35 | // NOTE: sortedErrorMarshalForDocs is an optional parameter used here for 36 | // documentation purposes to ensure the order of keys in the JSON output. 37 | out, _ := json.MarshalIndent(val.Error(sortedErrorMarshalForDocs), "", " ") 38 | fmt.Println(string(out)) 39 | } 40 | 41 | // Output: { 42 | // "age": [ 43 | // "Age must be greater than \"18\"" 44 | // ], 45 | // "full_name": [ 46 | // "Full name must have a length between \"4\" and \"20\"" 47 | // ], 48 | // "status": [ 49 | // "Status is not valid" 50 | // ] 51 | // } 52 | 53 | } 54 | 55 | func ExampleNew() { 56 | 57 | month := 5 58 | monthDay := 11 59 | 60 | val := New() 61 | 62 | if month == 6 { 63 | val.Is(Number(monthDay, "month_day").LessOrEqualTo(10)) 64 | } 65 | 66 | if val.Valid() { 67 | fmt.Println("The validation passes") 68 | } 69 | 70 | // Output: The validation passes 71 | } 72 | 73 | func ExampleIn() { 74 | type Address struct { 75 | Name string 76 | Street string 77 | } 78 | 79 | type Person struct { 80 | Name string 81 | Address Address 82 | } 83 | 84 | p := Person{"Bob", Address{"", "1600 Amphitheatre Pkwy"}} 85 | 86 | val := Is(String(p.Name, "name").OfLengthBetween(4, 20)). 87 | In("address", Is( 88 | String(p.Address.Name, "name").Not().Blank()).Is( 89 | String(p.Address.Street, "street").Not().Blank())) 90 | 91 | if !val.Valid() { 92 | // NOTE: sortedErrorMarshalForDocs is an optional parameter used here for 93 | // documentation purposes to ensure the order of keys in the JSON output. 94 | out, _ := json.MarshalIndent(val.Error(sortedErrorMarshalForDocs), "", " ") 95 | fmt.Println(string(out)) 96 | } 97 | 98 | // output: { 99 | // "address.name": [ 100 | // "Name can't be blank" 101 | // ], 102 | // "name": [ 103 | // "Name must have a length between \"4\" and \"20\"" 104 | // ] 105 | // } 106 | } 107 | 108 | func ExampleInRow() { 109 | type Address struct { 110 | Name string 111 | Street string 112 | } 113 | 114 | type Person struct { 115 | Name string 116 | Addresses []Address 117 | } 118 | 119 | p := Person{ 120 | "Bob", 121 | []Address{ 122 | {"", "1600 Amphitheatre Pkwy"}, 123 | {"Home", ""}, 124 | }, 125 | } 126 | 127 | val := Is(String(p.Name, "name").OfLengthBetween(4, 20)) 128 | 129 | for i, a := range p.Addresses { 130 | val.InRow("addresses", i, Is( 131 | String(a.Name, "name").Not().Blank()).Is( 132 | String(a.Street, "street").Not().Blank())) 133 | } 134 | 135 | if !val.Valid() { 136 | // NOTE: sortedErrorMarshalForDocs is an optional parameter used here for 137 | // documentation purposes to ensure the order of keys in the JSON output. 138 | out, _ := json.MarshalIndent(val.Error(sortedErrorMarshalForDocs), "", " ") 139 | fmt.Println(string(out)) 140 | } 141 | 142 | // output: { 143 | // "addresses[0].name": [ 144 | // "Name can't be blank" 145 | // ], 146 | // "addresses[1].street": [ 147 | // "Street can't be blank" 148 | // ], 149 | // "name": [ 150 | // "Name must have a length between \"4\" and \"20\"" 151 | // ] 152 | // } 153 | } 154 | 155 | func ExampleCheck() { 156 | val := Check(String("", "full_name").Not().Blank().OfLengthBetween(4, 20)) 157 | 158 | if !val.Valid() { 159 | // NOTE: sortedErrorMarshalForDocs is an optional parameter used here for 160 | // documentation purposes to ensure the order of keys in the JSON output. 161 | out, _ := json.MarshalIndent(val.Error(sortedErrorMarshalForDocs), "", " ") 162 | fmt.Println(string(out)) 163 | } 164 | 165 | // output: { 166 | // "full_name": [ 167 | // "Full name can't be blank", 168 | // "Full name must have a length between \"4\" and \"20\"" 169 | // ] 170 | // } 171 | } 172 | -------------------------------------------------------------------------------- /validation.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // The [Validation] session in Valgo is the main structure for validating one or 10 | // more values. It is called Validation in code. 11 | // 12 | // A [Validation] session will contain one or more Validators, where each [Validator] 13 | // will have the responsibility to validate a value with one or more rules. 14 | // 15 | // There are multiple functions to create a [Validation] session, depending on the 16 | // requirements: 17 | // 18 | // - [New]() 19 | // - [Is](...) 20 | // - [In](...) 21 | // - [InRow](...) 22 | // - [Check](...) 23 | // - [AddErrorMessage](...) 24 | // 25 | // the function [Is](...) is likely to be the most frequently used function in your 26 | // validations. When [Is](...) is called, the function creates a validation and 27 | // receives a validator at the same time. 28 | type Validation struct { 29 | valid bool 30 | 31 | _locale *Locale 32 | errors map[string]*valueError 33 | currentIndex int 34 | marshalJsonFunc func(e *Error) ([]byte, error) 35 | } 36 | 37 | // Options struct is used to specify options when creating a new [Validation] 38 | // session with the [New()] function. 39 | // 40 | // It contains parameters for specifying a specific locale code, modify or add a 41 | // locale, and set a custom JSON marshaler for [Error]. 42 | 43 | type Options struct { 44 | localeCodeDefaultFromFactory string // Only specified by the factory 45 | localesFromFactory map[string]*Locale // Only specified by the factory 46 | 47 | // A string field that represents the locale code to use by the [Validation] 48 | // session 49 | LocaleCode string 50 | // A map field that allows to modify or add a new [Locale] 51 | Locale *Locale 52 | // A function field that allows to set a custom JSON marshaler for [Error] 53 | MarshalJsonFunc func(e *Error) ([]byte, error) 54 | } 55 | 56 | // Add one or more validators to a [Validation] session. 57 | func (validation *Validation) Is(validators ...Validator) *Validation { 58 | for _, v := range validators { 59 | validation = v.Context().validateIs(validation) 60 | } 61 | return validation 62 | } 63 | 64 | // Add one or more validators to a [Validation] session. But unlike [Is()], 65 | // the validators are not short-circuited. 66 | func (validation *Validation) Check(validators ...Validator) *Validation { 67 | for _, v := range validators { 68 | validation = v.Context().validateCheck(validation) 69 | } 70 | return validation 71 | } 72 | 73 | // A [Validation] session provides this function which returns either true if 74 | // all their validators are valid or false if any one of them is invalid. 75 | // 76 | // In the following example, even though the [Validator] for age is valid, the 77 | // [Validator] for status is invalid, making the entire Validator session 78 | // invalid. 79 | func (validation *Validation) Valid() bool { 80 | return validation.valid 81 | } 82 | 83 | // Add a map namespace to a [Validation] session. 84 | func (validation *Validation) In(name string, _validation *Validation) *Validation { 85 | return validation.merge(name, _validation) 86 | } 87 | 88 | // Add an indexed namespace to a [Validation] session. 89 | func (validation *Validation) InRow(name string, index int, _validation *Validation) *Validation { 90 | return validation.merge(fmt.Sprintf("%s[%v]", name, index), _validation) 91 | } 92 | 93 | // Using [Merge](...) you can merge two [Validation] sessions. When two 94 | // validations are merged, errors with the same value name will be merged. It is 95 | // useful for reusing validation logic. 96 | // 97 | // The following example merges the [Validation] session returned by the 98 | // validatePreStatus function. Since both [Validation] sessions validate a value 99 | // with the name status, the error returned will return two error messages, and 100 | // without duplicate the Not().Blank() error message rule. 101 | func (validation *Validation) Merge(_validation *Validation) *Validation { 102 | return validation.merge("", _validation) 103 | } 104 | 105 | func (validation *Validation) merge(prefix string, _validation *Validation) *Validation { 106 | 107 | var _prefix string 108 | if len(strings.TrimSpace(prefix)) > 0 { 109 | _prefix = prefix + "." 110 | } 111 | 112 | LOOP1: 113 | for _field, _err := range _validation.Errors() { 114 | for field, err := range validation.Errors() { 115 | if _prefix+_field == field { 116 | LOOP2: 117 | for _, _errMsg := range _err.Messages() { 118 | for _, errMsg := range err.Messages() { 119 | if _errMsg == errMsg { 120 | continue LOOP2 121 | } 122 | } 123 | validation.AddErrorMessage(_prefix+_field, _errMsg) 124 | } 125 | continue LOOP1 126 | } 127 | } 128 | for _, _errMsg := range _err.Messages() { 129 | validation.AddErrorMessage(_prefix+_field, _errMsg) 130 | } 131 | } 132 | return validation 133 | } 134 | 135 | // Add an error message to the [Validation] session without executing a field 136 | // validator. By adding this error message, the [Validation] session will be 137 | // marked as invalid. 138 | func (v *Validation) AddErrorMessage(name string, message string) *Validation { 139 | if v.errors == nil { 140 | v.errors = map[string]*valueError{} 141 | } 142 | 143 | v.valid = false 144 | 145 | ev := v.getOrCreateValueError(name, nil) 146 | 147 | ev.errorMessages = append(ev.errorMessages, message) 148 | 149 | return v 150 | } 151 | 152 | func (v *Validation) mergeError(prefix string, err *Error) *Validation { 153 | 154 | if err != nil && len(err.errors) > 0 { 155 | if v.errors == nil { 156 | v.errors = map[string]*valueError{} 157 | } 158 | 159 | v.valid = false 160 | 161 | var _prefix string 162 | if len(strings.TrimSpace(prefix)) > 0 { 163 | _prefix = prefix + "." 164 | } 165 | 166 | for name, _ev := range err.errors { 167 | for _, message := range _ev.Messages() { 168 | v.AddErrorMessage(_prefix+name, message) 169 | } 170 | } 171 | } 172 | 173 | return v 174 | } 175 | 176 | // MergeError allows merging Valgo errors from an already validated [Validation] session. 177 | // The function takes an Valgo [Error] pointer as an argument and returns a [Validation] pointer. 178 | func (v *Validation) MergeError(err *Error) *Validation { 179 | return v.mergeError("", err) 180 | } 181 | 182 | // MergeErrorIn allows merging Valgo errors from already validated [Validation] sessions 183 | // within a map namespace. The function takes a namespace name and an [Error] pointer 184 | // as arguments and returns a [Validation] pointer. 185 | func (v *Validation) MergeErrorIn(name string, err *Error) *Validation { 186 | return v.mergeError(name, err) 187 | } 188 | 189 | // MergeErrorInRow allows merging Valgo errors from already validated [Validation] sessions 190 | // within an indexed namespace. The function takes a namespace name, an index, and an [Error] pointer 191 | // as arguments and returns a [Validation] pointer. 192 | func (v *Validation) MergeErrorInRow(name string, index int, err *Error) *Validation { 193 | return v.mergeError(fmt.Sprintf("%s[%v]", name, index), err) 194 | } 195 | 196 | func (validation *Validation) invalidate(name *string, title *string, fragment *validatorFragment) { 197 | if validation.errors == nil { 198 | validation.errors = map[string]*valueError{} 199 | } 200 | 201 | validation.valid = false 202 | 203 | var _name string 204 | if name == nil { 205 | _name = concatString("value_", strconv.Itoa(validation.currentIndex-1)) 206 | } else { 207 | _name = *name 208 | } 209 | 210 | ev := validation.getOrCreateValueError(_name, title) 211 | 212 | errorKey := fragment.errorKey 213 | 214 | if !fragment.boolOperation { 215 | errorKey = concatString("not_", errorKey) 216 | } 217 | 218 | if _, ok := ev.errorTemplates[errorKey]; !ok { 219 | ev.errorTemplates[errorKey] = &errorTemplate{ 220 | key: errorKey, 221 | } 222 | } 223 | 224 | et := ev.errorTemplates[errorKey] 225 | if len(fragment.template) > 0 { 226 | et.template = &fragment.template[0] 227 | } 228 | et.params = fragment.templateParams 229 | } 230 | 231 | // Return a map with the information for each invalid field validator 232 | // in the Validation session. 233 | func (session *Validation) Errors() map[string]*valueError { 234 | return session.errors 235 | } 236 | 237 | // Return an error object that encapsulates the validation errors created during 238 | // the Validation session. If the session is valid, it returns nil. 239 | // 240 | // An optional JSON marshaling function can be passed to customize how the 241 | // validation errors are serialized into JSON. If no function is provided, a 242 | // default marshaling behavior is used. 243 | func (validation *Validation) Error(marshalJsonFun ...func(e *Error) ([]byte, error)) error { 244 | if !validation.valid { 245 | fn := validation.marshalJsonFunc 246 | if len(marshalJsonFun) > 0 { 247 | fn = marshalJsonFun[0] 248 | } 249 | return &Error{ 250 | errors: validation.errors, 251 | marshalJsonFunc: fn, 252 | } 253 | } 254 | return nil 255 | } 256 | 257 | // Return true if a specific field validator is valid. 258 | func (validation *Validation) IsValid(name string) bool { 259 | if _, isNotValid := validation.errors[name]; isNotValid { 260 | return false 261 | } 262 | 263 | return true 264 | } 265 | 266 | func (validation *Validation) getOrCreateValueError(name string, title *string) *valueError { 267 | if _, ok := validation.errors[name]; !ok { 268 | validation.errors[name] = &valueError{ 269 | name: &name, 270 | title: title, 271 | errorTemplates: map[string]*errorTemplate{}, 272 | errorMessages: []string{}, 273 | validator: validation, 274 | } 275 | } 276 | 277 | ev := validation.errors[name] 278 | ev.dirty = true 279 | 280 | return ev 281 | } 282 | 283 | func newValidation(options ...Options) *Validation { 284 | v := &Validation{ 285 | valid: true, 286 | } 287 | 288 | if len(options) == 0 { 289 | v._locale = getLocale(localeCodeDefault) 290 | } else { 291 | _options := options[0] 292 | 293 | // If the factory has default locale specified, we try to use it as fallback 294 | if options[0].localeCodeDefaultFromFactory != "" { 295 | // Skipping default option will return nil, so we can use the factory 296 | // locale default 297 | v._locale = getLocaleAndSkipDefaultOption(_options.LocaleCode, options[0].localesFromFactory) 298 | if v._locale == nil { 299 | v._locale = getLocale(options[0].localeCodeDefaultFromFactory, options[0].localesFromFactory) 300 | } 301 | } else { 302 | v._locale = getLocale(_options.LocaleCode, options[0].localesFromFactory) 303 | } 304 | 305 | // If locale entries were specified, then we merge it with the calculated 306 | // Locale from the options localeCode 307 | if _options.Locale != nil { 308 | v._locale.merge(_options.Locale) 309 | } 310 | v.marshalJsonFunc = _options.MarshalJsonFunc 311 | } 312 | 313 | return v 314 | } 315 | -------------------------------------------------------------------------------- /validator.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | // Interface implemented by valgo Validators and custom Validators. 4 | type Validator interface { 5 | Context() *ValidatorContext 6 | } 7 | -------------------------------------------------------------------------------- /validator_any.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // The Any validator's type that keeps its validator context. 8 | type ValidatorAny struct { 9 | context *ValidatorContext 10 | } 11 | 12 | // Receive a value to validate. 13 | // 14 | // The value can be any type; 15 | // 16 | // Optionally, the function can receive a name and title, in that order, to be 17 | // displayed in the error messages. A value_%N` pattern is used as a name in the 18 | // error messages if a name and title are not supplied; for example: value_0. 19 | // When the name is provided but not the title, then the name is humanized to be 20 | // used as the title as well; for example the name phone_number will be 21 | // humanized as Phone Number. 22 | func Any(value any, nameAndTitle ...string) *ValidatorAny { 23 | return &ValidatorAny{context: NewContext(value, nameAndTitle...)} 24 | } 25 | 26 | // Return the context of the validator. The context is useful to create a custom 27 | // validator by extending this validator. 28 | func (validator *ValidatorAny) Context() *ValidatorContext { 29 | return validator.context 30 | } 31 | 32 | // Invert the logical value associated with the next validator function. 33 | // For example: 34 | // 35 | // // It will return false because `Not()` inverts the boolean value associated with the `Equal()` function 36 | // Is(v.Any("a").Not().Equal("a")).Valid() 37 | func (validator *ValidatorAny) Not() *ValidatorAny { 38 | validator.context.Not() 39 | 40 | return validator 41 | } 42 | 43 | // Introduces a logical OR in the chain of validation conditions, affecting the 44 | // evaluation order and priority of subsequent validators. A value passes the 45 | // validation if it meets any one condition following the Or() call, adhering to 46 | // a left-to-right evaluation. This mechanism allows for validating against 47 | // multiple criteria where satisfying any single criterion is sufficient. 48 | // Example: 49 | // 50 | // // This validator will pass because the string is equals "test". 51 | // input := "test" 52 | // isValid := v.Is(v.String(input).MinLength(5).Or().EqualTo("test")).Valid() 53 | func (validator *ValidatorAny) Or() *ValidatorAny { 54 | validator.context.Or() 55 | 56 | return validator 57 | } 58 | 59 | // Validate if a value is equal to another. This function internally uses 60 | // the golang `==` operator. 61 | // For example: 62 | // 63 | // status := "running" 64 | // Is(v.Any(status).Equal("running")) 65 | func (validator *ValidatorAny) EqualTo(value any, template ...string) *ValidatorAny { 66 | validator.context.AddWithValue( 67 | func() bool { 68 | return validator.context.Value() == value 69 | }, 70 | ErrorKeyEqualTo, value, template...) 71 | 72 | return validator 73 | } 74 | 75 | // Validate if a value passes a custom function. 76 | // For example: 77 | // 78 | // status := "" 79 | // Is(v.Any(status).Passing((v any) bool { 80 | // return v == getNewStatus() 81 | // }) 82 | func (validator *ValidatorAny) Passing(function func(v any) bool, template ...string) *ValidatorAny { 83 | validator.context.Add( 84 | func() bool { 85 | return function(validator.context.Value()) 86 | }, 87 | ErrorKeyPassing, template...) 88 | 89 | return validator 90 | } 91 | 92 | // Validate if a value is nil. 93 | // For example: 94 | // 95 | // var status *string 96 | // Is(v.Any(status).Nil()) 97 | func (validator *ValidatorAny) Nil(template ...string) *ValidatorAny { 98 | validator.context.Add( 99 | func() bool { 100 | val := validator.context.Value() 101 | // In Golang nil sometimes is not equal to raw nil, such as it's explained 102 | // here: https://dev.to/arxeiss/in-go-nil-is-not-equal-to-nil-sometimes-jn8 103 | // So, seems using reflection is the only option here 104 | return val == nil || 105 | (reflect.ValueOf(val).Kind() == reflect.Ptr && reflect.ValueOf(val).IsNil()) 106 | }, 107 | ErrorKeyNil, template...) 108 | 109 | return validator 110 | } 111 | -------------------------------------------------------------------------------- /validator_any_test.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestValidatorAnyNot(t *testing.T) { 10 | 11 | v := Is(Any(10).Not().EqualTo(11)) 12 | assert.True(t, v.Valid()) 13 | assert.Empty(t, v.Errors()) 14 | } 15 | 16 | func TestValidatorAnyEqualToValid(t *testing.T) { 17 | 18 | var v *Validation 19 | 20 | v = Is(Any(10).EqualTo(10)) 21 | assert.True(t, v.Valid()) 22 | assert.Empty(t, v.Errors()) 23 | 24 | type X struct{ Value int } 25 | x := X{Value: 10} 26 | y := X{Value: 10} 27 | v = Is(Any(x).EqualTo(y)) 28 | assert.True(t, v.Valid()) 29 | assert.Empty(t, v.Errors()) 30 | 31 | var a *int 32 | var b *int 33 | 34 | v = Is(Any(a).EqualTo(b)) 35 | assert.True(t, v.Valid()) 36 | assert.Empty(t, v.Errors()) 37 | } 38 | 39 | func TestValidatorAnyEqualToInvalid(t *testing.T) { 40 | 41 | var v *Validation 42 | 43 | v = Is(Any(11).EqualTo(10)) 44 | assert.False(t, v.Valid()) 45 | assert.Equal(t, 46 | "Value 0 must be equal to \"10\"", 47 | v.Errors()["value_0"].Messages()[0]) 48 | 49 | type X struct{ Value int } 50 | x := X{Value: 10} 51 | y := X{Value: 11} 52 | v = Is(Any(x).EqualTo(y)) 53 | assert.False(t, v.Valid()) 54 | assert.Equal(t, 55 | "Value 0 must be equal to \"{11}\"", 56 | v.Errors()["value_0"].Messages()[0]) 57 | 58 | v = Is(Any(10).EqualTo(nil)) 59 | assert.False(t, v.Valid()) 60 | assert.Equal(t, 61 | "Value 0 must be equal to \"\"", 62 | v.Errors()["value_0"].Messages()[0]) 63 | 64 | // Both nil but different types 65 | var a *int 66 | var b *int64 67 | 68 | v = Is(Any(a).EqualTo(b)) 69 | assert.False(t, v.Valid()) 70 | assert.Equal(t, 71 | "Value 0 must be equal to \"\"", 72 | v.Errors()["value_0"].Messages()[0]) 73 | } 74 | 75 | func TestValidatorAnyNilValid(t *testing.T) { 76 | 77 | var v *Validation 78 | 79 | var a *int 80 | v = Is(Any(a).Nil()) 81 | assert.True(t, v.Valid()) 82 | assert.Empty(t, v.Errors()) 83 | 84 | type X struct{} 85 | var x *X 86 | v = Is(Any(x).Nil()) 87 | assert.True(t, v.Valid()) 88 | assert.Empty(t, v.Errors()) 89 | } 90 | 91 | func TestValidatorAnyNilInvalid(t *testing.T) { 92 | 93 | var v *Validation 94 | 95 | v = Is(Any(0).Nil()) 96 | assert.False(t, v.Valid()) 97 | assert.Equal(t, 98 | "Value 0 must be nil", 99 | v.Errors()["value_0"].Messages()[0]) 100 | 101 | type X struct{} 102 | x := X{} 103 | 104 | v = Is(Any(&x).Nil()) 105 | assert.False(t, v.Valid()) 106 | assert.Equal(t, 107 | "Value 0 must be nil", 108 | v.Errors()["value_0"].Messages()[0]) 109 | } 110 | 111 | func TestValidatorAnyPassingValid(t *testing.T) { 112 | 113 | var v *Validation 114 | 115 | valTen := 10 116 | 117 | v = Is(Any(valTen).Passing(func(val any) bool { 118 | return val == 10 119 | })) 120 | assert.True(t, v.Valid()) 121 | assert.Empty(t, v.Errors()) 122 | } 123 | 124 | func TestValidatorAnyPassingInvalid(t *testing.T) { 125 | 126 | var v *Validation 127 | 128 | valTen := 10 129 | 130 | v = Is(Any(valTen).Passing(func(val any) bool { 131 | return val == 9 132 | })) 133 | assert.False(t, v.Valid()) 134 | assert.Equal(t, 135 | "Value 0 is not valid", 136 | v.Errors()["value_0"].Messages()[0]) 137 | } 138 | 139 | func TestValidatorAnyOrOperatorWithIs(t *testing.T) { 140 | var v *Validation 141 | 142 | var _true = true 143 | var _false = false 144 | 145 | // Testing Or operation with two valid conditions 146 | v = Is(Any(true).EqualTo(true).Or().EqualTo(true)) 147 | assert.True(t, v.Valid()) 148 | assert.Equal(t, _true || true, v.Valid()) 149 | assert.Empty(t, v.Errors()) 150 | 151 | // Testing Or operation with left invalid and right valid conditions 152 | v = Is(Any(true).EqualTo(false).Or().EqualTo(true)) 153 | assert.True(t, v.Valid()) 154 | assert.Equal(t, false || true, v.Valid()) 155 | assert.Empty(t, v.Errors()) 156 | 157 | // Testing Or operation with left valid and right invalid conditions 158 | v = Is(Any(true).EqualTo(true).Or().EqualTo(false)) 159 | assert.True(t, v.Valid()) 160 | assert.Equal(t, true || false, v.Valid()) 161 | assert.Empty(t, v.Errors()) 162 | 163 | // Testing Or operation with two invalid conditions 164 | v = Is(Any(true).EqualTo(false).Or().EqualTo(false)) 165 | assert.False(t, v.Valid()) 166 | assert.Equal(t, _false || false, v.Valid()) 167 | assert.NotEmpty(t, v.Errors()) 168 | 169 | // Testing And operation (default when no Or() function is used) with left valid and right invalid conditions 170 | v = Is(Any(true).EqualTo(true).EqualTo(false)) 171 | assert.False(t, v.Valid()) 172 | assert.Equal(t, true && false, v.Valid()) 173 | assert.NotEmpty(t, v.Errors()) 174 | 175 | // Testing combination of Not and Or operators with left valid and right invalid conditions 176 | v = Is(Any(true).Not().EqualTo(false).Or().EqualTo(false)) 177 | assert.True(t, v.Valid()) 178 | assert.Equal(t, !false || false, v.Valid()) 179 | assert.Empty(t, v.Errors()) 180 | 181 | // Testing combination of Not and Or operators with left invalid and right valid conditions 182 | v = Is(Any(true).Not().EqualTo(true).Or().EqualTo(true)) 183 | assert.True(t, v.Valid()) 184 | assert.Equal(t, !true || true, v.Valid()) 185 | assert.Empty(t, v.Errors()) 186 | 187 | // Testing multiple Or operations in sequence with the first condition being valid 188 | v = Is(Any(true).EqualTo(true).Or().EqualTo(false).Or().EqualTo(false)) 189 | assert.True(t, v.Valid()) 190 | assert.Equal(t, true || _false || false, v.Valid()) 191 | assert.Empty(t, v.Errors()) 192 | 193 | // Testing multiple Or operations in sequence with the last condition being valid 194 | v = Is(Any(true).EqualTo(false).Or().EqualTo(false).Or().EqualTo(true)) 195 | assert.True(t, v.Valid()) 196 | assert.Equal(t, _false || false || true, v.Valid()) 197 | assert.Empty(t, v.Errors()) 198 | 199 | // Testing invalid Or operation then valid And operation 200 | v = Is(Any(true).EqualTo(false).Or().EqualTo(true).EqualTo(true)) 201 | assert.True(t, v.Valid()) 202 | assert.Equal(t, false || _true && true, v.Valid()) 203 | assert.Empty(t, v.Errors()) 204 | 205 | // Testing valid Or operation then invalid And operation 206 | v = Is(Any(true).EqualTo(false).Or().EqualTo(true).EqualTo(false)) 207 | assert.False(t, v.Valid()) 208 | assert.Equal(t, false || true && false, v.Valid()) 209 | assert.NotEmpty(t, v.Errors()) 210 | 211 | // Testing valid And operation then invalid Or operation 212 | v = Is(Any(true).EqualTo(true).EqualTo(true).Or().EqualTo(false)) 213 | assert.True(t, v.Valid()) 214 | assert.Equal(t, _true && true || false, v.Valid()) 215 | assert.Empty(t, v.Errors()) 216 | 217 | // Testing invalid And operation then valid Or operation 218 | v = Is(Any(true).EqualTo(true).EqualTo(false).Or().EqualTo(true)) 219 | assert.True(t, v.Valid()) 220 | assert.Equal(t, true && false || true, v.Valid()) 221 | assert.Empty(t, v.Errors()) 222 | 223 | } 224 | 225 | func TestValidatorAnyOrOperatorWithCheck(t *testing.T) { 226 | var v *Validation 227 | 228 | // Check are Non-Short-circuited operations 229 | 230 | var _true = true 231 | var _false = false 232 | 233 | // Testing Or operation with two valid conditions 234 | v = Check(Any(true).EqualTo(true).Or().EqualTo(true)) 235 | assert.True(t, v.Valid()) 236 | assert.Equal(t, _true || true, v.Valid()) 237 | assert.Empty(t, v.Errors()) 238 | 239 | // Testing Or operation with left invalid and right valid conditions 240 | v = Check(Any(true).EqualTo(false).Or().EqualTo(true)) 241 | assert.True(t, v.Valid()) 242 | assert.Equal(t, false || true, v.Valid()) 243 | assert.Empty(t, v.Errors()) 244 | 245 | // Testing Or operation with left valid and right invalid conditions 246 | v = Check(Any(true).EqualTo(true).Or().EqualTo(false)) 247 | assert.True(t, v.Valid()) 248 | assert.Equal(t, true || false, v.Valid()) 249 | assert.Empty(t, v.Errors()) 250 | 251 | // Testing Or operation with two invalid conditions 252 | v = Check(Any(true).EqualTo(false).Or().EqualTo(false)) 253 | assert.False(t, v.Valid()) 254 | assert.Equal(t, _false || false, v.Valid()) 255 | assert.NotEmpty(t, v.Errors()) 256 | 257 | // Testing And operation (default when no Or() function is used) with left valid and right invalid conditions 258 | v = Check(Any(true).EqualTo(true).EqualTo(false)) 259 | assert.False(t, v.Valid()) 260 | assert.Equal(t, true && false, v.Valid()) 261 | assert.NotEmpty(t, v.Errors()) 262 | 263 | // Testing combination of Not and Or operators with left valid and right invalid conditions 264 | v = Check(Any(true).Not().EqualTo(false).Or().EqualTo(false)) 265 | assert.True(t, v.Valid()) 266 | assert.Equal(t, !false || false, v.Valid()) 267 | assert.Empty(t, v.Errors()) 268 | 269 | // Testing combination of Not and Or operators with left invalid and right valid conditions 270 | v = Check(Any(true).Not().EqualTo(true).Or().EqualTo(true)) 271 | assert.True(t, v.Valid()) 272 | assert.Equal(t, !true || true, v.Valid()) 273 | assert.Empty(t, v.Errors()) 274 | 275 | // Testing multiple Or operations in sequence with the first condition being valid 276 | v = Check(Any(true).EqualTo(true).Or().EqualTo(false).Or().EqualTo(false)) 277 | assert.True(t, v.Valid()) 278 | assert.Equal(t, true || _false || false, v.Valid()) 279 | assert.Empty(t, v.Errors()) 280 | 281 | // Testing multiple Or operations in sequence with the last condition being valid 282 | v = Check(Any(true).EqualTo(false).Or().EqualTo(false).Or().EqualTo(true)) 283 | assert.True(t, v.Valid()) 284 | assert.Equal(t, _false || false || true, v.Valid()) 285 | assert.Empty(t, v.Errors()) 286 | 287 | // Testing invalid Or operation then valid And operation 288 | v = Check(Any(true).EqualTo(false).Or().EqualTo(true).EqualTo(true)) 289 | assert.True(t, v.Valid()) 290 | assert.Equal(t, false || _true && true, v.Valid()) 291 | assert.Empty(t, v.Errors()) 292 | 293 | // Testing valid Or operation then invalid And operation 294 | v = Check(Any(true).EqualTo(false).Or().EqualTo(true).EqualTo(false)) 295 | assert.False(t, v.Valid()) 296 | assert.Equal(t, false || true && false, v.Valid()) 297 | assert.NotEmpty(t, v.Errors()) 298 | 299 | // Testing valid And operation then invalid Or operation 300 | v = Check(Any(true).EqualTo(true).EqualTo(true).Or().EqualTo(false)) 301 | assert.True(t, v.Valid()) 302 | assert.Equal(t, _true && true || false, v.Valid()) 303 | assert.Empty(t, v.Errors()) 304 | 305 | // Testing invalid And operation then valid Or operation 306 | v = Check(Any(true).EqualTo(true).EqualTo(false).Or().EqualTo(true)) 307 | assert.True(t, v.Valid()) 308 | assert.Equal(t, true && false || true, v.Valid()) 309 | assert.Empty(t, v.Errors()) 310 | } 311 | -------------------------------------------------------------------------------- /validator_boolean.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | func isBoolTrue[T ~bool](v T) bool { 4 | return bool(v) 5 | } 6 | 7 | func isBoolFalse[T ~bool](v T) bool { 8 | return !bool(v) 9 | } 10 | 11 | func isBoolEqual[T ~bool](v0 T, v1 T) bool { 12 | return v0 == v1 13 | } 14 | 15 | func isBoolInSlice[T ~bool](v T, slice []T) bool { 16 | for _, _v := range slice { 17 | if v == _v { 18 | return true 19 | } 20 | } 21 | return false 22 | } 23 | 24 | // The Boolean validator type that keeps its validator context. 25 | type ValidatorBool[T ~bool] struct { 26 | context *ValidatorContext 27 | } 28 | 29 | // Receives a boolean value to validate. 30 | // 31 | // The value also can be a custom boolean type such as `type Active bool;` 32 | // 33 | // Optionally, the function can receive a name and title, in that order, 34 | // to be displayed in the error messages. A `value_%N`` pattern is used as a name in 35 | // error messages if a name and title are not supplied; for example: value_0. When the name is 36 | // provided but not the title, then the name is humanized to be used as the 37 | // title as well; for example the name `phone_number` will be humanized as 38 | // `Phone Number` 39 | 40 | func Bool[T ~bool](value T, nameAndTitle ...string) *ValidatorBool[T] { 41 | return &ValidatorBool[T]{context: NewContext(value, nameAndTitle...)} 42 | } 43 | 44 | // Return the context of the validator. The context is useful to create a custom 45 | // validator by extending this validator. 46 | func (validator *ValidatorBool[T]) Context() *ValidatorContext { 47 | return validator.context 48 | } 49 | 50 | // Invert the boolean value associated with the next validator function. 51 | // For example: 52 | // 53 | // // It will return false because `Not()` inverts the boolean value associated with the True() function 54 | // Is(v.Bool(true).Not().True()).Valid() 55 | func (validator *ValidatorBool[T]) Not() *ValidatorBool[T] { 56 | validator.context.Not() 57 | 58 | return validator 59 | } 60 | 61 | // Introduces a logical OR in the chain of validation conditions, affecting the 62 | // evaluation order and priority of subsequent validators. A value passes the 63 | // validation if it meets any one condition following the Or() call, adhering to 64 | // a left-to-right evaluation. This mechanism allows for validating against 65 | // multiple criteria where satisfying any single criterion is sufficient. 66 | // Example: 67 | // 68 | // // This validator will pass because the input is equals false. 69 | // input := true 70 | // isValid := v.Is(v.Bool(input).False().Or().True()).Valid() 71 | func (validator *ValidatorBool[T]) Or() *ValidatorBool[T] { 72 | validator.context.Or() 73 | 74 | return validator 75 | } 76 | 77 | // Validate if a boolean value is equal to another. 78 | // For example: 79 | // 80 | // activated := true 81 | // Is(v.Bool(activated).Equal(true)) 82 | func (validator *ValidatorBool[T]) EqualTo(value T, template ...string) *ValidatorBool[T] { 83 | validator.context.AddWithValue( 84 | func() bool { 85 | return isBoolEqual(validator.context.Value().(T), value) 86 | }, 87 | ErrorKeyEqualTo, value, template...) 88 | 89 | return validator 90 | } 91 | 92 | // Validate if a boolean value is true. 93 | // For example: 94 | // 95 | // activated := true 96 | // Is(v.Bool(activated).True()) 97 | func (validator *ValidatorBool[T]) True(template ...string) *ValidatorBool[T] { 98 | validator.context.Add( 99 | func() bool { 100 | return isBoolTrue(validator.context.Value().(T)) 101 | }, 102 | ErrorKeyTrue, template...) 103 | 104 | return validator 105 | } 106 | 107 | // Validate if a boolean value is false. 108 | // For example: 109 | // 110 | // activated := false 111 | // Is(v.Bool(activated).Equal(true)).Valid() 112 | func (validator *ValidatorBool[T]) False(template ...string) *ValidatorBool[T] { 113 | validator.context.Add( 114 | func() bool { 115 | return isBoolFalse(validator.context.Value().(T)) 116 | }, 117 | ErrorKeyFalse, template...) 118 | 119 | return validator 120 | } 121 | 122 | // Validate if a boolean value pass a custom function. 123 | // For example: 124 | // 125 | // activated := false 126 | // Is(v.Bool(activated).Passing((v bool) bool { 127 | // return v == someBoolFunction() 128 | // }) 129 | func (validator *ValidatorBool[T]) Passing(function func(v T) bool, template ...string) *ValidatorBool[T] { 130 | validator.context.Add( 131 | func() bool { 132 | return function(validator.context.Value().(T)) 133 | }, 134 | ErrorKeyPassing, template...) 135 | 136 | return validator 137 | } 138 | 139 | // Validate if the value of a boolean pointer is present in a boolean slice. 140 | // For example: 141 | // 142 | // activated := false 143 | // elements := []bool{true, false, true} 144 | // Is(v.Bool(activated).InSlice(elements)) 145 | func (validator *ValidatorBool[T]) InSlice(slice []T, template ...string) *ValidatorBool[T] { 146 | validator.context.AddWithValue( 147 | func() bool { 148 | return isBoolInSlice(validator.context.Value().(T), slice) 149 | }, 150 | ErrorKeyInSlice, validator.context.Value(), template...) 151 | 152 | return validator 153 | } 154 | -------------------------------------------------------------------------------- /validator_boolean_p.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | // The Boolean pointer validator type that keeps its validator context. 4 | type ValidatorBoolP[T ~bool] struct { 5 | context *ValidatorContext 6 | } 7 | 8 | // Receives a boolean pointer to validate. 9 | // 10 | // The value also can be a custom boolean type such as `type Active bool;` 11 | // 12 | // Optionally, the function can receive a name and title, in that order, 13 | // to be displayed in the error messages. A `value_%N“ pattern is used as a name in 14 | // error messages if a name and title are not supplied; for example: value_0. When the name is 15 | // provided but not the title, then the name is humanized to be used as the 16 | // title as well; for example the name `phone_number` will be humanized as 17 | // `Phone Number` 18 | func BoolP[T ~bool](value *T, nameAndTitle ...string) *ValidatorBoolP[T] { 19 | return &ValidatorBoolP[T]{context: NewContext(value, nameAndTitle...)} 20 | } 21 | 22 | // Return the context of the validator. The context is useful to create a custom 23 | // validator by extending this validator. 24 | func (validator *ValidatorBoolP[T]) Context() *ValidatorContext { 25 | return validator.context 26 | } 27 | 28 | // Invert the boolean value associated with the next validator function. 29 | // For example: 30 | // 31 | // // It will return false because Not() inverts the boolean value associated with the True() function 32 | // activated := true 33 | // Is(v.BoolP(&activated).Not().True()).Valid() 34 | func (validator *ValidatorBoolP[T]) Not() *ValidatorBoolP[T] { 35 | validator.context.Not() 36 | 37 | return validator 38 | } 39 | 40 | // Introduces a logical OR in the chain of validation conditions, affecting the 41 | // evaluation order and priority of subsequent validators. A value passes the 42 | // validation if it meets any one condition following the Or() call, adhering to 43 | // a left-to-right evaluation. This mechanism allows for validating against 44 | // multiple criteria where satisfying any single criterion is sufficient. 45 | // Example: 46 | // 47 | // // This validator will pass because the input is equals false. 48 | // input := true 49 | // isValid := v.Is(v.BoolP(&input).Nil().Or().EqualTo(false)).Valid() 50 | func (validator *ValidatorBoolP[T]) Or() *ValidatorBoolP[T] { 51 | validator.context.Or() 52 | 53 | return validator 54 | } 55 | 56 | // Validate if the value of a boolean pointer is equal to another value. 57 | // For example: 58 | // 59 | // activated := true 60 | // Is(v.BoolP(&activated).Equal(true)) 61 | func (validator *ValidatorBoolP[T]) EqualTo(value T, template ...string) *ValidatorBoolP[T] { 62 | validator.context.AddWithValue( 63 | func() bool { 64 | return validator.context.Value().(*T) != nil && isBoolEqual(*(validator.context.Value().(*T)), value) 65 | }, 66 | ErrorKeyEqualTo, value, template...) 67 | 68 | return validator 69 | } 70 | 71 | // Validate if the value of a boolean pointer is true. 72 | // For example: 73 | // 74 | // activated := true 75 | // Is(v.BoolP(&activated).True()) 76 | func (validator *ValidatorBoolP[T]) True(template ...string) *ValidatorBoolP[T] { 77 | validator.context.Add( 78 | func() bool { 79 | return validator.context.Value().(*T) != nil && isBoolTrue(*(validator.context.Value().(*T))) 80 | }, 81 | ErrorKeyTrue, template...) 82 | 83 | return validator 84 | } 85 | 86 | // Validate if the value of a boolean pointer is false. 87 | // For example: 88 | // 89 | // activated := false 90 | // Is(v.BoolP(&activated).False()) 91 | func (validator *ValidatorBoolP[T]) False(template ...string) *ValidatorBoolP[T] { 92 | validator.context.Add( 93 | func() bool { 94 | return validator.context.Value().(*T) != nil && isBoolFalse(*(validator.context.Value().(*T))) 95 | }, 96 | ErrorKeyFalse, template...) 97 | 98 | return validator 99 | } 100 | 101 | // Validate if the value of a boolean pointer is false or nil. 102 | // For example: 103 | // 104 | // var activated *bool 105 | // Is(v.BoolP(activated).FalseOrNil()) 106 | // *activated = false 107 | // Is(v.BoolP(activated).FalseOrNil()) 108 | func (validator *ValidatorBoolP[T]) FalseOrNil(template ...string) *ValidatorBoolP[T] { 109 | validator.context.Add( 110 | func() bool { 111 | return validator.context.Value().(*T) == nil || isBoolFalse(*(validator.context.Value().(*T))) 112 | }, 113 | ErrorKeyFalse, template...) 114 | 115 | return validator 116 | } 117 | 118 | // Validate if a boolean pointer is nil. 119 | // For example: 120 | // 121 | // var activated *bool 122 | // Is(v.BoolP(activated).Nil()) 123 | func (validator *ValidatorBoolP[T]) Nil(template ...string) *ValidatorBoolP[T] { 124 | validator.context.Add( 125 | func() bool { 126 | return validator.context.Value().(*T) == nil 127 | }, 128 | ErrorKeyNil, template...) 129 | 130 | return validator 131 | } 132 | 133 | // Validate if a boolean pointer pass a custom function. 134 | // For example: 135 | // 136 | // activated := false 137 | // Is(v.BoolP(&activated).Passing((v *bool) bool { 138 | // return *v == someBoolFunction() 139 | // }) 140 | func (validator *ValidatorBoolP[T]) Passing(function func(v *T) bool, template ...string) *ValidatorBoolP[T] { 141 | validator.context.Add( 142 | func() bool { 143 | return function(validator.context.Value().(*T)) 144 | }, 145 | ErrorKeyPassing, template...) 146 | 147 | return validator 148 | } 149 | 150 | // Validate if the value of a boolean pointer is present in a boolean slice. 151 | // For example: 152 | // 153 | // activated := false 154 | // elements := []bool{true, false, true} 155 | // Is(v.BoolP(&activated).InSlice(elements)) 156 | func (validator *ValidatorBoolP[T]) InSlice(slice []T, template ...string) *ValidatorBoolP[T] { 157 | validator.context.AddWithValue( 158 | func() bool { 159 | return validator.context.Value().(*T) != nil && isBoolInSlice(*(validator.context.Value().(*T)), slice) 160 | }, 161 | ErrorKeyInSlice, validator.context.Value(), template...) 162 | 163 | return validator 164 | } 165 | -------------------------------------------------------------------------------- /validator_boolean_test.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestValidatorBoolNot(t *testing.T) { 10 | 11 | v := Is(Bool(true).Not().EqualTo(false)) 12 | assert.True(t, v.Valid()) 13 | assert.Empty(t, v.Errors()) 14 | } 15 | 16 | func TestValidatorBoolEqualToWhenIsValid(t *testing.T) { 17 | 18 | var v *Validation 19 | 20 | v = Is(Bool(true).EqualTo(true)) 21 | assert.True(t, v.Valid()) 22 | assert.Empty(t, v.Errors()) 23 | 24 | v = Is(Bool(false).EqualTo(false)) 25 | assert.True(t, v.Valid()) 26 | assert.Empty(t, v.Errors()) 27 | 28 | // Custom Type 29 | type MyBool bool 30 | var mybool1 MyBool = true 31 | var mybool2 MyBool = true 32 | 33 | v = Is(Bool(mybool1).EqualTo(mybool2)) 34 | assert.True(t, v.Valid()) 35 | assert.Empty(t, v.Errors()) 36 | } 37 | 38 | func TestValidatorBoolEqualToWhenIsInvalid(t *testing.T) { 39 | 40 | var v *Validation 41 | 42 | v = Is(Bool(true).EqualTo(false)) 43 | assert.False(t, v.Valid()) 44 | assert.Equal(t, 45 | "Value 0 must be equal to \"false\"", 46 | v.Errors()["value_0"].Messages()[0]) 47 | 48 | // Custom Type 49 | type MyBool bool 50 | var mybool1 MyBool = true 51 | var mybool2 MyBool = false 52 | 53 | v = Is(Bool(mybool1).EqualTo(mybool2)) 54 | assert.False(t, v.Valid()) 55 | assert.Equal(t, 56 | "Value 0 must be equal to \"false\"", 57 | v.Errors()["value_0"].Messages()[0]) 58 | } 59 | 60 | func TestValidatorBoolTrueWhenIsValid(t *testing.T) { 61 | 62 | var v *Validation 63 | 64 | v = Is(Bool(true).True()) 65 | assert.True(t, v.Valid()) 66 | assert.Empty(t, v.Errors()) 67 | 68 | // Custom Type 69 | type MyBool bool 70 | var mybool1 MyBool = true 71 | 72 | v = Is(Bool(mybool1).True()) 73 | assert.True(t, v.Valid()) 74 | assert.Empty(t, v.Errors()) 75 | } 76 | 77 | func TestValidatorBoolTrueWhenIsInvalid(t *testing.T) { 78 | 79 | var v *Validation 80 | 81 | v = Is(Bool(false).True()) 82 | assert.False(t, v.Valid()) 83 | assert.Equal(t, 84 | "Value 0 must be true", 85 | v.Errors()["value_0"].Messages()[0]) 86 | 87 | // Custom Type 88 | type MyBool bool 89 | var mybool1 MyBool = false 90 | 91 | v = Is(Bool(mybool1).True()) 92 | assert.False(t, v.Valid()) 93 | assert.Equal(t, 94 | "Value 0 must be true", 95 | v.Errors()["value_0"].Messages()[0]) 96 | } 97 | 98 | func TestValidatorBoolFalseWhenIsValid(t *testing.T) { 99 | 100 | var v *Validation 101 | 102 | v = Is(Bool(false).False()) 103 | assert.True(t, v.Valid()) 104 | assert.Empty(t, v.Errors()) 105 | 106 | // Custom Type 107 | type MyBool bool 108 | var mybool1 MyBool = false 109 | 110 | v = Is(Bool(mybool1).False()) 111 | assert.True(t, v.Valid()) 112 | assert.Empty(t, v.Errors()) 113 | } 114 | 115 | func TestValidatorBoolFalseWhenIsInvalid(t *testing.T) { 116 | 117 | var v *Validation 118 | 119 | v = Is(Bool(true).False()) 120 | assert.False(t, v.Valid()) 121 | assert.Equal(t, 122 | "Value 0 must be false", 123 | v.Errors()["value_0"].Messages()[0]) 124 | 125 | // Custom Type 126 | type MyBool bool 127 | var mybool1 MyBool = true 128 | 129 | v = Is(Bool(mybool1).False()) 130 | assert.False(t, v.Valid()) 131 | assert.Equal(t, 132 | "Value 0 must be false", 133 | v.Errors()["value_0"].Messages()[0]) 134 | } 135 | 136 | func TestValidatorBoolPassingWhenIsValid(t *testing.T) { 137 | 138 | var v *Validation 139 | 140 | v = Is(Bool(true).Passing(func(val bool) bool { 141 | return val == true 142 | })) 143 | assert.True(t, v.Valid()) 144 | assert.Empty(t, v.Errors()) 145 | 146 | // Custom Type 147 | type MyBool bool 148 | var mybool1 MyBool = true 149 | var mybool2 MyBool = true 150 | 151 | v = Is(Bool(mybool1).Passing(func(val MyBool) bool { 152 | return val == mybool2 153 | })) 154 | assert.True(t, v.Valid()) 155 | assert.Empty(t, v.Errors()) 156 | } 157 | 158 | func TestValidatorBoolPassingWhenIsInvalid(t *testing.T) { 159 | 160 | var v *Validation 161 | 162 | v = Is(Bool(false).Passing(func(val bool) bool { 163 | return val == true 164 | })) 165 | assert.False(t, v.Valid()) 166 | assert.Equal(t, 167 | "Value 0 is not valid", 168 | v.Errors()["value_0"].Messages()[0]) 169 | 170 | // Custom Type 171 | type MyBool bool 172 | var mybool1 MyBool = false 173 | 174 | v = Is(Bool(mybool1).Passing(func(val MyBool) bool { 175 | return val == true 176 | })) 177 | assert.False(t, v.Valid()) 178 | assert.Equal(t, 179 | "Value 0 is not valid", 180 | v.Errors()["value_0"].Messages()[0]) 181 | } 182 | 183 | func TestValidatorBoolInSliceValid(t *testing.T) { 184 | 185 | var v *Validation 186 | 187 | v = Is(Bool(false).InSlice([]bool{true, false, true})) 188 | assert.True(t, v.Valid()) 189 | assert.Empty(t, v.Errors()) 190 | 191 | // Custom Type 192 | type MyBool bool 193 | var myBool1 MyBool = false 194 | 195 | v = Is(Bool(myBool1).InSlice([]MyBool{true, false, true})) 196 | assert.True(t, v.Valid()) 197 | assert.Empty(t, v.Errors()) 198 | } 199 | 200 | func TestValidatorBoolInSliceInvalid(t *testing.T) { 201 | 202 | var v *Validation 203 | 204 | v = Is(Bool(true).InSlice([]bool{false, false, false})) 205 | assert.False(t, v.Valid()) 206 | assert.Equal(t, 207 | "Value 0 is not valid", 208 | v.Errors()["value_0"].Messages()[0]) 209 | 210 | // Custom Type 211 | type MyBool bool 212 | var myBool1 MyBool = true 213 | 214 | v = Is(Bool(myBool1).InSlice([]MyBool{false, false, false})) 215 | assert.False(t, v.Valid()) 216 | assert.Equal(t, 217 | "Value 0 is not valid", 218 | v.Errors()["value_0"].Messages()[0]) 219 | } 220 | 221 | func TestValidatorBoolOrOperatorWithIs(t *testing.T) { 222 | var v *Validation 223 | 224 | var _true = true 225 | var _false = false 226 | 227 | // Testing Or operation with two valid conditions 228 | v = Is(Bool(true).EqualTo(true).Or().EqualTo(true)) 229 | assert.True(t, v.Valid()) 230 | assert.Equal(t, _true || true, v.Valid()) 231 | assert.Empty(t, v.Errors()) 232 | 233 | // Testing Or operation with left invalid and right valid conditions 234 | v = Is(Bool(true).EqualTo(false).Or().EqualTo(true)) 235 | assert.True(t, v.Valid()) 236 | assert.Equal(t, false || true, v.Valid()) 237 | assert.Empty(t, v.Errors()) 238 | 239 | // Testing Or operation with left valid and right invalid conditions 240 | v = Is(Bool(true).EqualTo(true).Or().EqualTo(false)) 241 | assert.True(t, v.Valid()) 242 | assert.Equal(t, true || false, v.Valid()) 243 | assert.Empty(t, v.Errors()) 244 | 245 | // Testing Or operation with two invalid conditions 246 | v = Is(Bool(true).EqualTo(false).Or().EqualTo(false)) 247 | assert.False(t, v.Valid()) 248 | assert.Equal(t, _false || false, v.Valid()) 249 | assert.NotEmpty(t, v.Errors()) 250 | 251 | // Testing And operation (default when no Or() function is used) with left valid and right invalid conditions 252 | v = Is(Bool(true).EqualTo(true).EqualTo(false)) 253 | assert.False(t, v.Valid()) 254 | assert.Equal(t, true && false, v.Valid()) 255 | assert.NotEmpty(t, v.Errors()) 256 | 257 | // Testing combination of Not and Or operators with left valid and right invalid conditions 258 | v = Is(Bool(true).Not().EqualTo(false).Or().EqualTo(false)) 259 | assert.True(t, v.Valid()) 260 | assert.Equal(t, !false || false, v.Valid()) 261 | assert.Empty(t, v.Errors()) 262 | 263 | // Testing combination of Not and Or operators with left invalid and right valid conditions 264 | v = Is(Bool(true).Not().EqualTo(true).Or().EqualTo(true)) 265 | assert.True(t, v.Valid()) 266 | assert.Equal(t, !true || true, v.Valid()) 267 | assert.Empty(t, v.Errors()) 268 | 269 | // Testing multiple Or operations in sequence with the first condition being valid 270 | v = Is(Bool(true).EqualTo(true).Or().EqualTo(false).Or().EqualTo(false)) 271 | assert.True(t, v.Valid()) 272 | assert.Equal(t, true || _false || false, v.Valid()) 273 | assert.Empty(t, v.Errors()) 274 | 275 | // Testing multiple Or operations in sequence with the last condition being valid 276 | v = Is(Bool(true).EqualTo(false).Or().EqualTo(false).Or().EqualTo(true)) 277 | assert.True(t, v.Valid()) 278 | assert.Equal(t, _false || false || true, v.Valid()) 279 | assert.Empty(t, v.Errors()) 280 | 281 | // Testing invalid Or operation then valid And operation 282 | v = Is(Bool(true).EqualTo(false).Or().EqualTo(true).EqualTo(true)) 283 | assert.True(t, v.Valid()) 284 | assert.Equal(t, false || _true && true, v.Valid()) 285 | assert.Empty(t, v.Errors()) 286 | 287 | // Testing valid Or operation then invalid And operation 288 | v = Is(Bool(true).EqualTo(false).Or().EqualTo(true).EqualTo(false)) 289 | assert.False(t, v.Valid()) 290 | assert.Equal(t, false || true && false, v.Valid()) 291 | assert.NotEmpty(t, v.Errors()) 292 | 293 | // Testing valid And operation then invalid Or operation 294 | v = Is(Bool(true).EqualTo(true).EqualTo(true).Or().EqualTo(false)) 295 | assert.True(t, v.Valid()) 296 | assert.Equal(t, _true && true || false, v.Valid()) 297 | assert.Empty(t, v.Errors()) 298 | 299 | // Testing invalid And operation then valid Or operation 300 | v = Is(Bool(true).EqualTo(true).EqualTo(false).Or().EqualTo(true)) 301 | assert.True(t, v.Valid()) 302 | assert.Equal(t, true && false || true, v.Valid()) 303 | assert.Empty(t, v.Errors()) 304 | 305 | } 306 | 307 | func TestValidatorBoolOrOperatorWithCheck(t *testing.T) { 308 | var v *Validation 309 | 310 | // Check are Non-Short-circuited operations 311 | 312 | var _true = true 313 | var _false = false 314 | 315 | // Testing Or operation with two valid conditions 316 | v = Check(Bool(true).EqualTo(true).Or().EqualTo(true)) 317 | assert.True(t, v.Valid()) 318 | assert.Equal(t, _true || true, v.Valid()) 319 | assert.Empty(t, v.Errors()) 320 | 321 | // Testing Or operation with left invalid and right valid conditions 322 | v = Check(Bool(true).EqualTo(false).Or().EqualTo(true)) 323 | assert.True(t, v.Valid()) 324 | assert.Equal(t, false || true, v.Valid()) 325 | assert.Empty(t, v.Errors()) 326 | 327 | // Testing Or operation with left valid and right invalid conditions 328 | v = Check(Bool(true).EqualTo(true).Or().EqualTo(false)) 329 | assert.True(t, v.Valid()) 330 | assert.Equal(t, true || false, v.Valid()) 331 | assert.Empty(t, v.Errors()) 332 | 333 | // Testing Or operation with two invalid conditions 334 | v = Check(Bool(true).EqualTo(false).Or().EqualTo(false)) 335 | assert.False(t, v.Valid()) 336 | assert.Equal(t, _false || false, v.Valid()) 337 | assert.NotEmpty(t, v.Errors()) 338 | 339 | // Testing And operation (default when no Or() function is used) with left valid and right invalid conditions 340 | v = Check(Bool(true).EqualTo(true).EqualTo(false)) 341 | assert.False(t, v.Valid()) 342 | assert.Equal(t, true && false, v.Valid()) 343 | assert.NotEmpty(t, v.Errors()) 344 | 345 | // Testing combination of Not and Or operators with left valid and right invalid conditions 346 | v = Check(Bool(true).Not().EqualTo(false).Or().EqualTo(false)) 347 | assert.True(t, v.Valid()) 348 | assert.Equal(t, !false || false, v.Valid()) 349 | assert.Empty(t, v.Errors()) 350 | 351 | // Testing combination of Not and Or operators with left invalid and right valid conditions 352 | v = Check(Bool(true).Not().EqualTo(true).Or().EqualTo(true)) 353 | assert.True(t, v.Valid()) 354 | assert.Equal(t, !true || true, v.Valid()) 355 | assert.Empty(t, v.Errors()) 356 | 357 | // Testing multiple Or operations in sequence with the first condition being valid 358 | v = Check(Bool(true).EqualTo(true).Or().EqualTo(false).Or().EqualTo(false)) 359 | assert.True(t, v.Valid()) 360 | assert.Equal(t, true || _false || false, v.Valid()) 361 | assert.Empty(t, v.Errors()) 362 | 363 | // Testing multiple Or operations in sequence with the last condition being valid 364 | v = Check(Bool(true).EqualTo(false).Or().EqualTo(false).Or().EqualTo(true)) 365 | assert.True(t, v.Valid()) 366 | assert.Equal(t, _false || false || true, v.Valid()) 367 | assert.Empty(t, v.Errors()) 368 | 369 | // Testing invalid Or operation then valid And operation 370 | v = Check(Bool(true).EqualTo(false).Or().EqualTo(true).EqualTo(true)) 371 | assert.True(t, v.Valid()) 372 | assert.Equal(t, false || _true && true, v.Valid()) 373 | assert.Empty(t, v.Errors()) 374 | 375 | // Testing valid Or operation then invalid And operation 376 | v = Check(Bool(true).EqualTo(false).Or().EqualTo(true).EqualTo(false)) 377 | assert.False(t, v.Valid()) 378 | assert.Equal(t, false || true && false, v.Valid()) 379 | assert.NotEmpty(t, v.Errors()) 380 | 381 | // Testing valid And operation then invalid Or operation 382 | v = Check(Bool(true).EqualTo(true).EqualTo(true).Or().EqualTo(false)) 383 | assert.True(t, v.Valid()) 384 | assert.Equal(t, _true && true || false, v.Valid()) 385 | assert.Empty(t, v.Errors()) 386 | 387 | // Testing invalid And operation then valid Or operation 388 | v = Check(Bool(true).EqualTo(true).EqualTo(false).Or().EqualTo(true)) 389 | assert.True(t, v.Valid()) 390 | assert.Equal(t, true && false || true, v.Valid()) 391 | assert.Empty(t, v.Errors()) 392 | } 393 | -------------------------------------------------------------------------------- /validator_context.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | type validatorFragment struct { 4 | errorKey string 5 | template []string 6 | templateParams map[string]any 7 | function func() bool 8 | boolOperation bool 9 | orOperation bool 10 | isValid bool 11 | } 12 | 13 | // The context keeps the state and provides the functions to control a 14 | // custom validator. 15 | type ValidatorContext struct { 16 | fragments []*validatorFragment 17 | value any 18 | name *string 19 | title *string 20 | boolOperation bool 21 | orOperation bool 22 | } 23 | 24 | // Create a new [ValidatorContext] to be used by a custom validator. 25 | func NewContext(value any, nameAndTitle ...string) *ValidatorContext { 26 | 27 | context := &ValidatorContext{ 28 | value: value, 29 | fragments: []*validatorFragment{}, 30 | boolOperation: true, 31 | orOperation: false, 32 | } 33 | 34 | sizeNameAndTitle := len(nameAndTitle) 35 | if sizeNameAndTitle > 0 { 36 | name := nameAndTitle[0] 37 | context.name = &name 38 | if sizeNameAndTitle > 1 { 39 | title := nameAndTitle[1] 40 | context.title = &title 41 | } 42 | } 43 | 44 | return context 45 | } 46 | 47 | // Invert the boolean value associated with the next validator function in 48 | // a custom validator. 49 | func (ctx *ValidatorContext) Not() *ValidatorContext { 50 | ctx.boolOperation = false 51 | return ctx 52 | } 53 | 54 | // Add Or operation to validation. 55 | func (ctx *ValidatorContext) Or() *ValidatorContext { 56 | if len(ctx.fragments) > 0 { 57 | ctx.orOperation = true 58 | } 59 | return ctx 60 | } 61 | 62 | // Add a function to a custom validator and pass a value used for the 63 | // validator function to be displayed in the error message. 64 | // 65 | // Use [AddWithParams()] if the error message requires more input values. 66 | func (ctx *ValidatorContext) AddWithValue(function func() bool, errorKey string, value any, template ...string) *ValidatorContext { 67 | return ctx.AddWithParams( 68 | function, 69 | errorKey, 70 | map[string]any{"title": ctx.title, "value": value}, template...) 71 | } 72 | 73 | // Add a function to a custom validator. 74 | func (ctx *ValidatorContext) Add(function func() bool, errorKey string, template ...string) *ValidatorContext { 75 | return ctx.AddWithParams( 76 | function, 77 | errorKey, 78 | map[string]any{"title": ctx.title}, template...) 79 | } 80 | 81 | // Add a function to a custom validator and pass a map with values used for the 82 | // validator function to be displayed in the error message. 83 | func (ctx *ValidatorContext) AddWithParams(function func() bool, errorKey string, templateParams map[string]any, template ...string) *ValidatorContext { 84 | 85 | fragment := &validatorFragment{ 86 | errorKey: errorKey, 87 | templateParams: templateParams, 88 | function: function, 89 | boolOperation: ctx.boolOperation, 90 | orOperation: ctx.orOperation, 91 | isValid: true, 92 | } 93 | if len(template) > 0 { 94 | fragment.template = template 95 | } 96 | ctx.fragments = append(ctx.fragments, fragment) 97 | ctx.boolOperation = true 98 | ctx.orOperation = false 99 | 100 | return ctx 101 | } 102 | 103 | func (ctx *ValidatorContext) validateIs(validation *Validation) *Validation { 104 | return ctx.validate(validation, true) 105 | } 106 | 107 | func (ctx *ValidatorContext) validateCheck(validation *Validation) *Validation { 108 | return ctx.validate(validation, false) 109 | } 110 | 111 | func (ctx *ValidatorContext) validate(validation *Validation, shortCircuit bool) *Validation { 112 | // valid := true 113 | validation.currentIndex++ 114 | 115 | // Iterating through each fragment in the context's fragment list 116 | for i, fragment := range ctx.fragments { 117 | 118 | // If the previous fragment is not valid, the current fragment is not in an "or" operation, and the short circuit flag is true, 119 | // we return the current state of the validation without evaluating the current fragment 120 | if i > 0 && !ctx.fragments[i-1].isValid && !fragment.orOperation && shortCircuit { 121 | break 122 | } 123 | 124 | // If the current fragment is a part of an "or" operation and the previous fragment in the "or" operation 125 | // is valid, we mark the current fragment as valid and move to the next iteration 126 | if fragment.orOperation && ctx.fragments[i-1].isValid { 127 | continue 128 | } 129 | 130 | // Evaluating the validation function of the current fragment and updating the valid flag 131 | // The valid flag will be true only if the fragment function returns a value matching the fragment's boolean operation 132 | // and the valid flag was true before this evaluation 133 | fragment.isValid = fragment.function() == fragment.boolOperation 134 | 135 | // If the current fragment is valid and is part of an "or" operation, we backtrack to mark all preceding 136 | // fragments in the "or" operation chain as valid 137 | if fragment.isValid && fragment.orOperation { 138 | for j := i - 1; j >= 0; j-- { 139 | ctx.fragments[j].isValid = true 140 | // Breaking the loop when we reach the start of the "or" operation chain 141 | if !ctx.fragments[j].orOperation { 142 | break 143 | } 144 | } 145 | } 146 | 147 | // Setting the validation state of the current fragment 148 | // valid = fragment.isValid && valid 149 | } 150 | 151 | for _, fragment := range ctx.fragments { 152 | if !fragment.isValid { 153 | validation.invalidate(ctx.name, ctx.title, fragment) 154 | } 155 | } 156 | 157 | return validation 158 | } 159 | 160 | // Return the value being validated in a custom validator. 161 | func (ctx *ValidatorContext) Value() any { 162 | return ctx.value 163 | } 164 | -------------------------------------------------------------------------------- /validator_number.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | //go:generate go run generator/main.go 4 | 5 | // Custom generic type covering all numeric types. This type is used as the 6 | // value type in ValidatorNumber and ValidatorNumberP. 7 | type TypeNumber interface { 8 | ~int | 9 | ~int8 | 10 | ~int16 | 11 | ~int32 | 12 | ~int64 | 13 | ~uint | 14 | ~uint8 | 15 | ~uint16 | 16 | ~uint32 | 17 | ~uint64 | 18 | ~float32 | 19 | ~float64 20 | } 21 | 22 | func isNumberEqualTo[T TypeNumber](v0 T, v1 T) bool { 23 | return v0 == v1 24 | } 25 | 26 | func isNumberGreaterThan[T TypeNumber](v0 T, v1 T) bool { 27 | return v0 > v1 28 | } 29 | func isNumberGreaterOrEqualTo[T TypeNumber](v0 T, v1 T) bool { 30 | return v0 >= v1 31 | } 32 | func isNumberLessThan[T TypeNumber](v0 T, v1 T) bool { 33 | return v0 < v1 34 | } 35 | func isNumberLessOrEqualTo[T TypeNumber](v0 T, v1 T) bool { 36 | return v0 <= v1 37 | } 38 | func isNumberBetween[T TypeNumber](v T, min T, max T) bool { 39 | return v >= min && v <= max 40 | } 41 | func isNumberZero[T TypeNumber](v T) bool { 42 | return v == 0 43 | } 44 | func isNumberInSlice[T TypeNumber](v T, slice []T) bool { 45 | for _, _v := range slice { 46 | if v == _v { 47 | return true 48 | } 49 | } 50 | return false 51 | } 52 | 53 | // The [ValidatorNumber] provides functions for setting validation rules for a 54 | // [TypeNumber] value type, or a custom type based on a [TypeNumber]. 55 | // 56 | // [TypeNumber] is a generic interface defined by Valgo that generalizes any 57 | // standard Golang type. 58 | type ValidatorNumber[T TypeNumber] struct { 59 | context *ValidatorContext 60 | } 61 | 62 | // Receives a numeric value to validate. 63 | // 64 | // The value can be any golang numeric type (int64, int32, float32, uint, 65 | // etc.) or a custom numeric type such as `type Level int32;` 66 | // 67 | // Optionally, the function can receive a name and title, in that order, 68 | // to be displayed in the error messages. A `value_%N`` pattern is used as a name in 69 | // error messages if a name and title are not supplied; for example: value_0. 70 | // When the name is provided but not the title, then the name is humanized to be 71 | // used as the title as well; for example the name `phone_number` will be 72 | // humanized as `Phone Number` 73 | 74 | func Number[T TypeNumber](value T, nameAndTitle ...string) *ValidatorNumber[T] { 75 | return &ValidatorNumber[T]{context: NewContext(value, nameAndTitle...)} 76 | } 77 | 78 | // Return the context of the validator. The context is useful to create a custom 79 | // validator by extending this validator. 80 | func (validator *ValidatorNumber[T]) Context() *ValidatorContext { 81 | return validator.context 82 | } 83 | 84 | // Invert the logical value associated with the next validator function. 85 | // For example: 86 | // 87 | // // It will return false because Not() inverts the boolean value associated with the Zero() function 88 | // Is(v.Number(0).Not().Zero()).Valid() 89 | func (validator *ValidatorNumber[T]) Not() *ValidatorNumber[T] { 90 | validator.context.Not() 91 | 92 | return validator 93 | } 94 | 95 | // Introduces a logical OR in the chain of validation conditions, affecting the 96 | // evaluation order and priority of subsequent validators. A value passes the 97 | // validation if it meets any one condition following the Or() call, adhering to 98 | // a left-to-right evaluation. This mechanism allows for validating against 99 | // multiple criteria where satisfying any single criterion is sufficient. 100 | // Example: 101 | // 102 | // // This validator will pass because the input is Zero. 103 | // input := 0 104 | // isValid := v.Is(v.Number(input).GreaterThan(5).Or().Zero()).Valid() 105 | func (validator *ValidatorNumber[T]) Or() *ValidatorNumber[T] { 106 | validator.context.Or() 107 | 108 | return validator 109 | } 110 | 111 | // Validate if a numeric value is equal to another. This function internally uses 112 | // the golang `==` operator. 113 | // For example: 114 | // 115 | // quantity := 2 116 | // Is(v.Number(quantity).Equal(2)) 117 | func (validator *ValidatorNumber[T]) EqualTo(value T, template ...string) *ValidatorNumber[T] { 118 | validator.context.AddWithValue( 119 | func() bool { 120 | return isNumberEqualTo(validator.context.Value().(T), value) 121 | }, 122 | ErrorKeyEqualTo, value, template...) 123 | 124 | return validator 125 | } 126 | 127 | // Validate if a numeric value is greater than another. This function internally 128 | // uses the golang `>` operator. 129 | // For example: 130 | // 131 | // quantity := 3 132 | // Is(v.Number(quantity).GreaterThan(2)) 133 | func (validator *ValidatorNumber[T]) GreaterThan(value T, template ...string) *ValidatorNumber[T] { 134 | validator.context.AddWithValue( 135 | func() bool { 136 | return isNumberGreaterThan(validator.context.Value().(T), value) 137 | }, 138 | ErrorKeyGreaterThan, value, template...) 139 | 140 | return validator 141 | } 142 | 143 | // Validate if a numeric value is greater than or equal to another. This function 144 | // internally uses the golang `>=` operator. 145 | // For example: 146 | // 147 | // quantity := 3 148 | // Is(v.Number(quantity).GreaterOrEqualTo(3)) 149 | func (validator *ValidatorNumber[T]) GreaterOrEqualTo(value T, template ...string) *ValidatorNumber[T] { 150 | validator.context.AddWithValue( 151 | func() bool { 152 | return isNumberGreaterOrEqualTo(validator.context.Value().(T), value) 153 | }, 154 | ErrorKeyGreaterOrEqualTo, value, template...) 155 | 156 | return validator 157 | } 158 | 159 | // Validate if a numeric value is less than another. This function internally 160 | // uses the golang `<` operator. 161 | // For example: 162 | // 163 | // quantity := 2 164 | // Is(v.Number(quantity).LessThan(3)) 165 | func (validator *ValidatorNumber[T]) LessThan(value T, template ...string) *ValidatorNumber[T] { 166 | validator.context.AddWithValue( 167 | func() bool { 168 | return isNumberLessThan(validator.context.Value().(T), value) 169 | }, 170 | ErrorKeyLessThan, value, template...) 171 | 172 | return validator 173 | } 174 | 175 | // Validate if a numeric value is less than or equal to another. This function 176 | // internally uses the golang `<=` operator. 177 | // For example: 178 | // 179 | // quantity := 2 180 | // Is(v.Number(quantity).LessOrEqualTo(2)) 181 | func (validator *ValidatorNumber[T]) LessOrEqualTo(value T, template ...string) *ValidatorNumber[T] { 182 | validator.context.AddWithValue( 183 | func() bool { 184 | return isNumberLessOrEqualTo(validator.context.Value().(T), value) 185 | }, 186 | ErrorKeyLessOrEqualTo, value, template...) 187 | 188 | return validator 189 | } 190 | 191 | // Validate if a number is within a range (inclusive). 192 | // For example: 193 | // 194 | // Is(v.Number(3).Between(2,6)) 195 | func (validator *ValidatorNumber[T]) Between(min T, max T, template ...string) *ValidatorNumber[T] { 196 | validator.context.AddWithParams( 197 | func() bool { 198 | return isNumberBetween(validator.context.Value().(T), min, max) 199 | }, 200 | ErrorKeyBetween, 201 | map[string]any{"title": validator.context.title, "min": min, "max": max}, 202 | template...) 203 | 204 | return validator 205 | } 206 | 207 | // Validate if a numeric value is zero. 208 | // 209 | // For example: 210 | // 211 | // Is(v.Number(0).Zero()) 212 | func (validator *ValidatorNumber[T]) Zero(template ...string) *ValidatorNumber[T] { 213 | validator.context.Add( 214 | func() bool { 215 | return isNumberZero(validator.context.Value().(T)) 216 | }, 217 | ErrorKeyZero, template...) 218 | 219 | return validator 220 | } 221 | 222 | // Validate if a numeric value passes a custom function. 223 | // For example: 224 | // 225 | // quantity := 2 226 | // Is(v.Number(quantity).Passing((v int) bool { 227 | // return v == getAllowedQuantity() 228 | // }) 229 | func (validator *ValidatorNumber[T]) Passing(function func(v T) bool, template ...string) *ValidatorNumber[T] { 230 | validator.context.Add( 231 | func() bool { 232 | return function(validator.context.Value().(T)) 233 | }, 234 | ErrorKeyPassing, template...) 235 | 236 | return validator 237 | } 238 | 239 | // Validate if a number is present in a numeric slice. 240 | // For example: 241 | // 242 | // quantity := 3 243 | // validQuantities := []int{1,3,5} 244 | // Is(v.Number(quantity).InSlice(validQuantities)) 245 | func (validator *ValidatorNumber[T]) InSlice(slice []T, template ...string) *ValidatorNumber[T] { 246 | validator.context.AddWithValue( 247 | func() bool { 248 | return isNumberInSlice(validator.context.Value().(T), slice) 249 | }, 250 | ErrorKeyInSlice, validator.context.Value(), template...) 251 | 252 | return validator 253 | } 254 | -------------------------------------------------------------------------------- /validator_number_p.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | //go:generate go run generator/main.go 4 | 5 | // The Numeric pointer validator type that keeps its validator context. 6 | type ValidatorNumberP[T TypeNumber] struct { 7 | context *ValidatorContext 8 | } 9 | 10 | // Receives a numeric pointer to validate. 11 | // 12 | // The value can be any golang numeric pointer type (*int64, *int32, *float32, *uint, 13 | // etc.) or a custom numeric type such as `type Level *int32;` 14 | // 15 | // Optionally, the function can receive a name and title, in that order, 16 | // to be used in the error messages. A `value_%N“ pattern is used as a name in 17 | // error messages if a name and title are not supplied; for example: value_0. 18 | // When the name is provided but not the title, then the name is humanized to be 19 | // used as the title as well; for example the name `phone_number` will be 20 | // humanized as `Phone Number` 21 | func NumberP[T TypeNumber](value *T, nameAndTitle ...string) *ValidatorNumberP[T] { 22 | return &ValidatorNumberP[T]{context: NewContext(value, nameAndTitle...)} 23 | } 24 | 25 | // Return the context of the validator. The context is useful to create a custom 26 | // validator by extending this validator. 27 | func (validator *ValidatorNumberP[T]) Context() *ValidatorContext { 28 | return validator.context 29 | } 30 | 31 | // Invert the boolean value associated with the next validator function. 32 | // For example: 33 | // 34 | // // It will return false because Not() inverts the boolean value associated with the Zero() function 35 | // n := 0 36 | // Is(v.NumberP(&n).Not().Zero()).Valid() 37 | func (validator *ValidatorNumberP[T]) Not() *ValidatorNumberP[T] { 38 | validator.context.Not() 39 | 40 | return validator 41 | } 42 | 43 | // Introduces a logical OR in the chain of validation conditions, affecting the 44 | // evaluation order and priority of subsequent validators. A value passes the 45 | // validation if it meets any one condition following the Or() call, adhering to 46 | // a left-to-right evaluation. This mechanism allows for validating against 47 | // multiple criteria where satisfying any single criterion is sufficient. 48 | // Example: 49 | // 50 | // // This validator will pass because the input is Zero. 51 | // input := 0 52 | // isValid := v.Is(v.NumberP(&input).GreaterThan(5).Or().Zero()).Valid() 53 | func (validator *ValidatorNumberP[T]) Or() *ValidatorNumberP[T] { 54 | validator.context.Or() 55 | 56 | return validator 57 | } 58 | 59 | // Validate if a numeric pointer value is equal to another value. This function internally uses 60 | // the golang `==` operator. 61 | // For example: 62 | // 63 | // quantity := 2 64 | // Is(v.NumberP(quantity).Equal(2)) 65 | func (validator *ValidatorNumberP[T]) EqualTo(value T, template ...string) *ValidatorNumberP[T] { 66 | validator.context.AddWithValue( 67 | func() bool { 68 | return validator.context.Value().(*T) != nil && isNumberEqualTo(*(validator.context.Value().(*T)), value) 69 | }, 70 | ErrorKeyEqualTo, value, template...) 71 | 72 | return validator 73 | } 74 | 75 | // Validate if a numeric pointer value is greater than another value. This function internally 76 | // uses the golang `>` operator. 77 | // For example: 78 | // 79 | // quantity := 3 80 | // Is(v.NumberP(&quantity).GreaterThan(2)) 81 | func (validator *ValidatorNumberP[T]) GreaterThan(value T, template ...string) *ValidatorNumberP[T] { 82 | validator.context.AddWithValue( 83 | func() bool { 84 | return validator.context.Value().(*T) != nil && isNumberGreaterThan(*(validator.context.Value().(*T)), value) 85 | }, 86 | ErrorKeyGreaterThan, value, template...) 87 | 88 | return validator 89 | } 90 | 91 | // Validate if a numeric pointer value is greater than or equal to another value. This function 92 | // internally uses the golang `>=` operator. 93 | // For example: 94 | // 95 | // quantity := 3 96 | // Is(v.NumberP(&quantity).GreaterOrEqualTo(3)) 97 | func (validator *ValidatorNumberP[T]) GreaterOrEqualTo(value T, template ...string) *ValidatorNumberP[T] { 98 | validator.context.AddWithValue( 99 | func() bool { 100 | return validator.context.Value().(*T) != nil && isNumberGreaterOrEqualTo(*(validator.context.Value().(*T)), value) 101 | }, 102 | ErrorKeyGreaterOrEqualTo, value, template...) 103 | 104 | return validator 105 | } 106 | 107 | // Validate if a numeric pointer value is less than another value. This function internally 108 | // uses the golang `<` operator. 109 | // For example: 110 | // 111 | // quantity := 2 112 | // Is(v.NumberP(&quantity).LessThan(3)) 113 | func (validator *ValidatorNumberP[T]) LessThan(value T, template ...string) *ValidatorNumberP[T] { 114 | validator.context.AddWithValue( 115 | func() bool { 116 | return validator.context.Value().(*T) != nil && isNumberLessThan(*(validator.context.Value().(*T)), value) 117 | }, 118 | ErrorKeyLessThan, value, template...) 119 | 120 | return validator 121 | } 122 | 123 | // Validate if a numeric pointer value is less than or equal to another value. This function 124 | // internally uses the golang `<=` operator. 125 | // For example: 126 | // 127 | // quantity := 2 128 | // Is(v.NumberP(&quantity).LessOrEqualTo(2)) 129 | func (validator *ValidatorNumberP[T]) LessOrEqualTo(value T, template ...string) *ValidatorNumberP[T] { 130 | validator.context.AddWithValue( 131 | func() bool { 132 | return validator.context.Value().(*T) != nil && isNumberLessOrEqualTo(*(validator.context.Value().(*T)), value) 133 | }, 134 | ErrorKeyLessOrEqualTo, value, template...) 135 | 136 | return validator 137 | } 138 | 139 | // Validate if the value of a numeric pointer is within a range (inclusive). 140 | // For example: 141 | // 142 | // n := 3 143 | // Is(v.NumberP(&n).Between(2,6)) 144 | func (validator *ValidatorNumberP[T]) Between(min T, max T, template ...string) *ValidatorNumberP[T] { 145 | validator.context.AddWithParams( 146 | func() bool { 147 | return validator.context.Value().(*T) != nil && isNumberBetween(*(validator.context.Value().(*T)), min, max) 148 | }, 149 | ErrorKeyBetween, 150 | map[string]any{"title": validator.context.title, "min": min, "max": max}, 151 | template...) 152 | 153 | return validator 154 | } 155 | 156 | // Validate if a numeric pointer value is zero. 157 | // 158 | // For example: 159 | // 160 | // n := 0 161 | // Is(v.NumberP(&n).Zero()) 162 | func (validator *ValidatorNumberP[T]) Zero(template ...string) *ValidatorNumberP[T] { 163 | validator.context.Add( 164 | func() bool { 165 | return validator.context.Value().(*T) != nil && isNumberZero(*(validator.context.Value().(*T))) 166 | }, 167 | ErrorKeyZero, template...) 168 | 169 | return validator 170 | } 171 | 172 | // Validate if a numeric pointer value is zero or nil. 173 | // 174 | // For example: 175 | // 176 | // var _quantity *int 177 | // Is(v.NumberP(_quantity).ZeroOrNil()) // Will be true 178 | func (validator *ValidatorNumberP[T]) ZeroOrNil(template ...string) *ValidatorNumberP[T] { 179 | validator.context.Add( 180 | func() bool { 181 | return validator.context.Value().(*T) == nil || isNumberZero(*(validator.context.Value().(*T))) 182 | }, 183 | ErrorKeyZero, template...) 184 | 185 | return validator 186 | } 187 | 188 | // Validate if a numeric pointer value is nil. 189 | // 190 | // For example: 191 | // 192 | // var quantity *int 193 | // Is(v.NumberP(quantity).Nil()) // Will be true 194 | func (validator *ValidatorNumberP[T]) Nil(template ...string) *ValidatorNumberP[T] { 195 | validator.context.Add( 196 | func() bool { 197 | return validator.context.Value().(*T) == nil 198 | }, 199 | ErrorKeyNil, template...) 200 | 201 | return validator 202 | } 203 | 204 | // Validate if a numeric pointer value passes a custom function. 205 | // For example: 206 | // 207 | // quantity := 2 208 | // Is(v.NumberP(&quantity).Passing((v *int) bool { 209 | // return *v == getAllowedQuantity() 210 | // }) 211 | func (validator *ValidatorNumberP[T]) Passing(function func(v *T) bool, template ...string) *ValidatorNumberP[T] { 212 | validator.context.Add( 213 | func() bool { 214 | return function(validator.context.Value().(*T)) 215 | }, 216 | ErrorKeyPassing, template...) 217 | 218 | return validator 219 | } 220 | 221 | // Validate if a numeric pointer value is present in a numeric slice. 222 | // For example: 223 | // 224 | // quantity := 3 225 | // validQuantities := []int{1,3,5} 226 | // Is(v.NumberP(&quantity).InSlice(validQuantities)) 227 | func (validator *ValidatorNumberP[T]) InSlice(slice []T, template ...string) *ValidatorNumberP[T] { 228 | validator.context.AddWithValue( 229 | func() bool { 230 | return validator.context.Value().(*T) != nil && isNumberInSlice(*(validator.context.Value().(*T)), slice) 231 | }, 232 | ErrorKeyInSlice, validator.context.Value(), template...) 233 | 234 | return validator 235 | } 236 | -------------------------------------------------------------------------------- /validator_string.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | func isStringEqualTo[T ~string](v0 T, v1 T) bool { 9 | return v0 == v1 10 | } 11 | func isStringGreaterThan[T ~string](v0 T, v1 T) bool { 12 | return v0 > v1 13 | } 14 | func isStringGreaterOrEqualTo[T ~string](v0 T, v1 T) bool { 15 | return v0 >= v1 16 | } 17 | func isStringLessThan[T ~string](v0 T, v1 T) bool { 18 | return v0 < v1 19 | } 20 | func isStringLessOrEqualTo[T ~string](v0 T, v1 T) bool { 21 | return v0 <= v1 22 | } 23 | func isStringBetween[T ~string](v T, min T, max T) bool { 24 | return v >= min && v <= max 25 | } 26 | func isStringEmpty[T ~string](v T) bool { 27 | return len(v) == 0 28 | } 29 | func isStringBlank[T ~string](v T) bool { 30 | return len(strings.TrimSpace(string(v))) == 0 31 | } 32 | func isStringInSlice[T ~string](v T, slice []T) bool { 33 | for _, _v := range slice { 34 | if v == _v { 35 | return true 36 | } 37 | } 38 | return false 39 | } 40 | func isStringMatchingTo[T ~string](v T, regex *regexp.Regexp) bool { 41 | return regex.MatchString(string(v)) 42 | } 43 | func isStringMaxLength[T ~string](v T, length int) bool { 44 | return len(v) <= length 45 | } 46 | func isStringMinLength[T ~string](v T, length int) bool { 47 | return len(v) >= length 48 | } 49 | func isStringLength[T ~string](v T, length int) bool { 50 | return len(v) == length 51 | } 52 | func isStringLengthBetween[T ~string](v T, min int, max int) bool { 53 | return len(v) >= min && len(v) <= max 54 | } 55 | 56 | // The `ValidatorString` provides functions for setting validation rules for 57 | // a string value type, or a custom type based on a string. 58 | type ValidatorString[T ~string] struct { 59 | context *ValidatorContext 60 | } 61 | 62 | // Receive a string value to validate. 63 | // 64 | // The value can also be a custom string type such as type Status string;. 65 | // 66 | // Optionally, the function can receive a name and title, in that order, to be 67 | // displayed in the error messages. A value_%N` pattern is used as a name in the 68 | // error messages if a name and title are not supplied; for example: value_0. 69 | // When the name is provided but not the title, then the name is humanized to be 70 | // used as the title as well; for example the name phone_number will be 71 | // humanized as Phone Number. 72 | 73 | func String[T ~string](value T, nameAndTitle ...string) *ValidatorString[T] { 74 | return &ValidatorString[T]{context: NewContext(value, nameAndTitle...)} 75 | } 76 | 77 | // Return the context of the validator. The context is useful to create a custom 78 | // validator by extending this validator. 79 | func (validator *ValidatorString[T]) Context() *ValidatorContext { 80 | return validator.context 81 | } 82 | 83 | // Invert the boolean value associated with the next validator function. 84 | // For example: 85 | // 86 | // // It will return false because Not() inverts the boolean value associated with the Blank() function 87 | // Is(v.String("").Not().Blank()).Valid() 88 | func (validator *ValidatorString[T]) Not() *ValidatorString[T] { 89 | validator.context.Not() 90 | 91 | return validator 92 | } 93 | 94 | // Introduces a logical OR in the chain of validation conditions, affecting the 95 | // evaluation order and priority of subsequent validators. A value passes the 96 | // validation if it meets any one condition following the Or() call, adhering to 97 | // a left-to-right evaluation. This mechanism allows for validating against 98 | // multiple criteria where satisfying any single criterion is sufficient. 99 | // Example: 100 | // 101 | // // This validator will pass because the string is equals "test". 102 | // input := "test" 103 | // isValid := v.Is(v.String(input).MinLength(5).Or().EqualTo("test")).Valid() 104 | func (validator *ValidatorString[T]) Or() *ValidatorString[T] { 105 | validator.context.Or() 106 | 107 | return validator 108 | } 109 | 110 | // Validate if a string value is equal to another. This function internally uses 111 | // the golang `==` operator. 112 | // For example: 113 | // 114 | // status := "running" 115 | // Is(v.String(status).Equal("running")) 116 | func (validator *ValidatorString[T]) EqualTo(value T, template ...string) *ValidatorString[T] { 117 | validator.context.AddWithValue( 118 | func() bool { 119 | return isStringEqualTo(validator.context.Value().(T), value) 120 | }, 121 | ErrorKeyEqualTo, value, template...) 122 | 123 | return validator 124 | } 125 | 126 | // Validate if a string value is greater than another. This function internally 127 | // uses the golang `>` operator. 128 | // For example: 129 | // 130 | // section := "bb" 131 | // Is(v.String(section).GreaterThan("ba")) 132 | func (validator *ValidatorString[T]) GreaterThan(value T, template ...string) *ValidatorString[T] { 133 | validator.context.AddWithValue( 134 | func() bool { 135 | return isStringGreaterThan(validator.context.Value().(T), value) 136 | }, 137 | ErrorKeyGreaterThan, value, template...) 138 | 139 | return validator 140 | } 141 | 142 | // Validate if a string value is greater than or equal to another. This function 143 | // internally uses the golang `>=` operator. 144 | // For example: 145 | // 146 | // section := "bc" 147 | // Is(v.String(section).GreaterOrEqualTo("bc")) 148 | func (validator *ValidatorString[T]) GreaterOrEqualTo(value T, template ...string) *ValidatorString[T] { 149 | validator.context.AddWithValue( 150 | func() bool { 151 | return isStringGreaterOrEqualTo(validator.context.Value().(T), value) 152 | }, 153 | ErrorKeyGreaterOrEqualTo, value, template...) 154 | 155 | return validator 156 | } 157 | 158 | // Validate if a string value is less than another. This function internally 159 | // uses the golang `<` operator. 160 | // For example: 161 | // 162 | // section := "bb" 163 | // Is(v.String(section).LessThan("bc")) 164 | func (validator *ValidatorString[T]) LessThan(value T, template ...string) *ValidatorString[T] { 165 | validator.context.AddWithValue( 166 | func() bool { 167 | return isStringLessThan(validator.context.Value().(T), value) 168 | }, 169 | ErrorKeyLessThan, value, template...) 170 | 171 | return validator 172 | } 173 | 174 | // Validate if a string value is less than or equal to another. This function 175 | // internally uses the golang `<=` operator to compare two strings. 176 | // For example: 177 | // 178 | // section := "bc" 179 | // Is(v.String(section).LessOrEqualTo("bc")) 180 | func (validator *ValidatorString[T]) LessOrEqualTo(value T, template ...string) *ValidatorString[T] { 181 | validator.context.AddWithValue( 182 | func() bool { 183 | return isStringLessOrEqualTo(validator.context.Value().(T), value) 184 | }, 185 | ErrorKeyLessOrEqualTo, value, template...) 186 | 187 | return validator 188 | } 189 | 190 | // Validate if a string value is empty. Return false if the length of the string 191 | // is greater than zero, even if the string has only spaces. 192 | // 193 | // For checking if the string has only spaces, use the function `Blank()` 194 | // instead. 195 | // For example: 196 | // 197 | // Is(v.String("").Empty()) // Will be true 198 | // Is(v.String(" ").Empty()) // Will be false 199 | func (validator *ValidatorString[T]) Empty(template ...string) *ValidatorString[T] { 200 | validator.context.Add( 201 | func() bool { 202 | return isStringEmpty(validator.context.Value().(T)) 203 | }, 204 | ErrorKeyEmpty, template...) 205 | 206 | return validator 207 | } 208 | 209 | // Validate if a string value is blank. Blank will be true if the length 210 | // of the string is zero or if the string only has spaces. 211 | // For example: 212 | // 213 | // Is(v.String("").Empty()) // Will be true 214 | // Is(v.String(" ").Empty()) // Will be true 215 | func (validator *ValidatorString[T]) Blank(template ...string) *ValidatorString[T] { 216 | validator.context.Add( 217 | func() bool { 218 | return isStringBlank(validator.context.Value().(T)) 219 | }, 220 | ErrorKeyBlank, template...) 221 | 222 | return validator 223 | } 224 | 225 | // Validate if a string value passes a custom function. 226 | // For example: 227 | // 228 | // status := "" 229 | // Is(v.String(status).Passing((v string) bool { 230 | // return v == getNewStatus() 231 | // }) 232 | func (validator *ValidatorString[T]) Passing(function func(v0 T) bool, template ...string) *ValidatorString[T] { 233 | validator.context.Add( 234 | func() bool { 235 | return function(validator.context.Value().(T)) 236 | }, 237 | ErrorKeyPassing, template...) 238 | 239 | return validator 240 | } 241 | 242 | // Validate if a string is present in a string slice. 243 | // For example: 244 | // 245 | // status := "idle" 246 | // validStatus := []string{"idle", "paused", "stopped"} 247 | // Is(v.String(status).InSlice(validStatus)) 248 | func (validator *ValidatorString[T]) InSlice(slice []T, template ...string) *ValidatorString[T] { 249 | validator.context.AddWithValue( 250 | func() bool { 251 | return isStringInSlice(validator.context.Value().(T), slice) 252 | }, 253 | ErrorKeyInSlice, validator.context.Value(), template...) 254 | 255 | return validator 256 | } 257 | 258 | // Validate if a string matches a regular expression. 259 | // For example: 260 | // 261 | // status := "pre-approved" 262 | // regex, _ := regexp.Compile("pre-.+") 263 | // Is(v.String(status).MatchingTo(regex)) 264 | func (validator *ValidatorString[T]) MatchingTo(regex *regexp.Regexp, template ...string) *ValidatorString[T] { 265 | validator.context.AddWithParams( 266 | func() bool { 267 | return isStringMatchingTo(validator.context.Value().(T), regex) 268 | }, 269 | ErrorKeyMatchingTo, 270 | map[string]any{"title": validator.context.title, "regexp": regex}, 271 | template...) 272 | 273 | return validator 274 | } 275 | 276 | // Validate the maximum length of a string. 277 | // For example: 278 | // 279 | // slug := "myname" 280 | // Is(v.String(slug).MaxLength(6)) 281 | func (validator *ValidatorString[T]) MaxLength(length int, template ...string) *ValidatorString[T] { 282 | validator.context.AddWithParams( 283 | func() bool { 284 | return isStringMaxLength(validator.context.Value().(T), length) 285 | }, 286 | ErrorKeyMaxLength, 287 | map[string]any{"title": validator.context.title, "length": length}, 288 | template...) 289 | 290 | return validator 291 | } 292 | 293 | // Validate the minimum length of a string. 294 | // For example: 295 | // 296 | // slug := "myname" 297 | // Is(v.String(slug).MinLength(6)) 298 | func (validator *ValidatorString[T]) MinLength(length int, template ...string) *ValidatorString[T] { 299 | validator.context.AddWithParams( 300 | func() bool { 301 | return isStringMinLength(validator.context.Value().(T), length) 302 | }, 303 | ErrorKeyMinLength, 304 | map[string]any{"title": validator.context.title, "length": length}, 305 | template...) 306 | 307 | return validator 308 | } 309 | 310 | // Validate the length of a string. 311 | // For example: 312 | // 313 | // slug := "myname" 314 | // Is(v.String(slug).OfLength(6)) 315 | func (validator *ValidatorString[T]) OfLength(length int, template ...string) *ValidatorString[T] { 316 | validator.context.AddWithParams( 317 | func() bool { 318 | return isStringLength(validator.context.Value().(T), length) 319 | }, 320 | ErrorKeyLength, 321 | map[string]any{"title": validator.context.title, "length": length}, 322 | template...) 323 | 324 | return validator 325 | } 326 | 327 | // Validate if the length of a string is within a range (inclusive). 328 | // For example: 329 | // 330 | // slug := "myname" 331 | // Is(v.String(slug).OfLengthBetween(2,6)) 332 | func (validator *ValidatorString[T]) OfLengthBetween(min int, max int, template ...string) *ValidatorString[T] { 333 | validator.context.AddWithParams( 334 | func() bool { 335 | return isStringLengthBetween(validator.context.Value().(T), min, max) 336 | }, 337 | ErrorKeyLengthBetween, 338 | map[string]any{"title": validator.context.title, "min": min, "max": max}, 339 | template...) 340 | 341 | return validator 342 | } 343 | 344 | // Validate if the value of a string is within a range (inclusive). 345 | // For example: 346 | // 347 | // slug := "ab" 348 | // Is(v.String(slug).Between("ab","ac")) 349 | func (validator *ValidatorString[T]) Between(min T, max T, template ...string) *ValidatorString[T] { 350 | validator.context.AddWithParams( 351 | func() bool { 352 | return isStringBetween(validator.context.Value().(T), min, max) 353 | }, 354 | ErrorKeyBetween, 355 | map[string]any{"title": validator.context.title, "min": min, "max": max}, 356 | template...) 357 | 358 | return validator 359 | } 360 | -------------------------------------------------------------------------------- /validator_string_p.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | // The String pointer validator type that keeps its validator context. 8 | type ValidatorStringP[T ~string] struct { 9 | context *ValidatorContext 10 | } 11 | 12 | // Receives a string pointer to validate. 13 | // 14 | // The value also can be a custom boolean type such as `type Status *string;` 15 | // 16 | // Optionally, the function can receive a name and title, in that order, 17 | // to be used in the error messages. A `value_%N`` pattern is used as a name in 18 | // error messages if a name and title are not supplied; for example: value_0. 19 | // When the name is provided but not the title, then the name is humanized to be 20 | // used as the title as well; for example the name `phone_number` will be 21 | // humanized as `Phone Number` 22 | 23 | func StringP[T ~string](value *T, nameAndTitle ...string) *ValidatorStringP[T] { 24 | return &ValidatorStringP[T]{context: NewContext(value, nameAndTitle...)} 25 | } 26 | 27 | // Return the context of the validator. The context is useful to create a custom 28 | // validator by extending this validator. 29 | func (validator *ValidatorStringP[T]) Context() *ValidatorContext { 30 | return validator.context 31 | } 32 | 33 | // Invert the logical value associated to the next validator function. 34 | // For example: 35 | // 36 | // // It will return false because Not() inverts the boolean value associated with the Blank() function 37 | // status := "" 38 | // Is(v.StringP(&status).Not().Blank()).Valid() 39 | func (validator *ValidatorStringP[T]) Not() *ValidatorStringP[T] { 40 | validator.context.Not() 41 | 42 | return validator 43 | } 44 | 45 | // Introduces a logical OR in the chain of validation conditions, affecting the 46 | // evaluation order and priority of subsequent validators. A value passes the 47 | // validation if it meets any one condition following the Or() call, adhering to 48 | // a left-to-right evaluation. This mechanism allows for validating against 49 | // multiple criteria where satisfying any single criterion is sufficient. 50 | // Example: 51 | // 52 | // // This validator will pass because the string is equals "test". 53 | // input := "test" 54 | // isValid := v.Is(v.StringP(&input).MinLength(5).Or().EqualTo("test")).Valid() 55 | func (validator *ValidatorStringP[T]) Or() *ValidatorStringP[T] { 56 | validator.context.Or() 57 | 58 | return validator 59 | } 60 | 61 | // Validate if the value of a string pointer is equal to a another value. 62 | // For example: 63 | // 64 | // status := "running" 65 | // Is(v.StringP(&status).Equal("running")) 66 | func (validator *ValidatorStringP[T]) EqualTo(value T, template ...string) *ValidatorStringP[T] { 67 | validator.context.AddWithValue( 68 | func() bool { 69 | return validator.context.Value().(*T) != nil && isStringEqualTo(*(validator.context.Value().(*T)), value) 70 | }, 71 | ErrorKeyEqualTo, value, template...) 72 | 73 | return validator 74 | } 75 | 76 | // Validate if a string value is greater than another. This function internally 77 | // uses the golang `>` operator. 78 | // For example: 79 | // 80 | // section := "bb" 81 | // Is(v.StringP(§ion).GreaterThan("ba")) 82 | func (validator *ValidatorStringP[T]) GreaterThan(value T, template ...string) *ValidatorStringP[T] { 83 | validator.context.AddWithValue( 84 | func() bool { 85 | return validator.context.Value().(*T) != nil && isStringGreaterThan(*(validator.context.Value().(*T)), value) 86 | }, 87 | ErrorKeyGreaterThan, value, template...) 88 | 89 | return validator 90 | } 91 | 92 | // Validate if a string value is greater than or equal to another. This function 93 | // internally uses the golang `>=` operator. 94 | // For example: 95 | // 96 | // section := "bc" 97 | // Is(v.StringP(§ion).GreaterOrEqualTo("bc")) 98 | 99 | func (validator *ValidatorStringP[T]) GreaterOrEqualTo(value T, template ...string) *ValidatorStringP[T] { 100 | validator.context.AddWithValue( 101 | func() bool { 102 | return validator.context.Value().(*T) != nil && isStringGreaterOrEqualTo(*(validator.context.Value().(*T)), value) 103 | }, 104 | ErrorKeyGreaterOrEqualTo, value, template...) 105 | 106 | return validator 107 | } 108 | 109 | // Validate if a string value is less than another. This function internally 110 | // uses the golang `<` operator. 111 | // For example: 112 | // 113 | // section := "bb" 114 | // Is(v.StringP(§ion).LessThan("bc")) 115 | func (validator *ValidatorStringP[T]) LessThan(value T, template ...string) *ValidatorStringP[T] { 116 | validator.context.AddWithValue( 117 | func() bool { 118 | return validator.context.Value().(*T) != nil && isStringLessThan(*(validator.context.Value().(*T)), value) 119 | }, 120 | ErrorKeyLessThan, value, template...) 121 | 122 | return validator 123 | } 124 | 125 | // Validate if a string value is less or equal to another. This function 126 | // internally uses the golang `<=` operator to compare two strings. 127 | // For example: 128 | // 129 | // section := "bc" 130 | // Is(v.StringP(§ion).LessOrEqualTo("bc")) 131 | func (validator *ValidatorStringP[T]) LessOrEqualTo(value T, template ...string) *ValidatorStringP[T] { 132 | validator.context.AddWithValue( 133 | func() bool { 134 | return validator.context.Value().(*T) != nil && isStringLessOrEqualTo(*(validator.context.Value().(*T)), value) 135 | }, 136 | ErrorKeyLessOrEqualTo, value, template...) 137 | 138 | return validator 139 | } 140 | 141 | // Validate if a string value is empty. Empty will be false if the length 142 | // of the string is greater than zero, even if the string has only spaces. 143 | // For checking if the string has only spaces, uses the function `Blank()` 144 | // instead. 145 | // For example: 146 | // 147 | // status := "" 148 | // Is(v.StringP(&status).Empty()) // Will be true 149 | // status = " " 150 | // Is(v.StringP(&status).Empty()) // Will be false 151 | func (validator *ValidatorStringP[T]) Empty(template ...string) *ValidatorStringP[T] { 152 | validator.context.Add( 153 | func() bool { 154 | return validator.context.Value().(*T) != nil && isStringEmpty(*(validator.context.Value().(*T))) 155 | }, 156 | ErrorKeyEmpty, template...) 157 | 158 | return validator 159 | } 160 | 161 | // Validate if a string value is empty or nil. Empty will be false if the length 162 | // of the string is greater than zero, even if the string has only spaces. 163 | // For checking if the string has only spaces, uses the function `BlankOrNil()` 164 | // instead. 165 | // For example: 166 | // 167 | // status := "" 168 | // Is(v.StringP(&status).EmptyOrNil()) // Will be true 169 | // status = " " 170 | // Is(v.StringP(&status).EmptyOrNil()) // Will be false 171 | // var _status *string 172 | // Is(v.StringP(_status).EmptyOrNil()) // Will be true 173 | func (validator *ValidatorStringP[T]) EmptyOrNil(template ...string) *ValidatorStringP[T] { 174 | validator.context.Add( 175 | func() bool { 176 | return validator.context.Value().(*T) == nil || isStringEmpty(*(validator.context.Value().(*T))) 177 | }, 178 | ErrorKeyEmpty, template...) 179 | 180 | return validator 181 | } 182 | 183 | // Validate if a string value is blank. Blank will be true if the length 184 | // of the string is zero or if the string only has spaces. 185 | // For example: 186 | // 187 | // status := "" 188 | // Is(v.StringP(&status).Blank()) // Will be true 189 | // status = " " 190 | // Is(v.StringP(&status).Blank()) // Will be true 191 | func (validator *ValidatorStringP[T]) Blank(template ...string) *ValidatorStringP[T] { 192 | validator.context.Add( 193 | func() bool { 194 | return validator.context.Value().(*T) != nil && isStringBlank(*(validator.context.Value().(*T))) 195 | }, 196 | ErrorKeyBlank, template...) 197 | 198 | return validator 199 | } 200 | 201 | // Validate if a string value is blank or nil. Blank will be true if the length 202 | // of the string is zero or if the string only has spaces. 203 | // For example: 204 | // 205 | // status := "" 206 | // Is(v.StringP(&status).BlankOrNil()) // Will be true 207 | // status = " " 208 | // Is(v.StringP(&status).BlankOrNil()) // Will be true 209 | // var _status *string 210 | // Is(v.StringP(_status).BlankOrNil()) // Will be true 211 | func (validator *ValidatorStringP[T]) BlankOrNil(template ...string) *ValidatorStringP[T] { 212 | validator.context.Add( 213 | func() bool { 214 | return validator.context.Value().(*T) == nil || isStringBlank(*(validator.context.Value().(*T))) 215 | }, 216 | ErrorKeyBlank, template...) 217 | 218 | return validator 219 | } 220 | 221 | // Validate if a string pointer pass a custom function. 222 | // For example: 223 | // 224 | // status := "" 225 | // Is(v.StringP(&status).Passing((v string) bool { 226 | // return v == getNewStatus() 227 | // }) 228 | func (validator *ValidatorStringP[T]) Passing(function func(v0 *T) bool, template ...string) *ValidatorStringP[T] { 229 | validator.context.Add( 230 | func() bool { 231 | return function(validator.context.Value().(*T)) 232 | }, 233 | ErrorKeyPassing, template...) 234 | 235 | return validator 236 | } 237 | 238 | // Validate if the value of a string pointer is present in a string slice. 239 | // For example: 240 | // 241 | // status := "idle" 242 | // validStatus := []string{"idle", "paused", "stopped"} 243 | // Is(v.StringP(&status).InSlice(validStatus)) 244 | func (validator *ValidatorStringP[T]) InSlice(slice []T, template ...string) *ValidatorStringP[T] { 245 | validator.context.AddWithValue( 246 | func() bool { 247 | return validator.context.Value().(*T) != nil && isStringInSlice(*(validator.context.Value().(*T)), slice) 248 | }, 249 | ErrorKeyInSlice, validator.context.Value(), template...) 250 | 251 | return validator 252 | } 253 | 254 | // Validate if the value of a string pointer match a regular expression. 255 | // For example: 256 | // 257 | // status := "pre-approved" 258 | // regex, _ := regexp.Compile("pre-.+") 259 | // Is(v.StringP(&status).MatchingTo(regex)) 260 | func (validator *ValidatorStringP[T]) MatchingTo(regex *regexp.Regexp, template ...string) *ValidatorStringP[T] { 261 | validator.context.AddWithParams( 262 | func() bool { 263 | return validator.context.Value().(*T) != nil && isStringMatchingTo(*(validator.context.Value().(*T)), regex) 264 | }, 265 | ErrorKeyMatchingTo, 266 | map[string]any{"title": validator.context.title, "regexp": regex}, 267 | template...) 268 | 269 | return validator 270 | } 271 | 272 | // Validate if the maximum length of a string pointer's value. 273 | // For example: 274 | // 275 | // slug := "myname" 276 | // Is(v.StringP(&slug).MaxLength(6)) 277 | func (validator *ValidatorStringP[T]) MaxLength(length int, template ...string) *ValidatorStringP[T] { 278 | validator.context.AddWithParams( 279 | func() bool { 280 | return validator.context.Value().(*T) != nil && isStringMaxLength(*(validator.context.Value().(*T)), length) 281 | }, 282 | ErrorKeyMaxLength, 283 | map[string]any{"title": validator.context.title, "length": length}, 284 | template...) 285 | 286 | return validator 287 | } 288 | 289 | // Validate the minimum length of a string pointer's value 290 | // For example: 291 | // 292 | // slug := "myname" 293 | // Is(v.StringP(&slug).MinLength(6)) 294 | func (validator *ValidatorStringP[T]) MinLength(length int, template ...string) *ValidatorStringP[T] { 295 | validator.context.AddWithParams( 296 | func() bool { 297 | return validator.context.Value().(*T) != nil && isStringMinLength(*(validator.context.Value().(*T)), length) 298 | }, 299 | ErrorKeyMinLength, 300 | map[string]any{"title": validator.context.title, "length": length}, 301 | template...) 302 | 303 | return validator 304 | } 305 | 306 | // Validate the length of a string pointer's value. 307 | // For example: 308 | // 309 | // slug := "myname" 310 | // Is(v.StringP(&slug).OfLength(6)) 311 | func (validator *ValidatorStringP[T]) OfLength(length int, template ...string) *ValidatorStringP[T] { 312 | validator.context.AddWithParams( 313 | func() bool { 314 | return validator.context.Value().(*T) != nil && isStringLength(*(validator.context.Value().(*T)), length) 315 | }, 316 | ErrorKeyLength, 317 | map[string]any{"title": validator.context.title, "length": length}, 318 | template...) 319 | 320 | return validator 321 | } 322 | 323 | // Validate if the length of a string pointer's value is in a range (inclusive). 324 | // For example: 325 | // 326 | // slug := "myname" 327 | // Is(v.StringP(&slug).OfLengthBetween(2,6)) 328 | func (validator *ValidatorStringP[T]) OfLengthBetween(min int, max int, template ...string) *ValidatorStringP[T] { 329 | validator.context.AddWithParams( 330 | func() bool { 331 | return validator.context.Value().(*T) != nil && isStringLengthBetween(*(validator.context.Value().(*T)), min, max) 332 | }, 333 | ErrorKeyLengthBetween, 334 | map[string]any{"title": validator.context.title, "min": min, "max": max}, 335 | template...) 336 | 337 | return validator 338 | } 339 | 340 | // Validate if the value of a string is in a range (inclusive). 341 | // For example: 342 | // 343 | // slug := "myname" 344 | // Is(v.StringP(&slug).Between(2,6)) 345 | func (validator *ValidatorStringP[T]) Between(min T, max T, template ...string) *ValidatorStringP[T] { 346 | validator.context.AddWithParams( 347 | func() bool { 348 | return validator.context.Value().(*T) != nil && isStringBetween(*(validator.context.Value().(*T)), min, max) 349 | }, 350 | ErrorKeyBetween, 351 | map[string]any{"title": validator.context.title, "min": min, "max": max}, 352 | template...) 353 | 354 | return validator 355 | } 356 | 357 | // Validate if a string pointer is nil. 358 | // For example: 359 | // 360 | // var status *string 361 | // Is(v.StringP(status).Nil()) 362 | func (validator *ValidatorStringP[T]) Nil(template ...string) *ValidatorStringP[T] { 363 | validator.context.Add( 364 | func() bool { 365 | return validator.context.Value().(*T) == nil 366 | }, 367 | ErrorKeyNil, template...) 368 | 369 | return validator 370 | } 371 | -------------------------------------------------------------------------------- /validator_time.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | func isTimeEqualTo(v0 time.Time, v1 time.Time) bool { 8 | return v0.Equal(v1) 9 | } 10 | 11 | func isTimeAfter(v0 time.Time, v1 time.Time) bool { 12 | return v0.After(v1) 13 | } 14 | 15 | func isTimeAfterOrEqualTo(v0 time.Time, v1 time.Time) bool { 16 | return v0.After(v1) || v0.Equal(v1) 17 | } 18 | 19 | func isTimeBefore(v0 time.Time, v1 time.Time) bool { 20 | return v0.Before(v1) 21 | } 22 | 23 | func isTimeBeforeOrEqualTo(v0 time.Time, v1 time.Time) bool { 24 | return v0.Before(v1) || v0.Equal(v1) 25 | } 26 | 27 | func isTimeZero(v time.Time) bool { 28 | return v.IsZero() 29 | } 30 | 31 | func isTimeBetween(v time.Time, min time.Time, max time.Time) bool { 32 | return (v.After(min) || v.Equal(min)) && (v.Before(max) || v.Equal(max)) 33 | } 34 | 35 | func isTimeInSlice(v time.Time, slice []time.Time) bool { 36 | for _, _v := range slice { 37 | if v.Equal(_v) { 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | 44 | // The `ValidatorTime` structure provides a set of methods to perform validation 45 | // checks on time.Time values, utilizing Go's native time package. 46 | type ValidatorTime struct { 47 | context *ValidatorContext 48 | } 49 | 50 | // The Time function initiates a new `ValidatorTime` instance to validate a given 51 | // time value. The optional name and title parameters can be used for enhanced 52 | // error reporting. If a name is provided without a title, the name is humanized 53 | // to be used as the title. 54 | // 55 | // For example: 56 | // 57 | // startTime := time.Now() 58 | // v := ValidatorTime{} 59 | // v.Time(startTime, "start_time", "Start Time") 60 | func Time(value time.Time, nameAndTitle ...string) *ValidatorTime { 61 | return &ValidatorTime{context: NewContext(value, nameAndTitle...)} 62 | } 63 | 64 | // The Context method returns the current context of the validator, which can 65 | // be utilized to create custom validations by extending this validator. 66 | func (validator *ValidatorTime) Context() *ValidatorContext { 67 | return validator.context 68 | } 69 | 70 | // The Not method inverts the boolean value associated with the next validator 71 | // method. This can be used to negate the check performed by the next validation 72 | // method in the chain. 73 | // 74 | // For example: 75 | // 76 | // // Will return false because Not() inverts the boolean value of the Zero() function 77 | // startTime := time.Now() 78 | // Is(v.Time(startTime).Not().Zero()).Valid() 79 | func (validator *ValidatorTime) Not() *ValidatorTime { 80 | validator.context.Not() 81 | return validator 82 | } 83 | 84 | // Introduces a logical OR in the chain of validation conditions, affecting the 85 | // evaluation order and priority of subsequent validators. A value passes the 86 | // validation if it meets any one condition following the Or() call, adhering to 87 | // a left-to-right evaluation. This mechanism allows for validating against 88 | // multiple criteria where satisfying any single criterion is sufficient. 89 | // Example: 90 | // 91 | // // This validator will pass because the time is before or equal to time.Now(). 92 | // t := time.Now() 93 | // isValid := v.Is(v.Time(t).Zero().Or().BeforeOrEqualTo(time.Now())).Valid() 94 | func (validator *ValidatorTime) Or() *ValidatorTime { 95 | validator.context.Or() 96 | return validator 97 | } 98 | 99 | // The EqualTo method validates if the time value is equal to another given time 100 | // value. It uses the equality (`==`) operator from Go for the comparison. 101 | // 102 | // For example: 103 | // 104 | // timeA := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) 105 | // timeB := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) 106 | // Is(v.Time(timeA).EqualTo(timeB)).Valid() 107 | func (validator *ValidatorTime) EqualTo(value time.Time, template ...string) *ValidatorTime { 108 | validator.context.AddWithValue( 109 | func() bool { 110 | return isTimeEqualTo(validator.context.Value().(time.Time), value) 111 | }, 112 | ErrorKeyEqualTo, value, template...) 113 | 114 | return validator 115 | } 116 | 117 | // The After method checks if the time value is after a specified time. 118 | // 119 | // For example: 120 | // 121 | // startTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) 122 | // endTime := time.Date(2023, 1, 1, 1, 0, 0, 0, time.UTC) 123 | // Is(v.Time(endTime).After(startTime)).Valid() 124 | func (validator *ValidatorTime) After(value time.Time, template ...string) *ValidatorTime { 125 | validator.context.AddWithValue( 126 | func() bool { 127 | return isTimeAfter(validator.context.Value().(time.Time), value) 128 | }, 129 | ErrorKeyAfter, value, template...) 130 | 131 | return validator 132 | } 133 | 134 | // The AfterOrEqualTo method checks if the time value is either after or equal to 135 | // a specified time. 136 | // 137 | // For example: 138 | // 139 | // timeA := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) 140 | // timeB := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) 141 | // Is(v.Time(timeA).AfterOrEqualTo(timeB)).Valid() 142 | func (validator *ValidatorTime) AfterOrEqualTo(value time.Time, template ...string) *ValidatorTime { 143 | validator.context.AddWithValue( 144 | func() bool { 145 | return isTimeAfterOrEqualTo(validator.context.Value().(time.Time), value) 146 | }, 147 | ErrorKeyAfterOrEqualTo, value, template...) 148 | 149 | return validator 150 | } 151 | 152 | // The Before method checks if the time value is before a specified time. 153 | // 154 | // For example: 155 | // 156 | // startTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) 157 | // endTime := time.Date(2023, 1, 1, 1, 0, 0, 0, time.UTC) 158 | // Is(v.Time(startTime).Before(endTime)).Valid() 159 | func (validator *ValidatorTime) Before(value time.Time, template ...string) *ValidatorTime { 160 | validator.context.AddWithValue( 161 | func() bool { 162 | return isTimeBefore(validator.context.Value().(time.Time), value) 163 | }, 164 | ErrorKeyBefore, value, template...) 165 | 166 | return validator 167 | } 168 | 169 | // The BeforeOrEqualTo method checks if the time value is either before or equal to 170 | // a specified time. 171 | // 172 | // For example: 173 | // 174 | // timeA := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) 175 | // timeB := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) 176 | // Is(v.Time(timeA).BeforeOrEqualTo(timeB)).Valid() 177 | func (validator *ValidatorTime) BeforeOrEqualTo(value time.Time, template ...string) *ValidatorTime { 178 | validator.context.AddWithValue( 179 | func() bool { 180 | return isTimeBeforeOrEqualTo(validator.context.Value().(time.Time), value) 181 | }, 182 | ErrorKeyBeforeOrEqualTo, value, template...) 183 | 184 | return validator 185 | } 186 | 187 | // The Between method verifies if the time value falls within a given time range, inclusive. 188 | // 189 | // For example: 190 | // 191 | // minTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) 192 | // maxTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC) 193 | // checkTime := time.Date(2023, 1, 1, 6, 0, 0, 0, time.UTC) 194 | // Is(v.Time(checkTime).Between(minTime, maxTime)).Valid() 195 | func (validator *ValidatorTime) Between(min time.Time, max time.Time, template ...string) *ValidatorTime { 196 | validator.context.AddWithParams( 197 | func() bool { 198 | return isTimeBetween(validator.context.Value().(time.Time), min, max) 199 | }, 200 | ErrorKeyBetween, 201 | map[string]any{"title": validator.context.title, "min": min, "max": max}, 202 | template...) 203 | 204 | return validator 205 | } 206 | 207 | // The Zero method verifies if the time value is a zero time, which means it hasn't 208 | // been initialized yet. 209 | // 210 | // For example: 211 | // 212 | // zeroTime := time.Time{} 213 | // Is(v.Time(zeroTime).Zero()).Valid() 214 | func (validator *ValidatorTime) Zero(template ...string) *ValidatorTime { 215 | validator.context.Add( 216 | func() bool { 217 | return isTimeZero(validator.context.Value().(time.Time)) 218 | }, 219 | ErrorKeyZero, template...) 220 | 221 | return validator 222 | } 223 | 224 | // The Passing method allows for custom validation logic by accepting a function 225 | // that returns a boolean indicating whether the validation passed or failed. 226 | // 227 | // For example: 228 | // 229 | // checkTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) 230 | // Is(v.Time(checkTime).Passing(func(t time.Time) bool { 231 | // return t.Year() == 2023 232 | // })).Valid() 233 | func (validator *ValidatorTime) Passing(function func(v0 time.Time) bool, template ...string) *ValidatorTime { 234 | validator.context.Add( 235 | func() bool { 236 | return function(validator.context.Value().(time.Time)) 237 | }, 238 | ErrorKeyPassing, template...) 239 | 240 | return validator 241 | } 242 | 243 | // The InSlice method validates if the time value is found within a provided slice 244 | // of time values. 245 | // 246 | // For example: 247 | // 248 | // timeA := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) 249 | // timeB := time.Date(2023, 1, 1, 1, 0, 0, 0, time.UTC) 250 | // timeSlice := []time.Time{timeA, timeB} 251 | // checkTime := time.Date(2023, 1, 1, 1, 0, 0, 0, time.UTC) 252 | // Is(v.Time(checkTime).InSlice(timeSlice)).Valid() 253 | func (validator *ValidatorTime) InSlice(slice []time.Time, template ...string) *ValidatorTime { 254 | validator.context.AddWithValue( 255 | func() bool { 256 | return isTimeInSlice(validator.context.Value().(time.Time), slice) 257 | }, 258 | ErrorKeyInSlice, validator.context.Value(), template...) 259 | 260 | return validator 261 | } 262 | -------------------------------------------------------------------------------- /validator_time_p.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // ValidatorTimeP is a type that facilitates validation for time pointer variables. 8 | // It retains a context that records details about the validation process. 9 | type ValidatorTimeP struct { 10 | context *ValidatorContext 11 | } 12 | 13 | // TimeP initializes a new ValidatorTimeP instance with the provided time pointer 14 | // and optional name and title arguments for detailed error messages. 15 | // 16 | // Usage example: 17 | // 18 | // var myTime *time.Time 19 | // v.TimeP(myTime, "start_time", "Start Time") 20 | func TimeP(value *time.Time, nameAndTitle ...string) *ValidatorTimeP { 21 | return &ValidatorTimeP{context: NewContext(value, nameAndTitle...)} 22 | } 23 | 24 | // Context retrieves the context associated with the validator. 25 | func (validator *ValidatorTimeP) Context() *ValidatorContext { 26 | return validator.context 27 | } 28 | 29 | // Not negates the result of the next validator function in the chain. 30 | // 31 | // Usage example: 32 | // 33 | // t := time.Now() 34 | // Is(v.TimeP(&t).Not().Zero()).Valid() // Will return false since t is not a zero time. 35 | func (validator *ValidatorTimeP) Not() *ValidatorTimeP { 36 | validator.context.Not() 37 | return validator 38 | } 39 | 40 | // Introduces a logical OR in the chain of validation conditions, affecting the 41 | // evaluation order and priority of subsequent validators. A value passes the 42 | // validation if it meets any one condition following the Or() call, adhering to 43 | // a left-to-right evaluation. This mechanism allows for validating against 44 | // multiple criteria where satisfying any single criterion is sufficient. 45 | // Example: 46 | // 47 | // // This validator will pass because the time is before or equal to time.Now(). 48 | // t := time.Now() 49 | // isValid := v.Is(v.TimeP(&t).Nil().Or().BeforeOrEqualTo(time.Now())).Valid() 50 | func (validator *ValidatorTimeP) Or() *ValidatorTimeP { 51 | validator.context.Or() 52 | return validator 53 | } 54 | 55 | // EqualTo validates that the time pointer is equal to the specified time value. 56 | // 57 | // Usage example: 58 | // 59 | // t1 := time.Now() 60 | // t2 := t1 61 | // Is(v.TimeP(&t1).EqualTo(t2)).Valid() // Will return true. 62 | func (validator *ValidatorTimeP) EqualTo(value time.Time, template ...string) *ValidatorTimeP { 63 | validator.context.AddWithValue( 64 | func() bool { 65 | return validator.context.Value().(*time.Time) != nil && isTimeEqualTo(*(validator.context.Value().(*time.Time)), value) 66 | }, 67 | ErrorKeyEqualTo, value, template...) 68 | 69 | return validator 70 | } 71 | 72 | // After validates that the time pointer is after the specified time value. 73 | // 74 | // Usage example: 75 | // 76 | // t1 := time.Now() 77 | // t2 := t1.Add(-time.Hour) 78 | // Is(v.TimeP(&t1).After(t2)).Valid() // Will return true. 79 | func (validator *ValidatorTimeP) After(value time.Time, template ...string) *ValidatorTimeP { 80 | validator.context.AddWithValue( 81 | func() bool { 82 | return validator.context.Value().(*time.Time) != nil && isTimeAfter(*(validator.context.Value().(*time.Time)), value) 83 | }, 84 | ErrorKeyAfter, value, template...) 85 | 86 | return validator 87 | } 88 | 89 | // AfterOrEqualTo validates that the time pointer is after or equal to the specified time value. 90 | // 91 | // Usage example: 92 | // 93 | // t1 := time.Now() 94 | // t2 := t1 95 | // Is(v.TimeP(&t1).AfterOrEqualTo(t2)).Valid() // Will return true. 96 | func (validator *ValidatorTimeP) AfterOrEqualTo(value time.Time, template ...string) *ValidatorTimeP { 97 | validator.context.AddWithValue( 98 | func() bool { 99 | return validator.context.Value().(*time.Time) != nil && isTimeAfterOrEqualTo(*(validator.context.Value().(*time.Time)), value) 100 | }, 101 | ErrorKeyAfterOrEqualTo, value, template...) 102 | 103 | return validator 104 | } 105 | 106 | // Before validates that the time pointer is before the specified time value. 107 | // 108 | // Usage example: 109 | // 110 | // t1 := time.Now() 111 | // t2 := t1.Add(time.Hour) 112 | // Is(v.TimeP(&t1).Before(t2)).Valid() // Will return true. 113 | func (validator *ValidatorTimeP) Before(value time.Time, template ...string) *ValidatorTimeP { 114 | validator.context.AddWithValue( 115 | func() bool { 116 | return validator.context.Value().(*time.Time) != nil && isTimeBefore(*(validator.context.Value().(*time.Time)), value) 117 | }, 118 | ErrorKeyBefore, value, template...) 119 | 120 | return validator 121 | } 122 | 123 | // BeforeOrEqualTo validates that the time pointer is before or equal to the specified time value. 124 | // 125 | // Usage example: 126 | // 127 | // t1 := time.Now() 128 | // t2 := t1 129 | // Is(v.TimeP(&t1).BeforeOrEqualTo(t2)).Valid() // Will return true. 130 | func (validator *ValidatorTimeP) BeforeOrEqualTo(value time.Time, template ...string) *ValidatorTimeP { 131 | validator.context.AddWithValue( 132 | func() bool { 133 | return validator.context.Value().(*time.Time) != nil && isTimeBeforeOrEqualTo(*(validator.context.Value().(*time.Time)), value) 134 | }, 135 | ErrorKeyBeforeOrEqualTo, value, template...) 136 | 137 | return validator 138 | } 139 | 140 | // Between validates that the time pointer is between the specified minimum and maximum time values (inclusive). 141 | // 142 | // Usage example: 143 | // 144 | // t1 := time.Now() 145 | // min := t1.Add(-time.Hour) 146 | // max := t1.Add(time.Hour) 147 | // Is(v.TimeP(&t1).Between(min, max)).Valid() // Will return true. 148 | func (validator *ValidatorTimeP) Between(min time.Time, max time.Time, template ...string) *ValidatorTimeP { 149 | validator.context.AddWithParams( 150 | func() bool { 151 | return validator.context.Value().(*time.Time) != nil && isTimeBetween(*(validator.context.Value().(*time.Time)), min, max) 152 | }, 153 | ErrorKeyBetween, 154 | map[string]any{"title": validator.context.title, "min": min, "max": max}, 155 | template...) 156 | 157 | return validator 158 | } 159 | 160 | // Zero validates that the time pointer is pointing to a zero time value. 161 | // 162 | // Usage example: 163 | // 164 | // var t *time.Time 165 | // Is(v.TimeP(t).Zero()).Valid() // Will return true as t is nil and thus pointing to a zero time. 166 | func (validator *ValidatorTimeP) Zero(template ...string) *ValidatorTimeP { 167 | validator.context.Add( 168 | func() bool { 169 | return validator.context.Value().(*time.Time) != nil && isTimeZero(*(validator.context.Value().(*time.Time))) 170 | }, 171 | ErrorKeyZero, template...) 172 | 173 | return validator 174 | } 175 | 176 | // Passing allows for custom validation function to be applied on the time pointer. 177 | // 178 | // Usage example: 179 | // 180 | // t := time.Now() 181 | // Is(v.TimeP(&t).Passing(func(v0 *time.Time) bool { return v0.After(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) })).Valid() // Custom validation. 182 | func (validator *ValidatorTimeP) Passing(function func(v0 *time.Time) bool, template ...string) *ValidatorTimeP { 183 | validator.context.Add( 184 | func() bool { 185 | return function(validator.context.Value().(*time.Time)) 186 | }, 187 | ErrorKeyPassing, template...) 188 | 189 | return validator 190 | } 191 | 192 | // InSlice validates that the time pointer is pointing to a time value present in the specified slice. 193 | // 194 | // Usage example: 195 | // 196 | // t := time.Now() 197 | // validTimes := []time.Time{t, time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)} 198 | // Is(v.TimeP(&t).InSlice(validTimes)).Valid() // Will return true. 199 | func (validator *ValidatorTimeP) InSlice(slice []time.Time, template ...string) *ValidatorTimeP { 200 | validator.context.AddWithValue( 201 | func() bool { 202 | return validator.context.Value().(*time.Time) != nil && isTimeInSlice(*(validator.context.Value().(*time.Time)), slice) 203 | }, 204 | ErrorKeyInSlice, validator.context.Value(), template...) 205 | 206 | return validator 207 | } 208 | 209 | // Nil validates that the time pointer is nil. 210 | // 211 | // Usage example: 212 | // 213 | // var t *time.Time 214 | // Is(v.TimeP(t).Nil()).Valid() // Will return true as t is nil. 215 | func (validator *ValidatorTimeP) Nil(template ...string) *ValidatorTimeP { 216 | validator.context.Add( 217 | func() bool { 218 | return validator.context.Value().(*time.Time) == nil 219 | }, 220 | ErrorKeyNil, template...) 221 | 222 | return validator 223 | } 224 | 225 | // NilOrZero validates that the time pointer is either nil or pointing to a zero time value. 226 | // 227 | // Usage example: 228 | // 229 | // var t *time.Time 230 | // Is(v.TimeP(t).NilOrZero()).Valid() // Will return true as t is nil. 231 | func (validator *ValidatorTimeP) NilOrZero(template ...string) *ValidatorTimeP { 232 | validator.context.Add( 233 | func() bool { 234 | return validator.context.Value().(*time.Time) == nil || isTimeZero(*(validator.context.Value().(*time.Time))) 235 | 236 | }, 237 | ErrorKeyNil, template...) 238 | 239 | return validator 240 | } 241 | -------------------------------------------------------------------------------- /validator_time_p_test.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestValidatorPTimeNot(t *testing.T) { 11 | time1 := time.Now() 12 | 13 | v := Is(TimeP(&time1).Not().EqualTo(time.Now().Add(1 * time.Hour))) 14 | assert.True(t, v.Valid()) 15 | assert.Empty(t, v.Errors()) 16 | } 17 | 18 | func TestValidatorPTimeEqualToValid(t *testing.T) { 19 | var v *Validation 20 | now := time.Now() 21 | 22 | v = Is(TimeP(&now).EqualTo(now)) 23 | assert.True(t, v.Valid()) 24 | assert.Empty(t, v.Errors()) 25 | } 26 | 27 | func TestValidatorPTimeEqualToInvalid(t *testing.T) { 28 | var v *Validation 29 | now := time.Now() 30 | 31 | v = Is(TimeP(&now).EqualTo(now.Add(1 * time.Hour))) 32 | assert.False(t, v.Valid()) 33 | assert.NotEmpty(t, v.Errors()) 34 | assert.Equal(t, 35 | "Value 0 must be equal to \""+now.Add(1*time.Hour).String()+"\"", 36 | v.Errors()["value_0"].Messages()[0]) 37 | } 38 | 39 | func TestValidatorPTimeAfterValid(t *testing.T) { 40 | var v *Validation 41 | now := time.Now() 42 | 43 | v = Is(TimeP(&now).After(now.Add(-1 * time.Hour))) 44 | assert.True(t, v.Valid()) 45 | assert.Empty(t, v.Errors()) 46 | } 47 | 48 | func TestValidatorPTimeAfterInvalid(t *testing.T) { 49 | var v *Validation 50 | now := time.Now() 51 | 52 | v = Is(TimeP(&now).After(now)) 53 | assert.False(t, v.Valid()) 54 | assert.Equal(t, 55 | "Value 0 must be after \""+now.String()+"\"", 56 | v.Errors()["value_0"].Messages()[0]) 57 | 58 | v = Is(TimeP(&now).After(now.Add(1 * time.Hour))) 59 | assert.False(t, v.Valid()) 60 | assert.Equal(t, 61 | "Value 0 must be after \""+now.Add(1*time.Hour).String()+"\"", 62 | v.Errors()["value_0"].Messages()[0]) 63 | } 64 | 65 | func TestValidatorPTimeAfterOrEqualToValid(t *testing.T) { 66 | var v *Validation 67 | now := time.Now() 68 | 69 | v = Is(TimeP(&now).AfterOrEqualTo(now)) 70 | assert.True(t, v.Valid()) 71 | assert.Empty(t, v.Errors()) 72 | 73 | v = Is(TimeP(&now).AfterOrEqualTo(now.Add(-1 * time.Hour))) 74 | assert.True(t, v.Valid()) 75 | assert.Empty(t, v.Errors()) 76 | } 77 | 78 | func TestValidatorPTimeAfterOrEqualToInvalid(t *testing.T) { 79 | var v *Validation 80 | now := time.Now() 81 | 82 | v = Is(TimeP(&now).AfterOrEqualTo(now.Add(1 * time.Hour))) 83 | assert.False(t, v.Valid()) 84 | assert.Equal(t, 85 | "Value 0 must be after or equal to \""+now.Add(1*time.Hour).String()+"\"", 86 | v.Errors()["value_0"].Messages()[0]) 87 | } 88 | 89 | func TestValidatorPTimeBeforeValid(t *testing.T) { 90 | var v *Validation 91 | now := time.Now() 92 | 93 | v = Is(TimeP(&now).Before(now.Add(1 * time.Hour))) 94 | assert.True(t, v.Valid()) 95 | assert.Empty(t, v.Errors()) 96 | } 97 | 98 | func TestValidatorPTimeBeforeInvalid(t *testing.T) { 99 | var v *Validation 100 | now := time.Now() 101 | 102 | v = Is(TimeP(&now).Before(now)) 103 | assert.False(t, v.Valid()) 104 | assert.Equal(t, 105 | "Value 0 must be before \""+now.String()+"\"", 106 | v.Errors()["value_0"].Messages()[0]) 107 | 108 | v = Is(TimeP(&now).Before(now.Add(-1 * time.Hour))) 109 | assert.False(t, v.Valid()) 110 | assert.Equal(t, 111 | "Value 0 must be before \""+now.Add(-1*time.Hour).String()+"\"", 112 | v.Errors()["value_0"].Messages()[0]) 113 | } 114 | 115 | func TestValidatorPTimeBeforeOrEqualToValid(t *testing.T) { 116 | var v *Validation 117 | now := time.Now() 118 | 119 | v = Is(TimeP(&now).BeforeOrEqualTo(now)) 120 | assert.True(t, v.Valid()) 121 | assert.Empty(t, v.Errors()) 122 | 123 | v = Is(TimeP(&now).BeforeOrEqualTo(now.Add(1 * time.Hour))) 124 | assert.True(t, v.Valid()) 125 | assert.Empty(t, v.Errors()) 126 | } 127 | 128 | func TestValidatorPTimeBeforeOrEqualToInvalid(t *testing.T) { 129 | var v *Validation 130 | now := time.Now() 131 | 132 | v = Is(TimeP(&now).BeforeOrEqualTo(now.Add(-1 * time.Hour))) 133 | assert.False(t, v.Valid()) 134 | assert.Equal(t, 135 | "Value 0 must be before or equal to \""+now.Add(-1*time.Hour).String()+"\"", 136 | v.Errors()["value_0"].Messages()[0]) 137 | } 138 | 139 | func TestValidatorPTimeBetweenValid(t *testing.T) { 140 | var v *Validation 141 | now := time.Now() 142 | 143 | v = Is(TimeP(&now).Between(now.Add(-1*time.Hour), now.Add(1*time.Hour))) 144 | assert.True(t, v.Valid()) 145 | assert.Empty(t, v.Errors()) 146 | } 147 | 148 | func TestValidatorPTimeBetweenInvalid(t *testing.T) { 149 | var v *Validation 150 | now := time.Now() 151 | 152 | v = Is(TimeP(&now).Between(now.Add(1*time.Hour), now.Add(2*time.Hour))) 153 | assert.False(t, v.Valid()) 154 | assert.Equal(t, 155 | "Value 0 must be between \""+now.Add(1*time.Hour).String()+"\" and \""+now.Add(2*time.Hour).String()+"\"", 156 | v.Errors()["value_0"].Messages()[0]) 157 | } 158 | 159 | func TestValidatorPTimeInSliceValid(t *testing.T) { 160 | var v *Validation 161 | now := time.Now() 162 | timeSlice := []time.Time{now.Add(-1 * time.Hour), now, now.Add(1 * time.Hour)} 163 | 164 | v = Is(TimeP(&now).InSlice(timeSlice)) 165 | assert.True(t, v.Valid()) 166 | assert.Empty(t, v.Errors()) 167 | } 168 | 169 | func TestValidatorPTimeInSliceInvalid(t *testing.T) { 170 | var v *Validation 171 | now := time.Now() 172 | timeSlice := []time.Time{now.Add(-1 * time.Hour), now.Add(-30 * time.Minute), now.Add(-15 * time.Minute)} 173 | 174 | v = Is(TimeP(&now).InSlice(timeSlice)) 175 | assert.False(t, v.Valid()) 176 | assert.NotEmpty(t, v.Errors()) 177 | assert.Equal(t, 178 | "Value 0 is not valid", 179 | v.Errors()["value_0"].Messages()[0]) 180 | } 181 | 182 | func TestValidatorPTimePassingValid(t *testing.T) { 183 | 184 | var v *Validation 185 | 186 | now := time.Now() 187 | v = Is(TimeP(&now).Passing(func(val *time.Time) bool { 188 | return val.Equal(now) 189 | })) 190 | assert.True(t, v.Valid()) 191 | assert.Empty(t, v.Errors()) 192 | } 193 | 194 | func TestValidatorPTimePassingInvalid(t *testing.T) { 195 | 196 | var v *Validation 197 | 198 | now := time.Now() 199 | v = Is(TimeP(&now).Passing(func(val *time.Time) bool { 200 | return val.Equal(now.Add(1 * time.Hour)) 201 | })) 202 | assert.False(t, v.Valid()) 203 | assert.Equal(t, 204 | "Value 0 is not valid", 205 | v.Errors()["value_0"].Messages()[0]) 206 | } 207 | 208 | func TestValidatorPTimeZeroValid(t *testing.T) { 209 | 210 | var v *Validation 211 | 212 | zeroTime := time.Time{} 213 | 214 | v = Is(TimeP(&zeroTime).Zero()) 215 | assert.True(t, v.Valid()) 216 | assert.Empty(t, v.Errors()) 217 | } 218 | 219 | func TestValidatorPTimeZeroInvalid(t *testing.T) { 220 | 221 | var v *Validation 222 | 223 | nonZeroTime := time.Now() 224 | 225 | v = Is(TimeP(&nonZeroTime).Zero()) 226 | assert.False(t, v.Valid()) 227 | assert.Equal(t, 228 | "Value 0 must be zero", 229 | v.Errors()["value_0"].Messages()[0]) 230 | } 231 | 232 | func TestValidatorTimePOrOperatorWithIs(t *testing.T) { 233 | var v *Validation 234 | 235 | var _true = true 236 | var _false = false 237 | 238 | timeZero := time.Time{} 239 | timeOne := time.Time{}.Add(time.Second) 240 | 241 | // Testing Or operation with two valid conditions 242 | v = Is(TimeP(&timeOne).EqualTo(timeOne).Or().EqualTo(timeOne)) 243 | assert.True(t, v.Valid()) 244 | assert.Equal(t, _true || true, v.Valid()) 245 | assert.Empty(t, v.Errors()) 246 | 247 | // Testing Or operation with left invalid and right valid conditions 248 | v = Is(TimeP(&timeOne).EqualTo(timeZero).Or().EqualTo(timeOne)) 249 | assert.True(t, v.Valid()) 250 | assert.Equal(t, false || true, v.Valid()) 251 | assert.Empty(t, v.Errors()) 252 | 253 | // Testing Or operation with left valid and right invalid conditions 254 | v = Is(TimeP(&timeOne).EqualTo(timeOne).Or().EqualTo(timeZero)) 255 | assert.True(t, v.Valid()) 256 | assert.Equal(t, true || false, v.Valid()) 257 | assert.Empty(t, v.Errors()) 258 | 259 | // Testing Or operation with two invalid conditions 260 | v = Is(TimeP(&timeOne).EqualTo(timeZero).Or().EqualTo(timeZero)) 261 | assert.False(t, v.Valid()) 262 | assert.Equal(t, _false || false, v.Valid()) 263 | assert.NotEmpty(t, v.Errors()) 264 | 265 | // Testing And operation (default when no Or() function is used) with left valid and right invalid conditions 266 | v = Is(TimeP(&timeOne).EqualTo(timeOne).EqualTo(timeZero)) 267 | assert.False(t, v.Valid()) 268 | assert.Equal(t, true && false, v.Valid()) 269 | assert.NotEmpty(t, v.Errors()) 270 | 271 | // Testing combination of Not and Or operators with left valid and right invalid conditions 272 | v = Is(TimeP(&timeOne).Not().EqualTo(timeZero).Or().EqualTo(timeZero)) 273 | assert.True(t, v.Valid()) 274 | assert.Equal(t, !false || false, v.Valid()) 275 | assert.Empty(t, v.Errors()) 276 | 277 | // Testing combination of Not and Or operators with left invalid and right valid conditions 278 | v = Is(TimeP(&timeOne).Not().EqualTo(timeOne).Or().EqualTo(timeOne)) 279 | assert.True(t, v.Valid()) 280 | assert.Equal(t, !true || true, v.Valid()) 281 | assert.Empty(t, v.Errors()) 282 | 283 | // Testing multiple Or operations in sequence with the first condition being valid 284 | v = Is(TimeP(&timeOne).EqualTo(timeOne).Or().EqualTo(timeZero).Or().EqualTo(timeZero)) 285 | assert.True(t, v.Valid()) 286 | assert.Equal(t, true || _false || false, v.Valid()) 287 | assert.Empty(t, v.Errors()) 288 | 289 | // Testing multiple Or operations in sequence with the last condition being valid 290 | v = Is(TimeP(&timeOne).EqualTo(timeZero).Or().EqualTo(timeZero).Or().EqualTo(timeOne)) 291 | assert.True(t, v.Valid()) 292 | assert.Equal(t, _false || false || true, v.Valid()) 293 | assert.Empty(t, v.Errors()) 294 | 295 | // Testing invalid Or operation then valid And operation 296 | v = Is(TimeP(&timeOne).EqualTo(timeZero).Or().EqualTo(timeOne).EqualTo(timeOne)) 297 | assert.True(t, v.Valid()) 298 | assert.Equal(t, false || _true && true, v.Valid()) 299 | assert.Empty(t, v.Errors()) 300 | 301 | // Testing valid Or operation then invalid And operation 302 | v = Is(TimeP(&timeOne).EqualTo(timeZero).Or().EqualTo(timeOne).EqualTo(timeZero)) 303 | assert.False(t, v.Valid()) 304 | assert.Equal(t, false || true && false, v.Valid()) 305 | assert.NotEmpty(t, v.Errors()) 306 | 307 | // Testing valid And operation then invalid Or operation 308 | v = Is(TimeP(&timeOne).EqualTo(timeOne).EqualTo(timeOne).Or().EqualTo(timeZero)) 309 | assert.True(t, v.Valid()) 310 | assert.Equal(t, _true && true || false, v.Valid()) 311 | assert.Empty(t, v.Errors()) 312 | 313 | // Testing invalid And operation then valid Or operation 314 | v = Is(TimeP(&timeOne).EqualTo(timeOne).EqualTo(timeZero).Or().EqualTo(timeOne)) 315 | assert.True(t, v.Valid()) 316 | assert.Equal(t, true && false || true, v.Valid()) 317 | assert.Empty(t, v.Errors()) 318 | 319 | } 320 | 321 | func TestValidatorTimePOrOperatorWithCheck(t *testing.T) { 322 | var v *Validation 323 | 324 | // Check are Non-Short-circuited operations 325 | 326 | var _true = true 327 | var _false = false 328 | 329 | timeZero := time.Time{} 330 | timeOne := time.Time{}.Add(time.Second) 331 | 332 | // Testing Or operation with two valid conditions 333 | v = Check(TimeP(&timeOne).EqualTo(timeOne).Or().EqualTo(timeOne)) 334 | assert.True(t, v.Valid()) 335 | assert.Equal(t, _true || true, v.Valid()) 336 | assert.Empty(t, v.Errors()) 337 | 338 | // Testing Or operation with left invalid and right valid conditions 339 | v = Check(TimeP(&timeOne).EqualTo(timeZero).Or().EqualTo(timeOne)) 340 | assert.True(t, v.Valid()) 341 | assert.Equal(t, false || true, v.Valid()) 342 | assert.Empty(t, v.Errors()) 343 | 344 | // Testing Or operation with left valid and right invalid conditions 345 | v = Check(TimeP(&timeOne).EqualTo(timeOne).Or().EqualTo(timeZero)) 346 | assert.True(t, v.Valid()) 347 | assert.Equal(t, true || false, v.Valid()) 348 | assert.Empty(t, v.Errors()) 349 | 350 | // Testing Or operation with two invalid conditions 351 | v = Check(TimeP(&timeOne).EqualTo(timeZero).Or().EqualTo(timeZero)) 352 | assert.False(t, v.Valid()) 353 | assert.Equal(t, _false || false, v.Valid()) 354 | assert.NotEmpty(t, v.Errors()) 355 | 356 | // Testing And operation (default when no Or() function is used) with left valid and right invalid conditions 357 | v = Check(TimeP(&timeOne).EqualTo(timeOne).EqualTo(timeZero)) 358 | assert.False(t, v.Valid()) 359 | assert.Equal(t, true && false, v.Valid()) 360 | assert.NotEmpty(t, v.Errors()) 361 | 362 | // Testing combination of Not and Or operators with left valid and right invalid conditions 363 | v = Check(TimeP(&timeOne).Not().EqualTo(timeZero).Or().EqualTo(timeZero)) 364 | assert.True(t, v.Valid()) 365 | assert.Equal(t, !false || false, v.Valid()) 366 | assert.Empty(t, v.Errors()) 367 | 368 | // Testing combination of Not and Or operators with left invalid and right valid conditions 369 | v = Check(TimeP(&timeOne).Not().EqualTo(timeOne).Or().EqualTo(timeOne)) 370 | assert.True(t, v.Valid()) 371 | assert.Equal(t, !true || true, v.Valid()) 372 | assert.Empty(t, v.Errors()) 373 | 374 | // Testing multiple Or operations in sequence with the first condition being valid 375 | v = Check(TimeP(&timeOne).EqualTo(timeOne).Or().EqualTo(timeZero).Or().EqualTo(timeZero)) 376 | assert.True(t, v.Valid()) 377 | assert.Equal(t, true || _false || false, v.Valid()) 378 | assert.Empty(t, v.Errors()) 379 | 380 | // Testing multiple Or operations in sequence with the last condition being valid 381 | v = Check(TimeP(&timeOne).EqualTo(timeZero).Or().EqualTo(timeZero).Or().EqualTo(timeOne)) 382 | assert.True(t, v.Valid()) 383 | assert.Equal(t, _false || false || true, v.Valid()) 384 | assert.Empty(t, v.Errors()) 385 | 386 | // Testing invalid Or operation then valid And operation 387 | v = Check(TimeP(&timeOne).EqualTo(timeZero).Or().EqualTo(timeOne).EqualTo(timeOne)) 388 | assert.True(t, v.Valid()) 389 | assert.Equal(t, false || _true && true, v.Valid()) 390 | assert.Empty(t, v.Errors()) 391 | 392 | // Testing valid Or operation then invalid And operation 393 | v = Check(TimeP(&timeOne).EqualTo(timeZero).Or().EqualTo(timeOne).EqualTo(timeZero)) 394 | assert.False(t, v.Valid()) 395 | assert.Equal(t, false || true && false, v.Valid()) 396 | assert.NotEmpty(t, v.Errors()) 397 | 398 | // Testing valid And operation then invalid Or operation 399 | v = Check(TimeP(&timeOne).EqualTo(timeOne).EqualTo(timeOne).Or().EqualTo(timeZero)) 400 | assert.True(t, v.Valid()) 401 | assert.Equal(t, _true && true || false, v.Valid()) 402 | assert.Empty(t, v.Errors()) 403 | 404 | // Testing invalid And operation then valid Or operation 405 | v = Check(TimeP(&timeOne).EqualTo(timeOne).EqualTo(timeZero).Or().EqualTo(timeOne)) 406 | assert.True(t, v.Valid()) 407 | assert.Equal(t, true && false || true, v.Valid()) 408 | assert.Empty(t, v.Errors()) 409 | } 410 | -------------------------------------------------------------------------------- /validator_time_test.go: -------------------------------------------------------------------------------- 1 | package valgo 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestValidatorTimeNot(t *testing.T) { 11 | 12 | v := Is(Time(time.Now()).Not().EqualTo(time.Now().Add(1 * time.Hour))) 13 | assert.True(t, v.Valid()) 14 | assert.Empty(t, v.Errors()) 15 | } 16 | 17 | func TestValidatorTimeEqualToValid(t *testing.T) { 18 | var v *Validation 19 | now := time.Now() 20 | 21 | v = Is(Time(now).EqualTo(now)) 22 | assert.True(t, v.Valid()) 23 | assert.Empty(t, v.Errors()) 24 | } 25 | 26 | func TestValidatorTimeEqualToInvalid(t *testing.T) { 27 | var v *Validation 28 | now := time.Now() 29 | 30 | v = Is(Time(now).EqualTo(now.Add(1 * time.Hour))) 31 | assert.False(t, v.Valid()) 32 | assert.NotEmpty(t, v.Errors()) 33 | assert.Equal(t, 34 | "Value 0 must be equal to \""+now.Add(1*time.Hour).String()+"\"", 35 | v.Errors()["value_0"].Messages()[0]) 36 | } 37 | 38 | func TestValidatorTimeAfterValid(t *testing.T) { 39 | var v *Validation 40 | now := time.Now() 41 | 42 | v = Is(Time(now.Add(1 * time.Hour)).After(now)) 43 | assert.True(t, v.Valid()) 44 | assert.Empty(t, v.Errors()) 45 | } 46 | 47 | func TestValidatorTimeAfterInvalid(t *testing.T) { 48 | var v *Validation 49 | now := time.Now() 50 | 51 | v = Is(Time(now).After(now)) 52 | assert.False(t, v.Valid()) 53 | assert.Equal(t, 54 | "Value 0 must be after \""+now.String()+"\"", 55 | v.Errors()["value_0"].Messages()[0]) 56 | 57 | v = Is(Time(now).After(now.Add(1 * time.Hour))) 58 | assert.False(t, v.Valid()) 59 | assert.Equal(t, 60 | "Value 0 must be after \""+now.Add(1*time.Hour).String()+"\"", 61 | v.Errors()["value_0"].Messages()[0]) 62 | } 63 | 64 | func TestValidatorTimeAfterOrEqualToValid(t *testing.T) { 65 | var v *Validation 66 | now := time.Now() 67 | 68 | v = Is(Time(now).AfterOrEqualTo(now)) 69 | assert.True(t, v.Valid()) 70 | assert.Empty(t, v.Errors()) 71 | 72 | v = Is(Time(now.Add(1 * time.Hour)).AfterOrEqualTo(now)) 73 | assert.True(t, v.Valid()) 74 | assert.Empty(t, v.Errors()) 75 | } 76 | 77 | func TestValidatorTimeAfterOrEqualToInvalid(t *testing.T) { 78 | var v *Validation 79 | now := time.Now() 80 | 81 | v = Is(Time(now).AfterOrEqualTo(now.Add(1 * time.Hour))) 82 | assert.False(t, v.Valid()) 83 | assert.Equal(t, 84 | "Value 0 must be after or equal to \""+now.Add(1*time.Hour).String()+"\"", 85 | v.Errors()["value_0"].Messages()[0]) 86 | } 87 | 88 | func TestValidatorTimeBeforeValid(t *testing.T) { 89 | var v *Validation 90 | now := time.Now() 91 | 92 | v = Is(Time(now).Before(now.Add(1 * time.Hour))) 93 | assert.True(t, v.Valid()) 94 | assert.Empty(t, v.Errors()) 95 | } 96 | 97 | func TestValidatorTimeBeforeInvalid(t *testing.T) { 98 | var v *Validation 99 | now := time.Now() 100 | 101 | v = Is(Time(now).Before(now)) 102 | assert.False(t, v.Valid()) 103 | assert.Equal(t, 104 | "Value 0 must be before \""+now.String()+"\"", 105 | v.Errors()["value_0"].Messages()[0]) 106 | 107 | v = Is(Time(now.Add(1 * time.Hour)).Before(now)) 108 | assert.False(t, v.Valid()) 109 | assert.Equal(t, 110 | "Value 0 must be before \""+now.String()+"\"", 111 | v.Errors()["value_0"].Messages()[0]) 112 | } 113 | 114 | func TestValidatorTimeBeforeOrEqualToValid(t *testing.T) { 115 | var v *Validation 116 | now := time.Now() 117 | 118 | v = Is(Time(now).BeforeOrEqualTo(now)) 119 | assert.True(t, v.Valid()) 120 | assert.Empty(t, v.Errors()) 121 | 122 | v = Is(Time(now).BeforeOrEqualTo(now.Add(1 * time.Hour))) 123 | assert.True(t, v.Valid()) 124 | assert.Empty(t, v.Errors()) 125 | } 126 | 127 | func TestValidatorTimeBeforeOrEqualToInvalid(t *testing.T) { 128 | var v *Validation 129 | now := time.Now() 130 | 131 | v = Is(Time(now.Add(1 * time.Hour)).BeforeOrEqualTo(now)) 132 | assert.False(t, v.Valid()) 133 | assert.Equal(t, 134 | "Value 0 must be before or equal to \""+now.String()+"\"", 135 | v.Errors()["value_0"].Messages()[0]) 136 | } 137 | 138 | func TestValidatorTimeBetweenValid(t *testing.T) { 139 | var v *Validation 140 | now := time.Now() 141 | 142 | v = Is(Time(now).Between(now.Add(-1*time.Hour), now.Add(1*time.Hour))) 143 | assert.True(t, v.Valid()) 144 | assert.Empty(t, v.Errors()) 145 | } 146 | 147 | func TestValidatorTimeBetweenInvalid(t *testing.T) { 148 | var v *Validation 149 | now := time.Now() 150 | 151 | v = Is(Time(now).Between(now.Add(1*time.Hour), now.Add(2*time.Hour))) 152 | assert.False(t, v.Valid()) 153 | assert.Equal(t, 154 | "Value 0 must be between \""+now.Add(1*time.Hour).String()+"\" and \""+now.Add(2*time.Hour).String()+"\"", 155 | v.Errors()["value_0"].Messages()[0]) 156 | } 157 | 158 | func TestValidatorTimeInSliceValid(t *testing.T) { 159 | var v *Validation 160 | now := time.Now() 161 | timeSlice := []time.Time{now.Add(-1 * time.Hour), now, now.Add(1 * time.Hour)} 162 | 163 | v = Is(Time(now).InSlice(timeSlice)) 164 | assert.True(t, v.Valid()) 165 | assert.Empty(t, v.Errors()) 166 | } 167 | 168 | func TestValidatorTimeInSliceInvalid(t *testing.T) { 169 | var v *Validation 170 | now := time.Now() 171 | timeSlice := []time.Time{now.Add(-1 * time.Hour), now.Add(-30 * time.Minute), now.Add(-15 * time.Minute)} 172 | 173 | v = Is(Time(now).InSlice(timeSlice)) 174 | assert.False(t, v.Valid()) 175 | assert.NotEmpty(t, v.Errors()) 176 | assert.Equal(t, 177 | "Value 0 is not valid", 178 | v.Errors()["value_0"].Messages()[0]) 179 | } 180 | 181 | func TestValidatorTimePassingValid(t *testing.T) { 182 | 183 | var v *Validation 184 | 185 | now := time.Now() 186 | v = Is(Time(now).Passing(func(val time.Time) bool { 187 | return val.Equal(now) 188 | })) 189 | assert.True(t, v.Valid()) 190 | assert.Empty(t, v.Errors()) 191 | } 192 | 193 | func TestValidatorTimePassingInvalid(t *testing.T) { 194 | 195 | var v *Validation 196 | 197 | now := time.Now() 198 | v = Is(Time(now).Passing(func(val time.Time) bool { 199 | return val.Equal(now.Add(1 * time.Hour)) 200 | })) 201 | assert.False(t, v.Valid()) 202 | assert.Equal(t, 203 | "Value 0 is not valid", 204 | v.Errors()["value_0"].Messages()[0]) 205 | } 206 | 207 | func TestValidatorTimeZeroValid(t *testing.T) { 208 | 209 | var v *Validation 210 | 211 | zeroTime := time.Time{} 212 | 213 | v = Is(Time(zeroTime).Zero()) 214 | assert.True(t, v.Valid()) 215 | assert.Empty(t, v.Errors()) 216 | } 217 | 218 | func TestValidatorTimeZeroInvalid(t *testing.T) { 219 | 220 | var v *Validation 221 | 222 | nonZeroTime := time.Now() 223 | 224 | v = Is(Time(nonZeroTime).Zero()) 225 | assert.False(t, v.Valid()) 226 | assert.Equal(t, 227 | "Value 0 must be zero", 228 | v.Errors()["value_0"].Messages()[0]) 229 | } 230 | 231 | func TestValidatorTimeOrOperatorWithIs(t *testing.T) { 232 | var v *Validation 233 | 234 | var _true = true 235 | var _false = false 236 | 237 | timeZero := time.Time{} 238 | timeOne := time.Time{}.Add(time.Second) 239 | 240 | // Testing Or operation with two valid conditions 241 | v = Is(Time(timeOne).EqualTo(timeOne).Or().EqualTo(timeOne)) 242 | assert.True(t, v.Valid()) 243 | assert.Equal(t, _true || true, v.Valid()) 244 | assert.Empty(t, v.Errors()) 245 | 246 | // Testing Or operation with left invalid and right valid conditions 247 | v = Is(Time(timeOne).EqualTo(timeZero).Or().EqualTo(timeOne)) 248 | assert.True(t, v.Valid()) 249 | assert.Equal(t, false || true, v.Valid()) 250 | assert.Empty(t, v.Errors()) 251 | 252 | // Testing Or operation with left valid and right invalid conditions 253 | v = Is(Time(timeOne).EqualTo(timeOne).Or().EqualTo(timeZero)) 254 | assert.True(t, v.Valid()) 255 | assert.Equal(t, true || false, v.Valid()) 256 | assert.Empty(t, v.Errors()) 257 | 258 | // Testing Or operation with two invalid conditions 259 | v = Is(Time(timeOne).EqualTo(timeZero).Or().EqualTo(timeZero)) 260 | assert.False(t, v.Valid()) 261 | assert.Equal(t, _false || false, v.Valid()) 262 | assert.NotEmpty(t, v.Errors()) 263 | 264 | // Testing And operation (default when no Or() function is used) with left valid and right invalid conditions 265 | v = Is(Time(timeOne).EqualTo(timeOne).EqualTo(timeZero)) 266 | assert.False(t, v.Valid()) 267 | assert.Equal(t, true && false, v.Valid()) 268 | assert.NotEmpty(t, v.Errors()) 269 | 270 | // Testing combination of Not and Or operators with left valid and right invalid conditions 271 | v = Is(Time(timeOne).Not().EqualTo(timeZero).Or().EqualTo(timeZero)) 272 | assert.True(t, v.Valid()) 273 | assert.Equal(t, !false || false, v.Valid()) 274 | assert.Empty(t, v.Errors()) 275 | 276 | // Testing combination of Not and Or operators with left invalid and right valid conditions 277 | v = Is(Time(timeOne).Not().EqualTo(timeOne).Or().EqualTo(timeOne)) 278 | assert.True(t, v.Valid()) 279 | assert.Equal(t, !true || true, v.Valid()) 280 | assert.Empty(t, v.Errors()) 281 | 282 | // Testing multiple Or operations in sequence with the first condition being valid 283 | v = Is(Time(timeOne).EqualTo(timeOne).Or().EqualTo(timeZero).Or().EqualTo(timeZero)) 284 | assert.True(t, v.Valid()) 285 | assert.Equal(t, true || _false || false, v.Valid()) 286 | assert.Empty(t, v.Errors()) 287 | 288 | // Testing multiple Or operations in sequence with the last condition being valid 289 | v = Is(Time(timeOne).EqualTo(timeZero).Or().EqualTo(timeZero).Or().EqualTo(timeOne)) 290 | assert.True(t, v.Valid()) 291 | assert.Equal(t, _false || false || true, v.Valid()) 292 | assert.Empty(t, v.Errors()) 293 | 294 | // Testing invalid Or operation then valid And operation 295 | v = Is(Time(timeOne).EqualTo(timeZero).Or().EqualTo(timeOne).EqualTo(timeOne)) 296 | assert.True(t, v.Valid()) 297 | assert.Equal(t, false || _true && true, v.Valid()) 298 | assert.Empty(t, v.Errors()) 299 | 300 | // Testing valid Or operation then invalid And operation 301 | v = Is(Time(timeOne).EqualTo(timeZero).Or().EqualTo(timeOne).EqualTo(timeZero)) 302 | assert.False(t, v.Valid()) 303 | assert.Equal(t, false || true && false, v.Valid()) 304 | assert.NotEmpty(t, v.Errors()) 305 | 306 | // Testing valid And operation then invalid Or operation 307 | v = Is(Time(timeOne).EqualTo(timeOne).EqualTo(timeOne).Or().EqualTo(timeZero)) 308 | assert.True(t, v.Valid()) 309 | assert.Equal(t, _true && true || false, v.Valid()) 310 | assert.Empty(t, v.Errors()) 311 | 312 | // Testing invalid And operation then valid Or operation 313 | v = Is(Time(timeOne).EqualTo(timeOne).EqualTo(timeZero).Or().EqualTo(timeOne)) 314 | assert.True(t, v.Valid()) 315 | assert.Equal(t, true && false || true, v.Valid()) 316 | assert.Empty(t, v.Errors()) 317 | 318 | } 319 | 320 | func TestValidatorTimeOrOperatorWithCheck(t *testing.T) { 321 | var v *Validation 322 | 323 | // Check are Non-Short-circuited operations 324 | 325 | var _true = true 326 | var _false = false 327 | 328 | timeZero := time.Time{} 329 | timeOne := time.Time{}.Add(time.Second) 330 | 331 | // Testing Or operation with two valid conditions 332 | v = Check(Time(timeOne).EqualTo(timeOne).Or().EqualTo(timeOne)) 333 | assert.True(t, v.Valid()) 334 | assert.Equal(t, _true || true, v.Valid()) 335 | assert.Empty(t, v.Errors()) 336 | 337 | // Testing Or operation with left invalid and right valid conditions 338 | v = Check(Time(timeOne).EqualTo(timeZero).Or().EqualTo(timeOne)) 339 | assert.True(t, v.Valid()) 340 | assert.Equal(t, false || true, v.Valid()) 341 | assert.Empty(t, v.Errors()) 342 | 343 | // Testing Or operation with left valid and right invalid conditions 344 | v = Check(Time(timeOne).EqualTo(timeOne).Or().EqualTo(timeZero)) 345 | assert.True(t, v.Valid()) 346 | assert.Equal(t, true || false, v.Valid()) 347 | assert.Empty(t, v.Errors()) 348 | 349 | // Testing Or operation with two invalid conditions 350 | v = Check(Time(timeOne).EqualTo(timeZero).Or().EqualTo(timeZero)) 351 | assert.False(t, v.Valid()) 352 | assert.Equal(t, _false || false, v.Valid()) 353 | assert.NotEmpty(t, v.Errors()) 354 | 355 | // Testing And operation (default when no Or() function is used) with left valid and right invalid conditions 356 | v = Check(Time(timeOne).EqualTo(timeOne).EqualTo(timeZero)) 357 | assert.False(t, v.Valid()) 358 | assert.Equal(t, true && false, v.Valid()) 359 | assert.NotEmpty(t, v.Errors()) 360 | 361 | // Testing combination of Not and Or operators with left valid and right invalid conditions 362 | v = Check(Time(timeOne).Not().EqualTo(timeZero).Or().EqualTo(timeZero)) 363 | assert.True(t, v.Valid()) 364 | assert.Equal(t, !false || false, v.Valid()) 365 | assert.Empty(t, v.Errors()) 366 | 367 | // Testing combination of Not and Or operators with left invalid and right valid conditions 368 | v = Check(Time(timeOne).Not().EqualTo(timeOne).Or().EqualTo(timeOne)) 369 | assert.True(t, v.Valid()) 370 | assert.Equal(t, !true || true, v.Valid()) 371 | assert.Empty(t, v.Errors()) 372 | 373 | // Testing multiple Or operations in sequence with the first condition being valid 374 | v = Check(Time(timeOne).EqualTo(timeOne).Or().EqualTo(timeZero).Or().EqualTo(timeZero)) 375 | assert.True(t, v.Valid()) 376 | assert.Equal(t, true || _false || false, v.Valid()) 377 | assert.Empty(t, v.Errors()) 378 | 379 | // Testing multiple Or operations in sequence with the last condition being valid 380 | v = Check(Time(timeOne).EqualTo(timeZero).Or().EqualTo(timeZero).Or().EqualTo(timeOne)) 381 | assert.True(t, v.Valid()) 382 | assert.Equal(t, _false || false || true, v.Valid()) 383 | assert.Empty(t, v.Errors()) 384 | 385 | // Testing invalid Or operation then valid And operation 386 | v = Check(Time(timeOne).EqualTo(timeZero).Or().EqualTo(timeOne).EqualTo(timeOne)) 387 | assert.True(t, v.Valid()) 388 | assert.Equal(t, false || _true && true, v.Valid()) 389 | assert.Empty(t, v.Errors()) 390 | 391 | // Testing valid Or operation then invalid And operation 392 | v = Check(Time(timeOne).EqualTo(timeZero).Or().EqualTo(timeOne).EqualTo(timeZero)) 393 | assert.False(t, v.Valid()) 394 | assert.Equal(t, false || true && false, v.Valid()) 395 | assert.NotEmpty(t, v.Errors()) 396 | 397 | // Testing valid And operation then invalid Or operation 398 | v = Check(Time(timeOne).EqualTo(timeOne).EqualTo(timeOne).Or().EqualTo(timeZero)) 399 | assert.True(t, v.Valid()) 400 | assert.Equal(t, _true && true || false, v.Valid()) 401 | assert.Empty(t, v.Errors()) 402 | 403 | // Testing invalid And operation then valid Or operation 404 | v = Check(Time(timeOne).EqualTo(timeOne).EqualTo(timeZero).Or().EqualTo(timeOne)) 405 | assert.True(t, v.Valid()) 406 | assert.Equal(t, true && false || true, v.Valid()) 407 | assert.Empty(t, v.Errors()) 408 | } 409 | --------------------------------------------------------------------------------