aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-26 06:08:12 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-26 06:08:12 +0800
commite735671acb3a81e1b7e334e56b9ef3963ba0c2fc (patch)
tree46562d6630bb1582b41b6741a7a4f482febf84da
parent473cd8e575d03d8bd5439e81cb6835f56a1e964f (diff)
feat(core): decouple structured output from Groupped trait
Introduce `StructuralData` sealed trait and `pack_structural!` / `group_structural!` / `derive(StructuralData)` macros to control structured rendering separately from grouping. `Groupped` no longer requires `Serialize`.
-rw-r--r--CHANGELOG.md10
-rw-r--r--README.md3
-rw-r--r--examples/example-general-renderer/src/main.rs13
-rw-r--r--examples/example-pack-err/src/main.rs53
-rw-r--r--examples/test-examples.toml22
-rw-r--r--mingling/Cargo.toml8
-rw-r--r--mingling/src/example_docs.rs66
-rw-r--r--mingling/src/lib.rs19
-rw-r--r--mingling_core/src/any.rs31
-rw-r--r--mingling_core/src/any/group.rs92
-rw-r--r--mingling_core/src/lib.rs15
-rw-r--r--mingling_core/src/renderer/general.rs58
-rw-r--r--mingling_core/src/renderer/general/structural_data.rs15
-rw-r--r--mingling_core/tests/test-all/tests/integration.rs3
-rw-r--r--mingling_core/tests/test-general-renderer/tests/integration.rs6
-rw-r--r--mingling_macros/src/lib.rs88
-rw-r--r--mingling_macros/src/pack.rs12
-rw-r--r--mingling_macros/src/pack_err.rs116
-rw-r--r--mingling_macros/src/renderer.rs11
-rw-r--r--mingling_macros/src/structural_data.rs330
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:
diff --git a/README.md b/README.md
index b5d3362..f6e782d 100644
--- a/README.md
+++ b/README.md
@@ -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))
+ }
+ }
+}