Skip to content
1 change: 1 addition & 0 deletions xsd-parser/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ pub use self::naming::{
format_ident, format_unknown_variant, make_type_name, unify_string, ExplicitNameBuilder,
ExplicitNaming, NameBuilder, Naming,
};
pub(crate) use naming::KEYWORDS;
2 changes: 1 addition & 1 deletion xsd-parser/src/models/naming/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ where

/// List of keywords that needs to be replaced by something else.
/// This list needs to be sorted, because we use it in [`core::slice::binary_search_by`]
const KEYWORDS: &[(&str, &str)] = &[
pub(crate) const KEYWORDS: &[(&str, &str)] = &[
Comment thread
Exotik850 marked this conversation as resolved.
Outdated
("Self", "Self_"),
("abstract", "abstract_"),
("as", "as_"),
Expand Down
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, FlattenedContentHelpersRenderStep,
NamespaceConstantsRenderStep, NamespaceSerialization, PrefixConstantsRenderStep,
QuickXmlCollectNamespacesRenderStep, QuickXmlDeserializeRenderStep,
QuickXmlSerializeRenderStep, SerdeQuickXmlTypesRenderStep, SerdeXmlRsV7TypesRenderStep,
SerdeXmlRsV8TypesRenderStep, TypesRenderStep, WithNamespaceTraitRenderStep,
};

/// The [`Renderer`] is the central orchestrator for Rust code generation from
Expand Down
126 changes: 126 additions & 0 deletions xsd-parser/src/pipeline/renderer/steps/flattened.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use inflector::Inflector;
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> { ... }
/// }
/// Enabled via [`RendererFlags::FLATTENED_CONTENT_HELPERS`].
#[derive(Debug, Clone, Copy)]
pub struct FlattenedContentHelpersRenderStep;
Comment thread
Exotik850 marked this conversation as resolved.
Outdated
Comment thread
Exotik850 marked this conversation as resolved.
Outdated

impl RenderStep for FlattenedContentHelpersRenderStep {
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
Comment thread
Exotik850 marked this conversation as resolved.
Outdated
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().map(|e| {
let variant_ident = &e.variant_ident;
let method_ident = crate::models::KEYWORDS
.iter()
.find_map(|(key, value)| {
if variant_ident == key {
Some(format_ident!("{value}"))
} else {
None
}
})
.unwrap_or(snake_case_ident(variant_ident));
let mut_method_ident = format_ident!("{method_ident}_mut");
let target_ty = ctx.resolve_type_for_module(&e.target_type);
let option = ctx.resolve_build_in("::core::option::Option");

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,
}
})
}
}
});

quote! {
impl #struct_ident {
#( #methods )*
}
}
}

/// Convert an identifier to a snake_case method name identifier.
fn snake_case_ident(ident: &proc_macro2::Ident) -> proc_macro2::Ident {
format_ident!("{}", ident.to_string().to_snake_case())
}
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 flattened;
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::flattened::FlattenedContentHelpersRenderStep;
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::FlattenedContentHelpersRenderStep, Config, IdentType};

use crate::utils::{generate_test, ConfigEx};

fn config() -> Config {
Config::test_default()
.with_render_step(FlattenedContentHelpersRenderStep)
.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