1#[cfg(doctest)]
16#[doc = include_str!("../README.md")]
17mod readme_doctests {}
18
19mod analysis;
20mod cache;
21mod callsite;
22mod diagnostics;
23mod module_path;
24mod parser;
25mod pathing;
26mod query;
27mod syntax;
28
29moddef::moddef!(
30 flat (pub) mod {
31 },
32 flat (pub(crate)) mod {
33 presentation,
34 state,
35 machine,
36 transition,
37 validators
38 }
39);
40
41pub(crate) use presentation::{
42 PresentationAttr, PresentationTypesAttr, parse_present_attrs, parse_presentation_types_attr,
43 strip_present_attrs,
44};
45pub(crate) use syntax::{
46 ItemTarget, ModulePath, SourceFingerprint, crate_root_for_file, current_crate_root,
47 extract_derives, source_file_fingerprint,
48};
49
50use crate::callsite::{current_module_path_opt, module_path_for_span};
51use crate::diagnostics::DiagnosticMessage;
52use crate::{
53 LoadedMachineLookupFailure, MachinePath, ambiguous_transition_machine_error,
54 ambiguous_transition_machine_fallback_error, lookup_loaded_machine_in_module,
55 lookup_unique_loaded_machine_by_name,
56};
57use proc_macro::TokenStream;
58use proc_macro2::Span;
59use syn::spanned::Spanned;
60use syn::{Item, ItemImpl, parse_macro_input};
61
62pub(crate) fn strict_introspection_enabled() -> bool {
63 cfg!(feature = "strict-introspection")
64}
65
66#[proc_macro_attribute]
73pub fn state(attr: TokenStream, item: TokenStream) -> TokenStream {
74 if !attr.is_empty() {
75 return syn::Error::new(
76 Span::call_site(),
77 DiagnosticMessage::new("`#[state]` does not accept arguments.")
78 .found(format!("`#[state({attr})]`"))
79 .expected("`#[state] enum WorkflowState { Draft, Review(ReviewData) }`")
80 .fix("remove the attribute arguments and describe states with enum variants instead.".to_string())
81 .render(),
82 )
83 .to_compile_error()
84 .into();
85 }
86 let input = parse_macro_input!(item as Item);
87 let input = match input {
88 Item::Enum(item_enum) => item_enum,
89 other => return invalid_state_target_error(&other).into(),
90 };
91
92 if let Some(error) = validate_state_enum(&input) {
94 return error.into();
95 }
96
97 let enum_info = match EnumInfo::from_item_enum(&input) {
98 Ok(info) => info,
99 Err(err) => return err.to_compile_error().into(),
100 };
101
102 store_state_enum(&enum_info);
104
105 let expanded = generate_state_impls(&enum_info);
107
108 TokenStream::from(expanded)
109}
110
111#[proc_macro_attribute]
120pub fn machine(attr: TokenStream, item: TokenStream) -> TokenStream {
121 if !attr.is_empty() {
122 return syn::Error::new(
123 Span::call_site(),
124 DiagnosticMessage::new("`#[machine]` does not accept arguments.")
125 .found(format!("`#[machine({attr})]`"))
126 .expected("`#[machine] struct WorkflowMachine<WorkflowState> { ... }`")
127 .fix("remove the attribute arguments and link the machine to `#[state]` through its first generic parameter.".to_string())
128 .render(),
129 )
130 .to_compile_error()
131 .into();
132 }
133 let input = parse_macro_input!(item as Item);
134 let input = match input {
135 Item::Struct(item_struct) => item_struct,
136 other => return invalid_machine_target_error(&other).into(),
137 };
138 let machine_info = match MachineInfo::from_item_struct(&input) {
139 Ok(info) => info,
140 Err(err) => return err.to_compile_error().into(),
141 };
142
143 if let Some(error) = validate_machine_struct(&input, &machine_info) {
145 return error.into();
146 }
147
148 store_machine_struct(&machine_info);
150
151 let expanded = generate_machine_impls(&machine_info, &input);
153
154 TokenStream::from(expanded)
155}
156
157#[proc_macro_attribute]
170pub fn transition(
171 attr: proc_macro::TokenStream,
172 item: proc_macro::TokenStream,
173) -> proc_macro::TokenStream {
174 if !attr.is_empty() {
175 return syn::Error::new(
176 Span::call_site(),
177 DiagnosticMessage::new("`#[transition]` does not accept arguments.")
178 .found(format!("`#[transition({attr})]`"))
179 .expected("`#[transition] impl WorkflowMachine<Draft> { ... }`")
180 .fix("remove the attribute arguments and declare transition behavior with methods inside the impl block.".to_string())
181 .render(),
182 )
183 .to_compile_error()
184 .into();
185 }
186 let input = parse_macro_input!(item as ItemImpl);
187
188 let tr_impl = match parse_transition_impl(&input) {
190 Ok(parsed) => parsed,
191 Err(err) => return err.into(),
192 };
193
194 let module_path = match resolved_current_module_path(tr_impl.machine_span, "#[transition]") {
195 Ok(path) => path,
196 Err(err) => return err,
197 };
198
199 let machine_path: MachinePath = module_path.clone().into();
200 let machine_info_owned =
201 match lookup_loaded_machine_in_module(&machine_path, &tr_impl.machine_name) {
202 Ok(info) => Some(info),
203 Err(LoadedMachineLookupFailure::Ambiguous(candidates)) => {
204 return ambiguous_transition_machine_error(
205 &tr_impl.machine_name,
206 &module_path,
207 &candidates,
208 tr_impl.machine_span,
209 )
210 .into();
211 }
212 Err(LoadedMachineLookupFailure::NotFound) => {
213 match lookup_unique_loaded_machine_by_name(&tr_impl.machine_name) {
214 Ok(info) => Some(info),
215 Err(LoadedMachineLookupFailure::Ambiguous(candidates)) => {
216 return ambiguous_transition_machine_fallback_error(
217 &tr_impl.machine_name,
218 &module_path,
219 &candidates,
220 tr_impl.machine_span,
221 )
222 .into();
223 }
224 Err(LoadedMachineLookupFailure::NotFound) => None,
225 }
226 }
227 };
228 let machine_info = match machine_info_owned.as_ref() {
229 Some(info) => info,
230 None => {
231 return missing_transition_machine_error(
232 &tr_impl.machine_name,
233 &module_path,
234 tr_impl.machine_span,
235 )
236 .into();
237 }
238 };
239
240 if let Some(err) = validate_transition_functions(&tr_impl, machine_info) {
241 return err.into();
242 }
243
244 let expanded = generate_transition_impl(&input, &tr_impl, machine_info);
246
247 expanded.into()
250}
251
252#[proc_macro_attribute]
268pub fn validators(attr: TokenStream, item: TokenStream) -> TokenStream {
269 if attr.is_empty() {
270 return syn::Error::new(
271 Span::call_site(),
272 DiagnosticMessage::new("`#[validators(...)]` requires a machine path.")
273 .expected("`#[validators(WorkflowMachine)] impl PersistedRow { ... }`")
274 .fix("pass the target Statum machine type in the attribute, for example `#[validators(self::flow::WorkflowMachine)]`.".to_string())
275 .render(),
276 )
277 .to_compile_error()
278 .into();
279 }
280 let item_impl = parse_macro_input!(item as ItemImpl);
281 let module_path = match resolved_current_module_path(item_impl.self_ty.span(), "#[validators]")
282 {
283 Ok(path) => path,
284 Err(err) => return err,
285 };
286 parse_validators(attr, item_impl, &module_path)
287}
288
289fn resolved_current_module_path(span: Span, macro_name: &str) -> Result<String, TokenStream> {
290 let resolved = module_path_for_span(span)
291 .or_else(current_module_path_opt)
292 .or_else(|| {
293 crate::callsite::source_info_for_span(span)
294 .is_none()
295 .then_some("crate".to_string())
296 });
297
298 resolved.ok_or_else(|| {
299 let message = format!(
300 "Internal error: could not resolve the module path for `{macro_name}` at this call site."
301 );
302 quote::quote_spanned! { span =>
303 compile_error!(#message);
304 }
305 .into()
306 })
307}