polyxml 0.25.0__tar.gz → 0.27.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 (67) hide show
  1. {polyxml-0.25.0 → polyxml-0.27.0}/Cargo.lock +6 -6
  2. {polyxml-0.25.0 → polyxml-0.27.0}/Cargo.toml +1 -1
  3. {polyxml-0.25.0 → polyxml-0.27.0}/PKG-INFO +1 -1
  4. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/java/mod.rs +164 -4
  5. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/java/models.rs +10 -0
  6. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/typescript/mod.rs +15 -0
  7. polyxml-0.27.0/crates/polyxml-core/src/converters.rs +178 -0
  8. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/parser.rs +26 -0
  9. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/schema.rs +6 -0
  10. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/schema_parser/mod.rs +75 -0
  11. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/serializer.rs +16 -1
  12. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_core.rs +49 -0
  13. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_java_codegen.rs +56 -0
  14. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_pattern_codegen.rs +53 -0
  15. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_xsi_type.rs +15 -2
  16. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-python/tests/test_xsi_type.py +19 -0
  17. {polyxml-0.25.0 → polyxml-0.27.0}/pyproject.toml +1 -1
  18. polyxml-0.25.0/crates/polyxml-core/src/converters.rs +0 -80
  19. {polyxml-0.25.0 → polyxml-0.27.0}/README.md +0 -0
  20. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/Cargo.toml +0 -0
  21. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/README.md +0 -0
  22. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
  23. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/benches/support/tag_dispatch_fixtures.rs +0 -0
  24. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/benches/tag_dispatch.rs +0 -0
  25. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/cpp/mod.rs +0 -0
  26. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/csharp/mod.rs +0 -0
  27. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/go/mod.rs +0 -0
  28. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/java/codec.rs +0 -0
  29. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/mod.rs +0 -0
  30. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/python/aot.rs +0 -0
  31. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/python/mod.rs +0 -0
  32. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/rust/mod.rs +0 -0
  33. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/error.rs +0 -0
  34. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/ir/mod.rs +0 -0
  35. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/ir/tarjan.rs +0 -0
  36. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/json.rs +0 -0
  37. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/lib.rs +0 -0
  38. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/transcoder.rs +0 -0
  39. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/src/value.rs +0 -0
  40. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_adversarial_identifiers.rs +0 -0
  41. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_conformance.rs +0 -0
  42. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_cpp_codegen.rs +0 -0
  43. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_csharp_codegen.rs +0 -0
  44. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_go_codegen.rs +0 -0
  45. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_json.rs +0 -0
  46. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_python_codegen.rs +0 -0
  47. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_rust_codecs.rs +0 -0
  48. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_rust_codegen.rs +0 -0
  49. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_schema_audit.rs +0 -0
  50. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_schema_ir.rs +0 -0
  51. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_security.rs +0 -0
  52. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_transcoder.rs +0 -0
  53. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_ts_codegen.rs +0 -0
  54. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_unbounded_choice.rs +0 -0
  55. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-python/Cargo.toml +0 -0
  56. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-python/README.md +0 -0
  57. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-python/benches/aot_vs_dataclass.py +0 -0
  58. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-python/src/lib.rs +0 -0
  59. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-python/tests/test_conformance.py +0 -0
  60. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-python/tests/test_generated_models.py +0 -0
  61. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-python/tests/test_json.py +0 -0
  62. {polyxml-0.25.0 → polyxml-0.27.0}/crates/polyxml-python/tests/test_polyxml.py +0 -0
  63. {polyxml-0.25.0 → polyxml-0.27.0}/python/polyxml/AGENT_GUIDE.md +0 -0
  64. {polyxml-0.25.0 → polyxml-0.27.0}/python/polyxml/__init__.py +0 -0
  65. {polyxml-0.25.0 → polyxml-0.27.0}/python/polyxml/compat/__init__.py +0 -0
  66. {polyxml-0.25.0 → polyxml-0.27.0}/python/polyxml/compat/xsdata.py +0 -0
  67. {polyxml-0.25.0 → polyxml-0.27.0}/python/polyxml/py.typed +0 -0
