stilltypes 0.2.0

Domain-specific refined types for the Rust and Stillwater ecosystem
Documentation

Stilltypes

Domain-specific refined types for Rust

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
network Ipv4Addr, Ipv6Addr, DomainName, Port -
geo Latitude, Longitude -
numeric Percentage, UnitInterval -
identifiers Slug -
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");

Network (IP, Domain, Port)

use stilltypes::network::{Ipv4Addr, Ipv6Addr, Port, DomainName, Ipv4Ext, PortExt};

// IPv4 validation with semantic helpers
let ip = Ipv4Addr::new("192.168.1.1".to_string())?;
assert!(ip.is_private());
assert!(!ip.is_loopback());

// IPv6 validation
let ipv6 = Ipv6Addr::new("::1".to_string())?;
assert!(ipv6.is_loopback());

// Port validation with IANA range classification
let port = Port::new(443)?;
assert!(port.is_privileged());
assert!(port.is_well_known());

// Domain name validation (RFC 1035)
let domain = DomainName::new("api.example.com".to_string())?;
assert_eq!(domain.tld(), Some("com"));

Geographic Coordinates

use stilltypes::geo::{Latitude, Longitude, LatitudeExt, LongitudeExt};

// Latitude validates range -90 to 90 degrees
let lat = Latitude::new(37.7749)?;
assert!(lat.is_north());

// Longitude validates range -180 to 180 degrees
let lon = Longitude::new(-122.4194)?;
assert!(lon.is_west());

// Convert to degrees, minutes, seconds
let (deg, min, sec, hemi) = lat.to_dms();
// 37° 46' 29.64" N

Bounded Numerics

use stilltypes::numeric::{Percentage, UnitInterval, PercentageExt, UnitIntervalExt};

// Percentage validates range 0 to 100
let discount = Percentage::new(25.0)?;
let price = 100.0;
let discounted = price - discount.of(price);  // 75.0

// Convert between representations
let probability = UnitInterval::new(0.75)?;
let as_percent = probability.to_percentage();  // 75%

// Create from decimal
let half = Percentage::from_decimal(0.5)?;  // 50%

URL Slugs

use stilltypes::identifiers::{Slug, SlugExt};

// Validate existing slug
let slug = Slug::new("my-first-post".to_string())?;
assert_eq!(slug.get(), "my-first-post");

// Convert from title
let slug = Slug::from_title("My First Blog Post!")?;
assert_eq!(slug.get(), "my-first-blog-post");

// Error on invalid slugs
let invalid = Slug::new("Invalid Slug".to_string());
assert!(invalid.is_err());

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()
  • network_validation.rs - Server config validation with IP/port/domain
  • geo_validation.rs - Geographic coordinate validation with DMS conversion
  • discount_validation.rs - Percentage and pricing calculations with numeric types
  • slug_validation.rs - URL slug validation and title conversion

Run with:

cargo run --example form_validation --features full
cargo run --example api_handler --features full
cargo run --example network_validation --features full
cargo run --example geo_validation --features full
cargo run --example discount_validation --features full
cargo run --example slug_validation --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.