polyxml 0.33.6__tar.gz → 0.33.7__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 (76) hide show
  1. {polyxml-0.33.6 → polyxml-0.33.7}/Cargo.lock +6 -6
  2. {polyxml-0.33.6 → polyxml-0.33.7}/Cargo.toml +1 -1
  3. {polyxml-0.33.6 → polyxml-0.33.7}/PKG-INFO +1 -1
  4. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/codegen/csharp/mod.rs +84 -4
  5. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/codegen/go/mod.rs +1 -1
  6. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/codegen/python/mod.rs +13 -2
  7. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/parser.rs +49 -9
  8. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/schema.rs +3 -0
  9. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_csharp_codegen.rs +46 -0
  10. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_python_codegen.rs +1 -1
  11. polyxml-0.33.7/crates/polyxml-core/tests/test_qualified_attributes.rs +80 -0
  12. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_security.rs +27 -0
  13. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-python/src/lib.rs +9 -0
  14. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-python/tests/test_generated_models.py +67 -0
  15. {polyxml-0.33.6 → polyxml-0.33.7}/pyproject.toml +1 -1
  16. {polyxml-0.33.6 → polyxml-0.33.7}/README.md +0 -0
  17. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/Cargo.toml +0 -0
  18. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/README.md +0 -0
  19. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
  20. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/benches/support/tag_dispatch_fixtures.rs +0 -0
  21. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/benches/tag_dispatch.rs +0 -0
  22. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/codegen/cpp/mod.rs +0 -0
  23. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/codegen/java/codec.rs +0 -0
  24. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/codegen/java/mod.rs +0 -0
  25. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/codegen/java/models.rs +0 -0
  26. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/codegen/mod.rs +0 -0
  27. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/codegen/python/aot.rs +0 -0
  28. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/codegen/rust/mod.rs +0 -0
  29. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/codegen/typescript/mod.rs +0 -0
  30. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/converters.rs +0 -0
  31. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/error.rs +0 -0
  32. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/ir/chunker.rs +0 -0
  33. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/ir/mod.rs +0 -0
  34. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/ir/tarjan.rs +0 -0
  35. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/json.rs +0 -0
  36. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/lib.rs +0 -0
  37. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/schema_parser/mod.rs +0 -0
  38. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/serializer.rs +0 -0
  39. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/transcoder.rs +0 -0
  40. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/src/value.rs +0 -0
  41. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/fixtures/lexical_union.xsd +0 -0
  42. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/fixtures/mixed_content.xsd +0 -0
  43. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/fixtures/ts_mixed_content_inheritance.xsd +0 -0
  44. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_adversarial_identifiers.rs +0 -0
  45. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_any_attribute.rs +0 -0
  46. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_any_element.rs +0 -0
  47. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_conformance.rs +0 -0
  48. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_core.rs +0 -0
  49. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_cpp_codegen.rs +0 -0
  50. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_go_codegen.rs +0 -0
  51. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_java_codegen.rs +0 -0
  52. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_json.rs +0 -0
  53. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_lexical_union_codegen.rs +0 -0
  54. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_mixed_content.rs +0 -0
  55. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_pattern_codegen.rs +0 -0
  56. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_rust_codecs.rs +0 -0
  57. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_rust_codegen.rs +0 -0
  58. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_schema_audit.rs +0 -0
  59. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_schema_ir.rs +0 -0
  60. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_transcoder.rs +0 -0
  61. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_ts_codegen.rs +0 -0
  62. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_unbounded_choice.rs +0 -0
  63. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-core/tests/test_xsi_type.rs +0 -0
  64. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-python/Cargo.toml +0 -0
  65. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-python/README.md +0 -0
  66. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-python/benches/aot_vs_dataclass.py +0 -0
  67. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-python/tests/test_conformance.py +0 -0
  68. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-python/tests/test_json.py +0 -0
  69. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-python/tests/test_mixed_content.py +0 -0
  70. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-python/tests/test_polyxml.py +0 -0
  71. {polyxml-0.33.6 → polyxml-0.33.7}/crates/polyxml-python/tests/test_xsi_type.py +0 -0
  72. {polyxml-0.33.6 → polyxml-0.33.7}/python/polyxml/AGENT_GUIDE.md +0 -0
  73. {polyxml-0.33.6 → polyxml-0.33.7}/python/polyxml/__init__.py +0 -0
  74. {polyxml-0.33.6 → polyxml-0.33.7}/python/polyxml/compat/__init__.py +0 -0
  75. {polyxml-0.33.6 → polyxml-0.33.7}/python/polyxml/compat/xsdata.py +0 -0
  76. {polyxml-0.33.6 → polyxml-0.33.7}/python/polyxml/py.typed +0 -0
