aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src
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 /mingling_macros/src
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`.
Diffstat (limited to 'mingling_macros/src')
-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
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))
+ }
+ }
+}