use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::{ braced, parse::Parse, parse_macro_input, punctuated::Punctuated, token, Attribute, Ident, Token, Type, }; mod generate; /// This is the heart of the command api /// It mainly does two things: /// - Generate a command enum /// - Wrap the enum in all supported languages (only lua for now) /// - Generate wrapper lua function for each command /// - Generate a `add_lua_functions_to_globals` function, which adds /// the rust wrapper functions to the lua globals. /// /// The input and output values of the wrapped functions are derived from the values specified in /// the input to the `parse_command_enum` proc macro. /// /// For example this rust code: /// ```no_run /// parse_command_enum! { /// /// Greets the user /// greet: fn(String) -> String, /// } /// ``` /// results in this expanded code: /// ```no_run /// #[derive(Debug)] /// pub enum Command { /// Greet(String), /// } /// pub fn add_lua_functions_to_globals( /// lua: mlua::Lua, /// tx: tokio::sync::mpsc::Sender, /// ) -> mlua::Lua { /// lua.set_app_data(tx); /// let globals = lua.globals(); /// { /// let wrapped_lua_function_greet = lua /// .create_async_function(greet) /// .expect( /// format!( /// "The function: `{}` should be defined", /// "greet", /// ) /// ); /// globals /// .set("greet", wrapped_lua_function_greet) /// .expect("Setting a static global value should work"); /// } /// drop(globals); /// lua /// } /// async fn greet(lua: &mlua::Lua, input: String) -> Result { /// let (callback_tx, callback_rx) = tokio::sync::oneshot::channel::(); /// let tx: core::cell::Ref> = lua /// .app_data_ref() /// .expect("This should exist, it was set before"); /// (*tx) /// .send(Event::CommandEvent(Command::Greet(input.clone()), Some(callback_tx))) /// .await /// .expect("This should work, as the receiver is not dropped"); /// match callback_rx.await { /// Ok(output) => { /// return Ok(output); /// } /// Err(err) => { /// return Err(mlua::Error::ExternalError(std::sync::Arc::new(err))); /// } /// }; /// } /// ``` #[derive(Debug)] struct DataCommandEnum { #[allow(dead_code)] commands_token: kw::commands, #[allow(dead_code)] brace_token: token::Brace, fields: Punctuated, } mod kw { syn::custom_keyword!(commands); syn::custom_keyword!(namespace); syn::custom_keyword!(declare); } #[derive(Debug)] enum Field { Function(FunctionDeclaration), Namespace(Namespace), } #[derive(Debug)] struct Namespace { #[allow(dead_code)] namespace_token: kw::namespace, path: NamespacePath, #[allow(dead_code)] brace_token: token::Brace, fields: Punctuated, } type NamespacePath = Punctuated; #[derive(Debug)] struct FunctionDeclaration { #[allow(dead_code)] function_token: kw::declare, name: Ident, #[allow(dead_code)] colon_token: Token![:], ty: Type, } impl Parse for DataCommandEnum { fn parse(input: syn::parse::ParseStream) -> syn::Result { let content; Ok(DataCommandEnum { commands_token: input.parse()?, brace_token: braced!(content in input), fields: content.parse_terminated(Field::parse, Token![,])?, }) } } impl Parse for Field { fn parse(input: syn::parse::ParseStream) -> syn::Result { let lookahead = input.lookahead1(); if input.peek(Token![#]) { // FIXME(@soispha): We ignore doc comments, which should probably be replaced by adding // them to the output <2023-09-19> let _output = input.call(Attribute::parse_outer).unwrap_or(vec![]); let lookahead = input.lookahead1(); if lookahead.peek(kw::namespace) { input.parse().map(Field::Namespace) } else if lookahead.peek(kw::declare) { input.parse().map(Field::Function) } else { Err(lookahead.error()) } } else { if lookahead.peek(kw::declare) { input.parse().map(Field::Function) } else if lookahead.peek(kw::namespace) { input.parse().map(Field::Namespace) } else { Err(lookahead.error()) } } } } impl Parse for FunctionDeclaration { fn parse(input: syn::parse::ParseStream) -> syn::Result { Ok(FunctionDeclaration { function_token: input.parse()?, name: input.parse()?, colon_token: input.parse()?, ty: input.parse()?, }) } } impl Parse for Namespace { fn parse(input: syn::parse::ParseStream) -> syn::Result { let content; Ok(Namespace { namespace_token: input.parse()?, path: NamespacePath::parse_separated_nonempty(input)?, brace_token: braced!(content in input), fields: content.parse_terminated(Field::parse, Token![,])?, }) } } #[proc_macro] pub fn parse_command_enum(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DataCommandEnum); // Build the language wrappers let lua_wrapper: TokenStream2 = generate::lua_wrapper(&input); // Build the final enum let command_enum = generate::command_enum(&input); let output = quote! { #command_enum #lua_wrapper }; output.into() }