@@ -608,7 +608,7 @@ dependencies = [
608
608
 
609
609
  [[package]]
610
610
  name = "polyxml"
611
- version = "0.25.0"
611
+ version = "0.27.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.25.0"
631
+ version = "0.27.0"
632
632
  dependencies = [
633
633
  "polyxml",
634
634
  ]
635
635
 
636
636
  [[package]]
637
637
  name = "polyxml-cli"
638
- version = "0.25.0"
638
+ version = "0.27.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.25.0"
652
+ version = "0.27.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.25.0"
662
+ version = "0.27.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.25.0"
672
+ version = "0.27.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.25.0"
6
+ version = "0.27.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.25.0
3
+ Version: 0.27.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -57,6 +57,9 @@ pub struct JavaOptions {
57
57
  pub emit_direct_codec: bool,
58
58
  /// Generate facet validation in compact constructors (default: true)
59
59
  pub validate_facets: bool,
60
+ /// Emit Jakarta Bean Validation annotations (adds an optional API dependency).
61
+ #[serde(default)]
62
+ pub bean_validation: bool,
60
63
  /// Emit top-level aliases for root elements (default: true)
61
64
  pub emit_root_aliases: bool,
62
65
  /// Custom header text to prepend to generated files (default: None)
@@ -72,6 +75,7 @@ impl Default for JavaOptions {
72
75
  emit_builder: false,
73
76
  emit_direct_codec: false,
74
77
  validate_facets: true,
78
+ bean_validation: false,
75
79
  emit_root_aliases: true,
76
80
  custom_header: None,
77
81
  }
@@ -345,6 +349,9 @@ impl JavaCodegen {
345
349
  out.push_str("import java.time.*;\n");
346
350
  out.push_str("import java.math.*;\n");
347
351
  out.push_str("import java.util.regex.Pattern;\n");
352
+ if self.options.bean_validation {
353
+ out.push_str("import jakarta.validation.constraints.*;\n");
354
+ }
348
355
 
349
356
  if self.options.backend.is_jackson() {
350
357
  out.push('\n');
@@ -454,6 +461,12 @@ impl JavaCodegen {
454
461
 
455
462
  let field_type = self.field_type(field);
456
463
 
464
+ let bean_annotations = self.bean_annotations(field).join(" ");
465
+ let bean_prefix = if bean_annotations.is_empty() {
466
+ String::new()
467
+ } else {
468
+ format!("{bean_annotations} ")
469
+ };
457
470
  if jackson {
458
471
  let mut annotations = Vec::new();
459
472
  // @JsonProperty
@@ -482,9 +495,12 @@ impl JavaCodegen {
482
495
  }
483
496
 
484
497
  let annotation_str = annotations.join(" ");
485
- components.push(format!("{} {} {}", annotation_str, field_type, field_id));
498
+ components.push(format!(
499
+ "{}{} {} {}",
500
+ bean_prefix, annotation_str, field_type, field_id
501
+ ));
486
502
  } else {
487
- components.push(format!("{} {}", field_type, field_id));
503
+ components.push(format!("{}{} {}", bean_prefix, field_type, field_id));
488
504
  }
489
505
 
490
506
  if self.options.validate_facets {
@@ -714,10 +730,26 @@ impl JavaCodegen {
714
730
  };
715
731
 
716
732
  // Build the value component with optional Jackson annotations
733
+ let mut value_field = crate::ir::FieldDef::new(
734
+ "value",
735
+ "value",
736
+ crate::ir::FieldKind::Text,
737
+ s.base_type.clone(),
738
+ );
739
+ value_field.facets = Some(s.facets.clone());
740
+ let bean_prefix = self.bean_annotations(&value_field).join(" ");
741
+ let bean_prefix = if bean_prefix.is_empty() {
742
+ String::new()
743
+ } else {
744
+ format!("{bean_prefix} ")
745
+ };
717
746
  let value_component = if jackson {
718
- format!("@JsonValue @JacksonXmlText {} value", base_type)
747
+ format!(
748
+ "{bean_prefix}@JsonValue @JacksonXmlText {} value",
749
+ base_type
750
+ )
719
751
  } else {
720
- format!("{} value", base_type)
752
+ format!("{bean_prefix}{} value", base_type)
721
753
  };
722
754
 
723
755
  if checks.is_empty() && !jackson {
@@ -758,6 +790,129 @@ impl JavaCodegen {
758
790
  let _ = writeln!(out, "{}}}", indent);
759
791
  }
760
792
 
793
+ fn bean_annotations(&self, field: &crate::ir::FieldDef) -> Vec<String> {
794
+ if !self.options.bean_validation {
795
+ return Vec::new();
796
+ }
797
+ let mut annotations = Vec::new();
798
+ let is_list = field.cardinality.is_list() || field.type_ref.is_list();
799
+ let is_optional = field.cardinality.is_optional() || field.nillable;
800
+ if !is_optional
801
+ && !matches!(
802
+ self.field_type(field).as_str(),
803
+ "boolean" | "byte" | "short" | "int" | "long" | "float" | "double"
804
+ )
805
+ {
806
+ // Java primitive fields cannot be null; reference fields can.
807
+ annotations.push("@NotNull".to_string());
808
+ }
809
+ if matches!(&field.type_ref, TypeRef::Named(_) | TypeRef::List(_)) {
810
+ annotations.push("@jakarta.validation.Valid".to_string());
811
+ }
812
+ let facets = field.facets.as_ref();
813
+ let Some(facets) = facets else {
814
+ return annotations;
815
+ };
816
+ let primitive = &field.type_ref;
817
+ let is_string = matches!(
818
+ primitive,
819
+ TypeRef::Primitive(
820
+ PrimitiveType::String
821
+ | PrimitiveType::NormalizedString
822
+ | PrimitiveType::Token
823
+ | PrimitiveType::Name
824
+ | PrimitiveType::NCName
825
+ | PrimitiveType::QName
826
+ | PrimitiveType::Language
827
+ | PrimitiveType::NMTOKEN
828
+ | PrimitiveType::NMTOKENS
829
+ | PrimitiveType::AnyUri
830
+ | PrimitiveType::Id
831
+ | PrimitiveType::IdRef
832
+ | PrimitiveType::IdRefs
833
+ | PrimitiveType::Entity
834
+ | PrimitiveType::Entities
835
+ | PrimitiveType::GDay
836
+ | PrimitiveType::GMonth
837
+ | PrimitiveType::GYear
838
+ | PrimitiveType::GYearMonth
839
+ | PrimitiveType::GMonthDay
840
+ )
841
+ );
842
+ let is_numeric = matches!(
843
+ primitive,
844
+ TypeRef::Primitive(
845
+ PrimitiveType::Byte
846
+ | PrimitiveType::Short
847
+ | PrimitiveType::Int
848
+ | PrimitiveType::Integer
849
+ | PrimitiveType::Long
850
+ | PrimitiveType::Decimal
851
+ | PrimitiveType::UnsignedByte
852
+ | PrimitiveType::UnsignedShort
853
+ | PrimitiveType::UnsignedInt
854
+ | PrimitiveType::UnsignedLong
855
+ | PrimitiveType::PositiveInteger
856
+ | PrimitiveType::NegativeInteger
857
+ | PrimitiveType::NonPositiveInteger
858
+ | PrimitiveType::NonNegativeInteger
859
+ | PrimitiveType::Float
860
+ | PrimitiveType::Double
861
+ )
862
+ );
863
+ if is_list || (is_string && !is_optional) {
864
+ if let Some(length) = facets.length {
865
+ annotations.push(format!("@Size(min = {length}, max = {length})"));
866
+ } else if facets.min_length.is_some() || facets.max_length.is_some() {
867
+ annotations.push(format!(
868
+ "@Size(min = {}, max = {})",
869
+ facets.min_length.unwrap_or(0),
870
+ facets.max_length.unwrap_or(i32::MAX as usize)
871
+ ));
872
+ }
873
+ }
874
+ if is_string && !is_optional {
875
+ for pattern in &facets.patterns {
876
+ annotations.push(format!(
877
+ "@jakarta.validation.constraints.Pattern(regexp = {pattern:?})"
878
+ ));
879
+ }
880
+ }
881
+ if is_numeric
882
+ && !is_list
883
+ && !is_optional
884
+ && !matches!(
885
+ primitive,
886
+ TypeRef::Primitive(PrimitiveType::Float | PrimitiveType::Double)
887
+ )
888
+ {
889
+ for (value, minimum, inclusive) in [
890
+ (facets.min_inclusive.as_ref(), true, true),
891
+ (facets.max_inclusive.as_ref(), false, true),
892
+ (facets.min_exclusive.as_ref(), true, false),
893
+ (facets.max_exclusive.as_ref(), false, false),
894
+ ] {
895
+ if let Some(value) = value {
896
+ if inclusive && !matches!(primitive, TypeRef::Primitive(PrimitiveType::Decimal))
897
+ {
898
+ if let Ok(integer) = value.parse::<i64>() {
899
+ annotations.push(format!(
900
+ "@{}({integer})",
901
+ if minimum { "Min" } else { "Max" }
902
+ ));
903
+ continue;
904
+ }
905
+ }
906
+ let annotation = if minimum { "DecimalMin" } else { "DecimalMax" };
907
+ annotations.push(format!(
908
+ "@{annotation}(value = {value:?}, inclusive = {inclusive})"
909
+ ));
910
+ }
911
+ }
912
+ }
913
+ annotations
914
+ }
915
+
761
916
  fn build_facet_checks(
762
917
  &self,
763
918
  var_name: &str,
@@ -789,6 +944,11 @@ impl JavaCodegen {
789
944
  | PrimitiveType::IdRefs
790
945
  | PrimitiveType::Entity
791
946
  | PrimitiveType::Entities
947
+ | PrimitiveType::GDay
948
+ | PrimitiveType::GMonth
949
+ | PrimitiveType::GYear
950
+ | PrimitiveType::GYearMonth
951
+ | PrimitiveType::GMonthDay
792
952
  ),
793
953
  _ => false,
794
954
  };
@@ -16,6 +16,13 @@ impl JavaCodegen {
16
16
  let implements = interface.map(|v| format!(", {v}")).unwrap_or_default();
17
17
  let ty_name = self.context.map_type_ref(ty);
18
18
  let _ = writeln!(out, "{indent}public {nested}final class {name} implements java.io.Serializable{implements} {{\n{indent} private static final long serialVersionUID = 1L;");
19
+ if self.options.bean_validation {
20
+ let mut value_field = FieldDef::new("value", "value", FieldKind::Text, ty.clone());
21
+ value_field.facets = facets.cloned();
22
+ for annotation in self.bean_annotations(&value_field) {
23
+ let _ = writeln!(out, "{indent} {annotation}");
24
+ }
25
+ }
19
26
  if self.options.backend.is_jackson() {
20
27
  let _ = writeln!(out, "{indent} @JsonValue @JacksonXmlText");
21
28
  }
@@ -153,6 +160,9 @@ impl JavaCodegen {
153
160
  let own = &fields[fields.len() - s.fields.len()..];
154
161
  for (f, id) in own {
155
162
  self.emit_property_annotations(out, f, &format!("{indent} "));
163
+ for annotation in self.bean_annotations(f) {
164
+ let _ = writeln!(out, "{indent} {annotation}");
165
+ }
156
166
  let _ = writeln!(
157
167
  out,
158
168
  "{indent} private {} {id} = {};",
@@ -759,6 +759,11 @@ impl TypeScriptCodegen {
759
759
  | PrimitiveType::IdRefs
760
760
  | PrimitiveType::Entity
761
761
  | PrimitiveType::Entities
762
+ | PrimitiveType::GDay
763
+ | PrimitiveType::GMonth
764
+ | PrimitiveType::GYear
765
+ | PrimitiveType::GYearMonth
766
+ | PrimitiveType::GMonthDay
762
767
  ),
763
768
  _ => false,
764
769
  };
@@ -836,6 +841,11 @@ impl TypeScriptCodegen {
836
841
  | PrimitiveType::NCName
837
842
  | PrimitiveType::Language
838
843
  | PrimitiveType::AnyUri
844
+ | PrimitiveType::GDay
845
+ | PrimitiveType::GMonth
846
+ | PrimitiveType::GYear
847
+ | PrimitiveType::GYearMonth
848
+ | PrimitiveType::GMonthDay
839
849
  ),
840
850
  _ => false,
841
851
  };
@@ -909,6 +919,11 @@ impl TypeScriptCodegen {
909
919
  | PrimitiveType::NCName
910
920
  | PrimitiveType::Language
911
921
  | PrimitiveType::AnyUri
922
+ | PrimitiveType::GDay
923
+ | PrimitiveType::GMonth
924
+ | PrimitiveType::GYear
925
+ | PrimitiveType::GYearMonth
926
+ | PrimitiveType::GMonthDay
912
927
  ),
913
928
  _ => false,
914
929
  };
@@ -0,0 +1,178 @@
1
+ use crate::error::{PolyXmlError, Result};
2
+ use crate::ir::PrimitiveType;
3
+ use crate::schema::ScalarType;
4
+ use crate::value::PolyValue;
5
+
6
+ #[inline(always)]
7
+ pub fn trim_bytes(mut b: &[u8]) -> &[u8] {
8
+ while let Some((first, rest)) = b.split_first() {
9
+ if first.is_ascii_whitespace() {
10
+ b = rest;
11
+ } else {
12
+ break;
13
+ }
14
+ }
15
+ while let Some((last, rest)) = b.split_last() {
16
+ if last.is_ascii_whitespace() {
17
+ b = rest;
18
+ } else {
19
+ break;
20
+ }
21
+ }
22
+ b
23
+ }
24
+
25
+ pub struct ValueConverter;
26
+
27
+ impl ValueConverter {
28
+ pub fn parse_scalar(
29
+ scalar_type: &ScalarType,
30
+ bytes: &[u8],
31
+ field_name: &str,
32
+ ) -> Result<PolyValue> {
33
+ match scalar_type {
34
+ ScalarType::String => {
35
+ let s = std::str::from_utf8(bytes)?;
36
+ Ok(PolyValue::String(s.to_string()))
37
+ }
38
+ ScalarType::Int => {
39
+ let trimmed = trim_bytes(bytes);
40
+ let val: i64 =
41
+ lexical_core::parse(trimmed).map_err(|_| PolyXmlError::ScalarParseError {
42
+ field: field_name.to_string(),
43
+ expected: "integer",
44
+ value: String::from_utf8_lossy(bytes).to_string(),
45
+ })?;
46
+ Ok(PolyValue::Int(val))
47
+ }
48
+ ScalarType::Float => {
49
+ let trimmed = trim_bytes(bytes);
50
+ let val: f64 =
51
+ lexical_core::parse(trimmed).map_err(|_| PolyXmlError::ScalarParseError {
52
+ field: field_name.to_string(),
53
+ expected: "float",
54
+ value: String::from_utf8_lossy(bytes).to_string(),
55
+ })?;
56
+ Ok(PolyValue::Float(val))
57
+ }
58
+ ScalarType::Bool => {
59
+ let trimmed = trim_bytes(bytes);
60
+ match trimmed {
61
+ b"true" | b"1" => Ok(PolyValue::Bool(true)),
62
+ b"false" | b"0" => Ok(PolyValue::Bool(false)),
63
+ _ => Err(PolyXmlError::ScalarParseError {
64
+ field: field_name.to_string(),
65
+ expected: "boolean ('true', 'false', '1', '0')",
66
+ value: String::from_utf8_lossy(bytes).to_string(),
67
+ }),
68
+ }
69
+ }
70
+ ScalarType::Decimal
71
+ | ScalarType::XmlDate
72
+ | ScalarType::XmlDateTime
73
+ | ScalarType::XmlTime
74
+ | ScalarType::XmlDuration
75
+ | ScalarType::Any => {
76
+ let s = std::str::from_utf8(trim_bytes(bytes))?;
77
+ Ok(PolyValue::String(s.to_string()))
78
+ }
79
+ ScalarType::XmlGregorian(kind) => {
80
+ let value = std::str::from_utf8(trim_bytes(bytes))?;
81
+ if !valid_gregorian(*kind, value.as_bytes()) {
82
+ return Err(PolyXmlError::ScalarParseError {
83
+ field: field_name.to_string(),
84
+ expected: "XML Schema Gregorian partial date",
85
+ value: value.to_string(),
86
+ });
87
+ }
88
+ Ok(PolyValue::String(value.to_string()))
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ fn two_digits(bytes: &[u8]) -> Option<u8> {
95
+ if bytes.len() == 2 && bytes.iter().all(u8::is_ascii_digit) {
96
+ Some((bytes[0] - b'0') * 10 + bytes[1] - b'0')
97
+ } else {
98
+ None
99
+ }
100
+ }
101
+
102
+ fn valid_timezone(bytes: &[u8]) -> Option<&[u8]> {
103
+ if let Some(value) = bytes.strip_suffix(b"Z") {
104
+ return Some(value);
105
+ }
106
+ let split = bytes
107
+ .iter()
108
+ .rposition(|byte| *byte == b'+' || *byte == b'-')?;
109
+ let suffix = &bytes[split..];
110
+ if suffix.len() != 6 || suffix[3] != b':' {
111
+ return None;
112
+ }
113
+ let hours = two_digits(&suffix[1..3])?;
114
+ let minutes = two_digits(&suffix[4..6])?;
115
+ if hours > 14 || minutes > 59 || (hours == 14 && minutes != 0) {
116
+ return None;
117
+ }
118
+ Some(&bytes[..split])
119
+ }
120
+
121
+ fn valid_gregorian(kind: PrimitiveType, bytes: &[u8]) -> bool {
122
+ let has_timezone = bytes.ends_with(b"Z")
123
+ || bytes.contains(&b'+')
124
+ || (bytes.len() >= 6 && bytes[bytes.len() - 6] == b'-' && bytes[bytes.len() - 3] == b':');
125
+ let value = if has_timezone {
126
+ match valid_timezone(bytes) {
127
+ Some(value) => value,
128
+ None => return false,
129
+ }
130
+ } else {
131
+ bytes
132
+ };
133
+ match kind {
134
+ PrimitiveType::GDay => {
135
+ value.len() == 5
136
+ && value.starts_with(b"---")
137
+ && two_digits(&value[3..]).is_some_and(|day| (1..=31).contains(&day))
138
+ }
139
+ PrimitiveType::GMonth => {
140
+ value.len() == 4
141
+ && value.starts_with(b"--")
142
+ && two_digits(&value[2..]).is_some_and(|month| (1..=12).contains(&month))
143
+ }
144
+ PrimitiveType::GMonthDay => {
145
+ if value.len() != 7 || !value.starts_with(b"--") || value[4] != b'-' {
146
+ return false;
147
+ }
148
+ let (Some(month), Some(day)) = (two_digits(&value[2..4]), two_digits(&value[5..7]))
149
+ else {
150
+ return false;
151
+ };
152
+ let max_day = match month {
153
+ 2 => 29,
154
+ 4 | 6 | 9 | 11 => 30,
155
+ 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
156
+ _ => return false,
157
+ };
158
+ day >= 1 && day <= max_day
159
+ }
160
+ PrimitiveType::GYear => valid_year(value),
161
+ PrimitiveType::GYearMonth => {
162
+ if value.len() < 7 || value[value.len() - 3] != b'-' {
163
+ return false;
164
+ }
165
+ valid_year(&value[..value.len() - 3])
166
+ && two_digits(&value[value.len() - 2..])
167
+ .is_some_and(|month| (1..=12).contains(&month))
168
+ }
169
+ _ => false,
170
+ }
171
+ }
172
+
173
+ fn valid_year(value: &[u8]) -> bool {
174
+ let digits = value.strip_prefix(b"-").unwrap_or(value);
175
+ digits.len() >= 4
176
+ && digits.iter().all(u8::is_ascii_digit)
177
+ && (digits.len() == 4 || digits[0] != b'0')
178
+ }
@@ -355,6 +355,14 @@ impl XmlDeserializer {
355
355
  }
356
356
  }
357
357
  ValueType::Nested(sub_schema) => {
358
+ if is_nil {
359
+ stack
360
+ .last_mut()
361
+ .unwrap()
362
+ .push_list_item(field_idx, PolyValue::Null);
363
+ unknown_depth = 1;
364
+ continue;
365
+ }
358
366
  let frame_schema = resolve_record_schema(
359
367
  sub_schema,
360
368
  e,
@@ -367,6 +375,12 @@ impl XmlDeserializer {
367
375
  _ => {}
368
376
  },
369
377
  ValueType::Nested(sub_schema) => {
378
+ if is_nil {
379
+ stack.last_mut().unwrap().values[field_idx] =
380
+ Some(PolyValue::Null);
381
+ unknown_depth = 1;
382
+ continue;
383
+ }
370
384
  let frame_schema = resolve_record_schema(
371
385
  sub_schema,
372
386
  e,
@@ -414,6 +428,13 @@ impl XmlDeserializer {
414
428
  stack.last_mut().unwrap().push_list_item(field_idx, val);
415
429
  }
416
430
  ValueType::Nested(sub_schema) => {
431
+ if is_nil {
432
+ stack
433
+ .last_mut()
434
+ .unwrap()
435
+ .push_list_item(field_idx, PolyValue::Null);
436
+ continue;
437
+ }
417
438
  let frame_schema =
418
439
  resolve_record_schema(sub_schema, e, &scope)?;
419
440
  let mut frame = StackFrame::new(frame_schema);
@@ -427,6 +448,11 @@ impl XmlDeserializer {
427
448
  _ => {}
428
449
  },
429
450
  ValueType::Nested(sub_schema) => {
451
+ if is_nil {
452
+ stack.last_mut().unwrap().values[field_idx] =
453
+ Some(PolyValue::Null);
454
+ continue;
455
+ }
430
456
  let frame_schema = resolve_record_schema(sub_schema, e, &scope)?;
431
457
  let mut frame = StackFrame::new(frame_schema);
432
458
  Self::parse_attributes(e, &mut frame)?;
@@ -19,6 +19,7 @@ pub enum ScalarType {
19
19
  XmlDateTime,
20
20
  XmlTime,
21
21
  XmlDuration,
22
+ XmlGregorian(crate::ir::PrimitiveType),
22
23
  Any,
23
24
  }
24
25
 
@@ -218,6 +219,11 @@ impl ModelSchema {
218
219
  PrimitiveType::DateTime => ScalarType::XmlDateTime,
219
220
  PrimitiveType::Time => ScalarType::XmlTime,
220
221
  PrimitiveType::Date => ScalarType::XmlDate,
222
+ PrimitiveType::GDay
223
+ | PrimitiveType::GMonth
224
+ | PrimitiveType::GYear
225
+ | PrimitiveType::GYearMonth
226
+ | PrimitiveType::GMonthDay => ScalarType::XmlGregorian(prim),
221
227
  PrimitiveType::Int
222
228
  | PrimitiveType::Integer
223
229
  | PrimitiveType::NonPositiveInteger