Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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,
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
Expand Down
116 changes: 116 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,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<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;
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 )*
}
}
}
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,3 +1,4 @@
mod content_helper;
mod defaults;
mod enum_const;
mod namespace_const;
Expand All @@ -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;
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,
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
pub type KeywordFoo = KeywordFooType;
#[derive(Debug)]
pub struct KeywordFooType {
pub content: Vec<KeywordFooTypeContent>,
}
#[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,
})
}
}
29 changes: 29 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,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")]),
);
}
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>
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="KeywordFooType">
<xs:choice maxOccurs="unbounded">
<xs:element name="type" type="xs:string" />
<xs:element name="match" type="xs:int" />
</xs:choice>
</xs:complexType>

<xs:element name="KeywordFoo" type="tns:KeywordFooType" />
</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 @@ -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;
Expand Down
Loading