diff options
Diffstat (limited to 'mingling_pathf/src/patterns')
| -rw-r--r-- | mingling_pathf/src/patterns/chain.rs | 72 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/completion.rs | 65 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/dispatcher.rs | 116 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/dispatcher_clap.rs | 60 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/group.rs | 101 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/groupped_derive.rs | 93 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/help.rs | 65 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/pack.rs | 103 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/renderer.rs | 65 |
9 files changed, 740 insertions, 0 deletions
diff --git a/mingling_pathf/src/patterns/chain.rs b/mingling_pathf/src/patterns/chain.rs new file mode 100644 index 0000000..10d698e --- /dev/null +++ b/mingling_pathf/src/patterns/chain.rs @@ -0,0 +1,72 @@ +use syn::Item; + +use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; + +/// Match `#[chain]` functions, extract the generated internal struct name. +/// +/// `#[chain] fn handle_greet(...)` → `__internal_chain_handle_greet` +/// +/// Covered forms: +/// - `#[chain] fn handle(args: EntryType) -> Next { ... }` +/// - `#[chain] fn handle(args: EntryType, res: &mut Res) -> Next { ... }` +/// - async version +pub struct ChainPattern; + +impl AnalyzePattern for ChainPattern { + fn contains(&self, content: &str) -> bool { + content.contains("chain]") + } + + fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { + let Ok(syntax) = syn::parse_file(content) else { + return Vec::new(); + }; + + let mut items = Vec::new(); + + for item in &syntax.items { + collect_from_item(item, "", &mut items); + } + + items + } +} + +fn internal_name(fn_name: &str) -> String { + format!("__internal_chain_{fn_name}") +} + +fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem>) { + match item { + Item::Fn(f) if has_attr(&f.attrs, "chain") => { + let fn_name = f.sig.ident.to_string(); + items.push(AnalyzeItem { + module: current_mod.to_string(), + item_name: internal_name(&fn_name), + }); + } + Item::Mod(item_mod) => { + if let Some((_, nested)) = &item_mod.content { + let mod_name = &item_mod.ident.to_string(); + let nested_mod = if current_mod.is_empty() { + mod_name.clone() + } else { + format!("{current_mod}::{mod_name}") + }; + for n in nested { + collect_from_item(n, &nested_mod, items); + } + } + } + _ => {} + } +} + +fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool { + attrs.iter().any(|a| { + a.path() + .segments + .last() + .is_some_and(|s| s.ident == name) + }) +} diff --git a/mingling_pathf/src/patterns/completion.rs b/mingling_pathf/src/patterns/completion.rs new file mode 100644 index 0000000..7e4cd09 --- /dev/null +++ b/mingling_pathf/src/patterns/completion.rs @@ -0,0 +1,65 @@ +use syn::Item; + +use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; + +/// Matches `#[completion(T)]` functions, extracts the generated inner struct name. +/// +/// `#[completion(EntryGreet)] fn complete_greet_entry(...)` → `__internal_completion_complete_greet_entry` +pub struct CompletionPattern; + +impl AnalyzePattern for CompletionPattern { + fn contains(&self, content: &str) -> bool { + content.contains("completion(") + } + + fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { + let Ok(syntax) = syn::parse_file(content) else { + return Vec::new(); + }; + + let mut items = Vec::new(); + for item in &syntax.items { + collect_from_item(item, "", &mut items); + } + items + } +} + +fn internal_name(fn_name: &str) -> String { + format!("__internal_completion_{fn_name}") +} + +fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem>) { + match item { + Item::Fn(f) if has_attr(&f.attrs, "completion") => { + let fn_name = f.sig.ident.to_string(); + items.push(AnalyzeItem { + module: current_mod.to_string(), + item_name: internal_name(&fn_name), + }); + } + Item::Mod(item_mod) => { + if let Some((_, nested)) = &item_mod.content { + let mod_name = &item_mod.ident.to_string(); + let nested_mod = if current_mod.is_empty() { + mod_name.clone() + } else { + format!("{current_mod}::{mod_name}") + }; + for n in nested { + collect_from_item(n, &nested_mod, items); + } + } + } + _ => {} + } +} + +fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool { + attrs.iter().any(|a| { + a.path() + .segments + .last() + .is_some_and(|s| s.ident == name) + }) +} diff --git a/mingling_pathf/src/patterns/dispatcher.rs b/mingling_pathf/src/patterns/dispatcher.rs new file mode 100644 index 0000000..7bb076c --- /dev/null +++ b/mingling_pathf/src/patterns/dispatcher.rs @@ -0,0 +1,116 @@ +use syn::Item; + +use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; + +/// Matches the `dispatcher!` macro, extracts the entry type name. +/// +/// Supported forms: +/// - `dispatcher!("greet", CMDGreet => EntryGreet)` — explicit +/// - `dispatcher!("greet")` — implicit, infers EntryType +/// - `dispatcher! { ... }` — with braces +pub struct DispatcherPattern; + +impl AnalyzePattern for DispatcherPattern { + fn contains(&self, content: &str) -> bool { + content.contains("dispatcher!") + } + + fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { + let Ok(syntax) = syn::parse_file(content) else { + return Vec::new(); + }; + + let mut items = Vec::new(); + + for item in &syntax.items { + match item { + Item::Macro(m) => { + let macro_name = macro_simple_name(m); + if macro_name != "dispatcher" { + continue; + } + if let Some(entry_name) = extract_dispatcher_entry(&m.mac.tokens) { + items.push(AnalyzeItem { + module: String::new(), + item_name: entry_name, + }); + } + } + Item::Mod(item_mod) => { + if let Some((_, nested)) = &item_mod.content { + for n in nested { + if let Item::Macro(m) = n { + if macro_simple_name(m) != "dispatcher" { + continue; + } + if let Some(entry_name) = extract_dispatcher_entry(&m.mac.tokens) { + items.push(AnalyzeItem { + module: item_mod.ident.to_string(), + item_name: entry_name, + }); + } + } + } + } + } + _ => {} + } + } + + items + } +} + +fn macro_simple_name(m: &syn::ItemMacro) -> String { + m.mac + .path + .segments + .last() + .map(|s| s.ident.to_string()) + .unwrap_or_default() +} + +/// Extracts the entry type name from the `dispatcher!` macro arguments. +/// +/// Input examples: +/// - `"greet", CMDGreet => EntryGreet` → `EntryGreet` +/// - `"remote.add"` → `EntryRemoteAdd` (implicit inference) +fn extract_dispatcher_entry(tokens: &proc_macro2::TokenStream) -> Option<String> { + let stream = tokens.to_string(); + + // Explicit form: look for `=>` + if let Some(arrow_idx) = stream.find("=>") { + let after_arrow = stream[arrow_idx + 2..].trim(); + let entry_name = after_arrow + .split(|c: char| c.is_whitespace() || c == ',' || c == ')' || c == '}') + .next()?; + if !entry_name.is_empty() { + return Some(entry_name.trim().to_string()); + } + } + + // Implicit form: infer from command name + let stream = stream.trim(); + if let Some(start) = stream.find('"') { + let rest = &stream[start + 1..]; + let cmd_name = rest.split('"').next()?; + let last_segment = cmd_name.split('.').next_back()?; + let entry = format!("Entry{}", to_pascal_case(last_segment)); + Some(entry) + } else { + None + } +} + +fn to_pascal_case(s: &str) -> String { + s.split(['-', '_', '.']) + .filter(|s| !s.is_empty()) + .map(|s| { + let mut c = s.chars(); + match c.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::<String>() + c.as_str(), + } + }) + .collect() +} diff --git a/mingling_pathf/src/patterns/dispatcher_clap.rs b/mingling_pathf/src/patterns/dispatcher_clap.rs new file mode 100644 index 0000000..398b269 --- /dev/null +++ b/mingling_pathf/src/patterns/dispatcher_clap.rs @@ -0,0 +1,60 @@ +use syn::Item; + +use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; + +/// Match structs annotated with `#[dispatcher_clap]`, extracting the entry type name (i.e., the struct name). +/// +/// Covers the following forms: +/// - `#[dispatcher_clap] struct EntryGreet { ... }` +/// - `#[dispatcher_clap] #[command(...)] struct EntryGreet { ... }` +pub struct DispatcherClapPattern; + +impl AnalyzePattern for DispatcherClapPattern { + fn contains(&self, content: &str) -> bool { + content.contains("dispatcher_clap(") + } + + fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { + let Ok(syntax) = syn::parse_file(content) else { + return Vec::new(); + }; + + let mut items = Vec::new(); + + for item in &syntax.items { + match item { + Item::Struct(s) if has_attr(&s.attrs, "dispatcher_clap") => { + items.push(AnalyzeItem { + module: String::new(), + item_name: s.ident.to_string(), + }); + } + Item::Mod(item_mod) => { + if let Some((_, nested)) = &item_mod.content { + for n in nested { + if let Item::Struct(s) = n + && has_attr(&s.attrs, "dispatcher_clap") { + items.push(AnalyzeItem { + module: item_mod.ident.to_string(), + item_name: s.ident.to_string(), + }); + } + } + } + } + _ => {} + } + } + + items + } +} + +fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool { + attrs.iter().any(|a| { + a.path() + .segments + .last() + .is_some_and(|s| s.ident == name) + }) +} diff --git a/mingling_pathf/src/patterns/group.rs b/mingling_pathf/src/patterns/group.rs new file mode 100644 index 0000000..99d1137 --- /dev/null +++ b/mingling_pathf/src/patterns/group.rs @@ -0,0 +1,101 @@ +use syn::Item; + +use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; + +/// Matches the `group!` and `group_structural!` macros. +/// +/// Covered forms: +/// - `group!(TypeName)` +/// - `group!(Alias = path::Type)` +/// - `group_structural!(TypeName)` +/// - `group_structural!(Alias = path::Type)` +pub struct GroupPattern; + +impl AnalyzePattern for GroupPattern { + fn contains(&self, content: &str) -> bool { + content.contains("group!") || content.contains("group_structural!") + } + + fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { + let Ok(syntax) = syn::parse_file(content) else { + return Vec::new(); + }; + + let mut items = Vec::new(); + + for item in &syntax.items { + match item { + Item::Macro(m) => { + let Some(last) = m.mac.path.segments.last() else { + continue; + }; + let macro_name = last.ident.to_string(); + if macro_name != "group" && macro_name != "group_structural" { + continue; + } + if let Some(name) = extract_group_name(&m.mac.tokens) { + items.push(AnalyzeItem { + module: String::new(), + item_name: name, + }); + } + } + Item::Mod(item_mod) => { + if let Some((_, nested)) = &item_mod.content { + for n in nested { + if let Item::Macro(m) = n { + let Some(last) = m.mac.path.segments.last() else { + continue; + }; + let macro_name = last.ident.to_string(); + if macro_name != "group" && macro_name != "group_structural" { + continue; + } + if let Some(name) = extract_group_name(&m.mac.tokens) { + items.push(AnalyzeItem { + module: item_mod.ident.to_string(), + item_name: name, + }); + } + } + } + } + } + _ => {} + } + } + + items + } +} + +/// Extract the alias / type name from the arguments of `group!`. +/// +/// - `group!(ParseIntError)` → `ParseIntError` +/// - `group!(ErrorIo = std::io::Error)` → `ErrorIo` +fn extract_group_name(tokens: &proc_macro2::TokenStream) -> Option<String> { + let stream = tokens.clone(); + let mut iter = stream.into_iter(); + + loop { + match iter.next()? { + proc_macro2::TokenTree::Ident(ident) => { + let name = ident.to_string(); + + // Check if there is a `=` following + let next = iter.next(); + match next { + Some(proc_macro2::TokenTree::Punct(p)) if p.as_char() == '=' => { + // group!(Alias = path::Type) + return Some(name); + } + _ => { + // group!(TypeName) + return Some(name); + } + } + } + _ => continue, + } + } +} diff --git a/mingling_pathf/src/patterns/groupped_derive.rs b/mingling_pathf/src/patterns/groupped_derive.rs new file mode 100644 index 0000000..9f7301d --- /dev/null +++ b/mingling_pathf/src/patterns/groupped_derive.rs @@ -0,0 +1,93 @@ +use syn::Item; + +use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; + +/// Matches `#[derive(Groupped)]` and `#[derive(GrouppedSerialize)]`. +/// +/// Covers the forms: +/// - `#[derive(Groupped)] struct T { ... }` +/// - `#[derive(Groupped, Serialize, ...)] struct T { ... }` +/// - `#[derive(GrouppedSerialize)] struct T { ... }` +pub struct GrouppedDerivePattern; + +impl AnalyzePattern for GrouppedDerivePattern { + fn contains(&self, content: &str) -> bool { + content.contains("Groupped") + } + + fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { + let Ok(syntax) = syn::parse_file(content) else { + return Vec::new(); + }; + + let mut items = Vec::new(); + + for item in &syntax.items { + match item { + Item::Struct(s) => { + if has_groupped_derive(&s.attrs) { + items.push(AnalyzeItem { + module: String::new(), + item_name: s.ident.to_string(), + }); + } + } + Item::Enum(e) => { + if has_groupped_derive(&e.attrs) { + items.push(AnalyzeItem { + module: String::new(), + item_name: e.ident.to_string(), + }); + } + } + Item::Union(u) => { + if has_groupped_derive(&u.attrs) { + items.push(AnalyzeItem { + module: String::new(), + item_name: u.ident.to_string(), + }); + } + } + Item::Mod(item_mod) => { + if let Some((_, nested)) = &item_mod.content { + for n in nested { + match n { + Item::Struct(s) if has_groupped_derive(&s.attrs) => { + items.push(AnalyzeItem { + module: item_mod.ident.to_string(), + item_name: s.ident.to_string(), + }); + } + Item::Enum(e) if has_groupped_derive(&e.attrs) => { + items.push(AnalyzeItem { + module: item_mod.ident.to_string(), + item_name: e.ident.to_string(), + }); + } + _ => {} + } + } + } + } + _ => {} + } + } + + items + } +} + +fn has_groupped_derive(attrs: &[syn::Attribute]) -> bool { + attrs.iter().any(|attr| { + if attr.path().is_ident("derive") { + attr.parse_args::<syn::MetaList>().ok().is_some_and(|meta| { + meta.path.segments.iter().any(|seg| { + let name = seg.ident.to_string(); + name == "Groupped" || name == "GrouppedSerialize" + }) + }) + } else { + false + } + }) +} diff --git a/mingling_pathf/src/patterns/help.rs b/mingling_pathf/src/patterns/help.rs new file mode 100644 index 0000000..357626b --- /dev/null +++ b/mingling_pathf/src/patterns/help.rs @@ -0,0 +1,65 @@ +use syn::Item; + +use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; + +/// Matches `#[help]` functions, extracting the generated internal struct name. +/// +/// `#[help] fn help_my_entry(...)` → `__internal_help_help_my_entry` +pub struct HelpPattern; + +impl AnalyzePattern for HelpPattern { + fn contains(&self, content: &str) -> bool { + content.contains("help]") + } + + fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { + let Ok(syntax) = syn::parse_file(content) else { + return Vec::new(); + }; + + let mut items = Vec::new(); + for item in &syntax.items { + collect_from_item(item, "", &mut items); + } + items + } +} + +fn internal_name(fn_name: &str) -> String { + format!("__internal_help_{fn_name}") +} + +fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem>) { + match item { + Item::Fn(f) if has_attr(&f.attrs, "help") => { + let fn_name = f.sig.ident.to_string(); + items.push(AnalyzeItem { + module: current_mod.to_string(), + item_name: internal_name(&fn_name), + }); + } + Item::Mod(item_mod) => { + if let Some((_, nested)) = &item_mod.content { + let mod_name = &item_mod.ident.to_string(); + let nested_mod = if current_mod.is_empty() { + mod_name.clone() + } else { + format!("{current_mod}::{mod_name}") + }; + for n in nested { + collect_from_item(n, &nested_mod, items); + } + } + } + _ => {} + } +} + +fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool { + attrs.iter().any(|a| { + a.path() + .segments + .last() + .is_some_and(|s| s.ident == name) + }) +} diff --git a/mingling_pathf/src/patterns/pack.rs b/mingling_pathf/src/patterns/pack.rs new file mode 100644 index 0000000..f025f7d --- /dev/null +++ b/mingling_pathf/src/patterns/pack.rs @@ -0,0 +1,103 @@ +use syn::Item; + +use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; + +/// Matches types defined by `pack!`, `pack_err!`, `pack_structural!`, `pack_err_structural!` macros. +/// +/// Covered forms: +/// - `pack!(TypeName = InnerType)` +/// - `pack! { TypeName = InnerType }` +/// - `pack_err!(TypeName)` +/// - `pack_err!(TypeName = InnerType)` +/// - `pack_structural!` series same as above +pub struct PackPattern; + +impl AnalyzePattern for PackPattern { + fn contains(&self, content: &str) -> bool { + content.contains("pack!") || content.contains("pack_err!") + } + + fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { + let Ok(syntax) = syn::parse_file(content) else { + return Vec::new(); + }; + + let mut items = Vec::new(); + + for item in &syntax.items { + match item { + // Top-level macro calls + Item::Macro(m) => { + if let Some(name) = try_extract_pack_name(m) { + items.push(AnalyzeItem { + module: String::new(), + item_name: name, + }); + } + } + // Macro calls inside inline modules + Item::Mod(item_mod) => { + if let Some((_, nested)) = &item_mod.content { + for n in nested { + if let Item::Macro(m) = n + && let Some(name) = try_extract_pack_name(m) { + items.push(AnalyzeItem { + module: item_mod.ident.to_string(), + item_name: name, + }); + } + } + } + } + _ => {} + } + } + + items + } +} + +/// If the macro call is `pack!` / `pack_err!` / etc., extract the registered type name. +fn try_extract_pack_name(m: &syn::ItemMacro) -> Option<String> { + let macro_name = m.mac.path.segments.last()?.ident.to_string(); + + match macro_name.as_str() { + "pack" | "pack_err" | "pack_structural" | "pack_err_structural" => {} + _ => return None, + } + + let tokens = &m.mac.tokens; + + // `pack!(T)` or `pack!(T = U)` — the first ident is the type name + // Parse simply with syn + if let Ok(ident) = syn::parse2::<syn::Ident>(tokens.clone()) { + // pack!(TypeName) — just a single ident + return Some(ident.to_string()); + } + + // Try to parse `Ident = Type` + // Clone tokens first to avoid partial consumption + let stream = tokens.clone(); + let mut iter = stream.into_iter(); + + // Skip leading attributes/doc comments + loop { + match iter.next()? { + proc_macro2::TokenTree::Ident(ident) => { + // Found the first ident, this is the type name + let type_name = ident.to_string(); + + // Check if `=` follows + if let Some(proc_macro2::TokenTree::Punct(p)) = iter.next() + && p.as_char() == '=' { + // pack!(TypeName = InnerType) + return Some(type_name); + } + + // pack_err!(TypeName) — only a single ident + return Some(type_name); + } + _ => continue, + } + } +} diff --git a/mingling_pathf/src/patterns/renderer.rs b/mingling_pathf/src/patterns/renderer.rs new file mode 100644 index 0000000..410ae14 --- /dev/null +++ b/mingling_pathf/src/patterns/renderer.rs @@ -0,0 +1,65 @@ +use syn::Item; + +use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; + +/// Match `#[renderer]` functions, extract the generated internal struct name. +/// +/// `#[renderer] fn render_name(...)` → `__internal_renderer_render_name` +pub struct RendererPattern; + +impl AnalyzePattern for RendererPattern { + fn contains(&self, content: &str) -> bool { + content.contains("renderer]") + } + + fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { + let Ok(syntax) = syn::parse_file(content) else { + return Vec::new(); + }; + + let mut items = Vec::new(); + for item in &syntax.items { + collect_from_item(item, "", &mut items); + } + items + } +} + +fn internal_name(fn_name: &str) -> String { + format!("__internal_renderer_{fn_name}") +} + +fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem>) { + match item { + Item::Fn(f) if has_attr(&f.attrs, "renderer") => { + let fn_name = f.sig.ident.to_string(); + items.push(AnalyzeItem { + module: current_mod.to_string(), + item_name: internal_name(&fn_name), + }); + } + Item::Mod(item_mod) => { + if let Some((_, nested)) = &item_mod.content { + let mod_name = &item_mod.ident.to_string(); + let nested_mod = if current_mod.is_empty() { + mod_name.clone() + } else { + format!("{current_mod}::{mod_name}") + }; + for n in nested { + collect_from_item(n, &nested_mod, items); + } + } + } + _ => {} + } +} + +fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool { + attrs.iter().any(|a| { + a.path() + .segments + .last() + .is_some_and(|s| s.ident == name) + }) +} |
