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}