Fix(lua_macros): Rework to support new command focused layout
The internal commands are wrapped by a lua api, which allows to write the whole thing a little bit more language agnostic.
This commit is contained in:
parent
27e3ff228c
commit
c243c90cab
|
@ -3,61 +3,67 @@ mod struct_to_ci_enum;
|
|||
|
||||
use mark_as_ci_command::generate_final_function;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
use struct_to_ci_enum::{generate_command_enum, generate_generate_ci_function};
|
||||
use syn::{self, ItemFn, Field, parse::Parser};
|
||||
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_commands(_attrs: TokenStream, input: TokenStream) -> TokenStream {
|
||||
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 input = syn::parse(input)
|
||||
.expect("This should always be valid rust code, as it's extracted from direct code");
|
||||
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(&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()
|
||||
}
|
||||
|
||||
/// Generate a default lua function implementation.
|
||||
// TODO: Is this needed?
|
||||
#[proc_macro_attribute]
|
||||
pub fn gen_lua_function(_attrs: TokenStream, input: TokenStream) -> TokenStream {
|
||||
// Construct a representation of Rust code as a syntax tree
|
||||
// that we can manipulate
|
||||
let parser = Field::parse_named;
|
||||
let input = parser.parse(input)
|
||||
.expect("This is only defined for named fileds.");
|
||||
|
||||
|
||||
quote! {
|
||||
#input
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Turn a function into a valid ci command function
|
||||
#[proc_macro_attribute]
|
||||
pub fn ci_command(_attrs: TokenStream, input: TokenStream) -> TokenStream {
|
||||
// Construct a representation of Rust code as a syntax tree
|
||||
// that we can manipulate
|
||||
let mut input: ItemFn = syn::parse(input)
|
||||
.expect("This should always be valid rust code, as it's extracted from direct code");
|
||||
|
||||
// Build the trait implementation
|
||||
let output_function: TokenStream2 = generate_final_function(&mut input);
|
||||
|
||||
//panic!("{:#?}", output_function);
|
||||
quote! {
|
||||
#output_function
|
||||
}
|
||||
.into()
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -58,18 +58,13 @@ fn append_tx_send_code(input: &mut syn::ItemFn) -> &mut syn::ItemFn {
|
|||
|
||||
// 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
|
||||
)
|
||||
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
|
||||
)
|
||||
unreachable!("There should be more than zero filtered_output, but got: {:#?}", filtered_paths)
|
||||
}
|
||||
|
||||
if filtered_paths.len() == 2 {
|
||||
// There is something else than rlua
|
||||
// There is something else than mlua::Error
|
||||
let gen_type = if let GenericArgument::Type(ret_type) =
|
||||
filtered_paths
|
||||
.first()
|
||||
|
@ -97,36 +92,64 @@ fn append_tx_send_code(input: &mut syn::ItemFn) -> &mut syn::ItemFn {
|
|||
}
|
||||
_ => unimplemented!("Only for path types"),
|
||||
};
|
||||
|
||||
let send_data = match return_type {
|
||||
ReturnType::Default => {
|
||||
quote! {
|
||||
{
|
||||
Event::CommandEvent(Command::#function_name_pascal)
|
||||
Event::CommandEvent(Command::#function_name_pascal, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
ReturnType::Type(_, _) => {
|
||||
quote! {
|
||||
{
|
||||
Event::CommandEvent(Command::#function_name_pascal(input_str.clone()))
|
||||
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>
|
||||
> =
|
||||
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)
|
||||
.send(#send_data)
|
||||
.await
|
||||
.try_send(#send_data)
|
||||
.expect("This should work, as the reciever is not dropped");
|
||||
|
||||
#output_return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -134,6 +157,7 @@ fn append_tx_send_code(input: &mut syn::ItemFn) -> &mut syn::ItemFn {
|
|||
|
||||
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,
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
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,63 +1,13 @@
|
|||
use convert_case::{Case, Casing};
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{self, ReturnType};
|
||||
use syn::{parse_quote, ReturnType, Type};
|
||||
|
||||
pub fn generate_generate_ci_function(input: &syn::DeriveInput) -> TokenStream2 {
|
||||
let mut functions_to_generate: Vec<TokenStream2> = vec![];
|
||||
let input_tokens: TokenStream2 = match &input.data {
|
||||
syn::Data::Struct(input) => match &input.fields {
|
||||
syn::Fields::Named(named_fields) => named_fields
|
||||
.named
|
||||
.iter()
|
||||
.map(|field| -> TokenStream2 {
|
||||
if field.attrs.iter().any(|attribute| {
|
||||
attribute.path()
|
||||
== &syn::parse_str::<syn::Path>("gen_default_lua_function")
|
||||
.expect("This is valid rust code")
|
||||
}) {
|
||||
let function_name = field
|
||||
.ident
|
||||
.as_ref()
|
||||
.expect("These are only the named field, thus they all should have a name.");
|
||||
functions_to_generate.push(quote! {
|
||||
#[ci_command]
|
||||
async fn #function_name(lua: &mlua::Lua, input_str: String) -> Result<(), mlua::Error> {
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
generate_ci_part(field)
|
||||
} else {
|
||||
generate_ci_part(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();
|
||||
#input_tokens
|
||||
}
|
||||
#functions_to_generate
|
||||
};
|
||||
gen.into()
|
||||
}
|
||||
|
||||
fn generate_ci_part(field: &syn::Field) -> TokenStream2 {
|
||||
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! {
|
||||
|
@ -79,62 +29,77 @@ fn generate_ci_part(field: &syn::Field) -> TokenStream2 {
|
|||
.into()
|
||||
}
|
||||
|
||||
pub fn generate_command_enum(input: &syn::DeriveInput) -> TokenStream2 {
|
||||
let input_tokens: TokenStream2 = match &input.data {
|
||||
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 field_ident = field
|
||||
.ident
|
||||
.as_ref()
|
||||
.expect("These are only the named field, thus they all should have a name.");
|
||||
|
||||
let enum_variant_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(match *(return_type.to_owned()) {
|
||||
syn::Type::Path(path_type) => path_type
|
||||
.path
|
||||
.get_ident()
|
||||
.expect("A path should either be complete, or only conain one segment")
|
||||
.to_owned(),
|
||||
_ => unimplemented!("This is only implemented for path types"),
|
||||
}),
|
||||
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 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),
|
||||
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> {
|
||||
}
|
||||
.into()
|
||||
});
|
||||
} else {
|
||||
quote! {
|
||||
#enum_variant_name,
|
||||
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! {
|
||||
#[derive(Debug)]
|
||||
pub enum Command {
|
||||
#input_tokens
|
||||
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()
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
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