polyxml 0.33.7__tar.gz → 0.33.9__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 (78) hide show
  1. {polyxml-0.33.7 → polyxml-0.33.9}/Cargo.lock +6 -6
  2. {polyxml-0.33.7 → polyxml-0.33.9}/Cargo.toml +1 -1
  3. {polyxml-0.33.7 → polyxml-0.33.9}/PKG-INFO +1 -1
  4. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/codegen/csharp/mod.rs +40 -10
  5. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/codegen/go/mod.rs +50 -6
  6. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/codegen/python/mod.rs +11 -0
  7. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/ir/mod.rs +4 -0
  8. polyxml-0.33.9/crates/polyxml-core/src/ir/particle.rs +60 -0
  9. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/parser.rs +39 -3
  10. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/schema.rs +77 -0
  11. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/schema_parser/mod.rs +113 -1
  12. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/serializer.rs +52 -0
  13. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_csharp_codegen.rs +44 -0
  14. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_go_codegen.rs +45 -0
  15. polyxml-0.33.9/crates/polyxml-core/tests/test_value_constraints.rs +30 -0
  16. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-python/src/lib.rs +16 -0
  17. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-python/tests/test_generated_models.py +78 -0
  18. {polyxml-0.33.7 → polyxml-0.33.9}/pyproject.toml +1 -1
  19. {polyxml-0.33.7 → polyxml-0.33.9}/README.md +0 -0
  20. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/Cargo.toml +0 -0
  21. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/README.md +0 -0
  22. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
  23. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/benches/support/tag_dispatch_fixtures.rs +0 -0
  24. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/benches/tag_dispatch.rs +0 -0
  25. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/codegen/cpp/mod.rs +0 -0
  26. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/codegen/java/codec.rs +0 -0
  27. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/codegen/java/mod.rs +0 -0
  28. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/codegen/java/models.rs +0 -0
  29. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/codegen/mod.rs +0 -0
  30. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/codegen/python/aot.rs +0 -0
  31. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/codegen/rust/mod.rs +0 -0
  32. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/codegen/typescript/mod.rs +0 -0
  33. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/converters.rs +0 -0
  34. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/error.rs +0 -0
  35. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/ir/chunker.rs +0 -0
  36. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/ir/tarjan.rs +0 -0
  37. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/json.rs +0 -0
  38. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/lib.rs +0 -0
  39. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/transcoder.rs +0 -0
  40. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/src/value.rs +0 -0
  41. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/fixtures/lexical_union.xsd +0 -0
  42. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/fixtures/mixed_content.xsd +0 -0
  43. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/fixtures/ts_mixed_content_inheritance.xsd +0 -0
  44. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_adversarial_identifiers.rs +0 -0
  45. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_any_attribute.rs +0 -0
  46. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_any_element.rs +0 -0
  47. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_conformance.rs +0 -0
  48. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_core.rs +0 -0
  49. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_cpp_codegen.rs +0 -0
  50. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_java_codegen.rs +0 -0
  51. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_json.rs +0 -0
  52. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_lexical_union_codegen.rs +0 -0
  53. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_mixed_content.rs +0 -0
  54. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_pattern_codegen.rs +0 -0
  55. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_python_codegen.rs +0 -0
  56. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_qualified_attributes.rs +0 -0
  57. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_rust_codecs.rs +0 -0
  58. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_rust_codegen.rs +0 -0
  59. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_schema_audit.rs +0 -0
  60. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_schema_ir.rs +0 -0
  61. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_security.rs +0 -0
  62. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_transcoder.rs +0 -0
  63. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_ts_codegen.rs +0 -0
  64. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_unbounded_choice.rs +0 -0
  65. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-core/tests/test_xsi_type.rs +0 -0
  66. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-python/Cargo.toml +0 -0
  67. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-python/README.md +0 -0
  68. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-python/benches/aot_vs_dataclass.py +0 -0
  69. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-python/tests/test_conformance.py +0 -0
  70. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-python/tests/test_json.py +0 -0
  71. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-python/tests/test_mixed_content.py +0 -0
  72. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-python/tests/test_polyxml.py +0 -0
  73. {polyxml-0.33.7 → polyxml-0.33.9}/crates/polyxml-python/tests/test_xsi_type.py +0 -0
  74. {polyxml-0.33.7 → polyxml-0.33.9}/python/polyxml/AGENT_GUIDE.md +0 -0
  75. {polyxml-0.33.7 → polyxml-0.33.9}/python/polyxml/__init__.py +0 -0
  76. {polyxml-0.33.7 → polyxml-0.33.9}/python/polyxml/compat/__init__.py +0 -0
  77. {polyxml-0.33.7 → polyxml-0.33.9}/python/polyxml/compat/xsdata.py +0 -0
  78. {polyxml-0.33.7 → polyxml-0.33.9}/python/polyxml/py.typed +0 -0
