diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-06-28 09:06:08 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-06-28 09:06:08 +0800 |
| commit | 748c14588cf1c31c8b8d60a9c94349c0173ef607 (patch) | |
| tree | 4c09bfafd93b629a68f0f78902a33e8dd9ef18d1 | |
| parent | 50f2d767e2d07685e49fb7deae68d506ea11a79d (diff) | |
feat(pathf): add build-time type path resolution system
Add `mingling_pathf` sub-crate and `pathf` feature for automatic
resolution of Mingling type module paths at build time. Scans source
files, identifies macro invocations via pattern matchers, and generates
mapping files consumed by `gen_program!()`.
31 files changed, 1468 insertions, 7 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 9627967..0ec479d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -285,6 +285,47 @@ async fn greet(prev: HelloEntry, ec: &mut ResExitCode) -> Next { } ``` +13. **[`pathf`]** Added the `mingling_pathf` sub-crate and the `pathf` feature for build-time type path resolution. + +The `pathf` (pathfinder) system enables automatic resolution of type module paths at build time. It scans source files, identifies Mingling macro invocations (`pack!`, `#[chain]`, `#[renderer]`, `#[help]`, `#[completion]`, `dispatcher!`, `#[dispatcher_clap]`, `group!`, `#[derive(Groupped)]`, etc.), infers their module paths from the file system layout, and generates a mapping file consumed by `gen_program!()` at compile time. + +**Feature activation**: Enable the `pathf` feature on the `mingling` crate: + +```toml +mingling = { version = "0.2", features = ["pathf"] } +``` + +**Usage** — Add a `build.rs` to your project: + +```rust +// build.rs +fn main() { + mingling::pathf::analyze_and_build_type_mapping().unwrap(); +} +``` + +The pathfinder system consists of: + +- **`mingling_pathf` sub-crate** — A standalone crate for build-time source analysis: + - `module_pathf::analyze()` — Scans the crate's source tree and infers module paths from the directory structure + - `pattern_analyzer::init()` — Creates a `PatternAnalyzer` registered with all supported Mingling patterns + - `analyze_and_build_type_mapping()` / `analyze_and_build_type_mapping_for()` — Convenience functions for build scripts + - **Pattern matchers** — Individual pattern implementations for each Mingling macro: + - `PackPattern` — Matches `pack!`, `pack_err!`, `pack_structural!`, `pack_err_structural!` invocations + - `GroupPattern` — Matches `group!` and `group_structural!` invocations + - `GrouppedDerivePattern` — Matches `#[derive(Groupped)]` and `#[derive(GrouppedSerialize)]` + - `ChainPattern` — Matches `#[chain]` functions, extracts `__internal_chain_*` names + - `RendererPattern` — Matches `#[renderer]` functions, extracts `__internal_renderer_*` names + - `HelpPattern` — Matches `#[help]` functions, extracts `__internal_help_*` names + - `CompletionPattern` — Matches `#[completion(T)]` functions, extracts `__internal_completion_*` names + - `DispatcherPattern` — Matches `dispatcher!` invocations, extracts entry type names (supports both explicit and implicit forms) + - `DispatcherClapPattern` — Matches `#[dispatcher_clap]` structs, extracts struct names + - `type_mapping_builder` — Assembles the mapping from all analyzed files and writes `MAPPING` and `type_using.rs` output files + +- **Integration with `gen_program!()`** — When the `pathf` feature is enabled, `gen_program!()` includes the generated `type_using.rs` file via `include!()`, making all type paths available in scope for the generated dispatch code. + +- **Public re-exports** — The `mingling` crate re-exports `mingling_pathf` types under `mingling::pathf::*` and error types under `mingling::error::*` (behind the `pathf` feature gate). + #### **BREAKING CHANGES** (API CHANGES): --- diff --git a/dev_tools/src/bin/test-examples.rs b/dev_tools/src/bin/test-examples.rs index 5153709..539459e 100644 --- a/dev_tools/src/bin/test-examples.rs +++ b/dev_tools/src/bin/test-examples.rs @@ -94,7 +94,7 @@ fn run_all_tests(config: &TestConfig, bar: &ProgressBar) -> usize { /// Build the example binary, return true on success fn build_example(example_name: &str) -> bool { let manifest = format!("examples/{example_name}/Cargo.toml"); - tools::run_cmd_capture(&format!( + tools::run_cmd_capture(format!( "cargo build --manifest-path {manifest} --color always", )) .is_ok() diff --git a/dev_tools/src/lib.rs b/dev_tools/src/lib.rs index 2bd55a2..bb4a7d9 100644 --- a/dev_tools/src/lib.rs +++ b/dev_tools/src/lib.rs @@ -255,7 +255,7 @@ pub fn run_parallel(phase: &str, tasks: Vec<(String, String, String)>) -> Result code, )); if !output.is_empty() { - pb.println(output.trim_end().to_string()); + pb.println(output.trim_end()); } } } diff --git a/mingling_core/src/builds.rs b/mingling_core/src/builds.rs index 51bafe6..17dc7d2 100644 --- a/mingling_core/src/builds.rs +++ b/mingling_core/src/builds.rs @@ -3,4 +3,7 @@ pub mod comp; #[cfg(all(feature = "builds", feature = "pathf"))] -pub use mingling_pathf::*; +pub use mingling_pathf::analyze_and_build_type_mapping; + +#[cfg(all(feature = "builds", feature = "pathf"))] +pub use mingling_pathf::analyze_and_build_type_mapping_for; diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs index ef7d192..d49fa7f 100644 --- a/mingling_core/src/lib.rs +++ b/mingling_core/src/lib.rs @@ -44,6 +44,8 @@ pub mod error { pub use crate::program::error::*; #[cfg(feature = "structural_renderer")] pub use crate::renderer::structural::error::*; + #[cfg(feature = "pathf")] + pub use mingling_pathf::error::*; } pub use crate::program::*; @@ -92,3 +94,10 @@ pub mod core_res { #[cfg(feature = "repl")] pub use crate::program::repl_exec::res::ResREPL; } + +#[cfg(feature = "pathf")] +pub mod pathf { + pub use mingling_pathf::module_pathf::*; + pub use mingling_pathf::pattern_analyzer::*; + pub use mingling_pathf::patterns::*; +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index 1b0cbb5..8955b37 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -2037,7 +2037,17 @@ pub fn program_final_gen(_input: TokenStream) -> TokenStream { quote! { u128 } }; + let pathf_include = if cfg!(feature = "pathf") { + quote! { + include!(concat!(env!("OUT_DIR"), "/", env!("CARGO_PKG_NAME"), "/type_using.rs")); + } + } else { + quote! {} + }; + let expanded = quote! { + #pathf_include + #[derive(Debug, PartialEq, Eq, Clone)] #[repr(#repr_type)] #[allow(nonstandard_style)] diff --git a/mingling_pathf/README.md b/mingling_pathf/README.md index 9706083..6d8857a 100644 --- a/mingling_pathf/README.md +++ b/mingling_pathf/README.md @@ -41,7 +41,7 @@ fn main() { 1. **Build-time scanning**: `build.rs` traverses all `.rs` source files under `src/`, locating macro invocations such as `pack!`, `#[chain]`, `#[renderer]`, etc., via pattern matching. 2. **Module inference**: The module path is inferred from the file's directory path (e.g., `src/app/sub.rs` → `app::sub`). 3. **Reference tracking**: Following the chain of `mod use` re-exports (i.e., paths re-exported via `pub use` or `use`), the type name is resolved to the module path under which it is ultimately referenced. -4. **Mapping output**: The mapping from type names to their final referenceable module paths is written to `$OUT_DIR/CRATE_NAME/type-mapping.rs`. +4. **Mapping output**: The mapping from type names to their final referenceable module paths is written to `$OUT_DIR/CRATE_NAME/type-mapping`. 5. **Compile-time consumption**: `gen_program!()` reads this mapping file and uses the full paths for downcasting in the generated dispatch code. ## Constraints diff --git a/mingling_pathf/src/lib.rs b/mingling_pathf/src/lib.rs index d8d81db..40fd2ec 100644 --- a/mingling_pathf/src/lib.rs +++ b/mingling_pathf/src/lib.rs @@ -2,3 +2,7 @@ pub mod module_pathf; pub mod pattern_analyzer; pub mod error; pub mod patterns; + +mod type_mapping_builder; +pub use type_mapping_builder::analyze_and_build_type_mapping; +pub use type_mapping_builder::analyze_and_build_type_mapping_for; diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs index cb98a5f..4a1f8a4 100644 --- a/mingling_pathf/src/pattern_analyzer.rs +++ b/mingling_pathf/src/pattern_analyzer.rs @@ -16,6 +16,15 @@ pub fn init() -> PatternAnalyzer { __register![ BasicStructPattern, + PackPattern, + GroupPattern, + GrouppedDerivePattern, + ChainPattern, + RendererPattern, + HelpPattern, + CompletionPattern, + DispatcherPattern, + DispatcherClapPattern, ]; analyzer @@ -123,6 +132,22 @@ impl PatternAnalyzer { /// - `Ok(HashSet<String>)` —— On success, returns a formatted set of strings, each in the form `"::module_path::item_name"`. /// - `Err(MinglingPathfinderError)` —— If the file cannot be read, returns the corresponding I/O error wrapper. pub fn analyze_file(&self, path: impl AsRef<Path>) -> Result<HashSet<String>, MinglingPathfinderError> { + self.collect_items(path).map(|items| { + AnalyzeResult { items }.into_formatted() + }) + } + + /// Analyzes a single file and returns the raw `Vec<AnalyzeItem>`. + /// + /// Unlike `analyze_file`, this method does not format the results into strings, + /// preserving the original module-path and item-name information. Useful for + /// callers that need to combine the results with other data sources. + pub fn analyze_file_items(&self, path: impl AsRef<Path>) -> Result<Vec<AnalyzeItem>, MinglingPathfinderError> { + self.collect_items(path) + } + + /// Internal: collects raw `AnalyzeItem`s from a file. + fn collect_items(&self, path: impl AsRef<Path>) -> Result<Vec<AnalyzeItem>, MinglingPathfinderError> { let path = path.as_ref(); let content = std::fs::read_to_string(path)?; @@ -134,7 +159,6 @@ impl PatternAnalyzer { } } - let result = AnalyzeResult { items: all_items }; - Ok(result.into_formatted()) + Ok(all_items) } } diff --git a/mingling_pathf/src/patterns.rs b/mingling_pathf/src/patterns.rs index 33d3503..b3e0cd3 100644 --- a/mingling_pathf/src/patterns.rs +++ b/mingling_pathf/src/patterns.rs @@ -1,2 +1,21 @@ -mod basic_struct; pub use basic_struct::*; +pub use chain::*; +pub use completion::*; +pub use dispatcher::*; +pub use dispatcher_clap::*; +pub use groupped_derive::*; +pub use group::*; +pub use help::*; +pub use pack::*; +pub use renderer::*; + +mod basic_struct; +mod chain; +mod completion; +mod dispatcher; +mod dispatcher_clap; +mod groupped_derive; +mod group; +mod help; +mod pack; +mod renderer; 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) + }) +} diff --git a/mingling_pathf/src/type_mapping_builder.rs b/mingling_pathf/src/type_mapping_builder.rs new file mode 100644 index 0000000..c701536 --- /dev/null +++ b/mingling_pathf/src/type_mapping_builder.rs @@ -0,0 +1,101 @@ +use std::collections::HashSet; +use std::path::{Path}; + +use crate::error::MinglingPathfinderError; +use crate::module_pathf; +use crate::pattern_analyzer; + +/// Analyzes the Mingling types of the specified crate directory and generates mapping files to the specified output directory. +/// +/// `crate_dir` — crate root directory (i.e., the directory containing Cargo.toml) +/// `output_dir` — directory where mapping files will be written +/// +/// Mapping file format per line: `TypeName = crate::module::path::TypeName` +pub fn analyze_and_build_type_mapping_for( + crate_dir: &Path, + output_dir: &Path, +) -> Result<(), MinglingPathfinderError> { + let module_mapping = module_pathf::analyze(crate_dir)?; + let analyzer = pattern_analyzer::init(); + + let mut type_mappings: Vec<(String, String)> = Vec::new(); + + for item in module_mapping { + let file_abs = crate_dir.join(item.file_path()); + if !file_abs.is_file() { + continue; + } + + let module_path = item.module_path(); + let Ok(analyze_items) = analyzer.analyze_file_items(&file_abs) else { + continue; + }; + + for ai in analyze_items { + let full_path = if ai.module.is_empty() { + format!("{}::{}", module_path, ai.item_name) + } else { + format!("{}::{}::{}", module_path, ai.module, ai.item_name) + }; + type_mappings.push((ai.item_name, full_path)); + } + } + + // Sort by full path (ASCII order) + type_mappings.sort_by(|a, b| a.1.cmp(&b.1)); + + // Deduplicate by type name, keeping the first occurrence + let mut seen = HashSet::new(); + type_mappings.retain(|(name, _)| seen.insert(name.clone())); + + // Create output directory + std::fs::create_dir_all(output_dir)?; + + // Write files + let output_path = output_dir.join("MAPPING"); + let type_using_path = output_dir.join("type_using.rs"); + + let mut content_mapping = String::new(); + for (name, path) in &type_mappings { + content_mapping.push_str(&format!("{name} = {path}\n")); + } + std::fs::write(&output_path, content_mapping)?; + + let mut content_using = String::new(); + for (_, path) in &type_mappings { + content_using.push_str(&format!("use {path};\n")); + } + std::fs::write(&type_using_path, content_using)?; + + Ok(()) +} + +/// Convenience version to be called from `build.rs`, automatically reading configuration +/// from environment variables. +/// +/// Reads `CARGO_PKG_NAME` and `OUT_DIR`, and outputs to `{OUT_DIR}/{CARGO_PKG_NAME}/`. +pub fn analyze_and_build_type_mapping() -> Result<(), MinglingPathfinderError> { + let crate_name = std::env::var("CARGO_PKG_NAME") + .map_err(|_| MinglingPathfinderError::IoError( + std::io::Error::new(std::io::ErrorKind::NotFound, + "CARGO_PKG_NAME not set (not running in build.rs?)") + ))?; + + let out_dir = std::env::var("OUT_DIR") + .map_err(|_| MinglingPathfinderError::IoError( + std::io::Error::new(std::io::ErrorKind::NotFound, + "OUT_DIR not set (not running in build.rs?)") + ))?; + + let crate_dir = std::env::current_dir()?; + let output_dir = Path::new(&out_dir).join(&crate_name); + + analyze_and_build_type_mapping_for(&crate_dir, &output_dir)?; + + // Notify Cargo to re-run build.rs when source files change + println!("cargo:rerun-if-changed=src/"); + println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_OS"); + println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_ARCH"); + + Ok(()) +} diff --git a/mingling_pathf/test/src/.gitignore b/mingling_pathf/test/src/.gitignore new file mode 100644 index 0000000..13bd6e1 --- /dev/null +++ b/mingling_pathf/test/src/.gitignore @@ -0,0 +1 @@ +main.rs diff --git a/mingling_pathf/test/src/lib.rs b/mingling_pathf/test/src/lib.rs index f2ca3e0..2fcf01a 100644 --- a/mingling_pathf/test/src/lib.rs +++ b/mingling_pathf/test/src/lib.rs @@ -1,6 +1,7 @@ #![cfg(test)] use std::{collections::HashMap, env::current_dir}; +use mingling_pathf::analyze_and_build_type_mapping_for; #[test] fn test_module_pathf() { @@ -46,3 +47,237 @@ fn test_pattern_analyzer_once() { let result = analyzer.analyze_file(dir.join("src/has_sub_mod.rs")).unwrap(); assert!(result.contains("::directly_sub_mod::DirectlySubModStruct")); } + +#[test] +fn test_chain_analyze() { + let analyzer = mingling_pathf::pattern_analyzer::init(); + let file = current_dir().unwrap().join("src/test_files/test_chain.rs"); + + let r = analyzer.analyze_file(file).unwrap(); + let required_entries: Vec<&str> = vec![ + "::sub::__internal_chain_my_chain1", + "::sub::__internal_chain_my_chain2", + "::sub::__internal_chain_my_chain3", + "::sub::__internal_chain_my_chain4", + "::sub::__internal_chain_my_chain5", + "::sub::__internal_chain_my_chain6", + "::__internal_chain_my_chain1", + "::__internal_chain_my_chain2", + "::__internal_chain_my_chain3", + "::__internal_chain_my_chain4", + "::__internal_chain_my_chain5", + "::__internal_chain_my_chain6", + ]; + + assert_eq!(r.len(), required_entries.len(), "Result should contain exactly {} entries", required_entries.len()); + + for entry in &required_entries { + assert!(r.iter().any(|e| e == entry), "Result should contain: {}", entry); + } +} + +#[test] +fn test_renderer_analyze() { + let analyzer = mingling_pathf::pattern_analyzer::init(); + let file = current_dir().unwrap().join("src/test_files/test_renderer.rs"); + + let r = analyzer.analyze_file(file).unwrap(); + let required: Vec<&str> = vec![ + "::sub::__internal_renderer_my_renderer1", + "::sub::__internal_renderer_my_renderer2", + "::sub::__internal_renderer_my_renderer3", + "::sub::__internal_renderer_my_renderer4", + "::__internal_renderer_my_renderer1", + "::__internal_renderer_my_renderer2", + "::__internal_renderer_my_renderer3", + "::__internal_renderer_my_renderer4", + ]; + + assert_eq!(r.len(), required.len()); + for entry in &required { + assert!(r.contains(*entry), "Result should contain: {}", entry); + } +} + +#[test] +fn test_help_analyze() { + let analyzer = mingling_pathf::pattern_analyzer::init(); + let file = current_dir().unwrap().join("src/test_files/test_help.rs"); + + let r = analyzer.analyze_file(file).unwrap(); + let required: Vec<&str> = vec![ + "::sub::__internal_help_my_help1", + "::sub::__internal_help_my_help2", + "::sub::__internal_help_my_help3", + "::sub::__internal_help_my_help4", + "::__internal_help_my_help1", + "::__internal_help_my_help2", + "::__internal_help_my_help3", + "::__internal_help_my_help4", + ]; + + assert_eq!(r.len(), required.len()); + for entry in &required { + assert!(r.contains(*entry), "Result should contain: {}", entry); + } +} + +#[test] +fn test_completion_analyze() { + let analyzer = mingling_pathf::pattern_analyzer::init(); + let file = current_dir().unwrap().join("src/test_files/test_completion.rs"); + + let r = analyzer.analyze_file(file).unwrap(); + let required: Vec<&str> = vec![ + "::sub::__internal_completion_my_completion1", + "::sub::__internal_completion_my_completion2", + "::sub::__internal_completion_my_completion3", + "::sub::__internal_completion_my_completion4", + "::__internal_completion_my_completion1", + "::__internal_completion_my_completion2", + "::__internal_completion_my_completion3", + "::__internal_completion_my_completion4", + ]; + + assert_eq!(r.len(), required.len()); + for entry in &required { + assert!(r.contains(*entry), "Result should contain: {}", entry); + } +} + +#[test] +fn test_pack_analyze() { + let analyzer = mingling_pathf::pattern_analyzer::init(); + let file = current_dir().unwrap().join("src/test_files/test_pack.rs"); + + let r = analyzer.analyze_file(file).unwrap(); + let required: Vec<&str> = vec![ + "::ResultPack1", + "::ErrorPack1", + "::ErrorPack2", + "::ResultPack2", + "::ErrorPack3", + "::ErrorPack4", + "::sub::ResultPack1", + "::sub::ErrorPack1", + "::sub::ErrorPack2", + "::sub::ResultPack2", + "::sub::ErrorPack3", + "::sub::ErrorPack4", + ]; + + assert_eq!(r.len(), required.len()); + for entry in &required { + assert!(r.contains(*entry), "Result should contain: {}", entry); + } +} + +#[test] +fn test_group_analyze() { + let analyzer = mingling_pathf::pattern_analyzer::init(); + let file = current_dir().unwrap().join("src/test_files/test_group.rs"); + + let r = analyzer.analyze_file(file).unwrap(); + let required: Vec<&str> = vec![ + "::Group1", + "::GroupAlias1", + "::Group2", + "::GroupAlias2", + "::sub::Group1", + "::sub::GroupAlias1", + "::sub::Group2", + "::sub::GroupAlias2", + ]; + + assert_eq!(r.len(), required.len()); + for entry in &required { + assert!(r.contains(*entry), "Result should contain: {}", entry); + } +} + +#[test] +fn test_groupped_derive_analyze() { + let analyzer = mingling_pathf::pattern_analyzer::init(); + let file = current_dir().unwrap().join("src/test_files/test_groupped_derive.rs"); + + let r = analyzer.analyze_file(file).unwrap(); + let required: Vec<&str> = vec![ + "::Derived1", + "::Derived2", + "::Derived3", + "::sub::Derived1", + "::sub::Derived3", + ]; + + assert_eq!(r.len(), required.len()); + for entry in &required { + assert!(r.contains(*entry), "Result should contain: {}", entry); + } +} + +#[test] +fn test_dispatcher_analyze() { + let analyzer = mingling_pathf::pattern_analyzer::init(); + let file = current_dir().unwrap().join("src/test_files/test_dispatcher.rs"); + + let r = analyzer.analyze_file(file).unwrap(); + let required: Vec<&str> = vec![ + "::EntryGreet", + "::EntryRemoteAdd", + "::EntryAdd", + "::EntryDelete", + "::EntryRemoteRm", + "::EntryRm", + "::sub::EntryGreet", + "::sub::EntryDelete", + ]; + + assert_eq!(r.len(), required.len()); + for entry in &required { + assert!(r.contains(*entry), "Result should contain: {}", entry); + } +} + +#[test] +fn test_dispatcher_clap_analyze() { + let analyzer = mingling_pathf::pattern_analyzer::init(); + let file = current_dir().unwrap().join("src/test_files/test_dispatcher_clap.rs"); + + let r = analyzer.analyze_file(file).unwrap(); + let required: Vec<&str> = vec![ + "::EntryClap1", + "::EntryClap2", + "::EntryClap3", + "::EntryClap4", + "::sub::EntryClap1", + "::sub::EntryClap3", + ]; + + assert_eq!(r.len(), required.len()); + for entry in &required { + assert!(r.contains(*entry), "Result should contain: {}", entry); + } +} + +#[test] +fn test_type_mapping_file_created() { + let tmp = std::env::temp_dir().join("mingling_pathf_test_type_mapping"); + let _ = std::fs::remove_dir_all(&tmp); + + let crate_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + + analyze_and_build_type_mapping_for( + crate_dir, + &tmp, + ) + .unwrap(); + + let output_path = tmp.join("type-mapping"); + assert!( + output_path.exists(), + "type-mapping file should exist at: {}", + output_path.display() + ); + + let _ = std::fs::remove_dir_all(&tmp); +} diff --git a/mingling_pathf/test/src/test_files/test_chain.rs b/mingling_pathf/test/src/test_files/test_chain.rs new file mode 100644 index 0000000..e209a5e --- /dev/null +++ b/mingling_pathf/test/src/test_files/test_chain.rs @@ -0,0 +1,61 @@ +#[mingling::macros::chain] +fn my_chain1(prev: Some1) -> Next { + +} + +#[mingling::macros::chain] +pub fn my_chain2(prev: Some2) -> Next { + +} + +#[mingling::macros::chain] +pub async fn my_chain3(prev: Some3) -> Next { + +} + +#[chain] +fn my_chain4(prev: Some4) { + +} + +#[chain] +pub fn my_chain5(prev: Some5) { + +} + +#[chain] +pub async fn my_chain6(prev: Some6) { + +} + +pub mod sub { + #[mingling::macros::chain] + fn my_chain1(prev: Some1) -> Next { + + } + + #[mingling::macros::chain] + pub fn my_chain2(prev: Some2) -> Next { + + } + + #[mingling::macros::chain] + pub async fn my_chain3(prev: Some3) -> Next { + + } + + #[chain] + fn my_chain4(prev: Some4) { + + } + + #[chain] + pub fn my_chain5(prev: Some5) { + + } + + #[chain] + pub async fn my_chain6(prev: Some6) { + + } +} diff --git a/mingling_pathf/test/src/test_files/test_completion.rs b/mingling_pathf/test/src/test_files/test_completion.rs new file mode 100644 index 0000000..87a655f --- /dev/null +++ b/mingling_pathf/test/src/test_files/test_completion.rs @@ -0,0 +1,41 @@ +#[mingling::macros::completion(Some1)] +fn my_completion1(ctx: &mingling::ShellContext) -> mingling::Suggest { + mingling::Suggest::new() +} + +#[mingling::macros::completion(Some2)] +pub fn my_completion2(ctx: &mingling::ShellContext) -> mingling::Suggest { + mingling::Suggest::new() +} + +#[completion(Some3)] +fn my_completion3(ctx: &mingling::ShellContext) -> mingling::Suggest { + mingling::Suggest::new() +} + +#[completion(Some4)] +pub fn my_completion4(ctx: &mingling::ShellContext) -> mingling::Suggest { + mingling::Suggest::new() +} + +pub mod sub { + #[mingling::macros::completion(Some1)] + fn my_completion1(ctx: &mingling::ShellContext) -> mingling::Suggest { + mingling::Suggest::new() + } + + #[mingling::macros::completion(Some2)] + pub fn my_completion2(ctx: &mingling::ShellContext) -> mingling::Suggest { + mingling::Suggest::new() + } + + #[completion(Some3)] + fn my_completion3(ctx: &mingling::ShellContext) -> mingling::Suggest { + mingling::Suggest::new() + } + + #[completion(Some4)] + pub fn my_completion4(ctx: &mingling::ShellContext) -> mingling::Suggest { + mingling::Suggest::new() + } +} diff --git a/mingling_pathf/test/src/test_files/test_dispatcher.rs b/mingling_pathf/test/src/test_files/test_dispatcher.rs new file mode 100644 index 0000000..48f5e4d --- /dev/null +++ b/mingling_pathf/test/src/test_files/test_dispatcher.rs @@ -0,0 +1,17 @@ +mingling::macros::dispatcher!("greet", CMDGreet => EntryGreet); +mingling::macros::dispatcher!("greet"); +mingling::macros::dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); +mingling::macros::dispatcher!("remote.add"); + +dispatcher!("delete", CMDDelete => EntryDelete); +dispatcher!("delete"); +dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm); +dispatcher!("remote.rm"); + +pub mod sub { + mingling::macros::dispatcher!("greet", CMDGreet => EntryGreet); + mingling::macros::dispatcher!("greet"); + + dispatcher!("delete", CMDDelete => EntryDelete); + dispatcher!("delete"); +} diff --git a/mingling_pathf/test/src/test_files/test_dispatcher_clap.rs b/mingling_pathf/test/src/test_files/test_dispatcher_clap.rs new file mode 100644 index 0000000..0ba884d --- /dev/null +++ b/mingling_pathf/test/src/test_files/test_dispatcher_clap.rs @@ -0,0 +1,33 @@ +#[mingling::macros::dispatcher_clap] +struct EntryClap1 { + name: String, + age: i32, +} + +#[mingling::macros::dispatcher_clap] +#[command(name = "greet")] +pub struct EntryClap2 { + name: String, +} + +#[dispatcher_clap] +struct EntryClap3 { + value: String, +} + +#[dispatcher_clap] +pub struct EntryClap4 { + value: i32, +} + +pub mod sub { + #[mingling::macros::dispatcher_clap] + struct EntryClap1 { + name: String, + } + + #[dispatcher_clap] + struct EntryClap3 { + value: String, + } +} diff --git a/mingling_pathf/test/src/test_files/test_group.rs b/mingling_pathf/test/src/test_files/test_group.rs new file mode 100644 index 0000000..92c8cda --- /dev/null +++ b/mingling_pathf/test/src/test_files/test_group.rs @@ -0,0 +1,13 @@ +mingling::macros::group!(Group1); +mingling::macros::group!(GroupAlias1 = std::io::Error); + +group!(Group2); +group!(GroupAlias2 = std::num::ParseIntError); + +pub mod sub { + mingling::macros::group!(Group1); + mingling::macros::group!(GroupAlias1 = std::io::Error); + + group!(Group2); + group!(GroupAlias2 = std::num::ParseIntError); +} diff --git a/mingling_pathf/test/src/test_files/test_groupped_derive.rs b/mingling_pathf/test/src/test_files/test_groupped_derive.rs new file mode 100644 index 0000000..f6c6fa9 --- /dev/null +++ b/mingling_pathf/test/src/test_files/test_groupped_derive.rs @@ -0,0 +1,26 @@ +#[derive(Groupped)] +struct Derived1 { + value: String, +} + +#[derive(Groupped, Debug, Clone)] +struct Derived2 { + value: i32, +} + +#[derive(GrouppedSerialize)] +struct Derived3 { + value: bool, +} + +pub mod sub { + #[derive(Groupped)] + struct Derived1 { + value: String, + } + + #[derive(GrouppedSerialize)] + struct Derived3 { + value: bool, + } +} diff --git a/mingling_pathf/test/src/test_files/test_help.rs b/mingling_pathf/test/src/test_files/test_help.rs new file mode 100644 index 0000000..52d1408 --- /dev/null +++ b/mingling_pathf/test/src/test_files/test_help.rs @@ -0,0 +1,33 @@ +#[mingling::macros::help] +fn my_help1(prev: Some1) { +} + +#[mingling::macros::help] +pub fn my_help2(prev: Some2) { +} + +#[help] +fn my_help3(prev: Some3) { +} + +#[help] +pub fn my_help4(prev: Some4) { +} + +pub mod sub { + #[mingling::macros::help] + fn my_help1(prev: Some1) { + } + + #[mingling::macros::help] + pub fn my_help2(prev: Some2) { + } + + #[help] + fn my_help3(prev: Some3) { + } + + #[help] + pub fn my_help4(prev: Some4) { + } +} diff --git a/mingling_pathf/test/src/test_files/test_pack.rs b/mingling_pathf/test/src/test_files/test_pack.rs new file mode 100644 index 0000000..759e35f --- /dev/null +++ b/mingling_pathf/test/src/test_files/test_pack.rs @@ -0,0 +1,17 @@ +mingling::macros::pack!(ResultPack1 = String); +mingling::macros::pack_err!(ErrorPack1); +mingling::macros::pack_err!(ErrorPack2 = PathBuf); + +pack!(ResultPack2 = (u8, String)); +pack_err!(ErrorPack3); +pack_err!(ErrorPack4 = PathBuf); + +pub mod sub { + mingling::macros::pack!(ResultPack1 = String); + mingling::macros::pack_err!(ErrorPack1); + mingling::macros::pack_err!(ErrorPack2 = PathBuf); + + pack!(ResultPack2 = (u8, String)); + pack_err!(ErrorPack3); + pack_err!(ErrorPack4 = PathBuf); +} diff --git a/mingling_pathf/test/src/test_files/test_renderer.rs b/mingling_pathf/test/src/test_files/test_renderer.rs new file mode 100644 index 0000000..ea52f5c --- /dev/null +++ b/mingling_pathf/test/src/test_files/test_renderer.rs @@ -0,0 +1,33 @@ +#[mingling::macros::renderer] +fn my_renderer1(prev: Some1) { +} + +#[mingling::macros::renderer] +pub fn my_renderer2(prev: Some2) { +} + +#[renderer] +fn my_renderer3(prev: Some3) { +} + +#[renderer] +pub fn my_renderer4(prev: Some4) { +} + +pub mod sub { + #[mingling::macros::renderer] + fn my_renderer1(prev: Some1) { + } + + #[mingling::macros::renderer] + pub fn my_renderer2(prev: Some2) { + } + + #[renderer] + fn my_renderer3(prev: Some3) { + } + + #[renderer] + pub fn my_renderer4(prev: Some4) { + } +} |
