validation

package
v0.83.0 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2024 License: MPL-2.0 Imports: 13 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

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

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

func MapElementName added in v0.80.0

func MapElementName(mapName, key any) string

func SliceElementName

func SliceElementName(sliceName string, index int) string

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 Predicate

type Predicate[S any] func(S) bool

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"`
	Package    string     `json:"package,omitempty"`
	IsOptional bool       `json:"isOptional,omitempty"`
	IsHidden   bool       `json:"isHidden,omitempty"`
	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:

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 Rule

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

Rule is the interface for all validation rules.

type RuleError

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

func NewRequiredError

func NewRequiredError() *RuleError

func NewRuleError

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

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

func (r *RuleError) Error() string

func (*RuleError) HideValue

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

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

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

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

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

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

WithDetails adds details to each returned RuleError error message.

func (RuleSet[T]) WithErrorCode

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 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](lower, upper int) SingleRule[M]

func MapMaxLength

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

func MapMinLength

func MapMinLength[M ~map[K]V, K comparable, V any](limit 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](lower, upper int) SingleRule[S]

func SliceMaxLength

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

func SliceMinLength

func SliceMinLength[S ~[]E, E any](limit 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(lower, upper int) SingleRule[string]

func StringMatchRegexp

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

func StringMaxLength

func StringMaxLength(limit int) SingleRule[string]

func StringMinLength

func StringMinLength(limit 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:

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 Transformer[T, N any] func(T) (N, error)

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

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

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

Jump to

Keyboard shortcuts

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