validation

package
v0.67.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 7, 2023 License: MPL-2.0 Imports: 11 Imported by: 0

README

Validation

Package validation 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.

Validation pipeline is immutable and lazily loaded.

  • Immutable, as changing the pipeline through chained functions, will return a new pipeline. It allows extended reusability of validation components.
  • Lazily loaded, as properties are extracted through getter functions, which are only called when you call the Validate method. Functional approach allows validation components to only be called when needed. You should define your pipeline once and call it whenever you validate instances of your entity.

All that has been made possible by the introduction of generics in Go. Prior to that, there wasn't really any viable way to create type safe validation API. Although the current state of type inference is somewhat clunky, the API can only improve in time when generics support in Go is further extended.

NOTE: Work in progress!

Although already battle tested through SLO hellfire, this library is still a work in progress. The principles and the API at its core won't change, but the details and capabilities might hopefully will. Contributions and suggestions are most welcome!

Usage

This README goes through an abstract overview of the library.
Refer to example_test.go for a hands-on tutorial with runnable examples.

Legend
Validator

Validator aggregates property rules into a single validation scenario, most commonly associated with an entity, like struct.

If any property rules fail ValidatorError is returned.

Property rules

When validating structured data, namely struct, each structure consists of multiple properties. For struct, these will be its fields.

Most commonly, property has its name and value. Property name should be derived from the struct representation visible by the errors consumer, this will most likely be JSON format.

Nested properties are represented by paths, where each property is delimited by .. Arrays are represented by [<index>]. Let's examine a simple teacher/student example:

package university

type Teacher struct {
	Name     string    `json:"name"`
	Students []Student `json:"students"`
}

type Student struct {
	Index string
}

We can distinguish the following property paths:

  • name
  • students
  • students[0].Index (let's assume there's only a single student)

If any property rule fails PropertyError is returned.

PropertyRules

PropertyRules aggregates rules for a single property.

PropertyRulesForEach

PropertyRulesForEach is an extension of PropertyRules, it provides means of defining rules for each property in a slice.

Currently, it only works with slices, maps are not supported.

Rule

Rules validate a concrete value. If a rule is not met it returns RuleError.

SingleRule

This is the most basic validation building block. Its error code can be set using WithErrorCode function and its error message can also be enhanced using WithDetails function. Details are delimited by ; character.

RuleSet

Rule sets are used to aggregate multiple SingleRule into a single validation rule. It wraps any and all errors returned from single rules in a container which is later on unpacked. If you use either WithErrorCode or WithDetails functions, each error will be extended with the provided details and error code.

Errors

Each validation level defines an error which adds further details of what went wrong.

ValidatorError

Adds top level entity name, following our teacher example, it would be simply teacher. Although that once again depends on how your end use perceives this entity. It wraps multiple PropertyError.

PropertyError

Adds both property name and value. Property value is converted to a string representation. It wraps multiple RuleError.

RuleError

The most basic building block for validation errors, associated with a single failing SingleRule. It conveys an error message and ErrorCode.

Error codes

To aid the process of testing, ErrorCode has been introduced along with a helper functions WithErrorCode to associate Rule with an error code and AddCode to associate multiple error codes with a single Rule. Multiple error codes are delimited by :, similar to how wrapped errors are represented in Go.

To check if ErrorCode is part if a given validation error, use HasErrorCode.

FAQ

Why not use existing validation library?

Existing, established solutions are mostly based on struct tags and heavily utilize reflection. This leaves type safety an issue to be solved and handled by developers. For simple use cases, covered by predefined validation functions, this solutions works well enough. However when adding custom validation rules, type casting has to be heavily utilized, and it becomes increasingly harder to track what exactly is being validated. Another issue is the readability of the errors, it's often hard or even impossible to shape the error to the developer liking.

Acknowledgements

Heavily inspired by C# FluentValidation.

Documentation

Overview

Package validation implements a functional API for consistent struct level validation.

Index

Examples

Constants

View Source
const ErrorCodeSeparator = ":"

Variables

This section is empty.

Functions

func HasErrorCode added in v0.50.0

func HasErrorCode(err error, code ErrorCode) bool

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/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 JoinErrors

