diff --git a/xsd-parser/src/config/renderer.rs b/xsd-parser/src/config/renderer.rs index 3ea4a3c2..070557fc 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/pipeline/renderer/mod.rs b/xsd-parser/src/pipeline/renderer/mod.rs index 6cc74449..86c5e96d 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, + ContentHelpersRenderStep, DefaultsRenderStep, EnumConstantsRenderStep, + 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..1d8d28d3 --- /dev/null +++ b/xsd-parser/src/pipeline/renderer/steps/content_helper.rs @@ -0,0 +1,116 @@ +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 method_name = method_ident.to_string(); + let mut_method_name = method_name.strip_suffix('_').unwrap_or(&method_name); + let mut_method_ident = format_ident!("{mut_method_name}_mut"); + 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 )* + } + } +} diff --git a/xsd-parser/src/pipeline/renderer/steps/mod.rs b/xsd-parser/src/pipeline/renderer/steps/mod.rs index cca635c8..f271945d 100644 --- a/xsd-parser/src/pipeline/renderer/steps/mod.rs +++ b/xsd-parser/src/pipeline/renderer/steps/mod.rs @@ -1,3 +1,4 @@ +mod content_helper; mod defaults; mod enum_const; mod namespace_const; @@ -22,6 +23,7 @@ use crate::models::{ use super::Context; +pub use self::content_helper::ContentHelpersRenderStep; pub use self::defaults::DefaultsRenderStep; pub use self::enum_const::EnumConstantsRenderStep; pub use self::namespace_const::NamespaceConstantsRenderStep; 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/expected/keyword.rs b/xsd-parser/tests/feature/flattened_content_helpers/expected/keyword.rs new file mode 100644 index 00000000..7adc9b9d --- /dev/null +++ b/xsd-parser/tests/feature/flattened_content_helpers/expected/keyword.rs @@ -0,0 +1,40 @@ +pub type KeywordFoo = KeywordFooType; +#[derive(Debug)] +pub struct KeywordFooType { + pub content: Vec, +} +#[derive(Debug)] +pub enum KeywordFooTypeContent { + Type(String), + Match(i32), +} +impl KeywordFooType { + #[inline] + pub fn type_(&self) -> Option<&String> { + self.content.iter().find_map(|x| match x { + KeywordFooTypeContent::Type(v) => Option::Some(v), + _ => Option::None, + }) + } + #[inline] + pub fn type_mut(&mut self) -> Option<&mut String> { + self.content.iter_mut().find_map(|x| match x { + KeywordFooTypeContent::Type(v) => Option::Some(v), + _ => Option::None, + }) + } + #[inline] + pub fn match_(&self) -> Option<&i32> { + self.content.iter().find_map(|x| match x { + KeywordFooTypeContent::Match(v) => Option::Some(v), + _ => Option::None, + }) + } + #[inline] + pub fn match_mut(&mut self) -> Option<&mut i32> { + self.content.iter_mut().find_map(|x| match x { + KeywordFooTypeContent::Match(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..38dfb0f7 --- /dev/null +++ b/xsd-parser/tests/feature/flattened_content_helpers/mod.rs @@ -0,0 +1,29 @@ +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(), + ); +} + +#[test] +fn generate_keyword() { + generate_test( + "tests/feature/flattened_content_helpers/schema_keyword.xsd", + "tests/feature/flattened_content_helpers/expected/keyword.rs", + Config::test_default() + .with_render_step(ContentHelpersRenderStep) + .with_generate([(IdentType::Element, "tns:KeywordFoo")]), + ); +} 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/flattened_content_helpers/schema_keyword.xsd b/xsd-parser/tests/feature/flattened_content_helpers/schema_keyword.xsd new file mode 100644 index 00000000..75b7b32a --- /dev/null +++ b/xsd-parser/tests/feature/flattened_content_helpers/schema_keyword.xsd @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/xsd-parser/tests/feature/mod.rs b/xsd-parser/tests/feature/mod.rs index a62fe4f8..79deab0c 100644 --- a/xsd-parser/tests/feature/mod.rs +++ b/xsd-parser/tests/feature/mod.rs @@ -36,6 +36,7 @@ mod extension_simple_content; mod extra_derive; mod facets; mod facets_binary; +mod flattened_content_helpers; mod globally_allowed_attribute; mod group_modules; mod group_optional_followed_by_element;