@@ -608,7 +608,7 @@ dependencies = [
608
608
 
609
609
  [[package]]
610
610
  name = "polyxml"
611
- version = "0.33.7"
611
+ version = "0.33.9"
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.7"
632
+ version = "0.33.9"
633
633
  dependencies = [
634
634
  "polyxml",
635
635
  ]
636
636
 
637
637
  [[package]]
638
638
  name = "polyxml-cli"
639
- version = "0.33.7"
639
+ version = "0.33.9"
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.7"
653
+ version = "0.33.9"
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.7"
663
+ version = "0.33.9"
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.7"
673
+ version = "0.33.9"
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.7"
6
+ version = "0.33.9"
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.7
3
+ Version: 0.33.9
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -877,11 +877,11 @@ impl CSharpCodegen {
877
877
  ir: &SchemaIR,
878
878
  indent: &str,
879
879
  ) -> bool {
880
- let Some(default) = field.default_value.as_ref() else {
880
+ let Some(default) = field.default_value.as_ref().or(field.fixed_value.as_ref()) else {
881
881
  return false;
882
882
  };
883
883
  if !self.options.emit_xml_attributes
884
- || field.kind != FieldKind::Element
884
+ || !matches!(field.kind, FieldKind::Element | FieldKind::Attribute)
885
885
  || field.cardinality.is_list()
886
886
  || matches!(field.type_ref, TypeRef::Named(ref q) if matches!(ir.types.get(q), Some(TypeDef::Struct(_) | TypeDef::Union(_))))
887
887
  {
@@ -894,19 +894,49 @@ impl CSharpCodegen {
894
894
  .as_ref()
895
895
  .map(|ns| format!(", Namespace = {ns:?}"))
896
896
  .unwrap_or_default();
897
+ let parse = |text: &str| {
898
+ format!("({base})new XmlSerializer(typeof({base}), new XmlRootAttribute(\"value\")).Deserialize(new System.IO.StringReader(\"<value>\" + System.Security.SecurityElement.Escape({text}) + \"</value>\"))!")
899
+ };
900
+ if let Some(fixed) = &field.fixed_value {
901
+ let expected = parse(&format!("{fixed:?}"));
902
+ let initial = if field.cardinality.is_optional() && field.kind == FieldKind::Element {
903
+ "default!".to_string()
904
+ } else {
905
+ expected.clone()
906
+ };
907
+ writeln!(out, "{indent} private {ty} _{name} = {initial};
908
+ {indent} [XmlIgnore] public {ty} {name} {{ get => _{name}; set {{ if (value is not null && !object.Equals(value, {expected})) throw new System.ComponentModel.DataAnnotations.ValidationException(\"Fixed value constraint violated for {name}\"); _{name} = value; }} }}").unwrap();
909
+ } else {
910
+ let initial = if field.kind == FieldKind::Attribute {
911
+ parse(&format!("{default:?}"))
912
+ } else {
913
+ "default!".to_string()
914
+ };
915
+ writeln!(
916
+ out,
917
+ "{indent} [XmlIgnore] public {ty} {name} {{ get; set; }} = {initial};"
918
+ )
919
+ .unwrap();
920
+ }
921
+ let kind = if field.kind == FieldKind::Attribute {
922
+ "XmlAttribute"
923
+ } else {
924
+ "XmlElement"
925
+ };
897
926
  writeln!(
898
927
  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 {{",
928
+ "{indent} [{kind}({:?}{namespace})] public string? {name}Xml {{",
905
929
  field.xml_name
906
930
  )
907
931
  .unwrap();
908
932
  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>\"))!; }}
933
+ let text = if field.kind == FieldKind::Attribute {
934
+ "value".into()
935
+ } else {
936
+ format!("value.Length == 0 ? {default:?} : value")
937
+ };
938
+ let parsed = parse("text");
939
+ writeln!(out, "{indent} set {{ if (value is null) {{ {name} = default!; return; }} var text = {text}; {name} = {parsed}; }}
910
940
  {indent} }}").unwrap();
911
941
  if field.cardinality.is_optional() || field.nillable {
912
942
  writeln!(
@@ -1079,7 +1109,7 @@ impl CSharpCodegen {
1079
1109
  }
1080
1110
 
1081
1111
  let has_lexical_union = self.options.emit_xml_attributes && s.fields.iter().any(|f| {
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()))
1112
+ f.default_value.is_some() || f.fixed_value.is_some() || matches!(&f.type_ref, TypeRef::Named(q) if matches!(ir.types.get(q), Some(TypeDef::Union(u)) if u.is_lexical()))
1083
1113
  });
1084
1114
 
1085
1115
  if self.options.use_records && has_lexical_union {
@@ -371,6 +371,10 @@ impl GoCodegen {
371
371
  }
372
372
  self.emit_root_aliases(&mut body, ir);
373
373
 
374
+ if self.options.emit_xml_tags && !ir.content_models.is_empty() {
375
+ self.emit_content_helpers(&mut body);
376
+ }
377
+
374
378
  // Assemble final output with package and imports
375
379
  let mut out = String::new();
376
380
  if let Some(ref header) = self.options.custom_header {
@@ -399,20 +403,21 @@ impl GoCodegen {
399
403
  imports.push("\"encoding/xml\"");
400
404
  }
401
405
  if self.options.emit_xml_tags
402
- && ir
403
- .types
404
- .values()
405
- .any(|def| matches!(def, TypeDef::Struct(s) if ir.has_ordered_content(s)))
406
+ && (!ir.content_models.is_empty()
407
+ || ir
408
+ .types
409
+ .values()
410
+ .any(|def| matches!(def, TypeDef::Struct(s) if ir.has_ordered_content(s))))
406
411
  {
407
412
  imports.push("\"bytes\"");
408
413
  }
409
414
  if has_fmt || body.contains("fmt.") {
410
415
  imports.push("\"fmt\"");
411
416
  }
412
- if has_io {
417
+ if has_io || body.contains("io.") {
413
418
  imports.push("\"io\"");
414
419
  }
415
- if has_patterns {
420
+ if has_patterns || body.contains("regexp.") {
416
421
  imports.push("\"regexp\"");
417
422
  }
418
423
  if ir
@@ -1108,11 +1113,50 @@ impl GoCodegen {
1108
1113
  self.emit_mixed_struct_xml(out, s, ir, &field_names);
1109
1114
  }
1110
1115
 
1116
+ if self.options.emit_xml_tags && !ir.has_ordered_content(s) && !s.is_abstract {
1117
+ if let Some(model) = ir.content_models.get(&s.qname) {
1118
+ self.emit_content_codec(out, s, &format!("^(?:{})$", model.pattern()));
1119
+ }
1120
+ }
1111
1121
  if self.options.validate_facets {
1112
1122
  self.emit_struct_validator(out, s, ir, &field_names);
1113
1123
  }
1114
1124
  }
1115
1125
 
1126
+ fn emit_content_helpers(&self, out: &mut String) {
1127
+ out.push_str(r#"
1128
+ func polyxmlReadElement(d *xml.Decoder, start xml.StartElement) ([]byte,error) {
1129
+ var buffer bytes.Buffer; encoder:=xml.NewEncoder(&buffer);depth:=1
1130
+ token:=xml.Token(start)
1131
+ for {
1132
+ if element,ok:=token.(xml.StartElement);ok { attrs:=element.Attr[:0];for _,a:=range element.Attr {if a.Name.Space!="xmlns"&&a.Name.Local!="xmlns" {attrs=append(attrs,a)}};element.Attr=attrs;token=element }
1133
+ if err:=encoder.EncodeToken(token);err!=nil{return nil,err}
1134
+ if depth==0{break}
1135
+ next,err:=d.Token();if err!=nil{return nil,err};token=next
1136
+ switch token.(type){case xml.StartElement:depth++;case xml.EndElement:depth--}
1137
+ }
1138
+ if err:=encoder.Flush();err!=nil{return nil,err};return buffer.Bytes(),nil
1139
+ }
1140
+ func polyxmlValidateContent(data []byte,pattern string) error {
1141
+ decoder:=xml.NewDecoder(bytes.NewReader(data));depth:=0;tokens:=""
1142
+ for {token,err:=decoder.Token();if err==io.EOF{break};if err!=nil{return err};switch v:=token.(type){case xml.StartElement:depth++;if depth==2{if v.Name.Space!=""{tokens+="{"+v.Name.Space+"}"};tokens+=v.Name.Local+";"};case xml.EndElement:depth--}}
1143
+ matched,err:=regexp.MatchString(pattern,tokens);if err!=nil{return err};if !matched{return fmt.Errorf("content model constraint violated")};return nil
1144
+ }
1145
+ "#);
1146
+ }
1147
+ fn emit_content_codec(&self, out: &mut String, structure: &StructDef, pattern: &str) {
1148
+ let name = type_ident(&structure.qname);
1149
+ writeln!(out,"func (v *{name}) UnmarshalXML(d *xml.Decoder,start xml.StartElement) error {{
1150
+ data,err:=polyxmlReadElement(d,start);if err!=nil{{return err}};if err:=polyxmlValidateContent(data,{pattern:?});err!=nil{{return err}}
1151
+ type Alias {name};return xml.Unmarshal(data,(*Alias)(v))
1152
+ }}
1153
+ func (v {name}) MarshalXML(e *xml.Encoder,start xml.StartElement) error {{
1154
+ type Alias {name};var buffer bytes.Buffer;encoder:=xml.NewEncoder(&buffer);if err:=encoder.EncodeElement(Alias(v),start);err!=nil{{return err}};if err:=polyxmlValidateContent(buffer.Bytes(),{pattern:?});err!=nil{{return err}}
1155
+ decoder:=xml.NewDecoder(&buffer);for {{token,err:=decoder.Token();if err==io.EOF{{return nil}};if err!=nil{{return err}};if err:=e.EncodeToken(token);err!=nil{{return err}}}}
1156
+ }}
1157
+ ").unwrap();
1158
+ }
1159
+
1116
1160
  fn emit_mixed_struct_xml(
1117
1161
  &self,
1118
1162
  out: &mut String,
@@ -645,6 +645,14 @@ impl PythonCodegen {
645
645
  }
646
646
  let _ = writeln!(out, " name = \"{}\"", s.qname.local);
647
647
  out.push_str(" strict_root = False\n");
648
+ if let Some(model) = ir.content_models.get(&s.qname) {
649
+ writeln!(
650
+ out,
651
+ " content_pattern = {:?}",
652
+ format!("^(?:{})$", model.pattern())
653
+ )
654
+ .unwrap();
655
+ }
648
656
  if let Some(ref ns) = s.qname.namespace {
649
657
  let _ = writeln!(out, " namespace = \"{}\"", ns);
650
658
  }
@@ -993,6 +1001,9 @@ impl PythonCodegen {
993
1001
  parts.push(format!("\"namespace\": \"{}\"", ns));
994
1002
  }
995
1003
 
1004
+ if let Some(ref fixed) = field.fixed_value {
1005
+ parts.push(format!("\"fixed\": {fixed:?}"));
1006
+ }
996
1007
  if let Some(ref default) = field.default_value {
997
1008
  parts.push(format!("\"default\": {default:?}"));
998
1009
  }
@@ -1,4 +1,6 @@
1
1
  pub mod chunker;
2
+ pub mod particle;
3
+ pub use particle::Particle;
2
4
  pub mod tarjan;
3
5
 
4
6
  pub use chunker::{partition_topological_chunks, ChunkPlan, TypeChunk};
@@ -467,6 +469,8 @@ pub struct SchemaIR {
467
469
  /// repeated particle or substitution-member order during serialization.
468
470
  #[serde(default)]
469
471
  pub ordered_types: std::collections::BTreeSet<QName>,
472
+ #[serde(default)]
473
+ pub content_models: BTreeMap<QName, Particle>,
470
474
  /// Type owners outside the module currently being emitted. Definitions
471
475
  /// remain in `types` so generators can resolve their field semantics.
472
476
  #[serde(default)]
@@ -0,0 +1,60 @@
1
+ use super::QName;
2
+ use serde::{Deserialize, Serialize};
3
+
4
+ /// Source particle boundaries and occurrence constraints, independent of field layout.
5
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6
+ pub enum Particle {
7
+ Element(QName),
8
+ Sequence(Vec<Particle>),
9
+ Choice(Vec<Particle>),
10
+ Repeat {
11
+ particle: Box<Particle>,
12
+ min: usize,
13
+ max: Option<usize>,
14
+ },
15
+ }
16
+ impl Particle {
17
+ pub fn has_choice(&self) -> bool {
18
+ match self {
19
+ Self::Choice(_) => true,
20
+ Self::Sequence(items) => items.iter().any(Self::has_choice),
21
+ Self::Repeat { particle, .. } => particle.has_choice(),
22
+ _ => false,
23
+ }
24
+ }
25
+ pub fn adopt_namespace(&mut self, namespace: &str) {
26
+ match self {
27
+ Self::Element(name) => {
28
+ if name.namespace.is_none() {
29
+ name.namespace = Some(namespace.into());
30
+ }
31
+ }
32
+ Self::Sequence(items) | Self::Choice(items) => {
33
+ for item in items {
34
+ item.adopt_namespace(namespace);
35
+ }
36
+ }
37
+ Self::Repeat { particle, .. } => particle.adopt_namespace(namespace),
38
+ }
39
+ }
40
+ pub fn pattern(&self) -> String {
41
+ match self {
42
+ Self::Element(name) => regex::escape(&format!("{};", name)),
43
+ Self::Sequence(items) => items.iter().map(Self::pattern).collect(),
44
+ Self::Choice(items) => format!(
45
+ "(?:{})",
46
+ items
47
+ .iter()
48
+ .map(Self::pattern)
49
+ .collect::<Vec<_>>()
50
+ .join("|")
51
+ ),
52
+ Self::Repeat { particle, min, max } => format!(
53
+ "(?:{}){{{},{}}}",
54
+ particle.pattern(),
55
+ min,
56
+ max.map(|v| v.to_string()).unwrap_or_default()
57
+ ),
58
+ }
59
+ }
60
+ }
@@ -18,6 +18,7 @@ pub(crate) struct StackFrame {
18
18
  list_values: SmallVec<[Option<Vec<PolyValue>>; 4]>,
19
19
  frame_text_buf: Option<Vec<u8>>,
20
20
  mixed_parent_kind: Option<String>,
21
+ content_tokens: String,
21
22
  }
22
23
 
23
24
  impl StackFrame {
@@ -34,6 +35,7 @@ impl StackFrame {
34
35
  list_values: SmallVec::new(),
35
36
  frame_text_buf,
36
37
  mixed_parent_kind: None,
38
+ content_tokens: String::new(),
37
39
  }
38
40
  }
39
41
 
@@ -71,11 +73,13 @@ impl StackFrame {
71
73
  }
72
74
 
73
75
  fn finish(mut self) -> Result<PolyValue> {
76
+ crate::schema::validate_content(&self.schema, &self.content_tokens)?;
74
77
  for (index, field) in self.schema.fields.iter().enumerate() {
75
78
  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
+ if let (Some(default), ValueType::Scalar(scalar)) = (
80
+ &field.default_value.as_ref().or(field.fixed_value.as_ref()),
81
+ &field.val_type,
82
+ ) {
79
83
  self.values[index] = Some(ValueConverter::parse_scalar(
80
84
  scalar,
81
85
  default.as_bytes(),
@@ -113,6 +117,11 @@ impl StackFrame {
113
117
  }
114
118
  }
115
119
 
120
+ for (index, field) in self.schema.fields.iter().enumerate() {
121
+ if let Some(value) = self.values[index].as_ref() {
122
+ crate::schema::validate_fixed(field, value)?;
123
+ }
124
+ }
116
125
  Ok(PolyValue::Record {
117
126
  schema: self.schema,
118
127
  values: self.values.into_vec().into_boxed_slice(),
@@ -129,6 +138,7 @@ fn parse_field_scalar(
129
138
  field
130
139
  .default_value
131
140
  .as_ref()
141
+ .or(field.fixed_value.as_ref())
132
142
  .map_or(text, |value| value.as_bytes())
133
143
  } else {
134
144
  text
@@ -422,6 +432,13 @@ impl XmlDeserializer {
422
432
  unknown_depth += 1;
423
433
  continue;
424
434
  }
435
+ if active_scalar_field.is_none()
436
+ && active_mixed_scalar.is_none()
437
+ && any_stack.is_empty()
438
+ {
439
+ let scope = namespace_stack.last().unwrap();
440
+ record_content_token(stack.last_mut().unwrap(), e, scope);
441
+ }
425
442
  if active_scalar_field.is_some() || active_mixed_scalar.is_some() {
426
443
  unknown_depth = 1;
427
444
  continue;
@@ -598,6 +615,10 @@ impl XmlDeserializer {
598
615
  {
599
616
  continue;
600
617
  }
618
+ if any_stack.is_empty() {
619
+ let scope = namespace_scope(namespace_stack.last().unwrap(), e);
620
+ record_content_token(stack.last_mut().unwrap(), e, &scope);
621
+ }
601
622
  if !any_stack.is_empty() {
602
623
  let scope = namespace_scope(namespace_stack.last().unwrap(), e);
603
624
  let (ns, local) = resolve_element_qname(e, &scope);
@@ -1068,3 +1089,18 @@ impl<R: std::io::BufRead> XmlItemStream<R> {
1068
1089
  }
1069
1090
  }
1070
1091
  }
1092
+
1093
+ fn record_content_token(
1094
+ frame: &mut StackFrame,
1095
+ element: &BytesStart,
1096
+ scope: &HashMap<String, String>,
1097
+ ) {
1098
+ if frame.schema.content_pattern.is_some() {
1099
+ let (ns, local) = resolve_element_qname(element, scope);
1100
+ if let Some(ns) = ns {
1101
+ frame.content_tokens.push_str(&format!("{{{ns}}}"));
1102
+ }
1103
+ frame.content_tokens.push_str(&local);
1104
+ frame.content_tokens.push(';');
1105
+ }
1106
+ }
@@ -66,6 +66,7 @@ pub struct FieldSchema {
66
66
  pub val_type: ValueType,
67
67
  pub required: bool,
68
68
  pub default_value: Option<String>,
69
+ pub fixed_value: Option<String>,
69
70
  }
70
71
 
71
72
  impl FieldSchema {
@@ -83,6 +84,7 @@ impl FieldSchema {
83
84
  val_type,
84
85
  required: false,
85
86
  default_value: None,
87
+ fixed_value: None,
86
88
  }
87
89
  }
88
90
 
@@ -117,6 +119,7 @@ pub struct ModelSchema {
117
119
  pub is_abstract: bool,
118
120
  /// Require the document element to match this schema's expanded QName.
119
121
  pub strict_root: bool,
122
+ pub content_pattern: Option<regex::Regex>,
120
123
  /// Concrete derivations eligible for `xsi:type` dispatch. Python may
121
124
  /// refresh this registry when subclasses are defined after first use.
122
125
  variants: Arc<RwLock<Vec<Arc<ModelSchema>>>>,
@@ -354,6 +357,7 @@ impl ModelSchema {
354
357
 
355
358
  let mut field_schema = FieldSchema::new(&f.name, f.xml_name.as_bytes(), kind, val_type);
356
359
  field_schema.default_value = f.default_value.clone();
360
+ field_schema.fixed_value = f.fixed_value.clone();
357
361
  if let Some(ref ns) = f.namespace {
358
362
  field_schema = field_schema.namespace(ns);
359
363
  }
@@ -396,6 +400,12 @@ impl ModelSchema {
396
400
  builder = builder.namespace(ns);
397
401
  }
398
402
  builder = builder.is_abstract(s.is_abstract);
403
+ if let Some(model) = ir.content_models.get(&s.qname) {
404
+ builder = builder.content_pattern(
405
+ regex::Regex::new(&format!("^(?:{})$", model.pattern()))
406
+ .expect("validated content pattern"),
407
+ );
408
+ }
399
409
 
400
410
  // Flatten the xs:extension content model: base-chain fields
401
411
  // precede the struct's own fields (mirrors the Java codegen).
@@ -534,6 +544,7 @@ pub struct ModelSchemaBuilder {
534
544
  fields: Vec<FieldSchema>,
535
545
  is_abstract: bool,
536
546
  strict_root: bool,
547
+ content_pattern: Option<regex::Regex>,
537
548
  }
538
549
 
539
550
  impl ModelSchemaBuilder {
@@ -545,6 +556,7 @@ impl ModelSchemaBuilder {
545
556
  fields: Vec::new(),
546
557
  is_abstract: false,
547
558
  strict_root: false,
559
+ content_pattern: None,
548
560
  }
549
561
  }
550
562
 
@@ -559,6 +571,11 @@ impl ModelSchemaBuilder {
559
571
  self
560
572
  }
561
573
 
574
+ pub fn content_pattern(mut self, pattern: regex::Regex) -> Self {
575
+ self.content_pattern = Some(pattern);
576
+ self
577
+ }
578
+
562
579
  pub fn strict_root(mut self, strict_root: bool) -> Self {
563
580
  self.strict_root = strict_root;
564
581
  self
@@ -629,7 +646,67 @@ impl ModelSchemaBuilder {
629
646
  mixed_content: None,
630
647
  is_abstract: self.is_abstract,
631
648
  strict_root: self.strict_root,
649
+ content_pattern: self.content_pattern,
632
650
  variants: Arc::new(RwLock::new(Vec::new())),
633
651
  })
634
652
  }
635
653
  }
654
+
655
+ /// Validate fixed constraints in value space rather than comparing lexical spellings.
656
+ pub(crate) fn validate_fixed(
657
+ field: &FieldSchema,
658
+ value: &crate::value::PolyValue,
659
+ ) -> crate::error::Result<()> {
660
+ use crate::value::PolyValue;
661
+ let Some(fixed) = &field.fixed_value else {
662
+ return Ok(());
663
+ };
664
+ fn check(
665
+ ty: &ValueType,
666
+ value: &PolyValue,
667
+ fixed: &str,
668
+ name: &str,
669
+ ) -> crate::error::Result<()> {
670
+ if matches!(value, PolyValue::Null) {
671
+ return Ok(());
672
+ }
673
+ match (ty, value) {
674
+ (ValueType::Scalar(scalar), value) => {
675
+ let expected = crate::converters::ValueConverter::parse_scalar(
676
+ scalar,
677
+ fixed.as_bytes(),
678
+ name,
679
+ )?;
680
+ if *value != expected {
681
+ return Err(crate::error::PolyXmlError::SchemaError(format!(
682
+ "Fixed value constraint violated for {name}"
683
+ )));
684
+ }
685
+ }
686
+ (ValueType::List(inner), PolyValue::List(items)) => {
687
+ for item in items {
688
+ check(inner, item, fixed, name)?;
689
+ }
690
+ }
691
+ _ => {}
692
+ }
693
+ Ok(())
694
+ }
695
+ check(&field.val_type, value, fixed, &field.name)
696
+ }
697
+
698
+ pub(crate) fn validate_content(schema: &ModelSchema, tokens: &str) -> crate::error::Result<()> {
699
+ if let Some(pattern) = &schema.content_pattern {
700
+ if !pattern.is_match(tokens) {
701
+ return Err(crate::error::PolyXmlError::SchemaError(format!(
702
+ "Content model constraint violated for {}",
703
+ schema.name
704
+ )));
705
+ }
706
+ }
707
+ Ok(())
708
+ }
709
+
710
+ pub fn compile_content_pattern(pattern: &str) -> std::result::Result<regex::Regex, regex::Error> {
711
+ regex::Regex::new(pattern)
712
+ }