Skip to main content

auto_registry/
lib.rs

1#![feature(proc_macro_span)]
2use std::sync::LazyLock;
3use std::sync::Mutex;
4use std::collections::HashMap;
5
6use proc_macro::TokenStream;
7use quote::quote;
8use syn::parse::Parse;
9use syn::parse::ParseStream;
10use syn::parse_macro_input;
11use syn::ItemStruct;
12
13static REGISTRY: LazyLock<Mutex<HashMap<String, Vec<String>>>> = LazyLock::new(|| Mutex::new(HashMap::default()));
14
15/// Arguments for the [`auto_registry`] proc macro
16struct AutoRegistryArgs {
17	/// The registry name
18	registry: syn::LitStr,
19	/// The absolute path to the struct, if not specified the macro will try
20	/// to automatically infer the full path.
21	path: Option<syn::LitStr>,
22}
23
24/// Parser for [`AutoRegistryArgs`]
25impl Parse for AutoRegistryArgs {
26	fn parse(input: ParseStream) -> syn::Result<Self> {
27		let mut registry = None;
28		let mut path = None;
29		loop {
30			let key: syn::Ident = input.parse()?;
31			input.parse::<syn::Token![=]>()?;
32			let value: syn::LitStr = input.parse()?;
33
34			match key.to_string().as_str() {
35				"registry" => registry = Some(value),
36				"path" => path = Some(value),
37				_ => {
38					return Err(syn::Error::new(
39						key.span(),
40						format!(
41							"Unknown attribute `{}`, excepted `registry` or `path`",
42							key.to_string()
43						),
44					))
45				}
46			}
47			if input.is_empty() {
48				break;
49			}
50			input.parse::<syn::Token![,]>()?;
51		}
52
53		if registry.is_none() {
54			return Err(syn::Error::new(
55				input.span(),
56				"Missing required attribute `registry`".to_string(),
57			));
58		}
59
60		Ok(AutoRegistryArgs {
61			registry: registry.unwrap(),
62			path,
63		})
64	}
65}
66
67/// The proc macro used on a struct to add it to the registry
68///
69/// # Attributes
70///  - registry: (String) Name of the registry to collect the struct into
71///  - path: (Optional String) The crate path in which the struct is located
72///          If left empty, the macro will be try to infer the path.
73///
74/// # Example
75///
76/// ```
77/// #[auto_registry::auto_registry(registry = "listeners")]
78/// struct KeyboardListener {}
79/// ```
80/// This will register `KeyboardListener` to the `listeners` registry.
81///
82/// # Note
83///
84/// Due to a lacking implementation of `proc_macro_span` in rust-analyzer,
85/// it is highly advised the set the `path` attribute when using this macro.
86/// See https://github.com/rust-lang/rust-analyzer/issues/15950
87#[proc_macro_attribute]
88pub fn auto_registry(attr: TokenStream, input: TokenStream) -> TokenStream {
89	let args = parse_macro_input!(attr as AutoRegistryArgs);
90	let input = parse_macro_input!(input as ItemStruct);
91
92	let ident = &input.ident;
93
94	let path = if let Some(path) = args.path {
95		let value = path.value();
96		if value.is_empty() {
97			value
98		} else {
99			format!("{}::{}", value, ident.to_string().as_str())
100		}
101	} else {
102		// Attempt to get the path in a hacky way in case the path wasn't
103		// specified as an attribute to the macro
104		let path = match std::path::PathBuf::from(input
105			.ident
106			.span()
107			.unwrap()
108			.file())
109			.canonicalize()
110		{
111			Ok(path) => path,
112			Err(e) => {
113				return syn::Error::new(
114					input.ident.span(),
115					format!("Failed to canonicalize path: {}", e),
116				)
117				.to_compile_error()
118				.into();
119			}
120		};
121
122		let crate_path = std::env::var("CARGO_MANIFEST_DIR").unwrap();
123		let relative_path = path.strip_prefix(&crate_path).unwrap();
124		let relative_path_str = relative_path.to_string_lossy();
125		// Remove the first path component e.g "src/"
126		let pos = if let Some(pos) = relative_path_str.find("/") {
127			pos + 1
128		} else {
129			0
130		};
131
132		let module_path = relative_path_str
133			.split_at(pos)
134			.1
135			.strip_suffix(".rs")
136			.unwrap()
137			.replace("/", "::");
138
139		if module_path.is_empty() {
140			format!("crate::{}", ident.to_string())
141		} else {
142			format!("crate::{module_path}::{}", ident.to_string())
143		}
144	};
145
146	let mut reg = REGISTRY.lock().unwrap();
147	if let Some(ref mut vec) = reg.get_mut(args.registry.value().as_str()) {
148		vec.push(path);
149	} else {
150		reg.insert(args.registry.value(), vec![path]);
151	}
152
153	quote! {
154		#input
155	}
156	.into()
157}
158
159/// Arguments for the [`generate_registry`] proc macro
160struct GenerateRegistryArgs {
161	/// The registry name
162	registry: syn::LitStr,
163	/// The collector macro, takes all constructed items and processes them
164	collector: Option<syn::Expr>,
165	/// The maper macro, maps types to expressions
166	mapper: Option<syn::Expr>,
167	/// The name of the output macro
168	output: syn::Ident,
169}
170
171/// Parser for [`GenerateRegistryArgs`]
172impl Parse for GenerateRegistryArgs {
173	fn parse(input: ParseStream) -> syn::Result<Self> {
174		let mut registry = None;
175		let mut collector = None;
176		let mut mapper = None;
177		let mut output = None;
178		loop {
179			let key: syn::Ident = input.parse()?;
180			input.parse::<syn::Token![=]>()?;
181
182			match key.to_string().as_str() {
183				"registry" => registry = Some(input.parse()?),
184				"collector" => collector = Some(input.parse()?),
185				"mapper" => mapper = Some(input.parse()?),
186				"output" => output = Some(input.parse()?),
187				_ => {
188					return Err(syn::Error::new(
189						key.span(),
190						format!(
191							"Unknown attribute `{}`, excepted `registry`, `collector`, `mapper` or `output`",
192							key.to_string()
193						),
194					))
195				}
196			}
197			if input.is_empty() {
198				break;
199			}
200			input.parse::<syn::Token![,]>()?;
201		}
202
203		if registry.is_none() {
204			return Err(syn::Error::new(
205				input.span(),
206				"Missing required attribute `registry`".to_string(),
207			));
208		} else if output.is_none() {
209			return Err(syn::Error::new(
210				input.span(),
211				"Missing required attribute `output`".to_string(),
212			));
213		} else if collector.is_none() && mapper.is_none() {
214			return Err(syn::Error::new(
215				input.span(),
216				"Macro requires that either `collector` or `mapper` be set".to_string(),
217			));
218		}
219
220		Ok(GenerateRegistryArgs {
221			registry: registry.unwrap(),
222			collector,
223			mapper,
224			output: output.unwrap(),
225		})
226	}
227}
228
229/// The proc macro that generates the function to build the registry
230///
231/// # Attributes
232///  - registry: (String) Name of the registry to generate
233///  - collector: (Optional Macro) A macro that will take all the newly constructed
234///           objects comma-separated and create the resulting expression
235///  - mapper: (Optional Macro) A macro that will map each registered types to
236///            an expression. By default `$type::default()` will be called.
237///  - output: (Identifier) The generated macro to get access to all registered
238///            values. Calling to this macro is what actually generates the values
239///
240/// Note: Using `mapper` and `collector` will pass the results of calling `mapper`
241/// on all types in the registry to `collector`
242///
243/// # Example
244///
245/// Basic example
246/// ```
247/// pub trait Listener {}
248///
249/// #[auto_registry::auto_registry(registry = "listeners")]
250/// #[derive(Default)]
251/// pub struct KeyboardListener {}
252/// impl Listener for KeyboardListener {}
253///
254/// #[auto_registry::auto_registry(registry = "listeners")]
255/// #[derive(Default)]
256/// pub struct MouseListener {}
257/// impl Listener for MouseListener {}
258///
259/// macro_rules! collect_listeners { // Collects to a Vec<Box<dyn Listener>>
260/// 	( $($construct:expr);+ $(;)? ) => {{ // Macro must accepts `;`-separated arguments
261/// 		vec![$(Box::new($construct) as Box<dyn Listener + Send + Sync>,)+]
262/// 	}};
263/// }
264///
265/// #[auto_registry::generate_registry(registry = "listeners", collector = collect_listeners, output = get_listeners)]
266///
267/// fn main()
268/// {
269/// 	// All listeners will be initialized by calling to `::default()`
270/// 	let listeners = get_listeners!();
271/// 	assert_eq!(listeners.len(), 2);
272/// }
273/// ```
274///
275/// Example using `mapper`
276/// ```
277/// use std::sync::LazyLock;
278/// use std::sync::Mutex;
279///
280/// pub trait Listener {}
281///
282/// #[auto_registry::auto_registry(registry = "listeners")]
283/// #[derive(Default)]
284/// pub struct KeyboardListener {}
285/// impl Listener for KeyboardListener {}
286///
287/// #[auto_registry::auto_registry(registry = "listeners")]
288/// #[derive(Default)]
289/// pub struct MouseListener {}
290/// impl Listener for MouseListener {}
291///
292/// // Some global variable that will hold out registered listeners
293/// static LISTENERS: LazyLock<Mutex<Vec<Box<dyn Listener + Send + Sync>>>> = LazyLock::new(|| Mutex::new(Vec::default()));
294///
295/// macro_rules! register_listener { // Register a single listener
296/// 	($t:ty) => {{
297/// 		let mut listeners = LISTENERS.lock();
298/// 		listeners
299/// 			.unwrap()
300/// 			.push(Box::new(<$t>::default()) as Box<dyn Listener + Send + Sync>);
301/// 	}};
302/// }
303///
304/// #[auto_registry::generate_registry(registry = "listeners", mapper = register_listener, output = register_all_listeners)]
305///
306/// fn main()
307/// {
308/// 	register_all_listeners!();
309/// }
310/// ```
311#[proc_macro_attribute]
312pub fn generate_registry(attr: TokenStream, input: TokenStream) -> TokenStream {
313	let args = parse_macro_input!(attr as GenerateRegistryArgs);
314	let reg = REGISTRY.lock().unwrap();
315
316	let mut stream = proc_macro2::TokenStream::new();
317	if let Some(names) = reg.get(args.registry.value().as_str()) {
318		for name in names {
319			let struct_name: proc_macro2::TokenStream = name.parse().unwrap();
320			if let Some(ref mapper) = args.mapper
321			{
322				stream.extend(quote::quote_spanned!(proc_macro2::Span::call_site() =>
323					#mapper!(#struct_name);
324				));
325			}
326			else
327			{
328				stream.extend(quote::quote_spanned!(proc_macro2::Span::call_site() =>
329					#struct_name::default();
330				));
331			}
332		}
333	} else {
334		panic!(
335			"Unable to find registry item with key=`{}`",
336			args.registry.value()
337		);
338	}
339
340	let rest: proc_macro2::TokenStream = input.into();
341	let output = args.output;
342
343	if let Some(collector) = args.collector
344	{
345		quote! {
346			macro_rules! #output  {
347				() => { #collector!(#stream) };
348			}
349			#rest
350		}
351	}
352	else
353	{
354		quote! {
355			macro_rules! #output  {
356				() => { #stream };
357			}
358			#rest
359		}
360	}
361	.into()
362}