aboutsummaryrefslogtreecommitdiff
path: root/mingling_pathf
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_pathf')
-rw-r--r--mingling_pathf/Cargo.toml4
-rw-r--r--mingling_pathf/src/pattern_analyzer.rs2
-rw-r--r--mingling_pathf/src/patterns/dispatcher_clap.rs234
-rw-r--r--mingling_pathf/src/patterns/groupped_derive.rs53
-rw-r--r--mingling_pathf/test/Cargo.lock1
-rw-r--r--mingling_pathf/test/src/lib.rs59
-rw-r--r--mingling_pathf/test/src/test_files/test_dispatcher_clap.rs40
-rw-r--r--mingling_pathf/test/src/test_files/test_groupped_derive.rs17
8 files changed, 367 insertions, 43 deletions
diff --git a/mingling_pathf/Cargo.toml b/mingling_pathf/Cargo.toml
index 9619adc..0d4e37a 100644
--- a/mingling_pathf/Cargo.toml
+++ b/mingling_pathf/Cargo.toml
@@ -2,9 +2,13 @@
name = "mingling_pathf"
version.workspace = true
edition.workspace = true
+authors = ["Weicao-CatilGrass"]
license.workspace = true
repository.workspace = true
+readme = "README.md"
+description = "A library for automatically finding internal types generated by Mingling"
[dependencies]
syn.workspace = true
proc-macro2.workspace = true
+just_fmt.workspace = true
diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs
index bfc2dc3..c4b1971 100644
--- a/mingling_pathf/src/pattern_analyzer.rs
+++ b/mingling_pathf/src/pattern_analyzer.rs
@@ -23,7 +23,7 @@ pub fn init_with_config(config: PathfinderConfig) -> PatternAnalyzer {
analyzer.add_pattern(HelpPattern);
analyzer.add_pattern(CompletionPattern);
analyzer.add_pattern(DispatcherPattern::new(config.use_dispatch_tree));
- analyzer.add_pattern(DispatcherClapPattern);
+ analyzer.add_pattern(DispatcherClapPattern::new(config.use_dispatch_tree));
analyzer
}
diff --git a/mingling_pathf/src/patterns/dispatcher_clap.rs b/mingling_pathf/src/patterns/dispatcher_clap.rs
index 398b269..2e1ec6c 100644
--- a/mingling_pathf/src/patterns/dispatcher_clap.rs
+++ b/mingling_pathf/src/patterns/dispatcher_clap.rs
@@ -2,12 +2,27 @@ 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).
+/// Match structs annotated with `#[dispatcher_clap(...)]`, extracting:
+/// - The entry type (struct name, always)
+/// - The dispatcher struct (`CMD*`, always)
+/// - The error type, if `error = ErrorType` is specified
+/// - The help internal struct, if `help = true` is specified
+/// - `__internal_dispatcher_*` — dispatch tree static (when `use_dispatch_tree` is true)
///
-/// Covers the following forms:
-/// - `#[dispatcher_clap] struct EntryGreet { ... }`
-/// - `#[dispatcher_clap] #[command(...)] struct EntryGreet { ... }`
-pub struct DispatcherClapPattern;
+/// Covers forms:
+/// - `#[dispatcher_clap("greet", CMDGreet)] struct EntryGreet { ... }`
+/// - `#[dispatcher_clap("greet", CMDGreet, error = ErrorGreet)] struct EntryGreet { ... }`
+/// - `#[dispatcher_clap("greet", CMDGreet, help = true)] struct EntryGreet { ... }`
+/// - `#[dispatcher_clap("greet", CMDGreet, error = ErrorGreet, help = true)] struct EntryGreet { ... }`
+pub struct DispatcherClapPattern {
+ pub use_dispatch_tree: bool,
+}
+
+impl DispatcherClapPattern {
+ pub fn new(use_dispatch_tree: bool) -> Self {
+ Self { use_dispatch_tree }
+ }
+}
impl AnalyzePattern for DispatcherClapPattern {
fn contains(&self, content: &str) -> bool {
@@ -24,21 +39,136 @@ impl AnalyzePattern for DispatcherClapPattern {
for item in &syntax.items {
match item {
Item::Struct(s) if has_attr(&s.attrs, "dispatcher_clap") => {
+ // Entry type (struct name) — always
+ let entry_name = s.ident.to_string();
items.push(AnalyzeItem {
module: String::new(),
- item_name: s.ident.to_string(),
+ item_name: entry_name.clone(),
});
+
+ // Parse the attribute to extract CMD, error, and help info
+ if let Some(attr) = s.attrs.iter().find(|a| {
+ a.path()
+ .segments
+ .last()
+ .is_some_and(|seg| seg.ident == "dispatcher_clap")
+ }) {
+ let args = attr.meta.require_list().ok();
+ let args_str = args.map(|l| l.tokens.to_string()).unwrap_or_default();
+ let parsed = parse_dispatcher_clap_args(&args_str);
+
+ // CMD type — always
+ if let Some(ref cmd) = parsed.cmd_type {
+ items.push(AnalyzeItem {
+ module: String::new(),
+ item_name: cmd.clone(),
+ });
+ }
+
+ // Error type — if error = TypeName
+ if let Some(ref err) = parsed.error_type {
+ items.push(AnalyzeItem {
+ module: String::new(),
+ item_name: err.clone(),
+ });
+ }
+
+ // Help internal struct — if help = true
+ if parsed.help_enabled
+ && let Some(ref cmd) = parsed.cmd_type
+ {
+ let help_fn = format!("__{}_help", just_fmt::snake_case!(cmd));
+ let help_struct =
+ format!("__internal_help_{}", just_fmt::snake_case!(&help_fn));
+ items.push(AnalyzeItem {
+ module: String::new(),
+ item_name: help_struct,
+ });
+ }
+
+ // __internal_dispatcher_* — when configured
+ if self.use_dispatch_tree
+ && let Some(ref cmd_name) = parsed.cmd_name
+ {
+ let internal_name = format!(
+ "__internal_dispatcher_{}",
+ just_fmt::snake_case!(cmd_name)
+ );
+ items.push(AnalyzeItem {
+ module: String::new(),
+ item_name: internal_name,
+ });
+ }
+ }
}
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(),
- });
+ && has_attr(&s.attrs, "dispatcher_clap")
+ {
+ let entry_name = s.ident.to_string();
+ items.push(AnalyzeItem {
+ module: item_mod.ident.to_string(),
+ item_name: entry_name.clone(),
+ });
+
+ if let Some(attr) = s.attrs.iter().find(|a| {
+ a.path()
+ .segments
+ .last()
+ .is_some_and(|seg| seg.ident == "dispatcher_clap")
+ }) {
+ let args = attr.meta.require_list().ok();
+ let args_str =
+ args.map(|l| l.tokens.to_string()).unwrap_or_default();
+ let parsed = parse_dispatcher_clap_args(&args_str);
+
+ if let Some(ref cmd) = parsed.cmd_type {
+ items.push(AnalyzeItem {
+ module: item_mod.ident.to_string(),
+ item_name: cmd.clone(),
+ });
+ }
+
+ if let Some(ref err) = parsed.error_type {
+ items.push(AnalyzeItem {
+ module: item_mod.ident.to_string(),
+ item_name: err.clone(),
+ });
+ }
+
+ // Help internal struct — same naming rule as root level
+ if parsed.help_enabled
+ && let Some(ref cmd) = parsed.cmd_type
+ {
+ let help_fn =
+ format!("__{}_help", just_fmt::snake_case!(cmd));
+ let help_struct = format!(
+ "__internal_help_{}",
+ just_fmt::snake_case!(&help_fn)
+ );
+ items.push(AnalyzeItem {
+ module: item_mod.ident.to_string(),
+ item_name: help_struct,
+ });
+ }
+
+ // __internal_dispatcher_* — when configured
+ if self.use_dispatch_tree
+ && let Some(ref cmd_name) = parsed.cmd_name
+ {
+ let internal_name = format!(
+ "__internal_dispatcher_{}",
+ just_fmt::snake_case!(cmd_name)
+ );
+ items.push(AnalyzeItem {
+ module: item_mod.ident.to_string(),
+ item_name: internal_name,
+ });
+ }
}
+ }
}
}
}
@@ -50,11 +180,81 @@ impl AnalyzePattern for DispatcherClapPattern {
}
}
+struct ParsedClapArgs {
+ cmd_name: Option<String>,
+ cmd_type: Option<String>,
+ error_type: Option<String>,
+ help_enabled: bool,
+}
+
+/// Parse `#[dispatcher_clap("cmd", CMDType, error = ErrorType, help = true)]` arguments.
+fn parse_dispatcher_clap_args(args: &str) -> ParsedClapArgs {
+ let mut cmd_name = None;
+ let mut cmd_type = None;
+ let mut error_type = None;
+ let mut help_enabled = false;
+
+ let args = args.trim();
+
+ // Extract the first quoted string (the command name)
+ let after_cmd = if let Some(start) = args.find('"') {
+ let after_open = &args[start + 1..];
+ if let Some(end) = after_open.find('"') {
+ cmd_name = Some(after_open[..end].to_string());
+ after_open[end + 1..].trim()
+ } else {
+ args
+ }
+ } else {
+ args
+ };
+
+ // Split by commas and parse each part
+ for part in after_cmd.split(',') {
+ let part = part.trim();
+ if part.is_empty() {
+ continue;
+ }
+
+ // Skip the command name (first quoted string should already be removed)
+ if part.starts_with('"') {
+ continue;
+ }
+
+ // Check for key = value
+ if let Some(eq_idx) = part.find('=') {
+ let key = part[..eq_idx].trim();
+ let value = part[eq_idx + 1..].trim();
+ let value = value.trim_end_matches([')', ']']).trim();
+
+ match key {
+ "error" => {
+ error_type = Some(value.to_string());
+ }
+ "help" => {
+ help_enabled = value == "true";
+ }
+ _ => {}
+ }
+ } else {
+ // Bare ident — the CMD type
+ let clean = part.trim_end_matches([')', ']']).trim();
+ if !clean.is_empty() && cmd_type.is_none() {
+ cmd_type = Some(clean.to_string());
+ }
+ }
+ }
+
+ ParsedClapArgs {
+ cmd_name,
+ cmd_type,
+ error_type,
+ help_enabled,
+ }
+}
+
fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool {
- attrs.iter().any(|a| {
- a.path()
- .segments
- .last()
- .is_some_and(|s| s.ident == name)
- })
+ attrs
+ .iter()
+ .any(|a| a.path().segments.last().is_some_and(|s| s.ident == name))
}
diff --git a/mingling_pathf/src/patterns/groupped_derive.rs b/mingling_pathf/src/patterns/groupped_derive.rs
index 44e7731..8491121 100644
--- a/mingling_pathf/src/patterns/groupped_derive.rs
+++ b/mingling_pathf/src/patterns/groupped_derive.rs
@@ -24,27 +24,24 @@ impl AnalyzePattern for GrouppedDerivePattern {
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::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 {
@@ -77,12 +74,18 @@ impl AnalyzePattern for GrouppedDerivePattern {
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();
+ // Correctly parse comma-separated paths in #[derive(Groupped, Debug, ...)]
+ attr.parse_args_with(|input: syn::parse::ParseStream| {
+ let paths =
+ syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated(
+ input,
+ )?;
+ Ok(paths.iter().any(|p| {
+ let name = p.segments.last().unwrap().ident.to_string();
name == "Groupped" || name == "GrouppedSerialize"
- })
+ }))
})
+ .unwrap_or(false)
} else {
false
}
diff --git a/mingling_pathf/test/Cargo.lock b/mingling_pathf/test/Cargo.lock
index 6c89ae3..e5fd23a 100644
--- a/mingling_pathf/test/Cargo.lock
+++ b/mingling_pathf/test/Cargo.lock
@@ -12,6 +12,7 @@ checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
name = "mingling_pathf"
version = "0.2.0"
dependencies = [
+ "just_fmt",
"proc-macro2",
"syn",
]
diff --git a/mingling_pathf/test/src/lib.rs b/mingling_pathf/test/src/lib.rs
index 51e19a6..824cbbf 100644
--- a/mingling_pathf/test/src/lib.rs
+++ b/mingling_pathf/test/src/lib.rs
@@ -233,8 +233,11 @@ fn test_groupped_derive_analyze() {
"::Derived1",
"::Derived2",
"::Derived3",
+ "::EnumDerived1",
+ "::EnumDerived2",
"::sub::Derived1",
"::sub::Derived3",
+ "::sub::EnumDerived1",
];
assert_eq!(r.len(), required.len());
@@ -315,12 +318,41 @@ fn test_dispatcher_clap_analyze() {
let r = analyzer.analyze_file(file).unwrap();
let required: Vec<&str> = vec![
+ // Root: entry types (bare dispatcher_clap, no params)
"::EntryClap1",
"::EntryClap2",
"::EntryClap3",
"::EntryClap4",
+ // Root: with CMD type
+ "::EntryWithCmd",
+ "::CMDGreet",
+ // Root: with CMD + error
+ "::EntryWithError",
+ "::CMDDelete",
+ "::ErrorDelete",
+ // Root: with CMD + help
+ "::EntryWithHelp",
+ "::CMDHelp",
+ "::__internal_help_cmdhelp_help",
+ // Root: with CMD + error + help
+ "::EntryFull",
+ "::CMDFull",
+ "::ErrorFull",
+ "::__internal_help_cmdfull_help",
+ // Sub: entry types (bare dispatcher_clap)
"::sub::EntryClap1",
"::sub::EntryClap3",
+ // Sub: with CMD type
+ "::sub::EntryWithCmd",
+ "::sub::CMDGreet",
+ // Sub: with CMD + error
+ "::sub::EntryWithError",
+ "::sub::CMDDelete",
+ "::sub::ErrorDelete",
+ // Sub: with CMD + help
+ "::sub::EntryWithHelp",
+ "::sub::CMDHelp",
+ "::sub::__internal_help_cmdhelp_help",
];
assert_eq!(r.len(), required.len());
@@ -328,3 +360,30 @@ fn test_dispatcher_clap_analyze() {
assert!(r.contains(*entry), "Result should contain: {}", entry);
}
}
+
+#[test]
+fn test_dispatcher_clap_dispatch_tree() {
+ use mingling_pathf::config::PathfinderConfig;
+ use mingling_pathf::pattern_analyzer;
+
+ let file = current_dir()
+ .unwrap()
+ .join("src/test_files/test_dispatcher_clap.rs");
+
+ // Without dispatch_tree: 26 items (same set as test_dispatcher_clap_analyze)
+ let r1 = pattern_analyzer::init().analyze_file(&file).unwrap();
+ assert_eq!(r1.len(), 26);
+
+ // With dispatch_tree: 26 + 4 __internal (root) + 3 __internal (sub, no "full") = 33
+ let r2 = pattern_analyzer::init_with_config(PathfinderConfig::with_dispatch_tree())
+ .analyze_file(&file)
+ .unwrap();
+ assert_eq!(r2.len(), 33);
+ assert!(r2.contains("::__internal_dispatcher_greet"));
+ assert!(r2.contains("::__internal_dispatcher_delete"));
+ assert!(r2.contains("::__internal_dispatcher_helpcmd"));
+ assert!(r2.contains("::__internal_dispatcher_full"));
+ assert!(r2.contains("::sub::__internal_dispatcher_greet"));
+ assert!(r2.contains("::sub::__internal_dispatcher_delete"));
+ assert!(r2.contains("::sub::__internal_dispatcher_helpcmd"));
+}
diff --git a/mingling_pathf/test/src/test_files/test_dispatcher_clap.rs b/mingling_pathf/test/src/test_files/test_dispatcher_clap.rs
index 0ba884d..33d86e0 100644
--- a/mingling_pathf/test/src/test_files/test_dispatcher_clap.rs
+++ b/mingling_pathf/test/src/test_files/test_dispatcher_clap.rs
@@ -1,3 +1,4 @@
+// Basic: entry type only (no CMD type specified)
#[mingling::macros::dispatcher_clap]
struct EntryClap1 {
name: String,
@@ -20,6 +21,30 @@ pub struct EntryClap4 {
value: i32,
}
+// With CMD type
+#[dispatcher_clap("greet", CMDGreet)]
+struct EntryWithCmd {
+ name: String,
+}
+
+// With CMD + error
+#[dispatcher_clap("delete", CMDDelete, error = ErrorDelete)]
+struct EntryWithError {
+ id: u64,
+}
+
+// With CMD + help
+#[dispatcher_clap("helpcmd", CMDHelp, help = true)]
+struct EntryWithHelp {
+ verbose: bool,
+}
+
+// With CMD + error + help
+#[dispatcher_clap("full", CMDFull, error = ErrorFull, help = true)]
+struct EntryFull {
+ all: bool,
+}
+
pub mod sub {
#[mingling::macros::dispatcher_clap]
struct EntryClap1 {
@@ -30,4 +55,19 @@ pub mod sub {
struct EntryClap3 {
value: String,
}
+
+ #[dispatcher_clap("greet", CMDGreet)]
+ struct EntryWithCmd {
+ name: String,
+ }
+
+ #[dispatcher_clap("delete", CMDDelete, error = ErrorDelete)]
+ struct EntryWithError {
+ id: u64,
+ }
+
+ #[dispatcher_clap("helpcmd", CMDHelp, help = true)]
+ struct EntryWithHelp {
+ verbose: bool,
+ }
}
diff --git a/mingling_pathf/test/src/test_files/test_groupped_derive.rs b/mingling_pathf/test/src/test_files/test_groupped_derive.rs
index f6c6fa9..913587c 100644
--- a/mingling_pathf/test/src/test_files/test_groupped_derive.rs
+++ b/mingling_pathf/test/src/test_files/test_groupped_derive.rs
@@ -13,6 +13,18 @@ struct Derived3 {
value: bool,
}
+#[derive(Groupped)]
+enum EnumDerived1 {
+ A,
+ B,
+}
+
+#[derive(GrouppedSerialize)]
+enum EnumDerived2 {
+ X(String),
+ Y(i32),
+}
+
pub mod sub {
#[derive(Groupped)]
struct Derived1 {
@@ -23,4 +35,9 @@ pub mod sub {
struct Derived3 {
value: bool,
}
+
+ #[derive(Groupped)]
+ enum EnumDerived1 {
+ A,
+ }
}