aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md58
-rw-r--r--docs/example-pages/examples.json34
-rw-r--r--examples/example-combine-pathf-metadata/Cargo.lock226
-rw-r--r--examples/example-combine-pathf-metadata/Cargo.toml24
-rw-r--r--examples/example-combine-pathf-metadata/build.rs3
-rw-r--r--examples/example-combine-pathf-metadata/page.toml10
-rw-r--r--examples/example-combine-pathf-metadata/src/main.rs33
-rw-r--r--examples/example-combine-pathf-metadata/src/sub/mod.rs71
-rw-r--r--examples/example-metadata/Cargo.lock216
-rw-r--r--examples/example-metadata/Cargo.toml9
-rw-r--r--examples/example-metadata/page.toml10
-rw-r--r--examples/example-metadata/src/main.rs120
-rw-r--r--examples/test-examples.toml35
-rw-r--r--mingling/src/example_docs.rs202
-rw-r--r--mingling/src/gen_program.rs5
-rw-r--r--mingling/src/lib.rs7
-rw-r--r--mingling/src/metadata.rs2
-rw-r--r--mingling/src/metadata/description.rs57
-rw-r--r--mingling_core/src/asset.rs1
-rw-r--r--mingling_core/src/asset/metadata.rs14
-rw-r--r--mingling_core/src/comp.rs6
-rw-r--r--mingling_core/src/lib.rs1
-rw-r--r--mingling_core/src/program/collection.rs12
-rw-r--r--mingling_macros/src/attr.rs1
-rw-r--r--mingling_macros/src/attr/metadata.rs87
-rw-r--r--mingling_macros/src/func.rs1
-rw-r--r--mingling_macros/src/func/program_final_gen.rs35
-rw-r--r--mingling_macros/src/func/register_metadata.rs67
-rw-r--r--mingling_macros/src/lib.rs62
-rw-r--r--mingling_pathf/src/pattern_analyzer.rs1
-rw-r--r--mingling_pathf/src/patterns.rs2
-rw-r--r--mingling_pathf/src/patterns/metadata.rs165
-rw-r--r--mingling_pathf/test/src/lib.rs33
-rw-r--r--mingling_pathf/test/src/test_files/test_metadata.rs40
34 files changed, 1648 insertions, 2 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa80b79..1608466 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -209,6 +209,64 @@ None
- `mingling_macros/src/attr/completion.rs` updated to detect the `EntryFallback` identifier in attribute arguments.
- `CompletionHelper::complete` in `mingling_core/src/comp.rs` now invokes the fallback completion handler via `P::do_comp(&P::build_entry_fallback(vec![]), ctx)` and merges results with `Suggest::combine()`.
+9. **[`core`]** **[`macros`]** Added a compile-time **entry metadata** system that allows attaching arbitrary, compile-time-typed metadata to entries and retrieving it at runtime.
+
+ - **`Metadata<B>` trait** — Added to `mingling_core::asset::metadata` and re-exported from `mingling_core` / `mingling`. A type implementing `Metadata<B>` for an entry variant `E` provides `init_metadata() -> B`, defining the metadata value for that entry.
+
+ - **`#[metadata(EntryVariant)]` attribute macro** — Added `mingling_macros::metadata`, which converts a zero-argument function into:
+ - an `impl ::mingling::Metadata<ReturnType> for EntryVariant` whose `init_metadata()` calls the original function,
+ - a `register_metadata!(EntryVariant, ReturnType)` invocation that populates the global `METADATA` registry,
+ - the preserved original function unchanged (including attributes, visibility, and return signature).
+
+ Requirements: the function must take no parameters, must have an explicit return type, and cannot be async.
+
+ - **`register_metadata!(EntryVariant, MetadataType)` macro** — Added `mingling_macros::register_metadata` (doc-hidden) which parses the two type arguments and stores a match-arm-style string entry in the `METADATA` global registry for later consumption by `gen_program!`.
+
+ - **`ProgramCollect::get_metadata<T>(member_id) -> Option<T>`** — Added a default method on `ProgramCollect` that returns `None`. The `gen_program!` macro now overrides it: if the `METADATA` registry is non-empty, it generates a `get_metadata` implementation that matches on the enum member, compares the requested `TypeId::of::<T>()` against each registered metadata type's `TypeId`, and downcasts the boxed `Any` to `T`.
+
+ - **`pathf` integration** — Added `MetadataPattern` to `mingling_pathf` that matches functions annotated `#[metadata(BindType)]`, extracting both the `BindType` (attribute argument, always a local in-crate entry type) and the `DataType` (the function's return type — resolved as local or foreign via `use` imports) so `pathf` emits the appropriate `use` statements for `gen_program!`.
+
+ Usage:
+
+ ```rust,ignore
+ #[metadata(EntryGreet)]
+ pub fn greet_desc() -> Description {
+ Description { desc: "ok".to_string() }
+ }
+
+ // Later, at runtime:
+ let desc = ThisProgram::get_metadata::<Description>(ThisProgram::EntryGreet);
+ ```
+
+ The `#[metadata]` attribute and `Metadata` trait are re-exported as `mingling::macros::metadata` and `mingling::Metadata` respectively.
+
+10. **[`metadata:description`]** Added the `mingling::metadata` module and the `Description` convention metadata type. The `Description` type provides a human-readable description for any `Grouped` type, designed to be attached via the `#[metadata]` attribute macro introduced in item 9 above.
+
+ The `Description` struct wraps a `String` and provides:
+
+ - **`Description::new<S: Into<String>>(desc: S) -> Description`** — Constructs a new `Description` from any value convertible to `String`.
+ - **`From<String>`** / **`From<&str>`** — Constructs a `Description` from an owned `String` or a string slice.
+ - **`From<Description> for String`** / **`From<&Description> for String`** — Extracts the inner `String` (or a clone) from a `Description` value.
+ - **`Deref<Target = str>`** / **`DerefMut`** — Allows `Description` to be used transparently as a `str`, so string methods (`len()`, `contains()`, etc.) work directly on it.
+ - **`Display`** — Formats the description as its inner string, so `Description` can be used directly with `format!`, `print!`, and `String::from`-style operations.
+
+ Usage:
+
+ ```rust,ignore
+ use mingling::metadata::Description;
+
+ #[metadata(EntryGreet)]
+ pub fn greet_desc() -> Description {
+ Description::new("Greets the user by name.")
+ }
+
+ // Later, at runtime:
+ let desc = ThisProgram::get_metadata::<Description>(ThisProgram::EntryGreet);
+ println!("{desc}"); // "Greets the user by name."
+ ```
+
+ The module is gated behind the `core` feature and re-exported as `mingling::metadata`. This type is designed to work hand-in-hand with the compile-time entry metadata system from item 9, providing a first-party convention metadata for describing entries in generated documentation and help output.
+
#### **BREAKING CHANGES** (API CHANGES):
1. **[`macros`]** **[BREAKING]** Renamed the `extra_macros` feature to `extras`. All feature-gated macro re-exports in `mingling/src/lib.rs` (and throughout the codebase) have been updated from `#[cfg(feature = "extra_macros")]` to `#[cfg(feature = "extras")]`.
diff --git a/docs/example-pages/examples.json b/docs/example-pages/examples.json
index bde9d35..31336f1 100644
--- a/docs/example-pages/examples.json
+++ b/docs/example-pages/examples.json
@@ -95,6 +95,24 @@
]
},
{
+ "id": "example-combine-pathf-metadata",
+ "name": "Pathfinder + Metadata",
+ "icon": "🧭",
+ "category": "advanced",
+ "desc": "Combines the `pathf` feature with entry metadata. The metadata `DataType` and the entry `BindType` are defined inside a submodule, and `pathf` resolves them for `gen_program!()` at build time.\n",
+ "tags": [
+ "pathf",
+ "metadata",
+ "build.rs"
+ ],
+ "files": [
+ "src/main.rs",
+ "src/sub/mod.rs",
+ "build.rs",
+ "Cargo.toml"
+ ]
+ },
+ {
"id": "example-command-macro",
"name": "Command Macro",
"icon": "🚀",
@@ -254,6 +272,22 @@
]
},
{
+ "id": "example-metadata",
+ "name": "Entry Metadata",
+ "icon": "🏷️",
+ "category": "advanced",
+ "desc": "Demonstrates attaching arbitrary, compile-time-typed metadata to an entry via `#[metadata(Entry)]` and retrieving it at runtime with `ProgramCollect::get_metadata`. No `pathf` needed here — everything lives in a single module.\n",
+ "tags": [
+ "metadata",
+ "get_metadata",
+ "extras"
+ ],
+ "files": [
+ "src/main.rs",
+ "Cargo.toml"
+ ]
+ },
+ {
"id": "example-outside-type",
"name": "Outside Type",
"icon": "🆕",
diff --git a/examples/example-combine-pathf-metadata/Cargo.lock b/examples/example-combine-pathf-metadata/Cargo.lock
new file mode 100644
index 0000000..6fb41c4
--- /dev/null
+++ b/examples/example-combine-pathf-metadata/Cargo.lock
@@ -0,0 +1,226 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "example-combine-pathf-metadata"
+version = "0.1.0"
+dependencies = [
+ "mingling",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "just_fmt"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91b935090fce9a995a79798a22d523f1742b202f57ad2d8fcab6ad3dff528baf"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "might_be_async"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca871cea620b07bd2e6da0c883891a25bead698c43a9ab64b0fd663a7a78d5f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "toml",
+]
+
+[[package]]
+name = "mingling"
+version = "0.3.0"
+dependencies = [
+ "mingling_core",
+ "mingling_macros",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.3.0"
+dependencies = [
+ "just_fmt",
+ "might_be_async",
+ "mingling_pathf",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.3.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "mingling_pathf"
+version = "0.3.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
diff --git a/examples/example-combine-pathf-metadata/Cargo.toml b/examples/example-combine-pathf-metadata/Cargo.toml
new file mode 100644
index 0000000..fd14c74
--- /dev/null
+++ b/examples/example-combine-pathf-metadata/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "example-combine-pathf-metadata"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies.mingling]
+path = "../../mingling"
+features = [
+ # `extras` is required by the implicit `dispatcher!("hello")` form
+ "extras",
+ # `pathf` resolves types across modules at build time
+ "pathf",
+]
+
+[build-dependencies.mingling]
+path = "../../mingling"
+features = [
+ # Enable the `build` feature for build-time support
+ "build",
+ # `pathf` must also be enabled in build-dependencies
+ "pathf",
+]
+
+[workspace]
diff --git a/examples/example-combine-pathf-metadata/build.rs b/examples/example-combine-pathf-metadata/build.rs
new file mode 100644
index 0000000..d909431
--- /dev/null
+++ b/examples/example-combine-pathf-metadata/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ mingling::build::analyze_and_build_type_mapping().unwrap();
+}
diff --git a/examples/example-combine-pathf-metadata/page.toml b/examples/example-combine-pathf-metadata/page.toml
new file mode 100644
index 0000000..76c1700
--- /dev/null
+++ b/examples/example-combine-pathf-metadata/page.toml
@@ -0,0 +1,10 @@
+[example]
+id = "example-combine-pathf-metadata"
+name = "Pathfinder + Metadata"
+icon = "🧭"
+category = "advanced"
+desc = """
+Combines the `pathf` feature with entry metadata. The metadata `DataType` and the entry `BindType` are defined inside a submodule, and `pathf` resolves them for `gen_program!()` at build time.
+"""
+tags = ["pathf", "metadata", "build.rs"]
+files = ["src/main.rs", "src/sub/mod.rs", "build.rs", "Cargo.toml"]
diff --git a/examples/example-combine-pathf-metadata/src/main.rs b/examples/example-combine-pathf-metadata/src/main.rs
new file mode 100644
index 0000000..a68eac5
--- /dev/null
+++ b/examples/example-combine-pathf-metadata/src/main.rs
@@ -0,0 +1,33 @@
+//! Example: Combining pathf + entry metadata
+//!
+//! > Demonstrates combining the `pathf` feature with entry metadata. The metadata
+//! > `DataType` (`Description`) and the dispatchers/entries are defined in the `sub`
+//! > module. Thanks to `pathf`, `gen_program!()` resolves these types across
+//! > modules automatically, so `main` stays minimal.
+//!
+//! Run:
+//! ```bash
+//! cargo run --manifest-path examples/example-combine-pathf-metadata/Cargo.toml --quiet -- hello Alice
+//! cargo run --manifest-path examples/example-combine-pathf-metadata/Cargo.toml --quiet -- hello
+//! cargo run --manifest-path examples/example-combine-pathf-metadata/Cargo.toml --quiet -- desc
+//! ```
+//!
+//! Output:
+//! ```plaintext
+//! Hello, Alice!
+//! Hello, World!
+//! EntryHello desc = okay
+//! ```
+
+mod sub;
+
+use mingling::prelude::*;
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(sub::CMDHello);
+ program.with_dispatcher(sub::CMDDescription);
+ program.exec_and_exit();
+}
+
+gen_program!();
diff --git a/examples/example-combine-pathf-metadata/src/sub/mod.rs b/examples/example-combine-pathf-metadata/src/sub/mod.rs
new file mode 100644
index 0000000..2e6776e
--- /dev/null
+++ b/examples/example-combine-pathf-metadata/src/sub/mod.rs
@@ -0,0 +1,71 @@
+use crate::Next;
+use crate::ThisProgram;
+use mingling::ProgramCollect;
+use mingling::macros::metadata;
+use mingling::prelude::*;
+use std::io::Write;
+
+// Implicit dispatcher form — creates `CMDHello` / `EntryHello` in this module
+dispatcher!("hello");
+// Creates `CMDDescription` / `EntryDescription` in this module
+dispatcher!("desc", CMDDescription => EntryDescription);
+
+/// The metadata type attached to an entry (`DataType`).
+#[derive(Debug, PartialEq, Eq)]
+pub struct Description {
+ pub desc: String,
+}
+
+/// Attach a `Description` to `EntryHello`.
+///
+/// - `BindType` = `EntryHello` (the enum variant / entry type)
+/// - `DataType` = `Description` (the function's return type)
+#[metadata(EntryHello)]
+pub fn hello_desc() -> Description {
+ Description {
+ desc: "okay".to_string(),
+ }
+}
+
+pack!(ResultName = String);
+pack!(DescResult = String);
+
+/// Chain for `hello` — reads the name and produces a `ResultName`.
+#[chain]
+pub fn handle_hello(args: EntryHello) -> Next {
+ let name: ResultName = args
+ .inner
+ .first()
+ .cloned()
+ .unwrap_or_else(|| "World".to_string())
+ .into();
+ name.into()
+}
+
+/// Chain for `desc` — looks up the metadata bound to `EntryHello`.
+#[chain]
+pub fn handle_desc(_args: EntryDescription) -> Next {
+ // --------- IMPORTANT ---------
+ let msg = match ThisProgram::get_metadata::<Description>(ThisProgram::EntryHello) {
+ Some(d) => format!("EntryHello desc = {}", d.desc),
+ None => "EntryHello has no description".to_string(),
+ };
+ // --------- IMPORTANT ---------
+ DescResult::new(msg).to_render()
+}
+
+/// Renders the greeting message with the provided name.
+#[renderer]
+pub fn render_name(name: ResultName) -> RenderResult {
+ let mut render_result = RenderResult::new();
+ writeln!(render_result, "Hello, {}!", *name).ok();
+ render_result
+}
+
+/// Renders the metadata query result.
+#[renderer]
+pub fn render_desc(msg: DescResult) -> RenderResult {
+ let mut render_result = RenderResult::new();
+ writeln!(render_result, "{}", *msg).ok();
+ render_result
+}
diff --git a/examples/example-metadata/Cargo.lock b/examples/example-metadata/Cargo.lock
new file mode 100644
index 0000000..ec3d4f2
--- /dev/null
+++ b/examples/example-metadata/Cargo.lock
@@ -0,0 +1,216 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "example-metadata"
+version = "0.1.0"
+dependencies = [
+ "mingling",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "just_fmt"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91b935090fce9a995a79798a22d523f1742b202f57ad2d8fcab6ad3dff528baf"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "might_be_async"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca871cea620b07bd2e6da0c883891a25bead698c43a9ab64b0fd663a7a78d5f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "toml",
+]
+
+[[package]]
+name = "mingling"
+version = "0.3.0"
+dependencies = [
+ "mingling_core",
+ "mingling_macros",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.3.0"
+dependencies = [
+ "just_fmt",
+ "might_be_async",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.3.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
diff --git a/examples/example-metadata/Cargo.toml b/examples/example-metadata/Cargo.toml
new file mode 100644
index 0000000..eb34960
--- /dev/null
+++ b/examples/example-metadata/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "example-metadata"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+mingling = { path = "../../mingling" }
+
+[workspace]
diff --git a/examples/example-metadata/page.toml b/examples/example-metadata/page.toml
new file mode 100644
index 0000000..1a99048
--- /dev/null
+++ b/examples/example-metadata/page.toml
@@ -0,0 +1,10 @@
+[example]
+id = "example-metadata"
+name = "Entry Metadata"
+icon = "🏷️"
+category = "advanced"
+desc = """
+Demonstrates attaching arbitrary, compile-time-typed metadata to an entry via `#[metadata(Entry)]` and retrieving it at runtime with `ProgramCollect::get_metadata`. No `pathf` needed here — everything lives in a single module.
+"""
+tags = ["metadata", "get_metadata", "extras"]
+files = ["src/main.rs", "Cargo.toml"]
diff --git a/examples/example-metadata/src/main.rs b/examples/example-metadata/src/main.rs
new file mode 100644
index 0000000..855241e
--- /dev/null
+++ b/examples/example-metadata/src/main.rs
@@ -0,0 +1,120 @@
+//! Example: Entry Metadata (no `pathf`)
+//!
+//! > Demonstrates attaching arbitrary, compile-time-typed metadata (`Description`)
+//! > to an entry via `#[metadata(Entry)]`, and retrieving it at runtime through
+//! > `ProgramCollect::get_metadata`. The `desc` and `nodoc` subcommands dispatch
+//! > through the normal chain/render pipeline — exactly like `example-basic`.
+//!
+//! Run:
+//! ```bash
+//! cargo run --manifest-path examples/example-metadata/Cargo.toml --quiet -- greet Alice
+//! cargo run --manifest-path examples/example-metadata/Cargo.toml --quiet -- greet
+//! cargo run --manifest-path examples/example-metadata/Cargo.toml --quiet -- desc
+//! cargo run --manifest-path examples/example-metadata/Cargo.toml --quiet -- nodoc
+//! ```
+//!
+//! Output:
+//! ```plaintext
+//! Hello, Alice!
+//! Hello, World!
+//! EntryGreet desc = ok
+//! EntryDescription has no description
+//! ```
+
+use mingling::{macros::metadata, prelude::*};
+use std::io::Write;
+
+// Define the `greet` subcommand
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+// Define the `desc` subcommand, which queries metadata bound to EntryGreet
+dispatcher!("desc", CMDDescription => EntryDescription);
+
+// Define the `nodoc` subcommand, which queries metadata for an entry that has none
+dispatcher!("nodoc", CMDNoDescription => EntryNoDescription);
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(CMDGreet);
+ program.with_dispatcher(CMDDescription);
+ program.with_dispatcher(CMDNoDescription);
+ program.exec_and_exit();
+}
+
+/// The metadata type attached to an entry.
+#[derive(Debug, PartialEq, Eq)]
+pub struct Description {
+ pub desc: String,
+}
+
+// --------- IMPORTANT ---------
+/// Attach a `Description` to `EntryGreet`.
+///
+/// - `BindType` = `EntryGreet` (the enum variant / entry type)
+/// - `DataType` = `Description` (the function's return type)
+#[metadata(EntryGreet)]
+pub fn greet_desc() -> Description {
+ Description {
+ desc: "ok".to_string(),
+ }
+}
+// --------- IMPORTANT ---------
+
+pack!(ResultName = String);
+pack!(DescResult = String);
+
+/// Chain for `greet` — reads the name and produces a `ResultName`.
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name: ResultName = args
+ .inner
+ .first()
+ .cloned()
+ .unwrap_or_else(|| "World".to_string())
+ .into();
+ name.into()
+}
+
+/// Chain for `desc` — looks up the metadata bound to `EntryGreet`.
+#[chain]
+fn handle_desc(_args: EntryDescription) -> Next {
+ use mingling::ProgramCollect;
+ // --------- IMPORTANT ---------
+ let msg = match ThisProgram::get_metadata::<Description>(ThisProgram::EntryGreet) {
+ Some(d) => format!("EntryGreet desc = {}", d.desc),
+ None => "EntryGreet has no description".to_string(),
+ };
+ // --------- IMPORTANT ---------
+ DescResult::new(msg).to_render()
+}
+
+/// Chain for `nodoc` — asks for metadata on an entry that has none.
+#[chain]
+fn handle_nodoc(_args: EntryNoDescription) -> Next {
+ use mingling::ProgramCollect;
+ // --------- IMPORTANT ---------
+ let msg = match ThisProgram::get_metadata::<Description>(ThisProgram::EntryDescription) {
+ Some(d) => format!("EntryDescription desc = {}", d.desc),
+ None => "EntryDescription has no description".to_string(),
+ };
+ // --------- IMPORTANT ---------
+ DescResult::new(msg).to_render()
+}
+
+/// Renders the greeting message with the provided name.
+#[renderer]
+fn render_name(name: ResultName) -> RenderResult {
+ let mut render_result = RenderResult::new();
+ writeln!(render_result, "Hello, {}!", *name).ok();
+ render_result
+}
+
+/// Renders the metadata query result.
+#[renderer]
+fn render_desc(msg: DescResult) -> RenderResult {
+ let mut render_result = RenderResult::new();
+ writeln!(render_result, "{}", *msg).ok();
+ render_result
+}
+
+gen_program!();
diff --git a/examples/test-examples.toml b/examples/test-examples.toml
index b74c9a5..03df5e1 100644
--- a/examples/test-examples.toml
+++ b/examples/test-examples.toml
@@ -327,3 +327,38 @@ expect.result = "Hello, Alice"
command = "goodbye"
expect.exit-code = 0
expect.result = "Goodbye!"
+
+[[test.example-metadata]]
+command = "greet"
+expect.exit-code = 0
+expect.result = "Hello, World!"
+
+[[test.example-metadata]]
+command = "greet Alice"
+expect.exit-code = 0
+expect.result = "Hello, Alice!"
+
+[[test.example-metadata]]
+command = "desc"
+expect.exit-code = 0
+expect.result = "EntryGreet desc = ok"
+
+[[test.example-metadata]]
+command = "nodoc"
+expect.exit-code = 0
+expect.result = "EntryDescription has no description"
+
+[[test.example-combine-pathf-metadata]]
+command = "hello"
+expect.exit-code = 0
+expect.result = "Hello, World!"
+
+[[test.example-combine-pathf-metadata]]
+command = "hello Alice"
+expect.exit-code = 0
+expect.result = "Hello, Alice!"
+
+[[test.example-combine-pathf-metadata]]
+command = "desc"
+expect.exit-code = 0
+expect.result = "EntryHello desc = okay"
diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs
index cb6bbe7..e61551b 100644
--- a/mingling/src/example_docs.rs
+++ b/mingling/src/example_docs.rs
@@ -768,6 +768,71 @@ pub mod example_clap_binding {}
/// gen_program!();
/// ```
pub mod example_combine_pathf_dispatch_tree {}
+/// Example: Combining pathf + entry metadata
+///
+/// > Demonstrates combining the `pathf` feature with entry metadata. The metadata
+/// > `DataType` (`Description`) and the dispatchers/entries are defined in the `sub`
+/// > module. Thanks to `pathf`, `gen_program!()` resolves these types across
+/// > modules automatically, so `main` stays minimal.
+///
+/// Run:
+/// ```bash
+/// cargo run --manifest-path examples/example-combine-pathf-metadata/Cargo.toml --quiet -- hello Alice
+/// cargo run --manifest-path examples/example-combine-pathf-metadata/Cargo.toml --quiet -- hello
+/// cargo run --manifest-path examples/example-combine-pathf-metadata/Cargo.toml --quiet -- desc
+/// ```
+///
+/// Output:
+/// ```plaintext
+/// Hello, Alice!
+/// Hello, World!
+/// EntryHello desc = okay
+/// ```
+///
+/// Source code (./Cargo.toml)
+/// ```toml
+/// [package]
+/// name = "example-combine-pathf-metadata"
+/// version = "0.1.0"
+/// edition = "2024"
+///
+/// [dependencies.mingling]
+/// path = "../../mingling"
+/// features = [
+/// # `extras` is required by the implicit `dispatcher!("hello")` form
+/// "extras",
+/// # `pathf` resolves types across modules at build time
+/// "pathf",
+/// ]
+///
+/// [build-dependencies.mingling]
+/// path = "../../mingling"
+/// features = [
+/// # Enable the `build` feature for build-time support
+/// "build",
+/// # `pathf` must also be enabled in build-dependencies
+/// "pathf",
+/// ]
+///
+/// [workspace]
+/// ```
+///
+/// Source code (./src/main.rs)
+/// ```ignore
+/// mod sub;
+///
+/// use mingling::prelude::*;
+///
+/// fn main() {
+/// let mut program = ThisProgram::new();
+/// program.with_dispatcher(sub::CMDHello);
+/// program.with_dispatcher(sub::CMDDescription);
+/// program.exec_and_exit();
+/// }
+///
+/// gen_program!();
+/// ```
+pub mod example_combine_pathf_metadata {}
/// Example Command Macro
///
/// > Introduced how to use the `#[command]` macro to generate commands with minimal boilerplate
@@ -1935,6 +2000,143 @@ pub mod example_implicit_dispatcher {}
/// gen_program!();
/// ```
pub mod example_lazy_resources {}
+/// Example: Entry Metadata (no `pathf`)
+///
+/// > Demonstrates attaching arbitrary, compile-time-typed metadata (`Description`)
+/// > to an entry via `#[metadata(Entry)]`, and retrieving it at runtime through
+/// > `ProgramCollect::get_metadata`. The `desc` and `nodoc` subcommands dispatch
+/// > through the normal chain/render pipeline — exactly like `example-basic`.
+///
+/// Run:
+/// ```bash
+/// cargo run --manifest-path examples/example-metadata/Cargo.toml --quiet -- greet Alice
+/// cargo run --manifest-path examples/example-metadata/Cargo.toml --quiet -- greet
+/// cargo run --manifest-path examples/example-metadata/Cargo.toml --quiet -- desc
+/// cargo run --manifest-path examples/example-metadata/Cargo.toml --quiet -- nodoc
+/// ```
+///
+/// Output:
+/// ```plaintext
+/// Hello, Alice!
+/// Hello, World!
+/// EntryGreet desc = ok
+/// EntryDescription has no description
+/// ```
+///
+/// Source code (./Cargo.toml)
+/// ```toml
+/// [package]
+/// name = "example-metadata"
+/// version = "0.1.0"
+/// edition = "2024"
+///
+/// [dependencies]
+/// mingling = { path = "../../mingling" }
+///
+/// [workspace]
+/// ```
+///
+/// Source code (./src/main.rs)
+/// ```ignore
+/// use mingling::{macros::metadata, prelude::*};
+/// use std::io::Write;
+///
+/// // Define the `greet` subcommand
+/// dispatcher!("greet", CMDGreet => EntryGreet);
+///
+/// // Define the `desc` subcommand, which queries metadata bound to EntryGreet
+/// dispatcher!("desc", CMDDescription => EntryDescription);
+///
+/// // Define the `nodoc` subcommand, which queries metadata for an entry that has none
+/// dispatcher!("nodoc", CMDNoDescription => EntryNoDescription);
+///
+/// fn main() {
+/// let mut program = ThisProgram::new();
+/// program.with_dispatcher(CMDGreet);
+/// program.with_dispatcher(CMDDescription);
+/// program.with_dispatcher(CMDNoDescription);
+/// program.exec_and_exit();
+/// }
+///
+/// /// The metadata type attached to an entry.
+/// #[derive(Debug, PartialEq, Eq)]
+/// pub struct Description {
+/// pub desc: String,
+/// }
+///
+/// // --------- IMPORTANT ---------
+/// /// Attach a `Description` to `EntryGreet`.
+/// ///
+/// /// - `BindType` = `EntryGreet` (the enum variant / entry type)
+/// /// - `DataType` = `Description` (the function's return type)
+/// #[metadata(EntryGreet)]
+/// pub fn greet_desc() -> Description {
+/// Description {
+/// desc: "ok".to_string(),
+/// }
+/// }
+/// // --------- IMPORTANT ---------
+///
+/// pack!(ResultName = String);
+/// pack!(DescResult = String);
+///
+/// /// Chain for `greet` — reads the name and produces a `ResultName`.
+/// #[chain]
+/// fn handle_greet(args: EntryGreet) -> Next {
+/// let name: ResultName = args
+/// .inner
+/// .first()
+/// .cloned()
+/// .unwrap_or_else(|| "World".to_string())
+/// .into();
+/// name.into()
+/// }
+///
+/// /// Chain for `desc` — looks up the metadata bound to `EntryGreet`.
+/// #[chain]
+/// fn handle_desc(_args: EntryDescription) -> Next {
+/// use mingling::ProgramCollect;
+/// // --------- IMPORTANT ---------
+/// let msg = match ThisProgram::get_metadata::<Description>(ThisProgram::EntryGreet) {
+/// Some(d) => format!("EntryGreet desc = {}", d.desc),
+/// None => "EntryGreet has no description".to_string(),
+/// };
+/// // --------- IMPORTANT ---------
+/// DescResult::new(msg).to_render()
+/// }
+///
+/// /// Chain for `nodoc` — asks for metadata on an entry that has none.
+/// #[chain]
+/// fn handle_nodoc(_args: EntryNoDescription) -> Next {
+/// use mingling::ProgramCollect;
+/// // --------- IMPORTANT ---------
+/// let msg = match ThisProgram::get_metadata::<Description>(ThisProgram::EntryDescription) {
+/// Some(d) => format!("EntryDescription desc = {}", d.desc),
+/// None => "EntryDescription has no description".to_string(),
+/// };
+/// // --------- IMPORTANT ---------
+/// DescResult::new(msg).to_render()
+/// }
+///
+/// /// Renders the greeting message with the provided name.
+/// #[renderer]
+/// fn render_name(name: ResultName) -> RenderResult {
+/// let mut render_result = RenderResult::new();
+/// writeln!(render_result, "Hello, {}!", *name).ok();
+/// render_result
+/// }
+///
+/// /// Renders the metadata query result.
+/// #[renderer]
+/// fn render_desc(msg: DescResult) -> RenderResult {
+/// let mut render_result = RenderResult::new();
+/// writeln!(render_result, "{}", *msg).ok();
+/// render_result
+/// }
+///
+/// gen_program!();
+/// ```
+pub mod example_metadata {}
/// Example: Using the `group!()` Macro to Register Outside Types
///
/// This example demonstrates how to use the `group!()` macro to make outside
diff --git a/mingling/src/gen_program.rs b/mingling/src/gen_program.rs
index a3b7e29..ef32074 100644
--- a/mingling/src/gen_program.rs
+++ b/mingling/src/gen_program.rs
@@ -212,6 +212,11 @@ impl ProgramCollect for ThisProgram {
todo!()
}
+ fn get_metadata<T: 'static>(member_id: Self::Enum) -> Option<T> {
+ let _ = member_id;
+ todo!()
+ }
+
#[cfg(feature = "async")]
fn do_chain(
_any: mingling_core::AnyOutput<Self::Enum>,
diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs
index 108d61a..470c64b 100644
--- a/mingling/src/lib.rs
+++ b/mingling/src/lib.rs
@@ -16,6 +16,10 @@ pub mod CRATE_ROOT {
pub use crate::gen_program::*;
}
+/// Mingling's convention metadatas, which can be bound to types using `#[metadata]`, to provide identification for types
+#[cfg(feature = "core")]
+pub mod metadata;
+
#[cfg(feature = "core")]
mod example_docs;
@@ -76,6 +80,7 @@ pub mod macros {
#[cfg(all(feature = "structural_renderer", feature = "extras"))]
pub use mingling_macros::group_structural;
pub use mingling_macros::help;
+ pub use mingling_macros::metadata;
pub use mingling_macros::mlint;
pub use mingling_macros::node;
pub use mingling_macros::pack;
@@ -106,6 +111,8 @@ pub mod macros {
#[doc(hidden)]
pub use mingling_macros::register_help;
#[doc(hidden)]
+ pub use mingling_macros::register_metadata;
+ #[doc(hidden)]
pub use mingling_macros::register_renderer;
#[doc(hidden)]
pub use mingling_macros::register_type;
diff --git a/mingling/src/metadata.rs b/mingling/src/metadata.rs
new file mode 100644
index 0000000..329576c
--- /dev/null
+++ b/mingling/src/metadata.rs
@@ -0,0 +1,2 @@
+mod description;
+pub use description::*;
diff --git a/mingling/src/metadata/description.rs b/mingling/src/metadata/description.rs
new file mode 100644
index 0000000..48bf095
--- /dev/null
+++ b/mingling/src/metadata/description.rs
@@ -0,0 +1,57 @@
+/// Provides a description for any Grouped type.
+pub struct Description {
+ desc: String,
+}
+
+impl Description {
+ /// Creates a new `Description` instance.
+ pub fn new<S: Into<String>>(desc: S) -> Self {
+ Self { desc: desc.into() }
+ }
+}
+
+impl From<String> for Description {
+ fn from(desc: String) -> Self {
+ Self { desc }
+ }
+}
+
+impl From<&str> for Description {
+ fn from(desc: &str) -> Self {
+ Self {
+ desc: desc.to_string(),
+ }
+ }
+}
+
+impl From<Description> for String {
+ fn from(desc: Description) -> Self {
+ desc.desc
+ }
+}
+
+impl From<&Description> for String {
+ fn from(desc: &Description) -> Self {
+ desc.desc.clone()
+ }
+}
+
+impl std::ops::Deref for Description {
+ type Target = str;
+
+ fn deref(&self) -> &Self::Target {
+ &self.desc
+ }
+}
+
+impl std::ops::DerefMut for Description {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.desc
+ }
+}
+
+impl std::fmt::Display for Description {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.desc)
+ }
+}
diff --git a/mingling_core/src/asset.rs b/mingling_core/src/asset.rs
index 8c709ac..fc1c81b 100644
--- a/mingling_core/src/asset.rs
+++ b/mingling_core/src/asset.rs
@@ -5,6 +5,7 @@ pub(crate) mod enum_tag;
pub(crate) mod global_resource;
pub(crate) mod help;
pub(crate) mod lazy_resource;
+pub(crate) mod metadata;
pub(crate) mod node;
pub(crate) mod renderer;
pub(crate) mod routable;
diff --git a/mingling_core/src/asset/metadata.rs b/mingling_core/src/asset/metadata.rs
new file mode 100644
index 0000000..996b34c
--- /dev/null
+++ b/mingling_core/src/asset/metadata.rs
@@ -0,0 +1,14 @@
+/// Provides metadata for an Entry.
+///
+/// Any type can be attached to an Entry as metadata, allowing the program to
+/// carry compile-time-typed, arbitrary description data alongside each
+/// registered entry. The [`Metadata`] trait bridges an Entry type (`Self`) to
+/// an arbitrary metadata type `B`.
+///
+/// It is recommended to use the `#[metadata(Entry)]` attribute macro from
+/// [mingling_macros](https://crates.io/crates/mingling_macros) to implement this
+/// trait and register the entry via `register_metadata!`.
+pub trait Metadata<B> {
+ /// Initializes and returns the metadata value of type `B` for this entry.
+ fn init_metadata() -> B;
+}
diff --git a/mingling_core/src/comp.rs b/mingling_core/src/comp.rs
index 2317f06..5f851ca 100644
--- a/mingling_core/src/comp.rs
+++ b/mingling_core/src/comp.rs
@@ -182,7 +182,11 @@ impl CompletionHelper {
trace!("using default completion");
let fallback = P::do_comp(&P::build_entry_fallback(vec![]), ctx);
let default = default_completion::<P>(ctx);
- fallback.combine(default)
+ if fallback == Suggest::FileCompletion {
+ default
+ } else {
+ fallback.combine(default)
+ }
}
}
}
diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs
index cd42842..3a5acf4 100644
--- a/mingling_core/src/lib.rs
+++ b/mingling_core/src/lib.rs
@@ -70,6 +70,7 @@ pub use crate::asset::enum_tag::*;
pub use crate::asset::global_resource::*;
pub use crate::asset::help::*;
pub use crate::asset::lazy_resource::*;
+pub use crate::asset::metadata::*;
pub use crate::asset::node::*;
pub use crate::asset::renderer::*;
pub use crate::asset::routable::*;
diff --git a/mingling_core/src/program/collection.rs b/mingling_core/src/program/collection.rs
index 1b4d7dd..5b1152a 100644
--- a/mingling_core/src/program/collection.rs
+++ b/mingling_core/src/program/collection.rs
@@ -66,6 +66,18 @@ pub trait ProgramCollect {
/// Render help for Entry
fn render_help(any: AnyOutput<Self::Enum>) -> RenderResult;
+ /// Retrieves compile-time registered metadata of type `T` for the given
+ /// enum member, if any was registered via `#[metadata(Entry)]`.
+ ///
+ /// Returns `None` when no metadata of type `T` has been registered for the
+ /// provided member, or when the requested `T` does not match the registered
+ /// metadata type. The concrete implementation of this method is generated
+ /// by the `gen_program!` macro.
+ fn get_metadata<T: 'static>(member_id: Self::Enum) -> Option<T> {
+ let _ = member_id;
+ None
+ }
+
/// Find a matching chain to continue execution based on the input [AnyOutput](./struct.AnyOutput.html), returning a new [AnyOutput](./struct.AnyOutput.html)
#[cfg(feature = "async")]
fn do_chain(
diff --git a/mingling_macros/src/attr.rs b/mingling_macros/src/attr.rs
index 54fe2f1..59544a8 100644
--- a/mingling_macros/src/attr.rs
+++ b/mingling_macros/src/attr.rs
@@ -6,6 +6,7 @@ pub(crate) mod completion;
#[cfg(feature = "clap")]
pub(crate) mod dispatcher_clap;
pub(crate) mod help;
+pub(crate) mod metadata;
pub(crate) mod mlint;
#[cfg(feature = "extras")]
pub(crate) mod program_setup;
diff --git a/mingling_macros/src/attr/metadata.rs b/mingling_macros/src/attr/metadata.rs
new file mode 100644
index 0000000..b96e319
--- /dev/null
+++ b/mingling_macros/src/attr/metadata.rs
@@ -0,0 +1,87 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::spanned::Spanned;
+use syn::{Attribute, ItemFn, ReturnType, TypePath, parse_macro_input};
+
+/// Implements the `#[metadata(EntryVariant)]` attribute macro.
+///
+/// It takes the enum variant ident to attach metadata to, and rewrites the
+/// annotated function into:
+/// - an `impl ::mingling::Metadata<ReturnType> for EntryVariant` that calls the
+/// original function,
+/// - a `::mingling::macros::register_metadata!(EntryVariant, ReturnType)` call,
+/// - the preserved original function.
+pub(crate) fn metadata_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+ let entry_variant = parse_macro_input!(attr as syn::Ident);
+
+ let input_fn = parse_macro_input!(item as ItemFn);
+
+ // The metadata type is the function's return type.
+ let metadata_type = match &input_fn.sig.output {
+ ReturnType::Type(_, ty) => match syn::parse2::<TypePath>(quote! { #ty }) {
+ Ok(ty) => ty,
+ Err(e) => return e.to_compile_error().into(),
+ },
+ ReturnType::Default => {
+ return syn::Error::new(
+ input_fn.sig.span(),
+ "#[metadata] requires the function to have an explicit return type",
+ )
+ .to_compile_error()
+ .into();
+ }
+ };
+
+ // Preserve the original return type exactly as written, so the original
+ // function signature is reproduced unchanged.
+ let original_return_type = match &input_fn.sig.output {
+ ReturnType::Type(_, ty) => quote! { #ty },
+ ReturnType::Default => quote! { () },
+ };
+
+ // Reject async metadata functions: `Metadata::init_metadata` is synchronous.
+ if input_fn.sig.asyncness.is_some() {
+ return syn::Error::new(input_fn.sig.span(), "Metadata function cannot be async")
+ .to_compile_error()
+ .into();
+ }
+
+ let fn_name = &input_fn.sig.ident;
+ let vis = &input_fn.vis;
+ let original_inputs = input_fn.sig.inputs.clone();
+ let fn_body_stmts = &input_fn.block.stmts;
+
+ // Function attributes, excluding the metadata attribute itself.
+ let fn_attrs: Vec<&Attribute> = input_fn
+ .attrs
+ .iter()
+ .filter(|attr| !attr.path().is_ident("metadata"))
+ .collect();
+
+ // A metadata provider is a zero-argument function.
+ if !original_inputs.is_empty() {
+ return syn::Error::new(
+ input_fn.sig.span(),
+ "#[metadata] function cannot take any parameters",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ let expanded = quote! {
+ impl ::mingling::Metadata<#metadata_type> for #entry_variant {
+ fn init_metadata() -> #metadata_type {
+ #fn_name()
+ }
+ }
+
+ ::mingling::macros::register_metadata!(#entry_variant, #metadata_type);
+
+ #(#fn_attrs)*
+ #vis fn #fn_name(#original_inputs) -> #original_return_type {
+ #(#fn_body_stmts)*
+ }
+ };
+
+ expanded.into()
+}
diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs
index 9e0e15f..d566208 100644
--- a/mingling_macros/src/func.rs
+++ b/mingling_macros/src/func.rs
@@ -28,6 +28,7 @@ pub(crate) mod r_println;
pub(crate) mod register_chain;
pub(crate) mod register_dispatcher;
pub(crate) mod register_help;
+pub(crate) mod register_metadata;
pub(crate) mod register_renderer;
pub(crate) mod register_type;
#[cfg(feature = "extras")]
diff --git a/mingling_macros/src/func/program_final_gen.rs b/mingling_macros/src/func/program_final_gen.rs
index e8545f4..429e60c 100644
--- a/mingling_macros/src/func/program_final_gen.rs
+++ b/mingling_macros/src/func/program_final_gen.rs
@@ -8,6 +8,7 @@ use crate::COMPILE_TIME_DISPATCHERS;
#[cfg(feature = "comp")]
use crate::COMPLETIONS;
use crate::HELP_REQUESTS;
+use crate::METADATA;
use crate::PACKED_TYPES;
use crate::RENDERERS;
use crate::RENDERERS_EXIST;
@@ -269,6 +270,38 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream {
.map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
.collect();
+ let metadata_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&METADATA)
+ .lock()
+ .unwrap()
+ .clone()
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let get_metadata_fn = if metadata_tokens.is_empty() {
+ quote! {
+ fn get_metadata<T: 'static>(_member_id: Self::Enum) -> Option<T> {
+ None
+ }
+ }
+ } else {
+ let metadata_arms = metadata_tokens.iter().map(|entry| {
+ quote! {
+ #entry
+ }
+ });
+ quote! {
+ fn get_metadata<T: 'static>(member_id: Self::Enum) -> Option<T> {
+ let type_id = ::std::any::TypeId::of::<T>();
+ let any = match member_id {
+ #(#metadata_arms)*
+ _ => None,
+ };
+ any.and_then(|b| b.downcast::<T>().ok().map(|b| *b))
+ }
+ }
+ };
+
let num_variants = packed_types.len();
let repr_type = if u8::try_from(num_variants).is_ok() {
quote! { u8 }
@@ -313,6 +346,7 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream {
}
#render_fn
#do_chain_fn
+ #get_metadata_fn
fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
match any.member_id() {
#(#help_tokens)*
@@ -356,6 +390,7 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream {
get_global_set(&RENDERERS).lock().unwrap().clear();
get_global_set(&RENDERERS_EXIST).lock().unwrap().clear();
get_global_set(&HELP_REQUESTS).lock().unwrap().clear();
+ get_global_set(&METADATA).lock().unwrap().clear();
#[cfg(feature = "comp")]
get_global_set(&COMPLETIONS).lock().unwrap().clear();
#[cfg(feature = "dispatch_tree")]
diff --git a/mingling_macros/src/func/register_metadata.rs b/mingling_macros/src/func/register_metadata.rs
new file mode 100644
index 0000000..a1f9965
--- /dev/null
+++ b/mingling_macros/src/func/register_metadata.rs
@@ -0,0 +1,67 @@
+use proc_macro::TokenStream;
+use quote::ToTokens;
+use syn::TypePath;
+use syn::spanned::Spanned;
+
+use crate::METADATA;
+use crate::get_global_set;
+
+/// Parses and registers a metadata mapping of the form
+/// `register_metadata!(EntryGreet, Description)`.
+///
+/// Stores a match-arm-style string entry `Self::EntryGreet => { ... }` that is
+/// later consumed by `program_final_gen!` to generate the `get_metadata`
+/// method of `ProgramCollect`.
+pub(crate) fn register_metadata_impl(input: TokenStream) -> TokenStream {
+ // Parse the input as a comma-separated list of type arguments.
+ let input_parsed = syn::parse_macro_input!(
+ input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated
+ );
+
+ if input_parsed.len() != 2 {
+ return syn::Error::new(
+ input_parsed.span(),
+ "Expected exactly two comma-separated arguments: `EntryVariant, MetadataType`",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ let entry_expr = &input_parsed[0];
+ let metadata_expr = &input_parsed[1];
+
+ let entry_type = match syn::parse2::<TypePath>(entry_expr.to_token_stream()) {
+ Ok(ty) => ty,
+ Err(e) => return e.to_compile_error().into(),
+ };
+ let metadata_type = match syn::parse2::<TypePath>(metadata_expr.to_token_stream()) {
+ Ok(ty) => ty,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ let entry_str = build_metadata_entry(&entry_type, &metadata_type).to_string();
+
+ get_global_set(&METADATA).lock().unwrap().insert(entry_str);
+
+ quote::quote! {}.into()
+}
+
+/// Builds the match-arm entry for `get_metadata`, matching on the enum variant
+/// and then on the requested `TypeId`.
+fn build_metadata_entry(
+ entry_type: &TypePath,
+ metadata_type: &TypePath,
+) -> proc_macro2::TokenStream {
+ let enum_variant = entry_type.path.segments.last().unwrap().ident.clone();
+ quote::quote! {
+ Self::#enum_variant => {
+ let __metadata_type_id = ::std::any::TypeId::of::<#metadata_type>();
+ match type_id {
+ _ if type_id == __metadata_type_id => Some(::std::boxed::Box::new(
+ <#entry_type as ::mingling::Metadata<#metadata_type>>::init_metadata(),
+ ) as ::std::boxed::Box<dyn ::std::any::Any>),
+ _ => None,
+ }
+ }
+ }
+}
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index c955e36..ce3455e 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -165,7 +165,7 @@ use attr::completion;
use attr::dispatcher_clap;
#[cfg(feature = "extras")]
use attr::program_setup;
-use attr::{chain, help, renderer};
+use attr::{chain, help, metadata, renderer};
use derive::{enum_tag, grouped};
#[cfg(feature = "extras")]
use func::entry;
@@ -209,6 +209,7 @@ pub(crate) static RENDERERS: Registry = OnceLock::new();
pub(crate) static CHAINS_EXIST: Registry = OnceLock::new();
pub(crate) static RENDERERS_EXIST: Registry = OnceLock::new();
pub(crate) static HELP_REQUESTS: Registry = OnceLock::new();
+pub(crate) static METADATA: Registry = OnceLock::new();
/// Checks if a variant name already exists in a registered set.
/// Returns a `compile_error` token stream if a duplicate is found.
@@ -1297,6 +1298,26 @@ pub fn register_help(input: TokenStream) -> TokenStream {
func::register_help::register_help(input)
}
+/// Registers metadata mapping between an enum variant and a metadata type.
+///
+/// This macro is used internally by the `#[metadata]` attribute and is also
+/// available for manual registration if needed.
+///
+/// # Syntax
+///
+/// ```rust,ignore
+/// register_metadata!(EntryVariant, MetadataType);
+/// ```
+///
+/// This adds an entry to the global `METADATA` registry, mapping the enum
+/// variant for `EntryVariant` to the metadata provider trait
+/// `::mingling::Metadata<MetadataType>`. The entry is consumed by
+/// `gen_program!` to generate the `get_metadata` method of `ProgramCollect`.
+#[proc_macro]
+pub fn register_metadata(input: TokenStream) -> TokenStream {
+ func::register_metadata::register_metadata_impl(input)
+}
+
/// Registers a dispatcher at compile time for the `dispatch_tree` feature.
///
/// This macro is called internally by `dispatcher!` when the `dispatch_tree`
@@ -1404,6 +1425,45 @@ pub fn help(attr: TokenStream, item: TokenStream) -> TokenStream {
help::help_attr(item)
}
+/// Declares compile-time metadata for an entry variant.
+///
+/// The `#[metadata]` attribute attaches an arbitrary, compile-time-typed value
+/// to an entry. The annotated function becomes the provider for the metadata:
+/// its return type is the metadata type, and the attribute argument names the
+/// entry enum variant the metadata belongs to.
+///
+/// The macro works by:
+/// 1. Generating `impl ::mingling::Metadata<ReturnType> for EntryVariant` whose
+/// `init_metadata()` calls the annotated function.
+/// 2. Registering the entry via `register_metadata!` in the global `METADATA`
+/// registry so that `gen_program!` emits the `get_metadata` method.
+/// 3. Keeping the original function unchanged for direct calls.
+///
+/// # Syntax
+///
+/// ```rust,ignore
+/// #[metadata(EntryGreet)]
+/// fn greet_desc() -> Description {
+/// Description { desc: "ok".into() }
+/// }
+/// ```
+///
+/// The metadata is later retrieved with `ProgramCollect::get_metadata`:
+///
+/// ```rust,ignore
+/// let desc = ThisProgram::get_metadata::<Description>(ThisProgram::EntryGreet);
+/// ```
+///
+/// # Requirements
+///
+/// - The attribute argument must be the enum variant to attach metadata to.
+/// - The function must take no parameters and return a concrete type.
+/// - The function cannot be async.
+#[proc_macro_attribute]
+pub fn metadata(attr: TokenStream, item: TokenStream) -> TokenStream {
+ metadata::metadata_attr(attr, item)
+}
+
/// Marker attribute for the Mingling lint system.
///
/// The content of this attribute is ignored by rustc and reserved for
diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs
index 79b1c5a..4675dd1 100644
--- a/mingling_pathf/src/pattern_analyzer.rs
+++ b/mingling_pathf/src/pattern_analyzer.rs
@@ -36,6 +36,7 @@ pub fn init_with_config(config: PathfinderConfig) -> PatternAnalyzer {
analyzer.add_pattern(CommandPattern);
analyzer.add_pattern(RendererPattern);
analyzer.add_pattern(HelpPattern);
+ analyzer.add_pattern(MetadataPattern);
analyzer.add_pattern(CompletionPattern);
analyzer.add_pattern(DispatcherPattern::new(config.use_dispatch_tree));
analyzer.add_pattern(DispatcherClapPattern::new(config.use_dispatch_tree));
diff --git a/mingling_pathf/src/patterns.rs b/mingling_pathf/src/patterns.rs
index 964fe7c..fcc50c3 100644
--- a/mingling_pathf/src/patterns.rs
+++ b/mingling_pathf/src/patterns.rs
@@ -9,6 +9,7 @@ pub use dispatcher_clap::*;
pub use group::*;
pub use grouped_derive::*;
pub use help::*;
+pub use metadata::*;
pub use pack::*;
pub use renderer::*;
@@ -21,5 +22,6 @@ mod dispatcher_clap;
mod group;
mod grouped_derive;
mod help;
+mod metadata;
mod pack;
mod renderer;
diff --git a/mingling_pathf/src/patterns/metadata.rs b/mingling_pathf/src/patterns/metadata.rs
new file mode 100644
index 0000000..24243bf
--- /dev/null
+++ b/mingling_pathf/src/patterns/metadata.rs
@@ -0,0 +1,165 @@
+//! The `MetadataPattern` matches functions annotated with `#[metadata(BindType)]`
+//! and extracts two types referenced by the metadata system:
+//! - `BindType` — the entry enum variant the metadata is bound to (attribute argument)
+//! - `DataType` — the function's return type, i.e. the metadata type
+//!
+//! Both types are tracked so that `pathf` can emit the `use` statements needed to
+//! bring them into scope for `gen_program!` generated code.
+//!
+//! Example:
+//! ```ignore
+//! #[metadata(EntryGreet)] // BindType = EntryGreet
+//! pub fn get_desc() -> Description { ... } // DataType = Description
+//! ```
+
+use std::collections::HashMap;
+
+use syn::Item;
+use syn::UseTree;
+
+use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern};
+
+/// Matches `#[metadata(BindType)]` functions, extracting the bound entry type
+/// and the metadata (return) type.
+pub struct MetadataPattern;
+
+impl AnalyzePattern for MetadataPattern {
+ fn contains(&self, content: &str) -> bool {
+ content.contains("[metadata(") || content.contains("[metadata]")
+ }
+
+ fn analyze(&self, content: &str) -> Vec<AnalyzeItem> {
+ let Ok(syntax) = syn::parse_file(content) else {
+ return Vec::new();
+ };
+
+ let imports = collect_use_imports(&syntax.items);
+
+ let mut items = Vec::new();
+ for item in &syntax.items {
+ collect_from_item(item, "", &imports, &mut items);
+ }
+ items
+ }
+}
+
+fn collect_from_item(
+ item: &Item,
+ current_mod: &str,
+ imports: &HashMap<String, (String, String)>,
+ items: &mut Vec<AnalyzeItem>,
+) {
+ match item {
+ Item::Fn(f) => {
+ let Some(bind_type) = extract_bind_type(&f.attrs) else {
+ return;
+ };
+ let data_type = extract_data_type(f);
+ let Some(data_type) = data_type else {
+ return;
+ };
+
+ // BindType — always an in-crate entry type generated by dispatcher!/pack!.
+ items.push(AnalyzeItem::local(current_mod.to_string(), bind_type));
+
+ // DataType — may be a local type or a `use`-imported foreign type.
+ if let Some((module, _)) = imports.get(&data_type) {
+ items.push(AnalyzeItem::foreign(module.clone(), data_type));
+ } else {
+ items.push(AnalyzeItem::local(current_mod.to_string(), data_type));
+ }
+ }
+ 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}")
+ };
+ let inner_imports = collect_use_imports(nested);
+ for n in nested {
+ collect_from_item(n, &nested_mod, &inner_imports, items);
+ }
+ }
+ }
+ _ => {}
+ }
+}
+
+/// Extracts the `BindType` (the ident argument of `#[metadata(...)]`).
+fn extract_bind_type(attrs: &[syn::Attribute]) -> Option<String> {
+ for attr in attrs {
+ let path_ident = attr.path().segments.last()?.ident.to_string();
+ if path_ident != "metadata" {
+ continue;
+ }
+ if let syn::Meta::List(meta_list) = &attr.meta {
+ for token in meta_list.tokens.clone().into_iter() {
+ if let proc_macro2::TokenTree::Ident(ident) = token {
+ return Some(ident.to_string());
+ }
+ }
+ }
+ }
+ None
+}
+
+/// Extracts the `DataType` (the function's return type path last segment).
+fn extract_data_type(f: &syn::ItemFn) -> Option<String> {
+ let syn::ReturnType::Type(_, ty) = &f.sig.output else {
+ return None;
+ };
+ match ty.as_ref() {
+ syn::Type::Path(type_path) => type_path.path.segments.last().map(|s| s.ident.to_string()),
+ _ => None,
+ }
+}
+
+/// Collect `use` imports from a list of top-level items.
+///
+/// Returns a map of `short_name → (module_path, short_name)`.
+fn collect_use_imports(items: &[syn::Item]) -> HashMap<String, (String, String)> {
+ let mut map = HashMap::new();
+ for item in items {
+ if let Item::Use(use_item) = item {
+ collect_from_use_tree(&use_item.tree, "", &mut map);
+ }
+ }
+ map
+}
+
+/// Recursively traverse a `UseTree` and collect named imports.
+fn collect_from_use_tree(
+ tree: &UseTree,
+ prefix: &str,
+ map: &mut HashMap<String, (String, String)>,
+) {
+ match tree {
+ UseTree::Name(name) => {
+ let module = prefix.to_string();
+ let alias = name.ident.to_string();
+ map.entry(alias).or_insert((module, name.ident.to_string()));
+ }
+ UseTree::Path(use_path) => {
+ let new_prefix = if prefix.is_empty() {
+ use_path.ident.to_string()
+ } else {
+ format!("{}::{}", prefix, use_path.ident)
+ };
+ collect_from_use_tree(&use_path.tree, &new_prefix, map);
+ }
+ UseTree::Rename(rename) => {
+ let module = prefix.to_string();
+ let alias = rename.ident.to_string();
+ map.entry(alias)
+ .or_insert((module, rename.ident.to_string()));
+ }
+ UseTree::Glob(_) => {}
+ UseTree::Group(group) => {
+ for item in &group.items {
+ collect_from_use_tree(item, prefix, map);
+ }
+ }
+ }
+}
diff --git a/mingling_pathf/test/src/lib.rs b/mingling_pathf/test/src/lib.rs
index 7e7cbd5..8b13e52 100644
--- a/mingling_pathf/test/src/lib.rs
+++ b/mingling_pathf/test/src/lib.rs
@@ -387,3 +387,36 @@ fn test_dispatcher_clap_dispatch_tree() {
assert!(r2.contains("::sub::__internal_dispatcher_delete"));
assert!(r2.contains("::sub::__internal_dispatcher_helpcmd"));
}
+
+#[test]
+fn test_metadata_analyze() {
+ let analyzer = mingling_pathf::pattern_analyzer::init();
+ let file = current_dir()
+ .unwrap()
+ .join("src/test_files/test_metadata.rs");
+
+ let r = analyzer.analyze_file(file).unwrap();
+ let required: Vec<&str> = vec![
+ // Root: BindType + DataType pairs
+ "::EntryGreet1",
+ "::Description1",
+ "::EntryGreet2",
+ "::Description2",
+ "::EntryGreet3",
+ "::LocalType3",
+ "::EntryGreet4",
+ "::std::collections::HashMap",
+ "::EntryGreet5",
+ "::Qualified5",
+ // Sub: BindType + DataType pairs
+ "::sub::EntrySub1",
+ "::sub::SubType1",
+ "::sub::EntrySub2",
+ "::sub::SubType2",
+ ];
+
+ assert_eq!(r.len(), required.len());
+ for entry in &required {
+ assert!(r.contains(*entry), "Result should contain: {entry}");
+ }
+}
diff --git a/mingling_pathf/test/src/test_files/test_metadata.rs b/mingling_pathf/test/src/test_files/test_metadata.rs
new file mode 100644
index 0000000..52a2d4c
--- /dev/null
+++ b/mingling_pathf/test/src/test_files/test_metadata.rs
@@ -0,0 +1,40 @@
+// Root-level metadata functions
+#[mingling::macros::metadata(EntryGreet1)]
+pub fn get_desc1() -> Description1 {
+ Description1 {}
+}
+
+#[metadata(EntryGreet2)]
+fn get_desc2() -> Description2 {
+ Description2 {}
+}
+
+// Local DataType (defined in-crate) + foreign DataType
+#[metadata(EntryGreet3)]
+pub fn get_desc3() -> LocalType3 {
+ LocalType3 {}
+}
+
+use std::collections::HashMap;
+
+#[metadata(EntryGreet4)]
+fn get_desc4() -> HashMap<String, String> {
+ HashMap::new()
+}
+
+#[metadata(EntryGreet5)]
+pub fn get_desc5() -> crate::fully::Qualified5 {
+ crate::fully::Qualified5 {}
+}
+
+pub mod sub {
+ #[mingling::macros::metadata(EntrySub1)]
+ pub fn get_sub1() -> SubType1 {
+ SubType1 {}
+ }
+
+ #[metadata(EntrySub2)]
+ fn get_sub2() -> SubType2 {
+ SubType2 {}
+ }
+}