aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src/func/dispatcher.rs
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_macros/src/func/dispatcher.rs')
-rw-r--r--mingling_macros/src/func/dispatcher.rs266
1 files changed, 266 insertions, 0 deletions
diff --git a/mingling_macros/src/func/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs
new file mode 100644
index 0000000..a61dd26
--- /dev/null
+++ b/mingling_macros/src/func/dispatcher.rs
@@ -0,0 +1,266 @@
+#[cfg(feature = "dispatch_tree")]
+use just_fmt::snake_case;
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::{Attribute, Ident, LitStr, Result as SynResult, Token};
+
+#[cfg(feature = "dispatch_tree")]
+use crate::COMPILE_TIME_DISPATCHERS;
+
+enum DispatcherChainInput {
+ Default {
+ cmd_attrs: Vec<Attribute>,
+ entry_attrs: Vec<Attribute>,
+ command_name: syn::LitStr,
+ command_struct: Ident,
+ pack: Ident,
+ },
+ #[cfg(feature = "extra_macros")]
+ Auto {
+ cmd_attrs: Vec<Attribute>,
+ command_name: syn::LitStr,
+ },
+}
+
+impl Parse for DispatcherChainInput {
+ fn parse(input: ParseStream) -> SynResult<Self> {
+ // Collect outer attributes for the CMD struct
+ let cmd_attrs = input.call(Attribute::parse_outer)?;
+
+ if input.peek(syn::LitStr) {
+ // Parse the command name string first
+ let command_name: LitStr = input.parse()?;
+
+ // Check if this is the abbreviated form: just "command_name" without ", CMD => Entry"
+ if input.is_empty() {
+ #[cfg(feature = "extra_macros")]
+ {
+ return Ok(DispatcherChainInput::Auto {
+ cmd_attrs,
+ command_name,
+ });
+ }
+ #[cfg(not(feature = "extra_macros"))]
+ {
+ return Err(syn::Error::new(
+ command_name.span(),
+ "expected `, CommandStruct => EntryStruct` after command name",
+ ));
+ }
+ }
+
+ // Default format: "command_name", CommandStruct => ChainStruct
+ input.parse::<Token![,]>()?;
+ let command_struct = input.parse()?;
+ input.parse::<Token![=>]>()?;
+ let entry_attrs = input.call(Attribute::parse_outer)?;
+ let pack = input.parse()?;
+
+ Ok(DispatcherChainInput::Default {
+ cmd_attrs,
+ entry_attrs,
+ command_name,
+ command_struct,
+ pack,
+ })
+ } else {
+ Err(input.lookahead1().error())
+ }
+ }
+}
+
+// NOTICE: The token stream generation patterns in `dispatcher_chain` and `dispatcher_render`
+// are nearly identical and could benefit from refactoring into common helper functions.
+
+#[allow(clippy::too_many_lines)]
+pub(crate) fn dispatcher(input: TokenStream) -> TokenStream {
+ // Parse the input
+ let dispatcher_input = syn::parse_macro_input!(input as DispatcherChainInput);
+
+ #[cfg(not(feature = "extra_macros"))]
+ let (command_name, command_struct, pack, cmd_attrs, entry_attrs) = match dispatcher_input {
+ DispatcherChainInput::Default {
+ cmd_attrs,
+ entry_attrs,
+ command_name,
+ command_struct,
+ pack,
+ } => (command_name, command_struct, pack, cmd_attrs, entry_attrs),
+ };
+
+ #[cfg(feature = "extra_macros")]
+ let (command_name, command_struct, pack, cmd_attrs, entry_attrs) = match dispatcher_input {
+ DispatcherChainInput::Default {
+ cmd_attrs,
+ entry_attrs,
+ command_name,
+ command_struct,
+ pack,
+ } => (command_name, command_struct, pack, cmd_attrs, entry_attrs),
+ DispatcherChainInput::Auto {
+ cmd_attrs,
+ command_name,
+ } => {
+ let command_name_str = command_name.value();
+ let pascal = dotted_to_pascal_case(&command_name_str);
+ let command_struct = Ident::new(&format!("CMD{pascal}"), command_name.span());
+ let pack = Ident::new(&format!("Entry{pascal}"), command_name.span());
+ (command_name, command_struct, pack, cmd_attrs, Vec::new())
+ }
+ };
+
+ let command_name_str = command_name.value();
+
+ let comp_entry = get_comp_entry(&pack);
+
+ let dispatch_tree_entry = get_dispatch_tree_entry(&command_name_str, &command_struct, &pack);
+
+ let program_type = crate::default_program_path();
+
+ let expanded = quote! {
+ #(#cmd_attrs)*
+ #[derive(Debug, Default)]
+ pub struct #command_struct;
+
+ ::mingling::macros::pack!(#(#entry_attrs)* #pack = Vec<String>);
+
+ #comp_entry
+ #dispatch_tree_entry
+
+ impl ::mingling::Dispatcher<#program_type> for #command_struct {
+ fn node(&self) -> ::mingling::Node {
+ ::mingling::macros::node!(#command_name_str)
+ }
+ fn begin(&self, args: Vec<String>) -> ::mingling::ChainProcess<#program_type> {
+ use ::mingling::Grouped;
+ ::mingling::Routable::to_chain(#pack::new(args))
+ }
+ fn clone_dispatcher(&self) -> Box<dyn ::mingling::Dispatcher<#program_type>> {
+ Box::new(#command_struct)
+ }
+ }
+ };
+
+ expanded.into()
+}
+
+#[cfg(feature = "comp")]
+fn get_comp_entry(entry_name: &Ident) -> TokenStream2 {
+ let comp_entry = quote! {
+ impl ::mingling::CompletionEntry for #entry_name {
+ fn get_input(self) -> Vec<String> {
+ self.inner.clone()
+ }
+ }
+ };
+ comp_entry
+}
+
+#[cfg(not(feature = "comp"))]
+fn get_comp_entry(_entry_name: &Ident) -> TokenStream2 {
+ quote! {}
+}
+
+#[cfg(feature = "dispatch_tree")]
+fn get_dispatch_tree_entry(
+ command_name_str: &str,
+ command_struct: &Ident,
+ entry_name: &Ident,
+) -> TokenStream2 {
+ let node_name_lit = syn::LitStr::new(command_name_str, proc_macro2::Span::call_site());
+ quote! {
+ ::mingling::macros::register_dispatcher!(#node_name_lit, #command_struct, #entry_name);
+ }
+}
+
+#[cfg(not(feature = "dispatch_tree"))]
+fn get_dispatch_tree_entry(
+ _command_name_str: &str,
+ _command_struct: &Ident,
+ _entry_name: &Ident,
+) -> TokenStream2 {
+ quote! {}
+}
+
+#[cfg(feature = "dispatch_tree")]
+/// Input format: ("node.name", DispatcherType, EntryName)
+struct RegisterDispatcherInput {
+ node_name: syn::LitStr,
+ dispatcher_type: Ident,
+ entry_name: Ident,
+}
+
+#[cfg(feature = "dispatch_tree")]
+impl Parse for RegisterDispatcherInput {
+ fn parse(input: ParseStream) -> SynResult<Self> {
+ let node_name = input.parse()?;
+ input.parse::<Token![,]>()?;
+ let dispatcher_type = input.parse()?;
+ input.parse::<Token![,]>()?;
+ let entry_name = input.parse()?;
+ Ok(RegisterDispatcherInput {
+ node_name,
+ dispatcher_type,
+ entry_name,
+ })
+ }
+}
+
+#[cfg(feature = "dispatch_tree")]
+pub(crate) fn register_dispatcher(input: TokenStream) -> TokenStream {
+ let RegisterDispatcherInput {
+ node_name,
+ dispatcher_type,
+ entry_name,
+ } = syn::parse_macro_input!(input as RegisterDispatcherInput);
+
+ let node_name_str = node_name.value();
+ let static_name = format!(
+ "__internal_dispatcher_{}",
+ snake_case!(node_name_str.clone())
+ );
+ let static_ident = Ident::new(&static_name, proc_macro2::Span::call_site());
+
+ // Register node info in the global collection at compile time
+ // Format: "node.name:DispatcherType:EntryName"
+ crate::get_global_set(&COMPILE_TIME_DISPATCHERS)
+ .lock()
+ .unwrap()
+ .insert(format!(
+ "{}:{}:{}",
+ node_name_str, dispatcher_type, entry_name
+ ));
+
+ let expanded = quote! {
+ #[doc(hidden)]
+ #[allow(nonstandard_style)]
+ pub static #static_ident: #dispatcher_type = #dispatcher_type;
+ };
+
+ expanded.into()
+}
+
+#[cfg(not(feature = "dispatch_tree"))]
+pub(crate) fn register_dispatcher(_input: TokenStream) -> TokenStream {
+ quote! {}.into()
+}
+
+/// Converts a dotted command name (e.g. "remote.add") to `PascalCase` (e.g. "`RemoteAdd`").
+///
+/// Each segment is split by `.`, the first character of each segment is uppercased,
+/// and the segments are joined. This is used by the abbreviated `dispatcher!` syntax
+/// (when `Command => Entry` is omitted) to auto-derive struct names.
+#[cfg(feature = "extra_macros")]
+fn dotted_to_pascal_case(s: &str) -> String {
+ s.split('.')
+ .map(|segment| {
+ let mut chars = segment.chars();
+ match chars.next() {
+ None => String::new(),
+ Some(c) => c.to_uppercase().to_string() + chars.as_str(),
+ }
+ })
+ .collect()
+}