1#[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#[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 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_state_enum(&enum_info);
98
99 let expanded = generate_state_impls(&enum_info);
101
102 TokenStream::from(expanded)
103}
104
105#[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 if let Some(error) = validate_machine_struct(&input, &machine_info) {
139 return error.into();
140 }
141
142 store_machine_struct(&machine_info);
144
145 let expanded = generate_machine_impls(&machine_info, &input);
147
148 TokenStream::from(expanded)
149}
150
151#[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 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 let expanded = generate_transition_impl(&input, &tr_impl, machine_info);
240
241 expanded.into()
244}
245
246#[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}