nml/crates/auto-registry/src/lib.rs

297 lines
7.6 KiB
Rust
Raw Normal View History

2024-08-08 11:21:58 +02:00
#![feature(proc_macro_span)]
use std::cell::RefCell;
use std::collections::HashMap;
use lazy_static::lazy_static;
use proc_macro::TokenStream;
use quote::quote;
use std::sync::Mutex;
use syn::parse::Parse;
use syn::parse::ParseStream;
use syn::parse_macro_input;
use syn::ItemStruct;
lazy_static! {
/// The registry, each key corresponds to an identifier that needs to be
/// valid in the context of the [`genegenerate_registry`] macro.
static ref REGISTRY: Mutex<RefCell<HashMap<String, Vec<String>>>> =
Mutex::new(RefCell::new(HashMap::new()));
}
/// Arguments for the [`auto_registry`] proc macro
struct AutoRegistryArgs {
/// The registry name
registry: syn::LitStr,
/// The absolute path to the struct, if not specified the macro will try
/// to automatically infer the full path.
path: Option<syn::LitStr>,
}
/// Parser for [`AutoRegistryArgs`]
impl Parse for AutoRegistryArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut registry = None;
let mut path = None;
loop {
let key: syn::Ident = input.parse()?;
input.parse::<syn::Token![=]>()?;
let value: syn::LitStr = input.parse()?;
match key.to_string().as_str() {
"registry" => registry = Some(value),
"path" => path = Some(value),
_ => {
return Err(syn::Error::new(
key.span(),
format!(
"Unknown attribute `{}`, excepted `registry` or `path`",
key.to_string()
),
))
}
}
if input.is_empty() {
break;
}
input.parse::<syn::Token![,]>()?;
}
if registry.is_none() {
return Err(syn::Error::new(
input.span(),
"Missing required attribute `registry`".to_string(),
));
}
Ok(AutoRegistryArgs {
registry: registry.unwrap(),
path,
})
}
}
/// The proc macro used on a struct to add it to the registry
///
/// # Attributes
/// - registry: (String) Name of the registry to collect the struct into
/// - path: (Optional String) The crate path in which the struct is located
/// If left empty, the path will be try to be automatically-deduced
2024-08-08 17:11:32 +02:00
///
/// # Note
///
/// Due to a lacking implementation of `proc_macro_span` in rust-analyzer,
/// it is highly advised the set the `path` attribute when using this macro.
/// See https://github.com/rust-lang/rust-analyzer/issues/15950
2024-08-08 11:21:58 +02:00
#[proc_macro_attribute]
pub fn auto_registry(attr: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as AutoRegistryArgs);
let input = parse_macro_input!(input as ItemStruct);
let ident = &input.ident;
let path = if let Some(path) = args.path {
2024-08-08 17:11:32 +02:00
let value = path.value();
if value.is_empty() {
value
} else {
format!("{}::{}", value, ident.to_string().as_str())
}
2024-08-08 11:21:58 +02:00
} else {
// Attempt to get the path in a hacky way in case the path wasn't
// specified as an attribute to the macro
let path = match input
.ident
.span()
.unwrap()
.source_file()
.path()
.canonicalize()
{
Ok(path) => path,
Err(e) => {
return syn::Error::new(
input.ident.span(),
format!("Failed to canonicalize path: {}", e),
)
.to_compile_error()
.into();
}
};
let crate_path = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let relative_path = path.strip_prefix(&crate_path).unwrap();
let relative_path_str = relative_path.to_string_lossy();
// Remove the first path component e.g "src/"
let pos = if let Some(pos) = relative_path_str.find("/") {
pos + 1
} else {
0
};
let module_path = relative_path_str
.split_at(pos)
.1
.strip_suffix(".rs")
.unwrap()
.replace("/", "::");
if module_path.is_empty() {
format!("crate::{}", ident.to_string())
} else {
format!("crate::{module_path}::{}", ident.to_string())
}
};
let reg_mtx = REGISTRY.lock().unwrap();
let mut reg_borrow = reg_mtx.borrow_mut();
if let Some(ref mut vec) = reg_borrow.get_mut(args.registry.value().as_str()) {
vec.push(path);
} else {
reg_borrow.insert(args.registry.value(), vec![path]);
}
quote! {
#input
}
.into()
}
/// Arguments for the [`generate_registry`] proc macro
struct GenerateRegistryArgs {
/// The registry name
registry: syn::LitStr,
/// The target, i.e the generated function name
target: syn::Ident,
/// The maker macro, takes all constructed items and processes them
maker: syn::Expr,
/// The return type for the function
return_type: syn::Type,
}
/// Parser for [`GenerateRegistryArgs`]
impl Parse for GenerateRegistryArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut registry = None;
let mut target = None;
let mut maker = None;
let mut return_type = None;
loop {
let key: syn::Ident = input.parse()?;
input.parse::<syn::Token![=]>()?;
match key.to_string().as_str() {
"registry" => registry = Some(input.parse()?),
"target" => target = Some(input.parse()?),
"maker" => maker = Some(input.parse()?),
"return_type" => return_type = Some(input.parse()?),
_ => {
return Err(syn::Error::new(
key.span(),
format!(
"Unknown attribute `{}`, excepted `registry` or `target`",
key.to_string()
),
))
}
}
if input.is_empty() {
break;
}
input.parse::<syn::Token![,]>()?;
}
if registry.is_none() {
return Err(syn::Error::new(
input.span(),
"Missing required attribute `registry`".to_string(),
));
} else if target.is_none() {
return Err(syn::Error::new(
input.span(),
"Missing required attribute `target`".to_string(),
));
} else if maker.is_none() {
return Err(syn::Error::new(
input.span(),
"Missing required attribute `maker`".to_string(),
));
} else if return_type.is_none() {
return Err(syn::Error::new(
input.span(),
"Missing required attribute `return_type`".to_string(),
));
}
Ok(GenerateRegistryArgs {
registry: registry.unwrap(),
target: target.unwrap(),
maker: maker.unwrap(),
return_type: return_type.unwrap(),
})
}
}
/// The proc macro that generates the function to build the registry
///
/// # Attributes
/// - registry: (String) Name of the registry to generate
/// - target: (Identifier) Name of the resulting function
/// - maker: (Macro) A macro that will take all the newly constructed objects
/// comma-separated and create the resulting expression
/// - return_type: (Type) The return type of the generated function.
/// Must match the type of the macro invocation
2024-08-08 17:11:32 +02:00
///
/// # Example
/// ```
/// macro_rules! create_listeners {
/// ( $($construct:expr),+ $(,)? ) => {{
/// vec![$(Box::new($construct) as Box<dyn Listener>,)+]
/// }};
/// }
/// #[generate_registry(
/// registry = "listeners",
/// target = build_listeners,
/// return_type = Vec<Box<dyn Listener>>,
/// maker = create_listeners)]
///
/// fn main()
/// {
/// let all_listeners : Vec<Box<dyn Listener>> = build_listeners();
/// }
/// ```
2024-08-08 11:21:58 +02:00
#[proc_macro_attribute]
pub fn generate_registry(attr: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as GenerateRegistryArgs);
let reg_mtx = REGISTRY.lock().unwrap();
let mut stream = proc_macro2::TokenStream::new();
if let Some(names) = reg_mtx.borrow().get(args.registry.value().as_str()) {
for name in names {
let struct_name: proc_macro2::TokenStream = name.parse().unwrap();
stream.extend(quote::quote_spanned!(proc_macro2::Span::call_site() =>
#struct_name::new(),
));
}
} else {
panic!(
"Unable to find registry item with key=`{}`",
args.registry.value()
);
}
let function = args.target;
let return_type = args.return_type;
let maker = args.maker;
let rest: proc_macro2::TokenStream = input.into();
quote! {
fn #function() -> #return_type {
#maker!(
#stream
)
}
#rest
}
.into()
}