arvo
Validated, immutable value objects for common domain types
arvo — Finnish for value
Each type in arvo carries a single guarantee: if it exists, it is valid.
Construction always goes through ::new() returning Result — invalid states become unrepresentable at the type level. No more stringly-typed domain values, no runtime surprises.
// This compiles. This is guaranteed valid. Forever.
let email: EmailAddress = "user@example.com".try_into?;
Contents
- Installation
- Feature flags
- Quick start
- The
ValueObjecttrait - Error handling
- Serde support
- Roadmap
- Contributing
Documentation
| Document | Description |
|---|---|
| docs/value-objects.md | What value objects are, simple vs composite, normalisation |
| docs/implementing.md | How to implement the ValueObject trait for custom types |
| docs/contact.md | Reference for all contact module types |
Installation
[]
= { = "0.1", = ["contact", "serde"] }
Enable only the modules you need — unused features add zero dependencies.
Feature flags
| Feature | What you get | Extra deps |
|---|---|---|
contact |
EmailAddress, CountryCode, PhoneNumber |
once_cell, regex |
serde |
Serialize / Deserialize on all types |
serde |
full |
All domain modules | all of the above |
Tip:
serdeandfullare orthogonal — combine them freely:features = ["full", "serde"]
Quick start
use ;
use *;
// Simple value object — validated and normalised on construction
let email = new?;
assert_eq!; // always lowercase
assert_eq!;
// Ergonomic try_into from &str
let email: EmailAddress = "hello@example.com".try_into?;
// Country code — normalised to uppercase, ISO 3166-1 alpha-2
let country = new?;
assert_eq!;
// Composite value object — structured input, canonical E.164 output
let phone = new?;
assert_eq!;
assert_eq!;
// Invalid input → descriptive error, not a panic
let err = new.unwrap_err;
println!; // 'not-an-email' is not a valid EmailAddress
The ValueObject trait
Every type in arvo implements the same core interface:
Simple type — Input and Output are the same (String):
let email = new?;
email.value // &String → "user@example.com"
email.into_inner // String → "user@example.com"
Composite type — Input is a struct, Output is canonical string:
let phone = new?;
phone.value // &String → "+420123456789" (E.164)
phone.into_inner // PhoneNumberInput { country_code, number }
You can implement it for your own domain types using the provided implementations as a reference.
Error handling
All validation errors are variants of ValidationError:
use ValidationError;
match new
Serde support
Enable the serde feature. All types serialize as their raw primitive (transparent newtype):
use EmailAddress;
let email = new?;
let json = to_string?;
// → "\"user@example.com\""
// Deserialization validates — invalid JSON values are rejected at parse time
let parsed: EmailAddress = from_str?;
Roadmap
62 value object types planned across 8 domain modules. Types are only added when they bring validation, normalisation, or domain semantics that existing crates don't already provide.
| Feature | Highlights | Types | Status |
|---|---|---|---|
contact |
EmailAddress, PhoneNumber, CountryCode, PostalAddress |
5 | 3 / 5 |
identifiers |
Slug, Ean13, Isbn13, Vin |
7 | 0 / 7 |
finance |
Money, Iban, Bic, VatNumber, CreditCardNumber |
9 | 0 / 9 |
temporal |
BirthDate, ExpiryDate, TimeRange, BusinessHours |
5 | 0 / 5 |
geo |
Latitude, Longitude, Coordinate, BoundingBox, TimeZone |
6 | 0 / 6 |
net |
Url, IpAddress, MacAddress, ApiKey, Port |
10 | 0 / 10 |
measurement |
Length, Weight, Temperature, Speed ⚠️ needs unit conversion design |
10 | 0 / 10 |
primitives |
NonEmptyString, BoundedString, Locale, HexColor |
10 | 0 / 10 |
→ Full details and design rationale in ROADMAP.md
Contributing
Contributions are welcome! Please read CONTRIBUTING.md before opening a PR.
- Bug? → open a bug report
- Feature idea? → open a feature request
- Security issue? → see SECURITY.md — do not open a public issue
MIT License — © Codegress