diff --git a/Makefile b/Makefile index be8c7e6..d314bcc 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,13 @@ test: $(call _print_step,Running unit tests) go test -race -cover ./... +.PHONY: test/coverage +## Produce test coverage report and inspect it in browser. +test/coverage: + $(call _print_step,Running test coverage report) + go test -coverprofile=coverage.out ./... + go tool cover -html=coverage.out + .PHONY: check check/vet check/lint check/gosec check/spell check/trailing check/markdown check/format check/generate check/vulns ## Run all checks. check: check/vet check/lint check/gosec check/spell check/trailing check/markdown check/format check/generate check/vulns @@ -87,9 +94,9 @@ check/format: $(call _print_check_step,Checking if files are formatted) ./scripts/check-formatting.sh -.PHONY: generate generate/code generate/readme generate/gomarkdoc +.PHONY: generate generate/code generate/readme ## Auto generate files. -generate: generate/code generate/readme generate/gomarkdoc +generate: generate/code generate/readme ## Generate Golang code. generate/code: @@ -101,16 +108,6 @@ generate/readme: echo "Generating README.md embedded examples..." ./scripts/embed-example-in-readme.bash README.md -## Generate Markdown docs from Go docs. -generate/gomarkdoc: - echo "Generating Markdown docs from Go docs..." - gomarkdoc \ - --output docs/DOCUMENTATION.md \ - --format github \ - --repository.default-branch main \ - --repository.url https://github.com/nobl9/govy \ - ./pkg/... - .PHONY: format format/go format/cspell ## Format files. format: format/go format/cspell diff --git a/README.md b/README.md index a769d8c..12308ee 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,26 @@ to change, breaking changes may be introduced with new versions until v1 is released. Checkout [roadmap](./docs/ROADMAP.md) for upcoming, planned features. +## Legend + +1. [Getting started](#getting-started) + 1. [Use cases](#use-cases) +2. [Building blocks](#building-blocks) + 1. [Errors](#errors) +3. [Features](#features) + 1. [Type safety](#type-safety) + 2. [Immutability](#immutability) + 3. [Verbose error messages](#verbose-error-messages) + 4. [Predefined rules](#predefined-rules) + 5. [Custom rules](#custom-rules) + 6. [Validation plan](#validation-plan) + 7. [Properties name inference](#properties-name-inference) +4. [Rationale](#rationale) + 1. [Reflection](#reflection) + 2. [Trivia](#trivia) +5. [Development](#development) +6. [Acknowledgments](#acknowledgments) + ## Getting started In order to add the library to your project, run: @@ -28,8 +48,8 @@ powered by Go's [testable examples](https://go.dev/blog/examples), to access it visit [pkg.go.dev](https://pkg.go.dev/github.com/nobl9/govy) or locally at [example_test.go](./pkg/govy/example_test.go). -Govy's code documentation is available at [pkg.go.dev](https://pkg.go.dev/github.com/nobl9/govy) -as well as in generated [Markdown format](./docs/DOCUMENTATION.md). +Govy's code documentation is available at +[pkg.go.dev](https://pkg.go.dev/github.com/nobl9/govy). Here's a quick example of `govy` in action: @@ -133,7 +153,7 @@ func Example_basicUsage() { // - 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 + // - elements are not unique, 1st and 3rd elements collide // - 'students[1].index' with value '9182300123': // - length must be between 9 and 9 // - 'university.name': @@ -565,6 +585,11 @@ were associated with given property was tedious to say the least. Around the same time, Go 1.18 was released with generics support, we started playing with them, and the idea for `govy` was born. +## Development + +Checkout both [contributing guidelines](./docs/CONTRIBUTING.md) and +[development instructions](./docs/DEVELOPMENT.md). + ## Acknowledgments The API along with the accompanying nomenclature was heavily inspired by the diff --git a/cspell.yaml b/cspell.yaml index 6e1e301..0fe8910 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -38,7 +38,6 @@ words: - goimports - golangci - golines - - gomarkdoc - goroot - gosec - govulncheck @@ -47,7 +46,6 @@ words: - ldflags - nobl - pkgs - - plantuml - println - rtmp - sloctl diff --git a/devbox.json b/devbox.json index af1a7cc..f708811 100644 --- a/devbox.json +++ b/devbox.json @@ -8,8 +8,7 @@ "golangci-lint@1.60", "gosec@latest", "go@1.22", - "gotools@0.22", - "gomarkdoc@1.1" + "gotools@0.22" ], "shell": { "init_hook": [ diff --git a/devbox.lock b/devbox.lock index f99c5c7..fffff2a 100644 --- a/devbox.lock +++ b/devbox.lock @@ -189,53 +189,6 @@ } } }, - "gomarkdoc@1.1": { - "last_modified": "2024-08-14T11:41:26Z", - "resolved": "github:NixOS/nixpkgs/0cb2fd7c59fed0cd82ef858cbcbdb552b9a33465#gomarkdoc", - "source": "devbox-search", - "version": "1.1.0", - "systems": { - "aarch64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/yyfwkknqpmighcdhd7mpgs7j0wacyn9m-gomarkdoc-1.1.0", - "default": true - } - ], - "store_path": "/nix/store/yyfwkknqpmighcdhd7mpgs7j0wacyn9m-gomarkdoc-1.1.0" - }, - "aarch64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/0c1717ixhs89vpc8qc3dlraj89w09p27-gomarkdoc-1.1.0", - "default": true - } - ], - "store_path": "/nix/store/0c1717ixhs89vpc8qc3dlraj89w09p27-gomarkdoc-1.1.0" - }, - "x86_64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/w8ywczb794fvh2fwrb9906fdnwkax1si-gomarkdoc-1.1.0", - "default": true - } - ], - "store_path": "/nix/store/w8ywczb794fvh2fwrb9906fdnwkax1si-gomarkdoc-1.1.0" - }, - "x86_64-linux": { - "outputs": [ - { - "path": "/nix/store/ggpkp2n14pk7f4m3niwks5dpyxpxj9dx-gomarkdoc-1.1.0", - "default": true - } - ], - "store_path": "/nix/store/ggpkp2n14pk7f4m3niwks5dpyxpxj9dx-gomarkdoc-1.1.0" - } - } - }, "gosec@latest": { "last_modified": "2024-08-14T11:41:26Z", "resolved": "github:NixOS/nixpkgs/0cb2fd7c59fed0cd82ef858cbcbdb552b9a33465#gosec", diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..d23598c --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing to govy + +If you're here, chances are you want to contribute ;) +Thanks a lot for that! + +Your pull request will be reviewed by one of the maintainers. +We encourage and welcome any and all feedback. + +## Before you contribute + +The goal of this project is to develop a validation library with +functional interface which is strongly-typed, produces information rich errors +and has API that is easy to understand and use. + +Make sure you're familiarized with +[development instructions](./DEVELOPMENT.md). + +## Making a pull request + +Please make a fork of this repo and submit a PR from there. +More information can be found +[here](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request). + +## Merge Request title + +Try to be as descriptive as you can in your PR title. +Note that the title must adhere to the rules defined in +[this workflow](./.github/workflows/pr-title.yml). + +## License + +Govy is licensed under Mozilla Public License Version 2.0, see [LICENSE](../LICENSE). diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..367504a --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,76 @@ +# Development + +This document describes the intricacies of govy development workflow. +If you see anything missing, feel free to contribute to this document :) + +## Pull requests + +[Pull request template](../.github/pull_request_template.md) +is provided when you create new PR. +Section worth noting and getting familiar with is located under +`## Release Notes` header. + +## Makefile + +Govy ships with a Makefile which is well documented and should cover most if +not all development cycle needs. +Run `make help` to display short description for each target. + +## CI + +Continuous integration pipelines utilize the same Makefile commands which +you run locally within reproducible `devbox` environment. +This ensures consistent behavior of the executed checks +and makes local debugging easier. + +## Testing + +You can run all unit tests with `make test`. +We also encourage inspecting test coverage during development, you can verify +if the paths you're interested in are covered with `make test/coverage`. + +## Releases + +Govy adheres to the Go's official release workflow recommendations and +requirements. Refer to the official +[Go docs](https://go.dev/doc/modules/release-workflow) for more details. + +### Release automation + +We're using [Release Drafter](https://github.com/release-drafter/release-drafter) +to automate release notes creation. Drafter also does its best to propose +the next release version based on commit messages from `main` branch. + +Release Drafter is also responsible for auto-labeling pull requests. +It checks both title and body of the pull request and adds appropriate labels. \ +**NOTE:** The auto-labeling mechanism will not remove labels once they're +created. For example, If you end up changing PR title from `sec:` to `fix:` +you'll have to manually remove `security` label. + +On each commit to `main` branch, Release Drafter will update the next release +draft. + +In addition to Release Drafter, we're also running a script which extracts +explicitly listed release notes and breaking changes which are optionally +defined in `## Release Notes` and `## Breaking Changes` headers. +It also performs a cleanup of the PR draft mitigating Release Drafter +shortcomings. + +## Code generation + +Some parts of the codebase are automatically generated. +We use the following tools to do that: + +- [embed-example-in-readme.bash](../scripts/embed-example-in-readme.bash) + for embedding tested examples in [README.md](../README.md). + +## Validation + +We're using our own validation library to write validation for all objects. +Refer to this [README.md](../internal/validation/README.md) for more information. + +## Dependencies + +Renovate is configured to automatically merge minor and patch updates. +For major versions, which sadly includes GitHub Actions, manual approval +is required. diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md deleted file mode 100644 index 43381d9..0000000 --- a/docs/DOCUMENTATION.md +++ /dev/null @@ -1,3737 +0,0 @@ - - -# govy - -```go -import "github.com/nobl9/govy/pkg/govy" -``` - -Package govy implements a functional API for consistent, type\-safe validation. - -## Index - -- [Constants](<#constants>) -- [func HasErrorCode\(err error, code ErrorCode\) bool](<#HasErrorCode>) -- [func MapElementName\(mapName, key any\) string](<#MapElementName>) -- [func SliceElementName\(sliceName string, index int\) string](<#SliceElementName>) -- [type CascadeMode](<#CascadeMode>) -- [type ErrorCode](<#ErrorCode>) -- [type MapItem](<#MapItem>) -- [type Predicate](<#Predicate>) -- [type PropertyError](<#PropertyError>) - - [func NewPropertyError\(propertyName string, propertyValue interface\{\}, errs ...error\) \*PropertyError](<#NewPropertyError>) - - [func \(e \*PropertyError\) Equal\(cmp \*PropertyError\) bool](<#PropertyError.Equal>) - - [func \(e \*PropertyError\) Error\(\) string](<#PropertyError.Error>) - - [func \(e \*PropertyError\) HideValue\(\) \*PropertyError](<#PropertyError.HideValue>) - - [func \(e \*PropertyError\) PrependParentPropertyName\(name string\) \*PropertyError](<#PropertyError.PrependParentPropertyName>) -- [type PropertyErrors](<#PropertyErrors>) - - [func \(e PropertyErrors\) Error\(\) string](<#PropertyErrors.Error>) - - [func \(e PropertyErrors\) HideValue\(\) PropertyErrors](<#PropertyErrors.HideValue>) -- [type PropertyGetter](<#PropertyGetter>) - - [func GetSelf\[S any\]\(\) PropertyGetter\[S, S\]](<#GetSelf>) -- [type PropertyPlan](<#PropertyPlan>) -- [type PropertyRules](<#PropertyRules>) - - [func For\[T, S any\]\(getter PropertyGetter\[T, S\]\) PropertyRules\[T, S\]](<#For>) - - [func ForPointer\[T, S any\]\(getter PropertyGetter\[\*T, S\]\) PropertyRules\[T, S\]](<#ForPointer>) - - [func Transform\[T, N, S any\]\(getter PropertyGetter\[T, S\], transform Transformer\[T, N\]\) PropertyRules\[N, S\]](<#Transform>) - - [func \(r PropertyRules\[T, S\]\) Cascade\(mode CascadeMode\) PropertyRules\[T, S\]](<#PropertyRules[T, S].Cascade>) - - [func \(r PropertyRules\[T, S\]\) HideValue\(\) PropertyRules\[T, S\]](<#PropertyRules[T, S].HideValue>) - - [func \(r PropertyRules\[T, S\]\) Include\(rules ...Validator\[T\]\) PropertyRules\[T, S\]](<#PropertyRules[T, S].Include>) - - [func \(r PropertyRules\[T, S\]\) OmitEmpty\(\) PropertyRules\[T, S\]](<#PropertyRules[T, S].OmitEmpty>) - - [func \(r PropertyRules\[T, S\]\) Required\(\) PropertyRules\[T, S\]](<#PropertyRules[T, S].Required>) - - [func \(r PropertyRules\[T, S\]\) Rules\(rules ...validationInterface\[T\]\) PropertyRules\[T, S\]](<#PropertyRules[T, S].Rules>) - - [func \(r PropertyRules\[T, S\]\) Validate\(st S\) error](<#PropertyRules[T, S].Validate>) - - [func \(r PropertyRules\[T, S\]\) When\(predicate Predicate\[S\], opts ...WhenOptions\) PropertyRules\[T, S\]](<#PropertyRules[T, S].When>) - - [func \(r PropertyRules\[T, S\]\) WithExamples\(examples ...string\) PropertyRules\[T, S\]](<#PropertyRules[T, S].WithExamples>) - - [func \(r PropertyRules\[T, S\]\) WithName\(name string\) PropertyRules\[T, S\]](<#PropertyRules[T, S].WithName>) -- [type PropertyRulesForMap](<#PropertyRulesForMap>) - - [func ForMap\[M \~map\[K\]V, K comparable, V, S any\]\(getter PropertyGetter\[M, S\]\) PropertyRulesForMap\[M, K, V, S\]](<#ForMap>) - - [func \(r PropertyRulesForMap\[M, K, V, S\]\) Cascade\(mode CascadeMode\) PropertyRulesForMap\[M, K, V, S\]](<#PropertyRulesForMap[M, K, V, S].Cascade>) - - [func \(r PropertyRulesForMap\[M, K, V, S\]\) IncludeForItems\(rules ...Validator\[MapItem\[K, V\]\]\) PropertyRulesForMap\[M, K, V, S\]](<#PropertyRulesForMap[M, K, V, S].IncludeForItems>) - - [func \(r PropertyRulesForMap\[M, K, V, S\]\) IncludeForKeys\(validators ...Validator\[K\]\) PropertyRulesForMap\[M, K, V, S\]](<#PropertyRulesForMap[M, K, V, S].IncludeForKeys>) - - [func \(r PropertyRulesForMap\[M, K, V, S\]\) IncludeForValues\(rules ...Validator\[V\]\) PropertyRulesForMap\[M, K, V, S\]](<#PropertyRulesForMap[M, K, V, S].IncludeForValues>) - - [func \(r PropertyRulesForMap\[M, K, V, S\]\) Rules\(rules ...validationInterface\[M\]\) PropertyRulesForMap\[M, K, V, S\]](<#PropertyRulesForMap[M, K, V, S].Rules>) - - [func \(r PropertyRulesForMap\[M, K, V, S\]\) RulesForItems\(rules ...validationInterface\[MapItem\[K, V\]\]\) PropertyRulesForMap\[M, K, V, S\]](<#PropertyRulesForMap[M, K, V, S].RulesForItems>) - - [func \(r PropertyRulesForMap\[M, K, V, S\]\) RulesForKeys\(rules ...validationInterface\[K\]\) PropertyRulesForMap\[M, K, V, S\]](<#PropertyRulesForMap[M, K, V, S].RulesForKeys>) - - [func \(r PropertyRulesForMap\[M, K, V, S\]\) RulesForValues\(rules ...validationInterface\[V\]\) PropertyRulesForMap\[M, K, V, S\]](<#PropertyRulesForMap[M, K, V, S].RulesForValues>) - - [func \(r PropertyRulesForMap\[M, K, V, S\]\) Validate\(st S\) error](<#PropertyRulesForMap[M, K, V, S].Validate>) - - [func \(r PropertyRulesForMap\[M, K, V, S\]\) When\(predicate Predicate\[S\], opts ...WhenOptions\) PropertyRulesForMap\[M, K, V, S\]](<#PropertyRulesForMap[M, K, V, S].When>) - - [func \(r PropertyRulesForMap\[M, K, V, S\]\) WithExamples\(examples ...string\) PropertyRulesForMap\[M, K, V, S\]](<#PropertyRulesForMap[M, K, V, S].WithExamples>) - - [func \(r PropertyRulesForMap\[M, K, V, S\]\) WithName\(name string\) PropertyRulesForMap\[M, K, V, S\]](<#PropertyRulesForMap[M, K, V, S].WithName>) -- [type PropertyRulesForSlice](<#PropertyRulesForSlice>) - - [func ForSlice\[T, S any\]\(getter PropertyGetter\[\[\]T, S\]\) PropertyRulesForSlice\[T, S\]](<#ForSlice>) - - [func \(r PropertyRulesForSlice\[T, S\]\) Cascade\(mode CascadeMode\) PropertyRulesForSlice\[T, S\]](<#PropertyRulesForSlice[T, S].Cascade>) - - [func \(r PropertyRulesForSlice\[T, S\]\) IncludeForEach\(rules ...Validator\[T\]\) PropertyRulesForSlice\[T, S\]](<#PropertyRulesForSlice[T, S].IncludeForEach>) - - [func \(r PropertyRulesForSlice\[T, S\]\) Rules\(rules ...validationInterface\[\[\]T\]\) PropertyRulesForSlice\[T, S\]](<#PropertyRulesForSlice[T, S].Rules>) - - [func \(r PropertyRulesForSlice\[T, S\]\) RulesForEach\(rules ...validationInterface\[T\]\) PropertyRulesForSlice\[T, S\]](<#PropertyRulesForSlice[T, S].RulesForEach>) - - [func \(r PropertyRulesForSlice\[T, S\]\) Validate\(st S\) error](<#PropertyRulesForSlice[T, S].Validate>) - - [func \(r PropertyRulesForSlice\[T, S\]\) When\(predicate Predicate\[S\], opts ...WhenOptions\) PropertyRulesForSlice\[T, S\]](<#PropertyRulesForSlice[T, S].When>) - - [func \(r PropertyRulesForSlice\[T, S\]\) WithExamples\(examples ...string\) PropertyRulesForSlice\[T, S\]](<#PropertyRulesForSlice[T, S].WithExamples>) - - [func \(r PropertyRulesForSlice\[T, S\]\) WithName\(name string\) PropertyRulesForSlice\[T, S\]](<#PropertyRulesForSlice[T, S].WithName>) -- [type Rule](<#Rule>) - - [func NewRule\[T any\]\(validate func\(v T\) error\) Rule\[T\]](<#NewRule>) - - [func \(r Rule\[T\]\) Validate\(v T\) error](<#Rule[T].Validate>) - - [func \(r Rule\[T\]\) WithDescription\(description string\) Rule\[T\]](<#Rule[T].WithDescription>) - - [func \(r Rule\[T\]\) WithDetails\(format string, a ...any\) Rule\[T\]](<#Rule[T].WithDetails>) - - [func \(r Rule\[T\]\) WithErrorCode\(code ErrorCode\) Rule\[T\]](<#Rule[T].WithErrorCode>) - - [func \(r Rule\[T\]\) WithMessage\(format string, a ...any\) Rule\[T\]](<#Rule[T].WithMessage>) -- [type RuleError](<#RuleError>) - - [func NewRuleError\(message string, codes ...ErrorCode\) \*RuleError](<#NewRuleError>) - - [func \(r \*RuleError\) AddCode\(code ErrorCode\) \*RuleError](<#RuleError.AddCode>) - - [func \(r \*RuleError\) Error\(\) string](<#RuleError.Error>) - - [func \(r \*RuleError\) HideValue\(stringValue string\) \*RuleError](<#RuleError.HideValue>) -- [type RulePlan](<#RulePlan>) -- [type RuleSet](<#RuleSet>) - - [func NewRuleSet\[T any\]\(rules ...validationInterface\[T\]\) RuleSet\[T\]](<#NewRuleSet>) - - [func \(r RuleSet\[T\]\) Validate\(v T\) error](<#RuleSet[T].Validate>) - - [func \(r RuleSet\[T\]\) WithErrorCode\(code ErrorCode\) RuleSet\[T\]](<#RuleSet[T].WithErrorCode>) -- [type RuleSetError](<#RuleSetError>) - - [func \(r RuleSetError\) Error\(\) string](<#RuleSetError.Error>) -- [type Transformer](<#Transformer>) -- [type Validator](<#Validator>) - - [func New\[S any\]\(props ...validationInterface\[S\]\) Validator\[S\]](<#New>) - - [func \(v Validator\[S\]\) InferName\(\) Validator\[S\]](<#Validator[S].InferName>) - - [func \(v Validator\[S\]\) Validate\(st S\) error](<#Validator[S].Validate>) - - [func \(v Validator\[S\]\) When\(predicate Predicate\[S\], opts ...WhenOptions\) Validator\[S\]](<#Validator[S].When>) - - [func \(v Validator\[S\]\) WithName\(name string\) Validator\[S\]](<#Validator[S].WithName>) -- [type ValidatorError](<#ValidatorError>) - - [func NewValidatorError\(errs PropertyErrors\) \*ValidatorError](<#NewValidatorError>) - - [func \(e \*ValidatorError\) Error\(\) string](<#ValidatorError.Error>) - - [func \(e \*ValidatorError\) WithName\(name string\) \*ValidatorError](<#ValidatorError.WithName>) -- [type ValidatorPlan](<#ValidatorPlan>) - - [func Plan\[S any\]\(v Validator\[S\]\) \*ValidatorPlan](<#Plan>) -- [type WhenOptions](<#WhenOptions>) - - [func WhenDescription\(format string, a ...interface\{\}\) WhenOptions](<#WhenDescription>) - - -## Constants - - - -```go -const ( - ErrorCodeSeparator = ":" -) -``` - - -## func [HasErrorCode]() - -```go -func HasErrorCode(err error, code ErrorCode) bool -``` - -HasErrorCode checks if an error contains given [ErrorCode](<#ErrorCode>). It supports all govy errors. - -
Example -

