koruma
A per-field validation library for Rust with struct-based errors.
Features
- Per-field validation with strongly-typed error
- Multiple validators per field
- Generic validator support with type inference
- Optional field support (skips validation when
None) - Nested struct validation with
#[koruma(nested)] - Newtype wrapper support with
#[koruma(newtype)] - Validated constructors with
#[koruma(try_new)]
koruma-collection
provides a collection of common validators, with partial i18n support.
currently supported: en, fr
Installation
[]
= { = "*", = ["derive"] }
= { = "*" } # internally used by koruma
Examples
Quick Start
Defining Validators
Use #[koruma::validator] to define validation rules. Each validator must have a field marked with #[koruma(value)] to capture the validated value:
Generic Validators
For validators that work with multiple types, use generics with a blanket impl:
// Use a blanket impl with trait bounds
Type-specific Validators
use ;
Validating Structs
Apply validators to struct fields using #[derive(Koruma)] and the #[koruma(...)] attribute:
use Koruma;
// Use `::<_>` (turbofish) to infer the type from the field
Accessing Validation Errors
The generated error struct provides typed access to each field's validation errors:
let user = User ;
match user.validate
Multiple Validators Per Field
Apply multiple validators to a single field by separating them with commas:
// Access individual validators
let err = item.validate.unwrap_err;
if let Some = err.value.number_range_validation
if let Some = err.value.even_number_validation
// Or get all failed validators at once
let all_errors = err.value.all; // Vec<ItemValueValidator>
Collection Validation
Use the each(...) syntax to validate each element in a Vec:
// Errors include the index of the failing element
let order = Order ;
let err = order.validate.unwrap_err;
// Returns &[(usize, OrderScoresError)]
for in err.scores
Optional Field Validation
Fields of type Option<T> are automatically handled:
None: Validation is skipped entirelySome(value): The inner value is validated
// None fields are skipped
let profile = UserProfile ;
assert!;
// Some fields are validated
let profile = UserProfile ;
let err = profile.validate.unwrap_err;
// Error captures the inner value
let bio_err = err.bio.string_length_validation.unwrap;
assert_eq!;
Nested Struct Validation
For fields that are themselves structs deriving Koruma, use #[koruma(nested)] to automatically validate them:
// Validation cascades through nested structs
let customer = Customer ;
match customer.validate
Nested validation also works with optional fields:
// None is skipped
let customer = CustomerWithOptionalAddress ;
assert!;
Nesting can be arbitrarily deep - nested structs can themselves contain nested structs:
// Access deeply nested errors
let err = employee.validate.unwrap_err;
if let Some = err.employer
Newtype Wrappers
For single-field wrapper structs (newtypes), use #[koruma(newtype)] at both the struct level and field level to get transparent error access.
Defining a Newtype
Use #[koruma(newtype)] at the struct level to mark a single-field struct as a newtype:
// The error struct implements Deref, so you can access .all() directly
let num = PositiveNumber ;
let err = num.validate.unwrap_err;
// Access validators directly via Deref
let all_errors = err.all; // No need to go through .value()
if let Some = err.range_validation
Using Newtypes as Fields
When using a newtype as a field in another struct, use #[koruma(newtype)] instead of #[koruma(nested)] to get transparent error access:
let order = Order ;
let err = order.validate.unwrap_err;
// Access nested newtype errors directly via Deref
// No need for .unwrap() or pattern matching on Option
let all_qty_errors = err.quantity.all;
if let Some = err.quantity.range_validation
The difference between nested and newtype:
| Attribute | Use Case | Error Access |
|---|---|---|
#[koruma(nested)] |
Multi-field structs | err.field() returns Option<&InnerError> |
#[koruma(newtype)] |
Single-field wrappers | err.field() returns &Wrapper with Deref |
Validated Constructors with try_new
Use #[koruma(try_new)] at the struct level to generate a try_new constructor that validates on creation:
// Use try_new instead of struct literal + validate
match try_new
// Equivalent to:
// let user = ValidatedUser { username: "alice".to_string(), age: 25 };
// user.validate()?;
You can combine try_new with newtype for validated wrapper types:
// Create validated email
let email = try_new?;
// Invalid emails are rejected at construction
let result = try_new;
assert!;
Error Messages
Basic String Messages
For simple error messages, implement Display or a custom method on your validators:
// Usage
if let Some = errors.age.number_range_validation
Fluent Integration
For internationalized error messages, use es-fluent:
Derive EsFluent on your validators:
use EsFluent;
Create corresponding Fluent files:
# locales/en/main.ftl
number-range-validation = Value { $actual } must be between { $min } and { $max }
Use to_fluent_string() to get localized messages:
use ToFluentString as _;
if let Some = errors.age.number_range_validation
Fluent with all() Method
When using the all() method to get all failed validators, you can derive KorumaFluentEnum on the generated enum to implement ToFluentString:
use ToFluentString as _;
use KorumaFluentEnum;
// Derive KorumaFluentEnum on the generated validator enum
// This requires all inner validators to implement ToFluentString
// Now you can iterate over all errors
for validator in errors.value.all
Note: KorumaFluentEnum requires the fluent feature to be enabled and all variant types must implement ToFluentString.