polyxml 0.26.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.26.0 → polyxml-0.27.0}/Cargo.lock +6 -6
  2. {polyxml-0.26.0 → polyxml-0.27.0}/Cargo.toml +1 -1
  3. {polyxml-0.26.0 → polyxml-0.27.0}/PKG-INFO +1 -1
  4. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/java/mod.rs +10 -0
  5. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/typescript/mod.rs +15 -0
  6. polyxml-0.27.0/crates/polyxml-core/src/converters.rs +178 -0
  7. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/schema.rs +6 -0
  8. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/schema_parser/mod.rs +75 -0
  9. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/serializer.rs +16 -1
  10. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_core.rs +49 -0
  11. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_pattern_codegen.rs +53 -0
  12. {polyxml-0.26.0 → polyxml-0.27.0}/pyproject.toml +1 -1
  13. polyxml-0.26.0/crates/polyxml-core/src/converters.rs +0 -80
  14. {polyxml-0.26.0 → polyxml-0.27.0}/README.md +0 -0
  15. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/Cargo.toml +0 -0
  16. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/README.md +0 -0
  17. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
  18. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/benches/support/tag_dispatch_fixtures.rs +0 -0
  19. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/benches/tag_dispatch.rs +0 -0
  20. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/cpp/mod.rs +0 -0
  21. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/csharp/mod.rs +0 -0
  22. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/go/mod.rs +0 -0
  23. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/java/codec.rs +0 -0
  24. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/java/models.rs +0 -0
  25. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/mod.rs +0 -0
  26. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/python/aot.rs +0 -0
  27. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/python/mod.rs +0 -0
  28. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/codegen/rust/mod.rs +0 -0
  29. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/error.rs +0 -0
  30. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/ir/mod.rs +0 -0
  31. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/ir/tarjan.rs +0 -0
  32. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/json.rs +0 -0
  33. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/lib.rs +0 -0
  34. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/parser.rs +0 -0
  35. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/transcoder.rs +0 -0
  36. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/src/value.rs +0 -0
  37. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_adversarial_identifiers.rs +0 -0
  38. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_conformance.rs +0 -0
  39. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_cpp_codegen.rs +0 -0
  40. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_csharp_codegen.rs +0 -0
  41. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_go_codegen.rs +0 -0
  42. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_java_codegen.rs +0 -0
  43. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_json.rs +0 -0
  44. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_python_codegen.rs +0 -0
  45. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_rust_codecs.rs +0 -0
  46. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_rust_codegen.rs +0 -0
  47. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_schema_audit.rs +0 -0
  48. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_schema_ir.rs +0 -0
  49. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_security.rs +0 -0
  50. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_transcoder.rs +0 -0
  51. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_ts_codegen.rs +0 -0
  52. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_unbounded_choice.rs +0 -0
  53. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-core/tests/test_xsi_type.rs +0 -0
  54. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-python/Cargo.toml +0 -0
  55. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-python/README.md +0 -0
  56. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-python/benches/aot_vs_dataclass.py +0 -0
  57. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-python/src/lib.rs +0 -0
  58. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-python/tests/test_conformance.py +0 -0
  59. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-python/tests/test_generated_models.py +0 -0
  60. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-python/tests/test_json.py +0 -0
  61. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-python/tests/test_polyxml.py +0 -0
  62. {polyxml-0.26.0 → polyxml-0.27.0}/crates/polyxml-python/tests/test_xsi_type.py +0 -0
  63. {polyxml-0.26.0 → polyxml-0.27.0}/python/polyxml/AGENT_GUIDE.md +0 -0
  64. {polyxml-0.26.0 → polyxml-0.27.0}/python/polyxml/__init__.py +0 -0
  65. {polyxml-0.26.0 → polyxml-0.27.0}/python/polyxml/compat/__init__.py +0 -0
  66. {polyxml-0.26.0 → polyxml-0.27.0}/python/polyxml/compat/xsdata.py +0 -0
  67. {polyxml-0.26.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.26.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.26.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.26.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.26.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.26.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.26.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.26.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.26.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
@@ -832,6 +832,11 @@ impl JavaCodegen {
832
832
  | PrimitiveType::IdRefs
833
833
  | PrimitiveType::Entity
834
834
  | PrimitiveType::Entities
835
+ | PrimitiveType::GDay
836
+ | PrimitiveType::GMonth
837
+ | PrimitiveType::GYear
838
+ | PrimitiveType::GYearMonth
839
+ | PrimitiveType::GMonthDay
835
840
  )
836
841
  );