func JoinErrors[T error](b *strings.Builder, errs []T, indent string)

func SliceElementName added in v0.59.0

func SliceElementName(sliceName string, index int) string

Types

type ErrorCode added in v0.50.0

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"
	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"
)

type HashFunction added in v0.59.0

type HashFunction[V any, H comparable] func(v V) H

HashFunction accepts a value and returns a comparable hash.

func SelfHashFunc added in v0.59.0

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 Predicate added in v0.50.0

type Predicate[S any] func(S) bool

type PropertyError added in v0.50.0

type PropertyError struct {
	PropertyName  string       `json:"propertyName"`
	PropertyValue string       `json:"propertyValue"`
	Errors        []*RuleError `json:"errors"`
}

func NewPropertyError added in v0.50.0

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/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) Error added in v0.50.0

func (e *PropertyError) Error() string

func (*PropertyError) HideValue added in v0.66.0

func (e *PropertyError) HideValue() *PropertyError

HideValue hides the property value from PropertyError.Error and also hides it from.

func (*PropertyError) PrependPropertyName added in v0.50.0

func (e *PropertyError) PrependPropertyName(name string) *PropertyError

type PropertyErrors added in v0.59.0

type PropertyErrors []*PropertyError

func (PropertyErrors) Error added in v0.59.0

func (e PropertyErrors) Error() string

func (PropertyErrors) HideValue added in v0.66.0

func (e PropertyErrors) HideValue() PropertyErrors

type PropertyGetter added in v0.50.0

type PropertyGetter[T, S any] func(S) T

func GetSelf added in v0.50.0

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/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 PropertyRules added in v0.50.0

type PropertyRules[T, S any] struct {
	// contains filtered or unexported fields
}

PropertyRules is responsible for validating a single property.

func For added in v0.59.0

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 added in v0.59.0

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/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 added in v0.65.0

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]) HideValue added in v0.66.0

func (r PropertyRules[T, S]) HideValue() PropertyRules[T, S]

func (PropertyRules[T, S]) Include added in v0.50.0

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/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 added in v0.65.0

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/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 added in v0.59.0

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/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 added in v0.50.0

func (r PropertyRules[T, S]) Rules(rules ...Rule[T]) PropertyRules[T, S]

func (PropertyRules[T, S]) StopOnError added in v0.50.0

func (r PropertyRules[T, S]) StopOnError() PropertyRules[T, S]
Example

To fail validation immediately after certain Rule fails use PropertyRules.StopOnError. You need to call it directly AFTER you've called PropertyRules.Rules.

package main

import (
	"fmt"
	"time"

	"github.com/nobl9/nobl9-go/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").
			Rules(validation.NotEqualTo("Jerry")).
			StopOnError().
			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]) Validate added in v0.50.0

func (r PropertyRules[T, S]) Validate(st S) PropertyErrors

Validate validates the property value using provided rules. nolint: gocognit

func (PropertyRules[T, S]) When added in v0.50.0

func (r PropertyRules[T, S]) When(predicates ...Predicate[S]) PropertyRules[T, S]
Example

To only proceed with further validation on condition, use PropertyRules.When. Similar to PropertyRules.Rules predicates provided through PropertyRules.When are evaluated in the order they are provided. If a predicate is not met, proceeding (as defined in code) validation rules are not evaluated.

package main

import (
	"fmt"
	"time"

	"github.com/nobl9/nobl9-go/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").
			Rules(validation.NotEqualTo("Jerry")).
			When(func(t Teacher) bool { return t.Name == "Tom" }).
			Rules(alwaysFailingRule),
	).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 '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]) WithName added in v0.50.0

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/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 PropertyRulesForEach added in v0.59.0

type PropertyRulesForEach[T, S any] struct {
	// contains filtered or unexported fields
}

PropertyRulesForEach is responsible for validating a single property.

func ForEach added in v0.59.0

func ForEach[T, S any](getter PropertyGetter[[]T, S]) PropertyRulesForEach[T, S]

ForEach creates a new PropertyRulesForEach 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 ForEach function to do just that. It returns a new struct PropertyRulesForEach which behaves exactly the same as PropertyRules, but extends its API slightly.

