domainstack 1.1.1

Write validation once, use everywhere: Rust rules auto-generate JSON Schema + OpenAPI + TypeScript/Zod. WASM browser validation. Axum/Actix/Rocket adapters.
Documentation
use domainstack::prelude::*;

#[derive(Debug, Clone)]
pub struct Email(String);

impl Email {
    #[allow(clippy::result_large_err)]
    pub fn new(raw: String) -> Result<Self, ValidationError> {
        let rule = rules::email();
        validate("email", raw.as_str(), &rule)?;
        Ok(Self(raw))
    }
}

impl Validate for Email {
    fn validate(&self) -> Result<(), ValidationError> {
        let rule = rules::email();
        validate("email", self.0.as_str(), &rule)
    }
}

#[derive(Debug)]
pub struct Guest {
    pub name: String,
    pub email: Email,
}

impl Guest {
    #[allow(clippy::result_large_err)]
    pub fn new(name: String, email: Email) -> Result<Self, ValidationError> {
        let guest = Self { name, email };
        guest.validate()?;
        Ok(guest)
    }
}

impl Validate for Guest {
    fn validate(&self) -> Result<(), ValidationError> {
        let mut err = ValidationError::default();

        let rule = rules::min_len(1).and(rules::max_len(50));
        if let Err(e) = validate("name", self.name.as_str(), &rule) {
            err.extend(e);
        }

        if let Err(e) = self.email.validate() {
            err.merge_prefixed("email", e);
        }

        if err.is_empty() {
            Ok(())
        } else {
            Err(err)
        }
    }
}

#[derive(Debug)]
pub struct BookingRequest {
    pub guest: Guest,
    pub guests_count: u8,
}

impl BookingRequest {
    #[allow(clippy::result_large_err)]
    pub fn new(guest: Guest, guests_count: u8) -> Result<Self, ValidationError> {
        let booking = Self {
            guest,
            guests_count,
        };
        booking.validate()?;
        Ok(booking)
    }
}

impl Validate for BookingRequest {
    fn validate(&self) -> Result<(), ValidationError> {
        let mut err = ValidationError::default();

        let rule = rules::range(1, 10);
        if let Err(e) = validate("guests_count", &self.guests_count, &rule) {
            err.extend(e);
        }

        if let Err(e) = self.guest.validate() {
            err.merge_prefixed("guest", e);
        }

        if err.is_empty() {
            Ok(())
        } else {
            Err(err)
        }
    }
}

fn main() {
    println!("=== Booking Aggregate Example ===\n");

    println!("1. Valid booking:");
    let email = Email::new("john@example.com".to_string()).unwrap();
    let guest = Guest::new("John Doe".to_string(), email).unwrap();
    let booking = BookingRequest::new(guest, 2).unwrap();

    println!("   Valid booking created: {:?}", booking);

    println!("\n2. Invalid booking (empty name, too many guests):");
    if let Ok(email) = Email::new("jane@example.com".to_string()) {
        match Guest::new("".to_string(), email) {
            Ok(_) => println!("   Unexpected success for empty name"),
            Err(e) => {
                println!("   Guest validation failed:");
                for v in &e.violations {
                    println!("     [{} {}] {}", v.path, v.code, v.message);
                }
            }
        }
    }

    println!("\n3. Invalid booking (bad email):");
    match Email::new("not-an-email".to_string()) {
        Ok(_) => println!("   Unexpected success"),
        Err(e) => {
            println!("   Email validation failed:");
            for v in &e.violations {
                println!("     [{} {}] {}", v.path, v.code, v.message);
            }
        }
    }

    println!("\n4. Multiple nested errors:");
    let bad_email = Email::new("test@example.com".to_string()).unwrap();
    let bad_guest = Guest {
        name: "".to_string(),
        email: bad_email,
    };

    match BookingRequest::new(bad_guest, 15) {
        Ok(_) => println!("   Unexpected success"),
        Err(e) => {
            println!(
                "   Booking validation failed with {} errors:",
                e.violations.len()
            );
            for v in &e.violations {
                println!("     [{} {}] {}", v.path, v.code, v.message);
            }

            println!("\n   Field violations map:");
            let map = e.field_violations_map();
            for (field, violations) in map {
                let messages: Vec<&str> = violations.iter().map(|v| v.message.as_str()).collect();
                println!("     {}: {:?}", field, messages);
            }
        }
    }
}