diff --git a/xsd-parser/src/config/renderer.rs b/xsd-parser/src/config/renderer.rs index 3ea4a3c2..cfe3fe7f 100644 --- a/xsd-parser/src/config/renderer.rs +++ b/xsd-parser/src/config/renderer.rs @@ -195,6 +195,17 @@ pub enum RenderStep { /// that the serializer can discover which XML namespaces are actually needed /// at runtime before writing the root start element. QuickXmlCollectNamespaces, + + /// Renderer that generates ergonomic helper accessors for flattened struct content. + /// + /// This targets the pattern: + /// pub struct Foo { pub content: Vec, ... } + /// pub enum FooContent { PrivateNote(String), ... } + /// and generates: + /// impl Foo { + /// pub fn private_note(&self) -> Option<&String> { ... } + /// } + ContentHelpers, } /// Helper trait to deal with custom render steps. @@ -250,6 +261,7 @@ impl RenderStepConfig for RenderStep { Self::QuickXmlSerialize { .. } => RenderStepType::ExtraImpls, Self::QuickXmlDeserialize { .. } => RenderStepType::ExtraImpls, Self::QuickXmlCollectNamespaces => RenderStepType::ExtraImpls, + Self::ContentHelpers => RenderStepType::ExtraImpls, } } @@ -259,11 +271,11 @@ impl RenderStepConfig for RenderStep { fn into_render_step(self: Box) -> Box { use crate::pipeline::renderer::{ - DefaultsRenderStep, EnumConstantsRenderStep, NamespaceConstantsRenderStep, - PrefixConstantsRenderStep, QuickXmlCollectNamespacesRenderStep, - QuickXmlDeserializeRenderStep, QuickXmlSerializeRenderStep, - SerdeQuickXmlTypesRenderStep, SerdeXmlRsV7TypesRenderStep, SerdeXmlRsV8TypesRenderStep, - TypesRenderStep, WithNamespaceTraitRenderStep, + ContentHelpersRenderStep, DefaultsRenderStep, EnumConstantsRenderStep, + NamespaceConstantsRenderStep, PrefixConstantsRenderStep, + QuickXmlCollectNamespacesRenderStep, QuickXmlDeserializeRenderStep, + QuickXmlSerializeRenderStep, SerdeQuickXmlTypesRenderStep, SerdeXmlRsV7TypesRenderStep, + SerdeXmlRsV8TypesRenderStep, TypesRenderStep, WithNamespaceTraitRenderStep, }; match *self { @@ -291,16 +303,17 @@ impl RenderStepConfig for RenderStep { Box::new(QuickXmlDeserializeRenderStep { boxed_deserializer }) } Self::QuickXmlCollectNamespaces => Box::new(QuickXmlCollectNamespacesRenderStep), + Self::ContentHelpers => Box::new(ContentHelpersRenderStep), } } fn is_mutual_exclusive_to(&self, other: &dyn RenderStepConfig) -> bool { use crate::pipeline::renderer::{ - DefaultsRenderStep, EnumConstantsRenderStep, NamespaceConstantsRenderStep, - PrefixConstantsRenderStep, QuickXmlCollectNamespacesRenderStep, - QuickXmlDeserializeRenderStep, QuickXmlSerializeRenderStep, - SerdeQuickXmlTypesRenderStep, SerdeXmlRsV7TypesRenderStep, SerdeXmlRsV8TypesRenderStep, - TypesRenderStep, WithNamespaceTraitRenderStep, + ContentHelpersRenderStep, DefaultsRenderStep, EnumConstantsRenderStep, + NamespaceConstantsRenderStep, PrefixConstantsRenderStep, + QuickXmlCollectNamespacesRenderStep, QuickXmlDeserializeRenderStep, + QuickXmlSerializeRenderStep, SerdeQuickXmlTypesRenderStep, SerdeXmlRsV7TypesRenderStep, + SerdeXmlRsV8TypesRenderStep, TypesRenderStep, WithNamespaceTraitRenderStep, }; if self @@ -359,6 +372,7 @@ impl RenderStepConfig for RenderStep { (Self::QuickXmlCollectNamespaces, None) => { other_id == TypeId::of::() } + (Self::ContentHelpers, None) => other_id == TypeId::of::(), _ => false, } } @@ -380,7 +394,8 @@ impl RenderStep { | (Self::WithNamespaceTrait, Self::WithNamespaceTrait) | (Self::QuickXmlSerialize { .. }, Self::QuickXmlSerialize { .. }) | (Self::QuickXmlDeserialize { .. }, Self::QuickXmlDeserialize { .. }) - | (Self::QuickXmlCollectNamespaces, Self::QuickXmlCollectNamespaces) => true, + | (Self::QuickXmlCollectNamespaces, Self::QuickXmlCollectNamespaces) + | (Self::ContentHelpers, Self::ContentHelpers) => true, (_, _) => false, } } diff --git a/xsd-parser/src/models/mod.rs b/xsd-parser/src/models/mod.rs index a3720871..27769e61 100644 --- a/xsd-parser/src/models/mod.rs +++ b/xsd-parser/src/models/mod.rs @@ -29,4 +29,4 @@ pub use self::name::Name; pub use self::naming::{ format_ident, format_unknown_variant, make_type_name, unify_string, ExplicitNameBuilder, ExplicitNaming, NameBuilder, Naming, -}; +}; \ No newline at end of file diff --git a/xsd-parser/src/pipeline/renderer/mod.rs b/xsd-parser/src/pipeline/renderer/mod.rs index 6cc74449..e9ef279f 100644 --- a/xsd-parser/src/pipeline/renderer/mod.rs +++ b/xsd-parser/src/pipeline/renderer/mod.rs @@ -43,11 +43,11 @@ pub use self::custom::{ValueRenderer, ValueRendererBox}; pub use self::error::Error; pub use self::meta::MetaData; pub use self::steps::{ - DefaultsRenderStep, EnumConstantsRenderStep, NamespaceConstantsRenderStep, - NamespaceSerialization, PrefixConstantsRenderStep, QuickXmlCollectNamespacesRenderStep, - QuickXmlDeserializeRenderStep, QuickXmlSerializeRenderStep, SerdeQuickXmlTypesRenderStep, - SerdeXmlRsV7TypesRenderStep, SerdeXmlRsV8TypesRenderStep, TypesRenderStep, - WithNamespaceTraitRenderStep, + DefaultsRenderStep, EnumConstantsRenderStep, ContentHelpersRenderStep, + NamespaceConstantsRenderStep, NamespaceSerialization, PrefixConstantsRenderStep, + QuickXmlCollectNamespacesRenderStep, QuickXmlDeserializeRenderStep, + QuickXmlSerializeRenderStep, SerdeQuickXmlTypesRenderStep, SerdeXmlRsV7TypesRenderStep, + SerdeXmlRsV8TypesRenderStep, TypesRenderStep, WithNamespaceTraitRenderStep, }; /// The [`Renderer`] is the central orchestrator for Rust code generation from diff --git a/xsd-parser/src/pipeline/renderer/steps/content_helper.rs b/xsd-parser/src/pipeline/renderer/steps/content_helper.rs new file mode 100644 index 00000000..6b8c6ca6 --- /dev/null +++ b/xsd-parser/src/pipeline/renderer/steps/content_helper.rs @@ -0,0 +1,115 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +use crate::models::data::{ComplexData, ComplexDataEnum, DataTypeVariant, Occurs}; +use crate::pipeline::renderer::{Context, RenderStep, RenderStepType}; + +/// RenderStep that generates ergonomic helper accessors for flattened struct content. +/// +/// This targets the pattern: +/// pub struct Foo { pub content: Vec, ... } +/// pub enum FooContent { PrivateNote(String), ... } +/// +/// and generates: +/// impl Foo { +/// pub fn private_note(&self) -> Option<&String> { ... } +/// } +#[derive(Debug, Clone, Copy)] +pub struct ContentHelpersRenderStep; + +impl RenderStep for ContentHelpersRenderStep { + fn render_step_type(&self) -> RenderStepType { + RenderStepType::ExtraImpls + } + + fn render_type(&mut self, ctx: &mut Context<'_, '_>) { + let DataTypeVariant::Complex(complex) = &ctx.data.variant else { + return; + }; + let ComplexData::Struct { + type_, + content_type, + } = complex + else { + return; + }; + let Some(content) = type_.content() else { + return; + }; + if content.occurs != Occurs::DynamicList { + return; + }; + + // The content enum is stored inline as content_type of ComplexData::Struct + let Some(content_type) = content_type else { + return; + }; + + let impl_block = match content_type.as_ref() { + ComplexData::Enum { + type_: enum_type, .. + } => render_helpers_for_complex_enum( + ctx, + &type_.base.type_ident, + &content.field_ident, + enum_type, + ), + ComplexData::Struct { .. } => { + // Struct content (e.g. a sequence) - no enum variants to flatten + return; + } + }; + + ctx.current_module().append(impl_block); + } +} + +/// Generate helper accessor methods for a [`ComplexDataEnum`] content type. +fn render_helpers_for_complex_enum( + ctx: &Context<'_, '_>, + struct_ident: &proc_macro2::Ident, + content_field_ident: &proc_macro2::Ident, + enum_type: &ComplexDataEnum<'_>, +) -> TokenStream { + let enum_ident = &enum_type.base.type_ident; + + let methods = enum_type.elements.iter().filter_map(|e| { + if e.occurs != Occurs::Single { + return None; + } + let variant_ident = &e.variant_ident; + let method_ident = &e.field_ident; + let mut_method = format!("{}_mut", method_ident).replace("__", "_"); + let mut_method_ident = format_ident!("{mut_method}"); + let target_ty = ctx.resolve_type_for_module(&e.target_type); + let option = ctx.resolve_build_in("::core::option::Option"); + + let out = quote! { + #[inline] + pub fn #method_ident(&self) -> #option<&#target_ty> { + self.#content_field_ident.iter().find_map(|x| { + match x { + #enum_ident::#variant_ident(v) => #option::Some(v), + _ => #option::None, + } + }) + } + #[inline] + pub fn #mut_method_ident(&mut self) -> #option<&mut #target_ty> { + self.#content_field_ident.iter_mut().find_map(|x| { + match x { + #enum_ident::#variant_ident(v) => #option::Some(v), + _ => #option::None, + } + }) + } + }; + Some(out) + }); + + quote! { + impl #struct_ident { + #( #methods )* + } + } +} \ No newline at end of file diff --git a/xsd-parser/src/pipeline/renderer/steps/mod.rs b/xsd-parser/src/pipeline/renderer/steps/mod.rs index cca635c8..076c6228 100644 --- a/xsd-parser/src/pipeline/renderer/steps/mod.rs +++ b/xsd-parser/src/pipeline/renderer/steps/mod.rs @@ -1,5 +1,6 @@ mod defaults; mod enum_const; +mod content_helper; mod namespace_const; mod prefix_const; mod quick_xml; @@ -24,6 +25,7 @@ use super::Context; pub use self::defaults::DefaultsRenderStep; pub use self::enum_const::EnumConstantsRenderStep; +pub use self::content_helper::ContentHelpersRenderStep; pub use self::namespace_const::NamespaceConstantsRenderStep; pub use self::prefix_const::PrefixConstantsRenderStep; pub use self::quick_xml::{ diff --git a/xsd-parser/tests/feature/flattened_content_helpers/example/default.xml b/xsd-parser/tests/feature/flattened_content_helpers/example/default.xml new file mode 100644 index 00000000..1ffd5e0f --- /dev/null +++ b/xsd-parser/tests/feature/flattened_content_helpers/example/default.xml @@ -0,0 +1,6 @@ + + + hello + 42 + world + \ No newline at end of file diff --git a/xsd-parser/tests/feature/flattened_content_helpers/expected/default.rs b/xsd-parser/tests/feature/flattened_content_helpers/expected/default.rs new file mode 100644 index 00000000..ada04e50 --- /dev/null +++ b/xsd-parser/tests/feature/flattened_content_helpers/expected/default.rs @@ -0,0 +1,40 @@ +pub type Foo = FooType; +#[derive(Debug)] +pub struct FooType { + pub content: Vec, +} +#[derive(Debug)] +pub enum FooTypeContent { + Bar(String), + Baz(i32), +} +impl FooType { + #[inline] + pub fn bar(&self) -> Option<&String> { + self.content.iter().find_map(|x| match x { + FooTypeContent::Bar(v) => Option::Some(v), + _ => Option::None, + }) + } + #[inline] + pub fn bar_mut(&mut self) -> Option<&mut String> { + self.content.iter_mut().find_map(|x| match x { + FooTypeContent::Bar(v) => Option::Some(v), + _ => Option::None, + }) + } + #[inline] + pub fn baz(&self) -> Option<&i32> { + self.content.iter().find_map(|x| match x { + FooTypeContent::Baz(v) => Option::Some(v), + _ => Option::None, + }) + } + #[inline] + pub fn baz_mut(&mut self) -> Option<&mut i32> { + self.content.iter_mut().find_map(|x| match x { + FooTypeContent::Baz(v) => Option::Some(v), + _ => Option::None, + }) + } +} diff --git a/xsd-parser/tests/feature/flattened_content_helpers/mod.rs b/xsd-parser/tests/feature/flattened_content_helpers/mod.rs new file mode 100644 index 00000000..773ffe4b --- /dev/null +++ b/xsd-parser/tests/feature/flattened_content_helpers/mod.rs @@ -0,0 +1,18 @@ +use xsd_parser::{pipeline::renderer::ContentHelpersRenderStep, Config, IdentType}; + +use crate::utils::{generate_test, ConfigEx}; + +fn config() -> Config { + Config::test_default() + .with_render_step(ContentHelpersRenderStep) + .with_generate([(IdentType::Element, "tns:Foo")]) +} + +#[test] +fn generate_default() { + generate_test( + "tests/feature/flattened_content_helpers/schema.xsd", + "tests/feature/flattened_content_helpers/expected/default.rs", + config(), + ); +} diff --git a/xsd-parser/tests/feature/flattened_content_helpers/schema.xsd b/xsd-parser/tests/feature/flattened_content_helpers/schema.xsd new file mode 100644 index 00000000..8f3d40ac --- /dev/null +++ b/xsd-parser/tests/feature/flattened_content_helpers/schema.xsd @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/xsd-parser/tests/feature/mod.rs b/xsd-parser/tests/feature/mod.rs index a62fe4f8..44589db0 100644 --- a/xsd-parser/tests/feature/mod.rs +++ b/xsd-parser/tests/feature/mod.rs @@ -19,6 +19,7 @@ mod complex_type_with_repeated_content; mod content_display_name; mod custom_type; mod defaultable_content; +mod flattened_content_helpers; mod derive; mod documentation; mod duplicate_idents;