Types and conversions¶
The configurator knows nothing about the types of the values. It has two methods for the fields:
b.Field(&m.X)returns*shared.Field[T], the base configurator of a field of typeT;b.Slice(&m.X)returns*shared.SliceFieldConfigurator[E]of a[]Efield, with the rules of the slice itself:Required,MinLen,MaxLen,Custom,WhenandEach;b.Map(&m.X)returns*shared.MapFieldConfigurator[K, V]of amap[K]Vfield, with the rules of the map itself,EachKeyandEachValue, see Maps.
As converts a base configurator to the configurator of its type. A conversion is a function with a type constraint, so a conversion that doesn't fit the field doesn't compile:
b.Field(&u.Name).As(str.Of).Required() // string
b.Field(&u.Discount).As(num.Ptr).Max(50) // *int
b.Slice(&u.Tags).MaxLen(5).As(str.Slice).Email()
b.Field(&u.Age).As(str.Of)
// int does not satisfy ~string | *string (int missing in ~string | *string)
Built-in conversions¶
| Conversion | Fields | Configurator |
|---|---|---|
str.Of |
string, *string, named strings |
strings |
num.Of |
numbers and named numbers | numbers |
num.Slice |
Slice of numbers |
slices with the number rules of elements |
num.Ptr |
pointers to numbers | numbers, Required checks only that the pointer isn't nil |
timex.Of |
time.Time, *time.Time |
time |
eq.Of, eq.Ptr |
any comparable type and pointers to it: enums, bool |
comparable values |
vuuid.Of |
uuid.UUID, *uuid.UUID of github.com/google/uuid |
uuid, the separate module github.com/insei/valigo/rules/uuid |
str.Slice |
Slice of strings, *string or named strings |
slices with the string rules of elements |
timex.Slice |
Slice of time.Time, *time.Time |
slices with the time rules of elements |
eq.Slice |
Slice of a comparable type |
slices with Unique and the equality rules of elements |
vuuid.Slice |
Slice of uuid.UUID, *uuid.UUID |
slices with the uuid rules of elements |
shared.SlicePtr |
Field of *[]E |
the slice configurator, a nil pointer is absent |
The elements of a slice are converted in Each: b.Slice(&m.Sizes).Each(func(e *shared.Field[int]) { e.As(num.Of).Min(1) }).
The base rules¶
shared.Field[T] has the primitives all configurators are built of. They are enough for a one-off rule without a conversion:
| Rule | Behavior |
|---|---|
Check(ok, key, args...) |
an error with the message key when ok(value) is false, absent values are skipped |
Required(ok, key) |
an error when the value is absent or ok(value) is false |
Modify(fn) |
changes the value in place, e.g. trims it |
Custom(fn) |
your function with the typed value, see Custom rules |
When(cond) |
the next rules are applied only if cond(value) is true, see Conditions |
Your types¶
A conversion of your type is a function that takes the base configurator and returns your configurator over shared.Field of the type. Use the conversions of shared for pointers and named types:
shared.Derefconverts the rules of*Tto the rules ofT, a nil pointer is absent;shared.Retypeconverts the rules ofTto the rules ofUwith the same memory layout, e.g. of a named type;shared.RetypeSlicedoes the same for a slice configurator, also for the elements stored by pointer;shared.SliceRulesgivesRequired,MinLen,MaxLen,CustomandWhenof the slice to your slice configurator, see the source ofstr.Slice;CheckMsgandRequiredMsgtake a message ofshared.Mwith a stable code and named parameters:c.f.CheckMsg(ok, shared.M("time.after", key, shared.P("time", t))).
A configurator of time.Time (the built-in one is timex, this one shows how it's made):
package main
import (
"context"
"fmt"
"time"
"github.com/insei/valigo"
"github.com/insei/valigo/shared"
)
// TimeConfigurator configures the rules of a time.Time value.
type TimeConfigurator struct {
f *shared.Field[time.Time]
}
// TimeOf converts a time.Time or *time.Time field: b.Field(&m.At).As(TimeOf).
func TimeOf[T time.Time | *time.Time](f *shared.Field[T]) *TimeConfigurator {
if p, ok := any(f).(*shared.Field[*time.Time]); ok {
return &TimeConfigurator{f: shared.Deref(p)}
}
return &TimeConfigurator{f: shared.Retype[T, time.Time](f)}
}
func (c *TimeConfigurator) Required() *TimeConfigurator {
c.f.Required(func(v time.Time) bool { return !v.IsZero() }, "Should be fulfilled")
return c
}
func (c *TimeConfigurator) After(t time.Time) *TimeConfigurator {
c.f.Check(func(v time.Time) bool { return v.After(t) }, "Should be after %s", t.Format(time.DateOnly))
return c
}
type Event struct {
StartsAt time.Time
EndsAt *time.Time
}
func main() {
start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
v := valigo.New()
valigo.Configure[Event](v, func(b valigo.Configurator[Event], e *Event) {
b.Field(&e.StartsAt).As(TimeOf).Required().After(start)
b.Field(&e.EndsAt).As(TimeOf).After(start) // a nil EndsAt is skipped
})
before := start.AddDate(0, 0, -1)
fmt.Println(v.Validate(context.Background(), &Event{}))
fmt.Println(v.Validate(context.Background(), &Event{StartsAt: start.AddDate(0, 1, 0), EndsAt: &before}))
}
Output:
[Should be fulfilled (StartsAt: 0001-01-01 00:00:00 +0000 UTC) Should be after 2026-01-01 (StartsAt: 0001-01-01 00:00:00 +0000 UTC)]
[Should be after 2026-01-01 (EndsAt: 2025-12-31 00:00:00 +0000 UTC)]
The type constraint of TimeOf makes b.Field(&e.Title).As(TimeOf) a compile error for a string field. The rules of another uuid package are added the same way, see the source of github.com/insei/valigo/rules/uuid.