Skip to main content

fsmentry_macros/
lib.rs

1//! A code generator for state machines with an entry API.
2//!
3//! See the [`fsmentry` crate](https://docs.rs/fsmentry).
4
5use fsmentry_core::FSMGenerator;
6use quote::ToTokens as _;
7use syn::parse_macro_input;
8
9/// Generates a state machine from the following language:
10/// ```
11/// # use fsmentry_macros::dsl;
12/// dsl! {
13///     /// This is documentation for the state machine.
14///     #[derive(Clone)] // these attributes will be passed to
15///                      // MyStateMachine and the State enum
16///     pub MyStateMachine {
17///         /// This is a node declaration.
18///         /// This documentation will be attached to the node.
19///         ShavingYaks;
20///
21///         /// This node contains data.
22///         SweepingHair: usize;
23///
24///         /// These are edge declarations
25///         /// This documentation will be shared with each edge.
26///         ShavingYaks -> SweepingHair -"this is edge-specific documentation"-> Resting;
27///                             // implicit nodes will be created as appropriate ^
28///     }
29/// }
30/// ```
31#[proc_macro]
32pub fn dsl(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
33    let generator = parse_macro_input!(item with FSMGenerator::parse_dsl);
34    let codegen = generator.codegen();
35    #[cfg(feature = "svg")]
36    let codegen = svg::attach(codegen, &generator);
37    codegen.into_token_stream().into()
38}
39
40/// Generates a state machine from the [`DOT` graph description language](https://en.wikipedia.org/wiki/DOT_%28graph_description_language%29):
41/// ```
42/// # use fsmentry_macros::dot;
43/// dot! {
44///     digraph my_state_machine {
45///         // declaring a node.
46///         shaving_yaks;
47///         
48///         // declaring some edges, with implicit nodes.
49///         shaving_yaks -> sweeping_hair -> resting;
50///     }
51/// }
52/// ```
53#[proc_macro]
54pub fn dot(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
55    let generator = parse_macro_input!(item with FSMGenerator::parse_dot);
56    let codegen = generator.codegen();
57    #[cfg(feature = "svg")]
58    let codegen = svg::attach(codegen, &generator);
59    codegen.into_token_stream().into()
60}
61
62#[cfg(feature = "svg")]
63mod svg {
64    use quote::ToTokens as _;
65    use std::{
66        io::Write as _,
67        process::{Command, Stdio},
68    };
69    use syn::parse_quote;
70
71    pub fn attach(mut file: syn::File, generator: &fsmentry_core::FSMGenerator) -> syn::File {
72        let Some(syn::Item::Mod(syn::ItemMod { attrs, .. })) = file.items.first_mut() else {
73            unreachable!("the code generates a module")
74        };
75        if let Some(svg) = render_dot(generator) {
76            let svg = format!("<div>{}</div>", svg);
77            if !attrs.is_empty() {
78                attrs.push(parse_quote!(#[doc = ""]))
79            }
80            attrs.push(parse_quote!(#[doc = #svg]))
81        }
82        file
83    }
84
85    fn render_dot(generator: &fsmentry_core::FSMGenerator) -> Option<String> {
86        let mut child = Command::new("dot")
87            .arg("-Tsvg")
88            .stdin(Stdio::piped())
89            .stderr(Stdio::inherit())
90            .stdout(Stdio::piped())
91            .spawn()
92            .ok()?;
93        child
94            .stdin
95            .take()
96            .unwrap()
97            .write_all(generator.dot().to_token_stream().to_string().as_bytes())
98            .ok()?;
99        let output = child.wait_with_output().ok()?;
100        match output.status.success() {
101            true => String::from_utf8(output.stdout).ok(),
102            false => None,
103        }
104    }
105}