diff --git a/go.mod b/go.mod index df454a9..4cd84dc 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,6 @@ module github.com/nobl9/govy go 1.22 require ( - github.com/nobl9/go-yaml v1.0.1 - github.com/nobl9/nobl9-go v0.83.0 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.9.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 @@ -12,11 +10,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fatih/color v1.17.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c7c0e9a..2821c6e 100644 --- a/go.sum +++ b/go.sum @@ -1,42 +1,13 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nobl9/go-yaml v1.0.1 h1:Aj1kSaYdRQTKlvS6ihvXzQJhCpoHhtf9nfA95zqWH4Q= -github.com/nobl9/go-yaml v1.0.1/go.mod h1:t7vCO8ctYdBweZxU5lUgxzAw31+ZcqJYeqRtrv+5RHI= -github.com/nobl9/nobl9-go v0.83.0 h1:2In5WdZh/GiChSIRLoxhMJf5DKdKCS492bURQJC7my8= -github.com/nobl9/nobl9-go v0.83.0/go.mod h1:Fx4m7n32rq+f0N+ezgx8+JbhGg89T6TikiGC/QlRE14= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/errors.go b/internal/errors.go new file mode 100644 index 0000000..82c6651 --- /dev/null +++ b/internal/errors.go @@ -0,0 +1,107 @@ +package internal + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" +) + +// RuleSetError is a container for transferring multiple errors reported by [govy.RuleSet]. +// It is intentionally not exported as it is only an intermediate stage before the +// aggregated errors are flattened. +type RuleSetError []error + +func (r RuleSetError) Error() string { + b := new(strings.Builder) + JoinErrors(b, r, "") + return b.String() +} + +// JoinErrors joins multiple errors into a single pretty-formmated string. +func JoinErrors[T error](b *strings.Builder, errs []T, indent string) { + for i, err := range errs { + buildErrorMessage(b, err.Error(), indent) + if i < len(errs)-1 { + b.WriteString("\n") + } + } +} + +const listPoint = "- " + +func buildErrorMessage(b *strings.Builder, errMsg, indent string) { + b.WriteString(indent) + if !strings.HasPrefix(errMsg, listPoint) { + b.WriteString(listPoint) + } + // Indent the whole error message. + errMsg = strings.ReplaceAll(errMsg, "\n", "\n"+indent) + b.WriteString(errMsg) +} + +var newLineReplacer = strings.NewReplacer("\n", "\\n", "\r", "\\r") + +// PropertyValueString returns the string representation of the given value. +// Structs, interfaces, maps and slices are converted to compacted JSON strings. +// It tries to improve readability by: +// - limiting the string to 100 characters +// - removing leading and trailing whitespaces +// - escaping newlines +// If value is a struct implementing [fmt.Stringer] String method will be used +// only if the struct does not contain any JSON tags. +func PropertyValueString(v interface{}) string { + if v == nil { + return "" + } + rv := reflect.ValueOf(v) + ft := reflect.Indirect(rv) + var s string + switch ft.Kind() { + case reflect.Interface, reflect.Map, reflect.Slice: + if reflect.ValueOf(v).IsZero() { + break + } + raw, _ := json.Marshal(v) + s = string(raw) + case reflect.Struct: + if reflect.ValueOf(v).IsZero() { + break + } + if stringer, ok := v.(fmt.Stringer); ok && !hasJSONTags(v, rv.Kind() == reflect.Pointer) { + s = stringer.String() + break + } + raw, _ := json.Marshal(v) + s = string(raw) + case reflect.Invalid: + return "" + default: + s = fmt.Sprint(ft.Interface()) + } + s = limitString(s, 100) + s = strings.TrimSpace(s) + s = newLineReplacer.Replace(s) + return s +} + +func hasJSONTags(v interface{}, isPointer bool) bool { + t := reflect.TypeOf(v) + if isPointer { + t = t.Elem() + } + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if _, hasTag := field.Tag.Lookup("json"); hasTag { + return true + } + } + return false +} + +func limitString(s string, limit int) string { + if len(s) > limit { + return s[:limit] + "..." + } + return s +} diff --git a/internal/helpers.go b/internal/helpers.go new file mode 100644 index 0000000..b58fe3c --- /dev/null +++ b/internal/helpers.go @@ -0,0 +1,13 @@ +package internal + +import "reflect" + +const RequiredErrorMessage = "property is required but was empty" + +const RequiredErrorCodeString = "required" + +// IsEmptyFunc verifies if the value is zero value of its type. +func IsEmptyFunc(v interface{}) bool { + rv := reflect.ValueOf(v) + return rv.Kind() == 0 || rv.IsZero() +} diff --git a/pkg/govy/README.md b/pkg/govy/README.md index 7a0643a..edabe19 100644 --- a/pkg/govy/README.md +++ b/pkg/govy/README.md @@ -1,6 +1,6 @@ # Validation -Package validation implements a functional API for consistent, +package govy implements a functional API for consistent, type safe validation. It puts heavy focus on end user errors readability, providing means of crafting clear and information-rich error messages. diff --git a/pkg/govy/cascade_mode.go b/pkg/govy/cascade_mode.go index 7d306df..a299576 100644 --- a/pkg/govy/cascade_mode.go +++ b/pkg/govy/cascade_mode.go @@ -1,4 +1,4 @@ -package validation +package govy // CascadeMode defines how validation should behave when an error is encountered. type CascadeMode uint diff --git a/pkg/govy/doc.go b/pkg/govy/doc.go index 9f97f89..aa3d17f 100644 --- a/pkg/govy/doc.go +++ b/pkg/govy/doc.go @@ -1,2 +1,2 @@ -// Package validation implements a functional API for consistent struct level validation. -package validation +// package govy implements a functional API for consistent struct level validation. +package govy diff --git a/pkg/govy/error_code.go b/pkg/govy/error_code.go new file mode 100644 index 0000000..560a87b --- /dev/null +++ b/pkg/govy/error_code.go @@ -0,0 +1,9 @@ +package govy + +// ErrorCode is a unique string that represents a specific [RuleError]. +// It can be used to precisely identify the error without inspecting its message. +type ErrorCode = string + +const ( + ErrorCodeTransform ErrorCode = "transform" +) diff --git a/pkg/govy/error_codes.go b/pkg/govy/error_codes.go deleted file mode 100644 index 3cbc5f6..0000000 --- a/pkg/govy/error_codes.go +++ /dev/null @@ -1,40 +0,0 @@ -package validation - -type ErrorCode = string - -const ( - ErrorCodeRequired ErrorCode = "required" - ErrorCodeTransform ErrorCode = "transform" - ErrorCodeForbidden ErrorCode = "forbidden" - ErrorCodeEqualTo ErrorCode = "equal_to" - ErrorCodeNotEqualTo ErrorCode = "not_equal_to" - ErrorCodeGreaterThan ErrorCode = "greater_than" - ErrorCodeGreaterThanOrEqualTo ErrorCode = "greater_than_or_equal_to" - ErrorCodeLessThan ErrorCode = "less_than" - ErrorCodeLessThanOrEqualTo ErrorCode = "less_than_or_equal_to" - ErrorCodeStringNotEmpty ErrorCode = "string_not_empty" - ErrorCodeStringMatchRegexp ErrorCode = "string_match_regexp" - ErrorCodeStringDenyRegexp ErrorCode = "string_deny_regexp" - ErrorCodeStringDescription ErrorCode = "string_description" - ErrorCodeStringIsDNSSubdomain ErrorCode = "string_is_dns_subdomain" - ErrorCodeStringASCII ErrorCode = "string_ascii" - ErrorCodeStringURL ErrorCode = "string_url" - ErrorCodeStringUUID ErrorCode = "string_uuid" - ErrorCodeStringJSON ErrorCode = "string_json" - ErrorCodeStringContains ErrorCode = "string_contains" - ErrorCodeStringStartsWith ErrorCode = "string_starts_with" - ErrorCodeStringLength ErrorCode = "string_length" - ErrorCodeStringMinLength ErrorCode = "string_min_length" - ErrorCodeStringMaxLength ErrorCode = "string_max_length" - ErrorCodeSliceLength ErrorCode = "slice_length" - ErrorCodeSliceMinLength ErrorCode = "slice_min_length" - ErrorCodeSliceMaxLength ErrorCode = "slice_max_length" - ErrorCodeMapLength ErrorCode = "map_length" - ErrorCodeMapMinLength ErrorCode = "map_min_length" - ErrorCodeMapMaxLength ErrorCode = "map_max_length" - ErrorCodeOneOf ErrorCode = "one_of" - ErrorCodeMutuallyExclusive ErrorCode = "mutually_exclusive" - ErrorCodeSliceUnique ErrorCode = "slice_unique" - ErrorCodeURL ErrorCode = "url" - ErrorCodeDurationPrecision ErrorCode = "duration_precision" -) diff --git a/pkg/govy/errors.go b/pkg/govy/errors.go index ac6519a..72f86a9 100644 --- a/pkg/govy/errors.go +++ b/pkg/govy/errors.go @@ -1,11 +1,17 @@ -package validation +package govy import ( - "encoding/json" "fmt" - "reflect" "sort" "strings" + + "github.com/nobl9/govy/internal" +) + +const ( + ErrorCodeSeparator = ":" + propertyNameSeparator = "." + hiddenValue = "[hidden]" ) func NewValidatorError(errs PropertyErrors) *ValidatorError { @@ -30,7 +36,7 @@ func (e *ValidatorError) Error() string { b.WriteString(e.Name) } b.WriteString(" has failed for the following properties:\n") - JoinErrors(&b, e.Errors, strings.Repeat(" ", 2)) + internal.JoinErrors(&b, e.Errors, strings.Repeat(" ", 2)) return b.String() } @@ -38,7 +44,7 @@ type PropertyErrors []*PropertyError func (e PropertyErrors) Error() string { b := strings.Builder{} - JoinErrors(&b, e, "") + internal.JoinErrors(&b, e, "") return b.String() } @@ -91,7 +97,7 @@ outer: func NewPropertyError(propertyName string, propertyValue interface{}, errs ...error) *PropertyError { return &PropertyError{ PropertyName: propertyName, - PropertyValue: propertyValueString(propertyValue), + PropertyValue: internal.PropertyValueString(propertyValue), Errors: unpackRuleErrors(errs, make([]*RuleError, 0, len(errs))), } } @@ -122,7 +128,7 @@ func (e *PropertyError) Error() string { b.WriteString(":\n") indent = strings.Repeat(" ", 2) } - JoinErrors(b, e.Errors, indent) + internal.JoinErrors(b, e.Errors, indent) return b.String() } @@ -133,11 +139,6 @@ func (e *PropertyError) Equal(cmp *PropertyError) bool { e.IsSliceElementError == cmp.IsSliceElementError } -const ( - propertyNameSeparator = "." - hiddenValue = "[hidden]" -) - func (e *PropertyError) PrependPropertyName(name string) *PropertyError { sep := propertyNameSeparator if e.IsSliceElementError && strings.HasPrefix(e.PropertyName, "[") { @@ -149,7 +150,7 @@ func (e *PropertyError) PrependPropertyName(name string) *PropertyError { // HideValue hides the property value from [PropertyError.Error] and also hides it from. func (e *PropertyError) HideValue() *PropertyError { - sv := propertyValueString(e.PropertyValue) + sv := internal.PropertyValueString(e.PropertyValue) e.PropertyValue = "" for _, err := range e.Errors { _ = err.HideValue(sv) @@ -176,8 +177,6 @@ func (r *RuleError) Error() string { return r.Message } -const ErrorCodeSeparator = ":" - // AddCode extends the [RuleError] with the given error code. // Codes are prepended, the last code in chain is always the first one set. // Example: @@ -241,109 +240,11 @@ func HasErrorCode(err error, code ErrorCode) bool { return false } -var newLineReplacer = strings.NewReplacer("\n", "\\n", "\r", "\\r") - -// propertyValueString returns the string representation of the given value. -// Structs, interfaces, maps and slices are converted to compacted JSON strings. -// It tries to improve readability by: -// - limiting the string to 100 characters -// - removing leading and trailing whitespaces -// - escaping newlines -// If value is a struct implementing [fmt.Stringer] String method will be used -// only if the struct does not contain any JSON tags. -func propertyValueString(v interface{}) string { - if v == nil { - return "" - } - rv := reflect.ValueOf(v) - ft := reflect.Indirect(rv) - var s string - switch ft.Kind() { - case reflect.Interface, reflect.Map, reflect.Slice: - if reflect.ValueOf(v).IsZero() { - break - } - raw, _ := json.Marshal(v) - s = string(raw) - case reflect.Struct: - if reflect.ValueOf(v).IsZero() { - break - } - if stringer, ok := v.(fmt.Stringer); ok && !hasJSONTags(v, rv.Kind() == reflect.Pointer) { - s = stringer.String() - break - } - raw, _ := json.Marshal(v) - s = string(raw) - case reflect.Invalid: - return "" - default: - s = fmt.Sprint(ft.Interface()) - } - s = limitString(s, 100) - s = strings.TrimSpace(s) - s = newLineReplacer.Replace(s) - return s -} - -func hasJSONTags(v interface{}, isPointer bool) bool { - t := reflect.TypeOf(v) - if isPointer { - t = t.Elem() - } - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - if _, hasTag := field.Tag.Lookup("json"); hasTag { - return true - } - } - return false -} - -// ruleSetError is a container for transferring multiple errors reported by [RuleSet]. -// It is intentionally not exported as it is only an intermediate stage before the -// aggregated errors are flattened. -type ruleSetError []error - -func (r ruleSetError) Error() string { - b := new(strings.Builder) - JoinErrors(b, r, "") - return b.String() -} - -func JoinErrors[T error](b *strings.Builder, errs []T, indent string) { - for i, err := range errs { - buildErrorMessage(b, err.Error(), indent) - if i < len(errs)-1 { - b.WriteString("\n") - } - } -} - -const listPoint = "- " - -func buildErrorMessage(b *strings.Builder, errMsg, indent string) { - b.WriteString(indent) - if !strings.HasPrefix(errMsg, listPoint) { - b.WriteString(listPoint) - } - // Indent the whole error message. - errMsg = strings.ReplaceAll(errMsg, "\n", "\n"+indent) - b.WriteString(errMsg) -} - -func limitString(s string, limit int) string { - if len(s) > limit { - return s[:limit] + "..." - } - return s -} - // unpackRuleErrors unpacks error messages recursively scanning [ruleSetError] if it is detected. func unpackRuleErrors(errs []error, ruleErrors []*RuleError) []*RuleError { for _, err := range errs { switch v := err.(type) { - case ruleSetError: + case internal.RuleSetError: ruleErrors = unpackRuleErrors(v, ruleErrors) case *RuleError: ruleErrors = append(ruleErrors, v) @@ -353,10 +254,3 @@ func unpackRuleErrors(errs []error, ruleErrors []*RuleError) []*RuleError { } return ruleErrors } - -func NewRequiredError() *RuleError { - return NewRuleError( - "property is required but was empty", - ErrorCodeRequired, - ) -} diff --git a/pkg/govy/errors_test.go b/pkg/govy/errors_test.go index 04c9492..ae87f01 100644 --- a/pkg/govy/errors_test.go +++ b/pkg/govy/errors_test.go @@ -1,4 +1,4 @@ -package validation +package govy_test import ( "embed" @@ -9,48 +9,51 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/nobl9/govy/internal" + "github.com/nobl9/govy/pkg/govy" ) //go:embed test_data var errorsTestData embed.FS func TestValidatorError(t *testing.T) { - for name, err := range map[string]*ValidatorError{ + for name, err := range map[string]*govy.ValidatorError{ "no_name": { - Errors: PropertyErrors{ + Errors: govy.PropertyErrors{ { PropertyName: "this", PropertyValue: "123", - Errors: []*RuleError{{Message: "this is an error"}}, + Errors: []*govy.RuleError{{Message: "this is an error"}}, }, { PropertyName: "that", - Errors: []*RuleError{{Message: "that is an error"}}, + Errors: []*govy.RuleError{{Message: "that is an error"}}, }, }, }, "with_name": { Name: "Teacher", - Errors: PropertyErrors{ + Errors: govy.PropertyErrors{ { PropertyName: "this", PropertyValue: "123", - Errors: []*RuleError{{Message: "this is an error"}}, + Errors: []*govy.RuleError{{Message: "this is an error"}}, }, { PropertyName: "that", - Errors: []*RuleError{{Message: "that is an error"}}, + Errors: []*govy.RuleError{{Message: "that is an error"}}, }, }, }, "prop_no_name": { - Errors: PropertyErrors{ + Errors: govy.PropertyErrors{ { - Errors: []*RuleError{{Message: "no name"}}, + Errors: []*govy.RuleError{{Message: "no name"}}, }, { PropertyName: "that", - Errors: []*RuleError{{Message: "that is an error"}}, + Errors: []*govy.RuleError{{Message: "that is an error"}}, }, }, }, @@ -63,18 +66,18 @@ func TestValidatorError(t *testing.T) { func TestNewPropertyError(t *testing.T) { t.Run("string value", func(t *testing.T) { - err := NewPropertyError("name", "value", - &RuleError{Message: "top", Code: "1"}, - ruleSetError{ - &RuleError{Message: "rule1", Code: "2"}, - &RuleError{Message: "rule2", Code: "3"}, + err := govy.NewPropertyError("name", "value", + &govy.RuleError{Message: "top", Code: "1"}, + internal.RuleSetError{ + &govy.RuleError{Message: "rule1", Code: "2"}, + &govy.RuleError{Message: "rule2", Code: "3"}, }, - &RuleError{Message: "top", Code: "4"}, + &govy.RuleError{Message: "top", Code: "4"}, ) - assert.Equal(t, &PropertyError{ + assert.Equal(t, &govy.PropertyError{ PropertyName: "name", PropertyValue: "value", - Errors: []*RuleError{ + Errors: []*govy.RuleError{ {Message: "top", Code: "1"}, {Message: "rule1", Code: "2"}, {Message: "rule2", Code: "3"}, @@ -143,14 +146,14 @@ my-table WHERE value = "abc" }, } { t.Run(name, func(t *testing.T) { - err := NewPropertyError( + err := govy.NewPropertyError( "name", test.InputValue, - &RuleError{Message: "msg"}) - assert.Equal(t, &PropertyError{ + &govy.RuleError{Message: "msg"}) + assert.Equal(t, &govy.PropertyError{ PropertyName: "name", PropertyValue: test.ExpectedValue, - Errors: []*RuleError{{Message: "msg"}}, + Errors: []*govy.RuleError{{Message: "msg"}}, }, err) }) } @@ -190,10 +193,10 @@ func TestPropertyError(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - err := &PropertyError{ + err := &govy.PropertyError{ PropertyName: "metadata.name", - PropertyValue: propertyValueString(value), - Errors: []*RuleError{ + PropertyValue: internal.PropertyValueString(value), + Errors: []*govy.RuleError{ {Message: "what a shame this happened"}, {Message: "this is outrageous..."}, {Message: "here's another error"}, @@ -203,8 +206,8 @@ func TestPropertyError(t *testing.T) { }) } t.Run("no name provided", func(t *testing.T) { - err := &PropertyError{ - Errors: []*RuleError{ + err := &govy.PropertyError{ + Errors: []*govy.RuleError{ {Message: "what a shame this happened"}, {Message: "this is outrageous..."}, {Message: "here's another error"}, @@ -216,24 +219,24 @@ func TestPropertyError(t *testing.T) { func TestPropertyError_PrependPropertyName(t *testing.T) { for _, test := range []struct { - PropertyError *PropertyError + PropertyError *govy.PropertyError InputName string ExpectedName string }{ { - PropertyError: &PropertyError{}, + PropertyError: &govy.PropertyError{}, }, { - PropertyError: &PropertyError{PropertyName: "test"}, + PropertyError: &govy.PropertyError{PropertyName: "test"}, ExpectedName: "test", }, { - PropertyError: &PropertyError{}, + PropertyError: &govy.PropertyError{}, InputName: "new", ExpectedName: "new", }, { - PropertyError: &PropertyError{PropertyName: "original"}, + PropertyError: &govy.PropertyError{PropertyName: "original"}, InputName: "added", ExpectedName: "added.original", }, @@ -244,33 +247,33 @@ func TestPropertyError_PrependPropertyName(t *testing.T) { func TestRuleError(t *testing.T) { for _, test := range []struct { - RuleError *RuleError - InputCode ErrorCode - ExpectedCode ErrorCode + RuleError *govy.RuleError + InputCode govy.ErrorCode + ExpectedCode govy.ErrorCode }{ { - RuleError: NewRuleError("test"), + RuleError: govy.NewRuleError("test"), }, { - RuleError: NewRuleError("test", "code"), + RuleError: govy.NewRuleError("test", "code"), ExpectedCode: "code", }, { - RuleError: NewRuleError("test"), + RuleError: govy.NewRuleError("test"), InputCode: "code", ExpectedCode: "code", }, { - RuleError: NewRuleError("test", "original"), + RuleError: govy.NewRuleError("test", "original"), InputCode: "added", ExpectedCode: "added:original", }, { - RuleError: NewRuleError("test", "code-1", "code-2"), + RuleError: govy.NewRuleError("test", "code-1", "code-2"), ExpectedCode: "code-2:code-1", }, { - RuleError: NewRuleError("test", "original-1", "original-2"), + RuleError: govy.NewRuleError("test", "original-1", "original-2"), InputCode: "added", ExpectedCode: "added:original-2:original-1", }, @@ -282,7 +285,7 @@ func TestRuleError(t *testing.T) { } func TestMultiRuleError(t *testing.T) { - err := ruleSetError{ + err := internal.RuleSetError{ errors.New("this is just a test!"), errors.New("another error..."), errors.New("that is just fatal."), @@ -293,7 +296,7 @@ func TestMultiRuleError(t *testing.T) { func TestHasErrorCode(t *testing.T) { for _, test := range []struct { Error error - Code ErrorCode + Code govy.ErrorCode HasErrorCode bool }{ { @@ -307,32 +310,34 @@ func TestHasErrorCode(t *testing.T) { HasErrorCode: false, }, { - Error: &RuleError{Code: "another"}, + Error: &govy.RuleError{Code: "another"}, Code: "code", HasErrorCode: false, }, { - Error: &RuleError{Code: "another:this"}, + Error: &govy.RuleError{Code: "another:this"}, Code: "code", HasErrorCode: false, }, { - Error: &RuleError{Code: "another:code:this"}, + Error: &govy.RuleError{Code: "another:code:this"}, Code: "code", HasErrorCode: true, }, { - Error: &PropertyError{Errors: []*RuleError{{Code: "another"}}}, + Error: &govy.PropertyError{Errors: []*govy.RuleError{{Code: "another"}}}, Code: "code", HasErrorCode: false, }, { - Error: &PropertyError{Errors: []*RuleError{{Code: "this:another"}, {}, {Code: "another:code:this"}}}, + Error: &govy.PropertyError{ + Errors: []*govy.RuleError{{Code: "this:another"}, {}, {Code: "another:code:this"}}, + }, Code: "code", HasErrorCode: true, }, } { - assert.Equal(t, test.HasErrorCode, HasErrorCode(test.Error, test.Code)) + assert.Equal(t, test.HasErrorCode, govy.HasErrorCode(test.Error, test.Code)) } } diff --git a/pkg/govy/example_test.go b/pkg/govy/example_test.go index 8bcfbe1..edfa911 100644 --- a/pkg/govy/example_test.go +++ b/pkg/govy/example_test.go @@ -1,15 +1,15 @@ // nolint: lll -package validation_test +package govy_test import ( + "encoding/json" "fmt" "os" "regexp" "time" - "github.com/nobl9/go-yaml" - - validation "github.com/nobl9/govy/pkg/govy" + "github.com/nobl9/govy/pkg/govy" + "github.com/nobl9/govy/pkg/rules" ) type Teacher struct { @@ -39,9 +39,9 @@ const year = 24 * 365 * time.Hour // Let's define simple [PropertyRules] for [Teacher.Name]. // For now, it will be always failing. func ExampleNew() { - v := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). - Rules(validation.NewSingleRule(func(name string) error { return fmt.Errorf("always fails") })), + v := govy.New( + govy.For(func(t Teacher) string { return t.Name }). + Rules(govy.NewSingleRule(func(name string) error { return fmt.Errorf("always fails") })), ) err := v.Validate(Teacher{}) @@ -57,9 +57,9 @@ func ExampleNew() { // To associate [Validator] with an entity name use [Validator.WithName] function. // When any of the rules fails, the error will contain the entity name you've provided. func ExampleValidator_WithName() { - v := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). - Rules(validation.NewSingleRule(func(name string) error { return fmt.Errorf("always fails") })), + v := govy.New( + govy.For(func(t Teacher) string { return t.Name }). + Rules(govy.NewSingleRule(func(name string) error { return fmt.Errorf("always fails") })), ).WithName("Teacher") err := v.Validate(Teacher{}) @@ -86,9 +86,9 @@ func ExampleValidator_WithName() { // tries to follow immutability principle. Calling any function on [Validator] // will not change its previous declaration (unless you assign it back to 'v'). func ExampleValidatorError_WithName() { - v := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). - Rules(validation.NewSingleRule(func(name string) error { return fmt.Errorf("always fails") })), + v := govy.New( + govy.For(func(t Teacher) string { return t.Name }). + Rules(govy.NewSingleRule(func(name string) error { return fmt.Errorf("always fails") })), ).WithName("Teacher") err := v.Validate(Teacher{}) @@ -106,9 +106,9 @@ func ExampleValidatorError_WithName() { // In this example, validation for [Teacher] instance will only be evaluated // if the [Age] property is less than 50 years. func ExampleValidator_When() { - v := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). - Rules(validation.NewSingleRule(func(name string) error { return fmt.Errorf("always fails") })), + v := govy.New( + govy.For(func(t Teacher) string { return t.Name }). + Rules(govy.NewSingleRule(func(name string) error { return fmt.Errorf("always fails") })), ). When(func(t Teacher) bool { return t.Age < (50 * year) }) @@ -150,10 +150,10 @@ func ExampleValidator_When() { // Validation package comes with a number of predefined [Rule], we'll use // [EqualTo] which accepts a single argument, value to compare with. func ExamplePropertyRules_WithName() { - v := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). + v := govy.New( + govy.For(func(t Teacher) string { return t.Name }). WithName("name"). - Rules(validation.EqualTo("Tom")), + Rules(rules.EqualTo("Tom")), ).WithName("Teacher") teacher := Teacher{ @@ -193,10 +193,10 @@ func ExamplePropertyRules_WithName() { // Not everyone has to have a middle name, that's why we've defined this field // as a pointer to string, rather than a string itself. func ExampleForPointer() { - v := validation.New[Teacher]( - validation.ForPointer(func(t Teacher) *string { return t.MiddleName }). + v := govy.New( + govy.ForPointer(func(t Teacher) *string { return t.MiddleName }). WithName("middleName"). - Rules(validation.StringMaxLength(5)), + Rules(rules.StringMaxLength(5)), ).WithName("Teacher") middleName := "Thaddeus" @@ -221,20 +221,20 @@ func ExampleForPointer() { // it will skip validation of the property if the pointer is nil. // To enforce a value is set for pointer use [PropertyRules.Required]. // -// You may ask yourself why not just use [validation.Required] rule instead? +// You may ask yourself why not just use [govy.Required] rule instead? // If we were to do that, we'd be forced to operate on pointer in all of our rules. // Other than checking if the pointer is nil, there aren't any rules which would // benefit from working on the pointer instead of the underlying value. // // If you want to also make sure the underlying value is filled, -// i.e. it's not a zero value, you can also use [validation.Required] rule +// i.e. it's not a zero value, you can also use [govy.Required] rule // on top of [PropertyRules.Required]. // // [PropertyRules.Required] when used with [For] constructor, will ensure // the property does not contain a zero value. // // NOTE: [PropertyRules.Required] is introducing a short circuit. -// If the assertion fails, validation will stop and return [validation.ErrorCodeRequired]. +// If the assertion fails, validation will stop and return [govy.ErrorCodeRequired]. // None of the rules you've defined would be evaluated. // // NOTE: Placement of [PropertyRules.Required] does not matter, @@ -242,16 +242,16 @@ func ExampleForPointer() { // However, we recommend you always place it below [PropertyRules.WithName] // to make your rules more readable. func ExamplePropertyRules_Required() { - alwaysFailingRule := validation.NewSingleRule(func(string) error { + alwaysFailingRule := govy.NewSingleRule(func(string) error { return fmt.Errorf("always fails") }) - v := validation.New[Teacher]( - validation.ForPointer(func(t Teacher) *string { return t.MiddleName }). + v := govy.New( + govy.ForPointer(func(t Teacher) *string { return t.MiddleName }). WithName("middleName"). Required(). Rules(alwaysFailingRule), - validation.For(func(t Teacher) string { return t.Name }). + govy.For(func(t Teacher) string { return t.Name }). WithName("name"). Required(). Rules(alwaysFailingRule), @@ -284,16 +284,16 @@ func ExamplePropertyRules_Required() { // NOTE: [PropertyRules.OmitEmpty] will have no effect on pointers handled // by [ForPointer], as they already behave in the same way. func ExamplePropertyRules_OmitEmpty() { - alwaysFailingRule := validation.NewSingleRule(func(string) error { + alwaysFailingRule := govy.NewSingleRule(func(string) error { return fmt.Errorf("always fails") }) - v := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). + v := govy.New( + govy.For(func(t Teacher) string { return t.Name }). WithName("name"). OmitEmpty(). Rules(alwaysFailingRule), - validation.ForPointer(func(t Teacher) *string { return t.MiddleName }). + govy.ForPointer(func(t Teacher) *string { return t.MiddleName }). WithName("middleName"). Rules(alwaysFailingRule), ).WithName("Teacher") @@ -321,12 +321,12 @@ func ExamplePropertyRules_OmitEmpty() { // You can provide your own rules using [NewSingleRule] constructor. // It returns new [SingleRule] instance which wraps your validation function. func ExampleGetSelf() { - customRule := validation.NewSingleRule(func(v Teacher) error { + customRule := govy.NewSingleRule(func(v Teacher) error { return fmt.Errorf("now I have access to the whole teacher") }) - v := validation.New[Teacher]( - validation.For(validation.GetSelf[Teacher]()). + v := govy.New( + govy.For(govy.GetSelf[Teacher]()). Rules(customRule), ).WithName("Teacher") @@ -349,10 +349,10 @@ func ExampleGetSelf() { // This allows you to extend existing rules by adding your use case context. // Let's give a regex validation some more clarity. func ExampleSingleRule_WithDetails() { - v := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). + v := govy.New( + govy.For(func(t Teacher) string { return t.Name }). WithName("name"). - Rules(validation.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")). + Rules(rules.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")). WithDetails("Teacher can be either Tom or Jerry :)")), ).WithName("Teacher") @@ -380,10 +380,10 @@ func ExampleSingleRule_WithDetails() { // Predefined rules have [ErrorCode] already associated with them. // To view the list of predefined [ErrorCode] checkout error_codes.go file. func ExampleSingleRule_WithErrorCode() { - v := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). + v := govy.New( + govy.For(func(t Teacher) string { return t.Name }). WithName("name"). - Rules(validation.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")). + Rules(rules.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")). WithDetails("Teacher can be either Tom or Jerry :)"). WithErrorCode("custom_code")), ).WithName("Teacher") @@ -413,16 +413,16 @@ func ExampleSingleRule_WithErrorCode() { // Note that validation package uses similar syntax to wrapped errors in Go; // a ':' delimiter is used to chain error codes together. func ExampleRuleSet() { - teacherNameRule := validation.NewRuleSet[string]( - validation.StringLength(1, 5), - validation.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")). + teacherNameRule := govy.NewRuleSet( + rules.StringLength(1, 5), + rules.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")). WithDetails("Teacher can be either Tom or Jerry :)"), ). WithErrorCode("teacher_name"). WithDetails("I will add that to both rules!") - v := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). + v := govy.New( + govy.For(func(t Teacher) string { return t.Name }). WithName("name"). Rules(teacherNameRule), ).WithName("Teacher") @@ -450,21 +450,21 @@ func ExampleRuleSet() { // - string does not match regular expression: '^(Tom|Jerry)$'; Teacher can be either Tom or Jerry :); I will add that to both rules! } -// To inspect if an error contains a given [validation.ErrorCode], use [HasErrorCode] function. +// To inspect if an error contains a given [govy.ErrorCode], use [HasErrorCode] function. // This function will also return true if the expected [ErrorCode] // is part of a chain of wrapped error codes. // In this example we're dealing with two error code chains: // - 'teacher_name:string_length' // - 'teacher_name:string_match_regexp' func ExampleHasErrorCode() { - teacherNameRule := validation.NewRuleSet[string]( - validation.StringLength(1, 5), - validation.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")), + teacherNameRule := govy.NewRuleSet( + rules.StringLength(1, 5), + rules.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")), ). WithErrorCode("teacher_name") - v := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). + v := govy.New( + govy.For(func(t Teacher) string { return t.Name }). WithName("name"). Rules(teacherNameRule), ).WithName("Teacher") @@ -476,12 +476,12 @@ func ExampleHasErrorCode() { err := v.Validate(teacher) if err != nil { - for _, code := range []validation.ErrorCode{ + for _, code := range []govy.ErrorCode{ "teacher_name", "string_length", "string_match_regexp", } { - if validation.HasErrorCode(err, code) { + if govy.HasErrorCode(err, code) { fmt.Println("Has error code:", code) } } @@ -500,15 +500,15 @@ func ExampleHasErrorCode() { // Note that you can still use [ErrorCode] and pass [RuleError] to the constructor. // You can pass any number of [RuleError]. func ExampleNewPropertyError() { - v := validation.New[Teacher]( - validation.For(validation.GetSelf[Teacher]()). - Rules(validation.NewSingleRule(func(t Teacher) error { + v := govy.New( + govy.For(govy.GetSelf[Teacher]()). + Rules(govy.NewSingleRule(func(t Teacher) error { if t.Name == "Jake" { - return validation.NewPropertyError( + return govy.NewPropertyError( "name", t.Name, - validation.NewRuleError("name cannot be Jake", "error_code_jake"), - validation.NewRuleError("you can pass me too!")) + govy.NewRuleError("name cannot be Jake", "error_code_jake"), + govy.NewRuleError("you can pass me too!")) } return nil })), @@ -546,16 +546,16 @@ func ExampleNewPropertyError() { // Notice how the nested property path is automatically built for you, // each segment separated by a dot. func ExamplePropertyRules_Include() { - universityValidation := validation.New[University]( - validation.For(func(u University) string { return u.Address }). + universityValidation := govy.New( + govy.For(func(u University) string { return u.Address }). WithName("address"). Required(), ) - teacherValidation := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). + teacherValidation := govy.New( + govy.For(func(t Teacher) string { return t.Name }). WithName("name"). - Rules(validation.EqualTo("Tom")), - validation.For(func(t Teacher) University { return t.University }). + Rules(rules.EqualTo("Tom")), + govy.For(func(t Teacher) University { return t.University }). WithName("university"). Include(universityValidation), ).WithName("Teacher") @@ -604,17 +604,17 @@ func ExamplePropertyRules_Include() { // Notice that property path for slices has the following format: // []. func ExampleForSlice() { - studentValidator := validation.New[Student]( - validation.For(func(s Student) string { return s.Index }). + studentValidator := govy.New( + govy.For(func(s Student) string { return s.Index }). WithName("index"). - Rules(validation.StringLength(9, 9)), + Rules(rules.StringLength(9, 9)), ) - teacherValidator := validation.New[Teacher]( - validation.ForSlice(func(t Teacher) []Student { return t.Students }). + teacherValidator := govy.New( + govy.ForSlice(func(t Teacher) []Student { return t.Students }). WithName("students"). Rules( - validation.SliceMaxLength[[]Student](2), - validation.SliceUnique(func(v Student) string { return v.Index })). + rules.SliceMaxLength[[]Student](2), + rules.SliceUnique(func(v Student) string { return v.Index })). IncludeForEach(studentValidator), ).When(func(t Teacher) bool { return t.Age < 50 }) @@ -673,24 +673,24 @@ func ExampleForSlice() { // Notice that property path for maps has the following format: // .. func ExampleForMap() { - teacherValidator := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). + teacherValidator := govy.New( + govy.For(func(t Teacher) string { return t.Name }). WithName("name"). - Rules(validation.NotEqualTo("Eve")), + Rules(rules.NotEqualTo("Eve")), ) - tutoringValidator := validation.New[Tutoring]( - validation.ForMap(func(t Tutoring) map[string]Teacher { return t.StudentIndexToTeacher }). + tutoringValidator := govy.New( + govy.ForMap(func(t Tutoring) map[string]Teacher { return t.StudentIndexToTeacher }). WithName("students"). Rules( - validation.MapMaxLength[map[string]Teacher](2), + rules.MapMaxLength[map[string]Teacher](2), ). RulesForKeys( - validation.StringLength(9, 9), + rules.StringLength(9, 9), ). IncludeForValues(teacherValidator). - RulesForItems(validation.NewSingleRule(func(v validation.MapItem[string, Teacher]) error { + RulesForItems(govy.NewSingleRule(func(v govy.MapItem[string, Teacher]) error { if v.Key == "918230013" && v.Value.Name == "Joan" { - return validation.NewRuleError( + return govy.NewRuleError( "Joan cannot be a teacher for student with index 918230013", "joan_teacher", ) @@ -730,11 +730,11 @@ func ExampleForMap() { // // It's recommended to define [PropertyRules.When] before [PropertyRules.Rules] declaration. func ExamplePropertyRules_When() { - v := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). + v := govy.New( + govy.For(func(t Teacher) string { return t.Name }). WithName("name"). When(func(t Teacher) bool { return t.Name == "Jerry" }). - Rules(validation.NotEqualTo("Jerry")), + Rules(rules.NotEqualTo("Jerry")), ).WithName("Teacher") for _, name := range []string{"Tom", "Jerry", "Mickey"} { @@ -755,15 +755,15 @@ func ExamplePropertyRules_When() { // Use [CascadeModeStop] to stop validation after the first error. // If you wish to revert to the default behavior, use [CascadeModeContinue]. func ExamplePropertyRules_Cascade() { - alwaysFailingRule := validation.NewSingleRule(func(string) error { + alwaysFailingRule := govy.NewSingleRule(func(string) error { return fmt.Errorf("always fails") }) - v := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). + v := govy.New( + govy.For(func(t Teacher) string { return t.Name }). WithName("name"). - Cascade(validation.CascadeModeStop). - Rules(validation.NotEqualTo("Jerry")). + Cascade(govy.CascadeModeStop). + Rules(rules.NotEqualTo("Jerry")). Rules(alwaysFailingRule), ).WithName("Teacher") @@ -786,30 +786,30 @@ func ExamplePropertyRules_Cascade() { // Bringing it all (mostly) together, let's create a fully fledged [Validator] for [Teacher]. func ExampleValidator() { - universityValidation := validation.New[University]( - validation.For(func(u University) string { return u.Address }). + universityValidation := govy.New( + govy.For(func(u University) string { return u.Address }). WithName("address"). Required(), ) - studentValidator := validation.New[Student]( - validation.For(func(s Student) string { return s.Index }). + studentValidator := govy.New( + govy.For(func(s Student) string { return s.Index }). WithName("index"). - Rules(validation.StringLength(9, 9)), + Rules(rules.StringLength(9, 9)), ) - teacherValidator := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). + teacherValidator := govy.New( + govy.For(func(t Teacher) string { return t.Name }). WithName("name"). Required(). Rules( - validation.StringNotEmpty(), - validation.OneOf("Jake", "George")), - validation.ForSlice(func(t Teacher) []Student { return t.Students }). + rules.StringNotEmpty(), + rules.OneOf("Jake", "George")), + govy.ForSlice(func(t Teacher) []Student { return t.Students }). WithName("students"). Rules( - validation.SliceMaxLength[[]Student](2), - validation.SliceUnique(func(v Student) string { return v.Index })). + rules.SliceMaxLength[[]Student](2), + rules.SliceUnique(func(v Student) string { return v.Index })). IncludeForEach(studentValidator), - validation.For(func(t Teacher) University { return t.University }). + govy.For(func(t Teacher) University { return t.University }). WithName("university"). Include(universityValidation), ).When(func(t Teacher) bool { return t.Age < 50 }) @@ -870,31 +870,31 @@ func ExampleValidator_branchingPattern() { } ) - csvValidation := validation.New[CSV]( - validation.For(func(c CSV) string { return c.Separator }). + csvValidation := govy.New( + govy.For(func(c CSV) string { return c.Separator }). WithName("separator"). Required(). - Rules(validation.OneOf(",", ";")), + Rules(rules.OneOf(",", ";")), ) - jsonValidation := validation.New[JSON]( - validation.For(func(j JSON) string { return j.Indent }). + jsonValidation := govy.New( + govy.For(func(j JSON) string { return j.Indent }). WithName("indent"). Required(). - Rules(validation.StringMatchRegexp(regexp.MustCompile(`^\s*$`))), + Rules(rules.StringMatchRegexp(regexp.MustCompile(`^\s*$`))), ) - fileValidation := validation.New[File]( - validation.ForPointer(func(f File) *CSV { return f.CSV }). + fileValidation := govy.New( + govy.ForPointer(func(f File) *CSV { return f.CSV }). When(func(f File) bool { return f.Format == "csv" }). Include(csvValidation), - validation.ForPointer(func(f File) *JSON { return f.JSON }). + govy.ForPointer(func(f File) *JSON { return f.JSON }). When(func(f File) bool { return f.Format == "json" }). Include(jsonValidation), - validation.For(func(f File) string { return f.Format }). + govy.For(func(f File) string { return f.Format }). WithName("format"). Required(). - Rules(validation.OneOf("csv", "json")), + Rules(rules.OneOf("csv", "json")), ).WithName("File") file := File{ @@ -928,33 +928,43 @@ func ExampleValidator_branchingPattern() { // For builtin rules, the description is already provided. // - Use [WhenDescription] to provide a plan-only description for your when conditions. func ExamplePlan() { - v := validation.New[Teacher]( - validation.For(func(t Teacher) string { return t.Name }). + v := govy.New( + govy.For(func(t Teacher) string { return t.Name }). WithName("name"). WithExamples("Jake", "John"). When( func(t Teacher) bool { return t.Name == "Jerry" }, - validation.WhenDescription("name is Jerry"), + govy.WhenDescription("name is Jerry"), ). Rules( - validation.NotEqualTo("Jerry"). + rules.NotEqualTo("Jerry"). WithDetails("Jerry is just a name!"), ), ) - properties := validation.Plan(v) - _ = yaml.NewEncoder(os.Stdout, yaml.Indent(2)).Encode(properties) - - // Output: - // - path: $.name - // type: string - // examples: - // - Jake - // - John - // rules: - // - description: should be not equal to 'Jerry' - // details: Jerry is just a name! - // errorCode: not_equal_to - // conditions: - // - name is Jerry + properties := govy.Plan(v) + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + _ = enc.Encode(properties) + + //[ + // { + // "path": "$.name", + // "type": "string", + // "examples": [ + // "Jake", + // "John" + // ], + // "rules": [ + // { + // "description": "should be not equal to 'Jerry'", + // "details": "Jerry is just a name!", + // "errorCode": "not_equal_to", + // "conditions": [ + // "name is Jerry" + // ] + // } + // ] + // } + //] } diff --git a/pkg/govy/forbidden.go b/pkg/govy/forbidden.go deleted file mode 100644 index e821f62..0000000 --- a/pkg/govy/forbidden.go +++ /dev/null @@ -1,15 +0,0 @@ -package validation - -import "github.com/pkg/errors" - -func Forbidden[T any]() SingleRule[T] { - msg := "property is forbidden" - return NewSingleRule(func(v T) error { - if isEmptyFunc(v) { - return nil - } - return errors.New(msg) - }). - WithErrorCode(ErrorCodeForbidden). - WithDescription(msg) -} diff --git a/pkg/govy/plan.go b/pkg/govy/plan.go index feebe0b..807ebdc 100644 --- a/pkg/govy/plan.go +++ b/pkg/govy/plan.go @@ -1,4 +1,4 @@ -package validation +package govy import ( "reflect" diff --git a/pkg/govy/plan_test.go b/pkg/govy/plan_test.go index 0c5d8d0..b94dd64 100644 --- a/pkg/govy/plan_test.go +++ b/pkg/govy/plan_test.go @@ -1,4 +1,4 @@ -package validation +package govy_test import ( "bytes" @@ -9,6 +9,9 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/nobl9/govy/pkg/govy" + "github.com/nobl9/govy/pkg/rules" ) //go:embed test_data/expected_pod_plan.json @@ -54,25 +57,25 @@ type PodStatus struct { } func TestPlan(t *testing.T) { - metadataValidator := New[PodMetadata]( - For(func(p PodMetadata) string { return p.Name }). + metadataValidator := govy.New[PodMetadata]( + govy.For(func(p PodMetadata) string { return p.Name }). WithName("name"). Required(). - Rules(StringNotEmpty()), - For(func(p PodMetadata) string { return p.Namespace }). + Rules(rules.StringNotEmpty()), + govy.For(func(p PodMetadata) string { return p.Namespace }). WithName("namespace"). Required(). - Rules(StringNotEmpty()), - ForMap(func(p PodMetadata) Labels { return p.Labels }). + Rules(rules.StringNotEmpty()), + govy.ForMap(func(p PodMetadata) Labels { return p.Labels }). WithName("labels"). - Rules(MapMaxLength[Labels](10)). - RulesForKeys(StringIsDNSSubdomain()). - RulesForValues(StringMaxLength(120)), - ForMap(func(p PodMetadata) Annotations { return p.Annotations }). + Rules(rules.MapMaxLength[Labels](10)). + RulesForKeys(rules.StringIsDNSSubdomain()). + RulesForValues(rules.StringMaxLength(120)), + govy.ForMap(func(p PodMetadata) Annotations { return p.Annotations }). WithName("annotations"). - Rules(MapMaxLength[Annotations](10)). + Rules(rules.MapMaxLength[Annotations](10)). RulesForItems( - NewSingleRule(func(a MapItem[string, string]) error { + govy.NewSingleRule(func(a govy.MapItem[string, string]) error { if a.Key == a.Value { return errors.New("key and value must not be equal") } @@ -81,56 +84,56 @@ func TestPlan(t *testing.T) { ), ) - specValidator := New[PodSpec]( - For(func(p PodSpec) string { return p.DNSPolicy }). + specValidator := govy.New[PodSpec]( + govy.For(func(p PodSpec) string { return p.DNSPolicy }). WithName("dnsPolicy"). Required(). - Rules(OneOf("ClusterFirst", "Default")), - ForSlice(func(p PodSpec) []Container { return p.Containers }). + Rules(rules.OneOf("ClusterFirst", "Default")), + govy.ForSlice(func(p PodSpec) []Container { return p.Containers }). WithName("containers"). Rules( - SliceMaxLength[[]Container](10), - SliceUnique(func(c Container) string { return c.Name }), + rules.SliceMaxLength[[]Container](10), + rules.SliceUnique(func(c Container) string { return c.Name }), ). - IncludeForEach(New[Container]( - For(func(c Container) string { return c.Name }). + IncludeForEach(govy.New[Container]( + govy.For(func(c Container) string { return c.Name }). WithName("name"). Required(). - Rules(StringIsDNSSubdomain()), - For(func(c Container) string { return c.Image }). + Rules(rules.StringIsDNSSubdomain()), + govy.For(func(c Container) string { return c.Image }). WithName("image"). Required(). - Rules(StringNotEmpty()), - ForSlice(func(c Container) []EnvVar { return c.Env }). + Rules(rules.StringNotEmpty()), + govy.ForSlice(func(c Container) []EnvVar { return c.Env }). WithName("env"). RulesForEach( - NewSingleRule(func(e EnvVar) error { + govy.NewSingleRule(func(e EnvVar) error { return nil }).WithDescription("custom error!"), ), )), ) - validator := New[Pod]( - For(func(p Pod) string { return p.APIVersion }). + validator := govy.New[Pod]( + govy.For(func(p Pod) string { return p.APIVersion }). WithName("apiVersion"). Required(). - Rules(OneOf("v1", "v2")), - For(func(p Pod) string { return p.Kind }). + Rules(rules.OneOf("v1", "v2")), + govy.For(func(p Pod) string { return p.Kind }). WithName("kind"). Required(). - Rules(EqualTo("Pod")), - For(func(p Pod) PodMetadata { return p.Metadata }). + Rules(rules.EqualTo("Pod")), + govy.For(func(p Pod) PodMetadata { return p.Metadata }). WithName("metadata"). Required(). Include(metadataValidator), - For(func(p Pod) PodSpec { return p.Spec }). + govy.For(func(p Pod) PodSpec { return p.Spec }). WithName("spec"). Required(). Include(specValidator), ) - properties := Plan(validator) + properties := govy.Plan(validator) buf := bytes.Buffer{} enc := json.NewEncoder(&buf) diff --git a/pkg/govy/predicate.go b/pkg/govy/predicate.go index 7c15ff0..0670541 100644 --- a/pkg/govy/predicate.go +++ b/pkg/govy/predicate.go @@ -1,4 +1,4 @@ -package validation +package govy import "fmt" diff --git a/pkg/govy/required.go b/pkg/govy/required.go deleted file mode 100644 index eaa20b2..0000000 --- a/pkg/govy/required.go +++ /dev/null @@ -1,23 +0,0 @@ -package validation - -import ( - "reflect" -) - -func Required[T any]() SingleRule[T] { - return NewSingleRule(func(v T) error { - if isEmptyFunc(v) { - return NewRequiredError() - } - return nil - }). - WithErrorCode(ErrorCodeRequired). - WithDescription("property is required") -} - -// isEmptyFunc checks only the types which it makes sense for. -// It's hard to consider 0 an empty value for anything really. -func isEmptyFunc(v interface{}) bool { - rv := reflect.ValueOf(v) - return rv.Kind() == 0 || rv.IsZero() -} diff --git a/pkg/govy/rule.go b/pkg/govy/rule.go index 153105b..d5ec750 100644 --- a/pkg/govy/rule.go +++ b/pkg/govy/rule.go @@ -1,7 +1,9 @@ -package validation +package govy import ( "fmt" + + "github.com/nobl9/govy/internal" ) // Rule is the interface for all validation rules. @@ -116,7 +118,7 @@ type RuleSet[T any] struct { // except each aggregated rule is validated individually. // The errors are aggregated and returned as a single error which serves as a container for them. func (r RuleSet[T]) Validate(v T) error { - var errs ruleSetError + var errs internal.RuleSetError for i := range r.rules { if err := r.rules[i].Validate(v); err != nil { switch ev := err.(type) { diff --git a/pkg/govy/rule_test.go b/pkg/govy/rule_test.go index e78b31e..bc01db0 100644 --- a/pkg/govy/rule_test.go +++ b/pkg/govy/rule_test.go @@ -1,14 +1,16 @@ -package validation +package govy_test import ( "testing" "github.com/pkg/errors" "github.com/stretchr/testify/assert" + + "github.com/nobl9/govy/pkg/govy" ) func TestSingleRule(t *testing.T) { - r := NewSingleRule(func(v int) error { + r := govy.NewSingleRule(func(v int) error { if v < 0 { return errors.Errorf("must be positive") } @@ -22,7 +24,7 @@ func TestSingleRule(t *testing.T) { } func TestSingleRule_WithErrorCode(t *testing.T) { - r := NewSingleRule(func(v int) error { + r := govy.NewSingleRule(func(v int) error { if v < 0 { return errors.Errorf("must be positive") } @@ -33,7 +35,7 @@ func TestSingleRule_WithErrorCode(t *testing.T) { assert.Nil(t, err) err = r.Validate(-1) assert.EqualError(t, err, "must be positive") - assert.Equal(t, "test", err.(*RuleError).Code) + assert.Equal(t, "test", err.(*govy.RuleError).Code) } func TestSingleRule_WithMessage(t *testing.T) { @@ -62,7 +64,7 @@ func TestSingleRule_WithMessage(t *testing.T) { ExpectedError: "message; details", }, } { - r := NewSingleRule(func(v int) error { + r := govy.NewSingleRule(func(v int) error { if v < 0 { return errors.Errorf(test.Error) } @@ -76,7 +78,7 @@ func TestSingleRule_WithMessage(t *testing.T) { assert.Nil(t, err) err = r.Validate(-1) assert.EqualError(t, err, test.ExpectedError) - assert.Equal(t, "test", err.(*RuleError).Code) + assert.Equal(t, "test", err.(*govy.RuleError).Code) } } @@ -102,7 +104,7 @@ func TestSingleRule_WithDetails(t *testing.T) { ExpectedError: "details", }, } { - r := NewSingleRule(func(v int) error { + r := govy.NewSingleRule(func(v int) error { if v < 0 { return errors.Errorf(test.Error) } @@ -115,6 +117,6 @@ func TestSingleRule_WithDetails(t *testing.T) { assert.Nil(t, err) err = r.Validate(-1) assert.EqualError(t, err, test.ExpectedError) - assert.Equal(t, "test", err.(*RuleError).Code) + assert.Equal(t, "test", err.(*govy.RuleError).Code) } } diff --git a/pkg/govy/rules.go b/pkg/govy/rules.go index fc316db..c4f32d1 100644 --- a/pkg/govy/rules.go +++ b/pkg/govy/rules.go @@ -1,7 +1,9 @@ -package validation +package govy import ( "github.com/pkg/errors" + + "github.com/nobl9/govy/internal" ) // For creates a new [PropertyRules] instance for the property @@ -34,7 +36,7 @@ func Transform[T, N, S any](getter PropertyGetter[T, S], transform Transformer[T return PropertyRules[N, S]{ transformGetter: func(s S) (transformed N, original any, err error) { v := getter(s) - if isEmptyFunc(v) { + if internal.IsEmptyFunc(v) { return transformed, nil, emptyErr{} } transformed, err = transform(v) @@ -243,14 +245,14 @@ func (r PropertyRules[T, S]) getValue(st S) (v T, skip bool, propErr *PropertyEr } return v, false, NewPropertyError(r.name, propValue, err) } - isEmpty := isEmptyError || (!r.isPointer && isEmptyFunc(v)) + isEmpty := isEmptyError || (!r.isPointer && internal.IsEmptyFunc(v)) // If the value is not empty we simply return it. if !isEmpty { return v, false, nil } // If the value is empty and the property is required, we return [ErrorCodeRequired]. if r.required { - return v, false, NewPropertyError(r.name, nil, NewRequiredError()) + return v, false, NewPropertyError(r.name, nil, newRequiredError()) } // If the value is empty and we're skipping empty values or the value is a pointer, we skip the validation. if r.omitEmpty || r.isPointer { @@ -258,3 +260,10 @@ func (r PropertyRules[T, S]) getValue(st S) (v T, skip bool, propErr *PropertyEr } return v, false, nil } + +func newRequiredError() *RuleError { + return NewRuleError( + internal.RequiredErrorMessage, + internal.RequiredErrorCodeString, + ) +} diff --git a/pkg/govy/rules_for_map.go b/pkg/govy/rules_for_map.go index c2cc498..76396e9 100644 --- a/pkg/govy/rules_for_map.go +++ b/pkg/govy/rules_for_map.go @@ -1,7 +1,9 @@ -package validation +package govy import ( "fmt" + + "github.com/nobl9/govy/internal" ) // ForMap creates a new [PropertyRulesForMap] instance for a map property @@ -57,7 +59,7 @@ func (r PropertyRulesForMap[M, K, V, S]) Validate(st S) PropertyErrors { for _, e := range forItemErr { // TODO: Figure out how to handle custom PropertyErrors. // Custom errors' value for nested item will be overridden by the actual value. - e.PropertyValue = propertyValueString(v) + e.PropertyValue = internal.PropertyValueString(v) err = append(err, e.PrependPropertyName(MapElementName(r.mapRules.name, k))) } } diff --git a/pkg/govy/rules_for_map_test.go b/pkg/govy/rules_for_map_test.go index 8d25e26..070a717 100644 --- a/pkg/govy/rules_for_map_test.go +++ b/pkg/govy/rules_for_map_test.go @@ -1,4 +1,4 @@ -package validation +package govy_test import ( "testing" @@ -6,6 +6,8 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/nobl9/govy/pkg/govy" ) func TestPropertyRulesForMap(t *testing.T) { @@ -15,12 +17,12 @@ func TestPropertyRulesForMap(t *testing.T) { } t.Run("no predicates, no error", func(t *testing.T) { - baseRules := ForMap(func(m mockStruct) map[string]string { return map[string]string{"key": "value"} }). + baseRules := govy.ForMap(func(m mockStruct) map[string]string { return map[string]string{"key": "value"} }). WithName("test.path") - for _, r := range []PropertyRulesForMap[map[string]string, string, string, mockStruct]{ - baseRules.RulesForKeys(NewSingleRule(func(v string) error { return nil })), - baseRules.RulesForValues(NewSingleRule(func(v string) error { return nil })), - baseRules.RulesForItems(NewSingleRule(func(v MapItem[string, string]) error { return nil })), + for _, r := range []govy.PropertyRulesForMap[map[string]string, string, string, mockStruct]{ + baseRules.RulesForKeys(govy.NewSingleRule(func(v string) error { return nil })), + baseRules.RulesForValues(govy.NewSingleRule(func(v string) error { return nil })), + baseRules.RulesForItems(govy.NewSingleRule(func(v govy.MapItem[string, string]) error { return nil })), } { errs := r.Validate(mockStruct{}) assert.Nil(t, errs) @@ -29,35 +31,37 @@ func TestPropertyRulesForMap(t *testing.T) { t.Run("no predicates, validate", func(t *testing.T) { expectedErr := errors.New("ops!") - baseRules := ForMap(func(m mockStruct) map[string]string { return map[string]string{"key": "value"} }). + baseRules := govy.ForMap(func(m mockStruct) map[string]string { return map[string]string{"key": "value"} }). WithName("test.path") for name, test := range map[string]struct { - Rules PropertyRulesForMap[map[string]string, string, string, mockStruct] - Expected *PropertyError + Rules govy.PropertyRulesForMap[map[string]string, string, string, mockStruct] + Expected *govy.PropertyError }{ "keys": { - Rules: baseRules.RulesForKeys(NewSingleRule(func(v string) error { return expectedErr })), - Expected: &PropertyError{ + Rules: baseRules.RulesForKeys(govy.NewSingleRule(func(v string) error { return expectedErr })), + Expected: &govy.PropertyError{ PropertyName: "test.path.key", PropertyValue: "key", IsKeyError: true, - Errors: []*RuleError{{Message: expectedErr.Error()}}, + Errors: []*govy.RuleError{{Message: expectedErr.Error()}}, }, }, "values": { - Rules: baseRules.RulesForValues(NewSingleRule(func(v string) error { return expectedErr })), - Expected: &PropertyError{ + Rules: baseRules.RulesForValues(govy.NewSingleRule(func(v string) error { return expectedErr })), + Expected: &govy.PropertyError{ PropertyName: "test.path.key", PropertyValue: "value", - Errors: []*RuleError{{Message: expectedErr.Error()}}, + Errors: []*govy.RuleError{{Message: expectedErr.Error()}}, }, }, "items": { - Rules: baseRules.RulesForItems(NewSingleRule(func(v MapItem[string, string]) error { return expectedErr })), - Expected: &PropertyError{ + Rules: baseRules.RulesForItems( + govy.NewSingleRule(func(v govy.MapItem[string, string]) error { return expectedErr }), + ), + Expected: &govy.PropertyError{ PropertyName: "test.path.key", PropertyValue: "value", - Errors: []*RuleError{{Message: expectedErr.Error()}}, + Errors: []*govy.RuleError{{Message: expectedErr.Error()}}, }, }, } { @@ -70,15 +74,17 @@ func TestPropertyRulesForMap(t *testing.T) { }) t.Run("predicate matches, don't validate", func(t *testing.T) { - baseRules := ForMap(func(m mockStruct) map[string]string { return map[string]string{"key": "value"} }). + baseRules := govy.ForMap(func(m mockStruct) map[string]string { return map[string]string{"key": "value"} }). WithName("test.path"). When(func(mockStruct) bool { return true }). When(func(mockStruct) bool { return true }). When(func(st mockStruct) bool { return len(st.StringMap) == 0 }) - for _, r := range []PropertyRulesForMap[map[string]string, string, string, mockStruct]{ - baseRules.RulesForKeys(NewSingleRule(func(v string) error { return errors.New("ops!") })), - baseRules.RulesForValues(NewSingleRule(func(v string) error { return errors.New("ops!") })), - baseRules.RulesForItems(NewSingleRule(func(v MapItem[string, string]) error { return errors.New("ops!") })), + for _, r := range []govy.PropertyRulesForMap[map[string]string, string, string, mockStruct]{ + baseRules.RulesForKeys(govy.NewSingleRule(func(v string) error { return errors.New("ops!") })), + baseRules.RulesForValues(govy.NewSingleRule(func(v string) error { return errors.New("ops!") })), + baseRules.RulesForItems( + govy.NewSingleRule(func(v govy.MapItem[string, string]) error { return errors.New("ops!") }), + ), } { errs := r.Validate(mockStruct{StringMap: map[string]string{"different": "map"}}) assert.Nil(t, errs) @@ -95,29 +101,29 @@ func TestPropertyRulesForMap(t *testing.T) { errNestedItem := errors.New("nested item error") errNestedRule := errors.New("nested rule error") - r := ForMap(func(m mockStruct) map[string]string { return m.StringMap }). + r := govy.ForMap(func(m mockStruct) map[string]string { return m.StringMap }). WithName("test.path"). - Rules(NewSingleRule(func(v map[string]string) error { return errRule })). + Rules(govy.NewSingleRule(func(v map[string]string) error { return errRule })). RulesForKeys( - NewSingleRule(func(v string) error { return errKey }), - NewSingleRule(func(v string) error { - return NewPropertyError("nested", "nestedKey", errNestedKey) + govy.NewSingleRule(func(v string) error { return errKey }), + govy.NewSingleRule(func(v string) error { + return govy.NewPropertyError("nested", "nestedKey", errNestedKey) }), ). RulesForValues( - NewSingleRule(func(v string) error { return errValue }), - NewSingleRule(func(v string) error { - return NewPropertyError("nested", "nestedValue", errNestedValue) + govy.NewSingleRule(func(v string) error { return errValue }), + govy.NewSingleRule(func(v string) error { + return govy.NewPropertyError("nested", "nestedValue", errNestedValue) }), ). RulesForItems( - NewSingleRule(func(v MapItem[string, string]) error { return errItem }), - NewSingleRule(func(v MapItem[string, string]) error { - return NewPropertyError("nested", "nestedItem", errNestedItem) + govy.NewSingleRule(func(v govy.MapItem[string, string]) error { return errItem }), + govy.NewSingleRule(func(v govy.MapItem[string, string]) error { + return govy.NewPropertyError("nested", "nestedItem", errNestedItem) }), ). - Rules(NewSingleRule(func(v map[string]string) error { - return NewPropertyError("nested", "nestedRule", errNestedRule) + Rules(govy.NewSingleRule(func(v map[string]string) error { + return govy.NewPropertyError("nested", "nestedRule", errNestedRule) })) errs := r.Validate(mockStruct{StringMap: map[string]string{ @@ -125,40 +131,40 @@ func TestPropertyRulesForMap(t *testing.T) { "key2": "value2", }}) require.Len(t, errs, 12) - assert.ElementsMatch(t, []*PropertyError{ + assert.ElementsMatch(t, []*govy.PropertyError{ { PropertyName: "test.path", PropertyValue: `{"key1":"value1","key2":"value2"}`, - Errors: []*RuleError{{Message: errRule.Error()}}, + Errors: []*govy.RuleError{{Message: errRule.Error()}}, }, { PropertyName: "test.path.key1", PropertyValue: "key1", IsKeyError: true, - Errors: []*RuleError{{Message: errKey.Error()}}, + Errors: []*govy.RuleError{{Message: errKey.Error()}}, }, { PropertyName: "test.path.key2", PropertyValue: "key2", IsKeyError: true, - Errors: []*RuleError{{Message: errKey.Error()}}, + Errors: []*govy.RuleError{{Message: errKey.Error()}}, }, { PropertyName: "test.path.key1.nested", PropertyValue: "nestedKey", IsKeyError: true, - Errors: []*RuleError{{Message: errNestedKey.Error()}}, + Errors: []*govy.RuleError{{Message: errNestedKey.Error()}}, }, { PropertyName: "test.path.key2.nested", PropertyValue: "nestedKey", IsKeyError: true, - Errors: []*RuleError{{Message: errNestedKey.Error()}}, + Errors: []*govy.RuleError{{Message: errNestedKey.Error()}}, }, { PropertyName: "test.path.key1", PropertyValue: "value1", - Errors: []*RuleError{ + Errors: []*govy.RuleError{ {Message: errValue.Error()}, {Message: errItem.Error()}, }, @@ -166,7 +172,7 @@ func TestPropertyRulesForMap(t *testing.T) { { PropertyName: "test.path.key2", PropertyValue: "value2", - Errors: []*RuleError{ + Errors: []*govy.RuleError{ {Message: errValue.Error()}, {Message: errItem.Error()}, }, @@ -174,27 +180,27 @@ func TestPropertyRulesForMap(t *testing.T) { { PropertyName: "test.path.key1.nested", PropertyValue: "nestedValue", - Errors: []*RuleError{{Message: errNestedValue.Error()}}, + Errors: []*govy.RuleError{{Message: errNestedValue.Error()}}, }, { PropertyName: "test.path.key2.nested", PropertyValue: "nestedValue", - Errors: []*RuleError{{Message: errNestedValue.Error()}}, + Errors: []*govy.RuleError{{Message: errNestedValue.Error()}}, }, { PropertyName: "test.path.key1.nested", PropertyValue: "value1", - Errors: []*RuleError{{Message: errNestedItem.Error()}}, + Errors: []*govy.RuleError{{Message: errNestedItem.Error()}}, }, { PropertyName: "test.path.key2.nested", PropertyValue: "value2", - Errors: []*RuleError{{Message: errNestedItem.Error()}}, + Errors: []*govy.RuleError{{Message: errNestedItem.Error()}}, }, { PropertyName: "test.path.nested", PropertyValue: "nestedRule", - Errors: []*RuleError{{Message: errNestedRule.Error()}}, + Errors: []*govy.RuleError{{Message: errNestedRule.Error()}}, }, }, errs) }) @@ -202,24 +208,24 @@ func TestPropertyRulesForMap(t *testing.T) { t.Run("cascade mode stop", func(t *testing.T) { keyErr := errors.New("key error") valueErr := errors.New("value error") - r := ForMap(func(m mockStruct) map[string]string { return map[string]string{"key": "value"} }). + r := govy.ForMap(func(m mockStruct) map[string]string { return map[string]string{"key": "value"} }). WithName("test.path"). - Cascade(CascadeModeStop). - RulesForValues(NewSingleRule(func(v string) error { return valueErr })). - RulesForKeys(NewSingleRule(func(v string) error { return keyErr })) + Cascade(govy.CascadeModeStop). + RulesForValues(govy.NewSingleRule(func(v string) error { return valueErr })). + RulesForKeys(govy.NewSingleRule(func(v string) error { return keyErr })) errs := r.Validate(mockStruct{}) require.Len(t, errs, 2) - assert.ElementsMatch(t, []*PropertyError{ + assert.ElementsMatch(t, []*govy.PropertyError{ { PropertyName: "test.path.key", PropertyValue: "key", IsKeyError: true, - Errors: []*RuleError{{Message: keyErr.Error()}}, + Errors: []*govy.RuleError{{Message: keyErr.Error()}}, }, { PropertyName: "test.path.key", PropertyValue: "value", - Errors: []*RuleError{{Message: valueErr.Error()}}, + Errors: []*govy.RuleError{{Message: valueErr.Error()}}, }, }, errs) }) @@ -233,47 +239,47 @@ func TestPropertyRulesForMap(t *testing.T) { errIncludedItem1 := errors.New("included item 1 error") errIncludedItem2 := errors.New("included item 2 error") - r := ForMap(func(m mockStruct) map[string]int { return m.IntMap }). + r := govy.ForMap(func(m mockStruct) map[string]int { return m.IntMap }). WithName("test.path"). - Rules(NewSingleRule(func(v map[string]int) error { return errRule })). - IncludeForKeys(New( - For(func(s string) string { return s }). + Rules(govy.NewSingleRule(func(v map[string]int) error { return errRule })). + IncludeForKeys(govy.New( + govy.For(func(s string) string { return s }). WithName("included_key"). Rules( - NewSingleRule(func(v string) error { return errIncludedKey1 }), - NewSingleRule(func(v string) error { return errIncludedKey2 }), + govy.NewSingleRule(func(v string) error { return errIncludedKey1 }), + govy.NewSingleRule(func(v string) error { return errIncludedKey2 }), ), )). - IncludeForValues(New( - For(func(i int) int { return i }). + IncludeForValues(govy.New( + govy.For(func(i int) int { return i }). WithName("included_value"). Rules( - NewSingleRule(func(v int) error { return errIncludedValue1 }), - NewSingleRule(func(v int) error { return errIncludedValue2 }), + govy.NewSingleRule(func(v int) error { return errIncludedValue1 }), + govy.NewSingleRule(func(v int) error { return errIncludedValue2 }), ), )). - IncludeForItems(New( - For(func(i MapItem[string, int]) MapItem[string, int] { return i }). + IncludeForItems(govy.New( + govy.For(func(i govy.MapItem[string, int]) govy.MapItem[string, int] { return i }). WithName("included_item"). Rules( - NewSingleRule(func(v MapItem[string, int]) error { return errIncludedItem1 }), - NewSingleRule(func(v MapItem[string, int]) error { return errIncludedItem2 }), + govy.NewSingleRule(func(v govy.MapItem[string, int]) error { return errIncludedItem1 }), + govy.NewSingleRule(func(v govy.MapItem[string, int]) error { return errIncludedItem2 }), ), )) errs := r.Validate(mockStruct{IntMap: map[string]int{"key": 1}}) require.Len(t, errs, 4) - assert.ElementsMatch(t, []*PropertyError{ + assert.ElementsMatch(t, []*govy.PropertyError{ { PropertyName: "test.path", PropertyValue: `{"key":1}`, - Errors: []*RuleError{{Message: errRule.Error()}}, + Errors: []*govy.RuleError{{Message: errRule.Error()}}, }, { PropertyName: "test.path.key.included_key", PropertyValue: "key", IsKeyError: true, - Errors: []*RuleError{ + Errors: []*govy.RuleError{ {Message: errIncludedKey1.Error()}, {Message: errIncludedKey2.Error()}, }, @@ -281,7 +287,7 @@ func TestPropertyRulesForMap(t *testing.T) { { PropertyName: "test.path.key.included_value", PropertyValue: "1", - Errors: []*RuleError{ + Errors: []*govy.RuleError{ {Message: errIncludedValue1.Error()}, {Message: errIncludedValue2.Error()}, }, @@ -289,7 +295,7 @@ func TestPropertyRulesForMap(t *testing.T) { { PropertyName: "test.path.key.included_item", PropertyValue: "1", - Errors: []*RuleError{ + Errors: []*govy.RuleError{ {Message: errIncludedItem1.Error()}, {Message: errIncludedItem2.Error()}, }, @@ -306,47 +312,47 @@ func TestPropertyRulesForMap(t *testing.T) { errIncludedItem1 := errors.New("included item 1 error") errIncludedItem2 := errors.New("included item 2 error") - r := ForMap(func(m mockStruct) map[string]string { return m.StringMap }). + r := govy.ForMap(func(m mockStruct) map[string]string { return m.StringMap }). WithName("test.path"). - Rules(NewSingleRule(func(v map[string]string) error { return errRule })). - IncludeForKeys(New( - For(func(s string) string { return s }). + Rules(govy.NewSingleRule(func(v map[string]string) error { return errRule })). + IncludeForKeys(govy.New( + govy.For(func(s string) string { return s }). WithName("included_key"). Rules( - NewSingleRule(func(v string) error { return errIncludedKey1 }), - NewSingleRule(func(v string) error { return errIncludedKey2 }), + govy.NewSingleRule(func(v string) error { return errIncludedKey1 }), + govy.NewSingleRule(func(v string) error { return errIncludedKey2 }), ), )). - IncludeForValues(New( - For(func(i string) string { return i }). + IncludeForValues(govy.New( + govy.For(func(i string) string { return i }). WithName("included_value"). Rules( - NewSingleRule(func(v string) error { return errIncludedValue1 }), - NewSingleRule(func(v string) error { return errIncludedValue2 }), + govy.NewSingleRule(func(v string) error { return errIncludedValue1 }), + govy.NewSingleRule(func(v string) error { return errIncludedValue2 }), ), )). - IncludeForItems(New( - For(func(i MapItem[string, string]) MapItem[string, string] { return i }). + IncludeForItems(govy.New( + govy.For(func(i govy.MapItem[string, string]) govy.MapItem[string, string] { return i }). WithName("included_item"). Rules( - NewSingleRule(func(v MapItem[string, string]) error { return errIncludedItem1 }), - NewSingleRule(func(v MapItem[string, string]) error { return errIncludedItem2 }), + govy.NewSingleRule(func(v govy.MapItem[string, string]) error { return errIncludedItem1 }), + govy.NewSingleRule(func(v govy.MapItem[string, string]) error { return errIncludedItem2 }), ), )) errs := r.Validate(mockStruct{StringMap: map[string]string{"key": "1"}}) require.Len(t, errs, 4) - assert.ElementsMatch(t, []*PropertyError{ + assert.ElementsMatch(t, []*govy.PropertyError{ { PropertyName: "test.path", PropertyValue: `{"key":"1"}`, - Errors: []*RuleError{{Message: errRule.Error()}}, + Errors: []*govy.RuleError{{Message: errRule.Error()}}, }, { PropertyName: "test.path.key.included_key", PropertyValue: "key", IsKeyError: true, - Errors: []*RuleError{ + Errors: []*govy.RuleError{ {Message: errIncludedKey1.Error()}, {Message: errIncludedKey2.Error()}, }, @@ -354,7 +360,7 @@ func TestPropertyRulesForMap(t *testing.T) { { PropertyName: "test.path.key.included_value", PropertyValue: "1", - Errors: []*RuleError{ + Errors: []*govy.RuleError{ {Message: errIncludedValue1.Error()}, {Message: errIncludedValue2.Error()}, }, @@ -362,7 +368,7 @@ func TestPropertyRulesForMap(t *testing.T) { { PropertyName: "test.path.key.included_item", PropertyValue: "1", - Errors: []*RuleError{ + Errors: []*govy.RuleError{ {Message: errIncludedItem1.Error()}, {Message: errIncludedItem2.Error()}, }, @@ -372,20 +378,20 @@ func TestPropertyRulesForMap(t *testing.T) { t.Run("include nested for map", func(t *testing.T) { expectedErr := errors.New("oh no!") - inc := New( - ForMap(GetSelf[map[string]string]()). - RulesForValues(NewSingleRule(func(v string) error { return expectedErr })), + inc := govy.New( + govy.ForMap(govy.GetSelf[map[string]string]()). + RulesForValues(govy.NewSingleRule(func(v string) error { return expectedErr })), ) - r := For(func(m mockStruct) map[string]string { return m.StringMap }). + r := govy.For(func(m mockStruct) map[string]string { return m.StringMap }). WithName("test.path"). Include(inc) errs := r.Validate(mockStruct{StringMap: map[string]string{"key": "value"}}) require.Len(t, errs, 1) - assert.Equal(t, &PropertyError{ + assert.Equal(t, &govy.PropertyError{ PropertyName: "test.path.key", PropertyValue: "value", - Errors: []*RuleError{{Message: expectedErr.Error()}}, + Errors: []*govy.RuleError{{Message: expectedErr.Error()}}, }, errs[0]) }) } diff --git a/pkg/govy/rules_for_slice.go b/pkg/govy/rules_for_slice.go index 13fcccc..be1c19f 100644 --- a/pkg/govy/rules_for_slice.go +++ b/pkg/govy/rules_for_slice.go @@ -1,4 +1,4 @@ -package validation +package govy import "fmt" diff --git a/pkg/govy/rules_for_slice_test.go b/pkg/govy/rules_for_slice_test.go index 6af7d16..4fdbc9c 100644 --- a/pkg/govy/rules_for_slice_test.go +++ b/pkg/govy/rules_for_slice_test.go @@ -1,4 +1,4 @@ -package validation +package govy_test import ( "testing" @@ -6,6 +6,8 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/nobl9/govy/pkg/govy" ) func TestPropertyRulesForEach(t *testing.T) { @@ -14,35 +16,35 @@ func TestPropertyRulesForEach(t *testing.T) { } t.Run("no predicates, no error", func(t *testing.T) { - r := ForSlice(func(m mockStruct) []string { return []string{"path"} }). + r := govy.ForSlice(func(m mockStruct) []string { return []string{"path"} }). WithName("test.path"). - RulesForEach(NewSingleRule(func(v string) error { return nil })) + RulesForEach(govy.NewSingleRule(func(v string) error { return nil })) errs := r.Validate(mockStruct{}) assert.Nil(t, errs) }) t.Run("no predicates, validate", func(t *testing.T) { expectedErr := errors.New("ops!") - r := ForSlice(func(m mockStruct) []string { return []string{"path"} }). + r := govy.ForSlice(func(m mockStruct) []string { return []string{"path"} }). WithName("test.path"). - RulesForEach(NewSingleRule(func(v string) error { return expectedErr })) + RulesForEach(govy.NewSingleRule(func(v string) error { return expectedErr })) errs := r.Validate(mockStruct{}) require.Len(t, errs, 1) - assert.Equal(t, &PropertyError{ + assert.Equal(t, &govy.PropertyError{ PropertyName: "test.path[0]", PropertyValue: "path", IsSliceElementError: true, - Errors: []*RuleError{{Message: expectedErr.Error()}}, + Errors: []*govy.RuleError{{Message: expectedErr.Error()}}, }, errs[0]) }) t.Run("predicate matches, don't validate", func(t *testing.T) { - r := ForSlice(func(m mockStruct) []string { return []string{"value"} }). + r := govy.ForSlice(func(m mockStruct) []string { return []string{"value"} }). WithName("test.path"). When(func(mockStruct) bool { return true }). When(func(mockStruct) bool { return true }). When(func(st mockStruct) bool { return len(st.Fields) == 0 }). - RulesForEach(NewSingleRule(func(v string) error { return errors.New("ops!") })) + RulesForEach(govy.NewSingleRule(func(v string) error { return errors.New("ops!") })) errs := r.Validate(mockStruct{Fields: []string{"something"}}) assert.Nil(t, errs) }) @@ -52,73 +54,73 @@ func TestPropertyRulesForEach(t *testing.T) { err2 := errors.New("another error...") err3 := errors.New("rule error") err4 := errors.New("rule error again") - r := ForSlice(func(m mockStruct) []string { return m.Fields }). + r := govy.ForSlice(func(m mockStruct) []string { return m.Fields }). WithName("test.path"). - Rules(NewSingleRule(func(v []string) error { return err3 })). + Rules(govy.NewSingleRule(func(v []string) error { return err3 })). RulesForEach( - NewSingleRule(func(v string) error { return err1 }), - NewSingleRule(func(v string) error { - return NewPropertyError("nested", "made-up", err2) + govy.NewSingleRule(func(v string) error { return err1 }), + govy.NewSingleRule(func(v string) error { + return govy.NewPropertyError("nested", "made-up", err2) }), ). - Rules(NewSingleRule(func(v []string) error { - return NewPropertyError("nested", "nestedValue", err4) + Rules(govy.NewSingleRule(func(v []string) error { + return govy.NewPropertyError("nested", "nestedValue", err4) })) errs := r.Validate(mockStruct{Fields: []string{"1", "2"}}) require.Len(t, errs, 6) - assert.ElementsMatch(t, []*PropertyError{ + assert.ElementsMatch(t, []*govy.PropertyError{ { PropertyName: "test.path", PropertyValue: `["1","2"]`, - Errors: []*RuleError{{Message: err3.Error()}}, + Errors: []*govy.RuleError{{Message: err3.Error()}}, }, { PropertyName: "test.path.nested", PropertyValue: "nestedValue", - Errors: []*RuleError{{Message: err4.Error()}}, + Errors: []*govy.RuleError{{Message: err4.Error()}}, }, { PropertyName: "test.path[0]", PropertyValue: "1", IsSliceElementError: true, - Errors: []*RuleError{{Message: err1.Error()}}, + Errors: []*govy.RuleError{{Message: err1.Error()}}, }, { PropertyName: "test.path[1]", PropertyValue: "2", IsSliceElementError: true, - Errors: []*RuleError{{Message: err1.Error()}}, + Errors: []*govy.RuleError{{Message: err1.Error()}}, }, { PropertyName: "test.path[0].nested", PropertyValue: "made-up", IsSliceElementError: true, - Errors: []*RuleError{{Message: err2.Error()}}, + Errors: []*govy.RuleError{{Message: err2.Error()}}, }, { PropertyName: "test.path[1].nested", PropertyValue: "made-up", IsSliceElementError: true, - Errors: []*RuleError{{Message: err2.Error()}}, + Errors: []*govy.RuleError{{Message: err2.Error()}}, }, }, errs) }) t.Run("cascade mode stop", func(t *testing.T) { expectedErr := errors.New("oh no!") - r := ForSlice(func(m mockStruct) []string { return []string{"value"} }). + r := govy.ForSlice(func(m mockStruct) []string { return []string{"value"} }). WithName("test.path"). - Cascade(CascadeModeStop). - RulesForEach(NewSingleRule(func(v string) error { return expectedErr })). - RulesForEach(NewSingleRule(func(v string) error { return errors.New("no") })) + Cascade(govy.CascadeModeStop). + RulesForEach(govy.NewSingleRule(func(v string) error { return expectedErr })). + RulesForEach(govy.NewSingleRule(func(v string) error { return errors.New("no") })) errs := r.Validate(mockStruct{}) require.Len(t, errs, 1) - assert.Equal(t, &PropertyError{ + assert.Equal(t, &govy.PropertyError{ PropertyName: "test.path[0]", PropertyValue: "value", IsSliceElementError: true, - Errors: []*RuleError{{Message: expectedErr.Error()}}, + Errors: []*govy.RuleError{{Message: expectedErr.Error()}}, }, errs[0]) }) @@ -126,31 +128,31 @@ func TestPropertyRulesForEach(t *testing.T) { err1 := errors.New("oh no!") err2 := errors.New("included") err3 := errors.New("included again") - r := ForSlice(func(m mockStruct) []string { return m.Fields }). + r := govy.ForSlice(func(m mockStruct) []string { return m.Fields }). WithName("test.path"). - RulesForEach(NewSingleRule(func(v string) error { return err1 })). - IncludeForEach(New( - For(func(s string) string { return "nested" }). + RulesForEach(govy.NewSingleRule(func(v string) error { return err1 })). + IncludeForEach(govy.New( + govy.For(func(s string) string { return "nested" }). WithName("included"). Rules( - NewSingleRule(func(v string) error { return err2 }), - NewSingleRule(func(v string) error { return err3 }), + govy.NewSingleRule(func(v string) error { return err2 }), + govy.NewSingleRule(func(v string) error { return err3 }), ), )) errs := r.Validate(mockStruct{Fields: []string{"value"}}) require.Len(t, errs, 2) - assert.ElementsMatch(t, []*PropertyError{ + assert.ElementsMatch(t, []*govy.PropertyError{ { PropertyName: "test.path[0]", PropertyValue: "value", IsSliceElementError: true, - Errors: []*RuleError{{Message: err1.Error()}}, + Errors: []*govy.RuleError{{Message: err1.Error()}}, }, { PropertyName: "test.path[0].included", PropertyValue: "nested", IsSliceElementError: true, - Errors: []*RuleError{ + Errors: []*govy.RuleError{ {Message: err2.Error()}, {Message: err3.Error()}, }, @@ -161,33 +163,33 @@ func TestPropertyRulesForEach(t *testing.T) { t.Run("include nested for slice", func(t *testing.T) { forEachErr := errors.New("oh no!") includedErr := errors.New("oh no!") - inc := New( - ForSlice(GetSelf[[]string]()). - RulesForEach(NewSingleRule(func(v string) error { + inc := govy.New( + govy.ForSlice(govy.GetSelf[[]string]()). + RulesForEach(govy.NewSingleRule(func(v string) error { if v == "value1" { return forEachErr } - return NewPropertyError("nested", "made-up", includedErr) + return govy.NewPropertyError("nested", "made-up", includedErr) })), ) - r := For(func(m mockStruct) []string { return m.Fields }). + r := govy.For(func(m mockStruct) []string { return m.Fields }). WithName("test.path"). Include(inc) errs := r.Validate(mockStruct{Fields: []string{"value1", "value2"}}) require.Len(t, errs, 2) - assert.ElementsMatch(t, []*PropertyError{ + assert.ElementsMatch(t, []*govy.PropertyError{ { PropertyName: "test.path[0]", PropertyValue: "value1", IsSliceElementError: true, - Errors: []*RuleError{{Message: forEachErr.Error()}}, + Errors: []*govy.RuleError{{Message: forEachErr.Error()}}, }, { PropertyName: "test.path[1].nested", PropertyValue: "made-up", IsSliceElementError: true, - Errors: []*RuleError{{Message: includedErr.Error()}}, + Errors: []*govy.RuleError{{Message: includedErr.Error()}}, }, }, errs) }) diff --git a/pkg/govy/rules_test.go b/pkg/govy/rules_test.go index 42db91a..cf602f2 100644 --- a/pkg/govy/rules_test.go +++ b/pkg/govy/rules_test.go @@ -1,4 +1,4 @@ -package validation +package govy_test import ( "strconv" @@ -7,6 +7,10 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/nobl9/govy/internal" + "github.com/nobl9/govy/pkg/govy" + "github.com/nobl9/govy/pkg/rules" ) func TestPropertyRules(t *testing.T) { @@ -16,65 +20,65 @@ func TestPropertyRules(t *testing.T) { } t.Run("no predicates, no error", func(t *testing.T) { - r := For(func(m mockStruct) string { return "path" }). + r := govy.For(func(m mockStruct) string { return "path" }). WithName("test.path"). - Rules(NewSingleRule(func(v string) error { return nil })) + Rules(govy.NewSingleRule(func(v string) error { return nil })) err := r.Validate(mockStruct{}) assert.Nil(t, err) }) t.Run("no predicates, validate", func(t *testing.T) { expectedErr := errors.New("ops!") - r := For(func(m mockStruct) string { return "path" }). + r := govy.For(func(m mockStruct) string { return "path" }). WithName("test.path"). - Rules(NewSingleRule(func(v string) error { return expectedErr })) + Rules(govy.NewSingleRule(func(v string) error { return expectedErr })) errs := r.Validate(mockStruct{}) require.Len(t, errs, 1) - assert.Equal(t, &PropertyError{ + assert.Equal(t, &govy.PropertyError{ PropertyName: "test.path", PropertyValue: "path", - Errors: []*RuleError{{Message: expectedErr.Error()}}, + Errors: []*govy.RuleError{{Message: expectedErr.Error()}}, }, errs[0]) }) t.Run("predicate matches, don't validate", func(t *testing.T) { - r := For(func(m mockStruct) string { return "value" }). + r := govy.For(func(m mockStruct) string { return "value" }). WithName("test.path"). When(func(mockStruct) bool { return true }). When(func(mockStruct) bool { return true }). When(func(st mockStruct) bool { return st.Field == "" }). - Rules(NewSingleRule(func(v string) error { return errors.New("ops!") })) + Rules(govy.NewSingleRule(func(v string) error { return errors.New("ops!") })) err := r.Validate(mockStruct{Field: "something"}) assert.Nil(t, err) }) t.Run("multiple rules", func(t *testing.T) { err1 := errors.New("oh no!") - r := For(func(m mockStruct) string { return "value" }). + r := govy.For(func(m mockStruct) string { return "value" }). WithName("test.path"). - Rules(NewSingleRule(func(v string) error { return nil })). - Rules(NewSingleRule(func(v string) error { return err1 })). - Rules(NewSingleRule(func(v string) error { return nil })). - Rules(NewSingleRule(func(v string) error { - return NewPropertyError("nested", "nestedValue", &RuleError{ + Rules(govy.NewSingleRule(func(v string) error { return nil })). + Rules(govy.NewSingleRule(func(v string) error { return err1 })). + Rules(govy.NewSingleRule(func(v string) error { return nil })). + Rules(govy.NewSingleRule(func(v string) error { + return govy.NewPropertyError("nested", "nestedValue", &govy.RuleError{ Message: "property is required", - Code: ErrorCodeRequired, + Code: rules.ErrorCodeRequired, }) })) errs := r.Validate(mockStruct{}) require.Len(t, errs, 2) - assert.ElementsMatch(t, PropertyErrors{ - &PropertyError{ + assert.ElementsMatch(t, govy.PropertyErrors{ + &govy.PropertyError{ PropertyName: "test.path", PropertyValue: "value", - Errors: []*RuleError{{Message: err1.Error()}}, + Errors: []*govy.RuleError{{Message: err1.Error()}}, }, - &PropertyError{ + &govy.PropertyError{ PropertyName: "test.path.nested", PropertyValue: "nestedValue", - Errors: []*RuleError{{ + Errors: []*govy.RuleError{{ Message: "property is required", - Code: ErrorCodeRequired, + Code: rules.ErrorCodeRequired, }}, }, }, errs) @@ -82,17 +86,17 @@ func TestPropertyRules(t *testing.T) { t.Run("cascade mode stop", func(t *testing.T) { expectedErr := errors.New("oh no!") - r := For(func(m mockStruct) string { return "value" }). + r := govy.For(func(m mockStruct) string { return "value" }). WithName("test.path"). - Cascade(CascadeModeStop). - Rules(NewSingleRule(func(v string) error { return expectedErr })). - Rules(NewSingleRule(func(v string) error { return errors.New("no") })) + Cascade(govy.CascadeModeStop). + Rules(govy.NewSingleRule(func(v string) error { return expectedErr })). + Rules(govy.NewSingleRule(func(v string) error { return errors.New("no") })) errs := r.Validate(mockStruct{}) require.Len(t, errs, 1) - assert.Equal(t, &PropertyError{ + assert.Equal(t, &govy.PropertyError{ PropertyName: "test.path", PropertyValue: "value", - Errors: []*RuleError{{Message: expectedErr.Error()}}, + Errors: []*govy.RuleError{{Message: expectedErr.Error()}}, }, errs[0]) }) @@ -100,122 +104,122 @@ func TestPropertyRules(t *testing.T) { err1 := errors.New("oh no!") err2 := errors.New("included") err3 := errors.New("included again") - r := For(func(m mockStruct) mockStruct { return m }). + r := govy.For(func(m mockStruct) mockStruct { return m }). WithName("test.path"). - Rules(NewSingleRule(func(v mockStruct) error { return err1 })). - Include(New( - For(func(s mockStruct) string { return "value" }). + Rules(govy.NewSingleRule(func(v mockStruct) error { return err1 })). + Include(govy.New( + govy.For(func(s mockStruct) string { return "value" }). WithName("included"). - Rules(NewSingleRule(func(v string) error { return err2 })). - Rules(NewSingleRule(func(v string) error { - return NewPropertyError("nested", "nestedValue", err3) + Rules(govy.NewSingleRule(func(v string) error { return err2 })). + Rules(govy.NewSingleRule(func(v string) error { + return govy.NewPropertyError("nested", "nestedValue", err3) })), )) errs := r.Validate(mockStruct{}) require.Len(t, errs, 3) - assert.ElementsMatch(t, PropertyErrors{ + assert.ElementsMatch(t, govy.PropertyErrors{ { PropertyName: "test.path", - Errors: []*RuleError{{Message: err1.Error()}}, + Errors: []*govy.RuleError{{Message: err1.Error()}}, }, { PropertyName: "test.path.included", PropertyValue: "value", - Errors: []*RuleError{{Message: err2.Error()}}, + Errors: []*govy.RuleError{{Message: err2.Error()}}, }, { PropertyName: "test.path.included.nested", PropertyValue: "nestedValue", - Errors: []*RuleError{{Message: err3.Error()}}, + Errors: []*govy.RuleError{{Message: err3.Error()}}, }, }, errs) }) t.Run("get self", func(t *testing.T) { expectedErrs := errors.New("self error") - r := For(GetSelf[mockStruct]()). + r := govy.For(govy.GetSelf[mockStruct]()). WithName("test.path"). - Rules(NewSingleRule(func(v mockStruct) error { return expectedErrs })) + Rules(govy.NewSingleRule(func(v mockStruct) error { return expectedErrs })) object := mockStruct{Field: "this"} errs := r.Validate(object) require.Len(t, errs, 1) - assert.Equal(t, &PropertyError{ + assert.Equal(t, &govy.PropertyError{ PropertyName: "test.path", - PropertyValue: propertyValueString(object), - Errors: []*RuleError{{Message: expectedErrs.Error()}}, + PropertyValue: internal.PropertyValueString(object), + Errors: []*govy.RuleError{{Message: expectedErrs.Error()}}, }, errs[0]) }) t.Run("hide value", func(t *testing.T) { expectedErr := errors.New("oh no! here's the value: 'secret'") - r := For(func(m mockStruct) string { return "secret" }). + r := govy.For(func(m mockStruct) string { return "secret" }). WithName("test.path"). HideValue(). - Rules(NewSingleRule(func(v string) error { return expectedErr })) + Rules(govy.NewSingleRule(func(v string) error { return expectedErr })) errs := r.Validate(mockStruct{}) require.Len(t, errs, 1) - assert.Equal(t, &PropertyError{ + assert.Equal(t, &govy.PropertyError{ PropertyName: "test.path", PropertyValue: "", - Errors: []*RuleError{{Message: "oh no! here's the value: '[hidden]'"}}, + Errors: []*govy.RuleError{{Message: "oh no! here's the value: '[hidden]'"}}, }, errs[0]) }) } func TestForPointer(t *testing.T) { t.Run("nil pointer", func(t *testing.T) { - r := ForPointer(func(s *string) *string { return s }) - v, err := r.getter(nil) - assert.Equal(t, "", v) - assert.ErrorIs(t, err, emptyErr{}) + r := govy.ForPointer(func(s *string) *string { return s }). + Required() + errs := r.Validate(nil) + assert.NotNil(t, errs) }) t.Run("non nil pointer", func(t *testing.T) { - r := ForPointer(func(s *string) *string { return s }) + r := govy.ForPointer(func(s *string) *string { return s }). + Required() s := "this string" - v, err := r.getter(&s) - assert.Equal(t, s, v) - assert.NoError(t, err) + errs := r.Validate(&s) + assert.Nil(t, errs) }) } func TestRequiredAndOmitEmpty(t *testing.T) { t.Run("nil pointer", func(t *testing.T) { - rules := ForPointer(func(s *string) *string { return s }). - Rules(StringMinLength(10)) + r := govy.ForPointer(func(s *string) *string { return s }). + Rules(rules.StringMinLength(10)) t.Run("implicit omitEmpty", func(t *testing.T) { - err := rules.Validate(nil) + err := r.Validate(nil) assert.Nil(t, err) }) t.Run("explicit omitEmpty", func(t *testing.T) { - err := rules.OmitEmpty().Validate(nil) + err := r.OmitEmpty().Validate(nil) assert.Nil(t, err) }) t.Run("required", func(t *testing.T) { - errs := rules.Required().Validate(nil) + errs := r.Required().Validate(nil) assert.Len(t, errs, 1) - assert.True(t, HasErrorCode(errs, ErrorCodeRequired)) + assert.True(t, govy.HasErrorCode(errs, rules.ErrorCodeRequired)) }) }) t.Run("non empty pointer", func(t *testing.T) { - rules := ForPointer(func(s *string) *string { return s }). - Rules(StringMinLength(10)) + r := govy.ForPointer(func(s *string) *string { return s }). + Rules(rules.StringMinLength(10)) t.Run("validate", func(t *testing.T) { - errs := rules.Validate(ptr("")) + errs := r.Validate(ptr("")) assert.Len(t, errs, 1) - assert.True(t, HasErrorCode(errs, ErrorCodeStringMinLength)) + assert.True(t, govy.HasErrorCode(errs, rules.ErrorCodeStringMinLength)) }) t.Run("omitEmpty", func(t *testing.T) { - errs := rules.OmitEmpty().Validate(ptr("")) + errs := r.OmitEmpty().Validate(ptr("")) assert.Len(t, errs, 1) - assert.True(t, HasErrorCode(errs, ErrorCodeStringMinLength)) + assert.True(t, govy.HasErrorCode(errs, rules.ErrorCodeStringMinLength)) }) t.Run("required", func(t *testing.T) { - errs := rules.Required().Validate(ptr("")) + errs := r.Required().Validate(ptr("")) assert.Len(t, errs, 1) - assert.True(t, HasErrorCode(errs, ErrorCodeStringMinLength)) + assert.True(t, govy.HasErrorCode(errs, rules.ErrorCodeStringMinLength)) }) }) } @@ -223,68 +227,68 @@ func TestRequiredAndOmitEmpty(t *testing.T) { func TestTransform(t *testing.T) { t.Run("passes", func(t *testing.T) { getter := func(s string) string { return s } - transformed := Transform(getter, strconv.Atoi). - Rules(GreaterThan(122)) + transformed := govy.Transform(getter, strconv.Atoi). + Rules(rules.GreaterThan(122)) errs := transformed.Validate("123") assert.Empty(t, errs) }) t.Run("fails validation", func(t *testing.T) { getter := func(s string) string { return s } - transformed := Transform(getter, strconv.Atoi). + transformed := govy.Transform(getter, strconv.Atoi). WithName("prop"). - Rules(GreaterThan(123)) + Rules(rules.GreaterThan(123)) errs := transformed.Validate("123") assert.Len(t, errs, 1) - assert.True(t, HasErrorCode(errs, ErrorCodeGreaterThan)) + assert.True(t, govy.HasErrorCode(errs, rules.ErrorCodeGreaterThan)) }) t.Run("zero value with omitEmpty", func(t *testing.T) { getter := func(s string) string { return s } - transformed := Transform(getter, strconv.Atoi). + transformed := govy.Transform(getter, strconv.Atoi). WithName("prop"). OmitEmpty(). - Rules(GreaterThan(123)) + Rules(rules.GreaterThan(123)) errs := transformed.Validate("") assert.Empty(t, errs) }) t.Run("zero value with required", func(t *testing.T) { getter := func(s string) string { return s } - transformed := Transform(getter, strconv.Atoi). + transformed := govy.Transform(getter, strconv.Atoi). WithName("prop"). Required(). - Rules(GreaterThan(123)) + Rules(rules.GreaterThan(123)) errs := transformed.Validate("") assert.Len(t, errs, 1) - assert.True(t, HasErrorCode(errs, ErrorCodeRequired)) + assert.True(t, govy.HasErrorCode(errs, rules.ErrorCodeRequired)) }) t.Run("skip zero value", func(t *testing.T) { getter := func(s string) string { return s } - transformed := Transform(getter, strconv.Atoi). + transformed := govy.Transform(getter, strconv.Atoi). WithName("prop"). - Rules(GreaterThan(123)) + Rules(rules.GreaterThan(123)) errs := transformed.Validate("") assert.Len(t, errs, 1) - assert.True(t, HasErrorCode(errs, ErrorCodeGreaterThan)) + assert.True(t, govy.HasErrorCode(errs, rules.ErrorCodeGreaterThan)) }) t.Run("fails transformation", func(t *testing.T) { getter := func(s string) string { return s } - transformed := Transform(getter, strconv.Atoi). + transformed := govy.Transform(getter, strconv.Atoi). WithName("prop"). - Rules(GreaterThan(123)) + Rules(rules.GreaterThan(123)) errs := transformed.Validate("123z") assert.Len(t, errs, 1) assert.EqualError(t, errs, expectedErrorOutput(t, "property_error_transform.txt")) - assert.True(t, HasErrorCode(errs, ErrorCodeTransform)) + assert.True(t, govy.HasErrorCode(errs, govy.ErrorCodeTransform)) }) t.Run("fail transformation with hidden value", func(t *testing.T) { getter := func(s string) string { return s } - transformed := Transform(getter, strconv.Atoi). + transformed := govy.Transform(getter, strconv.Atoi). WithName("prop"). HideValue(). - Rules(GreaterThan(123)) + Rules(rules.GreaterThan(123)) errs := transformed.Validate("secret!") assert.Len(t, errs, 1) assert.EqualError(t, errs, expectedErrorOutput(t, "property_error_transform_with_hidden_value.txt")) - assert.True(t, HasErrorCode(errs, ErrorCodeTransform)) + assert.True(t, govy.HasErrorCode(errs, govy.ErrorCodeTransform)) }) } diff --git a/pkg/govy/validator.go b/pkg/govy/validator.go index 8a7f904..e80e3bd 100644 --- a/pkg/govy/validator.go +++ b/pkg/govy/validator.go @@ -1,4 +1,4 @@ -package validation +package govy type validatorI[S any] interface { Validate(s S) *ValidatorError diff --git a/pkg/govy/validator_test.go b/pkg/govy/validator_test.go index 8a90b1a..c96cfb0 100644 --- a/pkg/govy/validator_test.go +++ b/pkg/govy/validator_test.go @@ -1,4 +1,4 @@ -package validation +package govy_test import ( "testing" @@ -6,14 +6,16 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/nobl9/govy/pkg/govy" ) func TestValidator(t *testing.T) { t.Run("no errors", func(t *testing.T) { - r := New( - For(func(m mockValidatorStruct) string { return "test" }). + r := govy.New( + govy.For(func(m mockValidatorStruct) string { return "test" }). WithName("test"). - Rules(NewSingleRule(func(v string) error { return nil })), + Rules(govy.NewSingleRule(func(v string) error { return nil })), ) errs := r.Validate(mockValidatorStruct{}) assert.Nil(t, errs) @@ -22,29 +24,29 @@ func TestValidator(t *testing.T) { t.Run("errors", func(t *testing.T) { err1 := errors.New("1") err2 := errors.New("2") - r := New( - For(func(m mockValidatorStruct) string { return "test" }). + r := govy.New( + govy.For(func(m mockValidatorStruct) string { return "test" }). WithName("test"). - Rules(NewSingleRule(func(v string) error { return nil })), - For(func(m mockValidatorStruct) string { return "name" }). + Rules(govy.NewSingleRule(func(v string) error { return nil })), + govy.For(func(m mockValidatorStruct) string { return "name" }). WithName("test.name"). - Rules(NewSingleRule(func(v string) error { return err1 })), - For(func(m mockValidatorStruct) string { return "display" }). + Rules(govy.NewSingleRule(func(v string) error { return err1 })), + govy.For(func(m mockValidatorStruct) string { return "display" }). WithName("test.display"). - Rules(NewSingleRule(func(v string) error { return err2 })), + Rules(govy.NewSingleRule(func(v string) error { return err2 })), ) err := r.Validate(mockValidatorStruct{}) require.Len(t, err.Errors, 2) - assert.Equal(t, &ValidatorError{Errors: PropertyErrors{ - &PropertyError{ + assert.Equal(t, &govy.ValidatorError{Errors: govy.PropertyErrors{ + &govy.PropertyError{ PropertyName: "test.name", PropertyValue: "name", - Errors: []*RuleError{{Message: err1.Error()}}, + Errors: []*govy.RuleError{{Message: err1.Error()}}, }, - &PropertyError{ + &govy.PropertyError{ PropertyName: "test.display", PropertyValue: "display", - Errors: []*RuleError{{Message: err2.Error()}}, + Errors: []*govy.RuleError{{Message: err2.Error()}}, }, }}, err) }) @@ -52,10 +54,10 @@ func TestValidator(t *testing.T) { func TestValidatorWhen(t *testing.T) { t.Run("when condition is not met, don't validate", func(t *testing.T) { - r := New( - For(func(m mockValidatorStruct) string { return "test" }). + r := govy.New( + govy.For(func(m mockValidatorStruct) string { return "test" }). WithName("test"). - Rules(NewSingleRule(func(v string) error { return errors.New("test") })), + Rules(govy.NewSingleRule(func(v string) error { return errors.New("test") })), ). When(func(validatorStruct mockValidatorStruct) bool { return false }) @@ -63,30 +65,30 @@ func TestValidatorWhen(t *testing.T) { assert.Nil(t, errs) }) t.Run("when condition is met, validate", func(t *testing.T) { - r := New( - For(func(m mockValidatorStruct) string { return "test" }). + r := govy.New( + govy.For(func(m mockValidatorStruct) string { return "test" }). WithName("test"). - Rules(NewSingleRule(func(v string) error { return errors.New("test") })), + Rules(govy.NewSingleRule(func(v string) error { return errors.New("test") })), ). When(func(validatorStruct mockValidatorStruct) bool { return true }) errs := r.Validate(mockValidatorStruct{}) require.Len(t, errs.Errors, 1) - assert.Equal(t, &ValidatorError{Errors: PropertyErrors{ - &PropertyError{ + assert.Equal(t, &govy.ValidatorError{Errors: govy.PropertyErrors{ + &govy.PropertyError{ PropertyName: "test", PropertyValue: "test", - Errors: []*RuleError{{Message: "test"}}, + Errors: []*govy.RuleError{{Message: "test"}}, }, }}, errs) }) } func TestValidatorWithName(t *testing.T) { - r := New( - For(func(m mockValidatorStruct) string { return "test" }). + r := govy.New( + govy.For(func(m mockValidatorStruct) string { return "test" }). WithName("test"). - Rules(NewSingleRule(func(v string) error { return errors.New("test") })), + Rules(govy.NewSingleRule(func(v string) error { return errors.New("test") })), ).WithName("validator") err := r.Validate(mockValidatorStruct{}) diff --git a/pkg/govy/comparable.go b/pkg/rules/comparable.go similarity index 75% rename from pkg/govy/comparable.go rename to pkg/rules/comparable.go index 7797a33..94e1d50 100644 --- a/pkg/govy/comparable.go +++ b/pkg/rules/comparable.go @@ -1,15 +1,17 @@ -package validation +package rules import ( "fmt" "github.com/pkg/errors" "golang.org/x/exp/constraints" + + "github.com/nobl9/govy/pkg/govy" ) -func EqualTo[T comparable](compared T) SingleRule[T] { +func EqualTo[T comparable](compared T) govy.SingleRule[T] { msg := fmt.Sprintf(comparisonFmt, cmpEqualTo, compared) - return NewSingleRule(func(v T) error { + return govy.NewSingleRule(func(v T) error { if v != compared { return errors.New(msg) } @@ -19,9 +21,9 @@ func EqualTo[T comparable](compared T) SingleRule[T] { WithDescription(msg) } -func NotEqualTo[T comparable](compared T) SingleRule[T] { +func NotEqualTo[T comparable](compared T) govy.SingleRule[T] { msg := fmt.Sprintf(comparisonFmt, cmpNotEqualTo, compared) - return NewSingleRule(func(v T) error { + return govy.NewSingleRule(func(v T) error { if v == compared { return errors.New(msg) } @@ -31,31 +33,31 @@ func NotEqualTo[T comparable](compared T) SingleRule[T] { WithDescription(msg) } -func GreaterThan[T constraints.Ordered](n T) SingleRule[T] { +func GreaterThan[T constraints.Ordered](n T) govy.SingleRule[T] { return orderedComparisonRule(cmpGreaterThan, n). WithErrorCode(ErrorCodeGreaterThan) } -func GreaterThanOrEqualTo[T constraints.Ordered](n T) SingleRule[T] { +func GreaterThanOrEqualTo[T constraints.Ordered](n T) govy.SingleRule[T] { return orderedComparisonRule(cmpGreaterThanOrEqual, n). WithErrorCode(ErrorCodeGreaterThanOrEqualTo) } -func LessThan[T constraints.Ordered](n T) SingleRule[T] { +func LessThan[T constraints.Ordered](n T) govy.SingleRule[T] { return orderedComparisonRule(cmpLessThan, n). WithErrorCode(ErrorCodeLessThan) } -func LessThanOrEqualTo[T constraints.Ordered](n T) SingleRule[T] { +func LessThanOrEqualTo[T constraints.Ordered](n T) govy.SingleRule[T] { return orderedComparisonRule(cmpLessThanOrEqual, n). WithErrorCode(ErrorCodeLessThanOrEqualTo) } var comparisonFmt = "should be %s '%v'" -func orderedComparisonRule[T constraints.Ordered](op comparisonOperator, compared T) SingleRule[T] { +func orderedComparisonRule[T constraints.Ordered](op comparisonOperator, compared T) govy.SingleRule[T] { msg := fmt.Sprintf(comparisonFmt, op, compared) - return NewSingleRule(func(v T) error { + return govy.NewSingleRule(func(v T) error { var passed bool switch op { case cmpGreaterThan: diff --git a/pkg/govy/comparable_test.go b/pkg/rules/comparable_test.go similarity index 83% rename from pkg/govy/comparable_test.go rename to pkg/rules/comparable_test.go index 4c8cdbf..2c5a032 100644 --- a/pkg/govy/comparable_test.go +++ b/pkg/rules/comparable_test.go @@ -1,4 +1,4 @@ -package validation +package rules import ( "fmt" @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/nobl9/govy/pkg/govy" ) func TestEqualTo(t *testing.T) { @@ -17,7 +19,7 @@ func TestEqualTo(t *testing.T) { err := EqualTo(1.1).Validate(1.3) require.Error(t, err) assert.EqualError(t, err, "should be equal to '1.1'") - assert.True(t, HasErrorCode(err, ErrorCodeEqualTo)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeEqualTo)) }) } @@ -30,7 +32,7 @@ func TestNotEqualTo(t *testing.T) { err := NotEqualTo(1.1).Validate(1.1) require.Error(t, err) assert.EqualError(t, err, "should be not equal to '1.1'") - assert.True(t, HasErrorCode(err, ErrorCodeNotEqualTo)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeNotEqualTo)) }) } @@ -44,7 +46,7 @@ func TestGreaterThan(t *testing.T) { err := GreaterThan(n).Validate(v) require.Error(t, err) assert.EqualError(t, err, fmt.Sprintf("should be greater than '%v'", n)) - assert.True(t, HasErrorCode(err, ErrorCodeGreaterThan)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeGreaterThan)) } }) } @@ -60,7 +62,7 @@ func TestGreaterThanOrEqual(t *testing.T) { err := GreaterThanOrEqualTo(4).Validate(2) require.Error(t, err) assert.EqualError(t, err, "should be greater than or equal to '4'") - assert.True(t, HasErrorCode(err, ErrorCodeGreaterThanOrEqualTo)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeGreaterThanOrEqualTo)) }) } @@ -74,7 +76,7 @@ func TestLessThan(t *testing.T) { err := LessThan(n).Validate(v) require.Error(t, err) assert.EqualError(t, err, fmt.Sprintf("should be less than '%v'", n)) - assert.True(t, HasErrorCode(err, ErrorCodeLessThan)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeLessThan)) } }) } @@ -90,6 +92,6 @@ func TestLessThanOrEqual(t *testing.T) { err := LessThanOrEqualTo(2).Validate(4) require.Error(t, err) assert.EqualError(t, err, "should be less than or equal to '2'") - assert.True(t, HasErrorCode(err, ErrorCodeLessThanOrEqualTo)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeLessThanOrEqualTo)) }) } diff --git a/pkg/rules/doc.go b/pkg/rules/doc.go new file mode 100644 index 0000000..239629b --- /dev/null +++ b/pkg/rules/doc.go @@ -0,0 +1,2 @@ +// Package rules provides predefined rules for common validation scenarios. +package rules diff --git a/pkg/govy/duration.go b/pkg/rules/duration.go similarity index 61% rename from pkg/govy/duration.go rename to pkg/rules/duration.go index 0fe3f79..e884709 100644 --- a/pkg/govy/duration.go +++ b/pkg/rules/duration.go @@ -1,15 +1,17 @@ -package validation +package rules import ( "fmt" "time" "github.com/pkg/errors" + + "github.com/nobl9/govy/pkg/govy" ) -func DurationPrecision(precision time.Duration) SingleRule[time.Duration] { +func DurationPrecision(precision time.Duration) govy.SingleRule[time.Duration] { msg := fmt.Sprintf("duration must be defined with %s precision", precision) - return NewSingleRule(func(v time.Duration) error { + return govy.NewSingleRule(func(v time.Duration) error { if v.Nanoseconds()%int64(precision) != 0 { return errors.New(msg) } diff --git a/pkg/govy/duration_test.go b/pkg/rules/duration_test.go similarity index 73% rename from pkg/govy/duration_test.go rename to pkg/rules/duration_test.go index ca4b9d1..b8f04e1 100644 --- a/pkg/govy/duration_test.go +++ b/pkg/rules/duration_test.go @@ -1,10 +1,12 @@ -package validation +package rules import ( "testing" "time" "github.com/stretchr/testify/assert" + + "github.com/nobl9/govy/pkg/govy" ) func TestDurationPrecision(t *testing.T) { @@ -30,19 +32,19 @@ func TestDurationPrecision(t *testing.T) { name: "invalid precision 1m1s", duration: time.Minute + time.Second, precision: time.Minute, - expected: NewRuleError("duration must be defined with 1m0s precision", ErrorCodeDurationPrecision), + expected: govy.NewRuleError("duration must be defined with 1m0s precision", ErrorCodeDurationPrecision), }, { name: "invalid precision", duration: time.Duration(123456), precision: 10 * time.Nanosecond, - expected: NewRuleError("duration must be defined with 10ns precision", ErrorCodeDurationPrecision), + expected: govy.NewRuleError("duration must be defined with 10ns precision", ErrorCodeDurationPrecision), }, { name: "minute precision", duration: time.Duration(123456), precision: time.Minute, - expected: NewRuleError("duration must be defined with 1m0s precision", ErrorCodeDurationPrecision), + expected: govy.NewRuleError("duration must be defined with 1m0s precision", ErrorCodeDurationPrecision), }, } diff --git a/pkg/rules/error_codes.go b/pkg/rules/error_codes.go new file mode 100644 index 0000000..9f3d166 --- /dev/null +++ b/pkg/rules/error_codes.go @@ -0,0 +1,42 @@ +package rules + +import ( + "github.com/nobl9/govy/internal" + "github.com/nobl9/govy/pkg/govy" +) + +const ( + ErrorCodeRequired govy.ErrorCode = internal.RequiredErrorCodeString + ErrorCodeForbidden govy.ErrorCode = "forbidden" + ErrorCodeEqualTo govy.ErrorCode = "equal_to" + ErrorCodeNotEqualTo govy.ErrorCode = "not_equal_to" + ErrorCodeGreaterThan govy.ErrorCode = "greater_than" + ErrorCodeGreaterThanOrEqualTo govy.ErrorCode = "greater_than_or_equal_to" + ErrorCodeLessThan govy.ErrorCode = "less_than" + ErrorCodeLessThanOrEqualTo govy.ErrorCode = "less_than_or_equal_to" + ErrorCodeStringNotEmpty govy.ErrorCode = "string_not_empty" + ErrorCodeStringMatchRegexp govy.ErrorCode = "string_match_regexp" + ErrorCodeStringDenyRegexp govy.ErrorCode = "string_deny_regexp" + ErrorCodeStringDescription govy.ErrorCode = "string_description" + ErrorCodeStringIsDNSSubdomain govy.ErrorCode = "string_is_dns_subdomain" + ErrorCodeStringASCII govy.ErrorCode = "string_ascii" + ErrorCodeStringURL govy.ErrorCode = "string_url" + ErrorCodeStringUUID govy.ErrorCode = "string_uuid" + ErrorCodeStringJSON govy.ErrorCode = "string_json" + ErrorCodeStringContains govy.ErrorCode = "string_contains" + ErrorCodeStringStartsWith govy.ErrorCode = "string_starts_with" + ErrorCodeStringLength govy.ErrorCode = "string_length" + ErrorCodeStringMinLength govy.ErrorCode = "string_min_length" + ErrorCodeStringMaxLength govy.ErrorCode = "string_max_length" + ErrorCodeSliceLength govy.ErrorCode = "slice_length" + ErrorCodeSliceMinLength govy.ErrorCode = "slice_min_length" + ErrorCodeSliceMaxLength govy.ErrorCode = "slice_max_length" + ErrorCodeMapLength govy.ErrorCode = "map_length" + ErrorCodeMapMinLength govy.ErrorCode = "map_min_length" + ErrorCodeMapMaxLength govy.ErrorCode = "map_max_length" + ErrorCodeOneOf govy.ErrorCode = "one_of" + ErrorCodeMutuallyExclusive govy.ErrorCode = "mutually_exclusive" + ErrorCodeSliceUnique govy.ErrorCode = "slice_unique" + ErrorCodeURL govy.ErrorCode = "url" + ErrorCodeDurationPrecision govy.ErrorCode = "duration_precision" +) diff --git a/pkg/rules/forbidden.go b/pkg/rules/forbidden.go new file mode 100644 index 0000000..73c8981 --- /dev/null +++ b/pkg/rules/forbidden.go @@ -0,0 +1,20 @@ +package rules + +import ( + "github.com/pkg/errors" + + "github.com/nobl9/govy/internal" + "github.com/nobl9/govy/pkg/govy" +) + +func Forbidden[T any]() govy.SingleRule[T] { + msg := "property is forbidden" + return govy.NewSingleRule(func(v T) error { + if internal.IsEmptyFunc(v) { + return nil + } + return errors.New(msg) + }). + WithErrorCode(ErrorCodeForbidden). + WithDescription(msg) +} diff --git a/pkg/govy/forbidden_test.go b/pkg/rules/forbidden_test.go similarity index 78% rename from pkg/govy/forbidden_test.go rename to pkg/rules/forbidden_test.go index 4e11c78..578bf5d 100644 --- a/pkg/govy/forbidden_test.go +++ b/pkg/rules/forbidden_test.go @@ -1,10 +1,12 @@ -package validation +package rules import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/nobl9/govy/pkg/govy" ) func TestForbidden(t *testing.T) { @@ -16,6 +18,6 @@ func TestForbidden(t *testing.T) { err := Forbidden[string]().Validate("test") require.Error(t, err) assert.EqualError(t, err, "property is forbidden") - assert.True(t, HasErrorCode(err, ErrorCodeForbidden)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeForbidden)) }) } diff --git a/pkg/govy/length.go b/pkg/rules/length.go similarity index 68% rename from pkg/govy/length.go rename to pkg/rules/length.go index 97e02f2..88e5735 100644 --- a/pkg/govy/length.go +++ b/pkg/rules/length.go @@ -1,15 +1,17 @@ -package validation +package rules import ( "fmt" "unicode/utf8" "github.com/pkg/errors" + + "github.com/nobl9/govy/pkg/govy" ) -func StringLength(lower, upper int) SingleRule[string] { +func StringLength(lower, upper int) govy.SingleRule[string] { msg := fmt.Sprintf("length must be between %d and %d", lower, upper) - return NewSingleRule(func(v string) error { + return govy.NewSingleRule(func(v string) error { length := utf8.RuneCountInString(v) if length < lower || length > upper { return errors.New(msg) @@ -20,9 +22,9 @@ func StringLength(lower, upper int) SingleRule[string] { WithDescription(msg) } -func StringMinLength(limit int) SingleRule[string] { +func StringMinLength(limit int) govy.SingleRule[string] { msg := fmt.Sprintf("length must be %s %d", cmpGreaterThanOrEqual, limit) - return NewSingleRule(func(v string) error { + return govy.NewSingleRule(func(v string) error { length := utf8.RuneCountInString(v) if length < limit { return errors.New(msg) @@ -33,9 +35,9 @@ func StringMinLength(limit int) SingleRule[string] { WithDescription(msg) } -func StringMaxLength(limit int) SingleRule[string] { +func StringMaxLength(limit int) govy.SingleRule[string] { msg := fmt.Sprintf("length must be %s %d", cmpLessThanOrEqual, limit) - return NewSingleRule(func(v string) error { + return govy.NewSingleRule(func(v string) error { length := utf8.RuneCountInString(v) if length > limit { return errors.New(msg) @@ -46,9 +48,9 @@ func StringMaxLength(limit int) SingleRule[string] { WithDescription(msg) } -func SliceLength[S ~[]E, E any](lower, upper int) SingleRule[S] { +func SliceLength[S ~[]E, E any](lower, upper int) govy.SingleRule[S] { msg := fmt.Sprintf("length must be between %d and %d", lower, upper) - return NewSingleRule(func(v S) error { + return govy.NewSingleRule(func(v S) error { length := len(v) if length < lower || length > upper { return errors.New(msg) @@ -59,9 +61,9 @@ func SliceLength[S ~[]E, E any](lower, upper int) SingleRule[S] { WithDescription(msg) } -func SliceMinLength[S ~[]E, E any](limit int) SingleRule[S] { +func SliceMinLength[S ~[]E, E any](limit int) govy.SingleRule[S] { msg := fmt.Sprintf("length must be %s %d", cmpGreaterThanOrEqual, limit) - return NewSingleRule(func(v S) error { + return govy.NewSingleRule(func(v S) error { length := len(v) if length < limit { return errors.New(msg) @@ -72,9 +74,9 @@ func SliceMinLength[S ~[]E, E any](limit int) SingleRule[S] { WithDescription(msg) } -func SliceMaxLength[S ~[]E, E any](limit int) SingleRule[S] { +func SliceMaxLength[S ~[]E, E any](limit int) govy.SingleRule[S] { msg := fmt.Sprintf("length must be %s %d", cmpLessThanOrEqual, limit) - return NewSingleRule(func(v S) error { + return govy.NewSingleRule(func(v S) error { length := len(v) if length > limit { return errors.New(msg) @@ -85,9 +87,9 @@ func SliceMaxLength[S ~[]E, E any](limit int) SingleRule[S] { WithDescription(msg) } -func MapLength[M ~map[K]V, K comparable, V any](lower, upper int) SingleRule[M] { +func MapLength[M ~map[K]V, K comparable, V any](lower, upper int) govy.SingleRule[M] { msg := fmt.Sprintf("length must be between %d and %d", lower, upper) - return NewSingleRule(func(v M) error { + return govy.NewSingleRule(func(v M) error { length := len(v) if length < lower || length > upper { return errors.New(msg) @@ -98,9 +100,9 @@ func MapLength[M ~map[K]V, K comparable, V any](lower, upper int) SingleRule[M] WithDescription(msg) } -func MapMinLength[M ~map[K]V, K comparable, V any](limit int) SingleRule[M] { +func MapMinLength[M ~map[K]V, K comparable, V any](limit int) govy.SingleRule[M] { msg := fmt.Sprintf("length must be %s %d", cmpGreaterThanOrEqual, limit) - return NewSingleRule(func(v M) error { + return govy.NewSingleRule(func(v M) error { length := len(v) if length < limit { return errors.New(msg) @@ -111,9 +113,9 @@ func MapMinLength[M ~map[K]V, K comparable, V any](limit int) SingleRule[M] { WithDescription(msg) } -func MapMaxLength[M ~map[K]V, K comparable, V any](limit int) SingleRule[M] { +func MapMaxLength[M ~map[K]V, K comparable, V any](limit int) govy.SingleRule[M] { msg := fmt.Sprintf("length must be %s %d", cmpLessThanOrEqual, limit) - return NewSingleRule(func(v M) error { + return govy.NewSingleRule(func(v M) error { length := len(v) if length > limit { return errors.New(msg) diff --git a/pkg/govy/length_test.go b/pkg/rules/length_test.go similarity index 85% rename from pkg/govy/length_test.go rename to pkg/rules/length_test.go index 6212efc..7a408a8 100644 --- a/pkg/govy/length_test.go +++ b/pkg/rules/length_test.go @@ -1,4 +1,4 @@ -package validation +package rules import ( "fmt" @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/nobl9/govy/pkg/govy" ) func TestStringLength(t *testing.T) { @@ -21,7 +23,7 @@ func TestStringLength(t *testing.T) { err := StringLength(min, max).Validate("test") require.Error(t, err) assert.EqualError(t, err, fmt.Sprintf("length must be between %d and %d", min, max)) - assert.True(t, HasErrorCode(err, ErrorCodeStringLength)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringLength)) } }) } @@ -35,7 +37,7 @@ func TestStringMinLength(t *testing.T) { err := StringMinLength(5).Validate("test") require.Error(t, err) assert.EqualError(t, err, "length must be greater than or equal to 5") - assert.True(t, HasErrorCode(err, ErrorCodeStringMinLength)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringMinLength)) }) } @@ -48,7 +50,7 @@ func TestStringMaxLength(t *testing.T) { err := StringMaxLength(3).Validate("test") require.Error(t, err) assert.EqualError(t, err, "length must be less than or equal to 3") - assert.True(t, HasErrorCode(err, ErrorCodeStringMaxLength)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringMaxLength)) }) } @@ -65,7 +67,7 @@ func TestSliceLength(t *testing.T) { err := SliceLength[[]string](min, max).Validate([]string{"test", "test"}) require.Error(t, err) assert.EqualError(t, err, fmt.Sprintf("length must be between %d and %d", min, max)) - assert.True(t, HasErrorCode(err, ErrorCodeSliceLength)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeSliceLength)) } }) } @@ -79,7 +81,7 @@ func TestSliceMinLength(t *testing.T) { err := SliceMinLength[[]string](2).Validate([]string{"test"}) require.Error(t, err) assert.EqualError(t, err, "length must be greater than or equal to 2") - assert.True(t, HasErrorCode(err, ErrorCodeSliceMinLength)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeSliceMinLength)) }) } @@ -92,7 +94,7 @@ func TestSliceMaxLength(t *testing.T) { err := SliceMaxLength[[]string](1).Validate([]string{"1", "2"}) require.Error(t, err) assert.EqualError(t, err, "length must be less than or equal to 1") - assert.True(t, HasErrorCode(err, ErrorCodeSliceMaxLength)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeSliceMaxLength)) }) } @@ -109,7 +111,7 @@ func TestMapLength(t *testing.T) { err := MapLength[map[string]string](min, max).Validate(map[string]string{"a": "b", "c": "d"}) require.Error(t, err) assert.EqualError(t, err, fmt.Sprintf("length must be between %d and %d", min, max)) - assert.True(t, HasErrorCode(err, ErrorCodeMapLength)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeMapLength)) } }) } @@ -123,7 +125,7 @@ func TestMapMinLength(t *testing.T) { err := MapMinLength[map[string]string](2).Validate(map[string]string{"a": "b"}) require.Error(t, err) assert.EqualError(t, err, "length must be greater than or equal to 2") - assert.True(t, HasErrorCode(err, ErrorCodeMapMinLength)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeMapMinLength)) }) } @@ -136,6 +138,6 @@ func TestMapMaxLength(t *testing.T) { err := MapMaxLength[map[string]string](1).Validate(map[string]string{"a": "b", "c": "d"}) require.Error(t, err) assert.EqualError(t, err, "length must be less than or equal to 1") - assert.True(t, HasErrorCode(err, ErrorCodeMapMaxLength)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeMapMaxLength)) }) } diff --git a/pkg/govy/one_of.go b/pkg/rules/one_of.go similarity index 84% rename from pkg/govy/one_of.go rename to pkg/rules/one_of.go index 6a71870..9f5630d 100644 --- a/pkg/govy/one_of.go +++ b/pkg/rules/one_of.go @@ -1,4 +1,4 @@ -package validation +package rules import ( "fmt" @@ -7,10 +7,13 @@ import ( "github.com/pkg/errors" "golang.org/x/exp/maps" "golang.org/x/exp/slices" + + "github.com/nobl9/govy/internal" + "github.com/nobl9/govy/pkg/govy" ) -func OneOf[T comparable](values ...T) SingleRule[T] { - return NewSingleRule(func(v T) error { +func OneOf[T comparable](values ...T) govy.SingleRule[T] { + return govy.NewSingleRule(func(v T) error { for i := range values { if v == values[i] { return nil @@ -29,12 +32,12 @@ func OneOf[T comparable](values ...T) SingleRule[T] { // MutuallyExclusive checks if properties are mutually exclusive. // This means, exactly one of the properties can be provided. // If required is true, then a single non-empty property is required. -func MutuallyExclusive[S any](required bool, getters map[string]func(s S) any) SingleRule[S] { - return NewSingleRule(func(s S) error { +func MutuallyExclusive[S any](required bool, getters map[string]func(s S) any) govy.SingleRule[S] { + return govy.NewSingleRule(func(s S) error { var nonEmpty []string for name, getter := range getters { v := getter(s) - if isEmptyFunc(v) { + if internal.IsEmptyFunc(v) { continue } nonEmpty = append(nonEmpty, name) diff --git a/pkg/govy/one_of_test.go b/pkg/rules/one_of_test.go similarity index 86% rename from pkg/govy/one_of_test.go rename to pkg/rules/one_of_test.go index d346c61..c339a05 100644 --- a/pkg/govy/one_of_test.go +++ b/pkg/rules/one_of_test.go @@ -1,10 +1,12 @@ -package validation +package rules import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/nobl9/govy/pkg/govy" ) func TestOneOf(t *testing.T) { @@ -16,7 +18,7 @@ func TestOneOf(t *testing.T) { err := OneOf("this", "that").Validate("those") require.Error(t, err) assert.EqualError(t, err, "must be one of [this, that]") - assert.True(t, HasErrorCode(err, ErrorCodeOneOf)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeOneOf)) }) } @@ -56,7 +58,7 @@ func TestMutuallyExclusive(t *testing.T) { Transfer: ptr("2$"), }) assert.EqualError(t, err, "[Card, Transfer] properties are mutually exclusive, provide only one of them") - assert.True(t, HasErrorCode(err, ErrorCodeMutuallyExclusive)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeMutuallyExclusive)) } }) t.Run("fails, multiple conflicts", func(t *testing.T) { @@ -67,7 +69,7 @@ func TestMutuallyExclusive(t *testing.T) { Transfer: ptr("2$"), }) assert.EqualError(t, err, "[Card, Cash, Transfer] properties are mutually exclusive, provide only one of them") - assert.True(t, HasErrorCode(err, ErrorCodeMutuallyExclusive)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeMutuallyExclusive)) } }) t.Run("required fails", func(t *testing.T) { @@ -77,6 +79,8 @@ func TestMutuallyExclusive(t *testing.T) { Transfer: nil, }) assert.EqualError(t, err, "one of [Card, Cash, Transfer] properties must be set, none was provided") - assert.True(t, HasErrorCode(err, ErrorCodeMutuallyExclusive)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeMutuallyExclusive)) }) } + +func ptr[T any](v T) *T { return &v } diff --git a/pkg/rules/required.go b/pkg/rules/required.go new file mode 100644 index 0000000..3a1c503 --- /dev/null +++ b/pkg/rules/required.go @@ -0,0 +1,20 @@ +package rules + +import ( + "github.com/nobl9/govy/internal" + "github.com/nobl9/govy/pkg/govy" +) + +func Required[T any]() govy.SingleRule[T] { + return govy.NewSingleRule(func(v T) error { + if internal.IsEmptyFunc(v) { + return govy.NewRuleError( + internal.RequiredErrorMessage, + ErrorCodeRequired, + ) + } + return nil + }). + WithErrorCode(ErrorCodeRequired). + WithDescription("property is required") +} diff --git a/pkg/govy/required_test.go b/pkg/rules/required_test.go similarity index 83% rename from pkg/govy/required_test.go rename to pkg/rules/required_test.go index 921043e..aa096da 100644 --- a/pkg/govy/required_test.go +++ b/pkg/rules/required_test.go @@ -1,10 +1,12 @@ -package validation +package rules import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/nobl9/govy/pkg/govy" ) func TestRequired(t *testing.T) { @@ -31,7 +33,7 @@ func TestRequired(t *testing.T) { } { err := Required[any]().Validate(v) require.Error(t, err) - assert.True(t, HasErrorCode(err, ErrorCodeRequired)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeRequired)) } }) } diff --git a/pkg/govy/string.go b/pkg/rules/string.go similarity index 78% rename from pkg/govy/string.go rename to pkg/rules/string.go index 351c627..2c597c9 100644 --- a/pkg/govy/string.go +++ b/pkg/rules/string.go @@ -1,4 +1,4 @@ -package validation +package rules import ( "encoding/json" @@ -8,11 +8,13 @@ import ( "strings" "github.com/pkg/errors" + + "github.com/nobl9/govy/pkg/govy" ) -func StringNotEmpty() SingleRule[string] { +func StringNotEmpty() govy.SingleRule[string] { msg := "string cannot be empty" - return NewSingleRule(func(s string) error { + return govy.NewSingleRule(func(s string) error { if len(strings.TrimSpace(s)) == 0 { return errors.New(msg) } @@ -22,12 +24,12 @@ func StringNotEmpty() SingleRule[string] { WithDescription(msg) } -func StringMatchRegexp(re *regexp.Regexp, examples ...string) SingleRule[string] { +func StringMatchRegexp(re *regexp.Regexp, examples ...string) govy.SingleRule[string] { msg := fmt.Sprintf("string must match regular expression: '%s'", re.String()) if len(examples) > 0 { msg += " " + prettyExamples(examples) } - return NewSingleRule(func(s string) error { + return govy.NewSingleRule(func(s string) error { if !re.MatchString(s) { return errors.New(msg) } @@ -37,12 +39,12 @@ func StringMatchRegexp(re *regexp.Regexp, examples ...string) SingleRule[string] WithDescription(msg) } -func StringDenyRegexp(re *regexp.Regexp, examples ...string) SingleRule[string] { +func StringDenyRegexp(re *regexp.Regexp, examples ...string) govy.SingleRule[string] { msg := fmt.Sprintf("string must not match regular expression: '%s'", re.String()) if len(examples) > 0 { msg += " " + prettyExamples(examples) } - return NewSingleRule(func(s string) error { + return govy.NewSingleRule(func(s string) error { if re.MatchString(s) { return errors.New(msg) } @@ -54,8 +56,8 @@ func StringDenyRegexp(re *regexp.Regexp, examples ...string) SingleRule[string] var dns1123SubdomainRegexp = regexp.MustCompile("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") -func StringIsDNSSubdomain() RuleSet[string] { - return NewRuleSet( +func StringIsDNSSubdomain() govy.RuleSet[string] { + return govy.NewRuleSet( StringLength(1, 63), StringMatchRegexp(dns1123SubdomainRegexp, "my-name", "123-abc"). WithDetails("a DNS-1123 compliant name must consist of lower case alphanumeric characters or '-',"+ @@ -66,7 +68,7 @@ func StringIsDNSSubdomain() RuleSet[string] { var validUUIDRegex = regexp. MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") -func StringUUID() SingleRule[string] { +func StringUUID() govy.SingleRule[string] { return StringMatchRegexp(validUUIDRegex, "00000000-0000-0000-0000-000000000000", "e190c630-8873-11ee-b9d1-0242ac120002", @@ -77,16 +79,16 @@ func StringUUID() SingleRule[string] { var asciiRegexp = regexp.MustCompile("^[\x00-\x7F]*$") -func StringASCII() SingleRule[string] { +func StringASCII() govy.SingleRule[string] { return StringMatchRegexp(asciiRegexp).WithErrorCode(ErrorCodeStringASCII) } -func StringDescription() SingleRule[string] { +func StringDescription() govy.SingleRule[string] { return StringLength(0, 1050).WithErrorCode(ErrorCodeStringDescription) } -func StringURL() SingleRule[string] { - return NewSingleRule(func(v string) error { +func StringURL() govy.SingleRule[string] { + return govy.NewSingleRule(func(v string) error { u, err := url.Parse(v) if err != nil { return errors.Wrap(err, "failed to parse URL") @@ -97,9 +99,9 @@ func StringURL() SingleRule[string] { WithDescription(urlDescription) } -func StringJSON() SingleRule[string] { +func StringJSON() govy.SingleRule[string] { msg := "string must be a valid JSON" - return NewSingleRule(func(s string) error { + return govy.NewSingleRule(func(s string) error { if !json.Valid([]byte(s)) { return errors.New(msg) } @@ -109,9 +111,9 @@ func StringJSON() SingleRule[string] { WithDescription(msg) } -func StringContains(substrings ...string) SingleRule[string] { +func StringContains(substrings ...string) govy.SingleRule[string] { msg := "string must contain the following substrings: " + prettyStringList(substrings) - return NewSingleRule(func(s string) error { + return govy.NewSingleRule(func(s string) error { matched := true for _, substr := range substrings { if !strings.Contains(s, substr) { @@ -128,14 +130,14 @@ func StringContains(substrings ...string) SingleRule[string] { WithDescription(msg) } -func StringStartsWith(prefixes ...string) SingleRule[string] { +func StringStartsWith(prefixes ...string) govy.SingleRule[string] { var msg string if len(prefixes) == 1 { msg = fmt.Sprintf("string must start with '%s' prefix", prefixes[0]) } else { msg = "string must start with one of the following prefixes: " + prettyStringList(prefixes) } - return NewSingleRule(func(s string) error { + return govy.NewSingleRule(func(s string) error { matched := false for _, prefix := range prefixes { if strings.HasPrefix(s, prefix) { diff --git a/pkg/govy/string_test.go b/pkg/rules/string_test.go similarity index 82% rename from pkg/govy/string_test.go rename to pkg/rules/string_test.go index 886d7b0..ad91ec8 100644 --- a/pkg/govy/string_test.go +++ b/pkg/rules/string_test.go @@ -1,4 +1,4 @@ -package validation +package rules import ( "regexp" @@ -6,6 +6,9 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/nobl9/govy/internal" + "github.com/nobl9/govy/pkg/govy" ) func TestStringNotEmpty(t *testing.T) { @@ -16,7 +19,7 @@ func TestStringNotEmpty(t *testing.T) { t.Run("fails", func(t *testing.T) { err := StringNotEmpty().Validate(" ") assert.Error(t, err) - assert.True(t, HasErrorCode(err, ErrorCodeStringNotEmpty)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringNotEmpty)) }) } @@ -29,12 +32,12 @@ func TestStringMatchRegexp(t *testing.T) { t.Run("fails", func(t *testing.T) { err := StringMatchRegexp(re).Validate("cd") assert.EqualError(t, err, "string must match regular expression: '[ab]+'") - assert.True(t, HasErrorCode(err, ErrorCodeStringMatchRegexp)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringMatchRegexp)) }) t.Run("examples output", func(t *testing.T) { err := StringMatchRegexp(re, "ab", "a", "b").Validate("cd") assert.EqualError(t, err, "string must match regular expression: '[ab]+' (e.g. 'ab', 'a', 'b')") - assert.True(t, HasErrorCode(err, ErrorCodeStringMatchRegexp)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringMatchRegexp)) }) } @@ -47,12 +50,12 @@ func TestStringDenyRegexp(t *testing.T) { t.Run("fails", func(t *testing.T) { err := StringDenyRegexp(re).Validate("ab") assert.EqualError(t, err, "string must not match regular expression: '[ab]+'") - assert.True(t, HasErrorCode(err, ErrorCodeStringDenyRegexp)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringDenyRegexp)) }) t.Run("examples output", func(t *testing.T) { err := StringDenyRegexp(re, "ab", "a", "b").Validate("ab") assert.EqualError(t, err, "string must not match regular expression: '[ab]+' (e.g. 'ab', 'a', 'b')") - assert.True(t, HasErrorCode(err, ErrorCodeStringDenyRegexp)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringDenyRegexp)) }) } @@ -83,8 +86,8 @@ func TestStringIsDNSSubdomain(t *testing.T) { } { err := StringIsDNSSubdomain().Validate(input) assert.Error(t, err) - for _, e := range err.(ruleSetError) { - assert.True(t, HasErrorCode(e, ErrorCodeStringIsDNSSubdomain)) + for _, e := range err.(internal.RuleSetError) { + assert.True(t, govy.HasErrorCode(e, ErrorCodeStringIsDNSSubdomain)) } } }) @@ -114,7 +117,7 @@ func TestStringASCII(t *testing.T) { } { err := StringASCII().Validate(input) assert.Error(t, err) - assert.True(t, HasErrorCode(err, ErrorCodeStringASCII)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringASCII)) } }) } @@ -141,7 +144,7 @@ func TestStringUUID(t *testing.T) { } { err := StringUUID().Validate(input) assert.Error(t, err) - assert.True(t, HasErrorCode(err, ErrorCodeStringUUID)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringUUID)) } }) } @@ -154,7 +157,7 @@ func TestStringDescription(t *testing.T) { t.Run("fails", func(t *testing.T) { err := StringDescription().Validate(strings.Repeat("l", 1051)) assert.Error(t, err) - assert.True(t, HasErrorCode(err, ErrorCodeStringDescription)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringDescription)) }) } @@ -169,7 +172,7 @@ func TestStringIsURL(t *testing.T) { for _, input := range invalidURLs { err := StringURL().Validate(input) assert.Error(t, err) - assert.True(t, HasErrorCode(err, ErrorCodeStringURL)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringURL)) } }) } @@ -182,7 +185,7 @@ func TestStringJSON(t *testing.T) { t.Run("fails", func(t *testing.T) { err := StringJSON().Validate(`{]}`) assert.Error(t, err) - assert.True(t, HasErrorCode(err, ErrorCodeStringJSON)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringJSON)) }) } @@ -195,7 +198,7 @@ func TestStringContains(t *testing.T) { err := StringContains("th", "ht").Validate("one") assert.Error(t, err) assert.EqualError(t, err, "string must contain the following substrings: 'th', 'ht'") - assert.True(t, HasErrorCode(err, ErrorCodeStringContains)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringContains)) }) } @@ -213,12 +216,12 @@ func TestStringStartsWith(t *testing.T) { err := StringStartsWith("th").Validate("one") assert.Error(t, err) assert.EqualError(t, err, "string must start with 'th' prefix") - assert.True(t, HasErrorCode(err, ErrorCodeStringStartsWith)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringStartsWith)) }) t.Run("fails with multiple prefixes", func(t *testing.T) { err := StringStartsWith("th", "ht").Validate("one") assert.Error(t, err) assert.EqualError(t, err, "string must start with one of the following prefixes: 'th', 'ht'") - assert.True(t, HasErrorCode(err, ErrorCodeStringStartsWith)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeStringStartsWith)) }) } diff --git a/pkg/govy/unique.go b/pkg/rules/unique.go similarity index 89% rename from pkg/govy/unique.go rename to pkg/rules/unique.go index 9c058f0..cbfae3c 100644 --- a/pkg/govy/unique.go +++ b/pkg/rules/unique.go @@ -1,8 +1,10 @@ -package validation +package rules import ( "fmt" "strings" + + "github.com/nobl9/govy/pkg/govy" ) // HashFunction accepts a value and returns a comparable hash. @@ -17,8 +19,8 @@ func SelfHashFunc[H comparable]() HashFunction[H, H] { // SliceUnique validates that a slice contains unique elements based on a provided HashFunction. // You can optionally specify constraints which will be included in the error message to further // clarify the reason for breaking uniqueness. -func SliceUnique[S []V, V any, H comparable](hashFunc HashFunction[V, H], constraints ...string) SingleRule[S] { - return NewSingleRule(func(slice S) error { +func SliceUnique[S []V, V any, H comparable](hashFunc HashFunction[V, H], constraints ...string) govy.SingleRule[S] { + return govy.NewSingleRule(func(slice S) error { unique := make(map[H]int) for i := range slice { hash := hashFunc(slice[i]) diff --git a/pkg/govy/unique_test.go b/pkg/rules/unique_test.go similarity index 83% rename from pkg/govy/unique_test.go rename to pkg/rules/unique_test.go index 657672b..9c759c3 100644 --- a/pkg/govy/unique_test.go +++ b/pkg/rules/unique_test.go @@ -1,10 +1,12 @@ -package validation +package rules import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/nobl9/govy/pkg/govy" ) func TestSliceUnique(t *testing.T) { @@ -16,7 +18,7 @@ func TestSliceUnique(t *testing.T) { err := SliceUnique(SelfHashFunc[string]()).Validate([]string{"a", "b", "c", "b"}) require.Error(t, err) assert.EqualError(t, err, "elements are not unique, index 1 collides with index 3") - assert.True(t, HasErrorCode(err, ErrorCodeSliceUnique)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeSliceUnique)) }) t.Run("fails with constraint", func(t *testing.T) { err := SliceUnique(SelfHashFunc[string](), "values must be unique"). @@ -24,7 +26,7 @@ func TestSliceUnique(t *testing.T) { require.Error(t, err) assert.EqualError(t, err, "elements are not unique, index 1 collides with index 3 "+ "based on constraints: values must be unique") - assert.True(t, HasErrorCode(err, ErrorCodeSliceUnique)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeSliceUnique)) }) t.Run("fails with constraints", func(t *testing.T) { err := SliceUnique(SelfHashFunc[string](), "constraint 1", "constraint 2"). @@ -32,6 +34,6 @@ func TestSliceUnique(t *testing.T) { require.Error(t, err) assert.EqualError(t, err, "elements are not unique, index 1 collides with index 3 "+ "based on constraints: constraint 1, constraint 2") - assert.True(t, HasErrorCode(err, ErrorCodeSliceUnique)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeSliceUnique)) }) } diff --git a/pkg/govy/url.go b/pkg/rules/url.go similarity index 80% rename from pkg/govy/url.go rename to pkg/rules/url.go index 88b21d8..7d1c0b0 100644 --- a/pkg/govy/url.go +++ b/pkg/rules/url.go @@ -1,13 +1,15 @@ -package validation +package rules import ( "net/url" "github.com/pkg/errors" + + "github.com/nobl9/govy/pkg/govy" ) -func URL() SingleRule[*url.URL] { - return NewSingleRule(validateURL). +func URL() govy.SingleRule[*url.URL] { + return govy.NewSingleRule(validateURL). WithErrorCode(ErrorCodeURL). WithDescription(urlDescription) } diff --git a/pkg/govy/url_test.go b/pkg/rules/url_test.go similarity index 93% rename from pkg/govy/url_test.go rename to pkg/rules/url_test.go index 68e9590..b5044c4 100644 --- a/pkg/govy/url_test.go +++ b/pkg/rules/url_test.go @@ -1,4 +1,4 @@ -package validation +package rules import ( "net/url" @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/nobl9/govy/pkg/govy" ) var validURLs = []string{ @@ -65,7 +67,7 @@ func TestURL(t *testing.T) { require.NoError(t, err) err = URL().Validate(u) require.Error(t, err) - assert.True(t, HasErrorCode(err, ErrorCodeURL)) + assert.True(t, govy.HasErrorCode(err, ErrorCodeURL)) } }) }