structural_typing/
lib.rs

1//! # Structural Typing for Rust
2//!
3//! Structural typing separates schemas (what fields can exist) from selections (what fields must be
4//! present). Optionality is context-dependent—a name is always a string, but different contexts may
5//! or may not require it. Instead of creating separate types for each context (`CreateUser`,
6//! `UpdateUser`, `DBUser`...) or using `Option<T>` everywhere, define your schema once and use
7//! generic trait bounds for context-specific requirements—all verified at compile time.
8//!
9//! This design is inspired by Rich Hickey's [`Maybe Not`](https://www.youtube.com/watch?v=YR5WdGrpoug)
10//! talk, which articulates the principle of separating attribute definitions from their use in aggregates,
11//! along with ideas from TypeScript's structural types and RDF's independent attributes.
12//!
13//! ## Installation
14//!
15//! ```toml
16//! [dependencies]
17//! structural-typing = "0.1.7"
18//! derive-where = "1.6"  # For deriving traits on structural types
19//! ```
20//!
21//! ## Usage
22//!
23//! ### Define the schema
24//!
25//! ```
26//! use structural_typing::structural;
27//!
28//! #[structural]
29//! struct User {
30//!     id: u32,
31//!     name: String,
32//!     email: String,
33//! }
34//! ```
35//!
36//! The `#[structural]` macro generates a module named `user` containing a `Fields` trait with
37//! associated types for each field. This enables compile-time tracking of which fields are present.
38//!
39//! ### Generic functions with field requirements
40//!
41//! Functions can require specific fields through trait bounds using the generated `Fields` trait.
42//! The `Presence` type can be `Present` (`T`), `Optional` (`Option<T>`), or `Absent` (`PhantomData<T>`):
43//!
44//! ```
45//! # use structural_typing::{structural, presence::Present};
46//! #
47//! # #[structural]
48//! # struct User {
49//! #     id: u32,
50//! #     name: String,
51//! #     email: String,
52//! # }
53//! #
54//! // Requires both id and name
55//! fn display_user<F: user::Fields<id = Present, name = Present>>(user: &User<F>) {
56//!     println!("User #{}: {}", user.id, user.name);
57//! }
58//! ```
59//!
60//! Relaxing requirements is backward compatible. Existing callers with id and name continue to work
61//! if we remove the `name` requirement—unlike changing a function parameter from `T` to `Option<T>`,
62//! which breaks all existing call sites.
63//!
64//! ```
65//! # use structural_typing::{structural, presence::Present};
66//! #
67//! # #[structural]
68//! # struct User {
69//! #     id: u32,
70//! #     name: String,
71//! #     email: String,
72//! # }
73//! #
74//! // Requires only id; adapts behavior based on whether name is present
75//! fn display_user<F: user::Fields<id = Present>>(user: &User<F>) {
76//!     if let Some(name) = user.get_name() {
77//!         println!("User #{}: {}", user.id, name);
78//!     } else {
79//!         println!("User #{}", user.id);
80//!     }
81//! }
82//! ```
83//!
84//! ### Build instances incrementally
85//!
86//! The builder API infers field presence from the value type:
87//!
88//! ```
89//! # use structural_typing::{structural, presence::Present};
90//! #
91//! # #[structural]
92//! # struct User {
93//! #     id: u32,
94//! #     name: String,
95//! #     email: String,
96//! # }
97//! #
98//! # fn display_user<F: user::Fields<id = Present>>(user: &User<F>) {
99//! #     if let Some(name) = user.get_name() {
100//! #         println!("User #{}: {}", user.id, name);
101//! #     } else {
102//! #         println!("User #{}", user.id);
103//! #     }
104//! # }
105//! #
106//! let bob = User::empty().id(123);
107//! display_user(&bob);
108//!
109//! let bob = bob.name("Bob".to_owned());
110//! ```
111//!
112//! ### Serde integration
113//!
114//! The `select!` macro creates concrete types, useful for serialization boundaries:
115//!
116//! ```ignore
117//! use serde::{Deserialize, Serialize};
118//!
119//! #[structural]
120//! #[derive(Serialize, Deserialize)]
121//! struct User {
122//!     id: u32,
123//!     name: String,
124//!     email: String,
125//! }
126//!
127//! let json = r#"{"name": "Alice", "email": "alice@example.com"}"#;
128//! // name optional, email present, id absent
129//! let alice: User<select!(user: ?name, email)> = serde_json::from_str(json)?;
130//!
131//! let response = alice.id(42);
132//! let json = serde_json::to_string(&response)?;
133//! // => {"id":42, "name": "Alice", "email": "alice@example.com"}
134//! ```
135//!
136//! ### Extract and merge
137//!
138//! ```
139//! # use structural_typing::{structural, select};
140//! #
141//! # #[structural]
142//! # struct User {
143//! #     id: u32,
144//! #     name: String,
145//! #     email: String,
146//! # }
147//! #
148//! # let alice = User::empty()
149//! #     .id(42)
150//! #     .name("Alice".to_owned())
151//! #     .email("alice@example.com".to_owned());
152//! #
153//! // Extract name and email, get back credentials and remainder with just id
154//! let (credentials, id_only) = alice.extract::<select!(user: name, email)>();
155//! assert_eq!(credentials.name, "Alice");
156//! assert_eq!(id_only.id, 42);
157//! ```
158//!
159//! Merge instances:
160//!
161//! ```
162//! # use structural_typing::structural;
163//! #
164//! # #[structural]
165//! # struct User {
166//! #     id: u32,
167//! #     name: String,
168//! #     email: String,
169//! # }
170//! #
171//! # let credentials = User::empty()
172//! #     .name("Alice".to_owned())
173//! #     .email("alice@example.com".to_owned());
174//! # let id_only = User::empty().id(42);
175//! #
176//! let alice = credentials.merge(id_only);
177//! // merged values override existing values
178//! let overridden = alice.merge(User::empty().id(21));
179//! assert_eq!(overridden.name, "Alice");
180//! assert_eq!(overridden.id, 21);
181//! ```
182//!
183//! ### Nested schemas
184//!
185//! Schemas can contain other structural types:
186//!
187//! ```ignore
188//! use structural_typing::{structural, presence::Present};
189//!
190//! #[structural]
191//! struct Address {
192//!     street: String,
193//!     city: String,
194//!     zip: String,
195//! }
196//!
197//! #[structural]
198//! struct User<A: address::Fields> {
199//!     id: u32,
200//!     name: String,
201//!     address: Address<A>,
202//! }
203//!
204//! // Require specific nested fields
205//! fn ship<A, F>(user: User<A, F>)
206//! where
207//!     A: address::Fields<street = Present, city = Present>,
208//!     F: user::Fields<id = Present, address = Present>
209//! {
210//!     println!("Shipping to {} at: {}, {}",
211//!         user.id, user.address.street, user.address.city);
212//! }
213//! ```
214//!
215//! See the [examples](https://github.com/Crazytieguy/structural-typing-rs/tree/master/examples)
216//! directory for more comprehensive usage.
217//!
218//! ## Design
219//!
220//! The `#[structural]` macro generates a module containing a `Fields` trait with associated types
221//! for each field. Field presence is tracked through type-level markers (`Present`, `Optional`,
222//! `Absent`) that implement the `Presence` trait. Functions express requirements through trait bounds,
223//! and the builder API uses type inference to determine field states.
224//!
225//! This approach enables:
226//! - **Compile-time verification**: All field requirements checked during compilation
227//! - **Zero runtime overhead**: No dynamic checks or boxing
228//! - **API evolution**: Relaxing requirements is backward compatible
229//! - **Flexible composition**: Merge, extract, and transform with compile-time guarantees
230
231#![deny(missing_docs)]
232#![warn(clippy::pedantic)]
233
234/// Runtime field access trait for checking presence and getting values.
235pub mod access;
236/// Type-level presence markers and traits for field state tracking.
237pub mod presence;
238/// Traits for splitting structs into selected fields and remainder.
239pub mod extract;
240
241pub use structural_typing_macros::structural;
242
243/// Construct a `FieldSet` by selecting fields from a module.
244///
245/// # Syntax
246/// - `select!(module: field)` - field is Present
247/// - `select!(module: ?field)` - field is Optional
248/// - `select!(module: ?all)` - all fields Optional
249/// - Multiple fields compose: `select!(module: field1, ?field2)` expands to `module::with::field1::Present<module::with::field2::Optional>`
250///
251/// # Examples
252/// ```ignore
253/// type Create = select!(user: name, email);
254/// type Update = select!(user: ?name, ?email);
255/// fn create_user(user: User<Create>) {...}
256/// ```
257#[macro_export]
258macro_rules! select {
259    // Error: invalid + prefix
260    ($($module:ident)::+ : + $field:ident $($rest:tt)*) => {
261        compile_error!("Invalid prefix '+' in select!. Use 'field' for Present or '?field' for Optional")
262    };
263
264    // Error: invalid - prefix
265    ($($module:ident)::+ : - $field:ident $($rest:tt)*) => {
266        compile_error!("Invalid prefix '-' in select!. Use 'field' for Present or '?field' for Optional")
267    };
268
269    // Error: empty field list
270    ($($module:ident)::+ :) => {
271        compile_error!("select! requires at least one field")
272    };
273
274    // Single field: Present
275    ($($module:ident)::+ : $field:ident) => {
276        $($module)::+::with::$field::Present
277    };
278
279    // Single field: Present with trailing comma
280    ($($module:ident)::+ : $field:ident,) => {
281        $($module)::+::with::$field::Present
282    };
283
284    // Single field: Optional
285    ($($module:ident)::+ : ? $field:ident) => {
286        $($module)::+::with::$field::Optional
287    };
288
289    // Single field: Optional with trailing comma
290    ($($module:ident)::+ : ? $field:ident,) => {
291        $($module)::+::with::$field::Optional
292    };
293
294    // Multiple fields: Present + rest
295    ($($module:ident)::+ : $field:ident, $($rest:tt)+) => {
296        $($module)::+::with::$field::Present<select!($($module)::+ : $($rest)+)>
297    };
298
299    // Multiple fields: Optional + rest
300    ($($module:ident)::+ : ? $field:ident, $($rest:tt)+) => {
301        $($module)::+::with::$field::Optional<select!($($module)::+ : $($rest)+)>
302    };
303}