From 4588e17f7ddacd8391a5c881a209735e08d48797 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Tue, 4 Aug 2026 13:26:48 +0800 Subject: feat: add entry metadata system with compile-time typed values Add `Metadata` trait and `#[metadata(Entry)]` attribute macro to attach arbitrary, compile-time-typed metadata to entries. Implement `ProgramCollect::get_metadata()` for runtime retrieval, backed by a global registry populated by `register_metadata!`. Extend `pathf` with `MetadataPattern` to resolve metadata types across modules at build time. Add two examples demonstrating metadata usage with and without pathf integration. --- CHANGELOG.md | 31 +++ docs/example-pages/examples.json | 34 ++++ examples/example-combine-pathf-metadata/Cargo.lock | 226 +++++++++++++++++++++ examples/example-combine-pathf-metadata/Cargo.toml | 24 +++ examples/example-combine-pathf-metadata/build.rs | 3 + examples/example-combine-pathf-metadata/page.toml | 10 + .../example-combine-pathf-metadata/src/main.rs | 33 +++ .../example-combine-pathf-metadata/src/sub/mod.rs | 71 +++++++ examples/example-metadata/Cargo.lock | 216 ++++++++++++++++++++ examples/example-metadata/Cargo.toml | 9 + examples/example-metadata/page.toml | 10 + examples/example-metadata/src/main.rs | 120 +++++++++++ examples/test-examples.toml | 35 ++++ mingling/src/example_docs.rs | 196 ++++++++++++++++++ mingling/src/lib.rs | 3 + mingling_core/src/asset.rs | 1 + mingling_core/src/asset/metadata.rs | 14 ++ mingling_core/src/lib.rs | 1 + mingling_core/src/program/collection.rs | 12 ++ mingling_macros/src/attr.rs | 1 + mingling_macros/src/attr/metadata.rs | 87 ++++++++ mingling_macros/src/func.rs | 1 + mingling_macros/src/func/program_final_gen.rs | 35 ++++ mingling_macros/src/func/register_metadata.rs | 67 ++++++ mingling_macros/src/lib.rs | 62 +++++- mingling_pathf/src/pattern_analyzer.rs | 1 + mingling_pathf/src/patterns.rs | 2 + mingling_pathf/src/patterns/metadata.rs | 165 +++++++++++++++ mingling_pathf/test/src/lib.rs | 33 +++ .../test/src/test_files/test_metadata.rs | 40 ++++ 30 files changed, 1542 insertions(+), 1 deletion(-) create mode 100644 examples/example-combine-pathf-metadata/Cargo.lock create mode 100644 examples/example-combine-pathf-metadata/Cargo.toml create mode 100644 examples/example-combine-pathf-metadata/build.rs create mode 100644 examples/example-combine-pathf-metadata/page.toml create mode 100644 examples/example-combine-pathf-metadata/src/main.rs create mode 100644 examples/example-combine-pathf-metadata/src/sub/mod.rs create mode 100644 examples/example-metadata/Cargo.lock create mode 100644 examples/example-metadata/Cargo.toml create mode 100644 examples/example-metadata/page.toml create mode 100644 examples/example-metadata/src/main.rs create mode 100644 mingling_core/src/asset/metadata.rs create mode 100644 mingling_macros/src/attr/metadata.rs create mode 100644 mingling_macros/src/func/register_metadata.rs create mode 100644 mingling_pathf/src/patterns/metadata.rs create mode 100644 mingling_pathf/test/src/test_files/test_metadata.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index aa80b79..9f4276d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -209,6 +209,37 @@ 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` trait** — Added to `mingling_core::asset::metadata` and re-exported from `mingling_core` / `mingling`. A type implementing `Metadata` 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 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(member_id) -> Option`** — 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::()` 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::(ThisProgram::EntryGreet); + ``` + + The `#[metadata]` attribute and `Metadata` trait are re-exported as `mingling::macros::metadata` and `mingling::Metadata` respectively. + #### **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 @@ -94,6 +94,24 @@ "Cargo.toml" ] }, + { + "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", @@ -253,6 +271,22 @@ "Cargo.toml" ] }, + { + "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", 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::(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::(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::(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..e3b9db6 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,137 @@ 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, +/// } +/// +/// /// 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(), +/// } +/// } +/// +/// 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; +/// let msg = match ThisProgram::get_metadata::(ThisProgram::EntryGreet) { +/// Some(d) => format!("EntryGreet desc = {}", d.desc), +/// None => "EntryGreet has no description".to_string(), +/// }; +/// 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; +/// let msg = match ThisProgram::get_metadata::(ThisProgram::EntryDescription) { +/// Some(d) => format!("EntryDescription desc = {}", d.desc), +/// None => "EntryDescription has no description".to_string(), +/// }; +/// 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/lib.rs b/mingling/src/lib.rs index 108d61a..01edae9 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -76,6 +76,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 +107,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_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 { + /// Initializes and returns the metadata value of type `B` for this entry. + fn init_metadata() -> B; +} 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) -> 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(member_id: Self::Enum) -> Option { + 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 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::(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::(s).unwrap()) .collect(); + let metadata_tokens: Vec = get_global_set(&METADATA) + .lock() + .unwrap() + .clone() + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + let get_metadata_fn = if metadata_tokens.is_empty() { + quote! { + fn get_metadata(_member_id: Self::Enum) -> Option { + None + } + } + } else { + let metadata_arms = metadata_tokens.iter().map(|entry| { + quote! { + #entry + } + }); + quote! { + fn get_metadata(member_id: Self::Enum) -> Option { + let type_id = ::std::any::TypeId::of::(); + let any = match member_id { + #(#metadata_arms)* + _ => None, + }; + any.and_then(|b| b.downcast::().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) -> ::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::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::(entry_expr.to_token_stream()) { + Ok(ty) => ty, + Err(e) => return e.to_compile_error().into(), + }; + let metadata_type = match syn::parse2::(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), + _ => 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`. 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 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::(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 { + 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, + items: &mut Vec, +) { + 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 { + 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 { + 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 { + 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, +) { + 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 { + 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 {} + } +} -- cgit