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}