837
842
  let is_numeric = matches!(
@@ -939,6 +944,11 @@ impl JavaCodegen {
939
944
  | PrimitiveType::IdRefs
940
945
  | PrimitiveType::Entity
941
946
  | PrimitiveType::Entities
947
+ | PrimitiveType::GDay
948
+ | PrimitiveType::GMonth
949
+ | PrimitiveType::GYear
950
+ | PrimitiveType::GYearMonth
951
+ | PrimitiveType::GMonthDay
942
952
  ),
943
953
  _ => false,
944
954
  };
@@ -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
+ }
@@ -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
@@ -167,6 +167,7 @@ impl XsdParser {
167
167
  // Frame post-passes: inherit pattern facets up derivation chains
168
168
  // and expand named model group references. Both are idempotent.
169
169
  // Cycle detection runs last so fields spliced in from groups are covered.
170
+ add_gregorian_types(&mut ir);
170
171
  inherit_pattern_facets(&mut ir);
171
172
  self.expand_group_refs(&mut ir, self.frame_depth == 0);
172
173
  ir.resolve_cycles();
@@ -1290,6 +1291,80 @@ impl XsdParser {
1290
1291
  }
1291
1292
  }
1292
1293
 
1294
+ /// Give XSD Gregorian primitives named, validated types in every target.
1295
+ /// The original primitive remains the base of each synthesized simple type.
1296
+ fn add_gregorian_types(ir: &mut SchemaIR) {
1297
+ const NS: &str = "urn:polyxml:builtins:gregorian";
1298
+ const TZ: &str = r"(?:Z|[+-](?:(?:0[0-9]|1[0-3]):[0-5][0-9]|14:00))?";
1299
+ fn pattern(kind: PrimitiveType) -> Option<String> {
1300
+ let body = match kind {
1301
+ PrimitiveType::GDay => r"---(?:0[1-9]|[12][0-9]|3[01])",
1302
+ PrimitiveType::GMonth => r"--(?:0[1-9]|1[0-2])",
1303
+ PrimitiveType::GYear => r"-?(?:[0-9]{4}|[1-9][0-9]{4,})",
1304
+ PrimitiveType::GYearMonth => r"-?(?:[0-9]{4}|[1-9][0-9]{4,})-(?:0[1-9]|1[0-2])",
1305
+ PrimitiveType::GMonthDay => {
1306
+ r"--(?:02-(?:0[1-9]|1[0-9]|2[0-9])|(?:04|06|09|11)-(?:0[1-9]|[12][0-9]|30)|(?:01|03|05|07|08|10|12)-(?:0[1-9]|[12][0-9]|3[01]))"
1307
+ }
1308
+ _ => return None,
1309
+ };
1310
+ Some(format!("{body}{TZ}"))
1311
+ }
1312
+ fn name(kind: PrimitiveType) -> &'static str {
1313
+ match kind {
1314
+ PrimitiveType::GDay => "GDay",
1315
+ PrimitiveType::GMonth => "GMonth",
1316
+ PrimitiveType::GYear => "GYear",
1317
+ PrimitiveType::GYearMonth => "GYearMonth",
1318
+ PrimitiveType::GMonthDay => "GMonthDay",
1319
+ _ => unreachable!(),
1320
+ }
1321
+ }
1322
+ fn rewrite(ty: &mut TypeRef, used: &mut HashSet<PrimitiveType>) {
1323
+ match ty {
1324
+ TypeRef::Primitive(kind) if pattern(*kind).is_some() => {
1325
+ used.insert(*kind);
1326
+ *ty = TypeRef::Named(QName::new(Some(NS), name(*kind)));
1327
+ }
1328
+ TypeRef::Boxed(inner) | TypeRef::List(inner) => rewrite(inner, used),
1329
+ _ => {}
1330
+ }
1331
+ }
1332
+ let mut used = HashSet::new();
1333
+ for element in ir.elements.values_mut() {
1334
+ rewrite(&mut element.type_ref, &mut used);
1335
+ }
1336
+ for def in ir.types.values_mut() {
1337
+ match def {
1338
+ TypeDef::Struct(s) => {
1339
+ for field in &mut s.fields {
1340
+ rewrite(&mut field.type_ref, &mut used);
1341
+ }
1342
+ }
1343
+ TypeDef::Enum(e) => rewrite(&mut e.base_type, &mut used),
1344
+ TypeDef::Union(u) => {
1345
+ for branch in &mut u.branches {
1346
+ rewrite(&mut branch.type_ref, &mut used);
1347
+ }
1348
+ }
1349
+ TypeDef::Simple(s) => rewrite(&mut s.base_type, &mut used),
1350
+ }
1351
+ }
1352
+ for kind in used {
1353
+ let qname = QName::new(Some(NS), name(kind));
1354
+ ir.types.entry(qname.clone()).or_insert_with(|| {
1355
+ TypeDef::Simple(Box::new(SimpleTypeDef {
1356
+ qname,
1357
+ base_type: TypeRef::Primitive(kind),
1358
+ facets: RestrictionFacets {
1359
+ patterns: vec![pattern(kind).unwrap()],
1360
+ ..Default::default()
1361
+ },
1362
+ documentation: Some("Validated W3C Gregorian partial date".into()),
1363
+ }))
1364
+ });
1365
+ }
1366
+ }
1367
+
1293
1368
  // Helpers
