Skip to content
37 changes: 26 additions & 11 deletions xsd-parser/src/config/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FooContent>, ... }
/// 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.
Expand Down Expand Up @@ -250,6 +261,7 @@ impl RenderStepConfig for RenderStep {
Self::QuickXmlSerialize { .. } => RenderStepType::ExtraImpls,
Self::QuickXmlDeserialize { .. } => RenderStepType::ExtraImpls,
Self::QuickXmlCollectNamespaces => RenderStepType::ExtraImpls,
Self::ContentHelpers => RenderStepType::ExtraImpls,
}
}

Expand All @@ -259,11 +271,11 @@ impl RenderStepConfig for RenderStep {

fn into_render_step(self: Box<Self>) -> Box<dyn RenderStepTrait> {
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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -359,6 +372,7 @@ impl RenderStepConfig for RenderStep {
(Self::QuickXmlCollectNamespaces, None) => {
other_id == TypeId::of::<QuickXmlCollectNamespacesRenderStep>()
}
(Self::ContentHelpers, None) => other_id == TypeId::of::<ContentHelpersRenderStep>(),
_ => false,
}
}
Expand All @@ -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,
}
}
Expand Down
2 changes: 1 addition & 1 deletion xsd-parser/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};
10 changes: 5 additions & 5 deletions xsd-parser/src/pipeline/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
115 changes: 115 additions & 0 deletions xsd-parser/src/pipeline/renderer/steps/content_helper.rs
Original file line number Diff line number Diff line change
@@ -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<FooContent>, ... }
/// 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;
Comment thread
Exotik850 marked this conversation as resolved.
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 )*
}
}
}
2 changes: 2 additions & 0 deletions xsd-parser/src/pipeline/renderer/steps/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod defaults;
mod enum_const;
mod content_helper;
mod namespace_const;
mod prefix_const;
mod quick_xml;
Expand All @@ -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::{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Foo xmlns="http://example.com">
<Bar>hello</Bar>
<Baz>42</Baz>
<Bar>world</Bar>
</Foo>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
pub type Foo = FooType;
#[derive(Debug)]
pub struct FooType {
pub content: Vec<FooTypeContent>,
}
#[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,
})
}
}
18 changes: 18 additions & 0 deletions xsd-parser/tests/feature/flattened_content_helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -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(),
);
}
15 changes: 15 additions & 0 deletions xsd-parser/tests/feature/flattened_content_helpers/schema.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://example.com"
targetNamespace="http://example.com"
elementFormDefault="qualified">

<xs:complexType name="FooType">
<xs:choice maxOccurs="unbounded">
<xs:element name="Bar" type="xs:string" />
<xs:element name="Baz" type="xs:int" />
</xs:choice>
</xs:complexType>

<xs:element name="Foo" type="tns:FooType" />
</xs:schema>
1 change: 1 addition & 0 deletions xsd-parser/tests/feature/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading