Refactor(language_macros): Complete rewrite
This **should** have everything the other implementation had, but the api is implemented in a way, which is more in line with the expectation raised at the lua functions (being that they are only wrappers over the command api, and nothing more). Aside of that, this version is actually documented!
This commit is contained in:
parent
a3b49b17f4
commit
27ad48c5e9
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "lua_macros"
|
name = "language_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::{DeriveInput, Field, Type};
|
||||||
|
|
||||||
|
use super::{get_input_type_of_bare_fn_field, parse_derive_input_as_named_fields};
|
||||||
|
|
||||||
|
pub fn command_enum(input: &DeriveInput) -> TokenStream2 {
|
||||||
|
let named_fields = parse_derive_input_as_named_fields(input);
|
||||||
|
let fields: TokenStream2 = named_fields
|
||||||
|
.named
|
||||||
|
.iter()
|
||||||
|
.map(|field| turn_struct_fieled_to_enum(field))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Command {
|
||||||
|
#fields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn turn_struct_fieled_to_enum(field: &Field) -> TokenStream2 {
|
||||||
|
let field_name = format_ident!(
|
||||||
|
"{}",
|
||||||
|
field
|
||||||
|
.ident
|
||||||
|
.as_ref()
|
||||||
|
.expect("These are named fields, it should be Some(<name>)")
|
||||||
|
.to_string()
|
||||||
|
.from_case(Case::Snake)
|
||||||
|
.to_case(Case::Pascal)
|
||||||
|
);
|
||||||
|
|
||||||
|
let input_type: Option<Type> = get_input_type_of_bare_fn_field(field);
|
||||||
|
|
||||||
|
match input_type {
|
||||||
|
Some(input_type) => {
|
||||||
|
quote! {
|
||||||
|
#field_name(#input_type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
quote! {
|
||||||
|
#field_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::{DeriveInput, Field};
|
||||||
|
|
||||||
|
use crate::generate::parse_derive_input_as_named_fields;
|
||||||
|
|
||||||
|
pub fn generate_add_lua_functions_to_globals(input: &DeriveInput) -> TokenStream2 {
|
||||||
|
let named_fields = parse_derive_input_as_named_fields(input);
|
||||||
|
let function_adders: TokenStream2 = named_fields
|
||||||
|
.named
|
||||||
|
.iter()
|
||||||
|
.map(|field| generate_function_adder(field))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
pub fn add_lua_functions_to_globals(
|
||||||
|
lua: mlua::Lua,
|
||||||
|
tx: tokio::sync::mpsc::Sender<Event>,
|
||||||
|
) -> mlua::Lua {
|
||||||
|
lua.set_app_data(tx);
|
||||||
|
let globals = lua.globals();
|
||||||
|
|
||||||
|
#function_adders
|
||||||
|
|
||||||
|
drop(globals);
|
||||||
|
lua
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_function_adder(field: &Field) -> TokenStream2 {
|
||||||
|
let field_ident = field
|
||||||
|
.ident
|
||||||
|
.as_ref()
|
||||||
|
.expect("This is should be a named field");
|
||||||
|
|
||||||
|
let function_ident = format_ident!("wrapped_lua_function_{}", field_ident);
|
||||||
|
let function_name = field_ident.to_string();
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
{
|
||||||
|
let #function_ident = lua.create_async_function(#field_ident).expect(
|
||||||
|
&format!(
|
||||||
|
"The function: `{}` should be defined",
|
||||||
|
#function_name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
globals.set(#function_name, #function_ident).expect(
|
||||||
|
"Setting a static global value should work"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::DeriveInput;
|
||||||
|
|
||||||
|
use crate::generate::lua_wrapper::{
|
||||||
|
lua_functions_to_globals::generate_add_lua_functions_to_globals,
|
||||||
|
rust_wrapper_functions::generate_rust_wrapper_functions,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod lua_functions_to_globals;
|
||||||
|
mod rust_wrapper_functions;
|
||||||
|
|
||||||
|
pub fn lua_wrapper(input: &DeriveInput) -> TokenStream2 {
|
||||||
|
let add_lua_functions_to_globals = generate_add_lua_functions_to_globals(input);
|
||||||
|
let rust_wrapper_functions = generate_rust_wrapper_functions(input);
|
||||||
|
quote! {
|
||||||
|
#add_lua_functions_to_globals
|
||||||
|
#rust_wrapper_functions
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::{
|
||||||
|
punctuated::Punctuated, token::Comma, DeriveInput, Field, GenericArgument, Lifetime, Type,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::generate::{
|
||||||
|
get_input_type_of_bare_fn_field, get_return_type_of_bare_fn_field,
|
||||||
|
parse_derive_input_as_named_fields,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn generate_rust_wrapper_functions(input: &DeriveInput) -> TokenStream2 {
|
||||||
|
let named_fields = parse_derive_input_as_named_fields(input);
|
||||||
|
let wrapped_functions: TokenStream2 = named_fields
|
||||||
|
.named
|
||||||
|
.iter()
|
||||||
|
.map(|field| wrap_lua_function(field))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#wrapped_functions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrap_lua_function(field: &Field) -> TokenStream2 {
|
||||||
|
let input_type = get_input_type_of_bare_fn_field(field);
|
||||||
|
let return_type = get_return_type_of_bare_fn_field(field);
|
||||||
|
|
||||||
|
let function_name = field.ident.as_ref().expect("This should be a named field");
|
||||||
|
let function_body = get_function_body(field, input_type.is_some(), &return_type);
|
||||||
|
|
||||||
|
let lifetime_args =
|
||||||
|
get_and_add_lifetimes_form_inputs_and_outputs(input_type.clone(), return_type);
|
||||||
|
|
||||||
|
let input_type = input_type
|
||||||
|
.unwrap_or(syn::parse(quote! {()}.into()).expect("This is static, it always works"));
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
async fn #function_name <#lifetime_args>(
|
||||||
|
lua: &mlua::Lua,
|
||||||
|
input: #input_type
|
||||||
|
) -> Result<mlua::Value, mlua::Error> {
|
||||||
|
#function_body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_and_add_lifetimes_form_inputs_and_outputs<'a>(
|
||||||
|
input_type: Option<syn::Type>,
|
||||||
|
return_type: Option<syn::Type>,
|
||||||
|
) -> Punctuated<Lifetime, Comma> {
|
||||||
|
fn get_lifetime_args_from_type<'a>(return_type: syn::Type) -> Option<Vec<Lifetime>> {
|
||||||
|
match return_type {
|
||||||
|
syn::Type::Path(path) => {
|
||||||
|
let args_to_final_path_segment = &path
|
||||||
|
.path
|
||||||
|
.segments
|
||||||
|
.last()
|
||||||
|
.expect("The path should have a last segment")
|
||||||
|
.arguments;
|
||||||
|
match args_to_final_path_segment {
|
||||||
|
syn::PathArguments::None =>
|
||||||
|
/* We ignore this case */
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
syn::PathArguments::AngleBracketed(angle) => {
|
||||||
|
let lifetime_args: Vec<_> = angle
|
||||||
|
.args
|
||||||
|
.iter()
|
||||||
|
.filter_map(|arg| {
|
||||||
|
if let GenericArgument::Lifetime(lifetime) = arg {
|
||||||
|
Some(lifetime.to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
return Some(lifetime_args);
|
||||||
|
}
|
||||||
|
syn::PathArguments::Parenthesized(_) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut output: Punctuated<Lifetime, Comma> = Punctuated::new();
|
||||||
|
if let Some(input_type) = input_type {
|
||||||
|
let lifetime_args = get_lifetime_args_from_type(input_type).unwrap_or(vec![]);
|
||||||
|
lifetime_args.into_iter().for_each(|arg| output.push(arg));
|
||||||
|
}
|
||||||
|
if let Some(return_type) = return_type {
|
||||||
|
let lifetime_args = get_lifetime_args_from_type(return_type).unwrap_or(vec![]);
|
||||||
|
lifetime_args.into_iter().for_each(|arg| output.push(arg));
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_function_body(field: &Field, has_input: bool, output_type: &Option<Type>) -> TokenStream2 {
|
||||||
|
let command_name = field
|
||||||
|
.ident
|
||||||
|
.as_ref()
|
||||||
|
.expect("These are named fields, it should be Some(<name>)")
|
||||||
|
.to_string()
|
||||||
|
.from_case(Case::Snake)
|
||||||
|
.to_case(Case::Pascal);
|
||||||
|
let command_ident = format_ident!("{}", command_name);
|
||||||
|
|
||||||
|
let send_output = if has_input {
|
||||||
|
quote! {
|
||||||
|
Event::CommandEvent(
|
||||||
|
Command::#command_ident(input.clone()),
|
||||||
|
Some(callback_tx),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
Event::CommandEvent(
|
||||||
|
Command::#command_ident,
|
||||||
|
Some(callback_tx),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let function_return = if let Some(_) = output_type {
|
||||||
|
quote! {
|
||||||
|
let output: mlua::Value = lua.to_value(output).expect("This conversion should (indirectely) be checked at compile time");
|
||||||
|
return Ok(output);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let does_function_expect_output = if output_type.is_some() {
|
||||||
|
quote! {
|
||||||
|
// We didn't receive output but expected output. Raise an error to notify the lua code
|
||||||
|
// about it
|
||||||
|
return Err(mlua::Error::ExternalError(std::sync::Arc::new(
|
||||||
|
err
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
// We didn't receive output and didn't expect output. Everything went well!
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
let (callback_tx, callback_rx) = tokio::sync::oneshot::channel::<CommandTransferValue>();
|
||||||
|
let tx: core::cell::Ref<tokio::sync::mpsc::Sender<Event>> =
|
||||||
|
lua.app_data_ref().expect("This should exist, it was set before");
|
||||||
|
|
||||||
|
(*tx)
|
||||||
|
.send(#send_output)
|
||||||
|
.await
|
||||||
|
.expect("This should work, as the receiver is not dropped");
|
||||||
|
|
||||||
|
cli_log::info!("Sent CommandEvent: `{}`", #command_name);
|
||||||
|
|
||||||
|
match callback_rx.await {
|
||||||
|
Ok(output) => {
|
||||||
|
cli_log::info!(
|
||||||
|
"Lua function: `{}` returned output to lua: `{}`", #command_name, &output
|
||||||
|
);
|
||||||
|
#function_return
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
#does_function_expect_output
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
mod command_enum;
|
||||||
|
mod lua_wrapper;
|
||||||
|
|
||||||
|
pub use command_enum::command_enum;
|
||||||
|
pub use lua_wrapper::lua_wrapper;
|
||||||
|
use syn::{DeriveInput, Field, FieldsNamed, ReturnType, Type, TypeBareFn};
|
||||||
|
|
||||||
|
pub fn parse_derive_input_as_named_fields(input: &DeriveInput) -> FieldsNamed {
|
||||||
|
match &input.data {
|
||||||
|
syn::Data::Struct(input) => match &input.fields {
|
||||||
|
syn::Fields::Named(named_fields) => named_fields,
|
||||||
|
_ => unimplemented!("The macro only works for named fields (e.g.: `Name: Type`)"),
|
||||||
|
},
|
||||||
|
_ => unimplemented!("The macro only works for structs"),
|
||||||
|
}
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_bare_fn_input_type(function: &TypeBareFn) -> Option<Type> {
|
||||||
|
if function.inputs.len() == 1 {
|
||||||
|
Some(
|
||||||
|
function
|
||||||
|
.inputs
|
||||||
|
.first()
|
||||||
|
.expect("Only one element exists, we checked the length above")
|
||||||
|
.ty
|
||||||
|
.clone(),
|
||||||
|
)
|
||||||
|
} else if function.inputs.len() == 0 {
|
||||||
|
// No inputs, so we can't return a type
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
unreachable!(
|
||||||
|
"The Function can only take one or zero arguments.
|
||||||
|
Use a tuple `(arg1, arg2)` if you want more"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_input_type_of_bare_fn_field(field: &Field) -> Option<Type> {
|
||||||
|
match &field.ty {
|
||||||
|
syn::Type::BareFn(function) => get_bare_fn_input_type(&function),
|
||||||
|
_ => unimplemented!(
|
||||||
|
"Please specify the type as a bare fn type.
|
||||||
|
That is: `fn(<args>) -> <outputs>`"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn get_return_type_of_bare_fn_field(field: &Field) -> Option<Type> {
|
||||||
|
match &field.ty {
|
||||||
|
syn::Type::BareFn(function) => get_bare_fn_return_type(&function),
|
||||||
|
_ => unimplemented!(
|
||||||
|
"Please specify the type as a bare fn type.
|
||||||
|
That is: `fn(<args>) -> <outputs>`"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_bare_fn_return_type(function: &TypeBareFn) -> Option<Type> {
|
||||||
|
let return_path: &ReturnType = &function.output;
|
||||||
|
match return_path {
|
||||||
|
ReturnType::Default => None,
|
||||||
|
ReturnType::Type(_, return_type) => Some(*return_type.to_owned()),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::DeriveInput;
|
||||||
|
|
||||||
|
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 `Commands` struct.
|
||||||
|
/// The returned values will be returned directly to the lua context, this allows to nest functions.
|
||||||
|
///
|
||||||
|
/// For example this rust code:
|
||||||
|
/// ```rust
|
||||||
|
/// #[ci_command_enum]
|
||||||
|
/// struct Commands {
|
||||||
|
/// /// Greets the user
|
||||||
|
/// greet: fn(String) -> String,
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// results in this expanded code:
|
||||||
|
/// ```rust
|
||||||
|
/// #[derive(Debug)]
|
||||||
|
/// pub enum Command {
|
||||||
|
/// Greet(String),
|
||||||
|
/// }
|
||||||
|
/// pub fn add_lua_functions_to_globals(
|
||||||
|
/// lua: mlua::Lua,
|
||||||
|
/// tx: tokio::sync::mpsc::Sender<Event>,
|
||||||
|
/// ) -> 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<String, mlua::Error> {
|
||||||
|
/// let (callback_tx, callback_rx) = tokio::sync::oneshot::channel::<String>();
|
||||||
|
/// let tx: core::cell::Ref<tokio::sync::mpsc::Sender<Event>> = 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)));
|
||||||
|
/// }
|
||||||
|
/// };
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn ci_command_enum(_attrs: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
|
// Construct a representation of Rust code as a syntax tree
|
||||||
|
// that we can manipulate
|
||||||
|
let input: DeriveInput = syn::parse(input)
|
||||||
|
.expect("This should always be valid rust code, as it's extracted from direct code");
|
||||||
|
|
||||||
|
// Build the language wrappers
|
||||||
|
let lua_wrapper: TokenStream2 = generate::lua_wrapper(&input);
|
||||||
|
|
||||||
|
// Build the final enum
|
||||||
|
let command_enum = generate::command_enum(&input);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#command_enum
|
||||||
|
#lua_wrapper
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
|
@ -1,69 +0,0 @@
|
||||||
mod mark_as_ci_command;
|
|
||||||
mod struct_to_ci_enum;
|
|
||||||
|
|
||||||
use mark_as_ci_command::generate_final_function;
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
|
||||||
use quote::{format_ident, quote};
|
|
||||||
use struct_to_ci_enum::{generate_command_enum, generate_generate_ci_function, generate_help_function};
|
|
||||||
use syn::{self, parse_quote, parse_str, DeriveInput, FieldMutability, ItemFn, Token, Visibility};
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn turn_struct_to_ci_command_enum(_attrs: TokenStream, input: TokenStream) -> TokenStream {
|
|
||||||
// Construct a representation of Rust code as a syntax tree
|
|
||||||
// that we can manipulate
|
|
||||||
let mut input: DeriveInput = syn::parse(input).expect("This should always be valid rust code, as it's extracted from direct code");
|
|
||||||
|
|
||||||
let mut named_fields = match &input.data {
|
|
||||||
syn::Data::Struct(input) => match &input.fields {
|
|
||||||
syn::Fields::Named(named_fields) => named_fields,
|
|
||||||
_ => unimplemented!("The macro only works for named fields (e.g.: `Name: Type`)"),
|
|
||||||
},
|
|
||||||
_ => unimplemented!("The macro only works for structs"),
|
|
||||||
}
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
let attr_parsed = parse_quote! {
|
|
||||||
/// This is a help function
|
|
||||||
};
|
|
||||||
|
|
||||||
named_fields.named.push(syn::Field {
|
|
||||||
attrs: vec![attr_parsed],
|
|
||||||
// attrs: attr_parser
|
|
||||||
// .parse("#[doc = r\"This is a help function\"]".to_token_stream().into())
|
|
||||||
// .expect("See reason for other one"),
|
|
||||||
vis: Visibility::Inherited,
|
|
||||||
mutability: FieldMutability::None,
|
|
||||||
ident: Some(format_ident!("help")),
|
|
||||||
colon_token: Some(Token![:](Span::call_site())),
|
|
||||||
ty: parse_str("fn(Option<String>) -> String").expect("This is static and valid rust code"),
|
|
||||||
});
|
|
||||||
|
|
||||||
match &mut input.data {
|
|
||||||
syn::Data::Struct(input) => input.fields = syn::Fields::Named(named_fields.clone()),
|
|
||||||
_ => unreachable!("This was a DataStruct before"),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build the trait implementation
|
|
||||||
let generate_ci_function: TokenStream2 = generate_generate_ci_function(&input);
|
|
||||||
|
|
||||||
let command_enum = generate_command_enum(&named_fields);
|
|
||||||
|
|
||||||
let help_function = generate_help_function(&named_fields);
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
#command_enum
|
|
||||||
|
|
||||||
#generate_ci_function
|
|
||||||
|
|
||||||
//#help_function
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn ci_command(_attrs: TokenStream, input: TokenStream) -> TokenStream {
|
|
||||||
let mut input: ItemFn = syn::parse(input).expect("This should always be valid rust code, as it's extracted from direct code");
|
|
||||||
let output_function = generate_final_function(&mut input);
|
|
||||||
output_function.into()
|
|
||||||
}
|
|
|
@ -1,173 +0,0 @@
|
||||||
use convert_case::{Case, Casing};
|
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
|
||||||
use quote::{format_ident, quote, ToTokens};
|
|
||||||
use syn::{Block, Expr, ExprBlock, GenericArgument, ReturnType, Stmt, Type};
|
|
||||||
|
|
||||||
pub fn generate_final_function(input: &mut syn::ItemFn) -> TokenStream2 {
|
|
||||||
append_tx_send_code(input);
|
|
||||||
|
|
||||||
let output: TokenStream2 = syn::parse(input.into_token_stream().into())
|
|
||||||
.expect("This is generated from valid rust code, it should stay that way.");
|
|
||||||
|
|
||||||
output
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_tx_send_code(input: &mut syn::ItemFn) -> &mut syn::ItemFn {
|
|
||||||
let function_name_pascal = format_ident!(
|
|
||||||
"{}",
|
|
||||||
input
|
|
||||||
.sig
|
|
||||||
.ident
|
|
||||||
.clone()
|
|
||||||
.to_string()
|
|
||||||
.from_case(Case::Snake)
|
|
||||||
.to_case(Case::Pascal)
|
|
||||||
);
|
|
||||||
|
|
||||||
let tx_send = match &input.sig.output {
|
|
||||||
syn::ReturnType::Default => {
|
|
||||||
unreachable!("All functions should have a output of (Result<$type, rlua::Error>)");
|
|
||||||
}
|
|
||||||
syn::ReturnType::Type(_, ret_type) => {
|
|
||||||
let return_type = match *(ret_type.clone()) {
|
|
||||||
syn::Type::Path(path) => {
|
|
||||||
match path
|
|
||||||
.path
|
|
||||||
.segments
|
|
||||||
.first()
|
|
||||||
.expect("This is expected to be only one path segment")
|
|
||||||
.arguments
|
|
||||||
.to_owned()
|
|
||||||
{
|
|
||||||
syn::PathArguments::AngleBracketed(angled_path) => {
|
|
||||||
let angled_path = angled_path.args.to_owned();
|
|
||||||
let filtered_paths: Vec<_> = angled_path
|
|
||||||
.into_iter()
|
|
||||||
.filter(|generic_arg| {
|
|
||||||
if let GenericArgument::Type(generic_type) = generic_arg {
|
|
||||||
if let Type::Path(_) = generic_type {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// There should only be two segments (the type is <String, rlua::Error>)
|
|
||||||
if filtered_paths.len() > 2 {
|
|
||||||
unreachable!("There should be no more than two filtered_output, but got: {:#?}", filtered_paths)
|
|
||||||
} else if filtered_paths.len() <= 0 {
|
|
||||||
unreachable!("There should be more than zero filtered_output, but got: {:#?}", filtered_paths)
|
|
||||||
}
|
|
||||||
|
|
||||||
if filtered_paths.len() == 2 {
|
|
||||||
// There is something else than mlua::Error
|
|
||||||
let gen_type = if let GenericArgument::Type(ret_type) =
|
|
||||||
filtered_paths
|
|
||||||
.first()
|
|
||||||
.expect("One path segment should exists")
|
|
||||||
.to_owned()
|
|
||||||
{
|
|
||||||
ret_type
|
|
||||||
} else {
|
|
||||||
unreachable!("These were filtered above.");
|
|
||||||
};
|
|
||||||
let return_type_as_type_prepared = quote! {-> #gen_type};
|
|
||||||
|
|
||||||
let return_type_as_return_type: ReturnType = syn::parse(
|
|
||||||
return_type_as_type_prepared.to_token_stream().into(),
|
|
||||||
)
|
|
||||||
.expect("This is valid.");
|
|
||||||
return_type_as_return_type
|
|
||||||
} else {
|
|
||||||
// There is only mlua::Error left
|
|
||||||
ReturnType::Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => unimplemented!("Only for angled paths"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => unimplemented!("Only for path types"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let send_data = match return_type {
|
|
||||||
ReturnType::Default => {
|
|
||||||
quote! {
|
|
||||||
{
|
|
||||||
Event::CommandEvent(Command::#function_name_pascal, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ReturnType::Type(_, _) => {
|
|
||||||
quote! {
|
|
||||||
{
|
|
||||||
Event::CommandEvent(Command::#function_name_pascal(input.clone()), Some(callback_tx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let output_return = match return_type {
|
|
||||||
ReturnType::Default => {
|
|
||||||
quote! {
|
|
||||||
{
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ReturnType::Type(_, _) => {
|
|
||||||
quote! {
|
|
||||||
{
|
|
||||||
if let Some(output) = callback_rx.recv().await {
|
|
||||||
callback_rx.close();
|
|
||||||
return Ok(output);
|
|
||||||
} else {
|
|
||||||
return Err(mlua::Error::ExternalError(Arc::new(Error::new(
|
|
||||||
ErrorKind::Other,
|
|
||||||
"Callback reciever dropped",
|
|
||||||
))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
{
|
|
||||||
let (callback_tx, mut callback_rx) = tokio::sync::mpsc::channel::<String>(256);
|
|
||||||
|
|
||||||
let tx:
|
|
||||||
core::cell::Ref<tokio::sync::mpsc::Sender<crate::app::events::event_types::Event>> =
|
|
||||||
lua
|
|
||||||
.app_data_ref()
|
|
||||||
.expect("This exists, it was set before");
|
|
||||||
|
|
||||||
(*tx)
|
|
||||||
.try_send(#send_data)
|
|
||||||
.expect("This should work, as the reciever is not dropped");
|
|
||||||
|
|
||||||
#output_return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let tx_send_block: Block =
|
|
||||||
syn::parse(tx_send.into()).expect("This is a static string, it will always parse");
|
|
||||||
|
|
||||||
let tx_send_expr_block = ExprBlock {
|
|
||||||
attrs: vec![],
|
|
||||||
label: None,
|
|
||||||
block: tx_send_block,
|
|
||||||
};
|
|
||||||
let mut tx_send_stmt = vec![Stmt::Expr(Expr::Block(tx_send_expr_block), None)];
|
|
||||||
|
|
||||||
let mut new_stmts: Vec<Stmt> = Vec::with_capacity(input.block.stmts.len() + 1);
|
|
||||||
new_stmts.append(&mut tx_send_stmt);
|
|
||||||
new_stmts.append(&mut input.block.stmts);
|
|
||||||
input.block.stmts = new_stmts;
|
|
||||||
input
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
use convert_case::{Case, Casing};
|
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
|
||||||
use quote::{format_ident, quote};
|
|
||||||
use syn::Type;
|
|
||||||
|
|
||||||
pub fn generate_command_enum(input: &syn::FieldsNamed) -> TokenStream2 {
|
|
||||||
let input_tokens: TokenStream2 = input
|
|
||||||
.named
|
|
||||||
.iter()
|
|
||||||
.map(|field| -> TokenStream2 {
|
|
||||||
let field_ident = field
|
|
||||||
.ident
|
|
||||||
.as_ref()
|
|
||||||
.expect("These are only the named fields, thus they should all have a ident.");
|
|
||||||
|
|
||||||
let enum_variant_type = match &field.ty {
|
|
||||||
syn::Type::BareFn(function) => {
|
|
||||||
let return_path = &function.inputs;
|
|
||||||
|
|
||||||
let input_type: Option<Type> = if return_path.len() == 1 {
|
|
||||||
Some(
|
|
||||||
return_path
|
|
||||||
.last()
|
|
||||||
.expect("The last element exists")
|
|
||||||
.ty
|
|
||||||
.clone(),
|
|
||||||
)
|
|
||||||
} else if return_path.len() == 0 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
panic!("The Function can only take on argument, or none");
|
|
||||||
};
|
|
||||||
input_type
|
|
||||||
}
|
|
||||||
_ => unimplemented!("This is only implemented for bare function types"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let enum_variant_name = format_ident!(
|
|
||||||
"{}",
|
|
||||||
field_ident
|
|
||||||
.to_string()
|
|
||||||
.from_case(Case::Snake)
|
|
||||||
.to_case(Case::Pascal)
|
|
||||||
);
|
|
||||||
if enum_variant_type.is_some() {
|
|
||||||
quote! {
|
|
||||||
#enum_variant_name (#enum_variant_type),
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
quote! {
|
|
||||||
#enum_variant_name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let gen = quote! {
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Command {
|
|
||||||
#input_tokens
|
|
||||||
}
|
|
||||||
};
|
|
||||||
gen.into()
|
|
||||||
}
|
|
|
@ -1,105 +0,0 @@
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
|
||||||
use quote::{format_ident, quote};
|
|
||||||
use syn::{parse_quote, ReturnType, Type};
|
|
||||||
|
|
||||||
fn generate_ci_function_exposure(field: &syn::Field) -> TokenStream2 {
|
|
||||||
let field_ident = field
|
|
||||||
.ident
|
|
||||||
.as_ref()
|
|
||||||
.expect("These are only the named field, thus they all should have a name.");
|
|
||||||
|
|
||||||
let function_name_ident = format_ident!("fun_{}", field_ident);
|
|
||||||
let function_name = format!("{}", field_ident);
|
|
||||||
quote! {
|
|
||||||
let #function_name_ident = lua.create_async_function(#field_ident).expect(
|
|
||||||
&format!(
|
|
||||||
"The function: `{}` should be defined",
|
|
||||||
#function_name
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
globals.set(#function_name, #function_name_ident).expect(
|
|
||||||
&format!(
|
|
||||||
"Setting a static global value ({}, fun_{}) should work",
|
|
||||||
#function_name,
|
|
||||||
#function_name
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_generate_ci_function(input: &syn::DeriveInput) -> TokenStream2 {
|
|
||||||
let mut functions_to_generate: Vec<TokenStream2> = vec![];
|
|
||||||
|
|
||||||
let functions_to_export_in_lua: TokenStream2 = match &input.data {
|
|
||||||
syn::Data::Struct(input) => match &input.fields {
|
|
||||||
syn::Fields::Named(named_fields) => named_fields
|
|
||||||
.named
|
|
||||||
.iter()
|
|
||||||
.map(|field| -> TokenStream2 {
|
|
||||||
let input_type = match &field.ty {
|
|
||||||
syn::Type::BareFn(bare_fn) => {
|
|
||||||
if bare_fn.inputs.len() == 1 {
|
|
||||||
bare_fn.inputs.last().expect("The last element exists").ty.clone()
|
|
||||||
} else if bare_fn.inputs.len() == 0 {
|
|
||||||
let input_type: Type = parse_quote! {()};
|
|
||||||
input_type
|
|
||||||
} else {
|
|
||||||
panic!("The Function can only take on argument, or none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => unimplemented!("This is only implemented for bare function types"),
|
|
||||||
};
|
|
||||||
let return_type = match &field.ty {
|
|
||||||
syn::Type::BareFn(function) => {
|
|
||||||
let return_path: &ReturnType = &function.output;
|
|
||||||
match return_path {
|
|
||||||
ReturnType::Default => None,
|
|
||||||
ReturnType::Type(_, return_type) => Some(return_type.to_owned()) }
|
|
||||||
}
|
|
||||||
_ => unimplemented!("This is only implemented for bare function types"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let function_name = field
|
|
||||||
.ident
|
|
||||||
.as_ref()
|
|
||||||
.expect("These are only the named field, thus they all should have a name.");
|
|
||||||
|
|
||||||
if let Some(ret_type) = return_type {
|
|
||||||
functions_to_generate.push(quote! {
|
|
||||||
#[ci_command]
|
|
||||||
async fn #function_name(lua: &mlua::Lua, input: #input_type) -> Result<#ret_type, mlua::Error> {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
functions_to_generate.push(quote! {
|
|
||||||
#[ci_command]
|
|
||||||
async fn #function_name(lua: &mlua::Lua, input: #input_type) -> Result<(), mlua::Error> {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
generate_ci_function_exposure(field)
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
|
|
||||||
_ => unimplemented!("Only implemented for named fileds"),
|
|
||||||
},
|
|
||||||
_ => unimplemented!("Only implemented for structs"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let functions_to_generate: TokenStream2 = functions_to_generate.into_iter().collect();
|
|
||||||
let gen = quote! {
|
|
||||||
pub fn generate_ci_functions(
|
|
||||||
lua: &mut mlua::Lua,
|
|
||||||
tx: tokio::sync::mpsc::Sender<crate::app::events::event_types::Event>
|
|
||||||
)
|
|
||||||
{
|
|
||||||
lua.set_app_data(tx);
|
|
||||||
let globals = lua.globals();
|
|
||||||
#functions_to_export_in_lua
|
|
||||||
}
|
|
||||||
#functions_to_generate
|
|
||||||
};
|
|
||||||
gen.into()
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
|
||||||
use quote::{quote, ToTokens};
|
|
||||||
|
|
||||||
pub fn generate_help_function(input: &syn::FieldsNamed) -> TokenStream2 {
|
|
||||||
let input: Vec<_> = input.named.iter().collect();
|
|
||||||
|
|
||||||
let combined_help_text: TokenStream2 = input
|
|
||||||
.iter()
|
|
||||||
.map(|field| {
|
|
||||||
let attrs_with_doc: Vec<TokenStream2> = field
|
|
||||||
.attrs
|
|
||||||
.iter()
|
|
||||||
.filter_map(|attr| {
|
|
||||||
if attr.path().is_ident("doc") {
|
|
||||||
let help_text = attr
|
|
||||||
.meta
|
|
||||||
.require_name_value()
|
|
||||||
.expect("This is a named value type, because all doc comments work this way")
|
|
||||||
.value
|
|
||||||
.clone();
|
|
||||||
Some(help_text.into_token_stream().into())
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
if attrs_with_doc.len() == 0 {
|
|
||||||
// TODO there should be a better panic function, than the generic one
|
|
||||||
panic!(
|
|
||||||
"The command named: `{}`, does not provide a help message",
|
|
||||||
field.ident.as_ref().expect("These are all named")
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let help_text_for_one_command_combined: TokenStream2 = attrs_with_doc.into_iter().collect();
|
|
||||||
return help_text_for_one_command_combined;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
#[ci_command]
|
|
||||||
async fn help(
|
|
||||||
lua: &mlua::Lua,
|
|
||||||
input_str: Option<String>
|
|
||||||
) -> Result<String, mlua::Error> {
|
|
||||||
// TODO add a way to filter the help based on the input
|
|
||||||
|
|
||||||
let output = "These functions exist:\n";
|
|
||||||
output.push_str(#combined_help_text);
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
pub mod generate_command_enum;
|
|
||||||
pub mod generate_generate_ci_function;
|
|
||||||
pub mod generate_help_function;
|
|
||||||
|
|
||||||
pub use generate_command_enum::*;
|
|
||||||
pub use generate_generate_ci_function::*;
|
|
||||||
pub use generate_help_function::*;
|
|
Reference in New Issue