1mod codegen;
2mod parser;
3mod validation;
4
5use proc_macro::TokenStream;
6use quote::quote;
7use syn::{DeriveInput, Expr, Lit, Meta, MetaNameValue, parse_macro_input};
8
9#[proc_macro_attribute]
32pub fn finitomata(attr: TokenStream, item: TokenStream) -> TokenStream {
33 let input = parse_macro_input!(item as DeriveInput);
34 let struct_name = input.ident.clone();
35
36 let attr_args = match parse_finitomata_attrs(attr) {
38 Ok(args) => args,
39 Err(err) => {
40 return syn::Error::new(proc_macro2::Span::call_site(), err)
41 .to_compile_error()
42 .into();
43 }
44 };
45
46 let fsm_def = match &attr_args.syntax {
48 Syntax::Mermaid => parser::parse_mermaid(&attr_args.fsm),
49 Syntax::PlantUml => parser::parse_plantuml(&attr_args.fsm),
50 };
51
52 let fsm = match fsm_def {
53 Ok(fsm) => fsm,
54 Err(err) => {
55 return syn::Error::new(
56 proc_macro2::Span::call_site(),
57 format!("FSM parse error: {err}"),
58 )
59 .to_compile_error()
60 .into();
61 }
62 };
63
64 if let Err(errors) = validation::validate(&fsm) {
66 let msg = errors.join("; ");
67 return syn::Error::new(
68 proc_macro2::Span::call_site(),
69 format!("FSM validation error: {msg}"),
70 )
71 .to_compile_error()
72 .into();
73 }
74
75 let config = codegen::CodegenConfig {
77 struct_name: struct_name.clone(),
78 timer: attr_args.timer,
79 auto_terminate: attr_args.auto_terminate,
80 cache_state: attr_args.cache_state,
81 };
82
83 let generated = codegen::generate(&fsm, &config);
84
85 let expanded = quote! {
86 #input
87
88 #generated
89 };
90
91 expanded.into()
92}
93
94#[derive(Debug, Clone, Copy)]
95enum Syntax {
96 Mermaid,
97 PlantUml,
98}
99
100struct FinitomataAttrs {
101 fsm: String,
102 syntax: Syntax,
103 timer: Option<u64>,
104 auto_terminate: bool,
105 cache_state: bool,
106}
107
108struct AttrList(syn::punctuated::Punctuated<Meta, syn::Token![,]>);
109
110impl syn::parse::Parse for AttrList {
111 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
112 Ok(Self(syn::punctuated::Punctuated::parse_terminated(input)?))
113 }
114}
115
116fn parse_finitomata_attrs(attr: TokenStream) -> Result<FinitomataAttrs, String> {
117 let attr2: proc_macro2::TokenStream = attr.into();
118
119 let mut fsm = None;
120 let mut syntax = Syntax::Mermaid;
121 let mut timer = None;
122 let mut auto_terminate = false;
123 let mut cache_state = true;
124
125 let parsed: AttrList =
126 syn::parse2(attr2).map_err(|e| format!("failed to parse attributes: {e}"))?;
127
128 for meta in parsed.0 {
129 if let Meta::NameValue(MetaNameValue { path, value, .. }) = meta {
130 let key = path.get_ident().map(|i| i.to_string()).unwrap_or_default();
131
132 match key.as_str() {
133 "fsm" => {
134 if let Expr::Lit(expr_lit) = &value
135 && let Lit::Str(lit) = &expr_lit.lit
136 {
137 fsm = Some(lit.value());
138 }
139 }
140 "syntax" => {
141 if let Expr::Lit(expr_lit) = &value
142 && let Lit::Str(lit) = &expr_lit.lit
143 {
144 syntax = match lit.value().as_str() {
145 "plantuml" | "plant_uml" => Syntax::PlantUml,
146 _ => Syntax::Mermaid,
147 };
148 }
149 }
150 "timer" => {
151 if let Expr::Lit(expr_lit) = &value
152 && let Lit::Int(lit) = &expr_lit.lit
153 {
154 timer = lit.base10_parse::<u64>().ok();
155 }
156 }
157 "auto_terminate" => {
158 if let Expr::Lit(expr_lit) = &value
159 && let Lit::Bool(lit) = &expr_lit.lit
160 {
161 auto_terminate = lit.value;
162 }
163 }
164 "cache_state" => {
165 if let Expr::Lit(expr_lit) = &value
166 && let Lit::Bool(lit) = &expr_lit.lit
167 {
168 cache_state = lit.value;
169 }
170 }
171 _ => {}
172 }
173 }
174 }
175
176 let fsm = fsm.ok_or_else(|| "missing required `fsm` attribute".to_string())?;
177
178 Ok(FinitomataAttrs {
179 fsm,
180 syntax,
181 timer,
182 auto_terminate,
183 cache_state,
184 })
185}