diff options
| -rw-r--r-- | CHANGELOG.md | 10 | ||||
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | examples/example-general-renderer/src/main.rs | 13 | ||||
| -rw-r--r-- | examples/example-pack-err/src/main.rs | 53 | ||||
| -rw-r--r-- | examples/test-examples.toml | 22 | ||||
| -rw-r--r-- | mingling/Cargo.toml | 8 | ||||
| -rw-r--r-- | mingling/src/example_docs.rs | 66 | ||||
| -rw-r--r-- | mingling/src/lib.rs | 19 | ||||
| -rw-r--r-- | mingling_core/src/any.rs | 31 | ||||
| -rw-r--r-- | mingling_core/src/any/group.rs | 92 | ||||
| -rw-r--r-- | mingling_core/src/lib.rs | 15 | ||||
| -rw-r--r-- | mingling_core/src/renderer/general.rs | 58 | ||||
| -rw-r--r-- | mingling_core/src/renderer/general/structural_data.rs | 15 | ||||
| -rw-r--r-- | mingling_core/tests/test-all/tests/integration.rs | 3 | ||||
| -rw-r--r-- | mingling_core/tests/test-general-renderer/tests/integration.rs | 6 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 88 | ||||
| -rw-r--r-- | mingling_macros/src/pack.rs | 12 | ||||
| -rw-r--r-- | mingling_macros/src/pack_err.rs | 116 | ||||
| -rw-r--r-- | mingling_macros/src/renderer.rs | 11 | ||||
| -rw-r--r-- | mingling_macros/src/structural_data.rs | 330 |
20 files changed, 798 insertions, 173 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 314bedc..c1ad00b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -351,6 +351,16 @@ let value = route!(prev.pick_or_route((), Error::default()).unpack()); - **Examples and internal callers updated** throughout the codebase to use the new hook API patterns. +6. **\[core\]** **\[general_renderer\]** Added the `pack_err_structural!`, `pack_structural!`, and `group_structural!` macros for creating types that support structured output (JSON/YAML/TOML/RON). These are like `pack_err!`, `pack!`, and `group!` respectively, but also mark the type with the `StructuralData` trait, enabling the `GeneralRenderer` to serialize them. + +7. **\[core\]** **\[general_renderer\]** Added the `StructuralData` derive macro and sealed trait, decoupling structured output from `Groupped`. Previously, under the `general_renderer` feature, all `pack!` and `pack_err!` types automatically derived `Serialize`. Now, structured output is an opt-in property controlled by `StructuralData`: + - `pack!` / `pack_err!` / `group!` no longer derive `Serialize` even when `general_renderer` is enabled. + - To enable structured output, use `pack_structural!` / `pack_err_structural!` / `group_structural!` or the `#[derive(StructuralData)]` marker. + - The `Groupped` trait no longer requires `Serialize` bounds, and `AnyOutput::new` no longer requires `Serialize`. + - `GeneralRenderer::render` now accepts `T: StructuralData + Send` instead of `T: Serialize + Send`, and the individual format methods (`render_to_json`, etc.) are now private. + +8. **\[core\]** **\[general_renderer\]** Added `mingling::__private::StructuralDataSealed` and `mingling::__private::StructuralData` (re-exported from `mingling_core::renderer::general::structural_data`) to support the sealed trait pattern. The `StructuralData` trait is only implementable via the derive macro or the `_structural` macro variants. + ### Release 0.1.9 (2026-05-29) #### Fixes: @@ -669,11 +669,12 @@ With the `general_renderer` feature, users can add `--json` or `--yaml` flags to use mingling::{prelude::*, setup::GeneralRendererSetup}; use mingling::Groupped; +use mingling::StructuralData; use serde::Serialize; dispatcher!("render", CMDRender => EntryRender); -#[derive(Default, Serialize, Groupped)] +#[derive(Default, StructuralData, Serialize, Groupped)] struct ResultInfo { name: String, age: i32, diff --git a/examples/example-general-renderer/src/main.rs b/examples/example-general-renderer/src/main.rs index c923d28..1e02afb 100644 --- a/examples/example-general-renderer/src/main.rs +++ b/examples/example-general-renderer/src/main.rs @@ -18,7 +18,7 @@ //! ``` use mingling::prelude::*; -use mingling::{parser::Picker, setup::GeneralRendererSetup, Groupped}; +use mingling::{parser::Picker, setup::GeneralRendererSetup, StructuralData, Groupped}; use serde::Serialize; dispatcher!("render", CMDRender => EntryRender); @@ -34,11 +34,12 @@ fn main() { // --------- IMPORTANT --------- // For beautiful output structure, do not use `pack!` to wrap the types that need to be output. // Instead, manually implement -// ____________________ Implement serde::Serialize -// / _________ Implement mingling::Groupped -// | / to ensure Mingling can recognize the type -// vvvvvvvvv vvvvvvvv -#[derive(Serialize, Groupped)] +// __________________________________ Mark as structured data so it can be rendered +// / ____________________ Implement serde::Serialize +// | / _________ Implement mingling::Groupped +// | | / to ensure Mingling can recognize the type +// vvvvvvvvvvvv vvvvvvvvv vvvvvvvv +#[derive(StructuralData, Serialize, Groupped)] struct Info { #[serde(rename = "member_name")] name: String, diff --git a/examples/example-pack-err/src/main.rs b/examples/example-pack-err/src/main.rs index 72fecd6..f859fae 100644 --- a/examples/example-pack-err/src/main.rs +++ b/examples/example-pack-err/src/main.rs @@ -7,21 +7,23 @@ //! Run: //! ```bash //! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find -//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find --json //! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find Cargo.toml -//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find Cargo.toml --json //! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find src -//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find src --json +//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find-structural --json +//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find-structural Cargo.toml --json +//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find-structural src --json //! ``` //! //! Output: //! ```plaintext //! Search path not provided -//! {"name":"error_not_found"} //! Not a directory: Cargo.toml -//! {"name":"error_not_dir","info":"Cargo.toml"} //! Found directory: src +//! {"name":"error_not_found"} +//! {"name":"error_not_dir","info":"Cargo.toml"} //! {"inner":"src"} +//! {"name":"error_not_found_structural"} +//! {"name":"error_not_dir_structural","info":"Cargo.toml"} //! ``` use mingling::prelude::*; @@ -29,6 +31,7 @@ use mingling::setup::GeneralRendererSetup; use std::path::PathBuf; dispatcher!("find", CMDFind => EntryFind); +dispatcher!("find-structural", CMDFindStructural => EntryFindStructural); // --------- IMPORTANT --------- // `pack_err!` is a convenient macro for defining error types. @@ -52,8 +55,14 @@ pack_err!(ErrorNotFound); // Typed form — name = "error_not_dir" pack_err!(ErrorNotDir = PathBuf); -// Success type using traditional pack! -pack!(ResultPath = PathBuf); +// Simple form — with StructuralData support for --json / --yaml +pack_err_structural!(ErrorNotFoundStructural); + +// Typed form — with StructuralData support for --json / --yaml +pack_err_structural!(ErrorNotDirStructural = PathBuf); + +// Success type with StructuralData support +pack_structural!(ResultPath = PathBuf); #[chain] fn handle_find(args: EntryFind) -> Next { @@ -72,6 +81,23 @@ fn handle_find(args: EntryFind) -> Next { } } +#[chain] +fn handle_find_structural(args: EntryFindStructural) -> Next { + let Some(path_str) = args.inner.first().cloned() else { + // No path provided → use the simple error form (Default) + return ErrorNotFoundStructural::default().to_render(); + }; + + let path = PathBuf::from(&path_str); + if path.is_dir() { + // Is a directory → success + ResultPath::new(path).to_render() + } else { + // Not a directory (or doesn't exist) → use the typed error form + ErrorNotDirStructural::new(path).to_render() + } +} + /// Renders the successful result with the found directory path. #[renderer] fn render_result_path(path: ResultPath) { @@ -90,6 +116,18 @@ fn render_error_not_dir(err: ErrorNotDir) { r_println!("Not a directory: {}", err.info.display()); } +/// Renders the structural error when no search path is provided. +#[renderer] +fn render_error_not_found_structural(_: ErrorNotFoundStructural) { + r_println!("Search path not provided"); +} + +/// Renders the structural error when the given path is not a directory. +#[renderer] +fn render_error_not_dir_structural(err: ErrorNotDirStructural) { + r_println!("Not a directory: {}", err.info.display()); +} + gen_program!(); fn main() { @@ -97,5 +135,6 @@ fn main() { // Add GeneralRendererSetup to support --json / --yaml flags program.with_setup(GeneralRendererSetup); program.with_dispatcher(CMDFind); + program.with_dispatcher(CMDFindStructural); let _ = program.exec(); } diff --git a/examples/test-examples.toml b/examples/test-examples.toml index 9c25781..149f2c6 100644 --- a/examples/test-examples.toml +++ b/examples/test-examples.toml @@ -234,16 +234,26 @@ expect.exit-code = 0 expect.result = "Search path not provided" [[test.example-pack-err]] -command = "find --json" +command = "find Cargo.toml" expect.exit-code = 0 -expect.result = '{"name":"error_not_found"}' +expect.result = "Not a directory: Cargo.toml" [[test.example-pack-err]] -command = "find Cargo.toml" +command = "find examples" expect.exit-code = 0 -expect.result = "Not a directory: Cargo.toml" +expect.result = "Found directory: examples" + +[[test.example-pack-err]] +command = "find-structural --json" +expect.exit-code = 0 +expect.result = '{"name":"error_not_found_structural"}' + +[[test.example-pack-err]] +command = "find-structural Cargo.toml --json" +expect.exit-code = 0 +expect.result = '{"name":"error_not_dir_structural","info":"Cargo.toml"}' [[test.example-pack-err]] -command = "find Cargo.toml --json" +command = "find-structural examples --json" expect.exit-code = 0 -expect.result = '{"name":"error_not_dir","info":"Cargo.toml"}' +expect.result = '{"inner":"examples"}' diff --git a/mingling/Cargo.toml b/mingling/Cargo.toml index cce64b5..427bed6 100644 --- a/mingling/Cargo.toml +++ b/mingling/Cargo.toml @@ -59,10 +59,10 @@ general_renderer_empty = [ ] general_renderer_full = [ - "mingling_core/general_renderer", - "dep:serde", - "mingling_macros/general_renderer", - "all_serde_fmt", + "general_renderer", + "yaml_serde_fmt", + "toml_serde_fmt", + "ron_serde_fmt", ] all_serde_fmt = [ diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index ea8e539..266a449 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -1212,7 +1212,7 @@ pub mod example_exitcode {} /// Source code (./src/main.rs) /// ```ignore /// use mingling::prelude::*; -/// use mingling::{parser::Picker, setup::GeneralRendererSetup, Groupped}; +/// use mingling::{parser::Picker, setup::GeneralRendererSetup, StructuralData, Groupped}; /// use serde::Serialize; /// /// dispatcher!("render", CMDRender => EntryRender); @@ -1228,11 +1228,12 @@ pub mod example_exitcode {} /// // --------- IMPORTANT --------- /// // For beautiful output structure, do not use `pack!` to wrap the types that need to be output. /// // Instead, manually implement -/// // ____________________ Implement serde::Serialize -/// // / _________ Implement mingling::Groupped -/// // | / to ensure Mingling can recognize the type -/// // vvvvvvvvv vvvvvvvv -/// #[derive(Serialize, Groupped)] +/// // __________________________________ Mark as structured data so it can be rendered +/// // / ____________________ Implement serde::Serialize +/// // | / _________ Implement mingling::Groupped +/// // | | / to ensure Mingling can recognize the type +/// // vvvvvvvvvvvv vvvvvvvvv vvvvvvvv +/// #[derive(StructuralData, Serialize, Groupped)] /// struct Info { /// #[serde(rename = "member_name")] /// name: String, @@ -1686,21 +1687,23 @@ pub mod example_outside_type {} /// Run: /// ```bash /// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find -/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find --json /// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find Cargo.toml -/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find Cargo.toml --json /// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find src -/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find src --json +/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find-structural --json +/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find-structural Cargo.toml --json +/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find-structural src --json /// ``` /// /// Output: /// ```plaintext /// Search path not provided -/// {"name":"error_not_found"} /// Not a directory: Cargo.toml -/// {"name":"error_not_dir","info":"Cargo.toml"} /// Found directory: src +/// {"name":"error_not_found"} +/// {"name":"error_not_dir","info":"Cargo.toml"} /// {"inner":"src"} +/// {"name":"error_not_found_structural"} +/// {"name":"error_not_dir_structural","info":"Cargo.toml"} /// ``` /// /// Source code (./Cargo.toml) @@ -1730,6 +1733,7 @@ pub mod example_outside_type {} /// use std::path::PathBuf; /// /// dispatcher!("find", CMDFind => EntryFind); +/// dispatcher!("find-structural", CMDFindStructural => EntryFindStructural); /// /// // --------- IMPORTANT --------- /// // `pack_err!` is a convenient macro for defining error types. @@ -1753,8 +1757,14 @@ pub mod example_outside_type {} /// // Typed form — name = "error_not_dir" /// pack_err!(ErrorNotDir = PathBuf); /// -/// // Success type using traditional pack! -/// pack!(ResultPath = PathBuf); +/// // Simple form — with StructuralData support for --json / --yaml +/// pack_err_structural!(ErrorNotFoundStructural); +/// +/// // Typed form — with StructuralData support for --json / --yaml +/// pack_err_structural!(ErrorNotDirStructural = PathBuf); +/// +/// // Success type with StructuralData support +/// pack_structural!(ResultPath = PathBuf); /// /// #[chain] /// fn handle_find(args: EntryFind) -> Next { @@ -1773,6 +1783,23 @@ pub mod example_outside_type {} /// } /// } /// +/// #[chain] +/// fn handle_find_structural(args: EntryFindStructural) -> Next { +/// let Some(path_str) = args.inner.first().cloned() else { +/// // No path provided → use the simple error form (Default) +/// return ErrorNotFoundStructural::default().to_render(); +/// }; +/// +/// let path = PathBuf::from(&path_str); +/// if path.is_dir() { +/// // Is a directory → success +/// ResultPath::new(path).to_render() +/// } else { +/// // Not a directory (or doesn't exist) → use the typed error form +/// ErrorNotDirStructural::new(path).to_render() +/// } +/// } +/// /// /// Renders the successful result with the found directory path. /// #[renderer] /// fn render_result_path(path: ResultPath) { @@ -1791,6 +1818,18 @@ pub mod example_outside_type {} /// r_println!("Not a directory: {}", err.info.display()); /// } /// +/// /// Renders the structural error when no search path is provided. +/// #[renderer] +/// fn render_error_not_found_structural(_: ErrorNotFoundStructural) { +/// r_println!("Search path not provided"); +/// } +/// +/// /// Renders the structural error when the given path is not a directory. +/// #[renderer] +/// fn render_error_not_dir_structural(err: ErrorNotDirStructural) { +/// r_println!("Not a directory: {}", err.info.display()); +/// } +/// /// gen_program!(); /// /// fn main() { @@ -1798,6 +1837,7 @@ pub mod example_outside_type {} /// // Add GeneralRendererSetup to support --json / --yaml flags /// program.with_setup(GeneralRendererSetup); /// program.with_dispatcher(CMDFind); +/// program.with_dispatcher(CMDFindStructural); /// let _ = program.exec(); /// } /// ``` diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index 70b69bc..4c49f15 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -96,15 +96,24 @@ pub mod macros { /// Used to register an external type as a group member #[cfg(feature = "extra_macros")] pub use mingling_macros::group; + /// Like `group!` but also marks the type for structured output + #[cfg(all(feature = "general_renderer", feature = "extra_macros"))] + pub use mingling_macros::group_structural; /// Used to generate a struct implementing the `HelpRequest` trait via a method pub use mingling_macros::help; /// Used to create a `Node` struct via a literal pub use mingling_macros::node; /// Used to create a wrapper type for use with `Chain` and `Renderer` pub use mingling_macros::pack; + /// Like `pack!` but also marks the type for structured output + #[cfg(feature = "general_renderer")] + pub use mingling_macros::pack_structural; /// Used to create an error struct with automatic `name` field #[cfg(feature = "extra_macros")] pub use mingling_macros::pack_err; + /// Like `pack_err!` but also marks the type for structured output + #[cfg(all(feature = "general_renderer", feature = "extra_macros"))] + pub use mingling_macros::pack_err_structural; #[cfg(feature = "comp")] #[doc(hidden)] pub use mingling_macros::program_comp_gen; @@ -148,6 +157,10 @@ pub use mingling_macros::EnumTag; /// derive macro Groupped pub use mingling_macros::Groupped; +/// derive macro `StructuralData` — marks a type as supporting structured output +#[cfg(feature = "general_renderer")] +pub use mingling_macros::StructuralData; + /// Example projects for `Mingling`, for learning how to use `Mingling` pub mod _mingling_examples { pub use crate::example_docs::*; @@ -200,9 +213,15 @@ pub mod prelude { pub use crate::macros::gen_program; /// Re-export of the `pack` macro for creating wrapper types. pub use crate::macros::pack; + /// Like `pack!` but also marks the type for structured output + #[cfg(feature = "general_renderer")] + pub use mingling_macros::pack_structural; /// Re-export of the `pack_err` macro for creating error types. #[cfg(feature = "extra_macros")] pub use crate::macros::pack_err; + /// Like `pack_err!` but also marks the type for structured output + #[cfg(all(feature = "general_renderer", feature = "extra_macros"))] + pub use mingling_macros::pack_err_structural; /// Re-export of the `r_print` macro for printing within a renderer context. pub use crate::macros::r_print; /// Re-export of the `r_println` macro for printing with a newline within a renderer diff --git a/mingling_core/src/any.rs b/mingling_core/src/any.rs index 8ee07f5..ef9f912 100644 --- a/mingling_core/src/any.rs +++ b/mingling_core/src/any.rs @@ -1,6 +1,3 @@ -#[cfg(feature = "general_renderer")] -use serde::Serialize; - use crate::Groupped; use crate::error::ChainProcessError; @@ -14,7 +11,8 @@ pub mod group; /// /// Note: /// - If an enum value that does not belong to this type is incorrectly specified, it will be **unsafely** unwrapped by the scheduler -/// - Under the `general_renderer` feature, the passed value must ensure it implements `serde::Serialize` +/// - Structured output via `--json`/`--yaml` is only available for types that implement +/// [`StructuralData`], which implies `serde::Serialize`. /// - It is recommended to use the `pack!` macro from [mingling_macros](https://crates.io/crates/mingling_macros) to create types that can be converted to `AnyOutput`, which guarantees runtime safety #[derive(Debug)] pub struct AnyOutput<G> { @@ -24,21 +22,7 @@ pub struct AnyOutput<G> { } impl<G> AnyOutput<G> { - /// Create an `AnyOutput` from a `Send + Groupped<G> + Serialize` type - #[cfg(feature = "general_renderer")] - pub fn new<T>(value: T) -> Self - where - T: Send + Groupped<G> + Serialize + 'static, - { - Self { - inner: Box::new(value), - type_id: std::any::TypeId::of::<T>(), - member_id: T::member_id(), - } - } - /// Create an `AnyOutput` from a `Send + Groupped<G>` type - #[cfg(not(feature = "general_renderer"))] pub fn new<T>(value: T) -> Self where T: Send + Groupped<G> + 'static, @@ -82,9 +66,14 @@ impl<G> AnyOutput<G> { ChainProcess::Ok((self, NextProcess::Renderer)) } - #[cfg(feature = "general_renderer")] - /// Restore `AnyOutput` back to the original Serialize type - pub fn restore<T: Serialize + 'static>(self) -> Option<T> { + /// Restore `AnyOutput` back to the original concrete type. + /// + /// # Safety + /// + /// This is only safe when `T` matches the `TypeId` stored in the `AnyOutput`. + /// Generated code (via `gen_program!()`) guarantees this by dispatching on + /// `member_id` before calling `restore`. + pub fn restore<T: 'static>(self) -> Option<T> { if self.type_id == std::any::TypeId::of::<T>() { match self.inner.downcast::<T>() { Ok(boxed) => Some(*boxed), diff --git a/mingling_core/src/any/group.rs b/mingling_core/src/any/group.rs index e9fce5e..07f8400 100644 --- a/mingling_core/src/any/group.rs +++ b/mingling_core/src/any/group.rs @@ -1,74 +1,34 @@ -#[cfg(feature = "general_renderer")] -pub use general_renderer_groupped::*; - -#[cfg(not(feature = "general_renderer"))] -pub use groupped::*; - -#[cfg(feature = "general_renderer")] -mod general_renderer_groupped { - use serde::Serialize; - - use crate::{AnyOutput, ChainProcess}; - /// Used to mark a type with a unique enum ID, assisting dynamic dispatch - pub trait Groupped<Group> +use crate::{AnyOutput, ChainProcess}; + +/// Used to mark a type with a unique enum ID, assisting dynamic dispatch +/// +/// **Note:** Unlike earlier versions, `Groupped` no longer requires `Serialize` +/// even when the `general_renderer` feature is enabled. Structured output is +/// controlled separately via the [`StructalData`] trait. +pub trait Groupped<Group> +where + Self: Sized + 'static, +{ + /// Returns the specific enum value representing its ID within that enum + fn member_id() -> Group; + + /// Converts the grouped item into a `ChainProcess` directed to the chain route. + /// + /// This wraps the item into an `AnyOutput` and routes it to the chain processing pipeline. + fn to_chain(self) -> ChainProcess<Group> where - Self: Sized + Serialize + 'static, + Self: Send, { - /// Returns the specific enum value representing its ID within that enum - fn member_id() -> Group; - - /// Converts the grouped item into a `ChainProcess` directed to the chain route. - /// - /// This wraps the item into an `AnyOutput` and routes it to the chain processing pipeline. - fn to_chain(self) -> ChainProcess<Group> - where - Self: Send + Serialize, - { - AnyOutput::new(self).route_chain() - } - - /// Converts the grouped item into a `ChainProcess` directed to the render route. - /// - /// This wraps the item into an `AnyOutput` and routes it to the render processing pipeline. - fn to_render(self) -> ChainProcess<Group> - where - Self: Send + Serialize, - { - AnyOutput::new(self).route_renderer() - } + AnyOutput::new(self).route_chain() } -} - -#[cfg(not(feature = "general_renderer"))] -mod groupped { - use crate::{AnyOutput, ChainProcess}; - /// Used to mark a type with a unique enum ID, assisting dynamic dispatch - pub trait Groupped<Group> + /// Converts the grouped item into a `ChainProcess` directed to the render route. + /// + /// This wraps the item into an `AnyOutput` and routes it to the render processing pipeline. + fn to_render(self) -> ChainProcess<Group> where - Self: Sized + 'static, + Self: Send, { - /// Returns the specific enum value representing its ID within that enum - fn member_id() -> Group; - - /// Converts the grouped item into a `ChainProcess` directed to the chain route. - /// - /// This wraps the item into an `AnyOutput` and routes it to the chain processing pipeline. - fn to_chain(self) -> ChainProcess<Group> - where - Self: Send, - { - AnyOutput::new(self).route_chain() - } - - /// Converts the grouped item into a `ChainProcess` directed to the render route. - /// - /// This wraps the item into an `AnyOutput` and routes it to the render processing pipeline. - fn to_render(self) -> ChainProcess<Group> - where - Self: Send, - { - AnyOutput::new(self).route_renderer() - } + AnyOutput::new(self).route_renderer() } } diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs index ddb5446..9d0ac2a 100644 --- a/mingling_core/src/lib.rs +++ b/mingling_core/src/lib.rs @@ -22,6 +22,9 @@ pub mod test { #[cfg(feature = "general_renderer")] pub use crate::renderer::general::GeneralRenderer; +// NOT re-exported at top level: the `StructuralData` trait is sealed and only +// accessible through the derive macro. Users who need the trait can access it +// via `mingling::renderer::general::StructuralData` (through the inner alias). pub use crate::any::group::*; pub use crate::any::*; @@ -72,6 +75,18 @@ pub mod setup { pub use crate::program::setup::ProgramSetup; } +/// Private API — not intended for direct use. +#[doc(hidden)] +pub mod __private { + /// Sealed trait for `StructuralData` — only implementable via derive macro. + pub trait StructuralDataSealed {} + + /// Re-export so the derive macro can reference the trait without + /// conflicting with the derive macro name at `::mingling::StructuralData`. + #[cfg(feature = "general_renderer")] + pub use crate::renderer::general::structural_data::StructuralData; +} + #[doc(hidden)] pub mod core_res { #[cfg(feature = "repl")] diff --git a/mingling_core/src/renderer/general.rs b/mingling_core/src/renderer/general.rs index 1a9647b..e6da06b 100644 --- a/mingling_core/src/renderer/general.rs +++ b/mingling_core/src/renderer/general.rs @@ -4,13 +4,16 @@ use crate::{ use serde::Serialize; pub mod error; +pub mod structural_data; + +use structural_data::StructuralData; /// A general renderer that supports multiple serialization formats. /// /// The `GeneralRenderer` provides methods to serialize data into various formats /// including JSON, YAML, TOML, and RON, with support for both regular and /// pretty-printed variants. It is designed to work with types that implement -/// the `Serialize` trait. +/// the [`StructuralData`] trait (which implies `Serialize`). pub struct GeneralRenderer; impl GeneralRenderer { @@ -20,7 +23,7 @@ impl GeneralRenderer { /// /// Returns `Err(GeneralRendererSerializeError)` if serialization fails. #[allow(unused_variables)] - pub fn render<T: Serialize + Send>( + pub fn render<T: StructuralData + Send>( data: &T, setting: &GeneralRendererSetting, r: &mut RenderResult, @@ -48,13 +51,13 @@ impl GeneralRenderer { /// /// Returns `Err(GeneralRendererSerializeError)` if serialization fails. #[cfg(feature = "json_serde_fmt")] - pub fn render_to_json<T: Serialize + Send>( + fn render_to_json<T: Serialize + Send>( data: &T, r: &mut RenderResult, ) -> Result<(), GeneralRendererSerializeError> { let json_string = serde_json::to_string(data) .map_err(|e| GeneralRendererSerializeError::new(e.to_string()))?; - r.print(json_string.clone().as_str()); + r.print(&json_string); Ok(()) } @@ -64,13 +67,13 @@ impl GeneralRenderer { /// /// Returns `Err(GeneralRendererSerializeError)` if serialization fails. #[cfg(feature = "json_serde_fmt")] - pub fn render_to_json_pretty<T: Serialize + Send>( + fn render_to_json_pretty<T: Serialize + Send>( data: &T, r: &mut RenderResult, ) -> Result<(), GeneralRendererSerializeError> { let json_string = serde_json::to_string_pretty(data) .map_err(|e| GeneralRendererSerializeError::new(e.to_string()))?; - r.print(json_string.clone().as_str()); + r.print(&json_string); Ok(()) } @@ -80,13 +83,13 @@ impl GeneralRenderer { /// /// Returns `Err(GeneralRendererSerializeError)` if serialization fails. #[cfg(feature = "ron_serde_fmt")] - pub fn render_to_ron<T: Serialize + Send>( + fn render_to_ron<T: Serialize + Send>( data: &T, r: &mut RenderResult, ) -> Result<(), GeneralRendererSerializeError> { let ron_string = ron::ser::to_string(data) .map_err(|e| GeneralRendererSerializeError::new(e.to_string()))?; - r.print(ron_string.to_string().as_str()); + r.print(&ron_string); Ok(()) } @@ -96,17 +99,17 @@ impl GeneralRenderer { /// /// Returns `Err(GeneralRendererSerializeError)` if serialization fails. #[cfg(feature = "ron_serde_fmt")] - pub fn render_to_ron_pretty<T: Serialize + Send>( + fn render_to_ron_pretty<T: Serialize + Send>( data: &T, r: &mut RenderResult, ) -> Result<(), GeneralRendererSerializeError> { - let mut pretty_config = ron::ser::PrettyConfig::new(); - pretty_config.new_line = std::borrow::Cow::from("\n"); - pretty_config.indentor = std::borrow::Cow::from(" "); + let pretty_config = ron::ser::PrettyConfig::new() + .new_line("\n") + .indentor(" "); let ron_string = ron::ser::to_string_pretty(data, pretty_config) .map_err(|e| GeneralRendererSerializeError::new(e.to_string()))?; - r.print(ron_string.to_string().as_str()); + r.print(&ron_string); Ok(()) } @@ -116,13 +119,13 @@ impl GeneralRenderer { /// /// Returns `Err(GeneralRendererSerializeError)` if serialization fails. #[cfg(feature = "toml_serde_fmt")] - pub fn render_to_toml<T: Serialize + Send>( + fn render_to_toml<T: Serialize + Send>( data: &T, r: &mut RenderResult, ) -> Result<(), GeneralRendererSerializeError> { let toml_string = toml::to_string(data).map_err(|e| GeneralRendererSerializeError::new(e.to_string()))?; - r.print(toml_string.to_string().as_str()); + r.print(&toml_string); Ok(()) } @@ -132,13 +135,13 @@ impl GeneralRenderer { /// /// Returns `Err(GeneralRendererSerializeError)` if serialization fails. #[cfg(feature = "yaml_serde_fmt")] - pub fn render_to_yaml<T: Serialize + Send>( + fn render_to_yaml<T: Serialize + Send>( data: &T, r: &mut RenderResult, ) -> Result<(), GeneralRendererSerializeError> { let yaml_string = serde_yaml::to_string(data) .map_err(|e| GeneralRendererSerializeError::new(e.to_string()))?; - r.print(yaml_string.to_string().as_str()); + r.print(&yaml_string); Ok(()) } } @@ -155,6 +158,9 @@ mod tests { value: i32, } + impl crate::__private::StructuralDataSealed for TestData {} + impl StructuralData for TestData {} + fn test_data() -> TestData { TestData { name: "hello".into(), @@ -175,7 +181,8 @@ mod tests { #[test] fn test_render_to_json() { let mut r = RenderResult::default(); - let result = GeneralRenderer::render_to_json(&test_data(), &mut r); + let result = + GeneralRenderer::render(&test_data(), &GeneralRendererSetting::Json, &mut r); assert!(result.is_ok()); assert!(!r.is_empty()); let output: String = r.into(); @@ -189,7 +196,8 @@ mod tests { #[test] fn test_render_to_json_pretty() { let mut r = RenderResult::default(); - let result = GeneralRenderer::render_to_json_pretty(&test_data(), &mut r); + let result = + GeneralRenderer::render(&test_data(), &GeneralRendererSetting::JsonPretty, &mut r); assert!(result.is_ok()); let output: String = r.into(); // Pretty JSON has newlines @@ -200,7 +208,8 @@ mod tests { #[test] fn test_render_to_yaml() { let mut r = RenderResult::default(); - let result = GeneralRenderer::render_to_yaml(&test_data(), &mut r); + let result = + GeneralRenderer::render(&test_data(), &GeneralRendererSetting::Yaml, &mut r); assert!(result.is_ok()); assert!(!r.is_empty()); } @@ -209,7 +218,8 @@ mod tests { #[test] fn test_render_to_toml() { let mut r = RenderResult::default(); - let result = GeneralRenderer::render_to_toml(&test_data(), &mut r); + let result = + GeneralRenderer::render(&test_data(), &GeneralRendererSetting::Toml, &mut r); assert!(result.is_ok()); assert!(!r.is_empty()); } @@ -218,7 +228,8 @@ mod tests { #[test] fn test_render_to_ron() { let mut r = RenderResult::default(); - let result = GeneralRenderer::render_to_ron(&test_data(), &mut r); + let result = + GeneralRenderer::render(&test_data(), &GeneralRendererSetting::Ron, &mut r); assert!(result.is_ok()); assert!(!r.is_empty()); } @@ -227,7 +238,8 @@ mod tests { #[test] fn test_render_to_ron_pretty() { let mut r = RenderResult::default(); - let result = GeneralRenderer::render_to_ron_pretty(&test_data(), &mut r); + let result = + GeneralRenderer::render(&test_data(), &GeneralRendererSetting::RonPretty, &mut r); assert!(result.is_ok()); let output: String = r.into(); assert!(output.contains('\n')); diff --git a/mingling_core/src/renderer/general/structural_data.rs b/mingling_core/src/renderer/general/structural_data.rs new file mode 100644 index 0000000..ac6363e --- /dev/null +++ b/mingling_core/src/renderer/general/structural_data.rs @@ -0,0 +1,15 @@ +use serde::Serialize; + +/// Marker trait for types that support structured output (JSON / YAML / TOML / RON). +/// +/// This trait is a **supertrait** of `serde::Serialize` and is sealed via +/// `__private::StructuralDataSealed`. It can only be implemented through: +/// +/// - `#[derive(StructuralData)]` +/// - `pack_structural!` +/// - `group_structural!` +/// +/// These entry points also register the type in the global `STRUCTURED_TYPES` +/// registry, which is required for the `general_render` match arm to be generated. +#[doc(hidden)] +pub trait StructuralData: Serialize + crate::__private::StructuralDataSealed {} diff --git a/mingling_core/tests/test-all/tests/integration.rs b/mingling_core/tests/test-all/tests/integration.rs index e173374..99910a9 100644 --- a/mingling_core/tests/test-all/tests/integration.rs +++ b/mingling_core/tests/test-all/tests/integration.rs @@ -3,6 +3,7 @@ use mingling::GeneralRenderer; use mingling::GeneralRendererSetting; use mingling::MockProgramCollect; use mingling::NextProcess; +use mingling::StructuralData; use mingling::Node; use mingling::Program; use mingling::RenderResult; @@ -90,7 +91,7 @@ fn test_render_result_print() { // GeneralRenderer -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize, StructuralData)] struct TestData { name: String, value: i32, diff --git a/mingling_core/tests/test-general-renderer/tests/integration.rs b/mingling_core/tests/test-general-renderer/tests/integration.rs index 0fcc38d..2e2472e 100644 --- a/mingling_core/tests/test-general-renderer/tests/integration.rs +++ b/mingling_core/tests/test-general-renderer/tests/integration.rs @@ -1,9 +1,7 @@ -use mingling::GeneralRenderer; -use mingling::GeneralRendererSetting; -use mingling::RenderResult; +use mingling::{GeneralRenderer, GeneralRendererSetting, RenderResult, StructuralData}; use serde::Serialize; -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize, StructuralData)] struct TestData { name: String, value: i32, diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index d08e129..27bb80c 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -156,6 +156,8 @@ mod enum_tag; mod group_impl; mod groupped; mod help; +#[cfg(feature = "general_renderer")] +mod structural_data; mod node; mod pack; #[cfg(feature = "extra_macros")] @@ -182,6 +184,12 @@ pub(crate) type Registry = OnceLock<Mutex<BTreeSet<String>>>; // Global variables #[cfg(feature = "general_renderer")] pub(crate) static GENERAL_RENDERERS: Registry = OnceLock::new(); + +/// Types explicitly marked with `#[derive(StructuralData)]` or created via +/// `pack_structural!` / `group_structural!`. +#[cfg(feature = "general_renderer")] +pub(crate) static STRUCTURED_TYPES: Registry = OnceLock::new(); + #[cfg(feature = "comp")] pub(crate) static COMPLETIONS: Registry = OnceLock::new(); @@ -275,6 +283,23 @@ pub fn group(input: TokenStream) -> TokenStream { group_impl::group_macro(input) } +/// Like `group!` but also marks the type as supporting structured output +/// (JSON / YAML / TOML / RON) via `StructuralData`. +/// +/// # Syntax +/// +/// ```rust,ignore +/// group_structural!(std::io::Error); +/// group_structural!(IoError = std::io::Error); +/// ``` +/// +/// Requires the `general_renderer` and `extra_macros` features. +#[cfg(all(feature = "general_renderer", feature = "extra_macros"))] +#[proc_macro] +pub fn group_structural(input: TokenStream) -> TokenStream { + structural_data::group_structural(input) +} + /// Creates a `Node` from a dot-separated path string. /// /// Each segment is converted to kebab-case (unless it starts with `_`). @@ -366,6 +391,28 @@ pub fn pack(input: TokenStream) -> TokenStream { pack::pack(input) } +/// Like `pack!` but also marks the type as supporting structured output +/// (JSON / YAML / TOML / RON) via `StructuralData`. +/// +/// # Syntax +/// +/// ```rust,ignore +/// pack_structural!(Info = (String, i32)); +/// ``` +/// +/// This is equivalent to: +/// ```rust,ignore +/// pack!(Info = (String, i32)); +/// impl ::mingling::StructuralData for Info {} +/// ``` +/// +/// Requires the `general_renderer` feature. +#[cfg(feature = "general_renderer")] +#[proc_macro] +pub fn pack_structural(input: TokenStream) -> TokenStream { + structural_data::pack_structural(input) +} + /// Creates an error struct with a `name: String` field and optional `info: Type` field. /// /// This macro provides a concise way to define error types that implement `Groupped` @@ -434,6 +481,23 @@ pub fn pack_err(input: TokenStream) -> TokenStream { pack_err::pack_err(input) } +/// Like `pack_err!` but also marks the type for structured output +/// (JSON / YAML / TOML / RON) via `StructuralData`. +/// +/// # Syntax +/// +/// ```rust,ignore +/// pack_err_structural!(ErrorNotFound); +/// pack_err_structural!(ErrorNotDir = PathBuf); +/// ``` +/// +/// Requires the `general_renderer` and `extra_macros` features. +#[cfg(all(feature = "general_renderer", feature = "extra_macros"))] +#[proc_macro] +pub fn pack_err_structural(input: TokenStream) -> TokenStream { + pack_err::pack_err_structural(input) +} + /// Early-returns an error from a `Result`, converting the `Ok` branch to a /// `ChainProcess`. /// @@ -1305,6 +1369,30 @@ pub fn derive_enum_tag(input: TokenStream) -> TokenStream { enum_tag::derive_enum_tag(input) } +/// Derive macro for [`StructuralData`], marking a type as eligible for structured +/// structured output (JSON / YAML / TOML / RON). +/// +/// The type must also implement `serde::Serialize` — the generated +/// `impl StructuralData` will fail to compile otherwise. +/// +/// # Syntax +/// +/// ```rust,ignore +/// use mingling::StructuralData; +/// use serde::Serialize; +/// +/// #[derive(Serialize, StructuralData)] +/// struct Info { +/// name: String, +/// age: i32, +/// } +/// ``` +#[cfg(feature = "general_renderer")] +#[proc_macro_derive(StructuralData)] +pub fn derive_structural_data(input: TokenStream) -> TokenStream { + structural_data::derive_structural_data(input) +} + /// Derive macro for implementing both `Groupped` and `serde::Serialize` on a struct. /// /// **This macro is only available with the `general_renderer` feature.** diff --git a/mingling_macros/src/pack.rs b/mingling_macros/src/pack.rs index ffb07f2..5a6ccb0 100644 --- a/mingling_macros/src/pack.rs +++ b/mingling_macros/src/pack.rs @@ -34,7 +34,8 @@ pub fn pack(input: TokenStream) -> TokenStream { let attrs = pack_input.attrs; // Generate the struct definition - #[cfg(not(feature = "general_renderer"))] + // Note: No longer derives Serialize under general_renderer. + // Use pack_structual! for structured output support. let struct_def = quote! { #(#attrs)* pub struct #type_name { @@ -42,15 +43,6 @@ pub fn pack(input: TokenStream) -> TokenStream { } }; - #[cfg(feature = "general_renderer")] - let struct_def = quote! { - #(#attrs)* - #[derive(serde::Serialize)] - pub struct #type_name { - pub(crate) inner: #inner_type, - } - }; - // Generate the new() method let new_impl = quote! { impl #type_name { diff --git a/mingling_macros/src/pack_err.rs b/mingling_macros/src/pack_err.rs index 51bc656..8f147be 100644 --- a/mingling_macros/src/pack_err.rs +++ b/mingling_macros/src/pack_err.rs @@ -2,6 +2,8 @@ use proc_macro::TokenStream; use quote::quote; use syn::{Ident, Token, Type, parse_macro_input}; +use crate::get_global_set; + /// Converts a PascalCase/UpperCamelCase identifier string to snake_case. /// /// Examples: @@ -67,16 +69,12 @@ pub fn pack_err(input: TokenStream) -> TokenStream { let name_str = type_name.to_string(); let snake_name = to_snake_case(&name_str); - #[cfg(not(feature = "general_renderer"))] + // Note: No longer derives Serialize under general_renderer. + // Use pack_err_structural for structured output support. let derive = quote! { #[derive(::mingling::Groupped)] }; - #[cfg(feature = "general_renderer")] - let derive = quote! { - #[derive(::mingling::Groupped, ::serde::Serialize)] - }; - let expanded = quote! { #derive pub struct #type_name { @@ -104,18 +102,114 @@ pub fn pack_err(input: TokenStream) -> TokenStream { let name_str = type_name.to_string(); let snake_name = to_snake_case(&name_str); - #[cfg(not(feature = "general_renderer"))] + // Note: No longer derives Serialize under general_renderer. + // Use pack_err_structural for structured output support. let derive = quote! { #[derive(::mingling::Groupped)] }; - #[cfg(feature = "general_renderer")] - let derive = quote! { + let expanded = quote! { + #derive + pub struct #type_name { + /// The snake_case name of this error, automatically set at compile time. + pub name: String, + /// Additional context info for this error. + pub info: #inner_type, + } + + impl #type_name { + /// Creates a new error with the given info. + /// The `name` field is automatically set to the snake_case of the struct name. + pub fn new(info: #inner_type) -> Self { + Self { + name: #snake_name.into(), + info, + } + } + } + + ::mingling::macros::register_type!(#type_name); + }; + + expanded.into() + } + } +} + +/// `pack_err_structural!` — like `pack_err!` but also marks the type as +/// supporting structured output via `StructuralData`. +/// +/// # Syntax +/// +/// ```rust,ignore +/// pack_err_structural!(ErrorNotFound); +/// pack_err_structural!(ErrorNotDir = PathBuf); +/// ``` +/// +/// This is equivalent to: +/// ```rust,ignore +/// pack_err!(ErrorNotFound); +/// impl ::mingling::__private::StructuralDataSealed for ErrorNotFound {} +/// impl ::mingling::__private::StructuralData for ErrorNotFound {} +/// ``` +#[cfg(feature = "general_renderer")] +pub fn pack_err_structural(input: TokenStream) -> TokenStream { + let parsed = parse_macro_input!(input as PackErrInput); + + let type_name = match &parsed { + PackErrInput::Simple { type_name } => type_name.clone(), + PackErrInput::Typed { type_name, .. } => type_name.clone(), + }; + + // Register in STRUCTURED_TYPES + let type_name_str = type_name.to_string(); + get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .insert(type_name_str); + + let structural_data = quote! { + impl ::mingling::__private::StructuralDataSealed for #type_name {} + impl ::mingling::__private::StructuralData for #type_name {} + }; + + // Generate the struct + impls (same as pack_err! but with Serialize derive + sealed) + match parsed { + PackErrInput::Simple { type_name } => { + let name_str = type_name.to_string(); + let snake_name = to_snake_case(&name_str); + + let expanded = quote! { #[derive(::mingling::Groupped, ::serde::Serialize)] + pub struct #type_name { + /// The snake_case name of this error, automatically set at compile time. + pub name: String, + } + + impl ::std::default::Default for #type_name { + fn default() -> Self { + Self { + name: #snake_name.into(), + } + } + } + + ::mingling::macros::register_type!(#type_name); + + #structural_data }; + expanded.into() + } + PackErrInput::Typed { + type_name, + inner_type, + } => { + let name_str = type_name.to_string(); + let snake_name = to_snake_case(&name_str); + let expanded = quote! { - #derive + #[derive(::mingling::Groupped, ::serde::Serialize)] pub struct #type_name { /// The snake_case name of this error, automatically set at compile time. pub name: String, @@ -135,6 +229,8 @@ pub fn pack_err(input: TokenStream) -> TokenStream { } ::mingling::macros::register_type!(#type_name); + + #structural_data }; expanded.into() diff --git a/mingling_macros/src/renderer.rs b/mingling_macros/src/renderer.rs index a82744a..6de3d59 100644 --- a/mingling_macros/src/renderer.rs +++ b/mingling_macros/src/renderer.rs @@ -270,8 +270,17 @@ pub fn register_renderer(input: TokenStream) -> TokenStream { renderers.insert(renderer_entry_str); renderer_exist.insert(renderer_exist_entry_str); + // Only register general renderer if the type is in STRUCTURED_TYPES #[cfg(feature = "general_renderer")] - general_renderers.insert(general_renderer_entry_str); + { + let is_structured = get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .contains(&variant_name); + if is_structured { + general_renderers.insert(general_renderer_entry_str); + } + } quote! {}.into() } diff --git a/mingling_macros/src/structural_data.rs b/mingling_macros/src/structural_data.rs new file mode 100644 index 0000000..593b52d --- /dev/null +++ b/mingling_macros/src/structural_data.rs @@ -0,0 +1,330 @@ +#![allow(dead_code)] + +use proc_macro::TokenStream; +use quote::quote; +use syn::{DeriveInput, Ident, TypePath, parse_macro_input}; + +use crate::get_global_set; + +/// Derive macro for `StructuralData`. +/// +/// This marks a type as eligible for structured output (JSON / YAML / TOML / RON). +/// The type must also implement `serde::Serialize` — the generated `impl StructuralData` +/// will fail to compile if `Serialize` is not in scope or implemented. +/// +/// Also registers the type name in the global `STRUCTURED_TYPES` registry so that +/// the `general_render` match arm is generated by `gen_program!()`. +pub(crate) fn derive_structural_data(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let type_name = input.ident; + + // Register in STRUCTURED_TYPES + let type_name_str = type_name.to_string(); + get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .insert(type_name_str); + + // Generate BOTH the sealed impl AND the StructuralData impl. + // Users cannot implement StructuralDataSealed manually (it's #[doc(hidden)]), + // so the only way to get StructuralData is through this derive macro. + let expanded = quote! { + impl ::mingling::__private::StructuralDataSealed for #type_name {} + impl ::mingling::__private::StructuralData for #type_name {} + }; + + expanded.into() +} + +/// `pack_structural!` — like `pack!` but also marks the type as supporting +/// structured output via `StructuralData`. +/// +/// # Syntax +/// +/// ```rust,ignore +/// pack_structural!(Info = (String, i32)); +/// ``` +/// +/// This is equivalent to: +/// ```rust,ignore +/// pack!(Info = (String, i32)); +/// impl ::mingling::StructuralData for Info {} +/// ``` +pub(crate) fn pack_structural(input: TokenStream) -> TokenStream { + // Parse same input format as `pack!` + let input_parsed = syn::parse_macro_input!(input as PackStructuralInput); + let type_name = input_parsed.type_name; + let inner_type = input_parsed.inner_type; + let attrs = input_parsed.attrs; + let program_path = crate::default_program_path(); + + // Register in STRUCTURED_TYPES + let type_name_str = type_name.to_string(); + get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .insert(type_name_str); + + // Struct definition (with Serialize derive, same as pack! under general_renderer) + #[cfg(not(feature = "general_renderer"))] + let struct_def = quote! { + #(#attrs)* + pub struct #type_name { + pub inner: #inner_type, + } + }; + + #[cfg(feature = "general_renderer")] + let struct_def = quote! { + #(#attrs)* + #[derive(serde::Serialize)] + pub struct #type_name { + pub inner: #inner_type, + } + }; + + // Helper impls (same as pack!) + let new_impl = quote! { + impl #type_name { + pub fn new(inner: #inner_type) -> Self { + Self { inner } + } + } + }; + + let from_into_impl = quote! { + impl From<#inner_type> for #type_name { + fn from(inner: #inner_type) -> Self { + Self::new(inner) + } + } + impl From<#type_name> for #inner_type { + fn from(wrapper: #type_name) -> #inner_type { + wrapper.inner + } + } + }; + + let as_ref_impl = quote! { + impl ::std::convert::AsRef<#inner_type> for #type_name { + fn as_ref(&self) -> &#inner_type { + &self.inner + } + } + impl ::std::convert::AsMut<#inner_type> for #type_name { + fn as_mut(&mut self) -> &mut #inner_type { + &mut self.inner + } + } + }; + + let deref_impl = quote! { + impl ::std::ops::Deref for #type_name { + type Target = #inner_type; + fn deref(&self) -> &Self::Target { + &self.inner + } + } + impl ::std::ops::DerefMut for #type_name { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } + } + }; + + let default_impl = quote! { + impl ::std::default::Default for #type_name + where + #inner_type: ::std::default::Default, + { + fn default() -> Self { + Self::new(::std::default::Default::default()) + } + } + }; + + let register_impl = quote! { + ::mingling::macros::register_type!(#type_name); + }; + + // StructuralData impl + sealed + registration + let structural_impl = quote! { + impl ::mingling::__private::StructuralDataSealed for #type_name {} + impl ::mingling::__private::StructuralData for #type_name {} + }; + + let expanded = quote! { + #struct_def + + #new_impl + #from_into_impl + #as_ref_impl + #deref_impl + #default_impl + #register_impl + #structural_impl + + impl Into<::mingling::AnyOutput<#program_path>> for #type_name { + fn into(self) -> ::mingling::AnyOutput<#program_path> { + ::mingling::AnyOutput::new(self) + } + } + + impl Into<::mingling::ChainProcess<#program_path>> for #type_name { + fn into(self) -> ::mingling::ChainProcess<#program_path> { + ::mingling::AnyOutput::new(self).route_chain() + } + } + + impl ::mingling::Groupped<#program_path> for #type_name { + fn member_id() -> #program_path { + #program_path::#type_name + } + } + }; + + expanded.into() +} + +/// Input for `pack_structural!` — same format as `pack!`. +struct PackStructuralInput { + attrs: Vec<syn::Attribute>, + type_name: Ident, + inner_type: syn::Type, +} + +impl syn::parse::Parse for PackStructuralInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + let attrs = input.call(syn::Attribute::parse_outer)?; + let type_name: Ident = input.parse()?; + input.parse::<syn::Token![=]>()?; + let inner_type: syn::Type = input.parse()?; + Ok(PackStructuralInput { + attrs, + type_name, + inner_type, + }) + } +} + +/// `group_structural!` — like `group!` but also marks the type as supporting +/// structured output via `StructuralData`. +/// +/// # Syntax +/// +/// ```rust,ignore +/// group_structural!(Info = (String, i32)); +/// ``` +/// +/// This is equivalent to: +/// ```rust,ignore +/// group!(Info = (String, i32)); +/// impl ::mingling::StructuralData for Info {} +/// ``` +pub(crate) fn group_structural(input: TokenStream) -> TokenStream { + + // Parse the same input as group! + let input_parsed = syn::parse_macro_input!(input as GroupStructuralInput); + + let is_aliased = matches!(&input_parsed, GroupStructuralInput::Aliased { .. }); + + let (type_path, type_name, alias_stmt) = match &input_parsed { + GroupStructuralInput::Plain(type_path) => { + let name = type_path + .path + .segments + .last() + .expect("TypePath must have at least one segment") + .ident + .clone(); + (type_path.clone(), name, quote! {}) + } + GroupStructuralInput::Aliased { alias, type_path } => { + let alias_stmt = quote! { + pub(crate) type #alias = #type_path; + }; + (type_path.clone(), alias.clone(), alias_stmt) + } + }; + + let type_name_str = type_name.to_string(); + + // Register in STRUCTURED_TYPES + get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .insert(type_name_str); + + let program_path = crate::default_program_path(); + + // Generate unique module name + let segments: Vec<String> = type_path + .path + .segments + .iter() + .map(|seg| seg.ident.to_string().to_lowercase()) + .collect(); + let module_name = Ident::new( + &format!("internal_group_{}", segments.join("_")), + proc_macro2::Span::call_site(), + ); + + // Generate the appropriate `use` statement + let type_use = if type_path.path.segments.len() > 1 { + quote! { #[allow(unused_imports)] use #type_path; } + } else { + let ident = &type_name; + quote! { #[allow(unused_imports)] use super::#ident; } + }; + + let alias_use = if is_aliased { + quote! { use super::#type_name; } + } else { + quote! {} + }; + + let expanded = quote! { + #alias_stmt + #[allow(non_camel_case_types)] + mod #module_name { + use #program_path as __MinglingProgram; + #type_use + #alias_use + + impl ::mingling::Groupped<__MinglingProgram> for #type_name { + fn member_id() -> __MinglingProgram { + __MinglingProgram::#type_name + } + } + + impl ::mingling::__private::StructuralDataSealed for #type_name {} + impl ::mingling::__private::StructuralData for #type_name {} + + ::mingling::macros::register_type!(#type_name); + } + }; + + expanded.into() +} + +/// Input for `group_structural!` — same format as `group!`. +enum GroupStructuralInput { + Plain(TypePath), + Aliased { alias: Ident, type_path: TypePath }, +} + +impl syn::parse::Parse for GroupStructuralInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + let fork = input.fork(); + let _first: Ident = fork.parse()?; + if fork.peek(syn::Token![=]) { + let alias: Ident = input.parse()?; + let _eq: syn::Token![=] = input.parse()?; + let type_path: TypePath = input.parse()?; + Ok(GroupStructuralInput::Aliased { alias, type_path }) + } else { + let type_path: TypePath = input.parse()?; + Ok(GroupStructuralInput::Plain(type_path)) + } + } +} |
