polyxml 0.24.0__tar.gz → 0.25.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {polyxml-0.24.0 → polyxml-0.25.0}/Cargo.lock +6 -6
  2. {polyxml-0.24.0 → polyxml-0.25.0}/Cargo.toml +1 -1
  3. {polyxml-0.24.0 → polyxml-0.25.0}/PKG-INFO +4 -4
  4. {polyxml-0.24.0/crates/polyxml-python → polyxml-0.25.0}/README.md +3 -3
  5. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/codegen/cpp/mod.rs +91 -22
  6. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/codegen/csharp/mod.rs +83 -22
  7. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/codegen/go/mod.rs +142 -28
  8. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/codegen/java/codec.rs +45 -2
  9. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/codegen/java/mod.rs +4 -2
  10. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/codegen/mod.rs +64 -1
  11. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/codegen/python/mod.rs +4 -2
  12. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/codegen/rust/mod.rs +6 -0
  13. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/codegen/typescript/mod.rs +32 -13
  14. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/schema_parser/mod.rs +131 -50
  15. polyxml-0.25.0/crates/polyxml-core/tests/test_adversarial_identifiers.rs +285 -0
  16. polyxml-0.25.0/crates/polyxml-core/tests/test_unbounded_choice.rs +283 -0
  17. {polyxml-0.24.0 → polyxml-0.25.0/crates/polyxml-python}/README.md +3 -3
  18. {polyxml-0.24.0 → polyxml-0.25.0}/pyproject.toml +1 -1
  19. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/Cargo.toml +0 -0
  20. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/README.md +0 -0
  21. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
  22. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/benches/support/tag_dispatch_fixtures.rs +0 -0
  23. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/benches/tag_dispatch.rs +0 -0
  24. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/codegen/java/models.rs +0 -0
  25. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/codegen/python/aot.rs +0 -0
  26. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/converters.rs +0 -0
  27. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/error.rs +0 -0
  28. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/ir/mod.rs +0 -0
  29. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/ir/tarjan.rs +0 -0
  30. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/json.rs +0 -0
  31. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/lib.rs +0 -0
  32. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/parser.rs +0 -0
  33. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/schema.rs +0 -0
  34. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/serializer.rs +0 -0
  35. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/transcoder.rs +0 -0
  36. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/src/value.rs +0 -0
  37. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_conformance.rs +0 -0
  38. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_core.rs +0 -0
  39. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_cpp_codegen.rs +0 -0
  40. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_csharp_codegen.rs +0 -0
  41. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_go_codegen.rs +0 -0
  42. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_java_codegen.rs +0 -0
  43. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_json.rs +0 -0
  44. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_pattern_codegen.rs +0 -0
  45. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_python_codegen.rs +0 -0
  46. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_rust_codecs.rs +0 -0
  47. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_rust_codegen.rs +0 -0
  48. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_schema_audit.rs +0 -0
  49. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_schema_ir.rs +0 -0
  50. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_security.rs +0 -0
  51. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_transcoder.rs +0 -0
  52. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_ts_codegen.rs +0 -0
  53. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-core/tests/test_xsi_type.rs +0 -0
  54. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-python/Cargo.toml +0 -0
  55. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-python/benches/aot_vs_dataclass.py +0 -0
  56. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-python/src/lib.rs +0 -0
  57. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-python/tests/test_conformance.py +0 -0
  58. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-python/tests/test_generated_models.py +0 -0
  59. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-python/tests/test_json.py +0 -0
  60. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-python/tests/test_polyxml.py +0 -0
  61. {polyxml-0.24.0 → polyxml-0.25.0}/crates/polyxml-python/tests/test_xsi_type.py +0 -0
  62. {polyxml-0.24.0 → polyxml-0.25.0}/python/polyxml/AGENT_GUIDE.md +0 -0
  63. {polyxml-0.24.0 → polyxml-0.25.0}/python/polyxml/__init__.py +0 -0
  64. {polyxml-0.24.0 → polyxml-0.25.0}/python/polyxml/compat/__init__.py +0 -0
  65. {polyxml-0.24.0 → polyxml-0.25.0}/python/polyxml/compat/xsdata.py +0 -0
  66. {polyxml-0.24.0 → polyxml-0.25.0}/python/polyxml/py.typed +0 -0
