initial commit

This commit is contained in:
TotallyNot 2023-12-31 21:26:43 +01:00
commit 86f9333aec
21 changed files with 6449 additions and 0 deletions

10
macros/src/lib.rs Normal file
View file

@ -0,0 +1,10 @@
mod log_message;
#[proc_macro_derive(LogMessage, attributes(log))]
pub fn derive_log_message(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = syn::parse(input).unwrap();
log_message::impl_log_message(&ast)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}

114
macros/src/log_message.rs Normal file
View file

@ -0,0 +1,114 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{spanned::Spanned, Data, DeriveInput, Fields};
fn camel_to_snake_case(input: String) -> String {
let mut prev_lowercase = false;
let mut output = String::with_capacity(input.len());
for char in input.chars() {
if char.is_lowercase() {
prev_lowercase = true;
output.push(char);
} else {
if prev_lowercase {
output.push('_');
}
prev_lowercase = false;
output.push(char.to_lowercase().next().unwrap())
}
}
output
}
enum LogOption {
Default,
Display,
Debug,
Player,
Weapon,
}
pub(crate) fn impl_log_message(ast: &DeriveInput) -> syn::Result<TokenStream> {
let Data::Struct(r#struct) = &ast.data else {
return Err(syn::Error::new(
ast.ident.span(),
"LogMessage can only be derived for structs",
));
};
let Fields::Named(named_fields) = &r#struct.fields else {
return Err(syn::Error::new(
ast.span(),
"LogMessage cannot be derived for tuple structs",
));
};
let mut fields = Vec::with_capacity(named_fields.named.len());
for field in &named_fields.named {
let mut option = None;
for attr in &field.attrs {
if attr.path().is_ident("log") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("display") {
option = Some(LogOption::Display);
Ok(())
} else if meta.path.is_ident("debug") {
option = Some(LogOption::Debug);
Ok(())
} else if meta.path.is_ident("player") {
option = Some(LogOption::Player);
Ok(())
} else if meta.path.is_ident("weapon") {
option = Some(LogOption::Weapon);
Ok(())
} else {
Err(meta.error("Unrecognised attribute"))
}
})?;
}
}
let option = option.unwrap_or(LogOption::Default);
let field_name = field.ident.as_ref().unwrap();
let name = field_name.to_string();
let getter = match option {
LogOption::Default => quote! {
(#name, crate::log::LogValue::from(self.#field_name.clone()))
},
LogOption::Display => quote! {
(#name, crate::log::LogValue::Display(&self.#field_name))
},
LogOption::Debug => quote! {
(#name, crate::log::LogValue::Debug(&self.#field_name))
},
LogOption::Player => quote! {
(#name, crate::log::LogValue::Player(self.#field_name))
},
LogOption::Weapon => quote! {
(#name, crate::log::LogValue::Weapon(self.#field_name))
},
};
fields.push(getter);
}
let name = &ast.ident;
let tag = camel_to_snake_case(name.to_string());
let gen = quote! {
impl crate::log::LogMessage for #name {
fn tag(&self) -> &'static str { #tag }
fn entries<'a>(&'a self) -> Vec<(&'static str, crate::log::LogValue<'a>)> {
vec![#(#fields,)*]
}
}
};
Ok(gen)
}