From e735671acb3a81e1b7e334e56b9ef3963ba0c2fc Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Fri, 26 Jun 2026 06:08:12 +0800 Subject: 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`. --- mingling_core/src/any.rs | 31 +++----- mingling_core/src/any/group.rs | 92 ++++++---------------- mingling_core/src/lib.rs | 15 ++++ mingling_core/src/renderer/general.rs | 58 ++++++++------ .../src/renderer/general/structural_data.rs | 15 ++++ mingling_core/tests/test-all/tests/integration.rs | 3 +- .../test-general-renderer/tests/integration.rs | 6 +- 7 files changed, 105 insertions(+), 115 deletions(-) create mode 100644 mingling_core/src/renderer/general/structural_data.rs (limited to 'mingling_core') 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 { @@ -24,21 +22,7 @@ pub struct AnyOutput { } impl AnyOutput { - /// Create an `AnyOutput` from a `Send + Groupped + Serialize` type - #[cfg(feature = "general_renderer")] - pub fn new(value: T) -> Self - where - T: Send + Groupped + Serialize + 'static, - { - Self { - inner: Box::new(value), - type_id: std::any::TypeId::of::(), - member_id: T::member_id(), - } - } - /// Create an `AnyOutput` from a `Send + Groupped` type - #[cfg(not(feature = "general_renderer"))] pub fn new(value: T) -> Self where T: Send + Groupped + 'static, @@ -82,9 +66,14 @@ impl AnyOutput { ChainProcess::Ok((self, NextProcess::Renderer)) } - #[cfg(feature = "general_renderer")] - /// Restore `AnyOutput` back to the original Serialize type - pub fn restore(self) -> Option { + /// 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(self) -> Option { if self.type_id == std::any::TypeId::of::() { match self.inner.downcast::() { 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 +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 +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 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 - 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 - 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 + /// 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 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 - 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 - 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( + pub fn render( 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( + fn render_to_json( 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( + fn render_to_json_pretty( 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( + fn render_to_ron( 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( + fn render_to_ron_pretty( 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( + fn render_to_toml( 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( + fn render_to_yaml( 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, -- cgit