Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Validation and Refinement — Constrained Types

Schemas parse structure. Validation adds constraints: an age must be positive, an email must contain @, a price must have at most two decimal places.

refine / filter

refine and filter attach a predicate to an existing schema. Parsing succeeds only when the base schema succeeds and the predicate returns true.

use effectful::schema::{i64, refine, string, filter};

let age_schema = refine(
    i64::<()>(),
    |n| (0..=150).contains(n),
    "age must be between 0 and 150",
);

let non_empty = filter(
    string::<()>(),
    |s: &String| !s.is_empty(),
    "must not be empty",
);

If the predicate fails, decoding returns ParseError::new("", message).

Fallible Transformation

Use transform when conversion can fail or when the semantic type differs from the wire type.

use effectful::schema::{ParseError, string, transform};

let url_schema = transform(
    string::<()>(),
    |s| url::Url::parse(&s).map_err(|e| ParseError::new("", format!("invalid URL: {e}"))),
    |url: url::Url| url.to_string(),
);

The decode closure returns Result<B, ParseError>. The encode closure maps the semantic value back to the base schema’s semantic type.

Brand

Brand<A, B> is a zero-cost nominal wrapper. Use Brand::nominal when the value was already validated, or RefinedBrand when construction should validate.

use effectful::schema::{Brand, RefinedBrand};

struct EmailMarker;
type Email = Brand<String, EmailMarker>;

let email = Brand::nominal("alice@example.com".to_string());

let make_email = RefinedBrand::<String, EmailMarker>::new(|s| {
    if s.contains('@') {
        Ok(())
    } else {
        Err("invalid email".to_string())
    }
});

let checked: Email = make_email.try_make("alice@example.com".to_string())?;

Now APIs can demand Email instead of a raw String.

fn send_welcome(to: Email) -> Effect<(), MailError, Mailer> { /* ... */ }

HasSchema

HasSchema attaches a canonical schema to a type family. The trait exposes associated types for semantic value, wire value, and schema marker.

use effectful::schema::{HasSchema, Schema, i64};

struct UserIdSchema;

impl HasSchema for UserIdSchema {
    type A = i64;
    type I = i64;
    type E = ();

    fn schema() -> Schema<Self::A, Self::I, Self::E> {
        i64::<()>()
    }
}

Use HasSchema for generic tooling that needs to ask for a canonical schema without knowing how it is built.

Summary

ToolWhen to use
refine / filterPredicate on a parsed value
transformFallible conversion or semantic/wire conversion
Brand::nominalNominal wrapper after validation elsewhere
RefinedBrandValidating branded constructor
HasSchemaAttach a canonical schema to a type-level provider