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 machine;
22mod presentation;
23mod source;
24mod state;
25mod transition;
26mod validators;
27
28pub(crate) use machine::{
29    LoadedMachineLookupFailure, MachineInfo, MachinePath, expand_machine,
30    format_loaded_machine_candidates, invalid_machine_target_error,
31    lookup_loaded_machine_in_module, lookup_unique_loaded_machine_by_name,
32};
33pub(crate) use state::{
34    EnumInfo, LoadedStateLookupFailure, StateModulePath, VariantInfo, VariantShape, expand_state,
35    format_loaded_state_candidates, invalid_state_target_error, lookup_loaded_state_enum,
36    lookup_loaded_state_enum_by_name, to_snake_case,
37};
38pub(crate) use transition::expand_transition;
39pub(crate) use validators::parse_validators;
40
41pub(crate) use presentation::{
42    PresentationAttr, PresentationTypesAttr, parse_present_attrs_for,
43    parse_presentation_types_attr, strip_present_attrs,
44};
45pub(crate) use source::{
46    ItemTarget, ModulePath, SourceFingerprint, crate_root_for_file, current_crate_root,
47    extract_derives, source_file_fingerprint,
48};
49
50use crate::diagnostics::DiagnosticMessage;
51use crate::source::{current_module_path_opt, module_path_for_span, source_info_for_span};
52use proc_macro::TokenStream;
53use proc_macro2::Span;
54use syn::spanned::Spanned;
55use syn::{Item, ItemImpl, parse_macro_input};
56
57pub(crate) fn strict_introspection_enabled() -> bool {
58    cfg!(feature = "strict-introspection")
59}
60
61/// Define the legal lifecycle phases for a Statum machine.
62///
63/// Apply `#[state]` to an enum with unit variants, single-field tuple
64/// variants, or named-field variants. Statum generates one marker type per
65/// variant plus the state-family traits used by `#[machine]`, `#[transition]`,
66/// and `#[validators]`.
67#[proc_macro_attribute]
68pub fn state(attr: TokenStream, item: TokenStream) -> TokenStream {
69    if !attr.is_empty() {
70        return syn::Error::new(
71            Span::call_site(),
72            DiagnosticMessage::new("`#[state]` does not accept arguments.")
73                .found(format!("`#[state({attr})]`"))
74                .expected("`#[state] enum WorkflowState { Draft, Review(ReviewData) }`")
75                .fix("remove the attribute arguments and describe states with enum variants instead.".to_string())
76                .render(),
77        )
78        .to_compile_error()
79        .into();
80    }
81    let input = parse_macro_input!(item as Item);
82    let input = match input {
83        Item::Enum(item_enum) => item_enum,
84        other => return invalid_state_target_error(&other).into(),
85    };
86    expand_state(input).into()
87}
88
89/// Define a typed machine that carries durable context across states.
90///
91/// Apply `#[machine]` to a struct whose first generic parameter is the
92/// `#[state]` enum family. Additional type and const generics are supported
93/// after that state generic. Statum generates the typed machine surface,
94/// builders, the machine-scoped `machine::SomeState` enum, a compatibility
95/// alias `machine::State = machine::SomeState`, and helper items such as
96/// `machine::Fields` for heterogeneous batch rebuilds.
97#[proc_macro_attribute]
98pub fn machine(attr: TokenStream, item: TokenStream) -> TokenStream {
99    if !attr.is_empty() {
100        return syn::Error::new(
101            Span::call_site(),
102            DiagnosticMessage::new("`#[machine]` does not accept arguments.")
103                .found(format!("`#[machine({attr})]`"))
104                .expected("`#[machine] struct WorkflowMachine<WorkflowState> { ... }`")
105                .fix("remove the attribute arguments and link the machine to `#[state]` through its first generic parameter.".to_string())
106                .render(),
107        )
108        .to_compile_error()
109        .into();
110    }
111    let input = parse_macro_input!(item as Item);
112    let input = match input {
113        Item::Struct(item_struct) => item_struct,
114        other => return invalid_machine_target_error(&other).into(),
115    };
116    expand_machine(input).into()
117}
118
119/// Validate and generate legal transitions for one source state.
120///
121/// Apply `#[transition]` to an `impl Machine<CurrentState>` block. Each method
122/// must consume `self` and return a legal `Machine<NextState>` shape or a
123/// source-declared type alias that expands to that shape, or a supported
124/// wrapper around it, such as `Result<Machine<NextState>, E>`,
125/// `Option<Machine<NextState>>`, or
126/// `statum::Branch<Machine<Left>, Machine<Right>>`.
127///
128/// When the `strict-introspection` feature is enabled, transition graph
129/// semantics must be directly readable from the written return type or from a
130/// local `#[introspect(return = ...)]` escape hatch on the method.
131#[proc_macro_attribute]
132pub fn transition(
133    attr: proc_macro::TokenStream,
134    item: proc_macro::TokenStream,
135) -> proc_macro::TokenStream {
136    if !attr.is_empty() {
137        return syn::Error::new(
138            Span::call_site(),
139            DiagnosticMessage::new("`#[transition]` does not accept arguments.")
140                .found(format!("`#[transition({attr})]`"))
141                .expected("`#[transition] impl WorkflowMachine<Draft> { ... }`")
142                .fix("remove the attribute arguments and declare transition behavior with methods inside the impl block.".to_string())
143                .render(),
144        )
145        .to_compile_error()
146        .into();
147    }
148    let input = parse_macro_input!(item as ItemImpl);
149    expand_transition(input).into()
150}
151
152/// Rebuild typed machines from persisted data.
153///
154/// Apply `#[validators(Machine)]` or an anchored path such as
155/// `#[validators(self::path::Machine)]`,
156/// `#[validators(super::path::Machine)]`, or
157/// `#[validators(crate::path::Machine)]` to an `impl PersistedRow` block.
158/// Statum expects one `is_{state}` method per state variant and generates
159/// `into_machine()`, `.into_machines()`, and `.into_machines_by(...)` helpers
160/// for typed rehydration. Validator methods can return `Result<T, _>` for
161/// ordinary membership checks or `Validation<T>` when rebuild reports should
162/// carry stable rejection details through `.build_report()` and
163/// `.build_reports()`. In relaxed mode, bare multi-segment paths like
164/// `#[validators(flow::Machine)]` are treated as local child-module paths, not
165/// imported aliases or re-exports. If Statum cannot resolve that local path,
166/// it emits a compile error asking for an anchored path instead.
167#[proc_macro_attribute]
168pub fn validators(attr: TokenStream, item: TokenStream) -> TokenStream {
169    if attr.is_empty() {
170        return syn::Error::new(
171            Span::call_site(),
172            DiagnosticMessage::new("`#[validators(...)]` requires a machine path.")
173                .expected("`#[validators(WorkflowMachine)] impl PersistedRow { ... }`")
174                .fix("pass the target Statum machine type in the attribute, for example `#[validators(self::flow::WorkflowMachine)]`.".to_string())
175                .render(),
176        )
177        .to_compile_error()
178        .into();
179    }
180    let item_impl = parse_macro_input!(item as ItemImpl);
181    let module_path = match resolved_current_module_path(item_impl.self_ty.span(), "#[validators]")
182    {
183        Ok(path) => path,
184        Err(err) => return err,
185    };
186    parse_validators(attr, item_impl, &module_path)
187}
188
189pub(crate) fn resolved_current_module_path(
190    span: Span,
191    macro_name: &str,
192) -> Result<String, TokenStream> {
193    let resolved = module_path_for_span(span)
194        .or_else(current_module_path_opt)
195        .or_else(|| {
196            source_info_for_span(span)
197                .is_none()
198                .then_some("crate".to_string())
199        });
200
201    resolved.ok_or_else(|| {
202        let message = format!(
203            "Internal error: could not resolve the module path for `{macro_name}` at this call site."
204        );
205        quote::quote_spanned! { span =>
206            compile_error!(#message);
207        }
208        .into()
209    })
210}