diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-06-26 06:08:12 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-06-26 06:08:12 +0800 |
| commit | e735671acb3a81e1b7e334e56b9ef3963ba0c2fc (patch) | |
| tree | 46562d6630bb1582b41b6741a7a4f482febf84da /mingling_macros/src | |
| parent | 473cd8e575d03d8bd5439e81cb6835f56a1e964f (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`.
Diffstat (limited to 'mingling_macros/src')
| -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 |
5 files changed, 536 insertions, 21 deletions
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)) + } + } +} |
