Structural Typing for Rust
Status: Experimental. API subject to change.
Overview
Structural typing separates schemas (what fields can exist) from selections (what fields must be present). Optionality is context-dependent—a name is always a string, but different contexts may or may not require it. Instead of creating separate types for each context (CreateUser, UpdateUser, DBUser...) or using Option<T> everywhere, define your schema once and use generic trait bounds for context-specific requirements—all verified at compile time.
This design is inspired by Rich Hickey's Maybe Not talk, which articulates the principle of separating attribute definitions from their use in aggregates, along with ideas from TypeScript's structural types and RDF's independent attributes.
Installation
[]
= "0.1.7"
= "1.6" # For deriving traits on structural types
Usage
Define the schema
use structural;
The #[structural] macro generates a module named user containing a Fields trait with associated types for each field. This enables compile-time tracking of which fields are present.
Generic functions with field requirements
Functions can require specific fields through trait bounds using the generated Fields trait. The Presence type can be Present (T), Optional (Option<T>), or Absent (PhantomData<T>):
use Present;
// Requires both id and name
Relaxing requirements is backward compatible. Existing callers with id and name continue to work if we remove the name requirement—unlike changing a function parameter from T to Option<T>, which breaks all existing call sites.
// Requires only id; adapts behavior based on whether name is present
Build instances incrementally
The builder API infers field presence from the value type:
let bob = empty.id;
display_user;
let bob = bob.name;
Serde integration
The select! macro creates concrete types, useful for serialization boundaries:
use ;
let json = r#"{"name": "Alice", "email": "alice@example.com"}"#;
// name optional, email present, id absent
let alice: > = from_str?;
let response = alice.id;
let json = to_string?;
// => {"id":42, "name": "Alice", "email": "alice@example.com"}
Extract and merge
use select;
// Extract name and email, get back credentials and remainder with just id
let = alice.>;
assert_eq!;
assert_eq!;
Merge instances:
let alice = credentials.merge;
// merged values override existing values
let overridden = alice.merge;
assert_eq!;
assert_eq!;
Nested schemas
Schemas can contain other structural types:
// Require specific nested fields
See examples/ for more comprehensive usage including the REST API example.
License
MIT OR Apache-2.0