typestate_pipeline_macros/lib.rs
1//! Proc-macros for the [`typestate-pipeline`] ecosystem.
2//!
3//! Two macros, two axes of typestate:
4//!
5//! - [`transitions`] — attribute on an `impl` block. Each method marked
6//! `#[transition(into = NextState)]` expands into a Resolved + InFlight
7//! method pair on a [`Pipeline`] newtype carrier.
8//! - [`TypestateFactory`] — derive on a struct. Generates a sibling
9//! `<Name>Factory<F1, …>` with one flag generic per field; `finalize()`
10//! is callable only once the required flags reach `Yes`.
11//!
12//! Use the macros through the [`typestate-pipeline`] facade — generated code
13//! references items via `::typestate_pipeline::__private::*` and depending
14//! on this crate alone produces unresolved paths.
15//!
16//! [`Pipeline`]: https://docs.rs/typestate-pipeline-core
17//! [`typestate-pipeline`]: https://docs.rs/typestate-pipeline
18
19use proc_macro::TokenStream;
20use syn::{DeriveInput, ItemImpl, parse_macro_input};
21
22mod diag;
23mod prefix;
24mod transitions;
25mod typestate_factory;
26
27/// Generate Resolved + InFlight method pairs from a single source body.
28///
29/// Decorates an inherent `impl` block on a tuple-struct newtype around
30/// `Pipeline`. Each method marked `#[transition(into = NextState)]`
31/// expands into two methods: one for `Resolved` mode and one for
32/// `InFlight` mode, both forwarding to a shared private body fn.
33///
34/// # Arguments
35///
36/// - `error = <Type>` *(optional)* — error type used by fallible bodies
37/// and the `Pipeline`'s `E` parameter. When omitted, the error is read
38/// from `<Self as Pipelined<'a>>::Error`.
39/// - `ctx = <ident>` *(optional, default `ctx`)* — name the macro
40/// recognizes as the borrowed-context parameter inside transition bodies.
41///
42/// # Method contract
43///
44/// Each `#[transition]` method is written as if the body owned the state:
45///
46/// - First parameter must be `state: <CurrentState>`.
47/// - Second parameter, if named `ctx` (configurable), receives the
48/// borrowed pipeline context.
49/// - Remaining parameters become the user-visible parameters on the
50/// generated methods (which take `self` as their receiver).
51///
52/// # Body shapes
53///
54/// The macro inspects each method's signature and picks one of four
55/// expansions:
56///
57/// | shape | when | resolved arm | in-flight arm |
58/// |------------------|---------------------------------------------------|-----------------------------------|----------------------------------------------------|
59/// | sync infallible | `fn` returning a non-`Result` | `Carrier<…, Resolved>` | `Carrier<…, InFlight>` |
60/// | sync fallible | `fn` returning `Result<_, E>` | `Result<Carrier<…, Resolved>, E>` | `Carrier<…, InFlight>` (Result folded into future) |
61/// | async deferred | `async fn` (default) | `Carrier<…, InFlight>` | `Carrier<…, InFlight>` |
62/// | async breakpoint | `async fn` with `#[transition(breakpoint)]` | `async fn` returning `Result<Carrier<…, Resolved>, E>` | same |
63///
64/// **Deferred async** (the default) lets a chain like
65/// `pipeline.tag(7).with_parallelism(8).deploy().await?` fold every async
66/// transition into a single terminal `.await?`. **Breakpoint async**
67/// resolves at that method, useful when a later transition needs the
68/// resolved state value to compute its arguments.
69///
70/// # Example
71///
72/// ```ignore
73/// use typestate_pipeline::{pipelined, transitions};
74///
75/// pipelined!(Author, ctx = Client, error = AuthoringError);
76///
77/// #[transitions]
78/// impl<'a> Author<'a, Registered> {
79/// #[transition(into = Versioned)]
80/// pub async fn tag_version(state: Registered, ctx: &Client, version: u32)
81/// -> Result<Versioned, AuthoringError>
82/// {
83/// ctx.tag(state.name.clone(), version).await;
84/// Ok(Versioned { name: state.name, version })
85/// }
86/// }
87/// ```
88///
89/// # Safety
90///
91/// Generated code uses no `unsafe`. Every shape expands into safe glue
92/// around `Pipeline::resolved`, `Pipeline::in_flight`, `Box::pin`, and
93/// `Pipeline::map_inner_sync` / `map_inner_sync_fallible`. State-machine
94/// soundness is enforced by the type system: a transition wired to the
95/// wrong destination state simply will not compile at the call site.
96#[doc(alias = "state machine")]
97#[doc(alias = "typestate")]
98#[doc(alias = "transition")]
99#[proc_macro_attribute]
100pub fn transitions(attr: TokenStream, item: TokenStream) -> TokenStream {
101 let args = parse_macro_input!(attr as transitions::TransitionsArgs);
102 let input = parse_macro_input!(item as ItemImpl);
103 match transitions::expand(args, input) {
104 Ok(ts) => ts.into(),
105 Err(err) => err.into_compile_error().into(),
106 }
107}
108
109/// Derive a sibling typestate factory for a struct.
110///
111/// Generates `<Name>Factory<F1, …>`, one flag generic per field. Setters
112/// transition the relevant flag from `No` to `Yes`; `finalize()` is
113/// callable only when every required flag is `Yes`.
114///
115/// # Container attributes — `#[factory(...)]`
116///
117/// - `name = MyFactory` — override the generated factory's type name
118/// (default `<Original>Factory`).
119/// - `error = MyError` — error type for fallible setters and Pipeline arms.
120/// Required when any field is `fallible`.
121/// - `pipeline(carrier = MyAuthor)` — also emit Resolved + InFlight method
122/// pairs on the user's carrier for every standalone transition. The
123/// carrier must implement `Pipelined<'a>`.
124/// - `finalize_async(via = my_fn, into = Target, error = E?)` — emit an
125/// additional `async fn finalize_async()` that calls `my_fn(raw).await`.
126/// - `no_unsafe` — opt into the safe-mode codegen path. Gated on the
127/// `no_unsafe` Cargo feature; without the feature the attribute is
128/// rejected at expansion time.
129///
130/// # Field attributes — `#[field(...)]`
131///
132/// - `required` *(default unless `default = …` is present)* — flag must
133/// be `Yes` for `finalize`.
134/// - `optional` — opt out of required.
135/// - `default` / `default = expr` — declare a default; emits a
136/// `<field>_default()` helper. Optional fields with defaults may
137/// finalize whether the flag is `Yes` or `No`.
138/// - `removable` — emit `drop_<field>(self)` reverting the flag to `No`.
139/// - `overridable` — emit `override_<field>(self, val)` on `Yes`-flagged
140/// bags.
141/// - `name = setter_name` — override the generated setter's name.
142/// - `setter = my_fn` — call `my_fn(val) -> FieldType` inside the setter.
143/// - `fallible` *(with `setter`)* — transformer returns
144/// `Result<FieldType, Error>`; setter returns `Result<NextBag, Error>`.
145/// - `async_fn` *(with `setter`)* — transformer is `async fn`; setter
146/// becomes `async fn`.
147/// - `internal` — locked at construction; field is set positionally on
148/// `new(…)` and has no setter / overrider / remover / default.
149/// - `input = <Type>` *(with `setter`)* — override the setter's input
150/// parameter type; the transformer bridges to the field's storage type.
151///
152/// `default` is rejected when combined with `fallible` or `async_fn`.
153///
154/// # Example
155///
156/// ```ignore
157/// use typestate_pipeline::TypestateFactory;
158///
159/// #[derive(TypestateFactory)]
160/// struct User {
161/// #[field(required)]
162/// name: String,
163/// #[field(required)]
164/// email: String,
165/// #[field(default = 18)]
166/// age: u32,
167/// }
168///
169/// let user = UserFactory::new()
170/// .name("Alice".into())
171/// .email("alice@example.com".into())
172/// .with_age(30)
173/// .finalize();
174/// ```
175///
176/// # Safety
177///
178/// By default the generated code uses three `unsafe` operations, each
179/// gated by a type-level invariant:
180///
181/// 1. `MaybeUninit::assume_init_ref` in getters — gated by the field's
182/// flag being pinned to `Yes` in the impl signature.
183/// 2. `MaybeUninit::assume_init_drop` in `Drop` — guarded at runtime by
184/// the flag's `Satisfiable::IS_SET` constant.
185/// 3. `ptr::read` in setters / `finalize` — paired with `ManuallyDrop`
186/// so the moved-from `MaybeUninit` slot is not touched again.
187///
188/// Setters compute the new value *before* wrapping `self` in
189/// `ManuallyDrop`, so a failing `?` or dropped future leaves `self` live
190/// and its normal `Drop` runs. The `factory_no_leak` test suite is the
191/// regression guard.
192///
193/// Opting in to `#[factory(no_unsafe)]` (with the `no_unsafe` Cargo
194/// feature on) emits a `MaybeUninit`-free implementation that uses
195/// `<Flag as Storage<T>>::Out` to give each flag combination a
196/// structurally distinct type. See [`Storage`] for details.
197///
198/// [`Storage`]: https://docs.rs/typestate-pipeline-core/latest/typestate_pipeline_core/flag/trait.Storage.html
199#[doc(alias = "builder")]
200#[doc(alias = "typestate")]
201#[doc(alias = "accumulator")]
202#[proc_macro_derive(TypestateFactory, attributes(factory, field))]
203pub fn derive_typestate_factory(input: TokenStream) -> TokenStream {
204 let input = parse_macro_input!(input as DeriveInput);
205 match typestate_factory::expand(input) {
206 Ok(tokens) => tokens.into(),
207 Err(err) => err.into_compile_error().into(),
208 }
209}