To define rules for each element use: - PropertyRulesForEach.RulesForEach - PropertyRulesForEach.IncludeForEach These work exactly the same way as PropertyRules.Rules and PropertyRules.Include on each slice element.

PropertyRulesForEach.Rules is in turn used to define rules for the whole slice.

NOTE: PropertyRulesForEach 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 fro slices has the following format: <slice_name>[<index>].<slice_property_name>

package main

import (
	"fmt"
	"time"

	"github.com/nobl9/nobl9-go/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.ForEach(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[1].index' with value '9182300123':
    - length must be between 9 and 9
  - '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

func (PropertyRulesForEach[T, S]) IncludeForEach added in v0.59.0

func (r PropertyRulesForEach[T, S]) IncludeForEach(rules ...Validator[T]) PropertyRulesForEach[T, S]

func (PropertyRulesForEach[T, S]) Rules added in v0.59.0

func (r PropertyRulesForEach[T, S]) Rules(rules ...Rule[[]T]) PropertyRulesForEach[T, S]

func (PropertyRulesForEach[T, S]) RulesForEach added in v0.59.0

func (r PropertyRulesForEach[T, S]) RulesForEach(rules ...Rule[T]) PropertyRulesForEach[T, S]

func (PropertyRulesForEach[T, S]) StopOnError added in v0.59.0

func (r PropertyRulesForEach[T, S]) StopOnError() PropertyRulesForEach[T, S]

func (PropertyRulesForEach[T, S]) Validate added in v0.59.0

func (r PropertyRulesForEach[T, S]) Validate(st S) PropertyErrors

Validate executes each of the rules sequentially and aggregates the encountered errors. nolint: prealloc, gocognit

func (PropertyRulesForEach[T, S]) When added in v0.59.0

func (r PropertyRulesForEach[T, S]) When(predicate Predicate[S]) PropertyRulesForEach[T, S]

func (PropertyRulesForEach[T, S]) WithName added in v0.59.0

func (r PropertyRulesForEach[T, S]) WithName(name string) PropertyRulesForEach[T, S]

type Rule

type Rule[T any] interface {
	Validate(v T) error
}

Rule is the interface for all validation rules.

type RuleError added in v0.50.0

type RuleError struct {
	Message string    `json:"error"`
	Code    ErrorCode `json:"code,omitempty"`
}

func NewRequiredError added in v0.59.0

func NewRequiredError() *RuleError

func NewRuleError added in v0.63.1

func NewRuleError(message string, codes ...ErrorCode) *RuleError

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.

func (*RuleError) AddCode added in v0.50.0

func (r *RuleError) AddCode(code ErrorCode) *RuleError

AddCode extends the RuleError with the given error code. Codes are prepended, the last code in chain is always the first one set. Example:

ruleError.AddCode("code").AddCode("another").AddCode("last")

This will result in 'last:another:code' ErrorCode.

func (*RuleError) Error added in v0.50.0

func (r *RuleError) Error() string

func (*RuleError) HideValue added in v0.66.0

func (r *RuleError) HideValue(stringValue string) *RuleError

HideValue replaces all occurrences of stringValue in the [RuleError.Message] with an '*' characters.

type RuleSet added in v0.50.0

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/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 added in v0.50.0

func NewRuleSet[T any](rules ...Rule[T]) RuleSet[T]

NewRuleSet creates a new RuleSet instance.

func StringIsDNSSubdomain

func StringIsDNSSubdomain() RuleSet[string]

func (RuleSet[T]) Validate added in v0.50.0

func (r RuleSet[T]) Validate(v T) error

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 added in v0.59.0

func (r RuleSet[T]) WithDetails(format string, a ...any) RuleSet[T]

WithDetails adds details to each returned RuleError error message.

func (RuleSet[T]) WithErrorCode added in v0.50.0

func (r RuleSet[T]) WithErrorCode(code ErrorCode) RuleSet[T]

WithErrorCode sets the error code for each returned RuleError.

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 EqualTo added in v0.50.0

func EqualTo[T comparable](compared T) SingleRule[T]

func Forbidden added in v0.59.0

func Forbidden[T any]() SingleRule[T]

func GreaterThan added in v0.50.0

func GreaterThan[T constraints.Ordered](n T) SingleRule[T]

func GreaterThanOrEqualTo added in v0.50.0

func GreaterThanOrEqualTo[T constraints.Ordered](n T) SingleRule[T]

func LessThan added in v0.50.0

func LessThan[T constraints.Ordered](n T) SingleRule[T]

func LessThanOrEqualTo added in v0.50.0

func LessThanOrEqualTo[T constraints.Ordered](n T) SingleRule[T]

func MapLength added in v0.59.0

func MapLength[M ~map[K]V, K comparable, V any](min, max int) SingleRule[M]

func MapMaxLength added in v0.59.0

func MapMaxLength[M ~map[K]V, K comparable, V any](max int) SingleRule[M]

func MapMinLength added in v0.59.0

func MapMinLength[M ~map[K]V, K comparable, V any](min int) SingleRule[M]

func MutuallyExclusive added in v0.61.0

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 added in v0.50.0

func NewSingleRule[T any](validate func(v T) error) SingleRule[T]

NewSingleRule creates a new SingleRule instance.

func NotEqualTo added in v0.50.0

func NotEqualTo[T comparable](compared T) SingleRule[T]

func OneOf added in v0.59.0

func OneOf[T comparable](values ...T) SingleRule[T]

func Required added in v0.50.0

func Required[T any]() SingleRule[T]

func SliceLength added in v0.59.0

func SliceLength[S ~[]E, E any](min, max int) SingleRule[S]

func SliceMaxLength added in v0.59.0

func SliceMaxLength[S ~[]E, E any](max int) SingleRule[S]

func SliceMinLength added in v0.59.0

func SliceMinLength[S ~[]E, E any](min int) SingleRule[S]

func SliceUnique added in v0.59.0

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 added in v0.59.0

func StringASCII() SingleRule[string]

func StringContains added in v0.59.0

func StringContains(substrings ...string) SingleRule[string]

func StringDenyRegexp added in v0.59.0

func StringDenyRegexp(re *regexp.Regexp, examples ...string) SingleRule[string]

func StringDescription

func StringDescription() SingleRule[string]

func StringJSON added in v0.59.0

func StringJSON() SingleRule[string]

func StringLength

func StringLength(min, max int) SingleRule[string]

func StringMatchRegexp added in v0.59.0

func StringMatchRegexp(re *regexp.Regexp, examples ...string) SingleRule[string]

func StringMaxLength added in v0.59.0

func StringMaxLength(max int) SingleRule[string]

func StringMinLength added in v0.59.0

func StringMinLength(min int) SingleRule[string]

func StringNotEmpty added in v0.59.0

func StringNotEmpty() SingleRule[string]

func StringURL added in v0.59.0

func StringURL() SingleRule[string]

func StringUUID added in v0.65.0

func StringUUID() SingleRule[string]

func URL added in v0.67.0

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:

By default, it will construct a new RuleError.

func (SingleRule[T]) WithDetails added in v0.59.0

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/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 does not match regular expression: '^(Tom|Jerry)$'; Teacher can be either Tom or Jerry :)

func (SingleRule[T]) WithErrorCode added in v0.50.0

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/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

type Transformer added in v0.65.0

type Transformer[T, N any] func(T) (N, error)

type Validator added in v0.50.0

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/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.ForEach(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[1].index' with value '9182300123':
    - length must be between 9 and 9
  - '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
  - '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/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 does not match regular expression: '^\s*$'

func New added in v0.50.0

func New[S any](props ...propertyRulesI[S]) Validator[S]

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/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 added in v0.50.0

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 added in v0.59.0

func (v Validator[S]) When(predicates ...Predicate[S]) 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/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 added in v0.59.0

func (v Validator[S]) WithName(name string) Validator[S]

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/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 added in v0.59.0

type ValidatorError struct {
	Errors PropertyErrors `json:"errors"`
	Name   string         `json:"name"`
}

func NewValidatorError added in v0.59.0

func NewValidatorError(errs PropertyErrors) *ValidatorError

func (*ValidatorError) Error added in v0.59.0

func (e *ValidatorError) Error() string

func (*ValidatorError) WithName added in v0.59.0

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/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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL