Skip to main content

statum_macros/
lib.rs

1//! Proc-macro implementation crate for Statum.
2//!
3//! Most users should depend on [`statum`](https://docs.rs/statum), which
4//! re-exports these macros with the public-facing documentation. This crate
5//! exists so the macro expansion logic can stay separate from the stable runtime
6//! traits in `statum-core`.
7//!
8//! The public macros are:
9//!
10//! - [`state`] for declaring legal lifecycle phases
11//! - [`machine`] for declaring the typed machine and durable context
12//! - [`transition`] for validating legal transition impls
13//! - [`validators`] for rebuilding typed machines from persisted data
14
15#[cfg(doctest)]
16#[doc = include_str!("../README.md")]
17mod readme_doctests {}
18
19mod contracts;
20mod diagnostics;
21mod source;
22
23moddef::moddef!(
24    flat (pub) mod {
25    },
26    flat (pub(crate)) mod {
27        presentation,
28        state,
29        machine,
30        transition,
31        validators
32    }
33);
34
35pub(crate) use presentation::{
36    PresentationAttr, PresentationTypesAttr, parse_present_attrs, parse_presentation_types_attr,
37    strip_present_attrs,
38};
39pub(crate) use source::{
40    ItemTarget, ModulePath, SourceFingerprint, crate_root_for_file, current_crate_root,
41    extract_derives, source_file_fingerprint,
42};
43
44use crate::diagnostics::DiagnosticMessage;
45use crate::source::{current_module_path_opt, module_path_for_span, source_info_for_span};
46use crate::{
47    LoadedMachineLookupFailure, MachinePath, ambiguous_transition_machine_error,
48    ambiguous_transition_machine_fallback_error, lookup_loaded_machine_in_module,
49    lookup_unique_loaded_machine_by_name,
50};
51use proc_macro::TokenStream;
52use proc_macro2::Span;
53use syn::spanned::Spanned;
54use syn::{Item, ItemImpl, parse_macro_input};
55
56pub(crate) fn strict_introspection_enabled() -> bool {
57    cfg!(feature = "strict-introspection")
58}
59
60/// Define the legal lifecycle phases for a Statum machine.
61///
62/// Apply `#[state]` to an enum with unit variants, single-field tuple
63/// variants, or named-field variants. Statum generates one marker type per
64/// variant plus the state-family traits used by `#[machine]`, `#[transition]`,
65/// and `#[validators]`.
66#[proc_macro_attribute]
67pub fn state(attr: TokenStream, item: TokenStream) -> TokenStream {
68    if !attr.is_empty() {
69        return syn::Error::new(
70            Span::call_site(),
71            DiagnosticMessage::new("`#[state]` does not accept arguments.")
72                .found(format!("`#[state({attr})]`"))
73                .expected("`#[state] enum WorkflowState { Draft, Review(ReviewData) }`")
74                .fix("remove the attribute arguments and describe states with enum variants instead.".to_string())
75                .render(),
76        )
77        .to_compile_error()
78        .into();
79    }
80    let input = parse_macro_input!(item as Item);
81    let input = match input {
82        Item::Enum(item_enum) => item_enum,
83        other => return invalid_state_target_error(&other).into(),
84    };
85
86    // Validate the enum before proceeding
87    if let Some(error) = validate_state_enum(&input) {
88        return error.into();
89    }
90
91    let enum_info = match EnumInfo::from_item_enum(&input) {
92        Ok(info) => info,
93        Err(err) => return err.to_compile_error().into(),
94    };
95
96    // Store metadata in `state_enum_map`
97    store_state_enum(&enum_info);
98
99    // Generate structs and implementations dynamically
100    let expanded = generate_state_impls(&enum_info);
101
102    TokenStream::from(expanded)
103}
104
105/// Define a typed machine that carries durable context across states.
106///
107/// Apply `#[machine]` to a struct whose first generic parameter is the
108/// `#[state]` enum family. Additional type and const generics are supported
109/// after that state generic. Statum generates the typed machine surface,
110/// builders, the machine-scoped `machine::SomeState` enum, a compatibility
111/// alias `machine::State = machine::SomeState`, and helper items such as
112/// `machine::Fields` for heterogeneous batch rebuilds.
113#[proc_macro_attribute]
114pub fn machine(attr: TokenStream, item: TokenStream) -> TokenStream {
115    if !attr.is_empty() {
116        return syn::Error::new(
117            Span::call_site(),
118            DiagnosticMessage::new("`#[machine]` does not accept arguments.")
119                .found(format!("`#[machine({attr})]`"))
120                .expected("`#[machine] struct WorkflowMachine<WorkflowState> { ... }`")
121                .fix("remove the attribute arguments and link the machine to `#[state]` through its first generic parameter.".to_string())
122                .render(),
123        )
124        .to_compile_error()
125        .into();
126    }
127    let input = parse_macro_input!(item as Item);
128    let input = match input {
129        Item::Struct(item_struct) => item_struct,
130        other => return invalid_machine_target_error(&other).into(),
131    };
132    let machine_info = match MachineInfo::from_item_struct(&input) {
133        Ok(info) => info,
134        Err(err) => return err.to_compile_error().into(),
135    };
136
137    // Validate the struct before proceeding
138    if let Some(error) = validate_machine_struct(&input, &machine_info) {
139        return error.into();
140    }
141
142    // Store metadata in `machine_map`
143    store_machine_struct(&machine_info);
144
145    // Generate any required structs or implementations dynamically
146    let expanded = generate_machine_impls(&machine_info, &input);
147
148    TokenStream::from(expanded)
149}
150
151/// Validate and generate legal transitions for one source state.
152///
153/// Apply `#[transition]` to an `impl Machine<CurrentState>` block. Each method
154/// must consume `self` and return a legal `Machine<NextState>` shape or a
155/// source-declared type alias that expands to that shape, or a supported
156/// wrapper around it, such as `Result<Machine<NextState>, E>`,
157/// `Option<Machine<NextState>>`, or
158/// `statum::Branch<Machine<Left>, Machine<Right>>`.
159///
160/// When the `strict-introspection` feature is enabled, transition graph
161/// semantics must be directly readable from the written return type or from a
162/// local `#[introspect(return = ...)]` escape hatch on the method.
163#[proc_macro_attribute]
164pub fn transition(
165    attr: proc_macro::TokenStream,
166    item: proc_macro::TokenStream,
167) -> proc_macro::TokenStream {
168    if !attr.is_empty() {
169        return syn::Error::new(
170            Span::call_site(),
171            DiagnosticMessage::new("`#[transition]` does not accept arguments.")
172                .found(format!("`#[transition({attr})]`"))
173                .expected("`#[transition] impl WorkflowMachine<Draft> { ... }`")
174                .fix("remove the attribute arguments and declare transition behavior with methods inside the impl block.".to_string())
175                .render(),
176        )
177        .to_compile_error()
178        .into();
179    }
180    let input = parse_macro_input!(item as ItemImpl);
181
182    // -- Step 1: Parse
183    let tr_impl = match parse_transition_impl(&input) {
184        Ok(parsed) => parsed,
185        Err(err) => return err.into(),
186    };
187
188    let module_path = match resolved_current_module_path(tr_impl.machine_span, "#[transition]") {
189        Ok(path) => path,
190        Err(err) => return err,
191    };
192
193    let machine_path: MachinePath = module_path.clone().into();
194    let machine_info_owned =
195        match lookup_loaded_machine_in_module(&machine_path, &tr_impl.machine_name) {
196            Ok(info) => Some(info),
197            Err(LoadedMachineLookupFailure::Ambiguous(candidates)) => {
198                return ambiguous_transition_machine_error(
199                    &tr_impl.machine_name,
200                    &module_path,
201                    &candidates,
202                    tr_impl.machine_span,
203                )
204                .into();
205            }
206            Err(LoadedMachineLookupFailure::NotFound) => {
207                match lookup_unique_loaded_machine_by_name(&tr_impl.machine_name) {
208                    Ok(info) => Some(info),
209                    Err(LoadedMachineLookupFailure::Ambiguous(candidates)) => {
210                        return ambiguous_transition_machine_fallback_error(
211                            &tr_impl.machine_name,
212                            &module_path,
213                            &candidates,
214                            tr_impl.machine_span,
215                        )
216                        .into();
217                    }
218                    Err(LoadedMachineLookupFailure::NotFound) => None,
219                }
220            }
221        };
222    let machine_info = match machine_info_owned.as_ref() {
223        Some(info) => info,
224        None => {
225            return missing_transition_machine_error(
226                &tr_impl.machine_name,
227                &module_path,
228                tr_impl.machine_span,
229            )
230            .into();
231        }
232    };
233
234    if let Some(err) = validate_transition_functions(&tr_impl, machine_info) {
235        return err.into();
236    }
237
238    // -- Step 3: Generate new code
239    let expanded = generate_transition_impl(&input, &tr_impl, machine_info);
240
241    // Combine expanded code with the original `impl` if needed
242    // or simply return the expanded code
243    expanded.into()
244}
245
246/// Rebuild typed machines from persisted data.
247///
248/// Apply `#[validators(Machine)]` or an anchored path such as
249/// `#[validators(self::path::Machine)]`,
250/// `#[validators(super::path::Machine)]`, or
251/// `#[validators(crate::path::Machine)]` to an `impl PersistedRow` block.
252/// Statum expects one `is_{state}` method per state variant and generates
253/// `into_machine()`, `.into_machines()`, and `.into_machines_by(...)` helpers
254/// for typed rehydration. Validator methods can return `Result<T, _>` for
255/// ordinary membership checks or `Validation<T>` when rebuild reports should
256/// carry stable rejection details through `.build_report()` and
257/// `.build_reports()`. In relaxed mode, bare multi-segment paths like
258/// `#[validators(flow::Machine)]` are treated as local child-module paths, not
259/// imported aliases or re-exports. If Statum cannot resolve that local path,
260/// it emits a compile error asking for an anchored path instead.
261#[proc_macro_attribute]
262pub fn validators(attr: TokenStream, item: TokenStream) -> TokenStream {
263    if attr.is_empty() {
264        return syn::Error::new(
265            Span::call_site(),
266            DiagnosticMessage::new("`#[validators(...)]` requires a machine path.")
267                .expected("`#[validators(WorkflowMachine)] impl PersistedRow { ... }`")
268                .fix("pass the target Statum machine type in the attribute, for example `#[validators(self::flow::WorkflowMachine)]`.".to_string())
269                .render(),
270        )
271        .to_compile_error()
272        .into();
273    }
274    let item_impl = parse_macro_input!(item as ItemImpl);
275    let module_path = match resolved_current_module_path(item_impl.self_ty.span(), "#[validators]")
276    {
277        Ok(path) => path,
278        Err(err) => return err,
279    };
280    parse_validators(attr, item_impl, &module_path)
281}
282
283fn resolved_current_module_path(span: Span, macro_name: &str) -> Result<String, TokenStream> {
284    let resolved = module_path_for_span(span)
285        .or_else(current_module_path_opt)
286        .or_else(|| {
287            source_info_for_span(span)
288                .is_none()
289                .then_some("crate".to_string())
290        });
291
292    resolved.ok_or_else(|| {
293        let message = format!(
294            "Internal error: could not resolve the module path for `{macro_name}` at this call site."
295        );
296        quote::quote_spanned! { span =>
297            compile_error!(#message);
298        }
299        .into()
300    })
301}