diff --git a/.agents/skills/govy/SKILL.md b/.agents/skills/govy/SKILL.md
new file mode 100644
index 00000000..d8b75b7f
--- /dev/null
+++ b/.agents/skills/govy/SKILL.md
@@ -0,0 +1,172 @@
+---
+name: govy
+description: >
+ Use this skill when writing, reviewing, or explaining validation code with
+ github.com/nobl9/govy. Trigger for govy validators, property rules, custom
+ rules, predefined rules, govytest assertions, validation plans, error
+ messages, message templates, path inference, or any task that mentions the
+ govy library.
+---
+
+# Govy
+
+Govy is a validation library for Go that uses a functional interface for building
+strongly-typed validation rules, powered by generics and reflection free.
+It puts heavy focus on end user errors readability,
+providing means of crafting clear and information-rich error messages.
+It also allows writing self-documenting validation rules through a
+validation plan.
+
+The references in this skill are self-contained.
+Load only the reference that matches the task.
+
+## Reference selection
+
+| Task | Load |
+| :--- | :--- |
+| Build a validator, name it, add conditions, validate slices, or compose validators | [core-validation.md](references/core-validation.md) |
+| Work with property getters, paths, pointers, transforms, required values, optional values, hidden values, or property-level conditions | [properties-and-paths.md](references/properties-and-paths.md) |
+| Create or modify rules, rule sets, error details, examples, error codes, custom messages, or message templates | [rules-and-messages.md](references/rules-and-messages.md) |
+| Validate nested objects, slices, maps, each element, or compose validators over collections | [collections-and-composition.md](references/collections-and-composition.md) |
+| Inspect, serialize, rename, or construct govy errors | [errors.md](references/errors.md) |
+| Test govy validation behavior without the govytest package | [testing.md](references/testing.md) |
+| Generate or validate a validation plan | [validation-plan.md](references/validation-plan.md) |
+| Configure runtime or generated path inference | [path-inference.md](references/path-inference.md) |
+| Choose from existing predefined rules in `pkg/rules` | [existing-rules.md](references/existing-rules.md) |
+| See examples of predefined rules in use | [predefined-rules.md](references/predefined-rules.md) |
+| Use `pkg/govytest` assertion helpers | [govytest.md](references/govytest.md) |
+
+## Usage guidance
+
+- Define rules and validators once and reuse them across validation calls.
+ They are relatively costly to construct and may lazily initialize internal
+ state during first use, so avoid rebuilding them every time validation runs.
+- Keep validator declarations immutable-style: chained methods return copies,
+ so derive runtime variants from existing validators instead of mutating them
+ in place.
+- Prefer explicit `WithName` or `WithPath` unless the user explicitly
+ mentions path inference or if the inference mechanism is already used.
+- Use `WithName` for one path segment and `WithPath` for multi-segment paths.
+- Use `ForPointer` for optional pointer fields so rules operate on the
+ pointed-to value and nil values are skipped unless `Required` is added.
+- Use `Required` and `OmitEmpty` on property rules to short-circuit empty values
+ before running normal rules.
+- Attach plan and error metadata to the receiver that owns it:
+ use `Rule.WithDescription`, `Rule.WithDetails`, `Rule.WithExamples`,
+ and `Rule.WithErrorCode` for rules; use `PropertyRules.WithExamples`
+ for property examples; use `govy.WhenDescription` for conditional rules.
+- Use `govytest` helpers for tests that need to match structured validation
+ errors rather than brittle full error strings.
+
+## General example
+
+[//]: # (embed: internal/examples/readme_intro_example_test.go)
+
+```go
+package examples
+
+import (
+ "fmt"
+ "regexp"
+ "time"
+
+ "github.com/nobl9/govy/pkg/govy"
+ "github.com/nobl9/govy/pkg/rules"
+)
+
+func Example_basicUsage() {
+ type University struct {
+ Name string `json:"name"`
+ Address string `json:"address"`
+ }
+ type Student struct {
+ Index string `json:"index"`
+ }
+ 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"`
+ }
+
+ universityValidation := govy.New(
+ govy.For(func(u University) string { return u.Name }).
+ WithName("name").
+ Required(),
+ govy.For(func(u University) string { return u.Address }).
+ WithName("address").
+ Required().
+ Rules(rules.StringMatchRegexp(
+ regexp.MustCompile(`[\w\s.]+, \d{2}-\d{3} \w+`),
+ ).
+ WithDetails("Polish address format must consist of the main address and zip code").
+ WithExamples("5 M. Skłodowska-Curie Square, 60-965 Poznan")),
+ )
+ 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.ForPointer(func(t Teacher) *string { return t.MiddleName }).
+ WithName("middleName").
+ Rules(rules.StringTitle()),
+ 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",
+ MiddleName: nil, // Validation for nil pointers by default is skipped.
+ Age: 48,
+ Students: []Student{
+ {Index: "918230014"},
+ {Index: "9182300123"},
+ {Index: "918230014"},
+ },
+ University: University{
+ Name: "",
+ Address: "10th University St.",
+ },
+ }
+
+ if err := teacherValidator.WithName("John").Validate(teacher); err != nil {
+ fmt.Println(err)
+ }
+ // When condition is not met, no validation errors.
+ johnFromTheFuture := teacher
+ johnFromTheFuture.Age = 51
+ if err := teacherValidator.WithName("John From The Future").Validate(johnFromTheFuture); 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, 1st and 3rd elements collide
+ // - 'students[1].index' with value '9182300123':
+ // - length must be between 9 and 9
+ // - 'university.name':
+ // - property is required but was empty
+ // - 'university.address' with value '10th University St.':
+ // - string must match regular expression: '[\w\s.]+, \d{2}-\d{3} \w+' (e.g. '5 M. Skłodowska-Curie Square, 60-965 Poznan'); Polish address format must consist of the main address and zip code
+}
+```
diff --git a/.agents/skills/govy/references/collections-and-composition.md b/.agents/skills/govy/references/collections-and-composition.md
new file mode 100644
index 00000000..137a936d
--- /dev/null
+++ b/.agents/skills/govy/references/collections-and-composition.md
@@ -0,0 +1,347 @@
+# Collections and Composition
+
+Nested validators, slice and map property rules, slice pointers, and validator variants derived from path removal.
+
+## Topics
+
+- [Compose nested validators](#compose-nested-validators)
+ - [Include a validator for a nested object.](#include-a-validator-for-a-nested-object)
+- [Validate collection elements](#validate-collection-elements)
+ - [Validate a slice and each of its elements.](#validate-a-slice-and-each-of-its-elements)
+ - [Handle slices whose elements are pointers.](#handle-slices-whose-elements-are-pointers)
+ - [Validate map keys, values, and key-value items.](#validate-map-keys-values-and-key-value-items)
+- [Derive validator variants](#derive-validator-variants)
+ - [Remove selected properties by path.](#remove-selected-properties-by-path)
+
+## Compose nested validators
+
+Use Include when a property has its own validator. Govy appends nested paths automatically so errors still point to the leaf property.
+
+
+
+**Include a validator for a nested object.**
+
+[//]: # (embed: ExamplePropertyRules_Include)
+
+```go
+// 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] to include another [govy.Validator] in our [govy.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.
+func ExamplePropertyRules_Include() {
+ 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':
+ // - must be equal to 'Tom'
+ // - 'university.address':
+ // - property is required but was empty
+}
+```
+
+## Validate collection elements
+
+Use collection-specific builders when both the collection and each element need validation. They preserve map keys or slice indexes in paths.
+
+
+
+**Validate a slice and each of its elements.**
+
+[//]: # (embed: ExampleForSlice)
+
+```go
+// When dealing with slices we often want to both validate the whole slice
+// and each of its elements.
+// You can use [govy.ForSlice] function to do just that.
+// It returns a new struct [govy.PropertyRulesForSlice] which behaves exactly
+// the same as [govy.PropertyRules], but extends its API slightly.
+//
+// To define rules for each element use:
+// - [govy.PropertyRulesForSlice.RulesForEach]
+// - [govy.PropertyRulesForSlice.IncludeForEach]
+//
+// These work exactly the same way as [govy.PropertyRules.Rules] and [govy.PropertyRules.Include]
+// verifying each slice element.
+//
+// [govy.PropertyRulesForSlice.Rules] is in turn used to define rules for the whole slice.
+//
+// Note: [govy.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].
+// Notice that property path for slices has the following format:
+// [].
+func ExampleForSlice() {
+ 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, 1st and 3rd elements collide
+ // - 'students[1].index' with value '9182300123':
+ // - length must be between 9 and 9
+}
+```
+
+
+
+**Handle slices whose elements are pointers.**
+
+[//]: # (embed: ExampleForSlice_sliceOfPointers)
+
+```go
+// When dealing with slices of pointers you may find it problematic to add [govy.Rule]
+// with [govy.PropertyRulesForSlice.RulesForEach].
+// The builtin rules, and most likely your custom rules as well, all operate on non-pointer values.
+// This means you cannot use them on your slice's pointer elements.
+//
+// To solve this problem you can use [govy.ForPointer] constructor and convert any [govy.Rule]
+// to work on pointers.
+//
+// In the below example we're defining two [govy.Validator] instances:
+// - 'faultyValidator' which will not fail for 'nil' value
+// - 'goodValidator' which will fail for 'nil' value by using [rules.Required] rule
+//
+// This behavior is consistent with [govy.ForPointer] constructor, which will skip the validation
+// unless you add [govy.PropertyRules.Required] to enforce the value to be a non-nil pointer.
+func ExampleForSlice_sliceOfPointers() {
+ type Pointers struct {
+ Pointers []*string `json:"pointers"`
+ }
+ pointersRules := govy.ForSlice(func(p Pointers) []*string { return p.Pointers }).
+ WithName("pointers").
+ Rules(rules.SliceMaxLength[[]*string](2)).
+ RulesForEach(
+ govy.RuleToPointer(rules.StringLength(9, 9)),
+ )
+ faultyValidator := govy.New(
+ pointersRules,
+ )
+ goodValidator := govy.New(
+ pointersRules.RulesForEach(rules.Required[*string]()),
+ )
+
+ pointers := Pointers{
+ Pointers: []*string{ptr("918230014"), ptr("9182300123"), ptr("918230014"), nil},
+ }
+
+ err := faultyValidator.Validate(pointers)
+ if err != nil {
+ fmt.Println(err)
+ }
+ err = goodValidator.Validate(pointers)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation has failed for the following properties:
+ // - 'pointers' with value '["918230014","9182300123","918230014",null]':
+ // - length must be less than or equal to 2
+ // - 'pointers[1]' with value '9182300123':
+ // - length must be between 9 and 9
+ // Validation has failed for the following properties:
+ // - 'pointers' with value '["918230014","9182300123","918230014",null]':
+ // - length must be less than or equal to 2
+ // - 'pointers[1]' with value '9182300123':
+ // - length must be between 9 and 9
+ // - 'pointers[3]':
+ // - property is required but was empty
+}
+```
+
+
+
+**Validate map keys, values, and key-value items.**
+
+[//]: # (embed: ExampleForMap)
+
+```go
+// When dealing with maps there are three forms of iteration:
+// - 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]
+//
+// These work exactly the same way as [govy.PropertyRules.Rules] and [govy.PropertyRules.Include]
+// verifying each map's key, value or [govy.MapItem].
+//
+// [govy.PropertyRulesForMap.Rules] is in turn used to define rules for the whole map.
+//
+// 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).
+//
+// Notice that property path for maps has the following format:
+// [].
+func ExampleForMap() {
+ 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':
+ // - must not be 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
+}
+```
+
+## Derive validator variants
+
+Use path-based removal when a caller needs a variant of an existing validator without mutating the original declaration.
+
+
+
+**Remove selected properties by path.**
+
+[//]: # (embed: ExampleValidator_RemovePropertiesByPath)
+
+```go
+// This example demonstrates how to remove specific properties from a [govy.Validator] by their paths.
+// This is useful when you want to create a modified validator without certain rules.
+func ExampleValidator_RemovePropertiesByPath() {
+ baseValidator := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Rules(rules.StringNotEmpty()),
+ govy.For(func(t Teacher) time.Duration { return t.Age }).
+ WithName("age").
+ Rules(rules.GT(time.Duration(0))),
+ )
+
+ teacher := Teacher{Name: "John", Age: -1}
+
+ // Base validator fails because age is negative
+ err := baseValidator.Validate(teacher)
+ if err != nil {
+ fmt.Println("Base validator failed")
+ }
+
+ // Modified validator passes because age validation is removed
+ modifiedValidator := baseValidator.RemovePropertiesByPath(jsonpath.New().Name("age"))
+ err = modifiedValidator.Validate(teacher)
+ if err == nil {
+ fmt.Println("Modified validator passed")
+ }
+
+ // Output:
+ // Base validator failed
+ // Modified validator passed
+}
+```
diff --git a/.agents/skills/govy/references/core-validation.md b/.agents/skills/govy/references/core-validation.md
new file mode 100644
index 00000000..2933345a
--- /dev/null
+++ b/.agents/skills/govy/references/core-validation.md
@@ -0,0 +1,447 @@
+# Core Validation
+
+Validator construction, naming, conditions, slice validation, cascade behavior, and validator composition patterns.
+
+## Topics
+
+- [Construct reusable validators](#construct-reusable-validators)
+ - [Create a validator from property rules.](#create-a-validator-from-property-rules)
+ - [Attach a static validator name.](#attach-a-static-validator-name)
+ - [Compute the validator name from the validated value.](#compute-the-validator-name-from-the-validated-value)
+- [Control when validators run](#control-when-validators-run)
+ - [Run a validator only when a predicate matches.](#run-a-validator-only-when-a-predicate-matches)
+ - [Branch validation by including different validators under property conditions.](#branch-validation-by-including-different-validators-under-property-conditions)
+- [Validate slices](#validate-slices)
+ - [Validate a slice while preserving indexed property paths.](#validate-a-slice-while-preserving-indexed-property-paths)
+ - [Validate each element directly with ValidateSlice.](#validate-each-element-directly-with-validateslice)
+- [Control error aggregation](#control-error-aggregation)
+ - [Stop evaluating later properties after a validator failure.](#stop-evaluating-later-properties-after-a-validator-failure)
+- [Compose a complete validator](#compose-a-complete-validator)
+ - [Build a full Teacher validator.](#build-a-full-teacher-validator)
+
+## Construct reusable validators
+
+Define validators once and reuse them. Name validators when the resulting error should identify the validated entity.
+
+
+
+**Create a validator from property rules.**
+
+[//]: # (embed: ExampleNew)
+
+```go
+// In order to create a new [govy.Validator] use [govy.New] constructor.
+// Let's define simple [govy.PropertyRules] for [Teacher.Name].
+// For now, it will be always failing.
+func ExampleNew() {
+ 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:
+ // - always fails
+}
+```
+
+
+
+**Attach a static validator name.**
+
+[//]: # (embed: ExampleValidator_WithName)
+
+```go
+// To associate [govy.Validator] with an entity name use [govy.Validator.WithName] function.
+// When any of the rules fails, the error will contain the entity name you've provided.
+func ExampleValidator_WithName() {
+ 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:
+ // - always fails
+}
+```
+
+
+
+**Compute the validator name from the validated value.**
+
+[//]: # (embed: ExampleValidator_WithNameFunc)
+
+```go
+// If statically defined name through [govy.Validator.WithName] is not enough,
+// you can use [govy.Validator.WithNameFunc].
+// The function receives the entity's instance you're validating and returns a string name.
+func ExampleValidator_WithNameFunc() {
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ Rules(govy.NewRule(func(name string) error { return fmt.Errorf("always fails") })),
+ ).WithNameFunc(func(t Teacher) string { return "Teacher " + t.Name })
+
+ err := v.Validate(Teacher{Name: "John"})
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation for Teacher John has failed:
+ // - always fails
+}
+```
+
+## Control when validators run
+
+Use validator-level conditions for whole-object gates. Use property-level conditions when only one property branch should be skipped.
+
+
+
+**Run a validator only when a predicate matches.**
+
+[//]: # (embed: ExampleValidator_When)
+
+```go
+// [govy.Validator] rules can be evaluated on condition, to specify the predicate use [govy.Validator.When] function.
+//
+// In this example, validation for [Teacher] instance will only be evaluated
+// if the [Teacher.Age] property is less than 50 years.
+func ExampleValidator_When() {
+ 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:
+ // - always fails
+}
+```
+
+
+
+**Branch validation by including different validators under property conditions.**
+
+[//]: # (embed: ExampleValidator_branchingPattern)
+
+```go
+// 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] and [govy.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] for [CSV] and [JSON] and use [govy.PropertyRules.When] to only validate
+// their included [govy.Validator] if the correct [File.Format] is provided.
+func ExampleValidator_branchingPattern() {
+ 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*$'
+}
+```
+
+## Validate slices
+
+Use ForSlice when the slice itself has rules or the path should include indexes. Use ValidateSlice when each value can be validated independently.
+
+
+
+**Validate a slice while preserving indexed property paths.**
+
+[//]: # (embed: ExampleValidator_Validate_slice)
+
+```go
+// If you want to validate a slice of entities, you can combine [govy.New] with [govy.ForSlice].
+// The produced errors will contain information about the failing entity's index
+// in their [govy.PropertyError.PropertyPath].
+func ExampleValidator_Validate_slice() {
+ teacherValidator := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Rules(govy.NewRule(func(name string) error { return fmt.Errorf("always fails") })),
+ )
+ v := govy.New(
+ govy.ForSlice(govy.GetSelf[[]Teacher]()).
+ IncludeForEach(teacherValidator),
+ )
+
+ err := v.Validate([]Teacher{
+ {Name: "John"},
+ {Name: "Jake"},
+ })
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation has failed for the following properties:
+ // - '[0].name' with value 'John':
+ // - always fails
+ // - '[1].name' with value 'Jake':
+ // - always fails
+}
+```
+
+
+
+**Validate each element directly with ValidateSlice.**
+
+[//]: # (embed: ExampleValidator_ValidateSlice)
+
+```go
+// If combining [govy.New] with [govy.ForSlice] is not verbose enough for you,
+// you can use [govy.Validator.ValidateSlice] function.
+// It will validate each element according to the rules defined by [govy.Validator].
+// It returns [govy.ValidatorErrors].
+//
+// Note: If you need to perform additional validation on the whole slice,
+// you should rather use [govy.New] with [govy.ForSlice] and [govy.GetSelf].
+// [govy.Validator.ValidateSlice] is designed to be used for processing independent values.
+//
+// Note: Since each element is validated in isolation,
+// the reported property paths will not start with the slice index,
+// they will instead start at the element's root.
+func ExampleValidator_ValidateSlice() {
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Rules(govy.NewRule(func(name string) error { return fmt.Errorf("always fails") })),
+ ).WithName("Teacher")
+
+ err := v.ValidateSlice([]Teacher{
+ {Name: "John"},
+ {Name: "Jake"},
+ })
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation for Teacher at index 0 has failed for the following properties:
+ // - 'name' with value 'John':
+ // - always fails
+ // Validation for Teacher at index 1 has failed for the following properties:
+ // - 'name' with value 'Jake':
+ // - always fails
+}
+```
+
+## Control error aggregation
+
+Cascade settings decide whether validation continues after failures. Validator cascade applies across properties unless a property-level cascade overrides it.
+
+
+
+**Stop evaluating later properties after a validator failure.**
+
+[//]: # (embed: ExampleValidator_Cascade)
+
+```go
+// Unlike [govy.PropertyRules.Cascade] which works on [govy.PropertyRules] level,
+// [govy.Validator.Cascade] propagates to all the properties of [govy.Validator] and
+// furthermore, will stop evaluating the next property if any preceding property fails.
+//
+// If [govy.PropertyRules.Cascade] is set, the setting will take precedence over
+// [govy.Validator] cascade mode.
+//
+// See [ExamplePropertyRules_Cascade] for more details on [govy.PropertyRules.Cascade].
+func ExampleValidator_Cascade() {
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Cascade(govy.CascadeModeContinue).
+ Rules(rules.NEQ("Jerry")).
+ Rules(rules.EQ("Tom")),
+ govy.For(func(t Teacher) time.Duration { return t.Age }).
+ WithName("age").
+ Rules(
+ rules.GT(18*year),
+ govy.NewRule(func(time.Duration) error {
+ return fmt.Errorf("always fails")
+ }),
+ ),
+ ).
+ Cascade(govy.CascadeModeStop)
+
+ for _, name := range []string{"Tom", "Jerry"} {
+ teacher := Teacher{
+ Name: name,
+ Age: 17 * year,
+ }
+ err := v.WithName(name).Validate(teacher)
+ if err != nil {
+ fmt.Println(err)
+ }
+ }
+
+ // Output:
+ // Validation for Tom has failed for the following properties:
+ // - 'age' with value '148920h0m0s':
+ // - must be greater than '157680h0m0s'
+ // Validation for Jerry has failed for the following properties:
+ // - 'name' with value 'Jerry':
+ // - must not be equal to 'Jerry'
+ // - must be equal to 'Tom'
+}
+```
+
+## Compose a complete validator
+
+Use the complete example as a reference shape for idiomatic validator declarations with named properties, builtin rules, and nested composition.
+
+
+
+**Build a full Teacher validator.**
+
+[//]: # (embed: ExampleValidator)
+
+```go
+// Bringing it all (mostly) together, let's create a fully fledged [govy.Validator] for [Teacher].
+func ExampleValidator() {
+ 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, 1st and 3rd elements collide
+ // - 'students[1].index' with value '9182300123':
+ // - length must be between 9 and 9
+ // - 'university.address':
+ // - property is required but was empty
+}
+```
diff --git a/.agents/skills/govy/references/errors.md b/.agents/skills/govy/references/errors.md
new file mode 100644
index 00000000..94fbbd78
--- /dev/null
+++ b/.agents/skills/govy/references/errors.md
@@ -0,0 +1,157 @@
+# Errors
+
+Validator errors, runtime error naming, structured JSON errors, and manually constructed property errors.
+
+## Topics
+
+- [Work with validator errors](#work-with-validator-errors)
+ - [Set or overwrite a validator name on returned errors.](#set-or-overwrite-a-validator-name-on-returned-errors)
+ - [Inspect and serialize validator error output.](#inspect-and-serialize-validator-error-output)
+- [Create property-scoped errors](#create-property-scoped-errors)
+ - [Construct a property error with one or more rule errors.](#construct-a-property-error-with-one-or-more-rule-errors)
+
+## Work with validator errors
+
+Use validator errors when validation failed at the validator level. Names can be attached after validation, and structured fields are available for serialization or inspection.
+
+
+
+**Set or overwrite a validator name on returned errors.**
+
+[//]: # (embed: ExampleValidatorError_WithName)
+
+```go
+// You can also add [govy.Validator] name during runtime,
+// by calling [govy.ValidatorError.WithName] function on the returned error.
+//
+// Note: We left the previous "Teacher" name assignment, to demonstrate that
+// the [govy.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] will not change its declared instance,
+// but rather create a copy of it.
+func ExampleValidatorError_WithName() {
+ 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:
+ // - always fails
+}
+```
+
+
+
+**Inspect and serialize validator error output.**
+
+[//]: # (embed: ExampleValidatorError)
+
+```go
+// All errors returned by [govy.Validator] are of type [govy.ValidatorError].
+// Type casting directly to [govy.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.
+func ExampleValidatorError() {
+ 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": [
+ // {
+ // "propertyPath": "name",
+ // "propertyValue": "John",
+ // "errors": [
+ // {
+ // "error": "always fails"
+ // }
+ // ]
+ // }
+ // ],
+ // "name": "Teacher"
+ // }
+}
+```
+
+## Create property-scoped errors
+
+Return property errors from custom rules when top-level validation logic needs to point the failure at a nested property path.
+
+
+
+**Construct a property error with one or more rule errors.**
+
+[//]: # (embed: ExampleNewPropertyError)
+
+```go
+// 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]
+// and return [govy.PropertyError] from your validation rule.
+// Note that you can still use [govy.ErrorCode] and pass [govy.RuleError] to the constructor.
+// You can pass any number of [govy.RuleError].
+func ExampleNewPropertyError() {
+ v := govy.New(
+ govy.For(govy.GetSelf[Teacher]()).
+ Rules(govy.NewRule(func(t Teacher) error {
+ if t.Name == "Jake" {
+ return govy.NewPropertyError(
+ jsonpath.Parse("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!
+}
+```
diff --git a/.agents/skills/govy/references/existing-rules.md b/.agents/skills/govy/references/existing-rules.md
new file mode 100644
index 00000000..d49b4f02
--- /dev/null
+++ b/.agents/skills/govy/references/existing-rules.md
@@ -0,0 +1,81 @@
+# Existing Rules
+
+Generated reference of exported predefined rule constructors from `pkg/rules`.
+Use this when choosing an existing rule before writing a custom one.
+
+[//]: # (docs: pkg/rules?kind=func&returns=govy.Rule,govy.RuleSet)
+
+- `DurationPrecision` - DurationPrecision ensures the duration is defined with the specified precision.
+- `EQ` - EQ ensures the property's value is equal to the compared value.
+- `EqualProperties` - EqualProperties checks if all the specified properties are equal.
+- `Forbidden` - Forbidden ensures the property's value is its type's zero value, i.e. it's empty.
+- `GT` - GT ensures the property's value is greater than the compared value.
+- `GTComparableProperties` - GTComparableProperties ensures the first property's value is greater than the second property's value.
+- `GTE` - GTE ensures the property's value is greater than or equal to the compared value.
+- `GTEComparableProperties` - GTEComparableProperties ensures the first property's value is greater than or equal to the second property's value.
+- `GTEProperties` - GTEProperties ensures the first property's value is greater than or equal to the second property's value.
+- `GTProperties` - GTProperties ensures the first property's value is greater than the second property's value.
+- `LT` - LT ensures the property's value is less than the compared value.
+- `LTComparableProperties` - LTComparableProperties ensures the first property's value is less than the second property's value.
+- `LTE` - LTE ensures the property's value is less than or equal to the compared value.
+- `LTEComparableProperties` - LTEComparableProperties ensures the first property's value is less than or equal to the second property's value.
+- `LTEProperties` - LTEProperties ensures the first property's value is less than or equal to the second property's value.
+- `LTProperties` - LTProperties ensures the first property's value is less than the second property's value.
+- `MapLength` - MapLength ensures the map's length is between min and max (closed interval).
+- `MapMaxLength` - MapMaxLength ensures the map's length is less than or equal to the limit.
+- `MapMinLength` - MapMinLength ensures the map's length is greater than or equal to the limit.
+- `MutuallyDependent` - MutuallyDependent checks if properties are mutually dependent.
+- `MutuallyExclusive` - MutuallyExclusive checks if properties are mutually exclusive.
+- `NEQ` - NEQ ensures the property's value is not equal to the compared value.
+- `NotOneOf` - NotOneOf checks if the property's value does not match any of the provided values.
+- `OneOf` - OneOf checks if the property's value matches one of the provided values.
+- `OneOfProperties` - OneOfProperties checks if at least one of the properties is set.
+- `Required` - Required ensures the property's value is not empty (i.e. it's not its type's zero value).
+- `SliceLength` - SliceLength ensures the slice's length is between min and max (closed interval).
+- `SliceMaxLength` - SliceMaxLength ensures the slice's length is less than or equal to the limit.
+- `SliceMinLength` - SliceMinLength ensures the slice's length is greater than or equal to the limit.
+- `SliceUnique` - SliceUnique ensures that a slice contains unique elements based on a provided [HashFunction].
+- `StringASCII` - StringASCII ensures property's value contains only ASCII characters.
+- `StringAlpha` - StringAlpha ensures the property's value consists only of ASCII letters.
+- `StringAlphaUnicode` - StringAlphaUnicode ensures the property's value consists only of Unicode letters.
+- `StringAlphanumeric` - StringAlphanumeric ensures the property's value consists only of ASCII letters and numbers.
+- `StringAlphanumericUnicode` - StringAlphanumericUnicode ensures the property's value consists only of Unicode letters and numbers.
+- `StringCIDR` - StringCIDR ensures property's value is a valid CIDR notation IP address.
+- `StringCIDRv4` - StringCIDRv4 ensures property's value is a valid CIDR notation IPv4 address.
+- `StringCIDRv6` - StringCIDRv6 ensures property's value is a valid CIDR notation IPv6 address.
+- `StringContains` - StringContains ensures the property's value contains all the provided substrings.
+- `StringCrontab` - StringCrontab ensures the property's value is a valid crontab schedule expression.
+- `StringDNSLabel` - StringDNSLabel ensures the property's value is a valid DNS label as defined by [RFC 1123].
+- `StringDNSSubdomain` - StringDNSSubdomain ensures the property's value is a valid DNS subdomain as defined by [RFC 1123].
+- `StringDateTime` - StringDateTime ensures the property's value is a valid date and time in the specified layout.
+- `StringDenyRegexp` - StringDenyRegexp ensures the property's value does not match the regular expression.
+- `StringDirPath` - StringDirPath ensures the property's value is a file system path pointing to an existing directory.
+- `StringEmail` - StringEmail ensures the property's value is a valid email address.
+- `StringEndsWith` - StringEndsWith ensures the property's value ends with one of the provided suffixes.
+- `StringExcludes` - StringExcludes ensures the property's value does not contain any of the provided substrings.
+- `StringFQDN` - StringFQDN ensures the property's value is a fully qualified domain name (FQDN).
+- `StringFilePath` - StringFilePath ensures the property's value is a file system path pointing to an existing file.
+- `StringFileSystemPath` - StringFileSystemPath ensures the property's value is an existing file system path.
+- `StringGitRef` - StringGitRef ensures a git reference name follows the [git-check-ref-format] rules.
+- `StringIP` - StringIP ensures property's value is a valid IP address.
+- `StringIPv4` - StringIPv4 ensures property's value is a valid IPv4 address.
+- `StringIPv6` - StringIPv6 ensures property's value is a valid IPv6 address.
+- `StringJSON` - StringJSON ensures property's value is a valid JSON literal.
+- `StringKubernetesQualifiedName` - StringKubernetesQualifiedName ensures the property's value is a valid "qualified name" as defined by [Kubernetes validation].
+- `StringLength` - StringLength ensures the string's length is between min and max (closed interval).
+- `StringMAC` - StringMAC ensures property's value is a valid MAC address.
+- `StringMatchFileSystemPath` - StringMatchFileSystemPath ensures the property's value matches the provided file path pattern.
+- `StringMatchRegexp` - StringMatchRegexp ensures the property's value matches the regular expression.
+- `StringMaxLength` - StringMaxLength ensures the string's length is less than or equal to the limit.
+- `StringMinLength` - StringMinLength ensures the string's length is greater than or equal to the limit.
+- `StringNotEmpty` - StringNotEmpty ensures the property's value is not empty.
+- `StringRegexp` - StringRegexp ensures the property's value is a valid regular expression.
+- `StringStartsWith` - StringStartsWith ensures the property's value starts with one of the provided prefixes.
+- `StringTimeZone` - StringTimeZone ensures the property's value is a valid time zone name which uniquely identifies a time zone in the IANA Time Zone database.
+- `StringTitle` - StringTitle ensures each word in a string starts with a capital letter.
+- `StringURL` - StringURL ensures property's value is a valid URL as defined by [url.Parse] function.
+- `StringUUID` - StringUUID ensures property's value is a valid UUID string as defined by [RFC 4122].
+- `URL` - URL ensures the URL is valid.
+- `UniqueProperties` - UniqueProperties ensures each property is unique based on a provided [HashFunction].
+
+[//]: # (end-docs)
diff --git a/.agents/skills/govy/references/govytest.md b/.agents/skills/govy/references/govytest.md
new file mode 100644
index 00000000..4456d8f9
--- /dev/null
+++ b/.agents/skills/govy/references/govytest.md
@@ -0,0 +1,374 @@
+# Govytest
+
+Assertion helpers for validating govy errors in tests.
+
+## Topics
+
+- [Assert validation results](#assert-validation-results)
+ - [Assert that validation succeeded.](#assert-that-validation-succeeded)
+ - [Assert exact structured rule errors.](#assert-exact-structured-rule-errors)
+ - [Assert nested ValidatorErrors produced by slice validation.](#assert-nested-validatorerrors-produced-by-slice-validation)
+ - [Assert that one expected error is present among other errors.](#assert-that-one-expected-error-is-present-among-other-errors)
+
+## Assert validation results
+
+Use govytest when tests should match structured validation failures. Prefer these helpers over comparing full formatted error strings.
+
+
+
+**Assert that validation succeeded.**
+
+[//]: # (embed: ExampleAssertNoError)
+
+```go
+// You can use [govytest.AssertNoError] to ensure no error was produced by [govy.Validator.Validate].
+// If an error was produced, it will be printed to the stdout in JSON format.
+//
+// To demonstrate the erroneous output of [govytest.AssertNoError] we'll fail the assertion.
+func ExampleAssertNoError() {
+ teacherValidator := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Required().
+ Rules(
+ rules.StringNotEmpty(),
+ rules.OneOf("Jake", "George")),
+ govy.For(func(t Teacher) University { return t.University }).
+ WithName("university").
+ Include(govy.New(
+ govy.For(func(u University) string { return u.Address }).
+ WithName("address").
+ Required(),
+ )),
+ )
+
+ teacher := Teacher{
+ Name: "John",
+ University: University{
+ Name: "Poznan University of Technology",
+ Address: "",
+ },
+ }
+
+ // We're using a mock testing.T to capture the error produced by the assertion.
+ mt := new(mockTestingT)
+
+ err := teacherValidator.WithName("John").Validate(teacher)
+ govytest.AssertNoError(mt, err)
+
+ // This will print the error produced by the assertion.
+ fmt.Println(mt.recordedError)
+
+ // Output:
+ // Received unexpected error:
+ // {
+ // "errors": [
+ // {
+ // "propertyPath": "name",
+ // "propertyValue": "John",
+ // "errors": [
+ // {
+ // "error": "must be one of: Jake, George",
+ // "code": "one_of",
+ // "description": "must be one of: Jake, George"
+ // }
+ // ]
+ // },
+ // {
+ // "propertyPath": "university.address",
+ // "errors": [
+ // {
+ // "error": "property is required but was empty",
+ // "code": "required"
+ // }
+ // ]
+ // }
+ // ],
+ // "name": "John"
+ // }
+}
+```
+
+
+
+**Assert exact structured rule errors.**
+
+[//]: # (embed: ExampleAssertError)
+
+```go
+// Verifying that expected errors were produced by [govy.Validator.Validate] can be a tedious task.
+// Often times we might only care about [govy.ErrorCode] and not the message or description of the error.
+// To help in that process, [govytest.AssertError] can be used to ensure that the expected errors were produced.
+// It accepts multiple [govytest.ExpectedRuleError], each being a short and concise
+// representation of the error we're expecting to occur.
+// For more details on how to use [govytest.ExpectedRuleError], see its code documentation.
+//
+// To demonstrate the erroneous output of [govytest.AssertError] we'll fail the assertion.
+func ExampleAssertError() {
+ teacherValidator := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Required().
+ Rules(
+ rules.StringNotEmpty(),
+ rules.OneOf("Jake", "George")),
+ govy.For(func(t Teacher) University { return t.University }).
+ WithName("university").
+ Include(govy.New(
+ govy.For(func(u University) string { return u.Address }).
+ WithName("address").
+ Required(),
+ )),
+ )
+
+ teacher := Teacher{
+ Name: "John",
+ University: University{
+ Name: "Poznan University of Technology",
+ Address: "",
+ },
+ }
+
+ // We're using a mock testing.T to capture the error produced by the assertion.
+ mt := new(mockTestingT)
+
+ err := teacherValidator.WithName("John").Validate(teacher)
+ govytest.AssertError(mt, err,
+ govytest.ExpectedRuleError{
+ PropertyPath: "name",
+ ContainsMessage: "one of",
+ },
+ govytest.ExpectedRuleError{
+ PropertyPath: "university.address",
+ Code: "greater_than",
+ },
+ )
+
+ // This will print the error produced by the assertion.
+ fmt.Println(mt.recordedError)
+
+ // Output:
+ // Expected error was not found.
+ // EXPECTED:
+ // {
+ // "propertyPath": "university.address",
+ // "code": "greater_than"
+ // }
+ // ACTUAL:
+ // [
+ // {
+ // "propertyPath": "name",
+ // "propertyValue": "John",
+ // "errors": [
+ // {
+ // "error": "must be one of: Jake, George",
+ // "code": "one_of",
+ // "description": "must be one of: Jake, George"
+ // }
+ // ]
+ // },
+ // {
+ // "propertyPath": "university.address",
+ // "errors": [
+ // {
+ // "error": "property is required but was empty",
+ // "code": "required"
+ // }
+ // ]
+ // }
+ // ]
+}
+```
+
+
+
+**Assert nested ValidatorErrors produced by slice validation.**
+
+[//]: # (embed: ExampleAssertError_validatorErrors)
+
+```go
+// [govytest.AssertError] can handle not only [govy.ValidatorError] but also a slice of these
+// wrapped into [govy.ValidatorErrors].
+//
+// For instance, [govy.ValidatorErrors] is returned from [govy.Validator.ValidateSlice],
+// you can also choose to construct this slice type yourself.
+// In any case, in order to match [govytest.ExpectedRuleError] the assertion function needs
+// to first match it with the right [govy.ValidatorError].
+//
+// In order to achieve that you need to set the following fields:
+// - [govytest.ExpectedRuleError.ValidatorName] which matches [govy.ValidatorError.Name]
+// - [govytest.ExpectedRuleError.ValidatorIndex] which matches [govy.ValidatorError.SliceIndex]
+//
+// You can set them both, but you need to provide at least one of them.
+//
+// Every [govytest.ExpectedRuleError] is aggregated per matched [govy.ValidatorError]
+// and the function runs recursively for every [govy.ValidatorError] and expected errors pair.
+func ExampleAssertError_validatorErrors() {
+ teacherValidator := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Required().
+ Rules(
+ rules.StringNotEmpty(),
+ rules.OneOf("Eve", "George")),
+ govy.For(func(t Teacher) University { return t.University }).
+ WithName("university").
+ Include(govy.New(
+ govy.For(func(u University) string { return u.Address }).
+ WithName("address").
+ Required(),
+ )),
+ )
+
+ teacherEve := Teacher{
+ Name: "Eve",
+ University: University{
+ Name: "Poznan University of Technology",
+ Address: "",
+ },
+ }
+ teacherJohn := Teacher{
+ Name: "John",
+ University: University{
+ Name: "Poznan University of Technology",
+ Address: "Some address",
+ },
+ }
+ teachers := []Teacher{teacherEve, teacherJohn}
+
+ // We're using a mock testing.T to capture the error produced by the assertion.
+ mt := new(mockTestingT)
+
+ err := teacherValidator.WithNameFunc(func(s Teacher) string { return s.Name }).ValidateSlice(teachers)
+ govytest.AssertError(mt, err,
+ govytest.ExpectedRuleError{
+ PropertyPath: "university.address",
+ Code: "greater_than",
+ ValidatorName: "Eve",
+ ValidatorIndex: ptr(0),
+ },
+ govytest.ExpectedRuleError{
+ PropertyPath: "name",
+ ContainsMessage: "one of",
+ ValidatorName: "John",
+ ValidatorIndex: ptr(1),
+ },
+ )
+
+ // This will print the error produced by the assertion.
+ fmt.Println(mt.recordedError)
+
+ // Output:
+ // Expected error was not found.
+ // EXPECTED:
+ // {
+ // "propertyPath": "university.address",
+ // "code": "greater_than",
+ // "validatorName": "Eve",
+ // "validatorIndex": 0
+ // }
+ // ACTUAL:
+ // [
+ // {
+ // "propertyPath": "university.address",
+ // "errors": [
+ // {
+ // "error": "property is required but was empty",
+ // "code": "required"
+ // }
+ // ]
+ // }
+ // ]
+}
+```
+
+
+
+**Assert that one expected error is present among other errors.**
+
+[//]: # (embed: ExampleAssertErrorContains)
+
+```go
+// If you don't want to verify all the errors returned by [govy.Validator],
+// but ensure a single, expected error is produced use [govytest.AssertErrorContains]
+// instead of [govytest.AssertError].
+//
+// To demonstrate the erroneous output of [govytest.AssertErrorContains]
+// we'll first match the error and then fail the assertion.
+func ExampleAssertErrorContains() {
+ teacherValidator := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Required().
+ Rules(
+ rules.StringNotEmpty(),
+ rules.OneOf("Jake", "George")),
+ govy.For(func(t Teacher) University { return t.University }).
+ WithName("university").
+ Include(govy.New(
+ govy.For(func(u University) string { return u.Address }).
+ WithName("address").
+ Required(),
+ )),
+ )
+
+ teacher := Teacher{
+ Name: "John",
+ University: University{
+ Name: "Poznan University of Technology",
+ Address: "",
+ },
+ }
+
+ // We're using a mock testing.T to capture the error produced by the assertion.
+ mt := new(mockTestingT)
+
+ // Match the error.
+ err := teacherValidator.WithName("John").Validate(teacher)
+ govytest.AssertErrorContains(mt, err, govytest.ExpectedRuleError{
+ PropertyPath: "name",
+ Code: "one_of",
+ })
+
+ // Fail to match the error.
+ err = teacherValidator.WithName("John").Validate(teacher)
+ govytest.AssertErrorContains(mt, err, govytest.ExpectedRuleError{
+ PropertyPath: "university.address",
+ Code: "greater_than",
+ })
+
+ // This will print the error produced by the assertion.
+ fmt.Println(mt.recordedError)
+
+ // Output:
+ // Expected error was not found.
+ // EXPECTED:
+ // {
+ // "propertyPath": "university.address",
+ // "code": "greater_than"
+ // }
+ // ACTUAL:
+ // [
+ // {
+ // "propertyPath": "name",
+ // "propertyValue": "John",
+ // "errors": [
+ // {
+ // "error": "must be one of: Jake, George",
+ // "code": "one_of",
+ // "description": "must be one of: Jake, George"
+ // }
+ // ]
+ // },
+ // {
+ // "propertyPath": "university.address",
+ // "errors": [
+ // {
+ // "error": "property is required but was empty",
+ // "code": "required"
+ // }
+ // ]
+ // }
+ // ]
+}
+```
diff --git a/.agents/skills/govy/references/path-inference.md b/.agents/skills/govy/references/path-inference.md
new file mode 100644
index 00000000..cad30663
--- /dev/null
+++ b/.agents/skills/govy/references/path-inference.md
@@ -0,0 +1,192 @@
+# Path Inference
+
+Runtime and generated path inference, govyconfig registration, and inference caching behavior.
+
+## Topics
+
+- [Choose a path inference mode](#choose-a-path-inference-mode)
+ - [Configure inference modes and include test files when needed.](#configure-inference-modes-and-include-test-files-when-needed)
+ - [Use generated inference by registering generated paths.](#use-generated-inference-by-registering-generated-paths)
+- [Avoid late inference changes](#avoid-late-inference-changes)
+ - [Changing inference mode after first validation does not recompute paths.](#changing-inference-mode-after-first-validation-does-not-recompute-paths)
+
+## Choose a path inference mode
+
+Prefer explicit paths unless inferred paths are part of the design. Runtime inference trades startup simplicity for a one-time AST lookup, while generated inference moves that work into code generation.
+
+
+
+**Configure inference modes and include test files when needed.**
+
+[//]: # (embed: ExampleInferPathMode)
+
+```go
+// In the interactive tutorial for govy, we've been using
+// [govy.PropertyRules.WithName] to provide explicit path segments for our properties.
+//
+// Ideally, we'd want govy to derive those paths directly from the getter expressions,
+// matching the struct fields selected by the user.
+// Go uses struct tags to achieve that,
+// and libraries like [encoding/json] use these tags 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 property path (with a catch).
+// The catch being that the path 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 paths.
+//
+// So, how do we do that properly?
+// Both [govy.Validator] and [govy.PropertyRules] (including variants) have a dedicated method
+// to configure how property paths are inferred.
+//
+// It depends on the [govy.InferPathMode] used:
+// - [govy.InferPathModeDisable], path inference is disabled (default), nothing to do here
+// - [govy.InferPathModeRuntime], the path is inferred during runtime from the getter expression.
+// This is the most flexible option, but also the slowest, although the slowdown
+// is incurred only once, whenever [govy.PropertyRules.Validate] is first called.
+// If you make sure that [govy.PropertyRules] is created only once and don't mind
+// the one-time performance hit, this should be enough for you.
+// - [govy.InferPathModeGenerate], the path is inferred during a separate code generation phase.
+// This mode requires you to run `govy inferpath` before you run your code.
+// It generates a file with inferred relative paths for your getter call sites,
+// which automatically registers them using [govyconfig.SetInferredPath].
+//
+// Since this tutorial is run as a test,
+// we need to explicitly instruct govy to infer paths from test files.
+// By default, test files are not parsed to improve performance.
+// In order to do that, we use [govyconfig.SetInferPathIncludeTestFiles].
+func ExampleInferPathMode() {
+ govyconfig.SetInferPathIncludeTestFiles(true)
+ defer govyconfig.SetInferPathIncludeTestFiles(false)
+
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ Rules(rules.EQ("Jerry")),
+ ).
+ InferPath(govy.InferPathModeRuntime).
+ 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':
+ // - must be equal to 'Jerry'
+}
+```
+
+
+
+**Use generated inference by registering generated paths.**
+
+[//]: # (embed: ExampleInferPathModeGenerate)
+
+```go
+// In the previous example we've seen [govy.InferPathModeRuntime] in action.
+// An alternative for the aforementioned mode which offers better runtime performance
+// is [govy.InferPathModeGenerate].
+//
+// It comes at a cost of having to run the code generation utility before running your code.
+// The utility generates code which uses [govyconfig.SetInferredPath].
+// We'll use this very function in this example to simulate the code generation step.
+// The first validator, 'v1', is created with [govy.InferPathModeDisable],
+// the second validator, 'v2' is created with [govy.InferPathModeGenerate].
+// As you can see in the output, only the second validator, 'v2' has the inferred path.
+func ExampleInferPathModeGenerate() {
+ govyconfig.SetInferPathIncludeTestFiles(true)
+ defer govyconfig.SetInferPathIncludeTestFiles(false)
+
+ v1 := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ Rules(rules.EQ("Jerry")),
+ ).
+ InferPath(govy.InferPathModeDisable).
+ WithName("Teacher")
+
+ govyconfig.SetInferredPath(govyconfig.InferredPath{
+ Path: jsonpath.New().Name("name"),
+ File: "pkg/govy/example_test.go",
+ Line: 2042,
+ })
+
+ v2 := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ Rules(rules.EQ("Thomas")),
+ ).
+ InferPath(govy.InferPathModeGenerate).
+ WithName("NotTeacher")
+
+ teacher := Teacher{Name: "Tom"}
+ if err := v1.Validate(teacher); err != nil {
+ fmt.Println(err)
+ }
+ if err := v2.Validate(teacher); err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation for Teacher has failed:
+ // - must be equal to 'Jerry'
+ // Validation for NotTeacher has failed for the following properties:
+ // - 'name' with value 'Tom':
+ // - must be equal to 'Thomas'
+}
+```
+
+## Avoid late inference changes
+
+Path inference is cached after first validation. Configure the mode before validation, not after an empty path has already been inferred.
+
+
+
+**Changing inference mode after first validation does not recompute paths.**
+
+[//]: # (embed: ExampleValidator_InferPath_changeModeInRuntime)
+
+```go
+// Knowing when to call [govy.Validator.InferPath] is important.
+// The path inference runs only once per [govy.PropertyRules] instance, on the first validation.
+// Once this happens, the result is cached, even if that result is an empty path.
+//
+// This example demonstrates that changing the mode after the first validation has no effect.
+// The first validation runs with [govy.InferPathModeDisable], which produces an empty path.
+// This empty result is then cached. Even after switching to [govy.InferPathModeRuntime],
+// the cached empty result persists, so no property path appears in the output.
+func ExampleValidator_InferPath_changeModeInRuntime() {
+ govyconfig.SetInferPathIncludeTestFiles(true)
+ defer govyconfig.SetInferPathIncludeTestFiles(false)
+
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ Rules(rules.EQ("Jerry")),
+ ).
+ InferPath(govy.InferPathModeDisable).
+ WithName("Teacher")
+
+ teacher := Teacher{Name: "Tom"}
+ err := v.Validate(teacher)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ fmt.Println("---\nAfter setting Runtime infer mode.\n---")
+ err = v.InferPath(govy.InferPathModeRuntime).Validate(teacher)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation for Teacher has failed:
+ // - must be equal to 'Jerry'
+ // ---
+ // After setting Runtime infer mode.
+ // ---
+ // Validation for Teacher has failed:
+ // - must be equal to 'Jerry'
+}
+```
diff --git a/.agents/skills/govy/references/predefined-rules.md b/.agents/skills/govy/references/predefined-rules.md
new file mode 100644
index 00000000..a545b2c2
--- /dev/null
+++ b/.agents/skills/govy/references/predefined-rules.md
@@ -0,0 +1,261 @@
+# Predefined Rules
+
+Examples that show selected predefined rules in use. For the generated catalog of all existing predefined rule constructors, see [Existing Rules](existing-rules.md).
+
+## Topics
+
+- [Collection constraints](#collection-constraints)
+ - [Require values in a slice to be unique.](#require-values-in-a-slice-to-be-unique)
+- [Property relationship rules](#property-relationship-rules)
+ - [Require exactly one value across mutually exclusive properties.](#require-exactly-one-value-across-mutually-exclusive-properties)
+ - [Require one populated value from a set of properties.](#require-one-populated-value-from-a-set-of-properties)
+- [Property comparison rules](#property-comparison-rules)
+ - [Compare two properties for equality.](#compare-two-properties-for-equality)
+ - [Compare ordered primitive properties.](#compare-ordered-primitive-properties)
+ - [Compare custom comparable properties.](#compare-custom-comparable-properties)
+
+## Collection constraints
+
+Use collection rules when the validation decision depends on all values, not a single property value.
+
+
+
+**Require values in a slice to be unique.**
+
+[//]: # (embed: ExampleSliceUnique)
+
+```go
+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
+}
+```
+
+## Property relationship rules
+
+Use property relationship rules when one field controls whether another field may or must be set.
+
+
+
+**Require exactly one value across mutually exclusive properties.**
+
+[//]: # (embed: ExampleMutuallyExclusive)
+
+```go
+func ExampleMutuallyExclusive() {
+ v := govy.New(
+ govy.ForSlice(func(t Teacher) []Student { return t.Students }).
+ WithName("students").
+ RulesForEach(rules.MutuallyExclusive(true, map[string]func(Student) any{
+ "index": func(s Student) any { return s.Index },
+ "name": func(s Student) any { return s.Name },
+ })),
+ )
+ teacher := Teacher{
+ Students: []Student{
+ {Index: "foo"},
+ {Index: "bar", Name: "John"},
+ {Name: "Eve"},
+ {},
+ },
+ }
+ err := v.Validate(teacher)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation has failed for the following properties:
+ // - 'students[1]' with value '{"index":"bar","name":"John"}':
+ // - [index, name] properties are mutually exclusive, provide only one of them
+ // - 'students[3]':
+ // - one of [index, name] properties must be set, none was provided
+}
+```
+
+
+
+**Require one populated value from a set of properties.**
+
+[//]: # (embed: ExampleOneOfProperties)
+
+```go
+func ExampleOneOfProperties() {
+ v := govy.New(
+ govy.ForSlice(func(t Teacher) []Student { return t.Students }).
+ WithName("students").
+ RulesForEach(rules.OneOfProperties(map[string]func(Student) any{
+ "index": func(s Student) any { return s.Index },
+ "name": func(s Student) any { return s.Name },
+ })),
+ )
+ teacher := Teacher{
+ Students: []Student{
+ {Index: "foo"},
+ {},
+ {Name: "John"},
+ {Index: "bar", Name: "Eve"},
+ },
+ }
+ err := v.Validate(teacher)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation has failed for the following properties:
+ // - 'students[1]':
+ // - one of [index, name] properties must be set, none was provided
+}
+```
+
+## Property comparison rules
+
+Use comparison rules when multiple properties must preserve ordering or equality. Pick comparable variants for types such as time.Time that define their own ordering contract.
+
+
+
+**Compare two properties for equality.**
+
+[//]: # (embed: ExampleEqualProperties)
+
+```go
+func ExampleEqualProperties() {
+ v := govy.New(
+ govy.ForSlice(func(t Teacher) []Student { return t.Students }).
+ WithName("students").
+ RulesForEach(rules.EqualProperties(rules.CompareFunc, map[string]func(Student) any{
+ "index": func(s Student) any { return s.Index },
+ "indexCopy": func(s Student) any { return s.IndexCopy },
+ })),
+ )
+ teacher := Teacher{
+ Students: []Student{
+ {Index: "foo", IndexCopy: "foo"},
+ {Index: "bar"},
+ {IndexCopy: "foo"},
+ {}, // Both index and indexCopy are empty strings, and thus equal.
+ },
+ }
+ err := v.Validate(teacher)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation has failed for the following properties:
+ // - 'students[1]' with value '{"index":"bar"}':
+ // - all of [index, indexCopy] properties must be equal, but 'index' is not equal to 'indexCopy'
+ // - 'students[2]' with value '{"indexCopy":"foo"}':
+ // - all of [index, indexCopy] properties must be equal, but 'index' is not equal to 'indexCopy'
+}
+```
+
+
+
+**Compare ordered primitive properties.**
+
+[//]: # (embed: ExampleLTProperties)
+
+```go
+func ExampleLTProperties() {
+ type IntRange struct {
+ Min int `json:"min"`
+ Max int `json:"max"`
+ }
+
+ v := govy.New(
+ govy.For(govy.GetSelf[IntRange]()).
+ Rules(
+ rules.LTProperties(
+ "min", func(r IntRange) int { return r.Min },
+ "max", func(r IntRange) int { return r.Max },
+ ),
+ ),
+ )
+
+ // Valid case: min < max
+ err := v.Validate(IntRange{Min: 1, Max: 10})
+ fmt.Println("Valid:", err == nil)
+
+ // Invalid case: min >= max
+ err = v.Validate(IntRange{Min: 10, Max: 1})
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Valid: true
+ // Validation has failed:
+ // - 'min' must be less than 'max'
+}
+```
+
+
+
+**Compare custom comparable properties.**
+
+[//]: # (embed: ExampleLTComparableProperties)
+
+```go
+// LTComparableProperties and other *ComparableProperties functions work with types
+// that implement [rules.Comparable] interface, such as [time.Time].
+func ExampleLTComparableProperties() {
+ type TimeRange struct {
+ StartTime time.Time `json:"startTime"`
+ EndTime time.Time `json:"endTime"`
+ }
+
+ v := govy.New(
+ govy.For(govy.GetSelf[TimeRange]()).
+ Rules(
+ rules.LTComparableProperties(
+ "startTime", func(tr TimeRange) time.Time { return tr.StartTime },
+ "endTime", func(tr TimeRange) time.Time { return tr.EndTime },
+ ),
+ ),
+ )
+
+ // Valid case: start is before end
+ err := v.Validate(TimeRange{
+ StartTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
+ EndTime: time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC),
+ })
+ fmt.Println("Valid:", err == nil)
+
+ // Invalid case: start is after end
+ err = v.Validate(TimeRange{
+ StartTime: time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC),
+ EndTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
+ })
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Valid: true
+ // Validation has failed:
+ // - 'startTime' must be before 'endTime'
+}
+```
diff --git a/.agents/skills/govy/references/properties-and-paths.md b/.agents/skills/govy/references/properties-and-paths.md
new file mode 100644
index 00000000..a5e409c4
--- /dev/null
+++ b/.agents/skills/govy/references/properties-and-paths.md
@@ -0,0 +1,550 @@
+# Properties and Paths
+
+Property getters, names, explicit JSON paths, pointers, transforms, required and optional values, hidden values, and property-level conditions.
+
+## Topics
+
+- [Name and path properties](#name-and-path-properties)
+ - [Name a property rule with one path segment.](#name-a-property-rule-with-one-path-segment)
+ - [Do not pass dotted paths to WithName.](#do-not-pass-dotted-paths-to-withname)
+ - [Set multi-segment paths with jsonpath.Path.](#set-multi-segment-paths-with-jsonpathpath)
+- [Validate optional and transformed values](#validate-optional-and-transformed-values)
+ - [Validate the pointed-to value while skipping nil by default.](#validate-the-pointed-to-value-while-skipping-nil-by-default)
+ - [Transform a property value before applying rules.](#transform-a-property-value-before-applying-rules)
+- [Handle empty or sensitive values](#handle-empty-or-sensitive-values)
+ - [Require a non-empty value before normal rules run.](#require-a-non-empty-value-before-normal-rules-run)
+ - [Skip rule evaluation for empty optional values.](#skip-rule-evaluation-for-empty-optional-values)
+ - [Hide sensitive property values in errors.](#hide-sensitive-property-values-in-errors)
+- [Use whole-object context](#use-whole-object-context)
+ - [Validate using the whole current object.](#validate-using-the-whole-current-object)
+ - [Run property rules only when predicates match.](#run-property-rules-only-when-predicates-match)
+- [Control property-level aggregation](#control-property-level-aggregation)
+ - [Stop evaluating later rules on one property.](#stop-evaluating-later-rules-on-one-property)
+
+## Name and path properties
+
+Prefer WithName for a single path segment. Use WithPath when the rule logically targets a nested path or a specific index.
+
+
+
+**Name a property rule with one path segment.**
+
+[//]: # (embed: ExamplePropertyRules_WithName)
+
+```go
+// So far we've been using a very simple [govy.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 an explicit path segment using [govy.PropertyRules.WithName].
+//
+// We can also change the [govy.Rule] to be something more real.
+// govy comes with a number of predefined [govy.Rule], we'll use
+// [rules.EQ] which accepts a single argument, value to compare with.
+func ExamplePropertyRules_WithName() {
+ 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':
+ // - must be equal to 'Tom'
+}
+```
+
+
+
+**Do not pass dotted paths to WithName.**
+
+[//]: # (embed: ExamplePropertyRules_WithName_wrongUsage)
+
+```go
+// Beware that anything passed into [govy.PropertyRules.WithName] is treated as a single path segment.
+// If you pass a dot-separated path-like string into this method, govy renders
+// the dots as escaped characters inside one bracket-quoted segment.
+// For multi-segment paths, use [govy.PropertyRules.WithPath] instead.
+//
+// Note: Prior to v0.25.0, [govy.PropertyRules.WithName] treated every string
+// as a path, so this usage was valid then.
+func ExamplePropertyRules_WithName_wrongUsage() {
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.University.Name }).
+ WithName("university.name"). // WRONG USAGE!
+ Rules(rules.EQ("Tom").WithMessage("yikes, looks like you used WithName instead of WithPath!")),
+ ).WithName("Teacher")
+
+ teacher := Teacher{
+ Name: "Jake",
+ University: University{
+ Name: "Poznan University of Technology",
+ },
+ }
+
+ err := v.Validate(teacher)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation for Teacher has failed for the following properties:
+ // - '['university.name']' with value 'Poznan University of Technology':
+ // - yikes, looks like you used WithName instead of WithPath!
+}
+```
+
+
+
+**Set multi-segment paths with jsonpath.Path.**
+
+[//]: # (embed: ExamplePropertyRules_WithPath)
+
+```go
+// While [govy.PropertyRules.WithName] is convenient and we recommend using it,
+// sometimes you might want to define rules that access nested fields directly.
+// That's what [govy.PropertyRules.WithPath] is for.
+//
+// Unlike [govy.PropertyRules.WithName], [govy.PropertyRules.WithPath] accepts a
+// [jsonpath.Path] with one or more segments.
+// [govy.PropertyRules.WithName] is just shorthand for `jsonpath.New().Name(...)`.
+//
+// You can either:
+// - pass a string representation of path directly with [jsonpath.Parse]
+// - construct the path with a builder API, starting with [jsonpath.New]
+func ExamplePropertyRules_WithPath() {
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.University.Name }).
+ WithPath(jsonpath.Parse("university.name")).
+ Rules(rules.EQ("Tom")),
+ govy.For(func(t Teacher) string { return t.Students[0].Index }).
+ WithPath(jsonpath.New().Name("students").Index(0).Name("index")).
+ Rules(rules.EQ("2")),
+ ).WithName("Teacher")
+
+ teacher := Teacher{
+ Name: "Jake",
+ University: University{
+ Name: "Poznan University of Technology",
+ },
+ Students: []Student{
+ {Index: "1"},
+ },
+ }
+
+ err := v.Validate(teacher)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation for Teacher has failed for the following properties:
+ // - 'university.name' with value 'Poznan University of Technology':
+ // - must be equal to 'Tom'
+ // - 'students[0].index' with value '1':
+ // - must be equal to '2'
+}
+```
+
+## Validate optional and transformed values
+
+Use ForPointer for optional pointer fields and Transform when rule input should differ from the stored field type.
+
+
+
+**Validate the pointed-to value while skipping nil by default.**
+
+[//]: # (embed: ExampleForPointer)
+
+```go
+// [govy.For] constructor creates new [govy.PropertyRules] instance.
+// It's only argument, [govy.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] 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] 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.
+func ExampleForPointer() {
+ 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
+}
+```
+
+
+
+**Transform a property value before applying rules.**
+
+[//]: # (embed: ExampleTransform)
+
+```go
+// [govy.Transform] constructor can be used to transform the property's value
+// before it's passed to the rules' evaluation.
+// It's useful when you want to use rules that operate on a different type than the property's.
+//
+// Along with the standard [govy.PropertyGetter] it accepts a [govy.Transformer] function
+// which takes the property value and returns the transformed value along with an error.
+// If the error is not nil, the validation will fail with the error message returned by [govy.Transformer] error.
+//
+// In this example we'll use [time.ParseDuration] to transform the string value of [Clock.Duration] to [time.Duration].
+// The first value we'll validate will force [govy.Transformer] to return an error,
+// the second will succeed transformation, but it will fail the validation for [rules.DurationPrecision].
+//
+// Notice how the [govy.Transformer] shape adheres to a lot of standard library conversion/parsing functions.
+func ExampleTransform() {
+ type Clock struct {
+ Duration string `json:"duration"`
+ }
+ v := govy.New(
+ govy.Transform(func(c Clock) string { return c.Duration }, time.ParseDuration).
+ WithName("duration").
+ Rules(rules.DurationPrecision(time.Minute)),
+ ).WithName("MyClock")
+
+ err := v.Validate(Clock{Duration: "bad duration!"})
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ err = v.Validate(Clock{Duration: (256 * time.Second).String()})
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation for MyClock has failed for the following properties:
+ // - 'duration' with value 'bad duration!':
+ // - time: invalid duration "bad duration!"
+ // Validation for MyClock has failed for the following properties:
+ // - 'duration' with value '4m16s':
+ // - duration must be defined with 1m0s precision
+}
+```
+
+## Handle empty or sensitive values
+
+Use Required to fail fast on empty values, OmitEmpty to skip optional direct values, and HideValue when errors must not expose the original input.
+
+
+
+**Require a non-empty value before normal rules run.**
+
+[//]: # (embed: ExamplePropertyRules_Required)
+
+```go
+// By default, when [govy.PropertyRules] is constructed using [govy.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].
+//
+// 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].
+//
+// [govy.PropertyRules.Required] when used with [govy.For] constructor, will ensure
+// the property does not contain a zero value.
+//
+// Note: [govy.PropertyRules.Required] is introducing a short circuit.
+// If the assertion fails, validation will stop and return [rules.ErrorCodeRequired].
+// None of the rules you've defined would be evaluated.
+//
+// Note: Placement of [govy.PropertyRules.Required] does not matter,
+// it's not evaluated in a sequential loop, unlike standard [govy.Rule].
+// However, we recommend you always place it below [govy.PropertyRules.WithName]
+// to make your rules more readable.
+func ExamplePropertyRules_Required() {
+ 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
+}
+```
+
+
+
+**Skip rule evaluation for empty optional values.**
+
+[//]: # (embed: ExamplePropertyRules_OmitEmpty)
+
+```go
+// While [govy.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] will do the trick.
+//
+// Note: [govy.PropertyRules.OmitEmpty] will have no effect on pointers handled
+// by [govy.ForPointer], as they already behave in the same way.
+func ExamplePropertyRules_OmitEmpty() {
+ 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
+}
+```
+
+
+
+**Hide sensitive property values in errors.**
+
+[//]: # (embed: ExamplePropertyRules_HideValue)
+
+```go
+// 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] 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').
+func ExamplePropertyRules_HideValue() {
+ 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
+}
+```
+
+## Use whole-object context
+
+Use GetSelf when the rule compares multiple fields in the same object. Use property-level When to keep branch-specific rules isolated.
+
+
+
+**Validate using the whole current object.**
+
+[//]: # (embed: ExampleGetSelf)
+
+```go
+// If you want to access the value of the entity you're writing the [govy.Validator] for,
+// you can use [govy.GetSelf] function which is a convenience [govy.PropertyGetter] that returns self.
+// Note that we don't call [govy.PropertyRules.WithName] here,
+// as we're comparing two properties in our top level, [Teacher] scope.
+//
+// You can provide your own rules using [govy.NewRule] constructor.
+// It returns new [govy.Rule] instance which wraps your validation function.
+func ExampleGetSelf() {
+ 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:
+ // - now I have access to the whole teacher
+}
+```
+
+
+
+**Run property rules only when predicates match.**
+
+[//]: # (embed: ExamplePropertyRules_When)
+
+```go
+// To only run property validation on condition, use [govy.PropertyRules.When].
+// Predicates set through [govy.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].
+//
+// It's recommended to define [govy.PropertyRules.When] before [govy.PropertyRules.Rules] declaration.
+func ExamplePropertyRules_When() {
+ 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':
+ // - must not be equal to 'Jerry'
+}
+```
+
+## Control property-level aggregation
+
+Property cascade overrides validator cascade for that property and is useful when later rules add noise after an earlier failure.
+
+
+
+**Stop evaluating later rules on one property.**
+
+[//]: # (embed: ExamplePropertyRules_Cascade)
+
+```go
+// To customize how [govy.Rule] are evaluated use [govy.PropertyRules.Cascade].
+// Use [govy.CascadeModeStop] to stop validation after the first error.
+// If you wish to revert to the default behavior, use [govy.CascadeModeContinue].
+//
+// Note: the cascade mode change only applies to the given [govy.PropertyRules] instance
+// and not the parent [govy.Validator] or neighboring [govy.PropertyRules].
+// It does however override the [govy.CascadeMode] set for [govy.Validator].
+func ExamplePropertyRules_Cascade() {
+ 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':
+ // - must not be equal to 'Jerry'
+}
+```
diff --git a/.agents/skills/govy/references/rules-and-messages.md b/.agents/skills/govy/references/rules-and-messages.md
new file mode 100644
index 00000000..0b994951
--- /dev/null
+++ b/.agents/skills/govy/references/rules-and-messages.md
@@ -0,0 +1,758 @@
+# Creating and Modifying Rules
+
+Creating new rules and modifying rules with details, examples, error codes, messages, message templates, template functions, pointer conversion, and rule sets.
+
+## Topics
+
+- [Create custom rules](#create-custom-rules)
+ - [Create a custom rule from a validation function.](#create-a-custom-rule-from-a-validation-function)
+- [Add rule context](#add-rule-context)
+ - [Attach static details to a rule error.](#attach-static-details-to-a-rule-error)
+ - [Attach formatted details when the context is computed.](#attach-formatted-details-when-the-context-is-computed)
+ - [Attach valid input examples for complex rules.](#attach-valid-input-examples-for-complex-rules)
+ - [Attach stable error codes for tests and integrations.](#attach-stable-error-codes-for-tests-and-integrations)
+- [Customize rule messages](#customize-rule-messages)
+ - [Override a rule message with a fixed string.](#override-a-rule-message-with-a-fixed-string)
+ - [Build a rule message dynamically from the failed value.](#build-a-rule-message-dynamically-from-the-failed-value)
+- [Use message templates](#use-message-templates)
+ - [Attach a template from a string.](#attach-a-template-from-a-string)
+ - [Attach a pre-parsed template.](#attach-a-pre-parsed-template)
+- [Add template helper functions](#add-template-helper-functions)
+ - [Register the builtin helper functions on a template.](#register-the-builtin-helper-functions-on-a-template)
+ - [Format example slices the same way default messages do.](#format-example-slices-the-same-way-default-messages-do)
+ - [Join slice values in custom template output.](#join-slice-values-in-custom-template-output)
+ - [Indent multiline template output.](#indent-multiline-template-output)
+- [Describe rules for generated plans](#describe-rules-for-generated-plans)
+ - [Attach a plan-only rule description.](#attach-a-plan-only-rule-description)
+- [Reuse rule sets](#reuse-rule-sets)
+ - [Convert a rule so it can validate pointer values.](#convert-a-rule-so-it-can-validate-pointer-values)
+ - [Group multiple rules into a reusable set.](#group-multiple-rules-into-a-reusable-set)
+ - [Convert a whole rule set for pointer validation.](#convert-a-whole-rule-set-for-pointer-validation)
+ - [Stop evaluating later rules in a rule set after failure.](#stop-evaluating-later-rules-in-a-rule-set-after-failure)
+
+## Create custom rules
+
+Use custom rules for validation that is not covered by pkg/rules. Keep the rule function focused on the validation decision and attach context through rule metadata methods.
+
+
+
+**Create a custom rule from a validation function.**
+
+[//]: # (embed: ExampleRule)
+
+```go
+// Govy comes with a set of predefined rules,
+// which you can use out of the box by importing [rules] package.
+//
+// However, you can also create your own rules by using [govy.NewRule] constructor.
+// It accepts a simple validation function which takes in a value
+// and returns an error if the validation failed.
+//
+// Note: the [govy.Rule] struct has all its fields private,
+// so you can only create and modify them using exported constructor and methods.
+func ExampleRule() {
+ myRule := govy.NewRule(func(name string) error {
+ if name != "Tom" {
+ return fmt.Errorf("Teacher can be only Tom")
+ }
+ return nil
+ })
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Rules(myRule),
+ )
+
+ teacher := Teacher{Name: "Jake"}
+
+ err := v.Validate(teacher)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation has failed for the following properties:
+ // - 'name' with value 'Jake':
+ // - Teacher can be only Tom
+}
+```
+
+## Add rule context
+
+Use details, examples, and error codes to make rule failures useful to both humans and tests. Details extend the message, examples document valid inputs, and error codes provide a stable assertion surface.
+
+
+
+**Attach static details to a rule error.**
+
+[//]: # (embed: ExampleRule_WithDetails)
+
+```go
+// You can use [govy.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.
+func ExampleRule_WithDetails() {
+ 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 :)
+}
+```
+
+
+
+**Attach formatted details when the context is computed.**
+
+[//]: # (embed: ExampleRule_WithDetailsf)
+
+```go
+// You can use [govy.Rule.WithDetailsf] to add formatted details to the returned [govy.RuleError] error message.
+func ExampleRule_WithDetailsf() {
+ minLen := 3
+ maxLen := 10
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Rules(rules.StringLength(minLen, maxLen).
+ WithDetailsf("Teacher name must be between %d and %d characters", minLen, maxLen)),
+ ).WithName("Teacher")
+
+ teacher := Teacher{
+ Name: "Jo",
+ 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 'Jo':
+ // - length must be between 3 and 10; Teacher name must be between 3 and 10 characters
+}
+```
+
+
+
+**Attach valid input examples for complex rules.**
+
+[//]: # (embed: ExampleRule_WithExamples)
+
+```go
+// You can use [govy.Rule.WithExamples] to add examples of valid inputs
+// which pass the [govy.Rule].
+// This can be useful for more complex rules, especially regex based, where
+// it might not be immediately obvious how a valid value should look like.
+//
+// Note: examples are added between the error message and details
+// (configured with [govy.Rule.WithDetails]).
+func ExampleRule_WithExamples() {
+ 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 :)").
+ WithExamples("Tom", "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)$' (e.g. 'Tom', 'Jerry'); Teacher can be either Tom or Jerry :)
+}
+```
+
+
+
+**Attach stable error codes for tests and integrations.**
+
+[//]: # (embed: ExampleRule_WithErrorCode)
+
+```go
+// When testing, it can be tedious to always rely on error messages as these can change over time.
+// Enter [govy.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] to associate [govy.ErrorCode] with a [govy.Rule].
+// Notice that our modified version of [rules.StringMatchRegexp] will now return a new [govy.ErrorCode].
+// Predefined rules have [govy.ErrorCode] already associated with them.
+// To view the list of predefined [govy.ErrorCode] checkout error_codes.go file.
+func ExampleRule_WithErrorCode() {
+ 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
+}
+```
+
+## Customize rule messages
+
+Use WithMessage for fixed replacements and WithMessagef when the failed value or details should shape the final message. These methods replace the base message while preserving details and examples.
+
+
+
+**Override a rule message with a fixed string.**
+
+[//]: # (embed: ExampleRule_WithMessage)
+
+```go
+// If you want to override the default error message, you can use [govy.Rule.WithMessage].
+func ExampleRule_WithMessage() {
+ 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 :)
+}
+```
+
+
+
+**Build a rule message dynamically from the failed value.**
+
+[//]: # (embed: ExampleRule_WithMessagef)
+
+```go
+// You can use [govy.Rule.WithMessagef] to override the default error message using printf-like formatting.
+func ExampleRule_WithMessagef() {
+ allowedNames := []string{"Tom", "Jerry"}
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Rules(rules.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")).
+ WithMessagef("name must be one of: %v", allowedNames)),
+ ).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':
+ // - name must be one of: [Tom Jerry]
+}
+```
+
+## Use message templates
+
+Use message templates when message formatting needs access to rule data such as details, examples, or property values. Start with a string template for simple cases and parse templates yourself when you need reuse or validation before attaching them.
+
+
+
+**Attach a template from a string.**
+
+[//]: # (embed: ExampleRule_WithMessageTemplateString)
+
+```go
+// If you want to have more control over the resulting error message, but [govy.Rule.WithMessage]
+// is not enough, you can utilize a template string which is parsed by [govy.Rule] into
+// [template.Template] to construct a custom error message.
+//
+// Each builtin rule supports different variables.
+// For instance, [rules.StringLength] supports 'MinLength' and 'MaxLength' variables.
+// Refer to the rule's documentation to see which variables are supported.
+//
+// Note: Builtin functions provided by [govy.AddTemplateFunctions], like 'formatExamples',
+// are automatically added to the parsed [template.Template].
+func ExampleRule_WithMessageTemplateString() {
+ tplString := `Teacher's name must be between {{ .MinLength }} and {{ .MaxLength }} characters {{ formatExamples .Examples }}.`
+
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Rules(rules.StringLength(5, 10).
+ WithExamples("Joanna", "Angeline").
+ WithMessageTemplateString(tplString)),
+ ).WithName("Teacher")
+
+ teacher := Teacher{
+ Name: "Eve",
+ }
+
+ err := v.Validate(teacher)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation for Teacher has failed for the following properties:
+ // - 'name' with value 'Eve':
+ // - Teacher's name must be between 5 and 10 characters (e.g. 'Joanna', 'Angeline').
+}
+```
+
+
+
+**Attach a pre-parsed template.**
+
+[//]: # (embed: ExampleRule_WithMessageTemplate)
+
+```go
+// If you want to have more control over the [template.Template] used for error message creation,
+// for instance, add custom functions, use [govy.Rule.WithMessageTemplate].
+//
+// In the example below, we're defining a custom template function 'join' which calls [strings.Join]
+// under the hood to join a slice of strings with a comma.
+//
+// Note: 'Examples' field is a plain slice of strings, If you wish to format it the same way
+// as the default message does, use 'formatExamples' function provided by [govy.AddTemplateFunctions].
+func ExampleRule_WithMessageTemplate() {
+ tplString := `Teacher's name '{{ .PropertyValue }}' is not supported. {{ .Details }} (e.g. {{ join .Examples ", " }}).`
+ tpl := template.New("").Funcs(template.FuncMap{"join": strings.Join})
+ tpl = template.Must(tpl.Parse(tplString))
+
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Rules(rules.StringLength(5, 10).
+ WithDetails("Teacher's name must be between 5 and 10 characters").
+ WithExamples("Joanna", "Angeline").
+ WithMessageTemplate(tpl)),
+ ).WithName("Teacher")
+
+ teacher := Teacher{
+ Name: "Eve",
+ }
+
+ err := v.Validate(teacher)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation for Teacher has failed for the following properties:
+ // - 'name' with value 'Eve':
+ // - Teacher's name 'Eve' is not supported. Teacher's name must be between 5 and 10 characters (e.g. Joanna, Angeline).
+}
+```
+
+## Add template helper functions
+
+Template helpers are registered through AddTemplateFunctions. Use the built-ins for common formatting instead of formatting slices or multiline text by hand inside every template.
+
+
+
+**Register the builtin helper functions on a template.**
+
+[//]: # (embed: ExampleAddTemplateFunctions)
+
+```go
+// Under the hood builtin rules' message templates utilize a set of custom template functions.
+// If you want to use them in your custom templates, you can add them to your [template.Template]
+// instance by calling [govy.AddTemplateFunctions].
+//
+// An example of such function is 'formatExamples' which takes in a slice of strings
+// and returns a formatted string.
+//
+// Note: Builtin functions are automatically added to the parsed [template.Template] if you're using
+// [govy.Rule.WithMessageTemplateString].
+//
+// Note: [govy.AddTemplateFunctions] calls [template.Template.Funcs], which will not add the functions
+// to your template If it was already parsed.
+func ExampleAddTemplateFunctions() {
+ tplString := `Teacher's name '{{ .PropertyValue }}' is not supported {{ formatExamples .Examples }}.`
+ tpl := template.New("")
+ tpl = govy.AddTemplateFunctions(tpl)
+ tpl = template.Must(tpl.Parse(tplString))
+
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Rules(rules.StringLength(5, 10).
+ WithExamples("Joanna", "Angeline").
+ WithMessageTemplate(tpl)),
+ ).WithName("Teacher")
+
+ teacher := Teacher{
+ Name: "Eve",
+ }
+
+ err := v.Validate(teacher)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation for Teacher has failed for the following properties:
+ // - 'name' with value 'Eve':
+ // - Teacher's name 'Eve' is not supported (e.g. 'Joanna', 'Angeline').
+}
+```
+
+
+
+**Format example slices the same way default messages do.**
+
+[//]: # (embed: ExampleAddTemplateFunctions_formatExamples)
+
+```go
+func ExampleAddTemplateFunctions_formatExamples() {
+ tplString := "{{ formatExamples .Examples }}"
+ tpl := template.New("")
+ tpl = govy.AddTemplateFunctions(tpl)
+ tpl = template.Must(tpl.Parse(tplString))
+
+ err := tpl.Execute(
+ os.Stdout,
+ map[string]any{"Examples": []string{"Joanna", "Angeline"}},
+ )
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // (e.g. 'Joanna', 'Angeline')
+}
+```
+
+
+
+**Join slice values in custom template output.**
+
+[//]: # (embed: ExampleAddTemplateFunctions_joinSlice)
+
+```go
+func ExampleAddTemplateFunctions_joinSlice() {
+ tplString := `{{ joinSlice .Slice "'" }}`
+ tpl := template.New("")
+ tpl = govy.AddTemplateFunctions(tpl)
+ tpl = template.Must(tpl.Parse(tplString))
+
+ err := tpl.Execute(
+ os.Stdout,
+ map[string]any{"Slice": []string{"Joanna", "Angeline"}},
+ )
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // 'Joanna', 'Angeline'
+}
+```
+
+
+
+**Indent multiline template output.**
+
+[//]: # (embed: ExampleAddTemplateFunctions_indent)
+
+```go
+func ExampleAddTemplateFunctions_indent() {
+ tplString := "{{ indent 2 .Details }}"
+ tpl := template.New("")
+ tpl = govy.AddTemplateFunctions(tpl)
+ tpl = template.Must(tpl.Parse(tplString))
+
+ err := tpl.Execute(
+ os.Stdout,
+ map[string]any{"Details": "foo\nbar"},
+ )
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // foo
+ // bar
+}
+```
+
+## Describe rules for generated plans
+
+Use descriptions when validation plans are part of the API contract. Descriptions are plan metadata; they do not change runtime validation behavior.
+
+
+
+**Attach a plan-only rule description.**
+
+[//]: # (embed: ExampleRule_WithDescription)
+
+```go
+// [govy.Rule] error might be static, i.e. a single [govy.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], if not
+// impossible, since the exact error might only be known during runtime.
+//
+// To solve this problem, you can use [govy.Rule.WithDescription] to provide a
+// verbose and informative rule description.
+// It will be only included in the [govy.RulePlan] and otherwise not displayed in
+// the default [govy.RuleError.Error].
+// However, it is available in the structured [govy.RuleError].
+func ExampleRule_WithDescription() {
+ 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 :)
+}
+```
+
+## Reuse rule sets
+
+Use rule sets to package related rules and reuse them across properties. Convert rules or rule sets to pointer variants when validating pointed-to values.
+
+
+
+**Convert a rule so it can validate pointer values.**
+
+[//]: # (embed: ExampleRuleToPointer)
+
+```go
+// The builtin rules, and most likely your custom rules as well, all operate on non-pointer values.
+// This means you cannot use them on pointers to the same type.
+//
+// If for whatever reason you don't want to use [govy.ForPointer] constructor,
+// you can use [govy.RuleToPointer] constructor and convert any [govy.Rule] to work on pointers.
+//
+// Note: [govy.RuleToPointer] will skip validation for nil pointers.
+// If you want to enforce the value to be non-nil, you can use [rules.Required].
+// This behavior is consistent with [govy.ForPointer] constructor, which will skip the validation
+// unless you add [govy.PropertyRules.Required] to enforce the value to be a non-nil pointer.
+func ExampleRuleToPointer() {
+ type Pointer struct {
+ Pointer *string `json:"pointer"`
+ }
+ validator := govy.New(
+ govy.For(func(p Pointer) *string { return p.Pointer }).
+ WithName("pointer").
+ Rules(govy.RuleToPointer(rules.EQ("foo"))),
+ )
+
+ pointer := Pointer{Pointer: ptr("bar")}
+
+ err := validator.Validate(pointer)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation has failed for the following properties:
+ // - 'pointer' with value 'bar':
+ // - must be equal to 'foo'
+}
+```
+
+
+
+**Group multiple rules into a reusable set.**
+
+[//]: # (embed: ExampleRuleSet)
+
+```go
+// Sometimes it's useful to aggregate multiple [govy.Rule] into a single, composite rule.
+// To do that we'll use [govy.RuleSet] and [govy.NewRuleSet] constructor.
+// RuleSet is a simple container for multiple [govy.Rule].
+// During validation it is unpacked and each [govy.RuleError] is reported separately.
+//
+// Note that govy uses similar syntax to wrapped errors in Go;
+// a ':' delimiter is used to chain error codes together.
+func ExampleRuleSet() {
+ 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 :)
+}
+```
+
+
+
+**Convert a whole rule set for pointer validation.**
+
+[//]: # (embed: ExampleRuleSetToPointer)
+
+```go
+// Similar to [govy.RuleToPointer], you can use [govy.RuleSetToPointer] to convert
+// [govy.RuleSet] to work with pointers.
+//
+// See [ExampleRuleToPointer] for more details.
+func ExampleRuleSetToPointer() {
+ type Pointer struct {
+ Pointer *string `json:"pointer"`
+ }
+ ruleSet := govy.NewRuleSet(
+ rules.StringStartsWith("f"),
+ rules.StringEndsWith("o"),
+ )
+ validator := govy.New(
+ govy.For(func(p Pointer) *string { return p.Pointer }).
+ WithName("pointer").
+ Rules(govy.RuleSetToPointer(ruleSet)),
+ )
+
+ pointer := Pointer{Pointer: ptr("bar")}
+
+ err := validator.Validate(pointer)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation has failed for the following properties:
+ // - 'pointer' with value 'bar':
+ // - string must start with 'f' prefix
+ // - string must end with 'o' suffix
+}
+```
+
+
+
+**Stop evaluating later rules in a rule set after failure.**
+
+[//]: # (embed: ExampleRuleSet_Cascade)
+
+```go
+// If you wish to control how rules aggregated by [govy.RuleSet] evaluate
+// you can use [govy.RuleSet.Cascade] to set a [govy.CascadeMode].
+//
+// Similar to how the cascade mode works when evaluating [govy.PropertyRules],
+// the [govy.CascadeModeStop] will stop validation after the first encountered error.
+//
+// In the example below we can see that although both rules should fail,
+// only the first one (order of definitions matters here!) returns an error.
+func ExampleRuleSet_Cascade() {
+ teacherNameRule := govy.NewRuleSet(
+ rules.StringLength(1, 5),
+ rules.StringMatchRegexp(regexp.MustCompile("^(Tom|Jerry)$")),
+ ).
+ Cascade(govy.CascadeModeStop)
+
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Rules(teacherNameRule),
+ ).WithName("Teacher")
+
+ teacher := Teacher{
+ Name: "Jonathan",
+ }
+
+ err := v.Validate(teacher)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Output:
+ // Validation for Teacher has failed for the following properties:
+ // - 'name' with value 'Jonathan':
+ // - length must be between 1 and 5
+}
+```
diff --git a/.agents/skills/govy/references/testing.md b/.agents/skills/govy/references/testing.md
new file mode 100644
index 00000000..799209a7
--- /dev/null
+++ b/.agents/skills/govy/references/testing.md
@@ -0,0 +1,134 @@
+# Testing
+
+Core govy error helpers and structured error flows useful when writing tests without the govytest package.
+
+## Topics
+
+- [Inspect validation errors](#inspect-validation-errors)
+ - [Check whether any nested rule error carries an error code.](#check-whether-any-nested-rule-error-carries-an-error-code)
+ - [Match validator errors returned from slice validation.](#match-validator-errors-returned-from-slice-validation)
+
+## Inspect validation errors
+
+Use these helpers when tests need to match structured govy errors without depending on entire formatted error strings.
+
+
+
+**Check whether any nested rule error carries an error code.**
+
+[//]: # (embed: ExampleHasErrorCode)
+
+```go
+// To inspect if an error contains a given [govy.ErrorCode], use [govy.HasErrorCode] function.
+// This function will also return true if the expected [govy.ErrorCode]
+// is part of a chain of wrapped error codes.
+// In this example we're dealing with two error code chains:
+// - 'teacher_name:string_length'
+// - 'teacher_name:string_match_regexp'
+func ExampleHasErrorCode() {
+ teacherNameRule := 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
+}
+```
+
+
+
+**Match validator errors returned from slice validation.**
+
+[//]: # (embed: ExampleValidatorErrors)
+
+```go
+// [govy.Validator.ValidateSlice] outputs [govy.ValidatorErrors] which is a slice of [govy.ValidatorError].
+// Each [govy.ValidatorError] has an additional property set: SliceIndex, which is a 0-based slice element index.
+func ExampleValidatorErrors() {
+ v := govy.New(
+ govy.For(func(t Teacher) string { return t.Name }).
+ WithName("name").
+ Rules(govy.NewRule(func(name string) error {
+ if name == "John" || name == "Jake" {
+ return fmt.Errorf("fails for John and Jake")
+ }
+ return nil
+ })),
+ ).WithName("Teacher")
+
+ err := v.ValidateSlice([]Teacher{
+ {Name: "John"},
+ {Name: "George"},
+ {Name: "Jake"},
+ })
+ if err != nil {
+ enc := json.NewEncoder(os.Stdout)
+ enc.SetIndent("", " ")
+ if err = enc.Encode(err); err != nil {
+ fmt.Printf("error encoding: %v\n", err)
+ }
+ }
+
+ // Output:
+ // [
+ // {
+ // "errors": [
+ // {
+ // "propertyPath": "name",
+ // "propertyValue": "John",
+ // "errors": [
+ // {
+ // "error": "fails for John and Jake"
+ // }
+ // ]
+ // }
+ // ],
+ // "name": "Teacher",
+ // "sliceIndex": 0
+ // },
+ // {
+ // "errors": [
+ // {
+ // "propertyPath": "name",
+ // "propertyValue": "Jake",
+ // "errors": [
+ // {
+ // "error": "fails for John and Jake"
+ // }
+ // ]
+ // }
+ // ],
+ // "name": "Teacher",
+ // "sliceIndex": 2
+ // }
+ // ]
+}
+```
diff --git a/.agents/skills/govy/references/validation-plan.md b/.agents/skills/govy/references/validation-plan.md
new file mode 100644
index 00000000..07c8643b
--- /dev/null
+++ b/.agents/skills/govy/references/validation-plan.md
@@ -0,0 +1,132 @@
+# Validation Plan
+
+Validation plan generation and strict validation of plan metadata.
+
+## Topics
+
+- [Generate and enforce validation plans](#generate-and-enforce-validation-plans)
+ - [Generate a plan from a validator.](#generate-a-plan-from-a-validator)
+ - [Validate plan metadata with strict plan options.](#validate-plan-metadata-with-strict-plan-options)
+
+## Generate and enforce validation plans
+
+Use validation plans when validation rules also need to describe an API contract. Add examples, descriptions, and predicate descriptions before generating the plan.
+
+
+
+**Generate a plan from a validator.**
+
+[//]: # (embed: ExamplePlan)
+
+```go
+// 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] function!
+//
+// There are multiple ways to improve the generated documentation:
+// - Use [govy.PropertyRules.WithExamples] to provide a list of example values for the property.
+// - Use [govy.Rule.WithDescription] to provide a plan-only description for your rule.
+// For builtin rules, the description is already provided.
+// - Use [govy.WhenDescription] to provide a plan-only description for your when conditions.
+func ExamplePlan() {
+ 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, err := govy.Plan(v)
+ if err != nil {
+ panic(err)
+ }
+ enc := json.NewEncoder(os.Stdout)
+ enc.SetIndent("", " ")
+ _ = enc.Encode(properties)
+
+ // Output:
+ // {
+ // "name": "Teacher",
+ // "properties": [
+ // {
+ // "path": "$.name",
+ // "typeInfo": {
+ // "name": "string",
+ // "kind": "string"
+ // },
+ // "examples": [
+ // "Jake",
+ // "John"
+ // ],
+ // "rules": [
+ // {
+ // "description": "must not be 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"
+ // ]
+ // }
+ // ]
+ // }
+ // ]
+ // }
+}
+```
+
+
+
+**Validate plan metadata with strict plan options.**
+
+[//]: # (embed: ExamplePlan_validation)
+
+```go
+// You can enforce certain rules upon [govy.Plan].
+// For instance, If you'd want to make sure every [govy.Predicate]
+// has a description attached to it, provide [govy.Plan] with [govy.PlanRequirePredicateDescription] option.
+//
+// If you want to follow our best recommendations, use [govy.PlanStrictMode].
+func ExamplePlan_validation() {
+ 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" }).
+ 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!"),
+ ),
+ ).
+ When(func(t Teacher) bool { return t.Age > 18 }).
+ WithName("Teacher")
+
+ _, err := govy.Plan(v, govy.PlanStrictMode())
+ fmt.Println(err)
+
+ // Output:
+ // predicates without description found at: validator level, $.name
+}
+```
diff --git a/Makefile b/Makefile
index 74a44473..e138b935 100644
--- a/Makefile
+++ b/Makefile
@@ -102,10 +102,10 @@ generate/code:
$(call _print_step,Generating Go code)
go generate ./...
-## Generate README.md file embedded examples.
+## Generate Markdown embedded examples.
generate/readme:
- $(call _print_step,Generating README.md embedded examples)
- $(SCRIPTS_DIR)/embed-example-in-readme.bash README.md
+ $(call _print_step,Generating Markdown embedded examples)
+ $(SCRIPTS_DIR)/embed-examples-in-markdown.bash README.md .agents/skills/govy
.PHONY: format format/go
## Format files.
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 7c1d10ab..6fe26742 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -61,8 +61,8 @@ shortcomings.
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).
+- [embed-examples-in-markdown.bash](../scripts/embed-examples-in-markdown.bash)
+ for embedding tested examples in Markdown documents.
## Validation
diff --git a/go.work.sum b/go.work.sum
index c0b9c192..9c7bbf8a 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -49,6 +49,7 @@ golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJ
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0=
golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548=
+golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
@@ -61,6 +62,7 @@ golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
+golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
diff --git a/internal/cmd/docextractor/main.go b/internal/cmd/docextractor/main.go
index 7c5d66fe..c6c2b2eb 100644
--- a/internal/cmd/docextractor/main.go
+++ b/internal/cmd/docextractor/main.go
@@ -6,10 +6,13 @@ import (
"go/ast"
"go/format"
"go/parser"
+ "go/printer"
"go/token"
+ "io/fs"
"log/slog"
"os"
"path/filepath"
+ "slices"
"strconv"
"strings"
@@ -29,6 +32,16 @@ func main() {
fmt.Println("Running docextractor...")
root := internal.FindModuleRoot()
+ if len(os.Args) > 1 {
+ switch os.Args[1] {
+ case "embed":
+ embedExamples(root, os.Args[2:])
+ return
+ default:
+ logFatal(nil, "Unknown docextractor command %q", os.Args[1])
+ }
+ }
+
docs := findTemplateFunctionsDocs(root)
path := filepath.Join(root, "pkg", "govy", "message_templates.go")
@@ -185,10 +198,427 @@ func findTemplateFunctionsDocs(root string) [][]string {
return docsList
}
+func embedExamples(root string, paths []string) {
+ if len(paths) == 0 {
+ logFatal(nil, "No Markdown files provided for example embedding")
+ }
+
+ for _, path := range paths {
+ info, err := os.Stat(path) // #nosec G703
+ if err != nil {
+ logFatal(err, "Failed to stat path %q", path)
+ }
+ if !info.IsDir() {
+ embedExamplesInMarkdown(root, path)
+ continue
+ }
+ if err = filepath.WalkDir(path, func(path string, entry fs.DirEntry, err error) error { // #nosec G703
+ if err != nil {
+ return err
+ }
+ if entry.IsDir() || filepath.Ext(path) != ".md" {
+ return nil
+ }
+ embedExamplesInMarkdown(root, path)
+ return nil
+ }); err != nil {
+ logFatal(err, "Failed to walk Markdown directory %q", path)
+ }
+ }
+}
+
+func embedExamplesInMarkdown(root, path string) {
+ contents, err := os.ReadFile(path) // #nosec G304,G703
+ if err != nil {
+ logFatal(err, "Failed to read Markdown file %q", path)
+ }
+
+ updated := replaceEmbeddedExamples(root, string(contents), path)
+ updated = replaceGeneratedDocs(root, updated, path)
+ if updated == string(contents) {
+ return
+ }
+ if err = os.WriteFile(path, []byte(updated), 0o600); err != nil { // #nosec G703
+ logFatal(err, "Failed to write Markdown file %q", path)
+ }
+}
+
+func replaceGeneratedDocs(root, markdown, markdownPath string) string {
+ const (
+ docsPrefix = "[//]: # (docs: "
+ docsSuffix = ")"
+ docsEnd = "[//]: # (end-docs)"
+ )
+
+ var builder strings.Builder
+ cursor := 0
+ for {
+ directiveStart := strings.Index(markdown[cursor:], docsPrefix)
+ if directiveStart < 0 {
+ builder.WriteString(markdown[cursor:])
+ return builder.String()
+ }
+ directiveStart += cursor
+ lineEnd := strings.IndexByte(markdown[directiveStart:], '\n')
+ if lineEnd < 0 {
+ logFatal(nil, "Docs directive in %q is not followed by an end directive", markdownPath)
+ }
+ lineEnd += directiveStart
+ directive := strings.TrimSpace(markdown[directiveStart:lineEnd])
+ if !strings.HasPrefix(directive, docsPrefix) || !strings.HasSuffix(directive, docsSuffix) {
+ logFatal(nil, "Malformed docs directive %q in %q", directive, markdownPath)
+ }
+ docsRef := strings.TrimSuffix(strings.TrimPrefix(directive, docsPrefix), docsSuffix)
+ docs := renderGeneratedDocs(root, docsRef)
+
+ endStart := strings.Index(markdown[lineEnd:], docsEnd)
+ if endStart < 0 {
+ logFatal(nil, "Docs directive %q in %q is missing %q", docsRef, markdownPath, docsEnd)
+ }
+ endStart += lineEnd
+ endLineEnd := strings.IndexByte(markdown[endStart:], '\n')
+ if endLineEnd < 0 {
+ endLineEnd = len(markdown)
+ } else {
+ endLineEnd += endStart
+ }
+
+ builder.WriteString(markdown[cursor : lineEnd+1])
+ builder.WriteByte('\n')
+ builder.WriteString(strings.TrimRight(docs, "\n"))
+ builder.WriteString("\n\n")
+ builder.WriteString(markdown[endStart:endLineEnd])
+ cursor = endLineEnd
+ }
+}
+
+type generatedDocsConfig struct {
+ path string
+ kind string
+ returns []string
+}
+
+type documentedFunction struct {
+ name string
+ description string
+}
+
+func renderGeneratedDocs(root, docsRef string) string {
+ config := parseGeneratedDocsConfig(docsRef)
+ if config.kind != "func" {
+ logFatal(nil, "Unsupported docs kind %q", config.kind)
+ }
+
+ path := filepath.Join(root, filepath.FromSlash(config.path))
+ var files []string
+ info, err := os.Stat(path) // #nosec G703
+ if err != nil {
+ logFatal(err, "Failed to stat generated docs path %q", path)
+ }
+ if info.IsDir() {
+ if err = filepath.WalkDir(path, func(path string, entry fs.DirEntry, err error) error { // #nosec G703
+ if err != nil {
+ return err
+ }
+ if entry.IsDir() || filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
+ return nil
+ }
+ files = append(files, path)
+ return nil
+ }); err != nil {
+ logFatal(err, "Failed to walk generated docs path %q", path)
+ }
+ } else {
+ files = append(files, path)
+ }
+ slices.Sort(files)
+
+ functions := make([]documentedFunction, 0, len(files))
+ for _, file := range files {
+ functions = append(functions, readDocumentedFunctions(file, config)...)
+ }
+ slices.SortFunc(functions, func(a, b documentedFunction) int {
+ return strings.Compare(a.name, b.name)
+ })
+
+ var builder strings.Builder
+ for _, function := range functions {
+ builder.WriteString("- `")
+ builder.WriteString(function.name)
+ builder.WriteString("` - ")
+ builder.WriteString(function.description)
+ builder.WriteByte('\n')
+ }
+ return builder.String()
+}
+
+func parseGeneratedDocsConfig(docsRef string) generatedDocsConfig {
+ path, query, _ := strings.Cut(docsRef, "?")
+ config := generatedDocsConfig{path: path, kind: "func"}
+ for part := range strings.SplitSeq(query, "&") {
+ key, value, ok := strings.Cut(part, "=")
+ if !ok {
+ continue
+ }
+ switch key {
+ case "kind":
+ config.kind = value
+ case "returns":
+ config.returns = strings.Split(value, ",")
+ default:
+ logFatal(nil, "Unsupported docs query parameter %q", key)
+ }
+ }
+ return config
+}
+
+func readDocumentedFunctions(path string, config generatedDocsConfig) []documentedFunction {
+ fset := token.NewFileSet()
+ astFile, err := parser.ParseFile(fset, path, nil, parser.ParseComments|parser.SkipObjectResolution)
+ if err != nil {
+ logFatal(err, "Failed to parse generated docs source %q", path)
+ }
+
+ var functions []documentedFunction
+ for _, decl := range astFile.Decls {
+ funcDecl, ok := decl.(*ast.FuncDecl)
+ if !ok || !funcDecl.Name.IsExported() || !matchesReturnFilter(fset, funcDecl, config.returns) {
+ continue
+ }
+ if funcDecl.Doc == nil {
+ logFatal(nil, "Exported function %q in %q is missing documentation", funcDecl.Name.Name, path)
+ }
+ functions = append(functions, documentedFunction{
+ name: funcDecl.Name.Name,
+ description: firstDocSentence(funcDecl.Doc),
+ })
+ }
+ return functions
+}
+
+func firstDocSentence(doc *ast.CommentGroup) string {
+ lines := make([]string, 0, len(doc.List))
+ for _, comment := range doc.List {
+ line := strings.TrimSpace(strings.TrimPrefix(comment.Text, "//"))
+ if line == "" {
+ break
+ }
+ lines = append(lines, line)
+ }
+ return firstSentence(strings.Join(lines, " "))
+}
+
+func firstSentence(text string) string {
+ bracketDepth := 0
+ for i, r := range text {
+ switch r {
+ case '[':
+ bracketDepth++
+ case ']':
+ if bracketDepth > 0 {
+ bracketDepth--
+ }
+ case '.':
+ if bracketDepth > 0 || isKnownAbbreviation(text[:i+1]) {
+ continue
+ }
+ if i == len(text)-1 || text[i+1] == ' ' || text[i+1] == '\n' || text[i+1] == '\t' {
+ return strings.TrimSpace(text[:i+1])
+ }
+ }
+ }
+ return strings.TrimSpace(text)
+}
+
+func isKnownAbbreviation(text string) bool {
+ lower := strings.ToLower(text)
+ return strings.HasSuffix(lower, "i.e.") || strings.HasSuffix(lower, "e.g.")
+}
+
+func matchesReturnFilter(fset *token.FileSet, funcDecl *ast.FuncDecl, returns []string) bool {
+ if len(returns) == 0 {
+ return true
+ }
+ if funcDecl.Type.Results == nil {
+ return false
+ }
+ for _, result := range funcDecl.Type.Results.List {
+ var builder strings.Builder
+ if err := printer.Fprint(&builder, fset, result.Type); err != nil {
+ logFatal(err, "Failed to print return type for function %q", funcDecl.Name.Name)
+ }
+ returnType := builder.String()
+ for _, expected := range returns {
+ if strings.HasPrefix(returnType, expected+"[") || returnType == expected {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func replaceEmbeddedExamples(root, markdown, markdownPath string) string {
+ const (
+ embedPrefix = "[//]: # (embed: "
+ embedSuffix = ")"
+ goFence = "```go\n"
+ )
+
+ var builder strings.Builder
+ cursor := 0
+ for {
+ directiveStart := strings.Index(markdown[cursor:], embedPrefix)
+ if directiveStart < 0 {
+ builder.WriteString(markdown[cursor:])
+ return builder.String()
+ }
+ directiveStart += cursor
+ lineEnd := strings.IndexByte(markdown[directiveStart:], '\n')
+ if lineEnd < 0 {
+ logFatal(nil, "Embed directive in %q is not followed by a code block", markdownPath)
+ }
+ lineEnd += directiveStart
+ directive := strings.TrimSpace(markdown[directiveStart:lineEnd])
+ if !strings.HasPrefix(directive, embedPrefix) || !strings.HasSuffix(directive, embedSuffix) {
+ logFatal(nil, "Malformed embed directive %q in %q", directive, markdownPath)
+ }
+ exampleRef := strings.TrimSuffix(strings.TrimPrefix(directive, embedPrefix), embedSuffix)
+ example := readEmbeddedExample(root, exampleRef)
+
+ fenceStart := strings.Index(markdown[lineEnd:], goFence)
+ if fenceStart < 0 {
+ logFatal(nil, "Embed directive %q in %q is missing a Go code block", exampleRef, markdownPath)
+ }
+ fenceStart += lineEnd
+ codeStart := fenceStart + len(goFence)
+ fenceEnd := findCodeFenceEnd(markdown[codeStart:])
+ if fenceEnd < 0 {
+ logFatal(nil, "Embed directive %q in %q has an unterminated Go code block", exampleRef, markdownPath)
+ }
+ fenceEnd += codeStart
+
+ builder.WriteString(markdown[cursor:codeStart])
+ builder.WriteString(strings.TrimRight(example, "\n"))
+ if markdown[fenceEnd] != '\n' {
+ builder.WriteByte('\n')
+ }
+ cursor = fenceEnd
+ }
+}
+
+func findCodeFenceEnd(markdown string) int {
+ if strings.HasPrefix(markdown, "```") {
+ return 0
+ }
+ if fenceEnd := strings.Index(markdown, "\n```"); fenceEnd >= 0 {
+ return fenceEnd
+ }
+ return strings.Index(markdown, "```")
+}
+
+func readEmbeddedExample(root, exampleRef string) string {
+ sourcePath, functionName, hasFunctionName := strings.Cut(exampleRef, "#")
+ if hasFunctionName {
+ return readEmbeddedFunction(root, sourcePath, functionName)
+ }
+ if strings.HasPrefix(exampleRef, "Example") {
+ return readEmbeddedFunctionByName(root, exampleRef)
+ }
+
+ contents, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(sourcePath))) // #nosec G304,G703
+ if err != nil {
+ logFatal(err, "Failed to read embedded example %q", exampleRef)
+ }
+ return string(contents)
+}
+
+func readEmbeddedFunctionByName(root, functionName string) string {
+ var matches []string
+ if err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { // #nosec G703
+ if err != nil {
+ return err
+ }
+ if entry.IsDir() || filepath.Base(path) != "example_test.go" {
+ return nil
+ }
+ if hasEmbeddedFunction(path, functionName) {
+ matches = append(matches, path)
+ }
+ return nil
+ }); err != nil {
+ logFatal(err, "Failed to find embedded example %q", functionName)
+ }
+
+ switch len(matches) {
+ case 0:
+ logFatal(nil, "Function %q was not found in example_test.go files", functionName)
+ case 1:
+ relPath, err := filepath.Rel(root, matches[0])
+ if err != nil {
+ logFatal(err, "Failed to resolve embedded example source %q", matches[0])
+ }
+ return readEmbeddedFunction(root, filepath.ToSlash(relPath), functionName)
+ default:
+ logFatal(
+ nil,
+ "Function %q is ambiguous across example_test.go files: %s",
+ functionName,
+ strings.Join(matches, ", "),
+ )
+ }
+ return ""
+}
+
+func hasEmbeddedFunction(path, functionName string) bool {
+ fset := token.NewFileSet()
+ astFile, err := parser.ParseFile(fset, path, nil, parser.SkipObjectResolution)
+ if err != nil {
+ logFatal(err, "Failed to parse embedded example source %q", path)
+ }
+ for _, decl := range astFile.Decls {
+ funcDecl, ok := decl.(*ast.FuncDecl)
+ if ok && funcDecl.Name.Name == functionName {
+ return true
+ }
+ }
+ return false
+}
+
+func readEmbeddedFunction(root, sourcePath, functionName string) string {
+ path := filepath.Join(root, filepath.FromSlash(sourcePath))
+ source, err := os.ReadFile(path) // #nosec G304,G703
+ if err != nil {
+ logFatal(err, "Failed to read embedded example source %q", path)
+ }
+
+ fset := token.NewFileSet()
+ astFile, err := parser.ParseFile(fset, path, source, parser.ParseComments|parser.SkipObjectResolution)
+ if err != nil {
+ logFatal(err, "Failed to parse embedded example source %q", path)
+ }
+
+ for _, decl := range astFile.Decls {
+ funcDecl, ok := decl.(*ast.FuncDecl)
+ if !ok || funcDecl.Name.Name != functionName {
+ continue
+ }
+ startPos := funcDecl.Pos()
+ if funcDecl.Doc != nil {
+ startPos = funcDecl.Doc.Pos()
+ }
+ start := fset.PositionFor(startPos, false)
+ end := fset.PositionFor(funcDecl.End(), false)
+ return strings.TrimSpace(string(source[start.Offset:end.Offset]))
+ }
+ logFatal(nil, "Function %q was not found in embedded example source %q", functionName, sourcePath)
+ return ""
+}
+
func logFatal(err error, msg string, a ...any) {
var attrs []slog.Attr
if err != nil {
attrs = append(attrs, slog.String("error", err.Error()))
}
logging.Logger().LogAttrs(context.Background(), slog.LevelError, fmt.Sprintf(msg, a...), attrs...)
+ os.Exit(1)
}
diff --git a/pkg/govy/example_test.go b/pkg/govy/example_test.go
index 3ead0400..580a1b61 100644
--- a/pkg/govy/example_test.go
+++ b/pkg/govy/example_test.go
@@ -446,7 +446,7 @@ func ExampleTransform() {
// the property does not contain a zero value.
//
// Note: [govy.PropertyRules.Required] is introducing a short circuit.
-// If the assertion fails, validation will stop and return [govy.govy.ErrorCodeRequired].
+// If the assertion fails, validation will stop and return [rules.ErrorCodeRequired].
// None of the rules you've defined would be evaluated.
//
// Note: Placement of [govy.PropertyRules.Required] does not matter,
@@ -1400,7 +1400,7 @@ func ExampleForSlice_sliceOfPointers() {
//
// 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]..
+// [govy.PropertyRulesForSlice].
//
// To define rules for keys use:
// - [govy.PropertyRulesForMap.RulesForKeys]
@@ -1424,7 +1424,7 @@ func ExampleForSlice_sliceOfPointers() {
// - Joan cannot be a teacher for student with index 918230013 (items).
//
// Notice that property path for maps has the following format:
-// ..
+// [].
func ExampleForMap() {
teacherValidator := govy.New(
govy.For(func(t Teacher) string { return t.Name }).
diff --git a/pkg/rules/comparable.go b/pkg/rules/comparable.go
index 191a1c6e..db583a25 100644
--- a/pkg/rules/comparable.go
+++ b/pkg/rules/comparable.go
@@ -339,6 +339,8 @@ func isTemporal(v any) bool {
}
}
+// GTComparableProperties ensures the first property's value is greater than the second property's value.
+// It works with types that implement [Comparable], such as [time.Time].
func GTComparableProperties[T Comparable[T], P any](
firstName string,
firstGetter func(parent P) T,
@@ -366,6 +368,8 @@ func GTComparableProperties[T Comparable[T], P any](
WithDescription(fmt.Sprintf("'%s' must be greater than '%s'", firstName, secondName))
}
+// GTEComparableProperties ensures the first property's value is greater than or equal to the second property's value.
+// It works with types that implement [Comparable], such as [time.Time].
func GTEComparableProperties[T Comparable[T], P any](
firstName string,
firstGetter func(parent P) T,
@@ -393,6 +397,8 @@ func GTEComparableProperties[T Comparable[T], P any](
WithDescription(fmt.Sprintf("'%s' must be greater than or equal to '%s'", firstName, secondName))
}
+// LTComparableProperties ensures the first property's value is less than the second property's value.
+// It works with types that implement [Comparable], such as [time.Time].
func LTComparableProperties[T Comparable[T], P any](
firstName string,
firstGetter func(parent P) T,
@@ -420,6 +426,8 @@ func LTComparableProperties[T Comparable[T], P any](
WithDescription(fmt.Sprintf("'%s' must be less than '%s'", firstName, secondName))
}
+// LTEComparableProperties ensures the first property's value is less than or equal to the second property's value.
+// It works with types that implement [Comparable], such as [time.Time].
func LTEComparableProperties[T Comparable[T], P any](
firstName string,
firstGetter func(parent P) T,
diff --git a/scripts/embed-example-in-readme.bash b/scripts/embed-example-in-readme.bash
deleted file mode 100755
index 293f2700..00000000
--- a/scripts/embed-example-in-readme.bash
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-if [ "$#" -ne 1 ]; then
- echo "Usage: $0 "
- exit 1
-fi
-
-README_PATH="$1"
-
-# sed 's/\\/\\\\/g' is used to add extra backslashes which are eaten up by awk.
-for file_embed in $(awk '/^\[\/\/\]: # \(embed: .*\)$/ {sub(/\)/, "", $4); print $4}' "$README_PATH"); do
- echo "Found embed directive for: $file_embed" >&2
- awk \
- -v file_embed="$file_embed" \
- -v file_embed_contents="$(sed 's/\\/\\\\/g' "$file_embed")" \
- 'q==1 && /^```go$/ {q=2};
- q<2 {print}
- $0 ~ file_embed {q=1}
- q==2 && !/go/ && /^```$/ {q=0; printf "```go\n%s\n```\n", file_embed_contents};' \
- "$README_PATH" >"$README_PATH.tmp"
- mv "$README_PATH.tmp" "$README_PATH"
-done
diff --git a/scripts/embed-examples-in-markdown.bash b/scripts/embed-examples-in-markdown.bash
new file mode 100755
index 00000000..aac4b0ea
--- /dev/null
+++ b/scripts/embed-examples-in-markdown.bash
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -e
+
+if [ "$#" -lt 1 ]; then
+ echo "Usage: $0 ..."
+ exit 1
+fi
+
+go run ./internal/cmd/docextractor embed "$@"