Documentation
¶
Overview ¶
Package validation provides a decorator that validates entities before mutation operations (Create, Update, Patch) on any r3.CRUD[T, ID] repository.
R3 does not include built-in validation rules. Instead, it provides a Validator interface that users implement using any validation library of their choice -- go-playground/validator, go-ozzo/ozzo-validation, or plain Go code. The decorator intercepts mutations, calls the user's Validator, and short-circuits with a structured Error if validation fails.
Read operations (Get, List) and Delete pass through without validation.
Key features:
- Library-agnostic: bring your own validation (go-playground, ozzo, custom)
- Patch-aware: the validator receives which fields are being patched
- State-transition aware: optionally fetches the existing entity before Update/Patch
- Structured errors: Error carries per-field error details
- Composable helpers: NoValidation, Compose, OperationValidators
Basic Usage ¶
repo := validation.WithValidation[Pet, int64](
innerRepo,
validation.ValidatorFunc[Pet, int64](func(ctx context.Context, req validation.Request[Pet, int64]) error {
if req.Entity.Name == "" {
return validation.NewError(req.Operation,
validation.NewFieldError("name", "is required", "required"),
)
}
return nil
}),
)
With go-playground/validator ¶
type petValidator struct {
validate *validator.Validate
}
func (v *petValidator) Validate(_ context.Context, req validation.Request[Pet, int64]) error {
if err := v.validate.Struct(req.Entity); err != nil {
// Convert go-playground errors to r3 validation.Error
var fieldErrors []validation.FieldError
for _, fe := range err.(validator.ValidationErrors) {
fieldErrors = append(fieldErrors, validation.NewFieldError(
fe.Field(), fe.Tag()+" validation failed", fe.Tag(),
))
}
return validation.NewError(req.Operation, fieldErrors...)
}
return nil
}
With ozzo-validation ¶
type petValidator struct{}
func (v petValidator) Validate(_ context.Context, req validation.Request[Pet, int64]) error {
pet := req.Entity
if err := ozzovalidation.ValidateStruct(&pet,
ozzovalidation.Field(&pet.Name, ozzovalidation.Required, ozzovalidation.Length(1, 100)),
ozzovalidation.Field(&pet.Price, ozzovalidation.Min(0.0)),
); err != nil {
// Convert ozzo errors to r3 validation.Error
var fieldErrors []validation.FieldError
if ozzoErrs, ok := err.(ozzovalidation.Errors); ok {
for field, fieldErr := range ozzoErrs {
fieldErrors = append(fieldErrors, validation.NewFieldError(field, fieldErr.Error(), "invalid"))
}
}
return validation.NewError(req.Operation, fieldErrors...)
}
return nil
}
State-Transition Validation ¶
When WithIDFunc is configured, the decorator fetches the existing entity before Update and Patch operations. This enables state-transition rules like "status can only move from draft to published":
repo := validation.WithValidation[Post, int64](
innerRepo, myValidator,
validation.WithIDFunc[Post, int64](func(p Post) int64 { return p.ID }),
)
The validator then receives req.Existing with the current DB state.
Patch-Aware Validation ¶
For Patch operations, Request.Fields contains the list of fields being updated. Validators can use this to skip rules for unchanged fields:
func (v *myValidator) Validate(ctx context.Context, req validation.Request[Post, int64]) error {
if req.Operation == validation.OpPatch {
// Only validate fields that are being patched
for _, f := range req.Fields {
switch f.Name {
case "name":
if req.Entity.Name == "" {
return validation.NewError(req.Operation,
validation.NewFieldError("name", "is required", "required"),
)
}
}
}
return nil
}
// Full validation for Create/Update ...
}
For whole-entity rules on Patch (e.g. "name must always be non-empty"), do NOT validate req.Entity directly — on a Patch it carries only the patched fields, with the rest zeroed, so the rule would wrongly fire. When WithIDFunc is set the request carries Request.Merged: the patch overlaid on the current DB state. Validate that instead:
if req.Operation == validation.OpPatch && req.Merged != nil {
return v.validate.Struct(*req.Merged)
}
Atomic State-Transition Validation ¶
The decorator fetches the existing entity (or merged state) and then writes; that fetch-validate-write sequence is only atomic when the decorator runs inside a transaction. Wrap the repository with the transactor feature and perform the Update/Patch within InTx so the read and the write share one transaction. Outside a transaction a concurrent writer can change the row between the fetch and the write (a TOCTOU window), so two concurrent updates may both pass a state-transition rule.
Index ¶
- Variables
- type CRUD
- func (v *CRUD[T, ID]) Aggregate(ctx context.Context, qarg ...r3.Query) ([]r3.AggregateRow, error)
- func (v *CRUD[T, ID]) Count(ctx context.Context, qarg ...r3.Query) (int64, error)
- func (v *CRUD[T, ID]) Create(ctx context.Context, entity T) (T, error)
- func (v *CRUD[T, ID]) Delete(ctx context.Context, id ID) error
- func (v *CRUD[T, ID]) Get(ctx context.Context, id ID, qarg ...r3.Query) (T, error)
- func (v *CRUD[T, ID]) Inner() r3.CRUD[T, ID]
- func (v *CRUD[T, ID]) List(ctx context.Context, qarg ...r3.Query) ([]T, int64, error)
- func (v *CRUD[T, ID]) Patch(ctx context.Context, entity T, fields r3.Fields) (T, error)
- func (v *CRUD[T, ID]) Rewrap(inner r3.CRUD[T, ID]) r3.CRUD[T, ID]
- func (v *CRUD[T, ID]) Unwrap() r3.CRUD[T, ID]
- func (v *CRUD[T, ID]) Update(ctx context.Context, entity T) (T, error)
- type Error
- type FieldError
- type IDFunc
- type Operation
- type Option
- type Options
- type Request
- type Validator
- type ValidatorFunc
Constants ¶
This section is empty.
Variables ¶
var ErrValidation = errors.New("r3/validation: validation failed")
ErrValidation is the sentinel error for validation failures. Use errors.Is(err, validation.ErrValidation) to detect validation errors.
Functions ¶
This section is empty.
Types ¶
type CRUD ¶
type CRUD[T any, ID comparable] struct { // contains filtered or unexported fields }
CRUD is a decorator that wraps any r3.CRUD[T, ID] and validates entities before mutation operations (Create, Update, Patch). Read operations (Get, List) and Delete pass through without validation.
The decorator is stateless -- it doesn't define validation rules. It delegates to a user-provided Validator implementation that can use any validation library.
It transparently satisfies the r3.CRUD[T, ID] interface, so it can be used as a drop-in replacement for any CRUD repository.
Usage:
repo := validation.WithValidation[Pet, int64](
innerRepo,
myValidator,
validation.WithIDFunc[Pet, int64](func(p Pet) int64 { return p.ID }),
)
func WithValidation ¶
func WithValidation[T any, ID comparable]( inner r3.CRUD[T, ID], validator Validator[T, ID], optFns ...Option[T, ID], ) *CRUD[T, ID]
WithValidation wraps an existing r3.CRUD with validation. The validator parameter defines the validation logic for mutation operations.
Example:
repo := validation.WithValidation[Pet, int64](
innerRepo,
myValidator,
)
func (*CRUD[T, ID]) Aggregate ¶ added in v0.0.3
Aggregate delegates directly to the inner CRUD's Aggregate (no validation on reads).
func (*CRUD[T, ID]) Inner ¶
Inner returns the underlying CRUD repository (unwrapped). Useful when you need to bypass validation for a specific operation.
func (*CRUD[T, ID]) Patch ¶
Patch optionally fetches the existing entity (if IDFunc is set), validates with the Fields list populated, then delegates to inner.Patch.
When IDFunc is set the request also carries Merged — the patch overlaid on the current state — so whole-entity validators don't see the sparse (zeroed) input.
The fetch-validate-write sequence is only atomic when this decorator runs inside a transaction: wrap the repo with the transactor feature and call Patch within InTx so the Get and the write share one transaction. Outside a transaction the read-before-validate is best-effort (a concurrent writer can change the row between the Get and the Patch — a TOCTOU window).
func (*CRUD[T, ID]) Rewrap ¶
Rewrap rebuilds this decorator around a different inner CRUD (used to re-apply the validation layer on top of a transaction-bound CRUD).
func (*CRUD[T, ID]) Unwrap ¶
Unwrap returns the wrapped CRUD so capability detection and transaction propagation can walk the decorator chain.
func (*CRUD[T, ID]) Update ¶
Update optionally fetches the existing entity (if IDFunc is set), validates, then delegates to inner.Update.
As with Patch, the fetch-validate-write sequence is atomic only when this decorator runs inside a transaction (see the transactor feature); otherwise the read-before-validate has a TOCTOU window against concurrent writers.
type Error ¶
type Error struct {
// Operation is the mutation that was attempted.
Operation Operation
// Errors is the list of per-field validation failures.
Errors []FieldError
}
Error provides structured details about validation failures. It satisfies errors.Is(err, ErrValidation) via its Is method.
func NewError ¶
func NewError(op Operation, errs ...FieldError) *Error
NewError creates a new Error with the given field errors.
type FieldError ¶
type FieldError struct {
// Field is the name of the field that failed validation (e.g. "name", "email").
Field string
// Message is a human-readable description of the failure (e.g. "is required").
Message string
// Code is a machine-readable error code (e.g. "required", "min", "email").
// Useful for i18n or client-side error mapping.
Code string
}
FieldError describes a validation failure for a single field.
func NewFieldError ¶
func NewFieldError(field, message, code string) FieldError
NewFieldError creates a new FieldError.
func (FieldError) Error ¶
func (e FieldError) Error() string
Error returns a human-readable representation of the field error.
type IDFunc ¶
type IDFunc[T any, ID comparable] func(entity T) ID
IDFunc extracts the primary key from an entity. Used by the decorator to fetch existing entities for state-transition validation (Update, Patch).
type Operation ¶
type Operation string
Operation represents a mutation type that triggers validation. Read operations (Get, List) and Delete do not trigger validation.
type Option ¶
type Option[T any, ID comparable] func(*Options[T, ID])
Option is a functional option for configuring the validation CRUD decorator.
func WithIDFunc ¶
func WithIDFunc[T any, ID comparable](fn IDFunc[T, ID]) Option[T, ID]
WithIDFunc sets the function that extracts the primary key from an entity. This enables state-transition validation: the decorator fetches the current DB state before Update and Patch, so the validator can compare old vs new values.
func WithRecordType ¶
func WithRecordType[T any, ID comparable](name string) Option[T, ID]
WithRecordType sets an explicit entity type name for error messages.
type Options ¶
type Options[T any, ID comparable] struct { // IDFunc extracts the primary key from an entity. // When set, the decorator fetches the existing entity before Update and Patch // operations, populating ValidationRequest.Existing for state-transition validation. // Without IDFunc, Existing is always nil. IDFunc IDFunc[T, ID] // RecordType is the entity type name used in error messages. // If empty, it is derived automatically from the struct type T // (e.g. Order -> "orders", CampaignAdset -> "campaign_adsets"). RecordType string }
Options configures the behavior of the validation decorator.
type Request ¶
type Request[T any, ID comparable] struct { // Operation is the mutation type: create, update, or patch. Operation Operation // Entity is the entity being validated. Always populated. Entity T // Fields is the list of fields being patched. Non-nil only for Patch operations. // Validators can use this to skip rules for fields that aren't being changed. Fields r3.Fields // Existing is the current database state of the entity. // Populated only for Update and Patch operations when WithIDFunc is configured. // Enables state-transition validation (e.g. "status can only go draft -> published"). Existing *T // Merged is the full entity as it will look AFTER the patch is applied: the // patched fields overlaid on Existing. Populated only for Patch when WithIDFunc // is configured. Validators should prefer Merged over Entity for whole-entity // rules on Patch, since Entity carries only the patched fields (the rest are // zeroed) — validating Entity directly would reject otherwise-valid patches. Merged *T }
Request carries all context needed for a validation decision.
Fields:
- Operation: which mutation is being performed
- Entity: the entity being validated (always populated)
- Fields: for Patch operations, which fields are being changed (nil for Create/Update)
- Existing: for Update/Patch, the current DB state (nil unless IDFunc is configured)
type Validator ¶
type Validator[T any, ID comparable] interface { Validate(ctx context.Context, req Request[T, ID]) error }
Validator validates entities before mutation operations. Implementations can use any validation library (go-playground/validator, ozzo-validation, custom logic, etc.)
Validate returns nil to allow the operation, or an error to reject it. Returning an *Error provides structured per-field error details. Any other error type is also accepted and will prevent the mutation.
func Compose ¶
func Compose[T any, ID comparable](validators ...Validator[T, ID]) Validator[T, ID]
Compose chains multiple validators. All validators are called, and their errors are collected into a single Error. If any validator returns a *Error, the field errors are merged. If a validator returns a non-Error, it is returned immediately (short-circuit).
An empty list of validators allows everything.
func NoValidation ¶
func NoValidation[T any, ID comparable]() Validator[T, ID]
NoValidation returns a validator that allows everything. Useful for testing or prototyping when you want the decorator in the chain but don't have validation rules yet.
func OperationValidators ¶
func OperationValidators[T any, ID comparable]( perOp map[Operation]Validator[T, ID], ) Validator[T, ID]
OperationValidators maps specific operations to specific validators. Unmapped operations pass through without validation.
Example:
validation.OperationValidators[Pet, int64](map[validation.Operation]validation.Validator[Pet, int64]{
validation.OpCreate: createValidator,
validation.OpUpdate: updateValidator,
// OpPatch not mapped -> passes through
})
type ValidatorFunc ¶
ValidatorFunc is an adapter to allow use of ordinary functions as Validators.