1294
1369
 
1295
1370
  fn strip_prefix(s: &str) -> &str {
@@ -5,8 +5,9 @@ use std::io::Cursor;
5
5
  use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
6
6
  use quick_xml::Writer;
7
7
 
8
+ use crate::converters::ValueConverter;
8
9
  use crate::error::{PolyXmlError, Result};
9
- use crate::schema::{FieldKind, ModelSchema, ValueType};
10
+ use crate::schema::{FieldKind, ModelSchema, ScalarType, ValueType};
10
11
  use crate::value::PolyValue;
11
12
 
12
13
  /// XML Schema Instance namespace, needed to write `xsi:type` selectors.
@@ -219,6 +220,13 @@ impl XmlSerializer {
219
220
  }
220
221
  }
221
222
 
223
+ fn validate_scalar(ty: &ValueType, text: &str, field_name: &str) -> Result<()> {
224
+ if let ValueType::Scalar(scalar @ ScalarType::XmlGregorian(_)) = ty {
225
+ ValueConverter::parse_scalar(scalar, text.as_bytes(), field_name)?;
226
+ }
227
+ Ok(())
228
+ }
229
+
222
230
  fn write_model<W: std::io::Write>(
223
231
  writer: &mut Writer<W>,
224
232
  tag_name: &[u8],
@@ -305,6 +313,7 @@ impl XmlSerializer {
305
313
  };
306
314
  let mut buf = [0u8; lexical_core::BUFFER_SIZE];
307
315
  if let Some(attr_str) = Self::format_scalar_to(val, &mut buf) {
316
+ Self::validate_scalar(&field.val_type, attr_str, &field.name)?;
308
317
  elem.push_attribute((attr_name.as_ref(), attr_str));
309
318
  }
310
319
  }
