structural-typing 0.1.9

Type-level field presence tracking for Rust structs, inspired by TypeScript
Documentation
# 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.9"
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
use structural_typing::access::Access;

// 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.name.get() {
        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

Enable the `serde` feature to use structural types with serde.

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,
}

fn insert_user<F: user::Fields<email = Present>>(
    user: User<F>
) -> User<select!(user: id, ..F)> {
    // Insert to database, get generated ID
    user.id(42)
}

let json = r#"{"name": "Alice", "email": "alice@example.com"}"#;
// name optional, email present, id absent
let req: User<select!(user: name?, email)> = serde_json::from_str(json)?;

let response = insert_user(req);
let json = serde_json::to_string(&response)?;
// => {"id":42, "name": "Alice", "email": "alice@example.com"}
```

### Extract and merge

```rust
use structural_typing::select;

let (credentials, id_only) = alice.extract::<select!(user: name, email)>();
assert_eq!(credentials.name, "Alice");
assert_eq!(id_only.id, 42);

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<F, A>(user: User<F, A>)
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);
}
```

#### Nested setters

Use `#[nested(...)]` to generate convenient setters for nested struct fields:

```rust
#[structural]
struct User<A: address::Fields = select!(address: all-)> {
    id: u32,
    name: String,
    #[nested(address: street, city, zip)]
    address: Address<A>,
}

// Setters are generated with field_nested_field naming
let user = user::empty()
    .id(1)
    .name("Alice".to_owned())
    .address(address::empty())
    .address_city("Seattle".to_owned())
    .address_street("Main St".to_owned());

assert_eq!(user.address.city, "Seattle");
assert_eq!(user.address.street, "Main St");
```

Nested setters work by extracting the nested field, updating it, and merging back—preserving all other fields' presence states. This makes them compatible with `extract()` and other structural operations.

**Limitations**:
- `#[nested]` requires the parent struct to have at least 2 fields because the implementation uses the spread operator, which needs other fields to preserve. Single-field parent structs will produce a compile error.
- Merge operations between instances with different nested field presence states are not supported. The nested struct's generic parameter must match for merge to work.

See [examples/](examples/) for more usage patterns, including a [REST API with SQLite](examples/todos-api/) demonstrating how one schema handles multiple endpoint types.

## License

MIT OR Apache-2.0