Skip to main content

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.9"
18//! derive-where = "1.6"  # For deriving traits on structural types
19//! ```
20//!
21//! ## Usage
22//!
23//! ### Define the schema
24//!
25//! ```ignore
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//! ```ignore
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//! ```ignore
65//! # use structural_typing::{structural, presence::Present, access::Access};
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.name.get() {
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//! ```ignore
89//! # use structural_typing::{structural, presence::Present, access::Access};
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.name.get() {
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//! Enable the `serde` feature to use structural types with serde.
115//!
116//! The `select!` macro creates concrete types, useful for serialization boundaries:
117//!
118//! ```ignore
119//! use serde::{Deserialize, Serialize};
120//!
121//! #[structural]
122//! #[derive(Serialize, Deserialize)]
123//! struct User {
124//!     id: u32,
125//!     name: String,
126//!     email: String,
127//! }
128//!
129//! fn insert_user<F: user::Fields<email = Present>>(
130//!     user: User<F>
131//! ) -> User<select!(user: id, ..F)> {
132//!     // Insert to database, get generated ID
133//!     user.id(42)
134//! }
135//!
136//! let json = r#"{"name": "Alice", "email": "alice@example.com"}"#;
137//! // name optional, email present, id absent
138//! let req: User<select!(user: name?, email)> = serde_json::from_str(json)?;
139//!
140//! let response = insert_user(req);
141//! let json = serde_json::to_string(&response)?;
142//! // => {"id":42, "name": "Alice", "email": "alice@example.com"}
143//! ```
144//!
145//! ### Extract and merge
146//!
147//! ```ignore
148//! # use structural_typing::{structural, select};
149//! #
150//! # #[structural]
151//! # struct User {
152//! #     id: u32,
153//! #     name: String,
154//! #     email: String,
155//! # }
156//! #
157//! # let alice = user::empty()
158//! #     .id(42)
159//! #     .name("Alice".to_owned())
160//! #     .email("alice@example.com".to_owned());
161//! #
162//! let (credentials, id_only) = alice.extract::<select!(user: name, email)>();
163//! assert_eq!(credentials.name, "Alice");
164//! assert_eq!(id_only.id, 42);
165//!
166//! let alice = credentials.merge(id_only);
167//! // Merged values override existing values
168//! let overridden = alice.merge(user::empty().id(21));
169//! assert_eq!(overridden.name, "Alice");
170//! assert_eq!(overridden.id, 21);
171//! ```
172//!
173//! ### Nested schemas
174//!
175//! Schemas can contain other structural types:
176//!
177//! ```ignore
178//! use structural_typing::{structural, presence::Present};
179//!
180//! #[structural]
181//! struct Address {
182//!     street: String,
183//!     city: String,
184//!     zip: String,
185//! }
186//!
187//! #[structural]
188//! struct User<A: address::Fields> {
189//!     id: u32,
190//!     name: String,
191//!     address: Address<A>,
192//! }
193//!
194//! // Require specific nested fields
195//! fn ship<A, F>(user: User<A, F>)
196//! where
197//!     A: address::Fields<street = Present, city = Present>,
198//!     F: user::Fields<id = Present, address = Present>
199//! {
200//!     println!("Shipping to {} at: {}, {}",
201//!         user.id, user.address.street, user.address.city);
202//! }
203//! ```
204//!
205//! #### Nested setters
206//!
207//! Use `#[nested(...)]` to generate convenient setters for nested struct fields:
208//!
209//! ```ignore
210//! use structural_typing::{structural, select};
211//!
212//! #[structural]
213//! struct User<A: address::Fields = select!(address: all-)> {
214//!     id: u32,
215//!     name: String,
216//!     #[nested(address: street, city, zip)]
217//!     address: Address<A>,
218//! }
219//!
220//! // Setters are generated with field_nested_field naming
221//! let user = user::empty()
222//!     .id(1)
223//!     .name("Alice".to_owned())
224//!     .address(address::empty())
225//!     .address_city("Seattle".to_owned())
226//!     .address_street("Main St".to_owned());
227//!
228//! assert_eq!(user.address.city, "Seattle");
229//! assert_eq!(user.address.street, "Main St");
230//! ```
231//!
232//! Nested setters work by extracting the nested field, updating it, and merging back—preserving all
233//! other fields' presence states. This makes them compatible with `extract()` and other structural operations.
234//!
235//! **Limitations**:
236//! - `#[nested]` requires the parent struct to have at least 2 fields because the
237//!   implementation uses the spread operator, which needs other fields to preserve. Single-field parent
238//!   structs will produce a compile error.
239//! - Merge operations between instances with different nested field presence states are not supported.
240//!   The nested struct's generic parameter must match for merge to work.
241//!
242//! See the [examples](https://github.com/Crazytieguy/structural-typing-rs/tree/master/examples)
243//! directory for more comprehensive usage.
244//!
245//! ## Design
246//!
247//! The `#[structural]` macro generates a module containing a `Fields` trait with associated types
248//! for each field. Field presence is tracked through type-level markers (`Present`, `Optional`,
249//! `Absent`) that implement the `Presence` trait. Functions express requirements through trait bounds,
250//! and the builder API uses type inference to determine field states.
251//!
252//! This approach enables:
253//! - **Compile-time verification**: All field requirements checked during compilation
254//! - **Zero runtime overhead**: No dynamic checks or boxing
255//! - **API evolution**: Relaxing requirements is backward compatible
256//! - **Flexible composition**: Merge, extract, and transform with compile-time guarantees
257
258#![deny(missing_docs)]
259#![warn(clippy::pedantic)]
260
261/// Runtime field access trait for checking presence and getting values.
262pub mod access;
263/// Traits for splitting structs into selected fields and remainder.
264pub mod extract;
265/// Type-level presence markers and traits for field state tracking.
266pub mod presence;
267/// Serde integration helpers.
268#[cfg(feature = "serde")]
269pub mod serde;
270
271pub use structural_typing_macros::structural;
272
273/// Construct a `FieldSet` by selecting fields from a module.
274///
275/// # Presence States
276///
277/// | Suffix | Presence | Field Type |
278/// |--------|----------|------------|
279/// | (none) | Present | `T` |
280/// | `?` | Optional | `Option<T>` |
281/// | `-` | Absent | `PhantomData<T>` |
282///
283/// Access is always `user.field`. In generic code use `.get()` from the `Access` trait.
284///
285/// # Syntax Reference
286///
287/// | Pattern | Description |
288/// |---------|-------------|
289/// | `select!(mod: field)` | field is Present (required) |
290/// | `select!(mod: field?)` | field is Optional |
291/// | `select!(mod: field-)` | field is Absent |
292/// | `select!(mod: field<P>)` | field has custom presence type P |
293/// | `select!(mod: all)` | all fields Present |
294/// | `select!(mod: all?)` | all fields Optional |
295/// | `select!(mod: all-)` | all fields Absent |
296/// | `select!(mod: f1, f2?)` | multiple fields (f1=Present, f2=Optional) |
297/// | `select!(mod: f, ..F)` | spread: f=Present, rest inherited from F |
298/// | `select!(mod: f?, ..F)` | spread: f=Optional, rest inherited from F |
299/// | `select!(mod: f, all-)` | f=Present, all others Absent |
300///
301/// # Examples
302///
303/// ```ignore
304/// // Concrete types for API boundaries
305/// type Create = select!(user: name, email);      // Both required
306/// type Update = select!(user: name?, email?);    // Both optional
307///
308/// // Generic: add id field to any existing fields
309/// type WithId<F: user::Fields> = select!(user: id, ..F);
310///
311/// // Function using concrete type
312/// fn create(user: User<Create>) -> User<WithId<Create>> { ... }
313/// ```
314#[macro_export]
315macro_rules! select {
316    // Error: spread without fields
317    ($($module:ident)::+ : .. $spread:ty) => {
318        compile_error!("select! requires at least one explicit field before spread operator")
319    };
320
321    // Error: empty field list
322    ($($module:ident)::+ :) => {
323        compile_error!("select! requires at least one field")
324    };
325
326    // Single field with spread: Present
327    ($($module:ident)::+ : $field:ident, .. $spread:ty) => {
328        $($module)::+::with::$field<::structural_typing::presence::Present, $spread>
329    };
330
331    // Single field with spread: Optional
332    ($($module:ident)::+ : $field:ident ?, .. $spread:ty) => {
333        $($module)::+::with::$field<::structural_typing::presence::Optional, $spread>
334    };
335
336    // Single field with spread: Absent
337    ($($module:ident)::+ : $field:ident -, .. $spread:ty) => {
338        $($module)::+::with::$field<::structural_typing::presence::Absent, $spread>
339    };
340
341    // Single field with spread: Custom type
342    ($($module:ident)::+ : $field:ident < $presence:ty >, .. $spread:ty) => {
343        $($module)::+::with::$field<$presence, $spread>
344    };
345
346    // Single field: Present
347    ($($module:ident)::+ : $field:ident) => {
348        $($module)::+::with::$field
349    };
350
351    // Single field: Present with trailing comma
352    ($($module:ident)::+ : $field:ident,) => {
353        $($module)::+::with::$field
354    };
355
356    // Single field: Optional
357    ($($module:ident)::+ : $field:ident ?) => {
358        $($module)::+::with::$field<::structural_typing::presence::Optional>
359    };
360
361    // Single field: Optional with trailing comma
362    ($($module:ident)::+ : $field:ident ?,) => {
363        $($module)::+::with::$field<::structural_typing::presence::Optional>
364    };
365
366    // Single field: Absent
367    ($($module:ident)::+ : $field:ident -) => {
368        $($module)::+::with::$field<::structural_typing::presence::Absent>
369    };
370
371    // Single field: Absent with trailing comma
372    ($($module:ident)::+ : $field:ident -,) => {
373        $($module)::+::with::$field<::structural_typing::presence::Absent>
374    };
375
376    // Single field: Custom type
377    ($($module:ident)::+ : $field:ident < $presence:ty >) => {
378        $($module)::+::with::$field<$presence>
379    };
380
381    // Single field: Custom type with trailing comma
382    ($($module:ident)::+ : $field:ident < $presence:ty >,) => {
383        $($module)::+::with::$field<$presence>
384    };
385
386    // Multiple fields: Optional + rest
387    ($($module:ident)::+ : $field:ident ?, $($rest:tt)+) => {
388        $($module)::+::with::$field<::structural_typing::presence::Optional, select!($($module)::+ : $($rest)+)>
389    };
390
391    // Multiple fields: Absent + rest
392    ($($module:ident)::+ : $field:ident -, $($rest:tt)+) => {
393        $($module)::+::with::$field<::structural_typing::presence::Absent, select!($($module)::+ : $($rest)+)>
394    };
395
396    // Multiple fields: Custom type + rest
397    ($($module:ident)::+ : $field:ident < $presence:ty >, $($rest:tt)+) => {
398        $($module)::+::with::$field<$presence, select!($($module)::+ : $($rest)+)>
399    };
400
401    // Multiple fields: Present + rest
402    ($($module:ident)::+ : $field:ident, $($rest:tt)+) => {
403        $($module)::+::with::$field<::structural_typing::presence::Present, select!($($module)::+ : $($rest)+)>
404    };
405}