@@ -608,7 +608,7 @@ dependencies = [
608
608
 
609
609
  [[package]]
610
610
  name = "polyxml"
611
- version = "0.33.6"
611
+ version = "0.33.7"
612
612
  dependencies = [
613
613
  "criterion",
614
614
  "heck",
@@ -629,14 +629,14 @@ dependencies = [
629
629
 
630
630
  [[package]]
631
631
  name = "polyxml-c"
632
- version = "0.33.6"
632
+ version = "0.33.7"
633
633
  dependencies = [
634
634
  "polyxml",
635
635
  ]
636
636
 
637
637
  [[package]]
638
638
  name = "polyxml-cli"
639
- version = "0.33.6"
639
+ version = "0.33.7"
640
640
  dependencies = [
641
641
  "clap",
642
642
  "glob",
@@ -650,7 +650,7 @@ dependencies = [
650
650
 
651
651
  [[package]]
652
652
  name = "polyxml-js"
653
- version = "0.33.6"
653
+ version = "0.33.7"
654
654
  dependencies = [
655
655
  "napi",
656
656
  "napi-build",
@@ -660,7 +660,7 @@ dependencies = [
660
660
 
661
661
  [[package]]
662
662
  name = "polyxml-python"
663
- version = "0.33.6"
663
+ version = "0.33.7"
664
664
  dependencies = [
665
665
  "lexical-core",
666
666
  "polyxml",
@@ -670,7 +670,7 @@ dependencies = [
670
670
 
671
671
  [[package]]
672
672
  name = "polyxml-wasm"
673
- version = "0.33.6"
673
+ version = "0.33.7"
674
674
  dependencies = [
675
675
  "polyxml",
676
676
  "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.33.6"
6
+ version = "0.33.7"
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.33.6
3
+ Version: 0.33.7
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -869,6 +869,55 @@ impl CSharpCodegen {
869
869
  let _ = writeln!(out, "{} }};", indent);
870
870
  }
871
871
 
872
+ fn emit_default_scalar_proxy(
873
+ &self,
874
+ out: &mut String,
875
+ field: &FieldDef,
876
+ name: &str,
877
+ ir: &SchemaIR,
878
+ indent: &str,
879
+ ) -> bool {
880
+ let Some(default) = field.default_value.as_ref() else {
881
+ return false;
882
+ };
883
+ if !self.options.emit_xml_attributes
884
+ || field.kind != FieldKind::Element
885
+ || field.cardinality.is_list()
886
+ || matches!(field.type_ref, TypeRef::Named(ref q) if matches!(ir.types.get(q), Some(TypeDef::Struct(_) | TypeDef::Union(_))))
887
+ {
888
+ return false;
889
+ }
890
+ let ty = self.map_field_type(field, ir);
891
+ let base = ty.trim_end_matches('?');
892
+ let namespace = field
893
+ .namespace
894
+ .as_ref()
895
+ .map(|ns| format!(", Namespace = {ns:?}"))
896
+ .unwrap_or_default();
897
+ writeln!(
898
+ out,
899
+ "{indent} [XmlIgnore] public {ty} {name} {{ get; set; }} = default!;"
900
+ )
901
+ .unwrap();
902
+ writeln!(
903
+ out,
904
+ "{indent} [XmlElement({:?}{namespace})] public string? {name}Xml {{",
905
+ field.xml_name
906
+ )
907
+ .unwrap();
908
+ writeln!(out, "{indent} get {{ if ({name} is null) return null; var writer = new System.IO.StringWriter(System.Globalization.CultureInfo.InvariantCulture); new XmlSerializer(typeof({base}), new XmlRootAttribute(\"value\")).Serialize(writer, {name}); return System.Xml.Linq.XDocument.Parse(writer.ToString()).Root!.Value; }}").unwrap();
909
+ writeln!(out, "{indent} set {{ if (value is null) {{ {name} = default!; return; }} var text = value.Length == 0 ? {default:?} : value; {name} = ({base})new XmlSerializer(typeof({base}), new XmlRootAttribute(\"value\")).Deserialize(new System.IO.StringReader(\"<value>\" + System.Security.SecurityElement.Escape(text) + \"</value>\"))!; }}
910
+ {indent} }}").unwrap();
911
+ if field.cardinality.is_optional() || field.nillable {
912
+ writeln!(
913
+ out,
914
+ "{indent} public bool ShouldSerialize{name}Xml() => {name} is not null;"
915
+ )
916
+ .unwrap();
917
+ }
918
+ true
919
+ }
920
+
872
921
  fn emit_mixed_struct(
873
922
  &self,
874
923
  out: &mut String,
@@ -1030,7 +1079,7 @@ impl CSharpCodegen {
1030
1079
  }
1031
1080
 
1032
1081
  let has_lexical_union = self.options.emit_xml_attributes && s.fields.iter().any(|f| {
1033
- matches!(&f.type_ref, TypeRef::Named(q) if matches!(ir.types.get(q), Some(TypeDef::Union(u)) if u.is_lexical()))
1082
+ f.default_value.is_some() || matches!(&f.type_ref, TypeRef::Named(q) if matches!(ir.types.get(q), Some(TypeDef::Union(u)) if u.is_lexical()))
1034
1083
  });
1035
1084
 
1036
1085
  if self.options.use_records && has_lexical_union {
@@ -1041,6 +1090,9 @@ impl CSharpCodegen {
1041
1090
  )
1042
1091
  .unwrap();
1043
1092
  for (f, name) in s.fields.iter().zip(&prop_names) {
1093
+ if self.emit_default_scalar_proxy(out, f, name, ir, indent) {
1094
+ continue;
1095
+ }
1044
1096
  let ty = self.map_field_type(f, ir);
1045
1097
  if let TypeRef::Named(q) = &f.type_ref {
1046
1098
  if matches!(ir.types.get(q), Some(TypeDef::Union(u)) if u.is_lexical()) {
@@ -1059,7 +1111,16 @@ impl CSharpCodegen {
1059
1111
  } else {
1060
1112
  "XmlElement"
1061
1113
  };
1062
- let _ = writeln!(out, "{} [{}({:?})]", indent, xml_kind, f.xml_name);
1114
+ let namespace = f
1115
+ .namespace
1116
+ .as_ref()
1117
+ .map(|ns| format!(", Namespace = {ns:?}"))
1118
+ .unwrap_or_default();
1119
+ let _ = writeln!(
1120
+ out,
1121
+ "{} [{}({:?}{namespace})]",
1122
+ indent, xml_kind, f.xml_name
1123
+ );
1063
1124
  if self.options.emit_json_attributes {
1064
1125
  let _ = writeln!(out, "{} [JsonIgnore]", indent);
1065
1126
  }
@@ -1093,6 +1154,9 @@ impl CSharpCodegen {
1093
1154
  .unwrap();
1094
1155
  writeln!(out, "{} public {}() {{ }}", indent, struct_name).unwrap();
1095
1156
  for (f, name) in s.fields.iter().zip(&prop_names) {
1157
+ if self.emit_default_scalar_proxy(out, f, name, ir, indent) {
1158
+ continue;
1159
+ }
1096
1160
  let ty = self.map_field_type(f, ir);
1097
1161
  if let TypeRef::Named(q) = &f.type_ref {
1098
1162
  if self.options.emit_xml_attributes
@@ -1113,7 +1177,16 @@ impl CSharpCodegen {
1113
1177
  } else {
1114
1178
  "XmlElement"
1115
1179
  };
1116
- let _ = writeln!(out, "{} [{}({:?})]", indent, xml_kind, f.xml_name);
1180
+ let namespace = f
1181
+ .namespace
1182
+ .as_ref()
1183
+ .map(|ns| format!(", Namespace = {ns:?}"))
1184
+ .unwrap_or_default();
1185
+ let _ = writeln!(
1186
+ out,
1187
+ "{} [{}({:?}{namespace})]",
1188
+ indent, xml_kind, f.xml_name
1189
+ );
1117
1190
  if self.options.emit_json_attributes {
1118
1191
  let _ = writeln!(out, "{} [JsonIgnore]", indent);
1119
1192
  }
@@ -1459,7 +1532,14 @@ impl CSharpCodegen {
1459
1532
  }
1460
1533
  if parts.is_empty() {
1461
1534
  match f.kind {
1462
- FieldKind::Attribute => parts.push(format!("XmlAttribute(\"{}\")", f.xml_name)),
1535
+ FieldKind::Attribute => {
1536
+ let namespace = f
1537
+ .namespace
1538
+ .as_ref()
1539
+ .map(|ns| format!(", Namespace = {ns:?}"))
1540
+ .unwrap_or_default();
1541
+ parts.push(format!("XmlAttribute({:?}{namespace})", f.xml_name));
1542
+ }
1463
1543
  FieldKind::Text => parts.push("XmlText".to_string()),
1464
1544
  FieldKind::Any => parts.push("XmlAnyElement".to_string()),
1465
1545
  FieldKind::AnyAttribute => parts.push("XmlAnyAttribute".to_string()),
@@ -1230,7 +1230,7 @@ impl GoCodegen {
1230
1230
  let xml_val = match f.kind {
1231
1231
  FieldKind::Attribute => {
1232
1232
  let attr_name = match &f.namespace {
1233
- Some(ns) if !ns.is_empty() && is_namespaced_ref(f, s, ir) => {
1233
+ Some(ns) if !ns.is_empty() => {
1234
1234
  format!("{} {}", ns, f.xml_name)
1235
1235
  }
1236
1236
  _ => f.xml_name.clone(),
@@ -840,7 +840,11 @@ impl PythonCodegen {
840
840
  format!("field(default_factory=list, metadata={})", meta_dict),
841
841
  )
842
842
  } else if field.cardinality.is_optional() || field.nillable {
843
- if let Some(ref def) = field.default_value {
843
+ if let Some(def) = field
844
+ .default_value
845
+ .as_ref()
846
+ .filter(|_| field.kind == FieldKind::Attribute)
847
+ {
844
848
  let py_val = self.format_default_value(def, &field.type_ref, ir);
845
849
  (
846
850
  format!("{} | None", base_type),
@@ -928,7 +932,11 @@ impl PythonCodegen {
928
932
  format!("Field(default_factory=list, {})", extra_clause),
929
933
  )
930
934
  } else if field.cardinality.is_optional() || field.nillable {
931
- if let Some(ref def) = field.default_value {
935
+ if let Some(def) = field
936
+ .default_value
937
+ .as_ref()
938
+ .filter(|_| field.kind == FieldKind::Attribute)
939
+ {
932
940
  let py_val = self.format_default_value(def, &field.type_ref, ir);
933
941
  (
934
942
  format!("{} | None", base_type),
@@ -985,6 +993,9 @@ impl PythonCodegen {
985
993
  parts.push(format!("\"namespace\": \"{}\"", ns));
986
994
  }
987
995
 
996
+ if let Some(ref default) = field.default_value {
997
+ parts.push(format!("\"default\": {default:?}"));
998
+ }
988
999
  if field.nillable {
989
1000
  parts.push("\"nillable\": True".to_string());
990
1001
  }
@@ -7,7 +7,7 @@ use smallvec::{smallvec, SmallVec};
7
7
 
8
8
  use crate::converters::ValueConverter;
9
9
  use crate::error::{PolyXmlError, Result};
10
- use crate::schema::{ModelSchema, ScalarType, ValueType};
10
+ use crate::schema::{FieldKind, ModelSchema, ScalarType, ValueType};
11
11
  use crate::value::PolyValue;
12
12
 
13
13
  type NamespaceScope = Arc<HashMap<String, String>>;
@@ -71,6 +71,19 @@ impl StackFrame {
71
71
  }
72
72
 
73
73
  fn finish(mut self) -> Result<PolyValue> {
74
+ for (index, field) in self.schema.fields.iter().enumerate() {
75
+ if field.kind == FieldKind::Attribute && self.values[index].is_none() {
76
+ if let (Some(default), ValueType::Scalar(scalar)) =
77
+ (&field.default_value, &field.val_type)
78
+ {
79
+ self.values[index] = Some(ValueConverter::parse_scalar(
80
+ scalar,
81
+ default.as_bytes(),
82
+ &field.name,
83
+ )?);
84
+ }
85
+ }
86
+ }
74
87
  if !self.list_values.is_empty() {
75
88
  for (idx, list_opt) in self.list_values.into_iter().enumerate() {
76
89
  if let Some(list) = list_opt {
@@ -107,6 +120,22 @@ impl StackFrame {
107
120
  }
108
121
  }
109
122
 
123
+ fn parse_field_scalar(
124
+ scalar: &ScalarType,
125
+ text: &[u8],
126
+ field: &crate::schema::FieldSchema,
127
+ ) -> Result<PolyValue> {
128
+ let text = if text.is_empty() && field.kind != FieldKind::Attribute {
129
+ field
130
+ .default_value
131
+ .as_ref()
132
+ .map_or(text, |value| value.as_bytes())
133
+ } else {
134
+ text
135
+ };
136
+ ValueConverter::parse_scalar(scalar, text, &field.name)
137
+ }
138
+
110
139
  pub struct XmlDeserializer;
111
140
 
112
141
  fn is_nil_element(e: &BytesStart) -> bool {
@@ -245,7 +274,10 @@ fn append_general_ref(
245
274
  } else if let Some(s) = quick_xml::escape::resolve_xml_entity(e.as_ref()) {
246
275
  s.as_bytes()
247
276
  } else {
248
- e.as_ref().as_bytes()
277
+ return Err(PolyXmlError::SchemaError(format!(
278
+ "Unsupported general entity reference: &{};",
279
+ e.as_ref()
280
+ )));
249
281
  };
250
282
 
251
283
  if active_scalar {
@@ -598,20 +630,20 @@ impl XmlDeserializer {
598
630
  {
599
631
  let field = &current_schema.fields[field_idx];
600
632
  match &field.val_type {
601
- ValueType::Scalar(_) => {
633
+ ValueType::Scalar(scalar) => {
602
634
  let val = if is_nil {
603
635
  PolyValue::Null
604
636
  } else {
605
- PolyValue::String(String::new())
637
+ parse_field_scalar(scalar, b"", field)?
606
638
  };
607
639
  stack.last_mut().unwrap().values[field_idx] = Some(val);
608
640
  }
609
641
  ValueType::List(inner) => match inner.as_ref() {
610
- ValueType::Scalar(_) => {
642
+ ValueType::Scalar(scalar) => {
611
643
  let val = if is_nil {
612
644
  PolyValue::Null
613
645
  } else {
614
- PolyValue::String(String::new())
646
+ parse_field_scalar(scalar, b"", field)?
615
647
  };
616
648
  stack.last_mut().unwrap().push_list_item(field_idx, val);
617
649
  }
@@ -779,6 +811,15 @@ impl XmlDeserializer {
779
811
  }
780
812
  }
781
813
  Ok(Event::GeneralRef(ref e)) => {
814
+ // Entity validity is independent of whether a field is retained.
815
+ if !e.is_char_ref()
816
+ && quick_xml::escape::resolve_xml_entity(e.as_ref()).is_none()
817
+ {
818
+ return Err(PolyXmlError::SchemaError(format!(
819
+ "Unsupported general entity reference: &{};",
820
+ e.as_ref()
821
+ )));
822
+ }
782
823
  if unknown_depth > 0 {
783
824
  continue;
784
825
  }
@@ -848,9 +889,8 @@ impl XmlDeserializer {
848
889
  } else if let Some((field_idx, ref scalar_type, is_list)) =
849
890
  active_scalar_field.take()
850
891
  {
851
- let field_name = &stack.last().unwrap().schema.fields[field_idx].name;
852
- let parsed_val =
853
- ValueConverter::parse_scalar(scalar_type, &text_buf, field_name)?;
892
+ let field = &stack.last().unwrap().schema.fields[field_idx];
893
+ let parsed_val = parse_field_scalar(scalar_type, &text_buf, field)?;
854
894
  let frame = stack.last_mut().unwrap();
855
895
  if is_list {
856
896
  frame.push_list_item(field_idx, parsed_val);
@@ -65,6 +65,7 @@ pub struct FieldSchema {
65
65
  pub kind: FieldKind,
66
66
  pub val_type: ValueType,
67
67
  pub required: bool,
68
+ pub default_value: Option<String>,
68
69
  }
69
70
 
70
71
  impl FieldSchema {
@@ -81,6 +82,7 @@ impl FieldSchema {
81
82
  kind,
82
83
  val_type,
83
84
  required: false,
85
+ default_value: None,
84
86
  }
85
87
  }
86
88
 
@@ -351,6 +353,7 @@ impl ModelSchema {
351
353
  }
352
354
 
353
355
  let mut field_schema = FieldSchema::new(&f.name, f.xml_name.as_bytes(), kind, val_type);
356
+ field_schema.default_value = f.default_value.clone();
354
357
  if let Some(ref ns) = f.namespace {
355
358
  field_schema = field_schema.namespace(ns);
356
359
  }
@@ -1232,3 +1232,49 @@ fn test_csharp_sequence_nested_inside_choice_codegen() {
1232
1232
  assert!(code.contains("string? Alternative = null"));
1233
1233
  assert!(code.contains("public record RootTypeSequence"));
1234
1234
  }
1235
+
1236
+ #[test]
1237
+ fn element_defaults_preserve_absence_and_empty_presence() {
1238
+ let ir = polyxml::schema_parser::XsdParser::new()
1239
+ .parse_str(include_str!(
1240
+ "../../../research/fixtures/element_defaults.xsd"
1241
+ ))
1242
+ .unwrap();
1243
+ for use_records in [true, false] {
1244
+ let temp = tempdir().unwrap();
1245
+ fs::write(temp.path().join("App.csproj"), r#"<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup></Project>"#).unwrap();
1246
+ fs::write(
1247
+ temp.path().join("Models.cs"),
1248
+ CSharpCodegen::new(CSharpOptions {
1249
+ namespace: "Models".into(),
1250
+ use_records,
1251
+ ..Default::default()
1252
+ })
1253
+ .generate_module(&ir),
1254
+ )
1255
+ .unwrap();
1256
+ fs::write(temp.path().join("Program.cs"), r#"using Models;using System.Xml.Serialization;
1257
+ var serializer=new XmlSerializer(typeof(Root));
1258
+ foreach(var document in new[]{"<Root/>","<Root><Flag/><Count></Count><Label/></Root>"}){
1259
+ var root=(Root)serializer.Deserialize(new StringReader(document))!;
1260
+ var present=document.Contains("Flag");
1261
+ if(present&&(root.Flag!=false||root.Count!=42||root.Label!="fallback"))throw new Exception("defaults missing");
1262
+ if(!present&&(root.Flag!=null||root.Count!=null||root.Label!=null))throw new Exception("absence lost");
1263
+ var writer=new StringWriter();serializer.Serialize(writer,root);
1264
+ if(!present&&writer.ToString().Contains("<Flag"))throw new Exception("absent value serialized");
1265
+ if(present&&!writer.ToString().Contains("<Flag>false</Flag>"))throw new Exception(writer.ToString());
1266
+ }"#).unwrap();
1267
+ let _lock = DOTNET_LOCK.lock().unwrap();
1268
+ let result = dotnet_command()
1269
+ .arg("run")
1270
+ .current_dir(temp.path())
1271
+ .output()
1272
+ .unwrap();
1273
+ assert!(
1274
+ result.status.success(),
1275
+ "{}{}",
1276
+ String::from_utf8_lossy(&result.stdout),
1277
+ String::from_utf8_lossy(&result.stderr)
1278
+ );
1279
+ }
1280
+ }
@@ -162,7 +162,7 @@ fn test_python_dataclass_codegen() {
162
162
  // Check fields
163
163
  assert!(code.contains("id: int = field(metadata={\"type\": \"Attribute\", \"name\": \"id\", \"json_name\": \"id\"})"));
164
164
  assert!(code.contains("type_: str | None = field(default=None, metadata={\"type\": \"Element\", \"name\": \"type\", \"json_name\": \"type\", \"namespace\": \"https://example.com/shop\", \"nillable\": True})"));
165
- assert!(code.contains("status: OrderStatus = field(default=\"pending\", metadata={\"type\": \"Element\", \"name\": \"status\", \"json_name\": \"status\"})"));
165
+ assert!(code.contains("status: OrderStatus = field(default=\"pending\", metadata={\"type\": \"Element\", \"name\": \"status\", \"json_name\": \"status\", \"default\": \"pending\"})"));
166
166
  assert!(code.contains("tags: list[str] = field(default_factory=list, metadata={\"type\": \"Element\", \"name\": \"tag\", \"json_name\": \"tag\"})"));
167
167
  }
168
168
 
@@ -0,0 +1,80 @@
1
+ use polyxml::codegen::{
2
+ csharp::{CSharpCodegen, CSharpOptions},
3
+ go::{GoCodegen, GoOptions},
4
+ };
5
+ use polyxml::schema_parser::XsdParser;
6
+ use std::{fs, process::Command};
7
+ use tempfile::tempdir;
8
+
9
+ #[test]
10
+ fn global_attribute_references_round_trip_by_namespace() {
11
+ let mut parser = XsdParser::new();
12
+ let ir = parser
13
+ .parse_file(concat!(
14
+ env!("CARGO_MANIFEST_DIR"),
15
+ "/../../research/fixtures/wave6/xlink_shared_attribute/root.xsd"
16
+ ))
17
+ .unwrap();
18
+ let temp = tempdir().unwrap();
19
+ fs::write(temp.path().join("go.mod"), "module attrs\n\ngo 1.22\n").unwrap();
20
+ fs::write(
21
+ temp.path().join("models.go"),
22
+ GoCodegen::new(GoOptions::default()).generate_module(&ir),
23
+ )
24
+ .unwrap();
25
+ fs::write(temp.path().join("models_test.go"), r#"package models
26
+ import("encoding/xml";"testing";"strings";"io")
27
+ func TestAttributes(t *testing.T) {
28
+ for _, document := range []string{`<Links xmlns:xl="http://www.w3.org/1999/xlink"><Resource xl:type="resource"/><Arc xl:type="arc"/></Links>`, `<Links><Resource xmlns:other="http://www.w3.org/1999/xlink" other:type="resource"/><Arc xmlns:a="http://www.w3.org/1999/xlink" a:type="arc"/></Links>`} {
29
+ var root Links; if err:=xml.Unmarshal([]byte(document),&root);err!=nil{t.Fatal(err)}
30
+ data,err:=xml.Marshal(root);if err!=nil{t.Fatal(err)}
31
+ decoder:=xml.NewDecoder(strings.NewReader(string(data)));count:=0
32
+ for {token,err:=decoder.Token();if err==io.EOF{break};if err!=nil{t.Fatal(err)};if start,ok:=token.(xml.StartElement);ok{for _,attr:=range start.Attr{if attr.Name.Space=="http://www.w3.org/1999/xlink"&&attr.Name.Local=="type"{count++;if attr.Value!="resource"&&attr.Value!="arc"{t.Fatal(attr)}}}}}
33
+ if count!=2{t.Fatalf("attributes lost: %s",data)}
34
+ }
35
+ }"#).unwrap();
36
+ let result = Command::new("go")
37
+ .args(["test", "./..."])
38
+ .current_dir(temp.path())
39
+ .output()
40
+ .unwrap();
41
+ assert!(
42
+ result.status.success(),
43
+ "{}{}",
44
+ String::from_utf8_lossy(&result.stdout),
45
+ String::from_utf8_lossy(&result.stderr)
46
+ );
47
+ for use_records in [true, false] {
48
+ let temp = tempdir().unwrap();
49
+ fs::write(temp.path().join("App.csproj"), r#"<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup></Project>"#).unwrap();
50
+ fs::write(
51
+ temp.path().join("Models.cs"),
52
+ CSharpCodegen::new(CSharpOptions {
53
+ namespace: "Models".into(),
54
+ use_records,
55
+ ..Default::default()
56
+ })
57
+ .generate_module(&ir),
58
+ )
59
+ .unwrap();
60
+ fs::write(temp.path().join("Program.cs"), r#"using System.Xml.Serialization;using System.Xml.Linq;using Models;
61
+ var serializer=new XmlSerializer(typeof(Links));
62
+ foreach(var document in new[]{"<Links xmlns:xl='http://www.w3.org/1999/xlink'><Resource xl:type='resource'/><Arc xl:type='arc'/></Links>","<Links><Resource xmlns:b='http://www.w3.org/1999/xlink' b:type='resource'/><Arc xmlns:c='http://www.w3.org/1999/xlink' c:type='arc'/></Links>"}){
63
+ var root=(Links)serializer.Deserialize(new StringReader(document))!;
64
+ var writer=new StringWriter();serializer.Serialize(writer,root);
65
+ var xml=XDocument.Parse(writer.ToString());XNamespace ns="http://www.w3.org/1999/xlink";
66
+ if((string?)xml.Root!.Element("Resource")!.Attribute(ns+"type")!="resource"||(string?)xml.Root.Element("Arc")!.Attribute(ns+"type")!="arc")throw new Exception(writer.ToString());
67
+ }"#).unwrap();
68
+ let result = Command::new("dotnet")
69
+ .arg("run")
70
+ .current_dir(temp.path())
71
+ .output()
72
+ .unwrap();
73
+ assert!(
74
+ result.status.success(),
75
+ "{}{}",
76
+ String::from_utf8_lossy(&result.stdout),
77
+ String::from_utf8_lossy(&result.stderr)
78
+ );
79
+ }
80
+ }
@@ -162,3 +162,30 @@ fn test_malformed_unclosed_cdata_rejection() {
162
162
  let bad_xml = br#"<Doc><body><![CDATA[unclosed cdata</body></Doc>"#;
163
163
  assert!(deserialize(bad_xml, schema).is_err());
164
164
  }
165
+
166
+ #[test]
167
+ fn unsupported_general_entities_are_rejected() {
168
+ let schema = ModelSchema::builder("Document")
169
+ .field(FieldSchema::new(
170
+ "payload",
171
+ b"Payload",
172
+ FieldKind::Element,
173
+ ValueType::Scalar(ScalarType::String),
174
+ ))
175
+ .build();
176
+ for xml in [
177
+ "<Document><Payload>&audit;</Payload></Document>",
178
+ "<!DOCTYPE Document [<!ENTITY audit 'EXPECTED'>]><Document><Payload>&audit;</Payload></Document>",
179
+ "<!DOCTYPE Document [<!ENTITY audit SYSTEM 'file:///nonexistent/sentinel'>]><Document><Payload>&audit;</Payload></Document>",
180
+ "<Document><Unknown>&audit;</Unknown></Document>",
181
+ ] {
182
+ let error = deserialize(xml.as_bytes(), Arc::clone(&schema)).unwrap_err();
183
+ assert!(error.to_string().contains("Unsupported general entity reference"), "{error}");
184
+ }
185
+ let value = deserialize(
186
+ b"<Document><Payload>&amp;&#65;&#x42;</Payload></Document>",
187
+ schema,
188
+ )
189
+ .unwrap();
190
+ assert_eq!(value.get("payload"), Some(&PolyValue::String("&AB".into())));
191
+ }
@@ -417,6 +417,10 @@ fn extract_schema_from_class<'py>(
417
417
  });
418
418
 
419
419
  let mut field_schema = FieldSchema::new(py_name, xml_name.as_bytes(), kind, val_type);
420
+ field_schema.default_value = meta
421
+ .as_ref()
422
+ .and_then(|m| m.get_item("default").ok())
423
+ .and_then(|v| v.extract::<String>().ok());
420
424
  if let Some(ns) = field_ns {
421
425
  field_schema = field_schema.namespace(ns);
422
426
  }
@@ -494,6 +498,11 @@ fn extract_schema_from_class<'py>(
494
498
  });
495
499
 
496
500
  let mut field_schema = FieldSchema::new(py_name, xml_name.as_bytes(), kind, val_type);
501
+ field_schema.default_value = field_obj
502
+ .getattr("metadata")
503
+ .ok()
504
+ .and_then(|m| m.get_item("default").ok())
505
+ .and_then(|v| v.extract::<String>().ok());
497
506
  if let Some(ns) = field_ns {
498
507
  field_schema = field_schema.namespace(ns);
499
508
  }
@@ -531,3 +531,70 @@ def test_codecs_flag_disabled():
531
531
  mod = _load_module_from_file("gen_no_codecs", out_dir / "warehouse.py")
532
532
  assert not hasattr(mod.Inventory, "from_xml")
533
533
  assert not hasattr(mod.Inventory, "to_xml")
534
+
535
+
536
+ @pytest.mark.parametrize("backend", ["dataclass", "pydantic"])
537
+ def test_generated_models_reject_general_entities(tmp_path, backend):
538
+ schema = pathlib.Path(__file__).resolve().parents[3] / "research/fixtures/wave7/xxe_text.xsd"
539
+ subprocess.run(
540
+ [
541
+ str(_get_polyxml_bin()),
542
+ "generate",
543
+ str(schema),
544
+ "--lang",
545
+ "python",
546
+ "--backend",
547
+ backend,
548
+ "--out",
549
+ str(tmp_path),
550
+ ],
551
+ check=True,
552
+ capture_output=True,
553
+ )
554
+ module = _load_module_from_file(f"entities_{backend}", tmp_path / "xxe_text.py")
555
+ sentinel = tmp_path / "sentinel.txt"
556
+ sentinel.write_text("PRIVATE_SENTINEL_VALUE")
557
+ for declaration in [
558
+ "",
559
+ "<!DOCTYPE Document [<!ENTITY audit 'EXPECTED'>]>",
560
+ f"<!DOCTYPE Document [<!ENTITY audit SYSTEM '{sentinel.as_uri()}'>]>",
561
+ ]:
562
+ with pytest.raises(ValueError, match="Unsupported general entity reference"):
563
+ module.Document.from_xml(
564
+ declaration + "<Document><Payload>&audit;</Payload></Document>"
565
+ )
566
+ model = module.Document.from_xml("<Document><Payload>&amp;&#65;&#x42;</Payload></Document>")
567
+ assert model.payload == "&AB"
568
+
569
+
570
+ @pytest.mark.parametrize("backend", ["dataclass", "pydantic"])
571
+ def test_element_defaults_preserve_absence(tmp_path, backend):
572
+ schema = pathlib.Path(__file__).resolve().parents[3] / "research/fixtures/element_defaults.xsd"
573
+ subprocess.run(
574
+ [
575
+ str(_get_polyxml_bin()),
576
+ "generate",
577
+ str(schema),
578
+ "--lang",
579
+ "python",
580
+ "--backend",
581
+ backend,
582
+ "--out",
583
+ str(tmp_path),
584
+ ],
585
+ check=True,
586
+ capture_output=True,
587
+ )
588
+ module = _load_module_from_file(f"defaults_{backend}", tmp_path / "element_defaults.py")
589
+ absent = module.Root.from_xml("<Root/>")
590
+ assert (absent.flag, absent.count, absent.label, absent.mode) == (None, None, None, "auto")
591
+ assert ET.fromstring(absent.to_xml()).find("Flag") is None
592
+ for document in [
593
+ "<Root><Flag/><Count></Count><Label/></Root>",
594
+ "<Root><Flag></Flag><Count/><Label></Label></Root>",
595
+ ]:
596
+ present = module.Root.from_xml(document)
597
+ assert (present.flag, present.count, present.label) == (False, 42, "fallback")
598
+ encoded = present.to_xml()
599
+ assert ET.fromstring(encoded).findtext("Flag") == "false"
600
+ assert module.Root.from_xml(encoded).count == 42
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "polyxml"
7
- version = "0.33.6"
7
+ version = "0.33.7"
8
8
  description = "High-performance, polyglot XML data-binding engine built in Rust"
9
9
  authors = [
10
10
  { name = "Bailey Nguyen", email = "bailey.tan.nguyen@gmail.com" }
File without changes