Documentation
¶
Overview ¶
Package validation validates entities before mutations (Create, Update, Patch, Upsert, PatchWhere) on any r3.CRUD[T, ID]. The decorator wraps the repo, transparently satisfies it, and drops in anywhere a CRUD is expected. Reads (Get, List) and Delete pass through unvalidated.
R3 ships no validation rules. You implement the Validator interface with any library - go-playground/validator, go-ozzo/ozzo-validation, or plain Go; the decorator calls it and short-circuits with a structured Error on failure. 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
}
State-Transition Validation ¶
With WithIDFunc, the decorator fetches the existing entity before Update and Patch, so the validator receives req.Existing (current DB state) and can enforce 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 }),
)
Patch-Aware Validation ¶
For Patch, Request.Fields lists the fields being changed, so validators can skip rules for unchanged ones:
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, the rest zeroed, so the rule would wrongly fire. When WithIDFunc is set, the request carries Request.Merged (the patch overlaid on current state) - validate that instead:
if req.Operation == validation.OpPatch && req.Merged != nil {
return v.validate.Struct(*req.Merged)
}
Atomic State-Transition Validation ¶
The fetch-validate-write sequence is only atomic when the decorator runs inside a transaction: wrap the repo with the transactor feature and run Update/Patch in InTx so the read and write share one transaction. Otherwise a concurrent writer can change the row between fetch and write (a TOCTOU window), and 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]) AggregateThroughRelation(ctx context.Context, relation string, 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]) PatchWhere(ctx context.Context, filters r3.Filters, entity T, fields r3.Fields) (int64, 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)
- func (v *CRUD[T, ID]) Upsert(ctx context.Context, entity T, opts ...r3.UpsertOption) (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 every validation failure matches via errors.Is.
Functions ¶
This section is empty.
Types ¶
type CRUD ¶
type CRUD[T any, ID comparable] struct { // contains filtered or unexported fields }
CRUD validates entities before mutations, delegating the rules to a user-provided Validator; reads and Delete pass through. See the package doc.
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 inner so mutations are validated by validator.
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]) AggregateThroughRelation ¶ added in v0.0.5
func (v *CRUD[T, ID]) AggregateThroughRelation( ctx context.Context, relation string, qarg ...r3.Query, ) ([]r3.AggregateRow, error)
AggregateThroughRelation delegates directly to the inner CRUD (no validation on reads).
func (*CRUD[T, ID]) Patch ¶
Patch fetches the existing entity (if IDFunc is set), validates with Fields populated, then delegates to inner.Patch. With IDFunc set the request also carries Merged (the patch overlaid on current state), so whole-entity validators don't see the sparse (zeroed) input.
Fetch-validate-write is atomic only inside a transaction: wrap the repo with the transactor feature and call Patch within InTx. Otherwise a concurrent writer can change the row between the Get and the Patch (a TOCTOU window).
func (*CRUD[T, ID]) PatchWhere ¶ added in v0.0.4
func (v *CRUD[T, ID]) PatchWhere( ctx context.Context, filters r3.Filters, entity T, fields r3.Fields, ) (int64, error)
PatchWhere validates the entity/fields (OpPatchWhere), then delegates to inner.PatchWhere. Like Patch, only Fields are meaningful; there is no single Existing row, so Existing/Merged stay nil. Returns r3.ErrBulkPatchNotSupported when the inner repo can't bulk-patch.
func (*CRUD[T, ID]) Rewrap ¶
Rewrap rebuilds this decorator around inner, re-applying the validation layer on top of a transaction-bound CRUD.
func (*CRUD[T, ID]) Unwrap ¶
Unwrap returns the wrapped CRUD so decorator-chain walks (capability detection, transaction propagation) can reach the backend.
func (*CRUD[T, ID]) Update ¶
Update fetches the existing entity (if IDFunc is set), validates, then delegates to inner.Update. As with Patch, fetch-validate-write is atomic only inside a transaction (see the transactor feature); otherwise the read 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 failures.
Errors []FieldError
}
Error carries structured per-field validation failures and matches 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 that failed (e.g. "name", "email").
Field string
// Message is the human-readable failure (e.g. "is required").
Message string
// Code is a machine-readable code (e.g. "required", "min") 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, letting the decorator fetch the existing entity 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.
const ( OpCreate Operation = "create" OpUpdate Operation = "update" OpPatch Operation = "patch" // OpUpsert validates the full entity of an Upsert (insert-or-update); the // whole entity is present, as on Create. OpUpsert Operation = "upsert" // OpPatchWhere validates the entity/fields of a bulk conditional update. Like // Patch, only Fields carry meaning; there is no single Existing row, so // Existing/Merged are never populated. OpPatchWhere Operation = "patch_where" )
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 primary-key extractor, enabling state-transition validation: the decorator fetches current DB state before Update and Patch so the validator can compare old vs new.
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, when set, makes the decorator fetch the existing entity before // Update and Patch, populating Request.Existing for state-transition // validation. Without it, Existing is always nil. IDFunc IDFunc[T, ID] // RecordType names the entity type in error messages. If empty, derived from // T (e.g. Order -> "orders", OrderItem -> "order_items"). RecordType string }
Options configures the validation decorator.
type Request ¶
type Request[T any, ID comparable] struct { // Operation is the mutation type. Operation Operation // Entity being validated. Always populated. Entity T // Fields being patched. Non-nil only for Patch; lets validators skip rules for // unchanged fields. Fields r3.Fields // Existing is the current DB state. Populated only for Update/Patch when // WithIDFunc is set - enables state-transition rules (e.g. "draft -> published"). Existing *T // Merged is the entity AFTER the patch is applied (patched fields overlaid on // Existing). Populated only for Patch when WithIDFunc is set. Prefer Merged over // Entity for whole-entity rules on Patch, since Entity carries only the patched // fields (rest zeroed) and validating it directly would reject valid patches. Merged *T }
Request carries the context for a validation decision.
type Validator ¶
type Validator[T any, ID comparable] interface { Validate(ctx context.Context, req Request[T, ID]) error }
Validator validates entities before mutations, using any library or plain Go. Validate returns nil to allow or an error to reject; an *Error carries structured per-field details, but any error type prevents the mutation.
func Compose ¶
func Compose[T any, ID comparable](validators ...Validator[T, ID]) Validator[T, ID]
Compose runs all validators, merging their *Error field errors into one Error. A non-*Error short-circuits and is returned immediately. An empty list allows everything.
func NoValidation ¶
func NoValidation[T any, ID comparable]() Validator[T, ID]
NoValidation returns a validator that allows everything - for tests or prototyping when the decorator is in the chain but rules aren't ready.
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.