@@ -343,6 +352,7 @@ impl XmlSerializer {
343
352
  if let Some(val) = get_field(text_idx, &field.name) {
344
353
  let mut buf = [0u8; lexical_core::BUFFER_SIZE];
345
354
  if let Some(text_content) = Self::format_scalar_to(val, &mut buf) {
355
+ Self::validate_scalar(&field.val_type, text_content, &field.name)?;
346
356
  if !text_content.is_empty() {
347
357
  writer
348
358
  .write_event(Event::Text(BytesText::new(text_content)))
@@ -352,6 +362,9 @@ impl XmlSerializer {
352
362
  for item in items {
353
363
  let mut item_buf = [0u8; lexical_core::BUFFER_SIZE];
354
364
  if let Some(text_content) = Self::format_scalar_to(item, &mut item_buf) {
365
+ if let ValueType::List(inner) = &field.val_type {
366
+ Self::validate_scalar(inner, text_content, &field.name)?;
367
+ }
355
368
  if !text_content.is_empty() {
356
369
  writer
357
370
  .write_event(Event::Text(BytesText::new(text_content)))
@@ -377,6 +390,7 @@ impl XmlSerializer {
377
390
  ValueType::Scalar(_) => {
378
391
  let mut buf = [0u8; lexical_core::BUFFER_SIZE];
379
392
  if let Some(text) = Self::format_scalar_to(val, &mut buf) {
393
+ Self::validate_scalar(&field.val_type, text, &field.name)?;
380
394
  let local_child = std::str::from_utf8(&field.xml_name)?;
381
395
  let child_tag = if let Some(ctx) = ns_ctx {
382
396
  ctx.qualify_element(local_child, child_element_ns)
@@ -404,6 +418,7 @@ impl XmlSerializer {
404
418
  if let Some(text) =
405
419
  Self::format_scalar_to(item, &mut buf)
406
420
  {
421
+ Self::validate_scalar(inner, text, &field.name)?;
407
422
  let local_child =
408
423
  std::str::from_utf8(&field.xml_name)?;
409
424
  let child_tag = if let Some(ctx) = ns_ctx {
@@ -1,10 +1,59 @@
1
1
  use std::sync::Arc;
2
2
 
3
3
  use polyxml::error::PolyXmlError;
4
+ use polyxml::ir::PrimitiveType;
4
5
  use polyxml::schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType};
5
6
  use polyxml::value::PolyValue;
6
7
  use polyxml::{deserialize, serialize};
7
8
 
9
+ #[test]
10
+ fn test_gregorian_partial_date_lexical_validation() {
11
+ use polyxml::converters::ValueConverter;
12
+
13
+ for (kind, valid, invalid) in [
14
+ (PrimitiveType::GDay, "---15Z", "---32"),
15
+ (PrimitiveType::GMonth, "--04+14:00", "--04+14:01"),
16
+ (PrimitiveType::GYear, "-0045", "012345"),
17
+ (PrimitiveType::GYearMonth, "2026-09-05:30", "2026-13"),
18
+ (PrimitiveType::GMonthDay, "--02-29", "--02-30"),
19
+ ] {
20
+ let scalar = ScalarType::XmlGregorian(kind);
21
+ assert_eq!(
22
+ ValueConverter::parse_scalar(&scalar, valid.as_bytes(), "date").unwrap(),
23
+ PolyValue::String(valid.into())
24
+ );
25
+ assert!(ValueConverter::parse_scalar(&scalar, invalid.as_bytes(), "date").is_err());
26
+ }
27
+ }
28
+
29
+ #[test]
30
+ fn test_xsd_gregorian_fields_round_trip_and_reject_invalid_dates() {
31
+ let xsd = r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
32
+ <xs:complexType name="Calendar"><xs:sequence>
33
+ <xs:element name="day" type="xs:gDay"/>
34
+ <xs:element name="month" type="xs:gMonth"/>
35
+ <xs:element name="year" type="xs:gYear"/>
36
+ <xs:element name="yearMonth" type="xs:gYearMonth"/>
37
+ <xs:element name="monthDay" type="xs:gMonthDay"/>
38
+ </xs:sequence></xs:complexType>
39
+ <xs:element name="calendar" type="Calendar"/>
40
+ </xs:schema>"#;
41
+ let ir = polyxml::schema_parser::XsdParser::new()
42
+ .parse_str(xsd)
43
+ .unwrap();
44
+ assert_eq!(ir.types.len(), 6);
45
+ let schema = ModelSchema::from_ir(&ir, Some("calendar")).unwrap();
46
+ let xml = br#"<calendar><day>---15Z</day><month>--04+14:00</month><year>-0045</year><yearMonth>2026-09-05:30</yearMonth><monthDay>--02-29</monthDay></calendar>"#;
47
+ let value = deserialize(xml, Arc::clone(&schema)).unwrap();
48
+ let out = serialize("calendar", &value, &schema, None).unwrap();
49
+ assert_eq!(deserialize(&out, Arc::clone(&schema)).unwrap(), value);
50
+ let invalid = br#"<calendar><day>---32</day><month>--04</month><year>2026</year><yearMonth>2026-09</yearMonth><monthDay>--02-29</monthDay></calendar>"#;
51
+ assert!(deserialize(invalid, Arc::clone(&schema)).is_err());
52
+ let mut bad_value = std::collections::HashMap::new();
53
+ bad_value.insert("day".into(), PolyValue::String("---32".into()));
54
+ assert!(serialize("calendar", &PolyValue::Object(bad_value), &schema, None).is_err());
55
+ }
56
+
8
57
  #[test]
9
58
  fn test_all_scalar_types() {
10
59
  let schema = ModelSchema::builder("Sensors")
@@ -26,6 +26,59 @@ const SCHEMA: &str = r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
26
26
  <xs:element name="document" type="Document"/>
27
27
  </xs:schema>"#;
28
28
 
29
+ const GREGORIAN_SCHEMA: &str = r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
30
+ <xs:complexType name="Calendar"><xs:sequence>
31
+ <xs:element name="day" type="xs:gDay"/>
32
+ <xs:element name="month" type="xs:gMonth"/>
33
+ <xs:element name="year" type="xs:gYear"/>
34
+ <xs:element name="yearMonth" type="xs:gYearMonth"/>
35
+ <xs:element name="monthDay" type="xs:gMonthDay"/>
36
+ </xs:sequence></xs:complexType>
37
+ </xs:schema>"#;
38
+
39
+ #[test]
40
+ fn gregorian_primitives_emit_validated_types_across_targets() {
41
+ let ir = XsdParser::new().parse_str(GREGORIAN_SCHEMA).unwrap();
42
+ assert_eq!(ir.types.len(), 6);
43
+ let rust = RustCodegen::new(RustOptions::default()).generate_module(&ir);
44
+ assert!(rust.contains("validate_GDay_patterns"));
45
+ assert!(rust.contains("validate_GMonthDay_patterns"));
46
+ let go = GoCodegen::new(GoOptions::default()).generate_module(&ir);
47
+ assert!(go.contains("func (s GDay) Validate() error"));
48
+ assert!(go.contains("func (s *GMonthDay) UnmarshalText(text []byte) error"));
49
+ let cpp = CppCodegen::new(CppOptions::default()).generate_header(&ir);
50
+ assert!(cpp.contains("validate_GDay_patterns"));
51
+ let java = JavaCodegen::new(JavaOptions::default()).generate_module(&ir, "Models");
52
+ assert!(java.contains("Pattern.compile(\"---"));
53
+ let python = PythonCodegen::new(PythonOptions {
54
+ backend: PythonBackend::Pydantic,
55
+ ..Default::default()
56
+ })
57
+ .generate_module(&ir);
58
+ assert!(python.contains("type GDay = Annotated[str, Field(pattern="));
59
+ let ts = TypeScriptCodegen::new(TypeScriptOptions {
60
+ backend: TypeScriptBackend::Zod,
61
+ ..Default::default()
62
+ })
63
+ .generate_module(&ir);
64
+ assert!(ts.contains("GDaySchema = z.string().regex("));
65
+ let valibot = TypeScriptCodegen::new(TypeScriptOptions {
66
+ backend: TypeScriptBackend::Valibot,
67
+ ..Default::default()
68
+ })
69
+ .generate_module(&ir);
70
+ assert!(valibot.contains("GDaySchema = v.pipe(v.string(), v.regex("));
71
+ let typebox = TypeScriptCodegen::new(TypeScriptOptions {
72
+ backend: TypeScriptBackend::TypeBox,
73
+ ..Default::default()
74
+ })
75
+ .generate_module(&ir);
76
+ assert!(typebox.contains("GDaySchema = Type.String({ pattern:"));
77
+ let csharp = CSharpCodegen::new(CSharpOptions::default()).generate_module(&ir);
78
+ assert!(csharp.contains("record GDay("));
79
+ assert!(csharp.contains("Regex.IsMatch(Value.ToString()"));
80
+ }
81
+
29
82
  fn schema() -> SchemaIR {
30
83
  let dir = tempdir().unwrap();
31
84
  let path = dir.path().join("pattern.xsd");
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "polyxml"
7
- version = "0.26.0"
7
+ version = "0.27.0"
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" }
@@ -1,80 +0,0 @@
1
- use crate::error::{PolyXmlError, Result};
2
- use crate::schema::ScalarType;
3
- use crate::value::PolyValue;
4
-
5
- #[inline(always)]
6
- pub fn trim_bytes(mut b: &[u8]) -> &[u8] {
7
- while let Some((first, rest)) = b.split_first() {
8
- if first.is_ascii_whitespace() {
9
- b = rest;
10
- } else {
11
- break;
12
- }
13
- }
14
- while let Some((last, rest)) = b.split_last() {
15
- if last.is_ascii_whitespace() {
16
- b = rest;
17
- } else {
18
- break;
19
- }
20
- }
21
- b
22
- }
23
-
24
- pub struct ValueConverter;
25
-
26
- impl ValueConverter {
27
- pub fn parse_scalar(
28
- scalar_type: &ScalarType,
29
- bytes: &[u8],
30
- field_name: &str,
31
- ) -> Result<PolyValue> {
32
- match scalar_type {
33
- ScalarType::String => {
34
- let s = std::str::from_utf8(bytes)?;
35
- Ok(PolyValue::String(s.to_string()))
36
- }
37
- ScalarType::Int => {
38
- let trimmed = trim_bytes(bytes);
39
- let val: i64 =
40
- lexical_core::parse(trimmed).map_err(|_| PolyXmlError::ScalarParseError {
41
- field: field_name.to_string(),
42
- expected: "integer",
43
- value: String::from_utf8_lossy(bytes).to_string(),
44
- })?;
45
- Ok(PolyValue::Int(val))
46
- }
47
- ScalarType::Float => {
48
- let trimmed = trim_bytes(bytes);
49
- let val: f64 =
50
- lexical_core::parse(trimmed).map_err(|_| PolyXmlError::ScalarParseError {
51
- field: field_name.to_string(),
52
- expected: "float",
53
- value: String::from_utf8_lossy(bytes).to_string(),
54
- })?;
55
- Ok(PolyValue::Float(val))
56
- }
57
- ScalarType::Bool => {
58
- let trimmed = trim_bytes(bytes);
59
- match trimmed {
60
- b"true" | b"1" => Ok(PolyValue::Bool(true)),
61
- b"false" | b"0" => Ok(PolyValue::Bool(false)),
62
- _ => Err(PolyXmlError::ScalarParseError {
63
- field: field_name.to_string(),
64
- expected: "boolean ('true', 'false', '1', '0')",
65
- value: String::from_utf8_lossy(bytes).to_string(),
66
- }),
67
- }
68
- }
69
- ScalarType::Decimal
70
- | ScalarType::XmlDate
71
- | ScalarType::XmlDateTime
72
- | ScalarType::XmlTime
73
- | ScalarType::XmlDuration
74
- | ScalarType::Any => {
75
- let s = std::str::from_utf8(trim_bytes(bytes))?;
76
- Ok(PolyValue::String(s.to_string()))
77
- }
78
- }
79
- }
80
- }
File without changes