mod codegen;
mod parser;
mod validation;
use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, Expr, Lit, Meta, MetaNameValue, parse_macro_input};
#[proc_macro_attribute]
pub fn finitomata(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let struct_name = input.ident.clone();
let attr_args = match parse_finitomata_attrs(attr) {
Ok(args) => args,
Err(err) => {
return syn::Error::new(proc_macro2::Span::call_site(), err)
.to_compile_error()
.into();
}
};
let fsm_def = match &attr_args.syntax {
Syntax::Mermaid => parser::parse_mermaid(&attr_args.fsm),
Syntax::PlantUml => parser::parse_plantuml(&attr_args.fsm),
};
let fsm = match fsm_def {
Ok(fsm) => fsm,
Err(err) => {
return syn::Error::new(
proc_macro2::Span::call_site(),
format!("FSM parse error: {err}"),
)
.to_compile_error()
.into();
}
};
if let Err(errors) = validation::validate(&fsm) {
let msg = errors.join("; ");
return syn::Error::new(
proc_macro2::Span::call_site(),
format!("FSM validation error: {msg}"),
)
.to_compile_error()
.into();
}
let config = codegen::CodegenConfig {
struct_name: struct_name.clone(),
timer: attr_args.timer,
auto_terminate: attr_args.auto_terminate,
cache_state: attr_args.cache_state,
};
let generated = codegen::generate(&fsm, &config);
let expanded = quote! {
#input
#generated
};
expanded.into()
}
#[derive(Debug, Clone, Copy)]
enum Syntax {
Mermaid,
PlantUml,
}
struct FinitomataAttrs {
fsm: String,
syntax: Syntax,
timer: Option<u64>,
auto_terminate: bool,
cache_state: bool,
}
struct AttrList(syn::punctuated::Punctuated<Meta, syn::Token![,]>);
impl syn::parse::Parse for AttrList {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
Ok(Self(syn::punctuated::Punctuated::parse_terminated(input)?))
}
}
fn parse_finitomata_attrs(attr: TokenStream) -> Result<FinitomataAttrs, String> {
let attr2: proc_macro2::TokenStream = attr.into();
let mut fsm = None;
let mut syntax = Syntax::Mermaid;
let mut timer = None;
let mut auto_terminate = false;
let mut cache_state = true;
let parsed: AttrList =
syn::parse2(attr2).map_err(|e| format!("failed to parse attributes: {e}"))?;
for meta in parsed.0 {
if let Meta::NameValue(MetaNameValue { path, value, .. }) = meta {
let key = path.get_ident().map(|i| i.to_string()).unwrap_or_default();
match key.as_str() {
"fsm" => {
if let Expr::Lit(expr_lit) = &value
&& let Lit::Str(lit) = &expr_lit.lit
{
fsm = Some(lit.value());
}
}
"syntax" => {
if let Expr::Lit(expr_lit) = &value
&& let Lit::Str(lit) = &expr_lit.lit
{
syntax = match lit.value().as_str() {
"plantuml" | "plant_uml" => Syntax::PlantUml,
_ => Syntax::Mermaid,
};
}
}
"timer" => {
if let Expr::Lit(expr_lit) = &value
&& let Lit::Int(lit) = &expr_lit.lit
{
timer = lit.base10_parse::<u64>().ok();
}
}
"auto_terminate" => {
if let Expr::Lit(expr_lit) = &value
&& let Lit::Bool(lit) = &expr_lit.lit
{
auto_terminate = lit.value;
}
}
"cache_state" => {
if let Expr::Lit(expr_lit) = &value
&& let Lit::Bool(lit) = &expr_lit.lit
{
cache_state = lit.value;
}
}
_ => {}
}
}
}
let fsm = fsm.ok_or_else(|| "missing required `fsm` attribute".to_string())?;
Ok(FinitomataAttrs {
fsm,
syntax,
timer,
auto_terminate,
cache_state,
})
}