arbiter_macros/
lib.rs

1extern crate proc_macro;
2extern crate quote;
3extern crate syn;
4
5use proc_macro::TokenStream;
6use quote::quote;
7use syn::{
8    parse::{Parse, ParseStream},
9    parse_macro_input, Data, DataEnum, DeriveInput, Fields, Ident, ItemFn, Lit,
10    Result as ParseResult, Type,
11};
12
13/// A procedural macro to derive the `Behaviors` trait for enums.
14///
15/// This macro generates an implementation of the `CreateStateMachine` trait for
16/// the specified enum, facilitating the creation of state machines from enum
17/// variants. It is designed to work exclusively with enums where each variant
18/// contains unnamed fields, ideally a single field that represents the
19/// state data for that variant.
20///
21/// # Panics
22/// The macro will panic if it is applied to anything other than an enum, or if
23/// any of the enum's variants do not contain exactly one unnamed field.
24///
25/// # Usage
26/// Attach this macro to an enum definition to automatically implement the
27/// `CreateStateMachine` trait for it. Each variant of the enum must contain a
28/// single unnamed field that implements the `StateMachine` trait.
29///
30/// ```ignore
31/// #[derive(Behaviors)]
32/// enum MyBehavior {
33///     StateOne(StateDataOne),
34///     StateTwo(StateDataTwo),
35/// }
36/// ```
37#[proc_macro_derive(Behaviors)]
38pub fn create_behavior_from_enum(input: TokenStream) -> TokenStream {
39    // Parse the input TokenStream into a DeriveInput object.
40    let input = parse_macro_input!(input as DeriveInput);
41
42    // Extract the identifier (name) of the enum.
43    let name = input.ident;
44
45    // Attempt to extract enum data, panicking if the input is not an enum.
46    let enum_data = if let Data::Enum(DataEnum { variants, .. }) = input.data {
47        variants
48    } else {
49        panic!("CreateBehaviorFromEnum is only defined for enums");
50    };
51
52    // Generate match arms for the `create_state_machine` function, one for each
53    // enum variant.
54    let match_arms = enum_data.into_iter().map(|variant| {
55        // Extract the variant name and the type of its single unnamed field.
56        let variant_name = variant.ident;
57        let _inner_type = if let Fields::Unnamed(fields) = variant.fields {
58            fields.unnamed.first().unwrap().ty.clone()
59        } else {
60            panic!("Expected unnamed fields in enum variant");
61        };
62
63        // Generate a match arm that constructs a new state machine instance for the
64        // variant.
65        quote! {
66            #name::#variant_name(inner) => {
67                Box::new(Engine::new(inner))
68            }
69        }
70    });
71
72    // Generate the full implementation of the `CreateStateMachine` trait for the
73    // enum.
74    let expanded = quote! {
75        impl CreateStateMachine for #name {
76            fn create_state_machine(self) -> Box<dyn StateMachine> {
77                match self {
78                    #(#match_arms,)*
79                }
80            }
81        }
82    };
83
84    // Convert the generated code back into a TokenStream to be returned from the
85    // macro.
86    TokenStream::from(expanded)
87}
88
89/// `MacroArgs` is a struct designed to capture and store the attributes
90/// provided to our custom macro. It specifically targets the parsing of `name`,
91/// `about`, and `behaviors` attributes, which are essential for configuring the
92/// behavior of the macro in a more dynamic and descriptive manner.
93///
94/// # Fields
95/// - `name`: A `String` representing the name attribute of the macro.
96/// - `about`: A `String` providing a brief description about the macro's
97///   purpose or usage.
98/// - `behaviors`: A `Type` indicating the type of behaviors that the macro will
99///   generate or manipulate.
100struct MacroArgs {
101    name: String,
102    about: String,
103    behaviors: Type,
104}
105
106/// Implements the `Parse` trait for `MacroArgs`.
107///
108/// This implementation is responsible for parsing the input `TokenStream` to
109/// extract macro arguments into a `MacroArgs` struct. It specifically looks for
110/// `name`, `about`, and `behaviors` fields within the input stream, parsing and
111/// assigning them appropriately.
112///
113/// # Arguments
114/// * `input` - A `ParseStream` containing the input tokens to be parsed.
115///
116/// # Returns
117/// * `ParseResult<Self>` - A result containing `MacroArgs` if parsing succeeds,
118///   or an error if it fails.
119impl Parse for MacroArgs {
120    fn parse(input: ParseStream) -> ParseResult<Self> {
121        // Initialize variables to store parsed values.
122        let mut name = String::new();
123        let mut about = String::new();
124        let mut behaviors: Option<Type> = None;
125
126        // Iterate through the input stream until it's empty.
127        while !input.is_empty() {
128            // Look ahead in the input stream to determine the next token type.
129            let lookahead = input.lookahead1();
130            if lookahead.peek(Ident) {
131                // Parse an identifier and an equals token.
132                let ident: Ident = input.parse()?;
133                let _eq_token: syn::token::Eq = input.parse()?;
134                // Match the identifier to known fields and parse their values.
135                if ident == "name" {
136                    if let Lit::Str(lit_str) = input.parse()? {
137                        name = lit_str.value();
138                    }
139                } else if ident == "about" {
140                    if let Lit::Str(lit_str) = input.parse()? {
141                        about = lit_str.value();
142                    }
143                } else if ident == "behaviors" {
144                    behaviors = Some(input.parse()?);
145                } else {
146                    // Return an error if the identifier is not recognized.
147                    return Err(lookahead.error());
148                }
149            } else {
150                // Return an error if the lookahead does not match an identifier.
151                return Err(lookahead.error());
152            }
153
154            // Parse a comma separator if the input stream is not empty.
155            if !input.is_empty() {
156                let _: syn::token::Comma = input.parse()?;
157            }
158        }
159
160        // Ensure `behaviors` is not None, returning an error if it is missing.
161        let behaviors = behaviors.ok_or_else(|| input.error("missing behaviors"))?;
162
163        // Return the parsed `MacroArgs`.
164        Ok(MacroArgs {
165            name,
166            about,
167            behaviors,
168        })
169    }
170}
171
172/// A procedural macro attribute to generate a main function with async support
173/// and CLI parsing.
174///
175/// This macro parses the provided attributes to configure the CLI application,
176/// including its name, version, and about information. It also sets up logging
177/// based on the verbosity level specified through CLI arguments.
178///
179/// The macro expects a specific structure of the input TokenStream, which
180/// should define the behavior of the application, particularly how it handles
181/// different commands specified through the CLI.
182///
183/// # Parameters
184/// - `attr`: TokenStream containing the macro attributes for configuring the
185///   CLI application.
186/// - `item`: TokenStream representing the input function, which contains the
187///   logic for the application's behavior based on the parsed CLI arguments.
188///
189/// # Returns
190/// A TokenStream that, when executed, will act as the main function of a CLI
191/// application. This includes setting up the CLI with `clap`, initializing
192/// logging with `tracing`, and executing the application logic based on the
193/// provided CLI arguments.
194///
195/// # Example
196/// ```ignore
197/// #[main(name = "my_app", about = "An example application")]
198/// fn app() {
199///     // Application logic here
200/// }
201/// ```
202#[proc_macro_attribute]
203pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
204    // Parse the macro attributes
205    let args: MacroArgs = syn::parse(attr).expect("Failed to parse macro arguments");
206
207    let name = args.name;
208    let about = args.about;
209    let behaviors = args.behaviors;
210
211    // Parse the input TokenStream for the function
212    let _input_fn = parse_macro_input!(item as ItemFn);
213
214    // Generate the CLI and logging setup code with async and tokio::main
215    let expanded = quote! {
216        #[tokio::main]
217        #[allow(unused_must_use)]
218        async fn main() -> Result<(), Box<dyn std::error::Error>> {
219            use clap::{Parser, Subcommand, ArgAction, CommandFactory};
220            use tracing::Level;
221            use arbiter_engine::world::World;
222
223            #[derive(Parser)]
224            #[clap(name = #name)]
225            #[clap(version = env!("CARGO_PKG_VERSION"))]
226            #[clap(about = #about, long_about = None)]
227            #[clap(author)]
228            struct Args {
229                #[command(subcommand)]
230                command: Option<Commands>,
231
232                #[clap(short, long, global = true, required = false, action = ArgAction::Count, value_parser = clap::value_parser!(u8))]
233                verbose: Option<u8>,
234            }
235
236            #[derive(Subcommand)]
237            enum Commands {
238                Simulate {
239                    #[clap(index = 1)]
240                    config_path: String,
241                },
242            }
243
244            let args = Args::parse();
245
246            let log_level = match args.verbose.unwrap_or(0) {
247                0 => Level::ERROR,
248                1 => Level::WARN,
249                2 => Level::INFO,
250                3 => Level::DEBUG,
251                _ => Level::TRACE,
252            };
253            tracing_subscriber::fmt().with_max_level(log_level).init();
254
255            match &args.command {
256                Some(Commands::Simulate { config_path }) => {
257                    println!("Simulating configuration: {}", config_path);
258                    let mut world = World::from_config::<#behaviors>(config_path)?;
259                    world.run().await?;
260                },
261                None => {
262                    // Handle displaying help message if no command is provided
263                    Args::command().print_help()?;
264                    println!(); // Ensure newline after help output
265                },
266            }
267
268            Ok(())
269        }
270    };
271
272    // Convert the generated code back into a TokenStream
273    TokenStream::from(expanded)
274}