# 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](https://www.youtube.com/watch?v=YR5WdGrpoug) 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
```toml
[dependencies]
structural-typing = "0.1.7"
derive-where = "1.6" # For deriving traits on structural types
```
## Usage
### Define the schema
```rust
use structural_typing::structural;
#[structural]
struct User {
id: u32,
name: String,
email: String,
}
```
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>`):
```rust
use structural_typing::presence::Present;
// Requires both id and name
fn display_user<F: user::Fields<id = Present, name = Present>>(user: &User<F>) {
println!("User #{}: {}", user.id, user.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.
```rust
// Requires only id; adapts behavior based on whether name is present
fn display_user<F: user::Fields<id = Present>>(user: &User<F>) {
if let Some(name) = user.get_name() {
println!("User #{}: {}", user.id, name);
} else {
println!("User #{}", user.id);
}
}
```
### Build instances incrementally
The builder API infers field presence from the value type:
```rust
let bob = User::empty().id(123);
display_user(&bob);
let bob = bob.name("Bob".to_owned());
```
### Serde integration
The `select!` macro creates concrete types, useful for serialization boundaries:
```rust
use serde::{Deserialize, Serialize};
#[structural]
#[derive(Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
let json = r#"{"name": "Alice", "email": "alice@example.com"}"#;
// name optional, email present, id absent
let alice: User<select!(user: ?name, email)> = serde_json::from_str(json)?;
let response = alice.id(42);
let json = serde_json::to_string(&response)?;
// => {"id":42, "name": "Alice", "email": "alice@example.com"}
```
### Extract and merge
```rust
use structural_typing::select;
// Extract name and email, get back credentials and remainder with just id
let (credentials, id_only) = alice.extract::<select!(user: name, email)>();
assert_eq!(credentials.name, "Alice");
assert_eq!(id_only.id, 42);
```
Merge instances:
```rust
let alice = credentials.merge(id_only);
// merged values override existing values
let overridden = alice.merge(User::empty().id(21));
assert_eq!(overridden.name, "Alice");
assert_eq!(overridden.id, 21);
```
### Nested schemas
Schemas can contain other structural types:
```rust
#[structural]
struct Address {
street: String,
city: String,
zip: String,
}
#[structural]
struct User<A: address::Fields> {
id: u32,
name: String,
address: Address<A>,
}
// Require specific nested fields
fn ship<A, F>(user: User<A, F>)
where
A: address::Fields<street = Present, city = Present>,
F: user::Fields<id = Present, address = Present>
{
println!("Shipping to {} at: {}, {}",
user.id, user.address.street, user.address.city);
}
```
See [examples/](examples/) for more comprehensive usage including the REST API example.
## License
MIT OR Apache-2.0