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

Schema Combinators — Describing Data Shapes

A schema describes how to decode wire data into a typed value and encode it back. In the current API the full shape is Schema<A, I, E>: semantic value A, wire/intermediate value I, and schema marker E.

Primitive Schemas

use effectful::schema::{bool_, f64, i64, string};

let name = string::<()>(); // Schema<String, String, ()>
let age = i64::<()>();     // Schema<i64, i64, ()>
let price = f64::<()>();   // Schema<f64, f64, ()>
let active = bool_::<()>(); // Schema<bool, bool, ()>

Each primitive can decode its typed wire value with decode, and can decode an Unknown with decode_unknown.

Struct-Like Schemas

struct_, struct3, and struct4 decode named object fields into tuples. Use transform to map the tuple into a domain struct.

use effectful::schema::{ParseError, Schema, i64, string, struct_, transform};

#[derive(Clone)]
struct User {
    name: String,
    age: i64,
}

let tuple_schema = struct_("name", string::<()>(), "age", i64::<()>());

let user_schema = transform(
    tuple_schema,
    |(name, age)| Ok(User { name, age }),
    |user: User| (user.name, user.age),
);

If a field is missing or has the wrong type, decode_unknown returns a ParseError with the field path.

Optional and Array Schemas

use effectful::schema::{array, optional, string};

let maybe_name = optional(string::<()>());
let tags = array(string::<()>());

optional(schema) accepts Unknown::Null as None. array(schema) decodes each element and prefixes parse errors with the failing index.

Validation and Transformation

Use free combinators, not schema methods.

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

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

let email_schema = transform(
    non_empty,
    |s| Email::parse(s).map_err(|e| ParseError::new("", format!("invalid email: {e}"))),
    |email: Email| email.into_string(),
);

filter keeps values satisfying a predicate. transform performs bidirectional conversion and may fail while decoding.

Unions

union_ tries a primary schema, then a fallback schema. For more than two branches, use union_chain from schema::extra.

use effectful::schema::{Schema, Unknown, union_};

let primary: Schema<UserId, Unknown, ()> = user_id_from_number();
let fallback: Schema<UserId, Unknown, ()> = user_id_from_string();
let user_id_schema = union_(primary, fallback);

Running a Schema

Run schemas directly through their methods.

use effectful::schema::{Unknown, i64};

let raw = Unknown::I64(30);
let age = i64::<()>().decode_unknown(&raw)?;

decode works on the schema’s typed wire value I. decode_unknown works on Unknown trees at I/O boundaries.