stilltypes 0.1.0

Domain-specific refined types for the Stillwater ecosystem
Documentation

Stilltypes

Domain-specific refined types for the Stillwater ecosystem

Crates.io Documentation License

Stilltypes provides production-ready domain predicates and refined types that integrate seamlessly with stillwater. Validate emails, URLs, phone numbers, and more with errors that accumulate and types that prove validity.

Quick Start

use stilltypes::prelude::*;

// Types validate on construction
let email = Email::new("user@example.com".to_string())?;
let url = SecureUrl::new("https://example.com".to_string())?;

// Invalid values fail with helpful errors
let bad = Email::new("invalid".to_string());
assert!(bad.is_err());
println!("{}", bad.unwrap_err());
// invalid email address: invalid format, expected local@domain (example: user@example.com)

Features

Enable only what you need:

[dependencies]
stilltypes = { version = "0.1", default-features = false, features = ["email", "url"] }
Feature Types Dependencies
email (default) Email email_address
url (default) Url, HttpUrl, SecureUrl url
uuid Uuid, UuidV4, UuidV7 uuid
phone PhoneNumber phonenumber
financial Iban, CreditCardNumber iban_validate, creditcard
serde Serialize/Deserialize for all types -
full All of the above -

Error Accumulation

Collect all validation errors at once using stillwater's Validation:

use stilltypes::prelude::*;
use stillwater::validation::Validation;

struct ValidForm {
    email: Email,
    phone: PhoneNumber,
}

fn validate_form(email: String, phone: String) -> Validation<ValidForm, Vec<DomainError>> {
    Validation::all((
        Email::new(email).map_err(|e| vec![e]),
        PhoneNumber::new(phone).map_err(|e| vec![e]),
    ))
    .map(|(email, phone)| ValidForm { email, phone })
}

match validate_form(email, phone) {
    Validation::Success(form) => handle_valid(form),
    Validation::Failure(errors) => {
        for err in errors {
            println!("Error: {}", err);
        }
    }
}

JSON Validation

With the serde feature, types validate during deserialization:

use stilltypes::prelude::*;
use serde::Deserialize;

#[derive(Deserialize)]
struct User {
    email: Email,
    website: Option<SecureUrl>,
}

// Invalid JSON fails to deserialize
let result: Result<User, _> = serde_json::from_str(json);

Available Domain Types

Email (RFC 5321)

use stilltypes::email::Email;

let email = Email::new("user@example.com".to_string())?;
assert_eq!(email.get(), "user@example.com");

// Plus addressing works
let plus = Email::new("user+tag@example.com".to_string())?;

URL (RFC 3986)

use stilltypes::url::{Url, HttpUrl, SecureUrl};

// Any valid URL
let any_url = Url::new("ftp://files.example.com".to_string())?;

// HTTP or HTTPS only
let http = HttpUrl::new("http://example.com".to_string())?;

// HTTPS only (secure)
let secure = SecureUrl::new("https://secure.example.com".to_string())?;
let insecure = SecureUrl::new("http://example.com".to_string());
assert!(insecure.is_err()); // HTTP rejected

UUID

use stilltypes::uuid::{Uuid, UuidV4, UuidV7, ToUuid};

// Any valid UUID
let any = Uuid::new("550e8400-e29b-41d4-a716-446655440000".to_string())?;

// Version-specific
let v4 = UuidV4::new("550e8400-e29b-41d4-a716-446655440000".to_string())?;
let v7 = UuidV7::new("018f6b8e-e4a0-7000-8000-000000000000".to_string())?;

// Convert to uuid::Uuid
let uuid_impl = v4.to_uuid();
assert_eq!(uuid_impl.get_version_num(), 4);

Phone Numbers (E.164)

use stilltypes::phone::{PhoneNumber, PhoneNumberExt};

let phone = PhoneNumber::new("+1 (415) 555-1234".to_string())?;

// Normalize to E.164 for storage
assert_eq!(phone.to_e164(), "+14155551234");

// Get country code
assert_eq!(phone.country_code(), 1);

Financial

use stilltypes::financial::{Iban, CreditCardNumber, IbanExt, CreditCardExt};

// IBAN validation
let iban = Iban::new("DE89370400440532013000".to_string())?;
assert_eq!(iban.country_code(), "DE");
assert_eq!(iban.masked(), "DE89****3000"); // For display

// Credit card validation (Luhn algorithm)
let card = CreditCardNumber::new("4111111111111111".to_string())?;
assert_eq!(card.masked(), "****1111"); // For display
assert_eq!(card.last_four(), "1111");

When to Use Stilltypes

Use Stilltypes when:

  • Validating forms with multiple fields (accumulate all errors)
  • Building APIs that need comprehensive input validation
  • You want type-level guarantees throughout your codebase
  • Working with the Stillwater ecosystem

Skip Stilltypes if:

  • Validating a single field in a simple script
  • Your domain already has validation (e.g., ORM validates emails)
  • You only need one domain type (just copy the predicate)

Philosophy

Stilltypes follows the Stillwater philosophy:

  • Pragmatism Over Purity - No unnecessary abstractions; just predicates
  • Parse, Don't Validate - Domain types encode invariants in the type
  • Composition Over Complexity - Uses stillwater's And, Or, Not
  • Errors Should Tell Stories - Rich context for user-facing messages

Examples

See the examples/ directory for complete working examples:

  • form_validation.rs - Error accumulation with Validation::all()
  • api_handler.rs - Effect composition with from_validation()

Run with:

cargo run --example form_validation --features full
cargo run --example api_handler --features full

The Stillwater Ecosystem

Library Purpose
stillwater Effect composition and validation core
stilltypes Domain-specific refined types
mindset Zero-cost state machines
premortem Configuration validation
postmortem JSON validation with path tracking

License

Licensed under the MIT license. See LICENSE for details.