- -To inspect if an error contains a given [govy.ErrorCode](<#ErrorCode>), use [govy.HasErrorCode](<#HasErrorCode>) function. This function will also return true if the expected [govy.ErrorCode](<#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' - -```go -package main - -import ( - "fmt" - "regexp" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.NewRuleSet( - rules.StringLength(1, 5), - rules.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")), - ). - WithErrorCode("teacher_name") - - v := govy.New( - govy.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 []govy.ErrorCode{ - "teacher_name", - "string_length", - "string_match_regexp", - } { - if govy.HasErrorCode(err, code) { - fmt.Println("Has error code:", code) - } - } - } - -} -``` - -#### Output - -``` -Has error code: teacher_name -Has error code: string_length -Has error code: string_match_regexp -``` - -

-
- - -## func [MapElementName]() - -```go -func MapElementName(mapName, key any) string -``` - -MapElementName generates a name for a map element denoted by its key. - - -## func [SliceElementName]() - -```go -func SliceElementName(sliceName string, index int) string -``` - -SliceElementName generates a name for a slice element. - - -## type [CascadeMode]() - -CascadeMode defines how validation should behave when an error is encountered. - -```go -type CascadeMode uint -``` - - - -```go -const ( - // CascadeModeContinue will continue validation after first error. - CascadeModeContinue CascadeMode = iota - // CascadeModeStop will stop validation on first error encountered. - CascadeModeStop -) -``` - - -## type [ErrorCode]() - -ErrorCode is a unique string that represents a specific [RuleError](<#RuleError>). It can be used to precisely identify the error without inspecting its message. - -```go -type ErrorCode = string -``` - - - -```go -const ( - ErrorCodeTransform ErrorCode = "transform" -) -``` - - -## type [MapItem]() - -MapItem is a tuple container for map's key and value pair. - -```go -type MapItem[K comparable, V any] struct { - Key K - Value V -} -``` - - -## type [Predicate]() - -Predicate defines a function that returns a boolean value. - -```go -type Predicate[T any] func(T) bool -``` - - -## type [PropertyError]() - - - -```go -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]() - -```go -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 [govy.NewPropertyError](<#NewPropertyError>) and return [govy.PropertyError](<#PropertyError>) from your validation rule. Note that you can still use [govy.ErrorCode](<#ErrorCode>) and pass [govy.RuleError](<#RuleError>) to the constructor. You can pass any number of [govy.RuleError](<#RuleError>). - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" -) - -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 := govy.New( - govy.For(govy.GetSelf[Teacher]()). - Rules(govy.NewRule(func(t Teacher) error { - if t.Name == "Jake" { - return govy.NewPropertyError( - "name", - t.Name, - govy.NewRuleError("name cannot be Jake", "error_code_jake"), - govy.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.(*govy.ValidatorError).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]() - -```go -func (e *PropertyError) Equal(cmp *PropertyError) bool -``` - -Equal checks if two [PropertyError](<#PropertyError>) are equal. - - -### func \(\*PropertyError\) [Error]() - -```go -func (e *PropertyError) Error() string -``` - - - - -### func \(\*PropertyError\) [HideValue]() - -```go -func (e *PropertyError) HideValue() *PropertyError -``` - -HideValue hides the property value from each of the \[PropertyError.Errors\]. - - -### func \(\*PropertyError\) [PrependParentPropertyName]() - -```go -func (e *PropertyError) PrependParentPropertyName(name string) *PropertyError -``` - -PrependParentPropertyName prepends a given name to the \[PropertyError.PropertyName\]. - - -## type [PropertyErrors]() - -PropertyErrors is a slice of [PropertyError](<#PropertyError>). - -```go -type PropertyErrors []*PropertyError -``` - - -### func \(PropertyErrors\) [Error]() - -```go -func (e PropertyErrors) Error() string -``` - - - - -### func \(PropertyErrors\) [HideValue]() - -```go -func (e PropertyErrors) HideValue() PropertyErrors -``` - - - - -## type [PropertyGetter]() - -PropertyGetter is a function that extracts a property value of type T from a given parent value of type S. - -```go -type PropertyGetter[T, S any] func(S) T -``` - - -### func [GetSelf]() - -```go -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 [govy.Validator](<#Validator>) for, you can use [govy.GetSelf](<#GetSelf>) function which is a convenience [govy.PropertyGetter](<#PropertyGetter>) that returns self. Note that we don't call [govy.PropertyRules.WithName](<#PropertyRules.WithName>) here, as we're comparing two properties in our top level, \[Teacher\] scope. - -You can provide your own rules using [govy.NewRule](<#NewRule>) constructor. It returns new [govy.Rule](<#Rule>) instance which wraps your validation function. - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" -) - -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 := govy.NewRule(func(v Teacher) error { - return fmt.Errorf("now I have access to the whole teacher") - }) - - v := govy.New( - govy.For(govy.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]() - -PropertyPlan is a validation plan for a single [PropertyRules](<#PropertyRules>). - -```go -type PropertyPlan struct { - // Path is a JSON path to the property. - Path string `json:"path"` - // Type is a Go type name of the property. - Type string `json:"type"` - // Package is the full package path of the Type. - Package string `json:"package,omitempty"` - // IsOptional indicates if the property was marked with [PropertyRules.OmitEmpty]. - IsOptional bool `json:"isOptional,omitempty"` - // IsHidden indicates if the property was marked with [PropertyRules.HideValue]. - IsHidden bool `json:"isHidden,omitempty"` - Examples []string `json:"examples,omitempty"` - Rules []RulePlan `json:"rules,omitempty"` -} -``` - - -## type [PropertyRules]() - -PropertyRules is responsible for validating a single property. It is a collection of rules, predicates, and other properties that define how the property should be validated. IT is the middle\-level building block of the validation process, aggregated by [Validator](<#Validator>) and aggregating [Rule](<#Rule>). - -```go -type PropertyRules[T, S any] struct { - // contains filtered or unexported fields -} -``` - - -### func [For]() - -```go -func For[T, S any](getter PropertyGetter[T, S]) PropertyRules[T, S] -``` - -For creates a new [PropertyRules](<#PropertyRules>) instance for the property which value is extracted through [PropertyGetter](<#PropertyGetter>) function. - - -### func [ForPointer]() - -```go -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 -

- -[govy.For](<#For>) constructor creates new [govy.PropertyRules](<#PropertyRules>) instance. It's only argument, [govy.PropertyGetter](<#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. - -[govy.ForPointer](<#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 [govy.PropertyGetter](<#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. - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.New( - govy.ForPointer(func(t Teacher) *string { return t.MiddleName }). - WithName("middleName"). - Rules(rules.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]() - -```go -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](<#PropertyGetter>) is transformed through [Transformer](<#Transformer>) function. If [Transformer](<#Transformer>) returns an error, the validation will not proceed and transformation error will be reported. [Transformer](<#Transformer>) is only called if [PropertyGetter](<#PropertyGetter>) returns a non\-zero value. - - -### func \(PropertyRules\[T, S\]\) [Cascade]() - -```go -func (r PropertyRules[T, S]) Cascade(mode CascadeMode) PropertyRules[T, S] -``` - -Cascade sets the [CascadeMode](<#CascadeMode>) for the property, which controls the flow of evaluating the validation rules. - -
Example -

- -To customize how [govy.Rule](<#Rule>) are evaluated use [govy.PropertyRules.Cascade](<#PropertyRules.Cascade>). Use [govy.CascadeModeStop](<#CascadeModeContinue>) to stop validation after the first error. If you wish to revert to the default behavior, use [govy.CascadeModeContinue](<#CascadeModeContinue>). - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.NewRule(func(string) error { - return fmt.Errorf("always fails") - }) - - v := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - WithName("name"). - Cascade(govy.CascadeModeStop). - Rules(rules.NEQ("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]() - -```go -func (r PropertyRules[T, S]) HideValue() PropertyRules[T, S] -``` - -HideValue hides the property value in the error message. It's useful when the value is sensitive and should not be exposed. - -
Example -

- -Sometimes you want to hide the value of the property in the error message. It can contain sensitive information, like a secret access key. You can use [govy.PropertyRules.HideValue](<#PropertyRules.HideValue>) to achieve that. - -You can see that the error message now contains "\[hidden\]" instead of the actual value, and the property value is not included in the property bullet point \(\- 'name'\). - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" -) - -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 := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - WithName("name"). - HideValue(). - Rules(govy.NewRule(func(name string) error { return fmt.Errorf("that Jake is secret") })), - ).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': - - that [hidden] is secret -``` - -

-
- - -### func \(PropertyRules\[T, S\]\) [Include]() - -```go -func (r PropertyRules[T, S]) Include(rules ...Validator[T]) PropertyRules[T, S] -``` - -Include embeds specified [Validator](<#Validator>) and its [PropertyRules](<#PropertyRules>) into the property. - -
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 [govy.PropertyRules.Include](<#PropertyRules.Include>) to include another [govy.Validator](<#Validator>) in our [govy.PropertyRules](<#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. - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.New( - govy.For(func(u University) string { return u.Address }). - WithName("address"). - Required(), - ) - teacherValidation := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - WithName("name"). - Rules(rules.EQ("Tom")), - govy.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]() - -```go -func (r PropertyRules[T, S]) OmitEmpty() PropertyRules[T, S] -``` - -OmitEmpty sets the property rules to be omitted if its value is its type's zero value. - -
Example -

- -While [govy.ForPointer](<#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. [govy.PropertyRules.OmitEmpty](<#PropertyRules.OmitEmpty>) will do the trick. - -Note: [govy.PropertyRules.OmitEmpty](<#PropertyRules.OmitEmpty>) will have no effect on pointers handled by [govy.ForPointer](<#ForPointer>), as they already behave in the same way. - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" -) - -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 := govy.NewRule(func(string) error { - return fmt.Errorf("always fails") - }) - - v := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - WithName("name"). - OmitEmpty(). - Rules(alwaysFailingRule), - govy.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]() - -```go -func (r PropertyRules[T, S]) Required() PropertyRules[T, S] -``` - -Required sets the property as required. If the property is its type's zero value a \[rules.ErrorCodeRequired\] will be returned. - -
Example -

- -By default, when [govy.PropertyRules](<#PropertyRules>) is constructed using [govy.ForPointer](<#ForPointer>) it will skip validation of the property if the pointer is nil. To enforce a value is set for pointer use [govy.PropertyRules.Required](<#PropertyRules.Required>). - -You may ask yourself why not just use \[rules.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 \[rules.Required\] rule on top of [govy.PropertyRules.Required](<#PropertyRules.Required>). - -[govy.PropertyRules.Required](<#PropertyRules.Required>) when used with [govy.For](<#For>) constructor, will ensure the property does not contain a zero value. - -Note: [govy.PropertyRules.Required](<#PropertyRules.Required>) is introducing a short circuit. If the assertion fails, validation will stop and return \[govy.govy.ErrorCodeRequired\]. None of the rules you've defined would be evaluated. - -Note: Placement of [govy.PropertyRules.Required](<#PropertyRules.Required>) does not matter, it's not evaluated in a sequential loop, unlike standard [govy.Rule](<#Rule>). However, we recommend you always place it below [govy.PropertyRules.WithName](<#PropertyRules.WithName>) to make your rules more readable. - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" -) - -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 := govy.NewRule(func(string) error { - return fmt.Errorf("always fails") - }) - - v := govy.New( - govy.ForPointer(func(t Teacher) *string { return t.MiddleName }). - WithName("middleName"). - Required(). - Rules(alwaysFailingRule), - govy.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]() - -```go -func (r PropertyRules[T, S]) Rules(rules ...validationInterface[T]) PropertyRules[T, S] -``` - -Rules associates provided [Rule](<#Rule>) with the property. - - -### func \(PropertyRules\[T, S\]\) [Validate]() - -```go -func (r PropertyRules[T, S]) Validate(st S) error -``` - -Validate validates the property value using provided rules. - - -### func \(PropertyRules\[T, S\]\) [When]() - -```go -func (r PropertyRules[T, S]) When(predicate Predicate[S], opts ...WhenOptions) PropertyRules[T, S] -``` - -When defines a [Predicate](<#Predicate>) which determines when the rules for this property should be evaluated. It can be called multiple times to set multiple predicates. Additionally, it accepts [WhenOptions](<#WhenOptions>) which customizes the behavior of the predicate. - -
Example -

- -To only run property validation on condition, use [govy.PropertyRules.When](<#PropertyRules.When>). Predicates set through [govy.PropertyRules.When](<#PropertyRules.When>) are evaluated in the order they are provided. If any predicate is not met, validation rules are not evaluated for the whole [govy.PropertyRules](<#PropertyRules>). - -It's recommended to define [govy.PropertyRules.When](<#PropertyRules.When>) before [govy.PropertyRules.Rules](<#PropertyRules.Rules>) declaration. - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - WithName("name"). - When(func(t Teacher) bool { return t.Name == "Jerry" }). - Rules(rules.NEQ("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]() - -```go -func (r PropertyRules[T, S]) WithExamples(examples ...string) PropertyRules[T, S] -``` - -WithExamples sets the examples for the property. - - -### func \(PropertyRules\[T, S\]\) [WithName]() - -```go -func (r PropertyRules[T, S]) WithName(name string) PropertyRules[T, S] -``` - -WithName sets the name of the property. If the name was inferred, it will be overridden. - -
Example -

- -So far we've been using a very simple [govy.PropertyRules](<#PropertyRules>) instance: - -``` -validation.For(func(t Teacher) string { return t.Name }). - Rules(validation.NewRule(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 [govy.PropertyRules.WithName](<#PropertyRules.WithName>). - -We can also change the [govy.Rule](<#Rule>) to be something more real. govy comes with a number of predefined [govy.Rule](<#Rule>), we'll use \[rules.EQ\] which accepts a single argument, value to compare with. - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - WithName("name"). - Rules(rules.EQ("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]() - -PropertyRulesForMap is responsible for validating a single property. - -```go -type PropertyRulesForMap[M ~map[K]V, K comparable, V, S any] struct { - // contains filtered or unexported fields -} -``` - - -### func [ForMap]() - -```go -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](<#PropertyRulesForMap>) instance for a map property which value is extracted through [PropertyGetter](<#PropertyGetter>) function. - -
Example -

- -When dealing with maps there are three forms of iteration: \- keys \- values \- key\-value pairs \(items\) - -You can use [govy.ForMap](<#ForMap>) function to define rules for all the aforementioned iterators. It returns a new struct [govy.PropertyRulesForMap](<#PropertyRulesForMap>) which behaves similar to [govy.PropertyRulesForSlice](<#PropertyRulesForSlice>).. - -To define rules for keys use: \- [govy.PropertyRulesForMap.RulesForKeys](<#PropertyRulesForMap.RulesForKeys>) \- [govy.PropertyRulesForMap.IncludeForKeys](<#PropertyRulesForMap.IncludeForKeys>) \- [govy.PropertyRulesForMap.RulesForValues](<#PropertyRulesForMap.RulesForValues>) \- [govy.PropertyRulesForMap.IncludeForValues](<#PropertyRulesForMap.IncludeForValues>) \- [govy.PropertyRulesForMap.RulesForItems](<#PropertyRulesForMap.RulesForItems>) \- [govy.PropertyRulesForMap.IncludeForItems](<#PropertyRulesForMap.IncludeForItems>) These work exactly the same way as [govy.PropertyRules.Rules](<#PropertyRules.Rules>) and [govy.PropertyRules.Include](<#PropertyRules.Include>) verifying each map's key, value or [govy.MapItem](<#MapItem>). - -[govy.PropertyRulesForMap.Rules](<#PropertyRulesForMap.Rules>) is in turn used to define rules for the whole map. - -Note: [govy.PropertyRulesForMap](<#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: \.\.\ - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - WithName("name"). - Rules(rules.NEQ("Eve")), - ) - tutoringValidator := govy.New( - govy.ForMap(func(t Tutoring) map[string]Teacher { return t.StudentIndexToTeacher }). - WithName("students"). - Rules( - rules.MapMaxLength[map[string]Teacher](2), - ). - RulesForKeys( - rules.StringLength(9, 9), - ). - IncludeForValues(teacherValidator). - RulesForItems(govy.NewRule(func(v govy.MapItem[string, Teacher]) error { - if v.Key == "918230013" && v.Value.Name == "Joan" { - return govy.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]() - -```go -func (r PropertyRulesForMap[M, K, V, S]) Cascade(mode CascadeMode) PropertyRulesForMap[M, K, V, S] -``` - -Cascade =\> refer to [PropertyRules.Cascade](<#PropertyRules.Cascade>) documentation. - - -### func \(PropertyRulesForMap\[M, K, V, S\]\) [IncludeForItems]() - -```go -func (r PropertyRulesForMap[M, K, V, S]) IncludeForItems(rules ...Validator[MapItem[K, V]]) PropertyRulesForMap[M, K, V, S] -``` - -IncludeForItems associates specified [Validator](<#Validator>) and its [PropertyRules](<#PropertyRules>) with [MapItem](<#MapItem>). It allows validating both key and value in conjunction. - - -### func \(PropertyRulesForMap\[M, K, V, S\]\) [IncludeForKeys]() - -```go -func (r PropertyRulesForMap[M, K, V, S]) IncludeForKeys(validators ...Validator[K]) PropertyRulesForMap[M, K, V, S] -``` - -IncludeForKeys associates specified [Validator](<#Validator>) and its [PropertyRules](<#PropertyRules>) with map's keys. - - -### func \(PropertyRulesForMap\[M, K, V, S\]\) [IncludeForValues]() - -```go -func (r PropertyRulesForMap[M, K, V, S]) IncludeForValues(rules ...Validator[V]) PropertyRulesForMap[M, K, V, S] -``` - -IncludeForValues associates specified [Validator](<#Validator>) and its [PropertyRules](<#PropertyRules>) with map's values. - - -### func \(PropertyRulesForMap\[M, K, V, S\]\) [Rules]() - -```go -func (r PropertyRulesForMap[M, K, V, S]) Rules(rules ...validationInterface[M]) PropertyRulesForMap[M, K, V, S] -``` - -Rules adds [Rule](<#Rule>) for the whole map. - - -### func \(PropertyRulesForMap\[M, K, V, S\]\) [RulesForItems]() - -```go -func (r PropertyRulesForMap[M, K, V, S]) RulesForItems(rules ...validationInterface[MapItem[K, V]]) PropertyRulesForMap[M, K, V, S] -``` - -RulesForItems adds [Rule](<#Rule>) for [MapItem](<#MapItem>). It allows validating both key and value in conjunction. - - -### func \(PropertyRulesForMap\[M, K, V, S\]\) [RulesForKeys]() - -```go -func (r PropertyRulesForMap[M, K, V, S]) RulesForKeys(rules ...validationInterface[K]) PropertyRulesForMap[M, K, V, S] -``` - -RulesForKeys adds [Rule](<#Rule>) for map's keys. - - -### func \(PropertyRulesForMap\[M, K, V, S\]\) [RulesForValues]() - -```go -func (r PropertyRulesForMap[M, K, V, S]) RulesForValues(rules ...validationInterface[V]) PropertyRulesForMap[M, K, V, S] -``` - -RulesForValues adds [Rule](<#Rule>) for map's values. - - -### func \(PropertyRulesForMap\[M, K, V, S\]\) [Validate]() - -```go -func (r PropertyRulesForMap[M, K, V, S]) Validate(st S) error -``` - -Validate executes each of the rules sequentially and aggregates the encountered errors. - - -### func \(PropertyRulesForMap\[M, K, V, S\]\) [When]() - -```go -func (r PropertyRulesForMap[M, K, V, S]) When(predicate Predicate[S], opts ...WhenOptions) PropertyRulesForMap[M, K, V, S] -``` - -When =\> refer to [PropertyRules.When](<#PropertyRules.When>) documentation. - - -### func \(PropertyRulesForMap\[M, K, V, S\]\) [WithExamples]() - -```go -func (r PropertyRulesForMap[M, K, V, S]) WithExamples(examples ...string) PropertyRulesForMap[M, K, V, S] -``` - -WithExamples =\> refer to [PropertyRules.WithExamples](<#PropertyRules.WithExamples>) documentation. - - -### func \(PropertyRulesForMap\[M, K, V, S\]\) [WithName]() - -```go -func (r PropertyRulesForMap[M, K, V, S]) WithName(name string) PropertyRulesForMap[M, K, V, S] -``` - -WithName =\> refer to [PropertyRules.When](<#PropertyRules.When>) documentation. - - -## type [PropertyRulesForSlice]() - -PropertyRulesForSlice is responsible for validating a single property. - -```go -type PropertyRulesForSlice[T, S any] struct { - // contains filtered or unexported fields -} -``` - - -### func [ForSlice]() - -```go -func ForSlice[T, S any](getter PropertyGetter[[]T, S]) PropertyRulesForSlice[T, S] -``` - -ForSlice creates a new [PropertyRulesForSlice](<#PropertyRulesForSlice>) instance for a slice property which value is extracted through [PropertyGetter](<#PropertyGetter>) function. - -
Example -

- -When dealing with slices we often want to both validate the whole slice and each of its elements. You can use [govy.ForSlice](<#ForSlice>) function to do just that. It returns a new struct [govy.PropertyRulesForSlice](<#PropertyRulesForSlice>) which behaves exactly the same as [govy.PropertyRules](<#PropertyRules>), but extends its API slightly. - -To define rules for each element use: \- [govy.PropertyRulesForSlice.RulesForEach](<#PropertyRulesForSlice.RulesForEach>) \- [govy.PropertyRulesForSlice.IncludeForEach](<#PropertyRulesForSlice.IncludeForEach>) These work exactly the same way as [govy.PropertyRules.Rules](<#PropertyRules.Rules>) and [govy.PropertyRules.Include](<#PropertyRules.Include>) verifying each slice element. - -[govy.PropertyRulesForSlice.Rules](<#PropertyRulesForSlice.Rules>) is in turn used to define rules for the whole slice. - -Note: [govy.PropertyRulesForSlice](<#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\] [govy.Validator](<#Validator>). Notice that property path for slices has the following format: \\[\\].\ - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.New( - govy.For(func(s Student) string { return s.Index }). - WithName("index"). - Rules(rules.StringLength(9, 9)), - ) - teacherValidator := govy.New( - govy.ForSlice(func(t Teacher) []Student { return t.Students }). - WithName("students"). - Rules( - rules.SliceMaxLength[[]Student](2), - rules.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]() - -```go -func (r PropertyRulesForSlice[T, S]) Cascade(mode CascadeMode) PropertyRulesForSlice[T, S] -``` - -Cascade =\> refer to [PropertyRules.Cascade](<#PropertyRules.Cascade>) documentation. - - -### func \(PropertyRulesForSlice\[T, S\]\) [IncludeForEach]() - -```go -func (r PropertyRulesForSlice[T, S]) IncludeForEach(rules ...Validator[T]) PropertyRulesForSlice[T, S] -``` - -IncludeForEach associates specified [Validator](<#Validator>) and its [PropertyRules](<#PropertyRules>) with each element of the slice. - - -### func \(PropertyRulesForSlice\[T, S\]\) [Rules]() - -```go -func (r PropertyRulesForSlice[T, S]) Rules(rules ...validationInterface[[]T]) PropertyRulesForSlice[T, S] -``` - -Rules adds [Rule](<#Rule>) for the whole slice. - - -### func \(PropertyRulesForSlice\[T, S\]\) [RulesForEach]() - -```go -func (r PropertyRulesForSlice[T, S]) RulesForEach(rules ...validationInterface[T]) PropertyRulesForSlice[T, S] -``` - -RulesForEach adds [Rule](<#Rule>) for each element of the slice. - - -### func \(PropertyRulesForSlice\[T, S\]\) [Validate]() - -```go -func (r PropertyRulesForSlice[T, S]) Validate(st S) error -``` - -Validate executes each of the rules sequentially and aggregates the encountered errors. - - -### func \(PropertyRulesForSlice\[T, S\]\) [When]() - -```go -func (r PropertyRulesForSlice[T, S]) When(predicate Predicate[S], opts ...WhenOptions) PropertyRulesForSlice[T, S] -``` - -When =\> refer to [PropertyRules.When](<#PropertyRules.When>) documentation. - - -### func \(PropertyRulesForSlice\[T, S\]\) [WithExamples]() - -```go -func (r PropertyRulesForSlice[T, S]) WithExamples(examples ...string) PropertyRulesForSlice[T, S] -``` - -WithExamples =\> refer to [PropertyRules.WithExamples](<#PropertyRules.WithExamples>) documentation. - - -### func \(PropertyRulesForSlice\[T, S\]\) [WithName]() - -```go -func (r PropertyRulesForSlice[T, S]) WithName(name string) PropertyRulesForSlice[T, S] -``` - -WithName =\> refer to [PropertyRules.WithName](<#PropertyRules.WithName>) documentation. - - -## type [Rule]() - -Rule is the basic validation building block. It evaluates the provided validation function and enhances it with optional [ErrorCode](<#ErrorCode>) and arbitrary details. - -```go -type Rule[T any] struct { - // contains filtered or unexported fields -} -``` - - -### func [NewRule]() - -```go -func NewRule[T any](validate func(v T) error) Rule[T] -``` - -NewRule creates a new [Rule](<#Rule>) instance. - - -### func \(Rule\[T\]\) [Validate]() - -```go -func (r Rule[T]) Validate(v T) error -``` - -Validate runs validation function on the provided value. It can handle different types of errors returned by the function: - -- \*[RuleError](<#RuleError>), which details and [ErrorCode](<#ErrorCode>) are optionally extended with the ones defined by [Rule](<#Rule>). -- \*[PropertyError](<#PropertyError>), for each of its errors their [ErrorCode](<#ErrorCode>) is extended with the one defined by [Rule](<#Rule>). - -By default, it will construct a new RuleError. - - -### func \(Rule\[T\]\) [WithDescription]() - -```go -func (r Rule[T]) WithDescription(description string) Rule[T] -``` - -WithDescription adds a custom description to the rule. It is used to enhance the [RulePlan](<#RulePlan>), but otherwise does not appear in standard [RuleError.Error](<#RuleError.Error>) output. - -
Example -

- -[govy.Rule](<#Rule>) error might be static, i.e. a single [govy.Rule](<#Rule>) always returns the same exact error message, but they don't have to. For instance, consider a rule which parses a URL using [net/url]() package. - -This makes it very hard to infer error message for [govy.RulePlan](<#RulePlan>), if not impossible, since the exact error might only be known during runtime. - -To solve this problem, you can use [govy.Rule.WithDescription](<#Rule.WithDescription>) to provide a verbose and informative rule description. It will be only included in the [govy.RulePlan](<#RulePlan>) and otherwise not displayed in the default [govy.RuleError.Error](<#RuleError.Error>). However, it is available in the structured [govy.RuleError](<#RuleError>). - -```go -package main - -import ( - "fmt" - "regexp" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - WithName("name"). - Rules(rules.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")). - WithDetails("Teacher can be either Tom or Jerry :)"). - WithMessage("unsupported name")), - ).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': - - unsupported name; Teacher can be either Tom or Jerry :) -``` - -

-
- - -### func \(Rule\[T\]\) [WithDetails]() - -```go -func (r Rule[T]) WithDetails(format string, a ...any) Rule[T] -``` - -WithDetails adds details to the returned [RuleError](<#RuleError>) error message. - -
Example -

- -You can use [govy.Rule.WithDetails](<#Rule.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. - -```go -package main - -import ( - "fmt" - "regexp" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - WithName("name"). - Rules(rules.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 \(Rule\[T\]\) [WithErrorCode]() - -```go -func (r Rule[T]) WithErrorCode(code ErrorCode) Rule[T] -``` - -WithErrorCode sets the error code for the returned [RuleError](<#RuleError>). - -
Example -

- -When testing, it can be tedious to always rely on error messages as these can change over time. Enter [govy.ErrorCode](<#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 [govy.Rule.WithErrorCode](<#Rule.WithErrorCode>) to associate [govy.ErrorCode](<#ErrorCode>) with a [govy.Rule](<#Rule>). Notice that our modified version of \[rules.StringMatchRegexp\] will now return a new [govy.ErrorCode](<#ErrorCode>). Predefined rules have [govy.ErrorCode](<#ErrorCode>) already associated with them. To view the list of predefined [govy.ErrorCode](<#ErrorCode>) checkout error\_codes.go file. - -```go -package main - -import ( - "fmt" - "regexp" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - WithName("name"). - Rules(rules.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.(*govy.ValidatorError).Errors - ruleErrors := propertyErrors[0].Errors - fmt.Println(ruleErrors[0].Code) - } - -} -``` - -#### Output - -``` -custom_code -``` - -

-
- - -### func \(Rule\[T\]\) [WithMessage]() - -```go -func (r Rule[T]) WithMessage(format string, a ...any) Rule[T] -``` - -WithMessage overrides the returned [RuleError](<#RuleError>) error message with message. - -
Example -

- -If you want to override the default error message, you can use [govy.Rule.WithMessage](<#Rule.WithMessage>). - -```go -package main - -import ( - "fmt" - "regexp" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - WithName("name"). - Rules(rules.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")). - WithDetails("Teacher can be either Tom or Jerry :)"). - WithMessage("unsupported name")), - ).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': - - unsupported name; Teacher can be either Tom or Jerry :) -``` - -

-
- - -## type [RuleError]() - -RuleError is the fundamental error container associated with a [Rule](<#Rule>). - -```go -type RuleError struct { - Message string `json:"error"` - Code ErrorCode `json:"code,omitempty"` - Details string `json:"details,omitempty"` - Description string `json:"description,omitempty"` -} -``` - - -### func [NewRuleError]() - -```go -func NewRuleError(message string, codes ...ErrorCode) *RuleError -``` - -NewRuleError creates a new [RuleError](<#RuleError>) with the given message and optional error codes. Error codes are added according to the rules defined by [RuleError.AddCode](<#RuleError.AddCode>). - - -### func \(\*RuleError\) [AddCode]() - -```go -func (r *RuleError) AddCode(code ErrorCode) *RuleError -``` - -AddCode extends the [RuleError](<#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](<#ErrorCode>). - - -### func \(\*RuleError\) [Error]() - -```go -func (r *RuleError) Error() string -``` - - - - -### func \(\*RuleError\) [HideValue]() - -```go -func (r *RuleError) HideValue(stringValue string) *RuleError -``` - -HideValue replaces all occurrences of a string in the \[RuleError.Message\] with a '\*' characters. - - -## type [RulePlan]() - -RulePlan is a validation plan for a single [Rule](<#Rule>). - -```go -type RulePlan struct { - Description string `json:"description"` - Details string `json:"details,omitempty"` - ErrorCode ErrorCode `json:"errorCode,omitempty"` - // Conditions are all the predicates set through [PropertyRules.When] and [Validator.When] - // which had [WhenDescription] added to the [WhenOptions]. - Conditions []string `json:"conditions,omitempty"` -} -``` - - -## type [RuleSet]() - -RuleSet allows defining a [Rule](<#Rule>) which aggregates multiple sub\-rules. - -```go -type RuleSet[T any] struct { - // contains filtered or unexported fields -} -``` - -
Example -

- -Sometimes it's useful to aggregate multiple [govy.Rule](<#Rule>) into a single, composite rule. To do that we'll use [govy.RuleSet](<#RuleSet>) and [govy.NewRuleSet](<#NewRuleSet>) constructor. RuleSet is a simple container for multiple [govy.Rule](<#Rule>). During validation it is unpacked and each [govy.RuleError](<#RuleError>) is reported separately. - -Note that govy uses similar syntax to wrapped errors in Go; a ':' delimiter is used to chain error codes together. - -```go -package main - -import ( - "fmt" - "regexp" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.NewRuleSet( - rules.StringLength(1, 5), - rules.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")). - WithDetails("Teacher can be either Tom or Jerry :)"), - ). - WithErrorCode("teacher_name") - - v := govy.New( - govy.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.(*govy.ValidatorError).Errors - ruleErrors := propertyErrors[0].Errors - fmt.Printf("Error codes: %s, %s\n\n", ruleErrors[0].Code, ruleErrors[1].Code) - fmt.Println(err) - } - -} -``` - -#### Output - -``` -Error codes: teacher_name:string_length, teacher_name:string_match_regexp - -Validation for Teacher has failed for the following properties: - - 'name' with value 'Jonathan': - - length must be between 1 and 5 - - string must match regular expression: '^(Tom|Jerry)$'; Teacher can be either Tom or Jerry :) -``` - -

-
- - -### func [NewRuleSet]() - -```go -func NewRuleSet[T any](rules ...validationInterface[T]) RuleSet[T] -``` - -NewRuleSet creates a new [RuleSet](<#RuleSet>) instance. - - -### func \(RuleSet\[T\]\) [Validate]() - -```go -func (r RuleSet[T]) Validate(v T) error -``` - -Validate works the same way as [Rule.Validate](<#Rule.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\]\) [WithErrorCode]() - -```go -func (r RuleSet[T]) WithErrorCode(code ErrorCode) RuleSet[T] -``` - -WithErrorCode sets the error code for each returned [RuleError](<#RuleError>). - - -## type [RuleSetError]() - -RuleSetError is a container for transferring multiple errors reported by [govy.RuleSet](<#RuleSet>). - -```go -type RuleSetError []error -``` - - -### func \(RuleSetError\) [Error]() - -```go -func (r RuleSetError) Error() string -``` - - - - -## type [Transformer]() - -Transformer is a function that transforms a value of type T to a value of type N. If the transformation fails, the function should return an error. - -```go -type Transformer[T, N any] func(T) (N, error) -``` - - -## type [Validator]() - -Validator is the top level validation entity. It serves as an aggregator for [PropertyRules](<#PropertyRules>). Typically, it represents a struct. - -```go -type Validator[S any] struct { - // contains filtered or unexported fields -} -``` - -
Example -

- -Bringing it all \(mostly\) together, let's create a fully fledged [govy.Validator](<#Validator>) for \[Teacher\]. - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.New( - govy.For(func(u University) string { return u.Address }). - WithName("address"). - Required(), - ) - studentValidator := govy.New( - govy.For(func(s Student) string { return s.Index }). - WithName("index"). - Rules(rules.StringLength(9, 9)), - ) - teacherValidator := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - WithName("name"). - Required(). - Rules( - rules.StringNotEmpty(), - rules.OneOf("Jake", "George")), - govy.ForSlice(func(t Teacher) []Student { return t.Students }). - WithName("students"). - Rules( - rules.SliceMaxLength[[]Student](2), - rules.SliceUnique(func(v Student) string { return v.Index })). - IncludeForEach(studentValidator), - govy.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 (Branching Pattern) -

- -When dealing with properties that should only be validated if a certain other property has specific value, it's recommended to use [govy.PropertyRules.When](<#PropertyRules.When>) and [govy.PropertyRules.Include](<#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 [govy.Validator](<#Validator>) for \[CSV\] and \[JSON\] and use [govy.PropertyRules.When](<#PropertyRules.When>) to only validate their included [govy.Validator](<#Validator>) if the correct \[File.Format\] is provided. - -```go -package main - -import ( - "fmt" - "regexp" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.New( - govy.For(func(c CSV) string { return c.Separator }). - WithName("separator"). - Required(). - Rules(rules.OneOf(",", ";")), - ) - - jsonValidation := govy.New( - govy.For(func(j JSON) string { return j.Indent }). - WithName("indent"). - Required(). - Rules(rules.StringMatchRegexp(regexp.MustCompile(`^\s*$`))), - ) - - fileValidation := govy.New( - govy.ForPointer(func(f File) *CSV { return f.CSV }). - When(func(f File) bool { return f.Format == "csv" }). - Include(csvValidation), - govy.ForPointer(func(f File) *JSON { return f.JSON }). - When(func(f File) bool { return f.Format == "json" }). - Include(jsonValidation), - govy.For(func(f File) string { return f.Format }). - WithName("format"). - Required(). - Rules(rules.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]() - -```go -func New[S any](props ...validationInterface[S]) Validator[S] -``` - -New creates a new [Validator](<#Validator>) aggregating the provided property rules. - -
Example -

- -In order to create a new [govy.Validator](<#Validator>) use [govy.New](<#New>) constructor. Let's define simple [govy.PropertyRules](<#PropertyRules>) for \[Teacher.Name\]. For now, it will be always failing. - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" -) - -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 := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - Rules(govy.NewRule(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\]\) [InferName]() - -```go -func (v Validator[S]) InferName() Validator[S] -``` - -InferName will set the name of the [Validator](<#Validator>) to its type S. If the name was already set through [Validator.WithName](<#Validator.WithName>), it will not be overridden. It does not use the same inference mechanisms as \[PropertyRules.InferName\], it simply checks the [Validator](<#Validator>) type parameter using reflection. - - -### func \(Validator\[S\]\) [Validate]() - -```go -func (v Validator[S]) Validate(st S) error -``` - -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](<#ValidatorError>). - - -### func \(Validator\[S\]\) [When]() - -```go -func (v Validator[S]) When(predicate Predicate[S], opts ...WhenOptions) Validator[S] -``` - -When accepts predicates which will be evaluated BEFORE [Validator](<#Validator>) validates ANY rules. - -
Example -

- -[govy.Validator](<#Validator>) rules can be evaluated on condition, to specify the predicate use [govy.Validator.When](<#Validator.When>) function. - -In this example, validation for \[Teacher\] instance will only be evaluated if the \[Teacher.Age\] property is less than 50 years. - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" -) - -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 := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - Rules(govy.NewRule(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.(*govy.ValidatorError).WithName("Tom")) - } - err = v.Validate(teacherJerry) - if err != nil { - fmt.Println(err.(*govy.ValidatorError).WithName("Jerry")) - } - -} -``` - -#### Output - -``` -Validation for Jerry has failed for the following properties: - - always fails -``` - -

-
- - -### func \(Validator\[S\]\) [WithName]() - -```go -func (v Validator[S]) WithName(name string) Validator[S] -``` - -WithName when a rule fails will pass the provided name to [ValidatorError.WithName](<#ValidatorError.WithName>). - -
Example -

- -To associate [govy.Validator](<#Validator>) with an entity name use [govy.Validator.WithName](<#Validator.WithName>) function. When any of the rules fails, the error will contain the entity name you've provided. - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" -) - -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 := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - Rules(govy.NewRule(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]() - -ValidatorError is the top\-level error type for validation errors. It aggregates the property errors of [Validator](<#Validator>). - -```go -type ValidatorError struct { - Errors PropertyErrors `json:"errors"` - Name string `json:"name"` -} -``` - -
Example -

- -All errors returned by [govy.Validator](<#Validator>) are of type [govy.ValidatorError](<#ValidatorError>). Type casting directly to [govy.ValidatorError](<#ValidatorError>) should be safe once an error was asserted to be non\-nil. However, you shouldn't trust any API with such promises, and always type check in your type assignments. - -All error types return by govy are JSON serializable. - -```go -package main - -import ( - "encoding/json" - "fmt" - "os" - "time" - - "github.com/nobl9/govy/pkg/govy" -) - -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 := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - Rules(govy.NewRule(func(name string) error { return fmt.Errorf("always fails") })). - WithName("name"), - ).WithName("Teacher") - - err := v.Validate(Teacher{Name: "John"}) - if err != nil { - if validatorErr, ok := err.(*govy.ValidatorError); ok { - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - if err = enc.Encode(validatorErr); err != nil { - fmt.Printf("error encoding: %v\n", err) - } - } - } - -} -``` - -#### Output - -``` -{ - "errors": [ - { - "propertyName": "name", - "propertyValue": "John", - "errors": [ - { - "error": "always fails" - } - ] - } - ], - "name": "Teacher" -} -``` - -

-
- - -### func [NewValidatorError]() - -```go -func NewValidatorError(errs PropertyErrors) *ValidatorError -``` - - - - -### func \(\*ValidatorError\) [Error]() - -```go -func (e *ValidatorError) Error() string -``` - - - - -### func \(\*ValidatorError\) [WithName]() - -```go -func (e *ValidatorError) WithName(name string) *ValidatorError -``` - -WithName sets the \[ValidatorError.Name\] field. - -
Example -

- -You can also add [govy.Validator](<#Validator>) name during runtime, by calling [govy.ValidatorError.WithName](<#ValidatorError.WithName>) function on the returned error. - -Note: We left the previous "Teacher" name assignment, to demonstrate that the [govy.ValidatorError.WithName](<#ValidatorError.WithName>) function call will overwrite it. - -Note: This would also work: - -``` -err := v.WithName("Jake").Validate(Teacher{}) -``` - -govy, excluding error handling, tries to follow immutability principle. Calling any method on [govy.Validator](<#Validator>) will not change its declared instance, but rather create a copy of it. - -```go -package main - -import ( - "fmt" - "time" - - "github.com/nobl9/govy/pkg/govy" -) - -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 := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - Rules(govy.NewRule(func(name string) error { return fmt.Errorf("always fails") })), - ).WithName("Teacher") - - err := v.Validate(Teacher{}) - if err != nil { - fmt.Println(err.(*govy.ValidatorError).WithName("Jake")) - } - -} -``` - -#### Output - -``` -Validation for Jake has failed for the following properties: - - always fails -``` - -

-
- - -## type [ValidatorPlan]() - -ValidatorPlan is a validation plan for a single [Validator](<#Validator>). - -```go -type ValidatorPlan struct { - Name string `json:"name,omitempty"` - Properties []PropertyPlan `json:"properties"` -} -``` - - -### func [Plan]() - -```go -func Plan[S any](v Validator[S]) *ValidatorPlan -``` - -Plan creates a validation plan for the provided [Validator](<#Validator>). Each property is represented by a [PropertyPlan](<#PropertyPlan>) which aggregates its every [RulePlan](<#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 [govy.Plan](<#Plan>) function\! - -There are multiple ways to improve the generated documentation: - -- Use [govy.PropertyRules.WithExamples](<#PropertyRules.WithExamples>) to provide a list of example values for the property. -- Use [govy.Rule.WithDescription](<#Rule.WithDescription>) to provide a plan\-only description for your rule. For builtin rules, the description is already provided. -- Use [govy.WhenDescription](<#WhenDescription>) to provide a plan\-only description for your when conditions. - -```go -package main - -import ( - "encoding/json" - "fmt" - "os" - "time" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/rules" -) - -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 := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - WithName("name"). - WithExamples("Jake", "John"). - When( - func(t Teacher) bool { return t.Name == "Jerry" }, - govy.WhenDescription("name is Jerry"), - ). - Rules( - rules.NEQ("Jerry"). - WithDetails("Jerry is just a name!"), - govy.NewRule(func(v string) error { - return fmt.Errorf("some custom error") - }). - WithDescription("this is a custom error!"), - ), - ).WithName("Teacher") - - properties := govy.Plan(v) - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - _ = enc.Encode(properties) - -} -``` - -#### Output - -``` -{ - "name": "Teacher", - "properties": [ - { - "path": "$.name", - "type": "string", - "examples": [ - "Jake", - "John" - ], - "rules": [ - { - "description": "should be not equal to 'Jerry'", - "details": "Jerry is just a name!", - "errorCode": "not_equal_to", - "conditions": [ - "name is Jerry" - ] - }, - { - "description": "this is a custom error!", - "conditions": [ - "name is Jerry" - ] - } - ] - } - ] -} -``` - -

-
- - -## type [WhenOptions]() - -WhenOptions defines optional parameters for the When conditions. - -```go -type WhenOptions struct { - // contains filtered or unexported fields -} -``` - - -### func [WhenDescription]() - -```go -func WhenDescription(format string, a ...interface{}) WhenOptions -``` - -WhenDescription sets the description for the When condition. - -# govyconfig - -```go -import "github.com/nobl9/govy/pkg/govyconfig" -``` - -Package govyconfig defines configuration functions for govy. It also holds internal, shared state for the library. The functions defined by govyconfig can be safely called concurrently. However, bear in mind that it is still important to take care of the order of calls to both govy and govyconfig functions. - -For instance calling \[govy.For\] before [SetNameInferMode](<#SetNameInferMode>) will not have any effect, for the \[govy.PropertyRules\] instance created with \[govy.For\]. - -## Index - -- [func GetInferredName\(file string, line int\) string](<#GetInferredName>) -- [func GetNameInferIncludeTestFiles\(\) bool](<#GetNameInferIncludeTestFiles>) -- [func NameInferDefaultRule\(fieldName, tagValue string\) string](<#NameInferDefaultRule>) -- [func SetInferredName\(loc InferredName\)](<#SetInferredName>) -- [func SetLogLevel\(level slog.Level\)](<#SetLogLevel>) -- [func SetNameInferFunc\(rule NameInferFunc\)](<#SetNameInferFunc>) -- [func SetNameInferIncludeTestFiles\(inc bool\)](<#SetNameInferIncludeTestFiles>) -- [func SetNameInferMode\(mode NameInferMode\)](<#SetNameInferMode>) -- [type InferredName](<#InferredName>) -- [type NameInferFunc](<#NameInferFunc>) - - [func GetNameInferFunc\(\) NameInferFunc](<#GetNameInferFunc>) -- [type NameInferMode](<#NameInferMode>) - - [func GetNameInferMode\(\) NameInferMode](<#GetNameInferMode>) - - - -## func [GetInferredName]() - -```go -func GetInferredName(file string, line int) string -``` - -GetInferredName returns the inferred property name for the given file and line. The name has to be first set using [SetInferredName](<#SetInferredName>). It is primarily exported for govy to utilize when NameInferModeGenerate mode is set. - - -## func [GetNameInferIncludeTestFiles]() - -```go -func GetNameInferIncludeTestFiles() bool -``` - -GetNameInferIncludeTestFiles returns whether to include test files in name inference mechanism. - - -## func [NameInferDefaultRule]() - -```go -func NameInferDefaultRule(fieldName, tagValue string) string -``` - -NameInferDefaultRule is the default rule for inferring field names from struct tags, it looks for json and yaml tags, preferring json if both are set. - - -## func [SetInferredName]() - -```go -func SetInferredName(loc InferredName) -``` - -SetInferredName sets the inferred property name for the given file and line. Once it's registered it can be retrieved using [GetInferredName](<#GetInferredName>). It is primarily exported for code generation utility of govy which runs in NameInferModeGenerate. - - -## func [SetLogLevel]() - -```go -func SetLogLevel(level slog.Level) -``` - -SetLogLevel sets the logging level for \[slog.Logger\] used by govy. It's safe to call this function concurrently. - - -## func [SetNameInferFunc]() - -```go -func SetNameInferFunc(rule NameInferFunc) -``` - -SetNameInferFunc sets the rule for inferring field names from struct tags. It overrides the default rule [NameInferDefaultRule](<#NameInferDefaultRule>). It's safe to call this function concurrently. - - -## func [SetNameInferIncludeTestFiles]() - -```go -func SetNameInferIncludeTestFiles(inc bool) -``` - -SetNameInferIncludeTestFiles sets whether to include test files in name inference mechanism. - - -## func [SetNameInferMode]() - -```go -func SetNameInferMode(mode NameInferMode) -``` - -SetNameInferMode sets the mode of property names' inference. It overrides the default mode [NameInferModeDisable](<#NameInferModeDisable>). It's safe to call this function concurrently. - -
Example -

- -In the interactive tutorial for govy, we've been using \[govy.PropertyRules.WithName\] to provide the name for our properties. - -Ideally, we'd want to make sure the names govy assigns to each property, match the name of the real\-world struct representation that the user interacts with. Go uses struct tags to achieve just that, and libraries like [encoding/json]() use their values to encode/decode structs. Unfortunately, there's no easy way to tell what exact property we're returning from \[govy.PropertyGetter\]. - -To solve this problem, govy provides a way to infer the name of the property \(with a catch\). The catch being that the name inference mechanism needs to parse the whole modules' AST. This can be a performance hit, especially for large projects if not done properly. - -By default govy WILL NOT attempt to infer ANY property names. - -So, how do we do that properly? It depends on the [govyconfig.NameInferMode](<#NameInferMode>) used: - -- [govyconfig.NameInferModeDisable](<#NameInferModeDisable>), name inference is disabled \(default\), nothing to do here -- [govyconfig.NameInferModeRuntime](<#NameInferModeDisable>), the name is inferred during runtime, whenever \[govy.For\] is called. This is the most flexible option, but also the slowest. However, If you make sure that \[govy.PropertyRules\] are created only once and don't mind the initial/startup performance hit, this should be enough for you. -- [govyconfig.NameInferModeGenerate](<#NameInferModeDisable>), the name is inferred during code generation. This mode requires you to run the 'cmd/govy nameinfer' BEFORE you run your code. It will generate a file with inferred names for your structs which automatically registers these names using [govyconfig.SetInferredName](<#SetInferredName>). - -Since this tutorial is run as a test, we need to explicitly instruct govy to infer names from test files. In order to do that, we use [govyconfig.SetNameInferIncludeTestFiles](<#SetNameInferIncludeTestFiles>). - -```go -package main - -import ( - "fmt" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/govyconfig" - "github.com/nobl9/govy/pkg/rules" -) - -type Teacher struct { - Name string `json:"name"` -} - -func main() { - govyconfig.SetNameInferIncludeTestFiles(true) - govyconfig.SetNameInferMode(govyconfig.NameInferModeRuntime) - defer govyconfig.SetNameInferIncludeTestFiles(false) - defer govyconfig.SetNameInferMode(govyconfig.NameInferModeDisable) - - v := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - Rules(rules.EQ("Jerry")), - ).WithName("Teacher") - - teacher := Teacher{Name: "Tom"} - err := v.Validate(teacher) - if err != nil { - fmt.Println(err) - } - -} -``` - -#### Output - -``` -Validation for Teacher has failed for the following properties: - - 'name' with value 'Tom': - - should be equal to 'Jerry' -``` - -

-
- -
Example (Invalid Usage) -

- -Beware where you call [govyconfig.SetNameInferMode](<#SetNameInferMode>). If you call it after the \[govy.For\] has been called, it won't do anything. This is because the name inference is done during the creation of \[govy.PropertyRules\]. - -```go -package main - -import ( - "fmt" - - "github.com/nobl9/govy/pkg/govy" - "github.com/nobl9/govy/pkg/govyconfig" - "github.com/nobl9/govy/pkg/rules" -) - -type Teacher struct { - Name string `json:"name"` -} - -func main() { - govyconfig.SetNameInferIncludeTestFiles(true) - defer govyconfig.SetNameInferIncludeTestFiles(false) - - v := govy.New( - govy.For(func(t Teacher) string { return t.Name }). - Rules(rules.EQ("Jerry")), - ).WithName("Teacher") - - govyconfig.SetNameInferMode(govyconfig.NameInferModeRuntime) - defer govyconfig.SetNameInferMode(govyconfig.NameInferModeDisable) - - teacher := Teacher{Name: "Tom"} - err := v.Validate(teacher) - if err != nil { - fmt.Println(err) - } - -} -``` - -#### Output - -``` -Validation for Teacher has failed for the following properties: - - should be equal to 'Jerry' -``` - -

-
- - -## type [InferredName]() - -InferredName represents an inferred property name. - -```go -type InferredName struct { - // Name is the inferred property name. - Name string - // File is the relative path to the file where the [govy.PropertyRules.For] is detected. - File string - // Line is the line number in the File where the [govy.PropertyRules.For] is detected. - Line int -} -``` - - -## type [NameInferFunc]() - -NameInferFunc is a function blueprint for inferring property names. It is only called for struct fields. Tag value is the raw value of the struct tag, it needs to be parsed with [reflect.StructTag](). - -```go -type NameInferFunc func(fieldName, tagValue string) string -``` - - -### func [GetNameInferFunc]() - -```go -func GetNameInferFunc() NameInferFunc -``` - - - - -## type [NameInferMode]() - -NameInferMode defines a mode of property names' inference. - -```go -type NameInferMode int -``` - - - -```go -const ( - // NameInferModeDisable disables property names' inference. - // It is the default mode. - NameInferModeDisable NameInferMode = iota - // NameInferModeRuntime infers property names' during runtime, - // whenever For, ForSlice, ForPointer or ForMap are created. - // If you're not reusing these [govy.PropertyRules], but rather creating them dynamically, - // beware of significant performance cost of the inference mechanism. - NameInferModeRuntime - // NameInferModeGenerate does the heavy lifting of inferring property names - // in a separate step which involves code generation. - // When creating new [govy.PropertyRules], the only performance hit is due to the - // usage of [runtime] package which helps us get the caller frame details. - NameInferModeGenerate -) -``` - - -### func [GetNameInferMode]() - -```go -func GetNameInferMode() NameInferMode -``` - - - -# rules - -```go -import "github.com/nobl9/govy/pkg/rules" -``` - -Package rules provides predefined rules for common validation scenarios. - -## Index - -- [Constants](<#constants>) -- [func DurationPrecision\(precision time.Duration\) govy.Rule\[time.Duration\]](<#DurationPrecision>) -- [func EQ\[T comparable\]\(compared T\) govy.Rule\[T\]](<#EQ>) -- [func Forbidden\[T any\]\(\) govy.Rule\[T\]](<#Forbidden>) -- [func GT\[T constraints.Ordered\]\(compared T\) govy.Rule\[T\]](<#GT>) -- [func GTE\[T constraints.Ordered\]\(compared T\) govy.Rule\[T\]](<#GTE>) -- [func LT\[T constraints.Ordered\]\(compared T\) govy.Rule\[T\]](<#LT>) -- [func LTE\[T constraints.Ordered\]\(compared T\) govy.Rule\[T\]](<#LTE>) -- [func MapLength\[M \~map\[K\]V, K comparable, V any\]\(minLen, maxLen int\) govy.Rule\[M\]](<#MapLength>) -- [func MapMaxLength\[M \~map\[K\]V, K comparable, V any\]\(limit int\) govy.Rule\[M\]](<#MapMaxLength>) -- [func MapMinLength\[M \~map\[K\]V, K comparable, V any\]\(limit int\) govy.Rule\[M\]](<#MapMinLength>) -- [func MutuallyExclusive\[S any\]\(required bool, getters map\[string\]func\(s S\) any\) govy.Rule\[S\]](<#MutuallyExclusive>) -- [func NEQ\[T comparable\]\(compared T\) govy.Rule\[T\]](<#NEQ>) -- [func OneOf\[T comparable\]\(values ...T\) govy.Rule\[T\]](<#OneOf>) -- [func Required\[T any\]\(\) govy.Rule\[T\]](<#Required>) -- [func SliceLength\[S \~\[\]E, E any\]\(minLen, maxLen int\) govy.Rule\[S\]](<#SliceLength>) -- [func SliceMaxLength\[S \~\[\]E, E any\]\(limit int\) govy.Rule\[S\]](<#SliceMaxLength>) -- [func SliceMinLength\[S \~\[\]E, E any\]\(limit int\) govy.Rule\[S\]](<#SliceMinLength>) -- [func SliceUnique\[S \[\]V, V any, H comparable\]\(hashFunc HashFunction\[V, H\], constraints ...string\) govy.Rule\[S\]](<#SliceUnique>) -- [func StringASCII\(\) govy.Rule\[string\]](<#StringASCII>) -- [func StringContains\(substrings ...string\) govy.Rule\[string\]](<#StringContains>) -- [func StringDNSLabel\(\) govy.RuleSet\[string\]](<#StringDNSLabel>) -- [func StringDenyRegexp\(re \*regexp.Regexp, examples ...string\) govy.Rule\[string\]](<#StringDenyRegexp>) -- [func StringEndsWith\(suffixes ...string\) govy.Rule\[string\]](<#StringEndsWith>) -- [func StringJSON\(\) govy.Rule\[string\]](<#StringJSON>) -- [func StringLength\(minLen, maxLen int\) govy.Rule\[string\]](<#StringLength>) -- [func StringMatchRegexp\(re \*regexp.Regexp, examples ...string\) govy.Rule\[string\]](<#StringMatchRegexp>) -- [func StringMaxLength\(limit int\) govy.Rule\[string\]](<#StringMaxLength>) -- [func StringMinLength\(limit int\) govy.Rule\[string\]](<#StringMinLength>) -- [func StringNotEmpty\(\) govy.Rule\[string\]](<#StringNotEmpty>) -- [func StringStartsWith\(prefixes ...string\) govy.Rule\[string\]](<#StringStartsWith>) -- [func StringTitle\(\) govy.Rule\[string\]](<#StringTitle>) -- [func StringURL\(\) govy.Rule\[string\]](<#StringURL>) -- [func StringUUID\(\) govy.Rule\[string\]](<#StringUUID>) -- [func URL\(\) govy.Rule\[\*url.URL\]](<#URL>) -- [type HashFunction](<#HashFunction>) - - [func HashFuncSelf\[H comparable\]\(\) HashFunction\[H, H\]](<#HashFuncSelf>) - - -## Constants - - - -```go -const ( - ErrorCodeRequired govy.ErrorCode = internal.RequiredErrorCodeString - ErrorCodeForbidden govy.ErrorCode = "forbidden" - ErrorCodeEqualTo govy.ErrorCode = "equal_to" - ErrorCodeNotEqualTo govy.ErrorCode = "not_equal_to" - ErrorCodeGreaterThan govy.ErrorCode = "greater_than" - ErrorCodeGreaterThanOrEqualTo govy.ErrorCode = "greater_than_or_equal_to" - ErrorCodeLessThan govy.ErrorCode = "less_than" - ErrorCodeLessThanOrEqualTo govy.ErrorCode = "less_than_or_equal_to" - ErrorCodeStringNotEmpty govy.ErrorCode = "string_not_empty" - ErrorCodeStringMatchRegexp govy.ErrorCode = "string_match_regexp" - ErrorCodeStringDenyRegexp govy.ErrorCode = "string_deny_regexp" - ErrorCodeStringDNSLabel govy.ErrorCode = "string_dns_label" - ErrorCodeStringASCII govy.ErrorCode = "string_ascii" - ErrorCodeStringURL govy.ErrorCode = "string_url" - ErrorCodeStringUUID govy.ErrorCode = "string_uuid" - ErrorCodeStringJSON govy.ErrorCode = "string_json" - ErrorCodeStringContains govy.ErrorCode = "string_contains" - ErrorCodeStringStartsWith govy.ErrorCode = "string_starts_with" - ErrorCodeStringEndsWith govy.ErrorCode = "string_ends_with" - ErrorCodeStringLength govy.ErrorCode = "string_length" - ErrorCodeStringMinLength govy.ErrorCode = "string_min_length" - ErrorCodeStringMaxLength govy.ErrorCode = "string_max_length" - ErrorCodeStringTitle govy.ErrorCode = "string_title" - ErrorCodeSliceLength govy.ErrorCode = "slice_length" - ErrorCodeSliceMinLength govy.ErrorCode = "slice_min_length" - ErrorCodeSliceMaxLength govy.ErrorCode = "slice_max_length" - ErrorCodeMapLength govy.ErrorCode = "map_length" - ErrorCodeMapMinLength govy.ErrorCode = "map_min_length" - ErrorCodeMapMaxLength govy.ErrorCode = "map_max_length" - ErrorCodeOneOf govy.ErrorCode = "one_of" - ErrorCodeMutuallyExclusive govy.ErrorCode = "mutually_exclusive" - ErrorCodeSliceUnique govy.ErrorCode = "slice_unique" - ErrorCodeURL govy.ErrorCode = "url" - ErrorCodeDurationPrecision govy.ErrorCode = "duration_precision" -) -``` - - -## func [DurationPrecision]() - -```go -func DurationPrecision(precision time.Duration) govy.Rule[time.Duration] -``` - -DurationPrecision ensures the duration is defined with the specified precision. - - -## func [EQ]() - -```go -func EQ[T comparable](compared T) govy.Rule[T] -``` - -EQ ensures the property's value is equal to the compared value. - - -## func [Forbidden]() - -```go -func Forbidden[T any]() govy.Rule[T] -``` - -Forbidden ensures the property's value is its type's zero value, i.e. it's empty. - - -## func [GT]() - -```go -func GT[T constraints.Ordered](compared T) govy.Rule[T] -``` - -GT ensures the property's value is greater than the compared value. - - -## func [GTE]() - -```go -func GTE[T constraints.Ordered](compared T) govy.Rule[T] -``` - -GTE ensures the property's value is greater than or equal to the compared value. - - -## func [LT]() - -```go -func LT[T constraints.Ordered](compared T) govy.Rule[T] -``` - -LT ensures the property's value is less than the compared value. - - -## func [LTE]() - -```go -func LTE[T constraints.Ordered](compared T) govy.Rule[T] -``` - -LTE ensures the property's value is less than or equal to the compared value. - - -## func [MapLength]() - -```go -func MapLength[M ~map[K]V, K comparable, V any](minLen, maxLen int) govy.Rule[M] -``` - -MapLength ensures the map's length is between min and max \(closed interval\). - - -## func [MapMaxLength]() - -```go -func MapMaxLength[M ~map[K]V, K comparable, V any](limit int) govy.Rule[M] -``` - -MapMaxLength ensures the map's length is less than or equal to the limit. - - -## func [MapMinLength]() - -```go -func MapMinLength[M ~map[K]V, K comparable, V any](limit int) govy.Rule[M] -``` - -MapMinLength ensures the map's length is greater than or equal to the limit. - - -## func [MutuallyExclusive]() - -```go -func MutuallyExclusive[S any](required bool, getters map[string]func(s S) any) govy.Rule[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 [NEQ]() - -```go -func NEQ[T comparable](compared T) govy.Rule[T] -``` - -NEQ ensures the property's value is not equal to the compared value. - - -## func [OneOf]() - -```go -func OneOf[T comparable](values ...T) govy.Rule[T] -``` - -OneOf checks if the property's value matches one of the provided values. The values must be comparable. - - -## func [Required]() - -```go -func Required[T any]() govy.Rule[T] -``` - -Required ensures the property's value is not empty \(i.e. it's not its type's zero value\). - - -## func [SliceLength]() - -```go -func SliceLength[S ~[]E, E any](minLen, maxLen int) govy.Rule[S] -``` - -SliceLength ensures the slice's length is between min and max \(closed interval\). - - -## func [SliceMaxLength]() - -```go -func SliceMaxLength[S ~[]E, E any](limit int) govy.Rule[S] -``` - -SliceMaxLength ensures the slice's length is less than or equal to the limit. - - -## func [SliceMinLength]() - -```go -func SliceMinLength[S ~[]E, E any](limit int) govy.Rule[S] -``` - -SliceMinLength ensures the slice's length is greater than or equal to the limit. - - -## func [SliceUnique]() - -```go -func SliceUnique[S []V, V any, H comparable](hashFunc HashFunction[V, H], constraints ...string) govy.Rule[S] -``` - -SliceUnique ensures 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]() - -```go -func StringASCII() govy.Rule[string] -``` - -StringASCII ensures property's value contains only ASCII characters. - - -## func [StringContains]() - -```go -func StringContains(substrings ...string) govy.Rule[string] -``` - -StringContains ensures the property's value contains all the provided substrings. - - -## func [StringDNSLabel]() - -```go -func StringDNSLabel() govy.RuleSet[string] -``` - -StringDNSLabel ensures the property's value is a valid DNS label as defined by RFC\-1123. - - -## func [StringDenyRegexp]() - -```go -func StringDenyRegexp(re *regexp.Regexp, examples ...string) govy.Rule[string] -``` - -StringDenyRegexp ensures the property's value does not match the regular expression. The error message can be enhanced with examples of invalid values. - - -## func [StringEndsWith]() - -```go -func StringEndsWith(suffixes ...string) govy.Rule[string] -``` - -StringEndsWith ensures the property's value ends with one of the provided suffixes. - - -## func [StringJSON]() - -```go -func StringJSON() govy.Rule[string] -``` - -StringJSON ensures property's value is a valid JSON literal. - - -## func [StringLength]() - -```go -func StringLength(minLen, maxLen int) govy.Rule[string] -``` - -StringLength ensures the string's length is between min and max \(closed interval\). - - -## func [StringMatchRegexp]() - -```go -func StringMatchRegexp(re *regexp.Regexp, examples ...string) govy.Rule[string] -``` - -StringMatchRegexp ensures the property's value matches the regular expression. The error message can be enhanced with examples of valid values. - - -## func [StringMaxLength]() - -```go -func StringMaxLength(limit int) govy.Rule[string] -``` - -StringMaxLength ensures the string's length is less than or equal to the limit. - - -## func [StringMinLength]() - -```go -func StringMinLength(limit int) govy.Rule[string] -``` - -StringMinLength ensures the string's length is greater than or equal to the limit. - - -## func [StringNotEmpty]() - -```go -func StringNotEmpty() govy.Rule[string] -``` - -StringNotEmpty ensures the property's value is not empty. The string is considered empty if it contains only whitespace characters. - - -## func [StringStartsWith]() - -```go -func StringStartsWith(prefixes ...string) govy.Rule[string] -``` - -StringStartsWith ensures the property's value starts with one of the provided prefixes. - - -## func [StringTitle]() - -```go -func StringTitle() govy.Rule[string] -``` - -StringTitle ensures each word in a string starts with a capital letter. - - -## func [StringURL]() - -```go -func StringURL() govy.Rule[string] -``` - - - - -## func [StringUUID]() - -```go -func StringUUID() govy.Rule[string] -``` - -StringUUID ensures property's value is a valid UUID string. - - -## func [URL]() - -```go -func URL() govy.Rule[*url.URL] -``` - -URL ensures the URL is valid. The URL must have a scheme \(e.g. https://\) and contain either host, fragment or opaque data. - - -## type [HashFunction]() - -HashFunction accepts a value and returns a comparable hash. - -```go -type HashFunction[V any, H comparable] func(v V) H -``` - - -### func [HashFuncSelf]() - -```go -func HashFuncSelf[H comparable]() HashFunction[H, H] -``` - -HashFuncSelf returns a HashFunction which returns its input value as a hash itself. The value must be comparable. - -Generated by [gomarkdoc]() diff --git a/internal/examples/readme_intro_example_test.go b/internal/examples/readme_intro_example_test.go index 19ae693..b2f27ec 100644 --- a/internal/examples/readme_intro_example_test.go +++ b/internal/examples/readme_intro_example_test.go @@ -95,7 +95,7 @@ func Example_basicUsage() { // - 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 + // - elements are not unique, 1st and 3rd elements collide // - 'students[1].index' with value '9182300123': // - length must be between 9 and 9 // - 'university.name': diff --git a/pkg/govy/example_test.go b/pkg/govy/example_test.go index c78d972..d5ca8f5 100644 --- a/pkg/govy/example_test.go +++ b/pkg/govy/example_test.go @@ -721,8 +721,9 @@ func ExamplePropertyRules_Include() { // the same as [govy.PropertyRules], but extends its API slightly. // // To define rules for each element use: -// - [govy.PropertyRulesForSlice.RulesForEach] -// - [govy.PropertyRulesForSlice.IncludeForEach] +// - [govy.PropertyRulesForSlice.RulesForEach] +// - [govy.PropertyRulesForSlice.IncludeForEach] +// // These work exactly the same way as [govy.PropertyRules.Rules] and [govy.PropertyRules.Include] // verifying each slice element. // @@ -768,27 +769,28 @@ func ExampleForSlice() { // 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 + // - elements are not unique, 1st and 3rd elements collide // - 'students[1].index' with value '9182300123': // - length must be between 9 and 9 } // When dealing with maps there are three forms of iteration: -// - keys -// - values -// - key-value pairs (items) +// - keys +// - values +// - key-value pairs (items) // // You can use [govy.ForMap] function to define rules for all the aforementioned iterators. // It returns a new struct [govy.PropertyRulesForMap] which behaves similar to // [govy.PropertyRulesForSlice].. // // To define rules for keys use: -// - [govy.PropertyRulesForMap.RulesForKeys] -// - [govy.PropertyRulesForMap.IncludeForKeys] -// - [govy.PropertyRulesForMap.RulesForValues] -// - [govy.PropertyRulesForMap.IncludeForValues] -// - [govy.PropertyRulesForMap.RulesForItems] -// - [govy.PropertyRulesForMap.IncludeForItems] +// - [govy.PropertyRulesForMap.RulesForKeys] +// - [govy.PropertyRulesForMap.IncludeForKeys] +// - [govy.PropertyRulesForMap.RulesForValues] +// - [govy.PropertyRulesForMap.IncludeForValues] +// - [govy.PropertyRulesForMap.RulesForItems] +// - [govy.PropertyRulesForMap.IncludeForItems] +// // These work exactly the same way as [govy.PropertyRules.Rules] and [govy.PropertyRules.Include] // verifying each map's key, value or [govy.MapItem]. // @@ -797,10 +799,10 @@ func ExampleForSlice() { // Note: [govy.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). +// - 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: // .. @@ -970,7 +972,7 @@ func ExampleValidator() { // - 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 + // - elements are not unique, 1st and 3rd elements collide // - 'students[1].index' with value '9182300123': // - length must be between 9 and 9 // - 'university.address': diff --git a/pkg/rules/example_test.go b/pkg/rules/example_test.go new file mode 100644 index 0000000..df067f9 --- /dev/null +++ b/pkg/rules/example_test.go @@ -0,0 +1,43 @@ +// nolint: lll +package rules_test + +import ( + "fmt" + + "github.com/nobl9/govy/pkg/govy" + "github.com/nobl9/govy/pkg/rules" +) + +type Teacher struct { + Students []Student `json:"students"` +} + +type Student struct { + Index string `json:"index"` +} + +func ExampleSliceUnique() { + v := govy.New( + govy.ForSlice(func(t Teacher) []Student { return t.Students }). + WithName("students"). + Rules(rules.SliceUnique(func(v Student) string { return v.Index }, + "each student must have unique index")), + ) + teacher := Teacher{ + Students: []Student{ + {Index: "foo"}, + {Index: "bar"}, // 2nd element + {Index: "baz"}, + {Index: "bar"}, // 4th element + }, + } + err := v.Validate(teacher) + if err != nil { + fmt.Println(err) + } + + // Output: + // Validation has failed for the following properties: + // - 'students' with value '[{"index":"foo"},{"index":"bar"},{"index":"baz"},{"index":"bar"}]': + // - elements are not unique, 2nd and 4th elements collide based on constraints: each student must have unique index +} diff --git a/pkg/rules/helpers.go b/pkg/rules/helpers.go new file mode 100644 index 0000000..203b4ab --- /dev/null +++ b/pkg/rules/helpers.go @@ -0,0 +1,24 @@ +package rules + +import "strconv" + +// ordinalString returns the ordinal representation of an integer. +// Stolen from: https://github.com/dustin/go-humanize. +func ordinalString(x int) string { + suffix := "th" + switch x % 10 { + case 1: + if x%100 != 11 { + suffix = "st" + } + case 2: + if x%100 != 12 { + suffix = "nd" + } + case 3: + if x%100 != 13 { + suffix = "rd" + } + } + return strconv.Itoa(x) + suffix +} diff --git a/pkg/rules/helpers_test.go b/pkg/rules/helpers_test.go new file mode 100644 index 0000000..cef6f7e --- /dev/null +++ b/pkg/rules/helpers_test.go @@ -0,0 +1,37 @@ +package rules + +import "testing" + +func TestOrdinalString(t *testing.T) { + tests := []struct { + in int + out string + }{ + {0, "0th"}, + {1, "1st"}, + {2, "2nd"}, + {3, "3rd"}, + {4, "4th"}, + {10, "10th"}, + {11, "11th"}, + {12, "12th"}, + {13, "13th"}, + {21, "21st"}, + {32, "32nd"}, + {43, "43rd"}, + {101, "101st"}, + {102, "102nd"}, + {103, "103rd"}, + {211, "211th"}, + {212, "212th"}, + {213, "213th"}, + } + for _, tt := range tests { + t.Run(tt.out, func(t *testing.T) { + got := ordinalString(tt.in) + if got != tt.out { + t.Errorf("ordinalString(%d) = %q; want %q", tt.in, got, tt.out) + } + }) + } +} diff --git a/pkg/rules/string.go b/pkg/rules/string.go index 502559d..9354305 100644 --- a/pkg/rules/string.go +++ b/pkg/rules/string.go @@ -93,6 +93,8 @@ func StringASCII() govy.Rule[string] { return StringMatchRegexp(asciiRegexp).WithErrorCode(ErrorCodeStringASCII) } +// StringURL ensures property's value is a valid URL as defined by [url.Parse] function. +// Unlike [URL] it does not impose any additional rules upon parsed [url.URL]. func StringURL() govy.Rule[string] { return govy.NewRule(func(v string) error { u, err := url.Parse(v) diff --git a/pkg/rules/unique.go b/pkg/rules/unique.go index 1f8b706..be20c4d 100644 --- a/pkg/rules/unique.go +++ b/pkg/rules/unique.go @@ -26,7 +26,8 @@ func SliceUnique[S []V, V any, H comparable](hashFunc HashFunction[V, H], constr for i := range slice { hash := hashFunc(slice[i]) if j, ok := unique[hash]; ok { - errMsg := fmt.Sprintf("elements are not unique, index %d collides with index %d", j, i) + errMsg := fmt.Sprintf("elements are not unique, %s and %s elements collide", + ordinalString(j+1), ordinalString(i+1)) if len(constraints) > 0 { errMsg += " based on constraints: " + strings.Join(constraints, ", ") } diff --git a/pkg/rules/unique_test.go b/pkg/rules/unique_test.go index 66a4d09..9d7016b 100644 --- a/pkg/rules/unique_test.go +++ b/pkg/rules/unique_test.go @@ -17,22 +17,22 @@ func TestSliceUnique(t *testing.T) { t.Run("fails", func(t *testing.T) { err := SliceUnique(HashFuncSelf[string]()).Validate([]string{"a", "b", "c", "b"}) require.Error(t, err) - assert.EqualError(t, err, "elements are not unique, index 1 collides with index 3") + assert.EqualError(t, err, "elements are not unique, 2nd and 4th elements collide") assert.True(t, govy.HasErrorCode(err, ErrorCodeSliceUnique)) }) t.Run("fails with constraint", func(t *testing.T) { err := SliceUnique(HashFuncSelf[string](), "values must be unique"). - Validate([]string{"a", "b", "c", "b"}) + Validate([]string{"a", "a", "c", "b"}) require.Error(t, err) - assert.EqualError(t, err, "elements are not unique, index 1 collides with index 3 "+ + assert.EqualError(t, err, "elements are not unique, 1st and 2nd elements collide "+ "based on constraints: values must be unique") assert.True(t, govy.HasErrorCode(err, ErrorCodeSliceUnique)) }) t.Run("fails with constraints", func(t *testing.T) { err := SliceUnique(HashFuncSelf[string](), "constraint 1", "constraint 2"). - Validate([]string{"a", "b", "c", "b"}) + Validate([]string{"a", "c", "c", "b"}) require.Error(t, err) - assert.EqualError(t, err, "elements are not unique, index 1 collides with index 3 "+ + assert.EqualError(t, err, "elements are not unique, 2nd and 3rd elements collide "+ "based on constraints: constraint 1, constraint 2") assert.True(t, govy.HasErrorCode(err, ErrorCodeSliceUnique)) })