Documentation ¶
Overview ¶
Package validation implements a functional API for consistent struct level validation.
Index ¶
- Constants
- func HasErrorCode(err error, code ErrorCode) bool
- func JoinErrors[T error](b *strings.Builder, errs []T, indent string)
- func MapElementName(mapName, key any) string
- func SliceElementName(sliceName string, index int) string
- type CascadeMode
- type ErrorCode
- type HashFunction
- type MapItem
- type Predicate
- type PropertyError
- type PropertyErrors
- type PropertyGetter
- type PropertyPlan
- type PropertyRules
- func (r PropertyRules[T, S]) Cascade(mode CascadeMode) PropertyRules[T, S]
- func (r PropertyRules[T, S]) HideValue() PropertyRules[T, S]
- func (r PropertyRules[T, S]) Include(rules ...Validator[T]) PropertyRules[T, S]
- func (r PropertyRules[T, S]) OmitEmpty() PropertyRules[T, S]
- func (r PropertyRules[T, S]) Required() PropertyRules[T, S]
- func (r PropertyRules[T, S]) Rules(rules ...Rule[T]) PropertyRules[T, S]
- func (r PropertyRules[T, S]) Validate(st S) PropertyErrors
- func (r PropertyRules[T, S]) When(predicate Predicate[S], opts ...WhenOptions) PropertyRules[T, S]
- func (r PropertyRules[T, S]) WithExamples(examples ...string) PropertyRules[T, S]
- func (r PropertyRules[T, S]) WithName(name string) PropertyRules[T, S]
- type PropertyRulesForMap
- func (r PropertyRulesForMap[M, K, V, S]) Cascade(mode CascadeMode) PropertyRulesForMap[M, K, V, S]
- func (r PropertyRulesForMap[M, K, V, S]) IncludeForItems(rules ...Validator[MapItem[K, V]]) PropertyRulesForMap[M, K, V, S]
- func (r PropertyRulesForMap[M, K, V, S]) IncludeForKeys(validators ...Validator[K]) PropertyRulesForMap[M, K, V, S]
- func (r PropertyRulesForMap[M, K, V, S]) IncludeForValues(rules ...Validator[V]) PropertyRulesForMap[M, K, V, S]
- func (r PropertyRulesForMap[M, K, V, S]) Rules(rules ...Rule[M]) PropertyRulesForMap[M, K, V, S]
- func (r PropertyRulesForMap[M, K, V, S]) RulesForItems(rules ...Rule[MapItem[K, V]]) PropertyRulesForMap[M, K, V, S]
- func (r PropertyRulesForMap[M, K, V, S]) RulesForKeys(rules ...Rule[K]) PropertyRulesForMap[M, K, V, S]
- func (r PropertyRulesForMap[M, K, V, S]) RulesForValues(rules ...Rule[V]) PropertyRulesForMap[M, K, V, S]
- func (r PropertyRulesForMap[M, K, V, S]) Validate(st S) PropertyErrors
- func (r PropertyRulesForMap[M, K, V, S]) When(predicate Predicate[S], opts ...WhenOptions) PropertyRulesForMap[M, K, V, S]
- func (r PropertyRulesForMap[M, K, V, S]) WithExamples(examples ...string) PropertyRulesForMap[M, K, V, S]
- func (r PropertyRulesForMap[M, K, V, S]) WithName(name string) PropertyRulesForMap[M, K, V, S]
- type PropertyRulesForSlice
- func (r PropertyRulesForSlice[T, S]) Cascade(mode CascadeMode) PropertyRulesForSlice[T, S]
- func (r PropertyRulesForSlice[T, S]) IncludeForEach(rules ...Validator[T]) PropertyRulesForSlice[T, S]
- func (r PropertyRulesForSlice[T, S]) Rules(rules ...Rule[[]T]) PropertyRulesForSlice[T, S]
- func (r PropertyRulesForSlice[T, S]) RulesForEach(rules ...Rule[T]) PropertyRulesForSlice[T, S]
- func (r PropertyRulesForSlice[T, S]) Validate(st S) PropertyErrors
- func (r PropertyRulesForSlice[T, S]) When(predicate Predicate[S], opts ...WhenOptions) PropertyRulesForSlice[T, S]
- func (r PropertyRulesForSlice[T, S]) WithExamples(examples ...string) PropertyRulesForSlice[T, S]
- func (r PropertyRulesForSlice[T, S]) WithName(name string) PropertyRulesForSlice[T, S]
- type Rule
- type RuleError
- type RulePlan
- type RuleSet
- type SingleRule
- func DurationPrecision(precision time.Duration) SingleRule[time.Duration]
- func EqualTo[T comparable](compared T) SingleRule[T]
- func Forbidden[T any]() SingleRule[T]
- func GreaterThan[T constraints.Ordered](n T) SingleRule[T]
- func GreaterThanOrEqualTo[T constraints.Ordered](n T) SingleRule[T]
- func LessThan[T constraints.Ordered](n T) SingleRule[T]
- func LessThanOrEqualTo[T constraints.Ordered](n T) SingleRule[T]
- func MapLength[M ~map[K]V, K comparable, V any](min, max int) SingleRule[M]
- func MapMaxLength[M ~map[K]V, K comparable, V any](max int) SingleRule[M]
- func MapMinLength[M ~map[K]V, K comparable, V any](min int) SingleRule[M]
- func MutuallyExclusive[S any](required bool, getters map[string]func(s S) any) SingleRule[S]
- func NewSingleRule[T any](validate func(v T) error) SingleRule[T]
- func NotEqualTo[T comparable](compared T) SingleRule[T]
- func OneOf[T comparable](values ...T) SingleRule[T]
- func Required[T any]() SingleRule[T]
- func SliceLength[S ~[]E, E any](min, max int) SingleRule[S]
- func SliceMaxLength[S ~[]E, E any](max int) SingleRule[S]
- func SliceMinLength[S ~[]E, E any](min int) SingleRule[S]
- func SliceUnique[S []V, V any, H comparable](hashFunc HashFunction[V, H], constraints ...string) SingleRule[S]
- func StringASCII() SingleRule[string]
- func StringContains(substrings ...string) SingleRule[string]
- func StringDenyRegexp(re *regexp.Regexp, examples ...string) SingleRule[string]
- func StringDescription() SingleRule[string]
- func StringJSON() SingleRule[string]
- func StringLength(min, max int) SingleRule[string]
- func StringMatchRegexp(re *regexp.Regexp, examples ...string) SingleRule[string]
- func StringMaxLength(max int) SingleRule[string]
- func StringMinLength(min int) SingleRule[string]
- func StringNotEmpty() SingleRule[string]
- func StringStartsWith(prefixes ...string) SingleRule[string]
- func StringURL() SingleRule[string]
- func StringUUID() SingleRule[string]
- func URL() SingleRule[*url.URL]
- func (r SingleRule[T]) Validate(v T) error
- func (r SingleRule[T]) WithDescription(description string) SingleRule[T]
- func (r SingleRule[T]) WithDetails(format string, a ...any) SingleRule[T]
- func (r SingleRule[T]) WithErrorCode(code ErrorCode) SingleRule[T]
- func (r SingleRule[T]) WithMessage(format string, a ...any) SingleRule[T]
- type Transformer
- type Validator
- type ValidatorError
- type WhenOptions
Examples ¶
- ForMap
- ForPointer
- ForSlice
- GetSelf
- HasErrorCode
- New
- NewPropertyError
- Plan
- PropertyRules.Cascade
- PropertyRules.Include
- PropertyRules.OmitEmpty
- PropertyRules.Required
- PropertyRules.When
- PropertyRules.WithName
- RuleSet
- SingleRule.WithDetails
- SingleRule.WithErrorCode
- Validator
- Validator (BranchingPattern)
- Validator.When
- Validator.WithName
- ValidatorError.WithName
Constants ¶
const ErrorCodeSeparator = ":"
Variables ¶
This section is empty.
Functions ¶
func HasErrorCode ¶
HasErrorCode checks if an error contains given ErrorCode. It supports all validation errors.
Example ¶
To inspect if an error contains a given validation.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'
package main import ( "fmt" "regexp" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } const year = 24 * 365 * time.Hour func main() { teacherNameRule := validation.NewRuleSet[string]( validation.StringLength(1, 5), validation.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")), ). WithErrorCode("teacher_name") v := validation.New[Teacher]( validation.For(func(t Teacher) string { return t.Name }). WithName("name"). Rules(teacherNameRule), ).WithName("Teacher") teacher := Teacher{ Name: "Jonathan", Age: 51 * year, } err := v.Validate(teacher) if err != nil { for _, code := range []validation.ErrorCode{ "teacher_name", "string_length", "string_match_regexp", } { if validation.HasErrorCode(err, code) { fmt.Println("Has error code:", code) } } } }
Output: Has error code: teacher_name Has error code: string_length Has error code: string_match_regexp
func MapElementName ¶ added in v0.80.0
func SliceElementName ¶
Types ¶
type CascadeMode ¶ added in v0.82.0
type CascadeMode uint
CascadeMode defines how validation should behave when an error is encountered.
const ( // CascadeModeContinue will continue validation after first error. CascadeModeContinue CascadeMode = iota // CascadeModeStop will stop validation on first error encountered. CascadeModeStop )
type ErrorCode ¶
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" )
type HashFunction ¶
type HashFunction[V any, H comparable] func(v V) H
HashFunction accepts a value and returns a comparable hash.
func SelfHashFunc ¶
func SelfHashFunc[H comparable]() HashFunction[H, H]
SelfHashFunc returns a HashFunction which returns it's input value as a hash itself. The value must be comparable.
type MapItem ¶ added in v0.80.0
type MapItem[K comparable, V any] struct { Key K Value V }
MapItem is a tuple container for map's key and value pair.
type PropertyError ¶
type PropertyError struct { PropertyName string `json:"propertyName"` PropertyValue string `json:"propertyValue"` // IsKeyError is set to true if the error was created through map key validation. // PropertyValue in this scenario will be the key value, equal to the last element of PropertyName path. IsKeyError bool `json:"isKeyError,omitempty"` // IsSliceElementError is set to true if the error was created through slice element validation. IsSliceElementError bool `json:"isSliceElementError,omitempty"` Errors []*RuleError `json:"errors"` }
func NewPropertyError ¶
func NewPropertyError(propertyName string, propertyValue interface{}, errs ...error) *PropertyError
Example ¶
Sometimes you need top level context, but you want to scope the error to a specific, nested property. One of the ways to do that is to use NewPropertyError and return PropertyError from your validation rule. Note that you can still use ErrorCode and pass RuleError to the constructor. You can pass any number of RuleError.
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } const year = 24 * 365 * time.Hour func main() { v := validation.New[Teacher]( validation.For(validation.GetSelf[Teacher]()). Rules(validation.NewSingleRule(func(t Teacher) error { if t.Name == "Jake" { return validation.NewPropertyError( "name", t.Name, validation.NewRuleError("name cannot be Jake", "error_code_jake"), validation.NewRuleError("you can pass me too!")) } return nil })), ).WithName("Teacher") teacher := Teacher{ Name: "Jake", Age: 51 * year, } err := v.Validate(teacher) if err != nil { propertyErrors := err.Errors ruleErrors := propertyErrors[0].Errors fmt.Printf("Error code: %s\n\n", ruleErrors[0].Code) fmt.Println(err) } }
Output: Error code: error_code_jake Validation for Teacher has failed for the following properties: - 'name' with value 'Jake': - name cannot be Jake - you can pass me too!
func (*PropertyError) Equal ¶ added in v0.82.0
func (e *PropertyError) Equal(cmp *PropertyError) bool
func (*PropertyError) Error ¶
func (e *PropertyError) Error() string
func (*PropertyError) HideValue ¶
func (e *PropertyError) HideValue() *PropertyError
HideValue hides the property value from PropertyError.Error and also hides it from.
func (*PropertyError) PrependPropertyName ¶
func (e *PropertyError) PrependPropertyName(name string) *PropertyError
type PropertyErrors ¶
type PropertyErrors []*PropertyError
func (PropertyErrors) Aggregate ¶ added in v0.82.0
func (e PropertyErrors) Aggregate() PropertyErrors
func (PropertyErrors) Error ¶
func (e PropertyErrors) Error() string
func (PropertyErrors) HideValue ¶
func (e PropertyErrors) HideValue() PropertyErrors
func (PropertyErrors) Sort ¶ added in v0.82.0
func (e PropertyErrors) Sort() PropertyErrors
Sort should be always called after Aggregate.
type PropertyGetter ¶
type PropertyGetter[T, S any] func(S) T
func GetSelf ¶
func GetSelf[S any]() PropertyGetter[S, S]
GetSelf is a convenience method for extracting 'self' property of a validated value.
Example ¶
If you want to access the value of the entity you're writing the Validator for, you can use GetSelf function which is a convenience PropertyGetter that returns self. Note that we don't call PropertyRules.WithName here, as we're comparing two properties in our top level, [Teacher] scope.
You can provide your own rules using NewSingleRule constructor. It returns new SingleRule instance which wraps your validation function.
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } const year = 24 * 365 * time.Hour func main() { customRule := validation.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]()). Rules(customRule), ).WithName("Teacher") teacher := Teacher{ Name: "Jake", Age: 51 * year, } err := v.Validate(teacher) if err != nil { fmt.Println(err) } }
Output: Validation for Teacher has failed for the following properties: - now I have access to the whole teacher
type PropertyPlan ¶ added in v0.82.0
type PropertyPlan struct { Path string `json:"path"` Type string `json:"type"` Examples []string `json:"examples,omitempty"` Rules []RulePlan `json:"rules,omitempty"` }
PropertyPlan is a validation plan for a single property.
func Plan ¶ added in v0.82.0
func Plan[S any](v Validator[S]) []PropertyPlan
Plan creates a validation plan for the provided Validator. Each property is represented by a PropertyPlan which aggregates its every RulePlan. If a property does not have any rules, it won't be included in the result.
Example ¶
When documenting an API it's often a struggle to keep consistency between the code and documentation we write for it. What If your code could be self-descriptive? Specifically, what If we could generate documentation out of our validation rules? We can achieve that by using Plan function!
There are multiple ways to improve the generated documentation:
- Use PropertyRules.WithExamples to provide a list of example values for the property.
- Use SingleRule.WithDescription to provide a plan-only description for your rule. For builtin rules, the description is already provided.
- Use WhenDescription to provide a plan-only description for your when conditions.
v := validation.New[Teacher]( validation.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"), ). Rules( validation.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
type PropertyRules ¶
type PropertyRules[T, S any] struct { // contains filtered or unexported fields }
PropertyRules is responsible for validating a single property.
func For ¶
func For[T, S any](getter PropertyGetter[T, S]) PropertyRules[T, S]
For creates a new PropertyRules instance for the property which value is extracted through PropertyGetter function.
func ForPointer ¶
func ForPointer[T, S any](getter PropertyGetter[*T, S]) PropertyRules[T, S]
ForPointer accepts a getter function returning a pointer and wraps its call in order to safely extract the value under the pointer or return a zero value for a give type T. If required is set to true, the nil pointer value will result in an error and the validation will not proceed.
Example ¶
For constructor creates new PropertyRules instance. It's only argument, PropertyGetter is used to extract the property value. It works fine for direct values, but falls short when working with pointers. Often times we use pointers to indicate that a property is optional, or we want to discern between nil and zero values. In either case we want our validation rules to work on direct values, not the pointer, otherwise we'd have to always check if pointer != nil.
ForPointer constructor can be used to solve this problem and allow us to work with the underlying value in our rules. Under the hood it wraps PropertyGetter and safely extracts the underlying value. If the value was nil, it will not attempt to evaluate any rules for this property. The rationale for that is it doesn't make sense to evaluate any rules for properties which are essentially empty. The only rule that makes sense in this context is to ensure the property is required. We'll learn about a way to achieve that in the next example: [ExamplePropertyRules_Required].
Let's define a rule for [Teacher.MiddleName] property. 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.
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } const year = 24 * 365 * time.Hour func main() { v := validation.New[Teacher]( validation.ForPointer(func(t Teacher) *string { return t.MiddleName }). WithName("middleName"). Rules(validation.StringMaxLength(5)), ).WithName("Teacher") middleName := "Thaddeus" teacher := Teacher{ Name: "Jake", Age: 51 * year, MiddleName: &middleName, } err := v.Validate(teacher) if err != nil { fmt.Println(err) } }
Output: Validation for Teacher has failed for the following properties: - 'middleName' with value 'Thaddeus': - length must be less than or equal to 5
func Transform ¶
func Transform[T, N, S any](getter PropertyGetter[T, S], transform Transformer[T, N]) PropertyRules[N, S]
Transform transforms value from one type to another. Value returned by PropertyGetter is transformed through Transformer function. If Transformer returns an error, the validation will not proceed and transformation error will be reported. Transformer is only called if PropertyGetter returns a non-zero value.
func (PropertyRules[T, S]) Cascade ¶ added in v0.82.0
func (r PropertyRules[T, S]) Cascade(mode CascadeMode) PropertyRules[T, S]
Example ¶
To customize how Rule are evaluated use PropertyRules.Cascade. Use CascadeModeStop to stop validation after the first error. If you wish to revert to the default behavior, use CascadeModeContinue.
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } func main() { alwaysFailingRule := validation.NewSingleRule(func(string) error { return fmt.Errorf("always fails") }) v := validation.New[Teacher]( validation.For(func(t Teacher) string { return t.Name }). WithName("name"). Cascade(validation.CascadeModeStop). Rules(validation.NotEqualTo("Jerry")). Rules(alwaysFailingRule), ).WithName("Teacher") for _, name := range []string{"Tom", "Jerry"} { teacher := Teacher{Name: name} err := v.Validate(teacher) if err != nil { fmt.Println(err) } } }
Output: Validation for Teacher has failed for the following properties: - 'name' with value 'Tom': - always fails Validation for Teacher has failed for the following properties: - 'name' with value 'Jerry': - should be not equal to 'Jerry'
func (PropertyRules[T, S]) HideValue ¶
func (r PropertyRules[T, S]) HideValue() PropertyRules[T, S]
func (PropertyRules[T, S]) Include ¶
func (r PropertyRules[T, S]) Include(rules ...Validator[T]) PropertyRules[T, S]
Example ¶
So far we've defined validation rules for simple, top-level properties. What If we want to define validation rules for nested properties? We can use PropertyRules.Include to include another Validator in our PropertyRules.
Let's extend our [Teacher] struct to include a nested [University] property. [University] in of itself is another struct with its own validation rules.
Notice how the nested property path is automatically built for you, each segment separated by a dot.
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } const year = 24 * 365 * time.Hour func main() { universityValidation := validation.New[University]( validation.For(func(u University) string { return u.Address }). WithName("address"). Required(), ) teacherValidation := validation.New[Teacher]( validation.For(func(t Teacher) string { return t.Name }). WithName("name"). Rules(validation.EqualTo("Tom")), validation.For(func(t Teacher) University { return t.University }). WithName("university"). Include(universityValidation), ).WithName("Teacher") teacher := Teacher{ Name: "Jerry", Age: 51 * year, University: University{ Name: "Poznan University of Technology", Address: "", }, } err := teacherValidation.Validate(teacher) if err != nil { fmt.Println(err) } }
Output: Validation for Teacher has failed for the following properties: - 'name' with value 'Jerry': - should be equal to 'Tom' - 'university.address': - property is required but was empty
func (PropertyRules[T, S]) OmitEmpty ¶
func (r PropertyRules[T, S]) OmitEmpty() PropertyRules[T, S]
Example ¶
While ForPointer will by default omit validation for nil pointers, it might be useful to have a similar behavior for optional properties which are direct values. PropertyRules.OmitEmpty will do the trick.
NOTE: PropertyRules.OmitEmpty will have no effect on pointers handled by ForPointer, as they already behave in the same way.
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } const year = 24 * 365 * time.Hour func main() { alwaysFailingRule := validation.NewSingleRule(func(string) error { return fmt.Errorf("always fails") }) v := validation.New[Teacher]( validation.For(func(t Teacher) string { return t.Name }). WithName("name"). OmitEmpty(). Rules(alwaysFailingRule), validation.ForPointer(func(t Teacher) *string { return t.MiddleName }). WithName("middleName"). Rules(alwaysFailingRule), ).WithName("Teacher") teacher := Teacher{ Name: "", Age: 51 * year, MiddleName: nil, } err := v.Validate(teacher) if err == nil { fmt.Println("no error! we skipped 'name' validation and 'middleName' is implicitly skipped") } }
Output: no error! we skipped 'name' validation and 'middleName' is implicitly skipped
func (PropertyRules[T, S]) Required ¶
func (r PropertyRules[T, S]) Required() PropertyRules[T, S]
Example ¶
By default, when PropertyRules is constructed using ForPointer 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? 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 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. None of the rules you've defined would be evaluated.
NOTE: Placement of PropertyRules.Required does not matter, it's not evaluated in a sequential loop, unlike standard Rule. However, we recommend you always place it below PropertyRules.WithName to make your rules more readable.
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } const year = 24 * 365 * time.Hour func main() { alwaysFailingRule := validation.NewSingleRule(func(string) error { return fmt.Errorf("always fails") }) v := validation.New[Teacher]( validation.ForPointer(func(t Teacher) *string { return t.MiddleName }). WithName("middleName"). Required(). Rules(alwaysFailingRule), validation.For(func(t Teacher) string { return t.Name }). WithName("name"). Required(). Rules(alwaysFailingRule), ).WithName("Teacher") teacher := Teacher{ Name: "", Age: 51 * year, MiddleName: nil, } err := v.Validate(teacher) if err != nil { fmt.Println(err) } }
Output: Validation for Teacher has failed for the following properties: - 'middleName': - property is required but was empty - 'name': - property is required but was empty
func (PropertyRules[T, S]) Rules ¶
func (r PropertyRules[T, S]) Rules(rules ...Rule[T]) PropertyRules[T, S]
func (PropertyRules[T, S]) Validate ¶
func (r PropertyRules[T, S]) Validate(st S) PropertyErrors
Validate validates the property value using provided rules. nolint: gocognit
func (PropertyRules[T, S]) When ¶
func (r PropertyRules[T, S]) When(predicate Predicate[S], opts ...WhenOptions) PropertyRules[T, S]
Example ¶
To only run property validation on condition, use PropertyRules.When. Predicates set through PropertyRules.When are evaluated in the order they are provided. If any predicate is not met, validation rules are not evaluated for the whole PropertyRules.
It's recommended to define PropertyRules.When before PropertyRules.Rules declaration.
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } func main() { v := validation.New[Teacher]( validation.For(func(t Teacher) string { return t.Name }). WithName("name"). When(func(t Teacher) bool { return t.Name == "Jerry" }). Rules(validation.NotEqualTo("Jerry")), ).WithName("Teacher") for _, name := range []string{"Tom", "Jerry", "Mickey"} { teacher := Teacher{Name: name} err := v.Validate(teacher) if err != nil { fmt.Println(err) } } }
Output: Validation for Teacher has failed for the following properties: - 'name' with value 'Jerry': - should be not equal to 'Jerry'
func (PropertyRules[T, S]) WithExamples ¶ added in v0.82.0
func (r PropertyRules[T, S]) WithExamples(examples ...string) PropertyRules[T, S]
func (PropertyRules[T, S]) WithName ¶
func (r PropertyRules[T, S]) WithName(name string) PropertyRules[T, S]
Example ¶
So far we've been using a very simple PropertyRules instance:
validation.For(func(t Teacher) string { return t.Name }). Rules(validation.NewSingleRule(func(name string) error { return fmt.Errorf("always fails") }))
The error message returned by this property rule does not tell us which property is failing. Let's change that by adding property name using PropertyRules.WithName.
We can also change the Rule to be something more real. Validation package comes with a number of predefined Rule, we'll use EqualTo which accepts a single argument, value to compare with.
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } const year = 24 * 365 * time.Hour func main() { v := validation.New[Teacher]( validation.For(func(t Teacher) string { return t.Name }). WithName("name"). Rules(validation.EqualTo("Tom")), ).WithName("Teacher") teacher := Teacher{ Name: "Jake", Age: 51 * year, } err := v.Validate(teacher) if err != nil { fmt.Println(err) } }
Output: Validation for Teacher has failed for the following properties: - 'name' with value 'Jake': - should be equal to 'Tom'
type PropertyRulesForMap ¶ added in v0.80.0
type PropertyRulesForMap[M ~map[K]V, K comparable, V, S any] struct { // contains filtered or unexported fields }
PropertyRulesForMap is responsible for validating a single property.
func ForMap ¶ added in v0.80.0
func ForMap[M ~map[K]V, K comparable, V, S any](getter PropertyGetter[M, S]) PropertyRulesForMap[M, K, V, S]
ForMap creates a new PropertyRulesForMap instance for a map property which value is extracted through PropertyGetter function.
Example ¶
When dealing with maps there are three forms of iteration: - keys - values - key-value pairs (items)
You can use ForMap function to define rules for all the aforementioned iterators. It returns a new struct PropertyRulesForMap which behaves similar to PropertyRulesForSlice..
To define rules for keys use: - PropertyRulesForMap.RulesForKeys - PropertyRulesForMap.IncludeForKeys - PropertyRulesForMap.RulesForValues - PropertyRulesForMap.IncludeForValues - PropertyRulesForMap.RulesForItems - PropertyRulesForMap.IncludeForItems These work exactly the same way as PropertyRules.Rules and PropertyRules.Include verifying each map's key, value or MapItem.
PropertyRulesForMap.Rules is in turn used to define rules for the whole map.
NOTE: PropertyRulesForMap does not implement Include function for the whole map.
In the below example, we're defining that student index to [Teacher] map: - Must have at most 2 elements (map). - Keys must have a length of 9 (keys). - Eve cannot be a teacher for any student (values). - Joan cannot be a teacher for student with index 918230013 (items).
Notice that property path for maps has the following format: <map_name>.<key>.<map_property_name>
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } type Tutoring struct { StudentIndexToTeacher map[string]Teacher `json:"studentIndexToTeacher"` } func main() { teacherValidator := validation.New[Teacher]( validation.For(func(t Teacher) string { return t.Name }). WithName("name"). Rules(validation.NotEqualTo("Eve")), ) tutoringValidator := validation.New[Tutoring]( validation.ForMap(func(t Tutoring) map[string]Teacher { return t.StudentIndexToTeacher }). WithName("students"). Rules( validation.MapMaxLength[map[string]Teacher](2), ). RulesForKeys( validation.StringLength(9, 9), ). IncludeForValues(teacherValidator). RulesForItems(validation.NewSingleRule(func(v validation.MapItem[string, Teacher]) error { if v.Key == "918230013" && v.Value.Name == "Joan" { return validation.NewRuleError( "Joan cannot be a teacher for student with index 918230013", "joan_teacher", ) } return nil })), ) tutoring := Tutoring{ StudentIndexToTeacher: map[string]Teacher{ "918230013": {Name: "Joan"}, "9182300123": {Name: "Eve"}, "918230014": {Name: "Joan"}, }, } err := tutoringValidator.Validate(tutoring) if err != nil { fmt.Println(err) } }
Output: Validation has failed for the following properties: - 'students' with value '{"9182300123":{"name":"Eve","age":0,"students":null,"university":{"name":"","address":""}},"91823001...': - length must be less than or equal to 2 - 'students.9182300123' with key '9182300123': - length must be between 9 and 9 - 'students.9182300123.name' with value 'Eve': - should be not equal to 'Eve' - 'students.918230013' with value '{"name":"Joan","age":0,"students":null,"university":{"name":"","address":""}}': - Joan cannot be a teacher for student with index 918230013
func (PropertyRulesForMap[M, K, V, S]) Cascade ¶ added in v0.82.0
func (r PropertyRulesForMap[M, K, V, S]) Cascade(mode CascadeMode) PropertyRulesForMap[M, K, V, S]
func (PropertyRulesForMap[M, K, V, S]) IncludeForItems ¶ added in v0.80.0
func (r PropertyRulesForMap[M, K, V, S]) IncludeForItems( rules ...Validator[MapItem[K, V]], ) PropertyRulesForMap[M, K, V, S]
func (PropertyRulesForMap[M, K, V, S]) IncludeForKeys ¶ added in v0.80.0
func (r PropertyRulesForMap[M, K, V, S]) IncludeForKeys(validators ...Validator[K]) PropertyRulesForMap[M, K, V, S]
func (PropertyRulesForMap[M, K, V, S]) IncludeForValues ¶ added in v0.80.0
func (r PropertyRulesForMap[M, K, V, S]) IncludeForValues(rules ...Validator[V]) PropertyRulesForMap[M, K, V, S]
func (PropertyRulesForMap[M, K, V, S]) Rules ¶ added in v0.80.0
func (r PropertyRulesForMap[M, K, V, S]) Rules(rules ...Rule[M]) PropertyRulesForMap[M, K, V, S]
func (PropertyRulesForMap[M, K, V, S]) RulesForItems ¶ added in v0.80.0
func (r PropertyRulesForMap[M, K, V, S]) RulesForItems(rules ...Rule[MapItem[K, V]]) PropertyRulesForMap[M, K, V, S]
func (PropertyRulesForMap[M, K, V, S]) RulesForKeys ¶ added in v0.80.0
func (r PropertyRulesForMap[M, K, V, S]) RulesForKeys(rules ...Rule[K]) PropertyRulesForMap[M, K, V, S]
func (PropertyRulesForMap[M, K, V, S]) RulesForValues ¶ added in v0.80.0
func (r PropertyRulesForMap[M, K, V, S]) RulesForValues(rules ...Rule[V]) PropertyRulesForMap[M, K, V, S]
func (PropertyRulesForMap[M, K, V, S]) Validate ¶ added in v0.80.0
func (r PropertyRulesForMap[M, K, V, S]) Validate(st S) PropertyErrors
Validate executes each of the rules sequentially and aggregates the encountered errors.
func (PropertyRulesForMap[M, K, V, S]) When ¶ added in v0.80.0
func (r PropertyRulesForMap[M, K, V, S]) When( predicate Predicate[S], opts ...WhenOptions, ) PropertyRulesForMap[M, K, V, S]
func (PropertyRulesForMap[M, K, V, S]) WithExamples ¶ added in v0.82.0
func (r PropertyRulesForMap[M, K, V, S]) WithExamples(examples ...string) PropertyRulesForMap[M, K, V, S]
func (PropertyRulesForMap[M, K, V, S]) WithName ¶ added in v0.80.0
func (r PropertyRulesForMap[M, K, V, S]) WithName(name string) PropertyRulesForMap[M, K, V, S]
type PropertyRulesForSlice ¶ added in v0.80.0
type PropertyRulesForSlice[T, S any] struct { // contains filtered or unexported fields }
PropertyRulesForSlice is responsible for validating a single property.
func ForSlice ¶ added in v0.80.0
func ForSlice[T, S any](getter PropertyGetter[[]T, S]) PropertyRulesForSlice[T, S]
ForSlice creates a new PropertyRulesForSlice instance for a slice property which value is extracted through PropertyGetter function.
Example ¶
When dealing with slices we often want to both validate the whole slice and each of its elements. You can use ForSlice function to do just that. It returns a new struct PropertyRulesForSlice which behaves exactly the same as PropertyRules, but extends its API slightly.
To define rules for each element use: - PropertyRulesForSlice.RulesForEach - PropertyRulesForSlice.IncludeForEach These work exactly the same way as PropertyRules.Rules and PropertyRules.Include verifying each slice element.
PropertyRulesForSlice.Rules is in turn used to define rules for the whole slice.
NOTE: PropertyRulesForSlice does not implement Include function for the whole slice.
In the below example, we're defining that students slice must have at most 2 elements and that each element's index must be unique. For each element we're also including [Student] Validator. Notice that property path for slices has the following format: <slice_name>[<index>].<slice_property_name>
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } func main() { studentValidator := validation.New[Student]( validation.For(func(s Student) string { return s.Index }). WithName("index"). Rules(validation.StringLength(9, 9)), ) teacherValidator := validation.New[Teacher]( validation.ForSlice(func(t Teacher) []Student { return t.Students }). WithName("students"). Rules( validation.SliceMaxLength[[]Student](2), validation.SliceUnique(func(v Student) string { return v.Index })). IncludeForEach(studentValidator), ).When(func(t Teacher) bool { return t.Age < 50 }) teacher := Teacher{ Name: "John", Students: []Student{ {Index: "918230014"}, {Index: "9182300123"}, {Index: "918230014"}, }, } err := teacherValidator.Validate(teacher) if err != nil { fmt.Println(err) } }
Output: Validation has failed for the following properties: - 'students' with value '[{"index":"918230014"},{"index":"9182300123"},{"index":"918230014"}]': - length must be less than or equal to 2 - elements are not unique, index 0 collides with index 2 - 'students[1].index' with value '9182300123': - length must be between 9 and 9
func (PropertyRulesForSlice[T, S]) Cascade ¶ added in v0.82.0
func (r PropertyRulesForSlice[T, S]) Cascade(mode CascadeMode) PropertyRulesForSlice[T, S]
func (PropertyRulesForSlice[T, S]) IncludeForEach ¶ added in v0.80.0
func (r PropertyRulesForSlice[T, S]) IncludeForEach(rules ...Validator[T]) PropertyRulesForSlice[T, S]
func (PropertyRulesForSlice[T, S]) Rules ¶ added in v0.80.0
func (r PropertyRulesForSlice[T, S]) Rules(rules ...Rule[[]T]) PropertyRulesForSlice[T, S]
func (PropertyRulesForSlice[T, S]) RulesForEach ¶ added in v0.80.0
func (r PropertyRulesForSlice[T, S]) RulesForEach(rules ...Rule[T]) PropertyRulesForSlice[T, S]
func (PropertyRulesForSlice[T, S]) Validate ¶ added in v0.80.0
func (r PropertyRulesForSlice[T, S]) Validate(st S) PropertyErrors
Validate executes each of the rules sequentially and aggregates the encountered errors.
func (PropertyRulesForSlice[T, S]) When ¶ added in v0.80.0
func (r PropertyRulesForSlice[T, S]) When(predicate Predicate[S], opts ...WhenOptions) PropertyRulesForSlice[T, S]
func (PropertyRulesForSlice[T, S]) WithExamples ¶ added in v0.82.0
func (r PropertyRulesForSlice[T, S]) WithExamples(examples ...string) PropertyRulesForSlice[T, S]
func (PropertyRulesForSlice[T, S]) WithName ¶ added in v0.80.0
func (r PropertyRulesForSlice[T, S]) WithName(name string) PropertyRulesForSlice[T, S]
type RuleError ¶
func NewRequiredError ¶
func NewRequiredError() *RuleError
func NewRuleError ¶
NewRuleError creates a new RuleError with the given message and optional error codes. Error codes are added according to the rules defined by RuleError.AddCode.
type RulePlan ¶ added in v0.82.0
type RulePlan struct { Description string `json:"description"` Details string `json:"details,omitempty"` ErrorCode ErrorCode `json:"errorCode,omitempty"` Conditions []string `json:"conditions,omitempty"` }
RulePlan is a validation plan for a single rule.
type RuleSet ¶
type RuleSet[T any] struct { // contains filtered or unexported fields }
RuleSet allows defining Rule which aggregates multiple sub-rules.
Example ¶
Sometimes it's useful to build a Rule using other rules. To do that we'll use RuleSet and NewRuleSet constructor. RuleSet is a simple container for multiple Rule. It is later on unpacked and each RuleError is reported separately. When RuleSet.WithErrorCode or RuleSet.WithDetails are used, error code and details are added to each RuleError. Note that validation package uses similar syntax to wrapped errors in Go; a ':' delimiter is used to chain error codes together.
package main import ( "fmt" "regexp" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } const year = 24 * 365 * time.Hour func main() { teacherNameRule := validation.NewRuleSet[string]( validation.StringLength(1, 5), validation.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 }). WithName("name"). Rules(teacherNameRule), ).WithName("Teacher") teacher := Teacher{ Name: "Jonathan", Age: 51 * year, } err := v.Validate(teacher) if err != nil { propertyErrors := err.Errors ruleErrors := propertyErrors[0].Errors fmt.Printf("Error codes: %s, %s\n\n", ruleErrors[0].Code, ruleErrors[1].Code) fmt.Println(err) } // nolint: lll
Output:
func NewRuleSet ¶
NewRuleSet creates a new RuleSet instance.
func StringIsDNSSubdomain ¶
func (RuleSet[T]) Validate ¶
Validate works the same way as SingleRule.Validate, 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 (RuleSet[T]) WithDetails ¶
WithDetails adds details to each returned RuleError error message.
type SingleRule ¶
type SingleRule[T any] struct { // contains filtered or unexported fields }
SingleRule is the basic validation building block. It evaluates the provided validation function and enhances it with optional ErrorCode and arbitrary details.
func DurationPrecision ¶ added in v0.78.0
func DurationPrecision(precision time.Duration) SingleRule[time.Duration]
func EqualTo ¶
func EqualTo[T comparable](compared T) SingleRule[T]
func Forbidden ¶
func Forbidden[T any]() SingleRule[T]
func GreaterThan ¶
func GreaterThan[T constraints.Ordered](n T) SingleRule[T]
func GreaterThanOrEqualTo ¶
func GreaterThanOrEqualTo[T constraints.Ordered](n T) SingleRule[T]
func LessThan ¶
func LessThan[T constraints.Ordered](n T) SingleRule[T]
func LessThanOrEqualTo ¶
func LessThanOrEqualTo[T constraints.Ordered](n T) SingleRule[T]
func MapLength ¶
func MapLength[M ~map[K]V, K comparable, V any](min, max int) SingleRule[M]
func MapMaxLength ¶
func MapMaxLength[M ~map[K]V, K comparable, V any](max int) SingleRule[M]
func MapMinLength ¶
func MapMinLength[M ~map[K]V, K comparable, V any](min int) SingleRule[M]
func MutuallyExclusive ¶
func MutuallyExclusive[S any](required bool, getters map[string]func(s S) any) SingleRule[S]
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 NewSingleRule ¶
func NewSingleRule[T any](validate func(v T) error) SingleRule[T]
NewSingleRule creates a new SingleRule instance.
func NotEqualTo ¶
func NotEqualTo[T comparable](compared T) SingleRule[T]
func OneOf ¶
func OneOf[T comparable](values ...T) SingleRule[T]
func Required ¶
func Required[T any]() SingleRule[T]
func SliceLength ¶
func SliceLength[S ~[]E, E any](min, max int) SingleRule[S]
func SliceMaxLength ¶
func SliceMaxLength[S ~[]E, E any](max int) SingleRule[S]
func SliceMinLength ¶
func SliceMinLength[S ~[]E, E any](min int) SingleRule[S]
func SliceUnique ¶
func SliceUnique[S []V, V any, H comparable](hashFunc HashFunction[V, H], constraints ...string) SingleRule[S]
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 StringASCII ¶
func StringASCII() SingleRule[string]
func StringContains ¶
func StringContains(substrings ...string) SingleRule[string]
func StringDenyRegexp ¶
func StringDenyRegexp(re *regexp.Regexp, examples ...string) SingleRule[string]
func StringDescription ¶
func StringDescription() SingleRule[string]
func StringJSON ¶
func StringJSON() SingleRule[string]
func StringLength ¶
func StringLength(min, max int) SingleRule[string]
func StringMatchRegexp ¶
func StringMatchRegexp(re *regexp.Regexp, examples ...string) SingleRule[string]
func StringMaxLength ¶
func StringMaxLength(max int) SingleRule[string]
func StringMinLength ¶
func StringMinLength(min int) SingleRule[string]
func StringNotEmpty ¶
func StringNotEmpty() SingleRule[string]
func StringStartsWith ¶
func StringStartsWith(prefixes ...string) SingleRule[string]
func StringURL ¶
func StringURL() SingleRule[string]
func StringUUID ¶
func StringUUID() SingleRule[string]
func URL ¶
func URL() SingleRule[*url.URL]
func (SingleRule[T]) Validate ¶
func (r SingleRule[T]) Validate(v T) error
Validate runs validation function on the provided value. It can handle different types of errors returned by the function:
- *RuleError, which details and ErrorCode are optionally extended with the ones defined by SingleRule.
- *PropertyError, for each of its errors their ErrorCode is extended with the one defined by SingleRule.
By default, it will construct a new RuleError.
func (SingleRule[T]) WithDescription ¶ added in v0.82.0
func (r SingleRule[T]) WithDescription(description string) SingleRule[T]
func (SingleRule[T]) WithDetails ¶
func (r SingleRule[T]) WithDetails(format string, a ...any) SingleRule[T]
WithDetails adds details to the returned RuleError error message.
Example ¶
You can use SingleRule.WithDetails to add additional details to the error message. This allows you to extend existing rules by adding your use case context. Let's give a regex validation some more clarity.
package main import ( "fmt" "regexp" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } const year = 24 * 365 * time.Hour func main() { v := validation.New[Teacher]( validation.For(func(t Teacher) string { return t.Name }). WithName("name"). Rules(validation.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")). WithDetails("Teacher can be either Tom or Jerry :)")), ).WithName("Teacher") teacher := Teacher{ Name: "Jake", Age: 51 * year, } err := v.Validate(teacher) if err != nil { fmt.Println(err) } }
Output: Validation for Teacher has failed for the following properties: - 'name' with value 'Jake': - string must match regular expression: '^(Tom|Jerry)$'; Teacher can be either Tom or Jerry :)
func (SingleRule[T]) WithErrorCode ¶
func (r SingleRule[T]) WithErrorCode(code ErrorCode) SingleRule[T]
WithErrorCode sets the error code for the returned RuleError.
Example ¶
When testing, it can be tedious to always rely on error messages as these can change over time. Enter ErrorCode, which is a simple string type alias used to ease testing, but also potentially allow third parties to integrate with your validation results. Use SingleRule.WithErrorCode to associate ErrorCode with a SingleRule. Notice that our modified version of StringMatchRegexp will now return a new ErrorCode. Predefined rules have ErrorCode already associated with them. To view the list of predefined ErrorCode checkout error_codes.go file.
package main import ( "fmt" "regexp" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } const year = 24 * 365 * time.Hour func main() { v := validation.New[Teacher]( validation.For(func(t Teacher) string { return t.Name }). WithName("name"). Rules(validation.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")). WithDetails("Teacher can be either Tom or Jerry :)"). WithErrorCode("custom_code")), ).WithName("Teacher") teacher := Teacher{ Name: "Jake", Age: 51 * year, } err := v.Validate(teacher) if err != nil { propertyErrors := err.Errors ruleErrors := propertyErrors[0].Errors fmt.Println(ruleErrors[0].Code) } }
Output: custom_code
func (SingleRule[T]) WithMessage ¶ added in v0.80.0
func (r SingleRule[T]) WithMessage(format string, a ...any) SingleRule[T]
WithMessage overrides the returned RuleError error message with message.
type Transformer ¶
type Validator ¶
type Validator[S any] struct { // contains filtered or unexported fields }
Validator is the top level validation entity. It serves as an aggregator for PropertyRules.
Example ¶
Bringing it all (mostly) together, let's create a fully fledged Validator for [Teacher].
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } func main() { universityValidation := validation.New[University]( validation.For(func(u University) string { return u.Address }). WithName("address"). Required(), ) studentValidator := validation.New[Student]( validation.For(func(s Student) string { return s.Index }). WithName("index"). Rules(validation.StringLength(9, 9)), ) teacherValidator := validation.New[Teacher]( validation.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 }). WithName("students"). Rules( validation.SliceMaxLength[[]Student](2), validation.SliceUnique(func(v Student) string { return v.Index })). IncludeForEach(studentValidator), validation.For(func(t Teacher) University { return t.University }). WithName("university"). Include(universityValidation), ).When(func(t Teacher) bool { return t.Age < 50 }) teacher := Teacher{ Name: "John", Students: []Student{ {Index: "918230014"}, {Index: "9182300123"}, {Index: "918230014"}, }, University: University{ Name: "Poznan University of Technology", Address: "", }, } err := teacherValidator.WithName("John").Validate(teacher) if err != nil { fmt.Println(err) } }
Output: Validation for John has failed for the following properties: - 'name' with value 'John': - must be one of [Jake, George] - 'students' with value '[{"index":"918230014"},{"index":"9182300123"},{"index":"918230014"}]': - length must be less than or equal to 2 - elements are not unique, index 0 collides with index 2 - 'students[1].index' with value '9182300123': - length must be between 9 and 9 - 'university.address': - property is required but was empty
Example (BranchingPattern) ¶
When dealing with properties that should only be validated if a certain other property has specific value, it's recommended to use PropertyRules.When and PropertyRules.Include to separate validation paths into non-overlapping branches.
Notice how in the below example [File.Format] is the common, shared property between [CSV] and [JSON] files. We define separate Validator for [CSV] and [JSON] and use PropertyRules.When to only validate their included Validator if the correct [File.Format] is provided.
package main import ( "fmt" "regexp" "github.com/nobl9/nobl9-go/internal/validation" ) func main() { type ( CSV struct { Separator string `json:"separator"` } JSON struct { Indent string `json:"indent"` } File struct { Format string `json:"format"` CSV *CSV `json:"csv,omitempty"` JSON *JSON `json:"json,omitempty"` } ) csvValidation := validation.New[CSV]( validation.For(func(c CSV) string { return c.Separator }). WithName("separator"). Required(). Rules(validation.OneOf(",", ";")), ) jsonValidation := validation.New[JSON]( validation.For(func(j JSON) string { return j.Indent }). WithName("indent"). Required(). Rules(validation.StringMatchRegexp(regexp.MustCompile(`^\s*$`))), ) fileValidation := validation.New[File]( validation.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 }). When(func(f File) bool { return f.Format == "json" }). Include(jsonValidation), validation.For(func(f File) string { return f.Format }). WithName("format"). Required(). Rules(validation.OneOf("csv", "json")), ).WithName("File") file := File{ Format: "json", CSV: nil, JSON: &JSON{ Indent: "invalid", }, } err := fileValidation.Validate(file) if err != nil { fmt.Println(err) } }
Output: Validation for File has failed for the following properties: - 'indent' with value 'invalid': - string must match regular expression: '^\s*$'
func New ¶
New creates a new Validator aggregating the provided property rules.
Example ¶
In order to create a new Validator use New constructor. Let's define simple PropertyRules for [Teacher.Name]. For now, it will be always failing.
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } func main() { 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") })), ) err := v.Validate(Teacher{}) if err != nil { fmt.Println(err) } }
Output: Validation has failed for the following properties: - always fails
func (Validator[S]) Validate ¶
func (v Validator[S]) Validate(st S) *ValidatorError
Validate will first evaluate predicates before validating any rules. If any predicate does not pass the validation won't be executed (returns nil). All errors returned by property rules will be aggregated and wrapped in ValidatorError.
func (Validator[S]) When ¶
func (v Validator[S]) When(predicate Predicate[S], opts ...WhenOptions) Validator[S]
When defines accepts predicates which will be evaluated BEFORE Validator validates ANY rules.
Example ¶
Validator rules can be evaluated on condition, to specify the predicate use Validator.When function.
In this example, validation for [Teacher] instance will only be evaluated if the [Age] property is less than 50 years.
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } const year = 24 * 365 * time.Hour func main() { 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") })), ). When(func(t Teacher) bool { return t.Age < (50 * year) }) // Prepare teachers. teacherTom := Teacher{ Name: "Tom", Age: 51 * year, } teacherJerry := Teacher{ Name: "Jerry", Age: 30 * year, } // Run validation. err := v.Validate(teacherTom) if err != nil { fmt.Println(err.WithName("Tom")) } err = v.Validate(teacherJerry) if err != nil { fmt.Println(err.WithName("Jerry")) } }
Output: Validation for Jerry has failed for the following properties: - always fails
func (Validator[S]) WithName ¶
WithName when a rule fails will pass the provided name to ValidatorError.WithName.
Example ¶
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.
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } func main() { 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") })), ).WithName("Teacher") err := v.Validate(Teacher{}) if err != nil { fmt.Println(err) } }
Output: Validation for Teacher has failed for the following properties: - always fails
type ValidatorError ¶
type ValidatorError struct { Errors PropertyErrors `json:"errors"` Name string `json:"name"` }
func NewValidatorError ¶
func NewValidatorError(errs PropertyErrors) *ValidatorError
func (*ValidatorError) Error ¶
func (e *ValidatorError) Error() string
func (*ValidatorError) WithName ¶
func (e *ValidatorError) WithName(name string) *ValidatorError
Example ¶
You can also add Validator name during runtime, by calling ValidatorError.WithName function on the returned error.
NOTE: We left the previous "Teacher" name assignment, to demonstrate that the ValidatorError.WithName function call will shadow it.
NOTE: This would also work:
err := v.WithName("Jake").Validate(Teacher{})
Validation package, aside from errors handling, tries to follow immutability principle. Calling any function on Validator will not change its previous declaration (unless you assign it back to 'v').
package main import ( "fmt" "time" "github.com/nobl9/nobl9-go/internal/validation" ) type Teacher struct { Name string `json:"name"` Age time.Duration `json:"age"` Students []Student `json:"students"` MiddleName *string `json:"middleName,omitempty"` University University `json:"university"` } type University struct { Name string `json:"name"` Address string `json:"address"` } type Student struct { Index string `json:"index"` } func main() { 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") })), ).WithName("Teacher") err := v.Validate(Teacher{}) if err != nil { fmt.Println(err.WithName("Jake")) } }
Output: Validation for Jake has failed for the following properties: - always fails
type WhenOptions ¶ added in v0.82.0
type WhenOptions struct {
// contains filtered or unexported fields
}
WhenOptions defines optional parameters for the When conditions.
func WhenDescription ¶ added in v0.82.0
func WhenDescription(format string, a ...interface{}) WhenOptions
WhenDescription sets the description for the When condition.