arvo 1.0.0

Validated, immutable value objects for common domain types (email, money, identifiers, …)
Documentation
# Implementing custom value objects

You can implement the `ValueObject` and `PrimitiveValue` traits for your own domain types. Use the existing types in `src/` as reference implementations.

## Simple value object

A simple VO wraps one raw primitive. Implement both `ValueObject` (construction + deconstruction) and `PrimitiveValue` (typed accessor).

```rust,ignore
use arvo::errors::ValidationError;
use arvo::traits::{PrimitiveValue, ValueObject};

pub type PercentageInput = f64;

pub struct Percentage(f64);

impl ValueObject for Percentage {
    type Input = f64;
    type Error = ValidationError;

    fn new(value: f64) -> Result<Self, ValidationError> {
        if !(0.0..=100.0).contains(&value) {
            return Err(ValidationError::OutOfRange {
                type_name: "Percentage",
                min:    "0".into(),
                max:    "100".into(),
                actual: value.to_string(),
            });
        }
        Ok(Self(value))
    }

    fn into_inner(self) -> f64 { self.0 }
}

impl PrimitiveValue for Percentage {
    type Primitive = f64;
    fn value(&self) -> &f64 { &self.0 }
}

impl TryFrom<f64> for Percentage {
    type Error = ValidationError;
    fn try_from(v: f64) -> Result<Self, Self::Error> { Self::new(v) }
}

impl std::fmt::Display for Percentage {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}%", self.0)
    }
}
```

## Composite value object

A composite VO accepts multiple typed inputs. Implement only `ValueObject`. Expose data through dedicated accessor methods and `Display`. Provide `value()` as an inherent method returning the canonical string if useful.

```rust,ignore
use arvo::errors::ValidationError;
use arvo::traits::ValueObject;

pub struct CoordinateInput {
    pub latitude:  f64,
    pub longitude: f64,
}

pub struct Coordinate {
    input:     CoordinateInput,
    canonical: String,
}

impl ValueObject for Coordinate {
    type Input = CoordinateInput;
    type Error = ValidationError;

    fn new(value: CoordinateInput) -> Result<Self, ValidationError> {
        if !(-90.0..=90.0).contains(&value.latitude) {
            return Err(ValidationError::invalid("Coordinate.latitude", &value.latitude.to_string()));
        }
        if !(-180.0..=180.0).contains(&value.longitude) {
            return Err(ValidationError::invalid("Coordinate.longitude", &value.longitude.to_string()));
        }
        let canonical = format!("{}, {}", value.latitude, value.longitude);
        Ok(Self { input: value, canonical })
    }

    fn into_inner(self) -> CoordinateInput { self.input }
}

impl Coordinate {
    pub fn value(&self) -> &str      { &self.canonical }
    pub fn latitude(&self)  -> f64   { self.input.latitude }
    pub fn longitude(&self) -> f64   { self.input.longitude }
}

impl TryFrom<&str> for Coordinate {
    type Error = ValidationError;
    fn try_from(s: &str) -> Result<Self, Self::Error> { /* parse canonical */ todo!() }
}

impl std::fmt::Display for Coordinate {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.canonical)
    }
}
```

## Checklist for every new type

- [ ] `type Input` type alias defined and exported
- [ ] `#[derive(Debug, Clone, PartialEq, Eq, Hash)]` on the struct
- [ ] Serde: `#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]`
      + `serde(try_from = "T", into = "T")` so deserialisation validates via `new()`
      + `impl TryFrom<T>` delegating to `new()` and `#[cfg(feature = "serde")] impl From<Type> for T`
- [ ] `impl ValueObject` with `new` and `into_inner`
- [ ] For simple types: `impl PrimitiveValue` with `type Primitive` and `value()`
- [ ] For composite types: inherent `pub fn value(&self) -> &str` (if canonical string exists)
- [ ] `impl TryFrom<&str>` (for string-input types and composite types with reversible canonical format)
- [ ] `impl Display`
- [ ] Extra accessors for composite types
- [ ] Unit tests: valid input, empty/invalid input, normalisation, `try_from`, `serde_roundtrip`, `serde_deserialize_validates`
- [ ] Doc comment with `# Example` block
- [ ] Registered in `mod.rs` and `prelude`
- [ ] Status updated in `ROADMAP.md`

## ORM / database integration

arvo does not bundle database integration — this keeps dependencies minimal and lets you use any framework. Integrate using the accessors arvo already provides:

**Raw sqlx:**
```rust,ignore
// Bind — extract the primitive
query.bind(email.value())
query.bind(addr.street())

// Read back — construct via new()
let s: String = row.get("email");
let email = EmailAddress::new(s)?;
```

**SeaORM / Diesel — composite types as multiple columns:**
```rust,ignore
// Define your own entity with individual columns
impl From<PostalAddress> for AddressModel {
    fn from(addr: PostalAddress) -> Self {
        let i = addr.into_inner();
        AddressModel { street: i.street, city: i.city, zip: i.zip,
                       country: i.country.into_inner() }
    }
}
```

See [CONTRIBUTING.md](../CONTRIBUTING.md) for the full development workflow.