@@ -608,7 +608,7 @@ dependencies = [
608
608
 
609
609
  [[package]]
610
610
  name = "polyxml"
611
- version = "0.24.0"
611
+ version = "0.25.0"
612
612
  dependencies = [
613
613
  "criterion",
614
614
  "heck",
@@ -628,14 +628,14 @@ dependencies = [
628
628
 
629
629
  [[package]]
630
630
  name = "polyxml-c"
631
- version = "0.24.0"
631
+ version = "0.25.0"
632
632
  dependencies = [
633
633
  "polyxml",
634
634
  ]
635
635
 
636
636
  [[package]]
637
637
  name = "polyxml-cli"
638
- version = "0.24.0"
638
+ version = "0.25.0"
639
639
  dependencies = [
640
640
  "clap",
641
641
  "glob",
@@ -649,7 +649,7 @@ dependencies = [
649
649
 
650
650
  [[package]]
651
651
  name = "polyxml-js"
652
- version = "0.24.0"
652
+ version = "0.25.0"
653
653
  dependencies = [
654
654
  "napi",
655
655
  "napi-build",
@@ -659,7 +659,7 @@ dependencies = [
659
659
 
660
660
  [[package]]
661
661
  name = "polyxml-python"
662
- version = "0.24.0"
662
+ version = "0.25.0"
663
663
  dependencies = [
664
664
  "lexical-core",
665
665
  "polyxml",
@@ -669,7 +669,7 @@ dependencies = [
669
669
 
670
670
  [[package]]
671
671
  name = "polyxml-wasm"
672
- version = "0.24.0"
672
+ version = "0.25.0"
673
673
  dependencies = [
674
674
  "polyxml",
675
675
  "quick-xml",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/polyxml-core", "crates/polyxml-python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.24.0"
6
+ version = "0.25.0"
7
7
  edition = "2021"
8
8
  rust-version = "1.80"
9
9
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyxml
3
- Version: 0.24.0
3
+ Version: 0.25.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -62,9 +62,9 @@ pip install polyxml
62
62
 
63
63
  ## Features
64
64
 
65
- - **⚡ Blazing Fast**: 16x faster deserialization and 38x faster serialization than standard Python tools.
66
- - **🌊 Streaming `iterparse()`**: Parse multi-gigabyte XML files with O(1) constant memory (<5 MB RAM).
67
- - **📦 Zero-GIL Binary Serialization**: Native `dumps_binary()` and `loads_binary()` using MessagePack (`msgspec`), up to 12.4x faster than pickle.
65
+ - **⚡ Fast typed XML binding**: 10.0x faster deserialization and 23.5x faster serialization than `xsdata` on the [10,000-item catalog benchmark](../../benchmarks/python/results.md).
66
+ - **🌊 Streaming `iterparse()`**: Process records incrementally; measure memory on your own document structure.
67
+ - **📦 Binary Serialization**: Native `dumps_binary()` and `loads_binary()` using MessagePack (`msgspec`).
68
68
  - **🎯 Full Type Support**: Dataclasses and Pydantic v2 models with zero boilerplate.
69
69
 
70
70
  ---
@@ -29,9 +29,9 @@ pip install polyxml
29
29
 
30
30
  ## Features
31
31
 
32
- - **⚡ Blazing Fast**: 16x faster deserialization and 38x faster serialization than standard Python tools.
33
- - **🌊 Streaming `iterparse()`**: Parse multi-gigabyte XML files with O(1) constant memory (<5 MB RAM).
34
- - **📦 Zero-GIL Binary Serialization**: Native `dumps_binary()` and `loads_binary()` using MessagePack (`msgspec`), up to 12.4x faster than pickle.
32
+ - **⚡ Fast typed XML binding**: 10.0x faster deserialization and 23.5x faster serialization than `xsdata` on the [10,000-item catalog benchmark](../../benchmarks/python/results.md).
33
+ - **🌊 Streaming `iterparse()`**: Process records incrementally; measure memory on your own document structure.
34
+ - **📦 Binary Serialization**: Native `dumps_binary()` and `loads_binary()` using MessagePack (`msgspec`).
35
35
  - **🎯 Full Type Support**: Dataclasses and Pydantic v2 models with zero boilerplate.
36
36
 
37
37
  ---
@@ -5,7 +5,8 @@ use heck::{AsPascalCase, AsSnakeCase};
5
5
  use serde::{Deserialize, Serialize};
6
6
 
7
7
  use crate::codegen::{
8
- build_type_name_map, lookup_type_name, sanitize_keyword, set_type_name_map, LanguageContext,
8
+ build_type_name_map, lookup_type_name, normalize_symbol_name, sanitize_keyword,
9
+ set_type_name_map, LanguageContext,
9
10
  };
10
11
  use crate::ir::{
11
12
  EnumDef, PrimitiveType, QName, RestrictionFacets, SchemaIR, SimpleTypeDef, StructDef, TypeDef,
@@ -224,7 +225,11 @@ pub fn to_cpp_field_name(raw: &str) -> String {
224
225
 
225
226
  /// Converts an enumeration variant raw value into a safe C++ scoped enum identifier.
226
227
  pub fn to_cpp_enum_variant(raw: &str) -> String {
227
- let pascal = AsPascalCase(raw).to_string();
228
+ if raw.trim().is_empty() {
229
+ return "Unknown".to_string();
230
+ }
231
+ let normalized = normalize_symbol_name(raw);
232
+ let pascal = AsPascalCase(&normalized).to_string();
228
233
  let safe = if pascal.is_empty() {
229
234
  "Unknown".to_string()
230
235
  } else if pascal.starts_with(|c: char| c.is_ascii_digit()) {
@@ -255,6 +260,39 @@ impl CppCodegen {
255
260
  }
256
261
  }
257
262
 
263
+ fn unique_field_name(
264
+ &self,
265
+ struct_name: &str,
266
+ raw: &str,
267
+ seen: &mut HashSet<String>,
268
+ ) -> String {
269
+ let base = to_cpp_field_name(raw);
270
+ let mut name = base.clone();
271
+ if name.eq_ignore_ascii_case(struct_name) {
272
+ name.push_str("_value");
273
+ }
274
+ let stem = name.clone();
275
+ let mut index = 2;
276
+ while seen.contains(&name) || name.eq_ignore_ascii_case(struct_name) {
277
+ name = format!("{}_{}", stem, index);
278
+ index += 1;
279
+ }
280
+ seen.insert(name.clone());
281
+ name
282
+ }
283
+
284
+ fn unique_enum_variant(&self, raw: &str, seen: &mut HashSet<String>) -> String {
285
+ let base = to_cpp_enum_variant(raw);
286
+ let mut name = base.clone();
287
+ let mut index = 2;
288
+ while seen.contains(&name) {
289
+ name = format!("{}_{}", base, index);
290
+ index += 1;
291
+ }
292
+ seen.insert(name.clone());
293
+ name
294
+ }
295
+
258
296
  /// Generate complete header-only source (`.hpp`).
259
297
  pub fn generate_header(&self, ir: &SchemaIR) -> String {
260
298
  set_type_name_map(build_type_name_map(ir, to_cpp_type_name));
@@ -501,8 +539,15 @@ endif()
501
539
  writeln!(out, " static constexpr auto value = enumerate();").unwrap();
502
540
  } else {
503
541
  writeln!(out, " static constexpr auto value = enumerate(").unwrap();
504
- for (i, v) in enum_def.variants.iter().enumerate() {
505
- let var_name = to_cpp_enum_variant(&v.name);
542
+ let mut seen = HashSet::new();
543
+ let variant_names: Vec<String> = enum_def
544
+ .variants
545
+ .iter()
546
+ .map(|v| self.unique_enum_variant(&v.name, &mut seen))
547
+ .collect();
548
+ for (i, (v, var_name)) in
549
+ enum_def.variants.iter().zip(&variant_names).enumerate()
550
+ {
506
551
  let comma = if i + 1 < enum_def.variants.len() {
507
552
  ","
508
553
  } else {
@@ -527,6 +572,7 @@ endif()
527
572
  writeln!(out, " using T = {};", full_type).unwrap();
528
573
 
529
574
  let mut field_bindings = Vec::new();
575
+ let mut seen = HashSet::new();
530
576
 
531
577
  // Check for simple_content_base "value"
532
578
  if let Some(ref base_qname) = s.base_type {
@@ -538,12 +584,13 @@ endif()
538
584
  && (PrimitiveType::from_xsd_name(&base_qname.local).is_some()
539
585
  || matches!(ir.types.get(base_qname), Some(TypeDef::Simple(_))))
540
586
  {
587
+ seen.insert("value".to_string());
541
588
  field_bindings.push(r#""value", &T::value"#.to_string());
542
589
  }
543
590
  }
544
591
 
545
592
  for f in &s.fields {
546
- let field_name = to_cpp_field_name(&f.name);
593
+ let field_name = self.unique_field_name(&struct_name, &f.name, &mut seen);
547
594
  field_bindings.push(format!("{:?}, &T::{}", f.xml_name, field_name));
548
595
  }
549
596
 
@@ -671,11 +718,17 @@ concept XmlModel = requires(T a) {{
671
718
  let enum_name = type_ident(&enum_def.qname);
672
719
  writeln!(out, "enum class {} {{", enum_name).unwrap();
673
720
 
674
- for variant in &enum_def.variants {
721
+ let mut seen = HashSet::new();
722
+ let variant_names: Vec<String> = enum_def
723
+ .variants
724
+ .iter()
725
+ .map(|v| self.unique_enum_variant(&v.name, &mut seen))
726
+ .collect();
727
+
728
+ for (variant, var_name) in enum_def.variants.iter().zip(&variant_names) {
675
729
  if let Some(ref doc) = variant.documentation {
676
730
  writeln!(out, " /// {}", doc).unwrap();
677
731
  }
678
- let var_name = to_cpp_enum_variant(&variant.name);
679
732
  writeln!(out, " {},", var_name).unwrap();
680
733
  }
681
734
  writeln!(out, "}};\n").unwrap();
@@ -689,8 +742,7 @@ concept XmlModel = requires(T a) {{
689
742
  )
690
743
  .unwrap();
691
744
  writeln!(out, " switch (value) {{").unwrap();
692
- for variant in &enum_def.variants {
693
- let var_name = to_cpp_enum_variant(&variant.name);
745
+ for (variant, var_name) in enum_def.variants.iter().zip(&variant_names) {
694
746
  writeln!(
695
747
  out,
696
748
  " case {}::{}: return \"{}\";",
@@ -718,8 +770,7 @@ concept XmlModel = requires(T a) {{
718
770
  enum_name, func_name
719
771
  )
720
772
  .unwrap();
721
- for variant in &enum_def.variants {
722
- let var_name = to_cpp_enum_variant(&variant.name);
773
+ for (variant, var_name) in enum_def.variants.iter().zip(&variant_names) {
723
774
  writeln!(
724
775
  out,
725
776
  " if (s == \"{}\") return {}::{};",
@@ -816,7 +867,7 @@ concept XmlModel = requires(T a) {{
816
867
 
817
868
  writeln!(out, "struct {}{} {{", struct_name, base_clause).unwrap();
818
869
 
819
- if let Some(base_type_str) = simple_content_base {
870
+ if let Some(ref base_type_str) = simple_content_base {
820
871
  let has_value_field = s
821
872
  .fields
822
873
  .iter()
@@ -828,8 +879,6 @@ concept XmlModel = requires(T a) {{
828
879
  || base_type_str.starts_with("std::uint")
829
880
  {
830
881
  " = 0"
831
- } else if base_type_str == "bool" {
832
- " = false"
833
882
  } else {
834
883
  " = {}"
835
884
  };
@@ -837,12 +886,27 @@ concept XmlModel = requires(T a) {{
837
886
  }
838
887
  }
839
888
 
889
+ let mut seen = HashSet::new();
890
+ if simple_content_base.is_some() {
891
+ let has_value_field = s
892
+ .fields
893
+ .iter()
894
+ .any(|f| f.name == "value" || f.kind == crate::ir::FieldKind::Text);
895
+ if !has_value_field {
896
+ seen.insert("value".to_string());
897
+ }
898
+ }
899
+ let field_names: Vec<String> = s
900
+ .fields
901
+ .iter()
902
+ .map(|f| self.unique_field_name(&struct_name, &f.name, &mut seen))
903
+ .collect();
904
+
840
905
  // Fields
841
- for f in &s.fields {
906
+ for (f, field_name) in s.fields.iter().zip(&field_names) {
842
907
  if let Some(ref doc) = f.documentation {
843
908
  writeln!(out, " /// {}", doc).unwrap();
844
909
  }
845
- let field_name = to_cpp_field_name(&f.name);
846
910
  let (field_type, init_val) = self.resolve_field_type_and_init(f);
847
911
  writeln!(out, " {} {}{};", field_type, field_name, init_val).unwrap();
848
912
  }
@@ -857,7 +921,7 @@ concept XmlModel = requires(T a) {{
857
921
  }
858
922
 
859
923
  if self.options.validate_facets {
860
- self.emit_struct_validator(out, s, ir);
924
+ self.emit_struct_validator(out, s, ir, &field_names);
861
925
  }
862
926
 
863
927
  writeln!(out, "}};\n").unwrap();
@@ -929,13 +993,19 @@ concept XmlModel = requires(T a) {{
929
993
  }
930
994
  }
931
995
 
932
- fn emit_struct_validator(&self, out: &mut String, s: &StructDef, ir: &SchemaIR) {
996
+ fn emit_struct_validator(
997
+ &self,
998
+ out: &mut String,
999
+ s: &StructDef,
1000
+ ir: &SchemaIR,
1001
+ field_names: &[String],
1002
+ ) {
933
1003
  writeln!(out, "\n [[nodiscard]] bool validate() const noexcept {{").unwrap();
934
1004
 
935
1005
  let mut has_checks = false;
936
- for f in &s.fields {
1006
+ for (f, field_name) in s.fields.iter().zip(field_names) {
937
1007
  if let Some(simple) = super::patterned_simple(&f.type_ref, ir) {
938
- let field = to_cpp_field_name(&f.name);
1008
+ let field = field_name.clone();
939
1009
  let name = type_ident(&simple.qname);
940
1010
  let is_string = self
941
1011
  .context
@@ -963,7 +1033,6 @@ concept XmlModel = requires(T a) {{
963
1033
  }
964
1034
  }
965
1035
  if let Some(ref facets) = f.facets {
966
- let field_name = to_cpp_field_name(&f.name);
967
1036
  let is_opt = f.cardinality.is_optional() || f.nillable;
968
1037
 
969
1038
  if is_opt {
@@ -976,7 +1045,7 @@ concept XmlModel = requires(T a) {{
976
1045
  );
977
1046
  writeln!(out, " }}").unwrap();
978
1047
  } else {
979
- self.emit_facet_checks(out, facets, &field_name, " ");
1048
+ self.emit_facet_checks(out, facets, field_name, " ");
980
1049
  }
981
1050
  has_checks = true;
982
1051
  }
@@ -3,13 +3,15 @@
3
3
  //! Emits idiomatic C# 12 records with primary constructors, standard System.Xml.Serialization
4
4
  //! attributes, polymorphic xs:choice abstract records, and IValidatableObject facet boundary checks.
5
5
 
6
+ use std::collections::HashSet;
6
7
  use std::fmt::Write as FmtWrite;
7
8
 
8
9
  use heck::{AsLowerCamelCase, AsPascalCase};
9
10
  use serde::{Deserialize, Serialize};
10
11
 
11
12
  use crate::codegen::{
12
- build_type_name_map, lookup_type_name, sanitize_keyword, set_type_name_map, LanguageContext,
13
+ build_type_name_map, lookup_type_name, normalize_symbol_name, sanitize_keyword,
14
+ set_type_name_map, LanguageContext,
13
15
  };
14
16
  use crate::ir::{
15
17
  EnumDef, FieldDef, FieldKind, PrimitiveType, QName, RestrictionFacets, SchemaIR, SimpleTypeDef,
@@ -135,7 +137,8 @@ pub fn to_csharp_param_name(raw: &str) -> String {
135
137
 
136
138
  /// Sanitizes an identifier into a PascalCase C# enum variant.
137
139
  pub fn to_csharp_variant_name(raw: &str) -> String {
138
- let pascal = AsPascalCase(raw).to_string();
140
+ let normalized = normalize_symbol_name(raw);
141
+ let pascal = AsPascalCase(&normalized).to_string();
139
142
  let safe = if pascal.is_empty() {
140
143
  "Value".to_string()
141
144
  } else if pascal.starts_with(|c: char| c.is_ascii_digit()) {
@@ -381,6 +384,7 @@ impl CSharpCodegen {
381
384
  if let TypeDef::Union(u) = def {
382
385
  let name = type_ident(&u.qname);
383
386
  writeln!(out, "{}[JsonSerializable(typeof({}))]", indent, name).unwrap();
387
+ writeln!(out, "{}[JsonSerializable(typeof(List<{}>))]", indent, name).unwrap();
384
388
  }
385
389
  }
386
390
 
@@ -428,8 +432,14 @@ impl CSharpCodegen {
428
432
  writeln!(out, "{}public enum {}", indent, enum_name).unwrap();
429
433
  writeln!(out, "{}{{", indent).unwrap();
430
434
 
431
- for variant in &e.variants {
432
- let variant_name = to_csharp_variant_name(&variant.name);
435
+ let mut seen_variants = HashSet::new();
436
+ let variant_names: Vec<String> = e
437
+ .variants
438
+ .iter()
439
+ .map(|v| self.unique_variant_name(&v.name, &mut seen_variants))
440
+ .collect();
441
+
442
+ for (variant, variant_name) in e.variants.iter().zip(&variant_names) {
433
443
  if let Some(ref doc) = variant.documentation {
434
444
  self.emit_docstring(out, doc, &format!("{} ", indent));
435
445
  }
@@ -453,8 +463,7 @@ impl CSharpCodegen {
453
463
  )
454
464
  .unwrap();
455
465
  writeln!(out, "{} {{", indent).unwrap();
456
- for variant in &e.variants {
457
- let variant_name = to_csharp_variant_name(&variant.name);
466
+ for variant_name in &variant_names {
458
467
  writeln!(
459
468
  out,
460
469
  "{} {}.{} => true,",
@@ -473,8 +482,7 @@ impl CSharpCodegen {
473
482
  )
474
483
  .unwrap();
475
484
  writeln!(out, "{} {{", indent).unwrap();
476
- for variant in &e.variants {
477
- let variant_name = to_csharp_variant_name(&variant.name);
485
+ for (variant, variant_name) in e.variants.iter().zip(&variant_names) {
478
486
  writeln!(
479
487
  out,
480
488
  "{} {}.{} => \"{}\",",
@@ -717,6 +725,13 @@ impl CSharpCodegen {
717
725
  format!(" : {}", base_clause.join(", "))
718
726
  };
719
727
 
728
+ let mut seen_props = HashSet::new();
729
+ let prop_names: Vec<String> = s
730
+ .fields
731
+ .iter()
732
+ .map(|f| self.unique_property_name(&f.name, &mut seen_props, Some(&struct_name)))
733
+ .collect();
734
+
720
735
  if !self.options.use_records {
721
736
  writeln!(
722
737
  out,
@@ -725,8 +740,7 @@ impl CSharpCodegen {
725
740
  )
726
741
  .unwrap();
727
742
  writeln!(out, "{} public {}() {{ }}", indent, struct_name).unwrap();
728
- for f in &s.fields {
729
- let name = to_csharp_property_name(&f.name, Some(&struct_name));
743
+ for (f, name) in s.fields.iter().zip(&prop_names) {
730
744
  let ty = self.map_field_type(f, ir);
731
745
  let attrs = self
732
746
  .build_field_attributes(f, ir)
@@ -744,7 +758,7 @@ impl CSharpCodegen {
744
758
  .unwrap();
745
759
  }
746
760
  if self.options.emit_validation {
747
- self.emit_struct_validator(out, s, indent);
761
+ self.emit_struct_validator(out, s, &prop_names, indent);
748
762
  }
749
763
  writeln!(out, "{}}}\n", indent).unwrap();
750
764
  return;
@@ -764,8 +778,7 @@ impl CSharpCodegen {
764
778
 
765
779
  writeln!(out, "{}public {} {}(", indent, record_keyword, struct_name).unwrap();
766
780
 
767
- for (i, f) in s.fields.iter().enumerate() {
768
- let prop_name = to_csharp_property_name(&f.name, Some(&struct_name));
781
+ for (i, (f, prop_name)) in s.fields.iter().zip(&prop_names).enumerate() {
769
782
  let field_type = self.map_field_type(f, ir);
770
783
  let is_opt = f.cardinality.is_optional()
771
784
  || f.nillable
@@ -822,14 +835,49 @@ impl CSharpCodegen {
822
835
 
823
836
  // IValidatableObject implementation
824
837
  if self.options.emit_validation {
825
- self.emit_struct_validator(out, s, indent);
838
+ self.emit_struct_validator(out, s, &prop_names, indent);
826
839
  }
827
840
 
828
841
  writeln!(out, "{}}}\n", indent).unwrap();
829
842
  }
830
843
 
831
- fn emit_struct_validator(&self, out: &mut String, s: &StructDef, indent: &str) {
832
- let struct_name = type_ident(&s.qname);
844
+ fn unique_property_name(
845
+ &self,
846
+ name: &str,
847
+ seen: &mut HashSet<String>,
848
+ enclosing: Option<&str>,
849
+ ) -> String {
850
+ let base = to_csharp_property_name(name, enclosing);
851
+ let mut candidate = base.clone();
852
+ let mut counter = 1;
853
+ while seen.contains(&candidate) {
854
+ counter += 1;
855
+ candidate = format!("{}{}", base, counter);
856
+ }
857
+ seen.insert(candidate.clone());
858
+ candidate
859
+ }
860
+
861
+ fn unique_variant_name(&self, name: &str, seen: &mut HashSet<String>) -> String {
862
+ let base = to_csharp_variant_name(name);
863
+ let mut candidate = base.clone();
864
+ let mut counter = 1;
865
+ while seen.contains(&candidate) {
866
+ counter += 1;
867
+ candidate = format!("{}{}", base, counter);
868
+ }
869
+ seen.insert(candidate.clone());
870
+ candidate
871
+ }
872
+
873
+ fn emit_struct_validator(
874
+ &self,
875
+ out: &mut String,
876
+ s: &StructDef,
877
+ prop_names: &[String],
878
+ indent: &str,
879
+ ) {
880
+ let _struct_name = type_ident(&s.qname);
833
881
  let new_kw = if !self.options.use_records {
834
882
  if s.base_type.is_some() {
835
883
  "override "
@@ -853,8 +901,7 @@ impl CSharpCodegen {
853
901
  writeln!(out, "{} foreach (var result in base.Validate(validationContext)) yield return result;", indent).unwrap();
854
902
  }
855
903
  let mut has_checks = false;
856
- for f in &s.fields {
857
- let prop_name = to_csharp_property_name(&f.name, Some(&struct_name));
904
+ for (f, prop_name) in s.fields.iter().zip(prop_names) {
858
905
  let is_opt = f.cardinality.is_optional()
859
906
  || f.nillable
860
907
  || (f.cardinality.is_list() && f.cardinality.min_occurs == 0);
@@ -866,12 +913,12 @@ impl CSharpCodegen {
866
913
  self.emit_facet_checks(
867
914
  out,
868
915
  facets,
869
- &prop_name,
916
+ prop_name,
870
917
  &format!("{} ", indent),
871
918
  );
872
919
  writeln!(out, "{} }}", indent).unwrap();
873
920
  } else {
874
- self.emit_facet_checks(out, facets, &prop_name, &format!("{} ", indent));
921
+ self.emit_facet_checks(out, facets, prop_name, &format!("{} ", indent));
875
922
  }
876
923
  has_checks = true;
877
924
  }
@@ -982,7 +1029,15 @@ impl CSharpCodegen {
982
1029
 
983
1030
  if self.options.emit_xml_attributes {
984
1031
  // If the field is a choice (UnionDef), emit XmlElement("branchXml", typeof(BranchType))
985
- if let TypeRef::Named(ref qname) = f.type_ref {
1032
+ let named_qname = match &f.type_ref {
1033
+ TypeRef::Named(q) => Some(q),
1034
+ TypeRef::List(inner) | TypeRef::Boxed(inner) => match &**inner {
1035
+ TypeRef::Named(q) => Some(q),
1036
+ _ => None,
1037
+ },
1038
+ _ => None,
1039
+ };
1040
+ if let Some(qname) = named_qname {
986
1041
  if let Some(TypeDef::Union(u)) = ir.types.get(qname) {
987
1042
  let choice_name = type_ident(&u.qname);
988
1043
  for branch in &u.branches {
@@ -1008,7 +1063,13 @@ impl CSharpCodegen {
1008
1063
  if self.options.emit_json_attributes {
1009
1064
  let json_name = match f.kind {
1010
1065
  FieldKind::Text => "value",
1011
- _ => &f.xml_name,
1066
+ _ => {
1067
+ if f.xml_name.is_empty() {
1068
+ &f.name
1069
+ } else {
1070
+ &f.xml_name
1071
+ }
1072
+ }
1012
1073
  };
1013
1074
  if f.kind != FieldKind::Any && f.kind != FieldKind::AnyAttribute {
1014
1075
  parts.push(format!("JsonPropertyName(\"{}\")", json_name));