From 88ed1f28194af4cfd0731ee19b0fe07f5dd5ae8f Mon Sep 17 00:00:00 2001 From: Bergmann89 Date: Wed, 25 Mar 2026 06:56:51 +0000 Subject: [PATCH] Add example for custom naming --- xsd-parser-types/src/xml/any_simple_type.rs | 1 + xsd-parser/examples/README.md | 1 + xsd-parser/examples/custom-naming.xsd | 20 ++++ xsd-parser/examples/custom_naming.rs | 105 ++++++++++++++++++++ xsd-parser/src/models/mod.rs | 4 +- xsd-parser/src/models/naming/default.rs | 32 +----- xsd-parser/src/models/naming/mod.rs | 35 +++++++ 7 files changed, 167 insertions(+), 31 deletions(-) create mode 100644 xsd-parser/examples/custom-naming.xsd create mode 100644 xsd-parser/examples/custom_naming.rs diff --git a/xsd-parser-types/src/xml/any_simple_type.rs b/xsd-parser-types/src/xml/any_simple_type.rs index 71043705..7859817c 100644 --- a/xsd-parser-types/src/xml/any_simple_type.rs +++ b/xsd-parser-types/src/xml/any_simple_type.rs @@ -604,6 +604,7 @@ fn parse_base64_binary(bytes: &str) -> Result { #[inline] #[cfg(not(feature = "base64"))] +#[allow(clippy::unnecessary_wraps)] fn parse_base64_binary(s: &str) -> Result { Ok(s.to_string()) } diff --git a/xsd-parser/examples/README.md b/xsd-parser/examples/README.md index abbba873..a31d95ae 100644 --- a/xsd-parser/examples/README.md +++ b/xsd-parser/examples/README.md @@ -3,5 +3,6 @@ This directory contains different examples that show some features of `xsd-parse - `simple` - Probably the most simple example, at least, a starting point to understand `xsd-parser` or start more complex applications. - `update_schema` - It demonstrates a more advanced use of the generator to create types from the schema with deserialization support enabled. - `custom_names` - Short example that shows how to assign custom defined names to the generated types. +- `custom_naming` - Self-contained example that shows how to plug a custom `Naming` implementation into `Config::with_naming` to avoid name collisions. - `custom_variants` - Similar to `custom_names`, this example shows how to assign custom names for enumeration variants. - `custom_render_step` - Example that explains how to implement user defined render steps. This is useful if you want to generate additional structures from the already parsed and interpreted schema. diff --git a/xsd-parser/examples/custom-naming.xsd b/xsd-parser/examples/custom-naming.xsd new file mode 100644 index 00000000..bb07bed5 --- /dev/null +++ b/xsd-parser/examples/custom-naming.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/xsd-parser/examples/custom_naming.rs b/xsd-parser/examples/custom_naming.rs new file mode 100644 index 00000000..af198197 --- /dev/null +++ b/xsd-parser/examples/custom_naming.rs @@ -0,0 +1,105 @@ +//! Example that shows how to implement a custom naming strategy using +//! [`Config::with_naming`] with a custom [`Naming`] implementation. +//! +//! The default naming strategy normalizes names into `PascalCase`, which means an +//! XSD type like `String..1` can collapse into the same Rust type name as +//! `String1`. This example keeps separators during `unify`, so both schema +//! names stay distinct while the final identifier formatting is still delegated +//! to the library helpers. + +#![allow(missing_docs)] + +use std::sync::{atomic::AtomicUsize, Arc}; + +use anyhow::{ensure, Error}; +use inflector::Inflector; +use xsd_parser::{ + config::Schema, + generate, + models::{ + format_ident, format_unknown_variant, make_type_name, meta::MetaType, + NameBuilder as DefaultNameBuilder, + }, + traits::{NameBuilder, Naming}, + Config, Ident2, IdentType, Name, TypeIdent, +}; + +fn main() -> Result<(), Error> { + let mut config = Config::default(); + config.parser.schemas = vec![Schema::named_schema( + "custom-naming.xsd", + include_str!("custom-naming.xsd"), + )]; + config = config + .with_naming(PreserveSeparatorsNaming::default()) + .with_generate([(IdentType::Type, "Document")]); + + let code = generate(config)?.to_string(); + + ensure!( + code.contains("String__1Type"), + "expected generated code to keep `String..1` distinct" + ); + ensure!( + code.contains("String1Type"), + "expected generated code to keep `String1` available as its own type" + ); + ensure!( + code.contains("type_"), + "expected keyword field names to still be formatted into valid Rust identifiers" + ); + + println!("{code}"); + + Ok(()) +} + +#[derive(Debug, Clone, Default)] +struct PreserveSeparatorsNaming(Arc); + +impl Naming for PreserveSeparatorsNaming { + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } + + fn builder(&self) -> Box { + Box::new(DefaultNameBuilder::new( + self.0.clone(), + Box::new(self.clone()), + )) + } + + fn unify(&self, s: &str) -> String { + s.chars() + .map(|ch| if ch.is_alphanumeric() { ch } else { '_' }) + .collect() + } + + fn make_type_name(&self, postfixes: &[String], ty: &MetaType, ident: &TypeIdent) -> Name { + make_type_name(self, postfixes, ty, ident) + } + + fn make_unknown_variant(&self, id: usize) -> Ident2 { + format_unknown_variant(id) + } + + fn format_module_name(&self, s: &str) -> String { + format_ident(self.unify(s).to_snake_case()) + } + + fn format_type_name(&self, s: &str) -> String { + format_ident(self.unify(s)) + } + + fn format_field_name(&self, s: &str) -> String { + format_ident(self.unify(s).to_snake_case()) + } + + fn format_variant_name(&self, s: &str) -> String { + format_ident(self.unify(s)) + } + + fn format_constant_name(&self, s: &str) -> String { + format_ident(self.unify(s).to_screaming_snake_case()) + } +} diff --git a/xsd-parser/src/models/mod.rs b/xsd-parser/src/models/mod.rs index 92611fc1..a3720871 100644 --- a/xsd-parser/src/models/mod.rs +++ b/xsd-parser/src/models/mod.rs @@ -27,6 +27,6 @@ pub use self::ident::{ pub use self::ident_cache::IdentCache; pub use self::name::Name; pub use self::naming::{ - format_ident, format_unknown_variant, unify_string, ExplicitNameBuilder, ExplicitNaming, - NameBuilder, Naming, + format_ident, format_unknown_variant, make_type_name, unify_string, ExplicitNameBuilder, + ExplicitNaming, NameBuilder, Naming, }; diff --git a/xsd-parser/src/models/naming/default.rs b/xsd-parser/src/models/naming/default.rs index d849c996..3b034592 100644 --- a/xsd-parser/src/models/naming/default.rs +++ b/xsd-parser/src/models/naming/default.rs @@ -7,11 +7,7 @@ use std::sync::{ use inflector::Inflector; use proc_macro2::Ident as Ident2; -use crate::models::{ - meta::{MetaType, MetaTypeVariant}, - schema::MaxOccurs, - TypeIdent, -}; +use crate::models::{meta::MetaType, TypeIdent}; use crate::traits::{NameBuilder as NameBuilderTrait, Naming as NamingTrait}; use super::Name; @@ -29,7 +25,7 @@ impl NamingTrait for Naming { } fn builder(&self) -> Box { - Box::new(NameBuilder::new(self.0.clone(), self.clone_boxed())) + Box::new(NameBuilder::new(self.0.clone(), Box::new(self.clone()))) } fn unify(&self, s: &str) -> String { @@ -37,29 +33,7 @@ impl NamingTrait for Naming { } fn make_type_name(&self, postfixes: &[String], ty: &MetaType, ident: &TypeIdent) -> Name { - if let MetaTypeVariant::Reference(ti) = &ty.variant { - if ident.name.is_generated() && ti.type_.name.is_named() { - let s = self.format_type_name(ti.type_.name.as_str()); - - if ti.max_occurs > MaxOccurs::Bounded(1) { - return Name::new_generated(format!("{s}List")); - } else if ti.min_occurs == 0 { - return Name::new_generated(format!("{s}Opt")); - } - } - } - - let postfix = postfixes - .get(ident.type_ as usize) - .map_or("", |s| s.as_str()); - - let s = self.format_type_name(ident.name.as_str()); - - if s.ends_with(postfix) { - ident.name.clone() - } else { - Name::new_generated(format!("{s}{postfix}")) - } + super::make_type_name(self, postfixes, ty, ident) } fn make_unknown_variant(&self, id: usize) -> Ident2 { diff --git a/xsd-parser/src/models/naming/mod.rs b/xsd-parser/src/models/naming/mod.rs index 133765b6..9589cf22 100644 --- a/xsd-parser/src/models/naming/mod.rs +++ b/xsd-parser/src/models/naming/mod.rs @@ -7,11 +7,46 @@ use inflector::Inflector; use proc_macro2::Ident as Ident2; use quote::format_ident; +use crate::config::MetaType; +use crate::models::meta::MetaTypeVariant; +use crate::models::schema::MaxOccurs; +use crate::TypeIdent; + use super::Name; pub use self::default::{NameBuilder, Naming}; pub use self::explicit::{ExplicitNameBuilder, ExplicitNaming}; +/// Make a type name based on the passed `ident` and `ty` and the provided `postfixes`. +pub fn make_type_name(naming: &X, postfixes: &[String], ty: &MetaType, ident: &TypeIdent) -> Name +where + X: crate::traits::Naming, +{ + if let MetaTypeVariant::Reference(ti) = &ty.variant { + if ident.name.is_generated() && ti.type_.name.is_named() { + let s = naming.format_type_name(ti.type_.name.as_str()); + + if ti.max_occurs > MaxOccurs::Bounded(1) { + return Name::new_generated(format!("{s}List")); + } else if ti.min_occurs == 0 { + return Name::new_generated(format!("{s}Opt")); + } + } + } + + let postfix = postfixes + .get(ident.type_ as usize) + .map_or("", |s| s.as_str()); + + let s = naming.format_type_name(ident.name.as_str()); + + if s.ends_with(postfix) { + ident.name.clone() + } else { + Name::new_generated(format!("{s}{postfix}")) + } +} + /// Unify the passed string `s` into a standard format. #[must_use] pub fn unify_string(s: &str) -> String {