polyxml 0.10.0__tar.gz → 0.11.1__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 (26) hide show
  1. {polyxml-0.10.0 → polyxml-0.11.1}/Cargo.lock +4 -4
  2. {polyxml-0.10.0 → polyxml-0.11.1}/Cargo.toml +1 -1
  3. {polyxml-0.10.0 → polyxml-0.11.1}/PKG-INFO +1 -1
  4. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/src/parser.rs +27 -0
  5. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/src/schema.rs +8 -0
  6. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/src/serializer.rs +14 -2
  7. polyxml-0.11.1/crates/polyxml-core/tests/test_conformance.rs +493 -0
  8. polyxml-0.11.1/crates/polyxml-python/tests/test_conformance.py +422 -0
  9. {polyxml-0.10.0 → polyxml-0.11.1}/pyproject.toml +1 -1
  10. {polyxml-0.10.0 → polyxml-0.11.1}/README.md +0 -0
  11. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/Cargo.toml +0 -0
  12. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/README.md +0 -0
  13. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
  14. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/src/converters.rs +0 -0
  15. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/src/error.rs +0 -0
  16. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/src/lib.rs +0 -0
  17. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/src/value.rs +0 -0
  18. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/tests/test_core.rs +0 -0
  19. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/tests/test_security.rs +0 -0
  20. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-python/Cargo.toml +0 -0
  21. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-python/README.md +0 -0
  22. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-python/src/lib.rs +0 -0
  23. {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-python/tests/test_polyxml.py +0 -0
  24. {polyxml-0.10.0 → polyxml-0.11.1}/python/polyxml/AGENT_GUIDE.md +0 -0
  25. {polyxml-0.10.0 → polyxml-0.11.1}/python/polyxml/__init__.py +0 -0
  26. {polyxml-0.10.0 → polyxml-0.11.1}/python/polyxml/py.typed +0 -0
@@ -395,7 +395,7 @@ dependencies = [
395
395
 
396
396
  [[package]]
397
397
  name = "polyxml"
398
- version = "0.10.0"
398
+ version = "0.11.1"
399
399
  dependencies = [
400
400
  "criterion",
401
401
  "lexical-core",
@@ -408,14 +408,14 @@ dependencies = [
408
408
 
409
409
  [[package]]
410
410
  name = "polyxml-c"
411
- version = "0.10.0"
411
+ version = "0.11.1"
412
412
  dependencies = [
413
413
  "polyxml",
414
414
  ]
415
415
 
416
416
  [[package]]
417
417
  name = "polyxml-js"
418
- version = "0.10.0"
418
+ version = "0.11.1"
419
419
  dependencies = [
420
420
  "napi",
421
421
  "napi-build",
@@ -425,7 +425,7 @@ dependencies = [
425
425
 
426
426
  [[package]]
427
427
  name = "polyxml-python"
428
- version = "0.10.0"
428
+ version = "0.11.1"
429
429
  dependencies = [
430
430
  "lexical-core",
431
431
  "polyxml",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/polyxml-core", "crates/polyxml-python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.10.0"
6
+ version = "0.11.1"
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.10.0
3
+ Version: 0.11.1
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -189,12 +189,21 @@ impl XmlDeserializer {
189
189
  stack.push(root_frame);
190
190
 
191
191
  let mut active_scalar_field: Option<(usize, ScalarType, bool)> = None;
192
+ let mut unknown_depth: usize = 0;
192
193
  let mut text_buf: Vec<u8> = Vec::new();
193
194
  let mut buf = Vec::new();
194
195
 
195
196
  loop {
196
197
  match reader.read_event_into(&mut buf) {
197
198
  Ok(Event::Start(ref e)) => {
199
+ if unknown_depth > 0 {
200
+ unknown_depth += 1;
201
+ continue;
202
+ }
203
+ if active_scalar_field.is_some() {
204
+ unknown_depth = 1;
205
+ continue;
206
+ }
198
207
  if stack.len() >= max_depth {
199
208
  return Err(PolyXmlError::MaxDepthExceeded {
200
209
  max_depth,
@@ -245,9 +254,14 @@ impl XmlDeserializer {
245
254
  stack.push(frame);
246
255
  }
247
256
  }
257
+ } else {
258
+ unknown_depth = 1;
248
259
  }
249
260
  }
250
261
  Ok(Event::Empty(ref e)) => {
262
+ if unknown_depth > 0 || active_scalar_field.is_some() {
263
+ continue;
264
+ }
251
265
  let local_name = e.local_name();
252
266
  let is_nil = is_nil_element(e);
253
267
 
@@ -296,6 +310,9 @@ impl XmlDeserializer {
296
310
  }
297
311
  }
298
312
  Ok(Event::Text(ref e)) => {
313
+ if unknown_depth > 0 {
314
+ continue;
315
+ }
299
316
  let unescaped = quick_xml::escape::unescape(e.as_ref())?;
300
317
  if active_scalar_field.is_some() {
301
318
  text_buf.extend_from_slice(unescaped.as_bytes());
@@ -306,6 +323,9 @@ impl XmlDeserializer {
306
323
  }
307
324
  }
308
325
  Ok(Event::CData(ref e)) => {
326
+ if unknown_depth > 0 {
327
+ continue;
328
+ }
309
329
  if active_scalar_field.is_some() {
310
330
  text_buf.extend_from_slice(e.as_ref().as_bytes());
311
331
  } else if let Some(frame) = stack.last_mut() {
@@ -315,10 +335,17 @@ impl XmlDeserializer {
315
335
  }
316
336
  }
317
337
  Ok(Event::GeneralRef(ref e)) => {
338
+ if unknown_depth > 0 {
339
+ continue;
340
+ }
318
341
  let frame_tb = stack.last_mut().and_then(|f| f.frame_text_buf.as_mut());
319
342
  append_general_ref(e, active_scalar_field.is_some(), &mut text_buf, frame_tb)?;
320
343
  }
321
344
  Ok(Event::End(ref e)) => {
345
+ if unknown_depth > 0 {
346
+ unknown_depth -= 1;
347
+ continue;
348
+ }
322
349
  if let Some((field_idx, ref scalar_type, is_list)) = active_scalar_field.take()
323
350
  {
324
351
  let field_name = &stack.last().unwrap().schema.fields[field_idx].name;
@@ -61,6 +61,10 @@ impl FieldSchema {
61
61
  self
62
62
  }
63
63
 
64
+ pub fn with_namespace(self, ns: impl Into<String>) -> Self {
65
+ self.namespace(ns)
66
+ }
67
+
64
68
  pub fn required(mut self) -> Self {
65
69
  self.required = true;
66
70
  self
@@ -111,6 +115,10 @@ impl ModelSchemaBuilder {
111
115
  self
112
116
  }
113
117
 
118
+ pub fn with_namespace(self, ns: impl Into<String>) -> Self {
119
+ self.namespace(ns)
120
+ }
121
+
114
122
  pub fn field(mut self, field: FieldSchema) -> Self {
115
123
  self.fields.push(field);
116
124
  self
@@ -106,22 +106,34 @@ impl NamespaceContext {
106
106
  }
107
107
 
108
108
  pub fn qualify_element<'a>(&'a self, local_name: &'a str, ns: Option<&str>) -> Cow<'a, str> {
109
+ let clean_name = if let Some(pos) = local_name.find(':') {
110
+ &local_name[pos + 1..]
111
+ } else {
112
+ local_name
113
+ };
109
114
  if let Some(ns_uri) = ns {
110
115
  if let Some(prefix) = self.uri_to_prefix.get(ns_uri) {
111
116
  if !prefix.is_empty() {
112
- return Cow::Owned(format!("{}:{}", prefix, local_name));
117
+ return Cow::Owned(format!("{}:{}", prefix, clean_name));
113
118
  }
119
+ return Cow::Borrowed(clean_name);
114
120
  }
115
121
  }
116
122
  Cow::Borrowed(local_name)
117
123
  }
118
124
 
119
125
  pub fn qualify_attribute<'a>(&'a self, local_name: &'a str, ns: Option<&str>) -> Cow<'a, str> {
126
+ let clean_name = if let Some(pos) = local_name.find(':') {
127
+ &local_name[pos + 1..]
128
+ } else {
129
+ local_name
130
+ };
120
131
  if let Some(ns_uri) = ns {
121
132
  if let Some(prefix) = self.uri_to_prefix.get(ns_uri) {
122
133
  if !prefix.is_empty() {
123
- return Cow::Owned(format!("{}:{}", prefix, local_name));
134
+ return Cow::Owned(format!("{}:{}", prefix, clean_name));
124
135
  }
136
+ return Cow::Borrowed(clean_name);
125
137
  }
126
138
  }
127
139
  Cow::Borrowed(local_name)
@@ -0,0 +1,493 @@
1
+ use std::collections::HashMap;
2
+ use std::fs;
3
+ use std::io::BufReader;
4
+ use std::path::PathBuf;
5
+ use std::sync::Arc;
6
+
7
+ use polyxml::schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType};
8
+ use polyxml::value::PolyValue;
9
+ use polyxml::{deserialize, serialize_with_options, XmlItemStream};
10
+
11
+ fn fixture_path(name: &str) -> PathBuf {
12
+ PathBuf::from(env!("CARGO_MANIFEST_DIR"))
13
+ .join("../../tests/fixtures")
14
+ .join(name)
15
+ }
16
+
17
+ #[test]
18
+ fn test_soap_envelope_conformance() {
19
+ let path = fixture_path("soap_envelope.xml");
20
+ let xml_bytes = fs::read(&path).expect("Failed to read soap_envelope.xml");
21
+
22
+ let sec_token_schema = ModelSchema::builder("SecurityToken")
23
+ .with_namespace("http://example.com/security")
24
+ .field(
25
+ FieldSchema::new(
26
+ "must_understand",
27
+ b"env:mustUnderstand",
28
+ FieldKind::Attribute,
29
+ ValueType::Scalar(ScalarType::Bool),
30
+ )
31
+ .with_namespace("http://www.w3.org/2003/05/soap-envelope"),
32
+ )
33
+ .field(FieldSchema::new(
34
+ "token",
35
+ b"token",
36
+ FieldKind::Text,
37
+ ValueType::Scalar(ScalarType::String),
38
+ ))
39
+ .build();
40
+
41
+ let header_schema = ModelSchema::builder("Header")
42
+ .with_namespace("http://www.w3.org/2003/05/soap-envelope")
43
+ .field(FieldSchema::new(
44
+ "security_token",
45
+ b"auth:SecurityToken",
46
+ FieldKind::Element,
47
+ ValueType::Nested(Arc::clone(&sec_token_schema)),
48
+ ))
49
+ .build();
50
+
51
+ let stock_schema = ModelSchema::builder("GetStockPrice")
52
+ .with_namespace("http://example.com/stock")
53
+ .field(FieldSchema::new(
54
+ "stock_name",
55
+ b"m:StockName",
56
+ FieldKind::Element,
57
+ ValueType::Scalar(ScalarType::String),
58
+ ))
59
+ .field(FieldSchema::new(
60
+ "currency",
61
+ b"m:Currency",
62
+ FieldKind::Element,
63
+ ValueType::Scalar(ScalarType::String),
64
+ ))
65
+ .field(FieldSchema::new(
66
+ "target_date",
67
+ b"m:TargetDate",
68
+ FieldKind::Element,
69
+ ValueType::Scalar(ScalarType::XmlDate),
70
+ ))
71
+ .build();
72
+
73
+ let body_schema = ModelSchema::builder("Body")
74
+ .with_namespace("http://www.w3.org/2003/05/soap-envelope")
75
+ .field(FieldSchema::new(
76
+ "get_stock_price",
77
+ b"m:GetStockPrice",
78
+ FieldKind::Element,
79
+ ValueType::Nested(Arc::clone(&stock_schema)),
80
+ ))
81
+ .build();
82
+
83
+ let envelope_schema = ModelSchema::builder("Envelope")
84
+ .with_namespace("http://www.w3.org/2003/05/soap-envelope")
85
+ .field(FieldSchema::new(
86
+ "header",
87
+ b"env:Header",
88
+ FieldKind::Element,
89
+ ValueType::Nested(header_schema),
90
+ ))
91
+ .field(FieldSchema::new(
92
+ "body",
93
+ b"env:Body",
94
+ FieldKind::Element,
95
+ ValueType::Nested(body_schema),
96
+ ))
97
+ .build();
98
+
99
+ // 1. Deserialize
100
+ let val = deserialize(&xml_bytes, Arc::clone(&envelope_schema))
101
+ .expect("Failed to deserialize SOAP envelope");
102
+
103
+ let header = val.get("header").unwrap();
104
+ let sec = header.get("security_token").unwrap();
105
+ assert_eq!(sec.get("must_understand"), Some(&PolyValue::Bool(true)));
106
+ assert_eq!(
107
+ sec.get("token"),
108
+ Some(&PolyValue::String("sec-tok-987654321".into()))
109
+ );
110
+
111
+ let body = val.get("body").unwrap();
112
+ let req = body.get("get_stock_price").unwrap();
113
+ assert_eq!(
114
+ req.get("stock_name"),
115
+ Some(&PolyValue::String("ACME".into()))
116
+ );
117
+ assert_eq!(req.get("currency"), Some(&PolyValue::String("USD".into())));
118
+ assert_eq!(
119
+ req.get("target_date"),
120
+ Some(&PolyValue::String("2026-09-17".into()))
121
+ );
122
+
123
+ // 2. Roundtrip serialize with prefixes
124
+ let mut ns_map = HashMap::new();
125
+ ns_map.insert(
126
+ "http://www.w3.org/2003/05/soap-envelope".to_string(),
127
+ "env".to_string(),
128
+ );
129
+ ns_map.insert("http://example.com/stock".to_string(), "m".to_string());
130
+ ns_map.insert(
131
+ "http://example.com/security".to_string(),
132
+ "auth".to_string(),
133
+ );
134
+
135
+ let serialized = serialize_with_options(
136
+ "Envelope",
137
+ &val,
138
+ &envelope_schema,
139
+ Some(4),
140
+ Some(true),
141
+ Some(&ns_map),
142
+ )
143
+ .expect("Failed to serialize SOAP envelope");
144
+
145
+ let serialized_str = std::str::from_utf8(&serialized).unwrap();
146
+ assert!(serialized_str.contains("xmlns:env=\"http://www.w3.org/2003/05/soap-envelope\""));
147
+ assert!(serialized_str.contains("<env:Envelope"));
148
+ assert!(serialized_str.contains("<env:Header>"));
149
+ assert!(serialized_str.contains("<env:Body>"));
150
+
151
+ // 3. Re-deserialize to ensure semantic identity
152
+ let val_re = deserialize(&serialized, Arc::clone(&envelope_schema))
153
+ .expect("Failed to re-deserialize SOAP envelope");
154
+ assert_eq!(val, val_re);
155
+ }
156
+
157
+ #[test]
158
+ fn test_atom_feed_conformance() {
159
+ let path = fixture_path("atom_feed.xml");
160
+ let xml_bytes = fs::read(&path).expect("Failed to read atom_feed.xml");
161
+
162
+ let author_schema = ModelSchema::builder("author")
163
+ .with_namespace("http://www.w3.org/2005/Atom")
164
+ .field(FieldSchema::new(
165
+ "name",
166
+ b"name",
167
+ FieldKind::Element,
168
+ ValueType::Scalar(ScalarType::String),
169
+ ))
170
+ .field(FieldSchema::new(
171
+ "email",
172
+ b"email",
173
+ FieldKind::Element,
174
+ ValueType::Scalar(ScalarType::String),
175
+ ))
176
+ .build();
177
+
178
+ let entry_schema = ModelSchema::builder("entry")
179
+ .with_namespace("http://www.w3.org/2005/Atom")
180
+ .field(FieldSchema::new(
181
+ "title",
182
+ b"title",
183
+ FieldKind::Element,
184
+ ValueType::Scalar(ScalarType::String),
185
+ ))
186
+ .field(FieldSchema::new(
187
+ "id",
188
+ b"id",
189
+ FieldKind::Element,
190
+ ValueType::Scalar(ScalarType::String),
191
+ ))
192
+ .field(FieldSchema::new(
193
+ "updated",
194
+ b"updated",
195
+ FieldKind::Element,
196
+ ValueType::Scalar(ScalarType::XmlDateTime),
197
+ ))
198
+ .field(FieldSchema::new(
199
+ "summary",
200
+ b"summary",
201
+ FieldKind::Element,
202
+ ValueType::Scalar(ScalarType::String),
203
+ ))
204
+ .field(FieldSchema::new(
205
+ "author",
206
+ b"author",
207
+ FieldKind::Element,
208
+ ValueType::Nested(author_schema),
209
+ ))
210
+ .build();
211
+
212
+ let feed_schema = ModelSchema::builder("feed")
213
+ .with_namespace("http://www.w3.org/2005/Atom")
214
+ .field(FieldSchema::new(
215
+ "title",
216
+ b"title",
217
+ FieldKind::Element,
218
+ ValueType::Scalar(ScalarType::String),
219
+ ))
220
+ .field(FieldSchema::new(
221
+ "id",
222
+ b"id",
223
+ FieldKind::Element,
224
+ ValueType::Scalar(ScalarType::String),
225
+ ))
226
+ .field(FieldSchema::new(
227
+ "updated",
228
+ b"updated",
229
+ FieldKind::Element,
230
+ ValueType::Scalar(ScalarType::XmlDateTime),
231
+ ))
232
+ .field(FieldSchema::new(
233
+ "entries",
234
+ b"entry",
235
+ FieldKind::Element,
236
+ ValueType::List(Box::new(ValueType::Nested(entry_schema))),
237
+ ))
238
+ .build();
239
+
240
+ // 1. Deserialize
241
+ let val =
242
+ deserialize(&xml_bytes, Arc::clone(&feed_schema)).expect("Failed to deserialize Atom feed");
243
+
244
+ assert_eq!(
245
+ val.get("title"),
246
+ Some(&PolyValue::String("PolyXML Engineering Updates".into()))
247
+ );
248
+
249
+ if let Some(PolyValue::List(entries)) = val.get("entries") {
250
+ assert_eq!(entries.len(), 2);
251
+ assert_eq!(
252
+ entries[0].get("title"),
253
+ Some(&PolyValue::String("Release v0.10.0".into()))
254
+ );
255
+ let author = entries[0].get("author").unwrap();
256
+ assert_eq!(
257
+ author.get("name"),
258
+ Some(&PolyValue::String("Bailey Nguyen".into()))
259
+ );
260
+ } else {
261
+ panic!("Expected entries list");
262
+ }
263
+
264
+ // 2. Roundtrip serialize with default namespace
265
+ let mut ns_map = HashMap::new();
266
+ ns_map.insert("http://www.w3.org/2005/Atom".to_string(), "".to_string());
267
+
268
+ let serialized = serialize_with_options(
269
+ "feed",
270
+ &val,
271
+ &feed_schema,
272
+ Some(2),
273
+ Some(true),
274
+ Some(&ns_map),
275
+ )
276
+ .expect("Failed to serialize Atom feed");
277
+
278
+ let val_re = deserialize(&serialized, Arc::clone(&feed_schema))
279
+ .expect("Failed to re-deserialize Atom feed");
280
+ assert_eq!(val, val_re);
281
+ }
282
+
283
+ #[test]
284
+ fn test_ubl_invoice_conformance() {
285
+ let path = fixture_path("ubl_invoice.xml");
286
+ let xml_bytes = fs::read(&path).expect("Failed to read ubl_invoice.xml");
287
+
288
+ let party_schema = ModelSchema::builder("Party")
289
+ .with_namespace("urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2")
290
+ .field(FieldSchema::new(
291
+ "name",
292
+ b"cbc:RegistrationName",
293
+ FieldKind::Element,
294
+ ValueType::Scalar(ScalarType::String),
295
+ ))
296
+ .field(FieldSchema::new(
297
+ "company_id",
298
+ b"cbc:CompanyID",
299
+ FieldKind::Element,
300
+ ValueType::Scalar(ScalarType::String),
301
+ ))
302
+ .build();
303
+
304
+ let supplier_schema = ModelSchema::builder("AccountingSupplierParty")
305
+ .with_namespace("urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2")
306
+ .field(FieldSchema::new(
307
+ "party",
308
+ b"cac:Party",
309
+ FieldKind::Element,
310
+ ValueType::Nested(Arc::clone(&party_schema)),
311
+ ))
312
+ .build();
313
+
314
+ let total_schema = ModelSchema::builder("LegalMonetaryTotal")
315
+ .with_namespace("urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2")
316
+ .field(FieldSchema::new(
317
+ "payable_amount",
318
+ b"cbc:PayableAmount",
319
+ FieldKind::Element,
320
+ ValueType::Scalar(ScalarType::Float),
321
+ ))
322
+ .build();
323
+
324
+ let invoice_schema = ModelSchema::builder("Invoice")
325
+ .with_namespace("urn:oasis:names:specification:ubl:schema:xsd:Invoice-2")
326
+ .field(FieldSchema::new(
327
+ "id",
328
+ b"cbc:ID",
329
+ FieldKind::Element,
330
+ ValueType::Scalar(ScalarType::String),
331
+ ))
332
+ .field(FieldSchema::new(
333
+ "issue_date",
334
+ b"cbc:IssueDate",
335
+ FieldKind::Element,
336
+ ValueType::Scalar(ScalarType::XmlDate),
337
+ ))
338
+ .field(FieldSchema::new(
339
+ "currency",
340
+ b"cbc:DocumentCurrencyCode",
341
+ FieldKind::Element,
342
+ ValueType::Scalar(ScalarType::String),
343
+ ))
344
+ .field(FieldSchema::new(
345
+ "supplier",
346
+ b"cac:AccountingSupplierParty",
347
+ FieldKind::Element,
348
+ ValueType::Nested(supplier_schema),
349
+ ))
350
+ .field(FieldSchema::new(
351
+ "totals",
352
+ b"cac:LegalMonetaryTotal",
353
+ FieldKind::Element,
354
+ ValueType::Nested(total_schema),
355
+ ))
356
+ .build();
357
+
358
+ // 1. Deserialize
359
+ let val = deserialize(&xml_bytes, Arc::clone(&invoice_schema))
360
+ .expect("Failed to deserialize UBL Invoice");
361
+
362
+ assert_eq!(
363
+ val.get("id"),
364
+ Some(&PolyValue::String("INV-2026-0042".into()))
365
+ );
366
+ assert_eq!(
367
+ val.get("issue_date"),
368
+ Some(&PolyValue::String("2026-09-17".into()))
369
+ );
370
+ assert_eq!(val.get("currency"), Some(&PolyValue::String("USD".into())));
371
+
372
+ let supplier = val.get("supplier").unwrap();
373
+ let party = supplier.get("party").unwrap();
374
+ assert_eq!(
375
+ party.get("name"),
376
+ Some(&PolyValue::String("PolyXML Tech Inc.".into()))
377
+ );
378
+
379
+ let totals = val.get("totals").unwrap();
380
+ assert_eq!(
381
+ totals.get("payable_amount"),
382
+ Some(&PolyValue::Float(1620.0))
383
+ );
384
+
385
+ // 2. Roundtrip serialize
386
+ let mut ns_map = HashMap::new();
387
+ ns_map.insert(
388
+ "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2".to_string(),
389
+ "".to_string(),
390
+ );
391
+ ns_map.insert(
392
+ "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2".to_string(),
393
+ "cac".to_string(),
394
+ );
395
+ ns_map.insert(
396
+ "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2".to_string(),
397
+ "cbc".to_string(),
398
+ );
399
+
400
+ let serialized = serialize_with_options(
401
+ "Invoice",
402
+ &val,
403
+ &invoice_schema,
404
+ Some(2),
405
+ Some(true),
406
+ Some(&ns_map),
407
+ )
408
+ .expect("Failed to serialize UBL Invoice");
409
+
410
+ let val_re = deserialize(&serialized, Arc::clone(&invoice_schema))
411
+ .expect("Failed to re-deserialize UBL Invoice");
412
+ assert_eq!(val, val_re);
413
+ }
414
+
415
+ #[test]
416
+ fn test_streaming_catalog_conformance() {
417
+ let path = fixture_path("streaming_catalog.xml");
418
+ let file = fs::File::open(&path).expect("Failed to open streaming_catalog.xml");
419
+ let reader = BufReader::new(file);
420
+
421
+ let item_schema = ModelSchema::builder("Item")
422
+ .field(FieldSchema::new(
423
+ "id",
424
+ b"id",
425
+ FieldKind::Attribute,
426
+ ValueType::Scalar(ScalarType::Int),
427
+ ))
428
+ .field(FieldSchema::new(
429
+ "name",
430
+ b"Name",
431
+ FieldKind::Element,
432
+ ValueType::Scalar(ScalarType::String),
433
+ ))
434
+ .field(FieldSchema::new(
435
+ "price",
436
+ b"Price",
437
+ FieldKind::Element,
438
+ ValueType::Scalar(ScalarType::Float),
439
+ ))
440
+ .field(FieldSchema::new(
441
+ "in_stock",
442
+ b"InStock",
443
+ FieldKind::Element,
444
+ ValueType::Scalar(ScalarType::Bool),
445
+ ))
446
+ .build();
447
+
448
+ let mut stream = XmlItemStream::new(reader, Arc::clone(&item_schema), b"Item");
449
+ let mut count = 0;
450
+ let mut first_item = None;
451
+ let mut mid_item = None;
452
+ let mut last_item = None;
453
+
454
+ while let Some(item) = stream.next_item().expect("Failed to stream item") {
455
+ count += 1;
456
+ if count == 1 {
457
+ first_item = Some(item.clone());
458
+ } else if count == 500 {
459
+ mid_item = Some(item.clone());
460
+ } else if count == 1000 {
461
+ last_item = Some(item);
462
+ }
463
+ }
464
+
465
+ assert_eq!(count, 1000);
466
+
467
+ let item1 = first_item.unwrap();
468
+ assert_eq!(item1.get("id"), Some(&PolyValue::Int(1)));
469
+ assert_eq!(
470
+ item1.get("name"),
471
+ Some(&PolyValue::String("Widget #1".into()))
472
+ );
473
+ assert_eq!(item1.get("price"), Some(&PolyValue::Float(1.50)));
474
+ assert_eq!(item1.get("in_stock"), Some(&PolyValue::Bool(false)));
475
+
476
+ let item500 = mid_item.unwrap();
477
+ assert_eq!(item500.get("id"), Some(&PolyValue::Int(500)));
478
+ assert_eq!(
479
+ item500.get("name"),
480
+ Some(&PolyValue::String("Widget #500".into()))
481
+ );
482
+ assert_eq!(item500.get("price"), Some(&PolyValue::Float(750.0)));
483
+ assert_eq!(item500.get("in_stock"), Some(&PolyValue::Bool(true)));
484
+
485
+ let item1000 = last_item.unwrap();
486
+ assert_eq!(item1000.get("id"), Some(&PolyValue::Int(1000)));
487
+ assert_eq!(
488
+ item1000.get("name"),
489
+ Some(&PolyValue::String("Widget #1000".into()))
490
+ );
491
+ assert_eq!(item1000.get("price"), Some(&PolyValue::Float(1500.0)));
492
+ assert_eq!(item1000.get("in_stock"), Some(&PolyValue::Bool(true)));
493
+ }
@@ -0,0 +1,422 @@
1
+ import pathlib
2
+ from dataclasses import dataclass, field
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ import polyxml
7
+
8
+ FIXTURES_DIR = pathlib.Path(__file__).parents[3] / "tests" / "fixtures"
9
+
10
+
11
+ # -------------------------------------------------------------
12
+ # 1. SOAP 1.2 Envelope (Dataclasses)
13
+ # -------------------------------------------------------------
14
+
15
+
16
+ @dataclass
17
+ class SoapSecurityToken:
18
+ class Meta:
19
+ name = "SecurityToken"
20
+ namespace = "http://example.com/security"
21
+
22
+ token: str = field(
23
+ metadata={"type": "Text"},
24
+ default="",
25
+ )
26
+ must_understand: bool = field(
27
+ metadata={
28
+ "name": "mustUnderstand",
29
+ "type": "Attribute",
30
+ "namespace": "http://www.w3.org/2003/05/soap-envelope",
31
+ },
32
+ default=False,
33
+ )
34
+
35
+
36
+ @dataclass
37
+ class SoapHeader:
38
+ class Meta:
39
+ name = "Header"
40
+ namespace = "http://www.w3.org/2003/05/soap-envelope"
41
+
42
+ security_token: SoapSecurityToken = field(
43
+ metadata={
44
+ "name": "SecurityToken",
45
+ "type": "Element",
46
+ "namespace": "http://example.com/security",
47
+ },
48
+ default_factory=SoapSecurityToken,
49
+ )
50
+
51
+
52
+ @dataclass
53
+ class GetStockPrice:
54
+ class Meta:
55
+ name = "GetStockPrice"
56
+ namespace = "http://example.com/stock"
57
+
58
+ stock_name: str = field(
59
+ metadata={
60
+ "name": "StockName",
61
+ "type": "Element",
62
+ "namespace": "http://example.com/stock",
63
+ },
64
+ default="",
65
+ )
66
+ currency: str = field(
67
+ metadata={
68
+ "name": "Currency",
69
+ "type": "Element",
70
+ "namespace": "http://example.com/stock",
71
+ },
72
+ default="",
73
+ )
74
+ target_date: str = field(
75
+ metadata={
76
+ "name": "TargetDate",
77
+ "type": "Element",
78
+ "namespace": "http://example.com/stock",
79
+ },
80
+ default="",
81
+ )
82
+
83
+
84
+ @dataclass
85
+ class SoapBody:
86
+ class Meta:
87
+ name = "Body"
88
+ namespace = "http://www.w3.org/2003/05/soap-envelope"
89
+
90
+ get_stock_price: GetStockPrice = field(
91
+ metadata={
92
+ "name": "GetStockPrice",
93
+ "type": "Element",
94
+ "namespace": "http://example.com/stock",
95
+ },
96
+ default_factory=GetStockPrice,
97
+ )
98
+
99
+
100
+ @dataclass
101
+ class SoapEnvelope:
102
+ class Meta:
103
+ name = "Envelope"
104
+ namespace = "http://www.w3.org/2003/05/soap-envelope"
105
+
106
+ header: SoapHeader = field(
107
+ metadata={
108
+ "name": "Header",
109
+ "type": "Element",
110
+ "namespace": "http://www.w3.org/2003/05/soap-envelope",
111
+ },
112
+ default_factory=SoapHeader,
113
+ )
114
+ body: SoapBody = field(
115
+ metadata={
116
+ "name": "Body",
117
+ "type": "Element",
118
+ "namespace": "http://www.w3.org/2003/05/soap-envelope",
119
+ },
120
+ default_factory=SoapBody,
121
+ )
122
+
123
+
124
+ def test_soap_envelope_conformance():
125
+ xml_path = FIXTURES_DIR / "soap_envelope.xml"
126
+ xml_bytes = xml_path.read_bytes()
127
+
128
+ env = polyxml.deserialize(xml_bytes, SoapEnvelope)
129
+ assert env.header.security_token.must_understand is True
130
+ assert env.header.security_token.token.strip() == "sec-tok-987654321"
131
+ assert env.body.get_stock_price.stock_name == "ACME"
132
+ assert env.body.get_stock_price.currency == "USD"
133
+ assert env.body.get_stock_price.target_date == "2026-09-17"
134
+
135
+ # Roundtrip serialization with prefix mapping
136
+ ns_map = {
137
+ "env": "http://www.w3.org/2003/05/soap-envelope",
138
+ "m": "http://example.com/stock",
139
+ "auth": "http://example.com/security",
140
+ }
141
+ serialized = polyxml.serialize(env, indent=4, ns_map=ns_map)
142
+ xml_str = serialized.decode("utf-8")
143
+ assert "xmlns:env=\"http://www.w3.org/2003/05/soap-envelope\"" in xml_str
144
+ assert "<env:Envelope" in xml_str
145
+ assert "<env:Header>" in xml_str
146
+ assert "<auth:SecurityToken" in xml_str
147
+
148
+ re_env = polyxml.deserialize(serialized, SoapEnvelope)
149
+ assert re_env.body.get_stock_price.stock_name == "ACME"
150
+ assert re_env.header.security_token.must_understand is True
151
+
152
+
153
+ # -------------------------------------------------------------
154
+ # 2. Atom 1.0 Feed (Pydantic v2)
155
+ # -------------------------------------------------------------
156
+
157
+
158
+ class AtomAuthor(BaseModel):
159
+ class Meta:
160
+ name = "author"
161
+ namespace = "http://www.w3.org/2005/Atom"
162
+
163
+ name: str = Field(
164
+ json_schema_extra={"name": "name", "type": "Element", "namespace": "http://www.w3.org/2005/Atom"}
165
+ )
166
+ email: str = Field(
167
+ json_schema_extra={"name": "email", "type": "Element", "namespace": "http://www.w3.org/2005/Atom"}
168
+ )
169
+
170
+
171
+ class AtomEntry(BaseModel):
172
+ class Meta:
173
+ name = "entry"
174
+ namespace = "http://www.w3.org/2005/Atom"
175
+
176
+ title: str = Field(
177
+ json_schema_extra={"name": "title", "type": "Element", "namespace": "http://www.w3.org/2005/Atom"}
178
+ )
179
+ id: str = Field(
180
+ json_schema_extra={"name": "id", "type": "Element", "namespace": "http://www.w3.org/2005/Atom"}
181
+ )
182
+ updated: str = Field(
183
+ json_schema_extra={"name": "updated", "type": "Element", "namespace": "http://www.w3.org/2005/Atom"}
184
+ )
185
+ summary: str = Field(
186
+ json_schema_extra={"name": "summary", "type": "Element", "namespace": "http://www.w3.org/2005/Atom"}
187
+ )
188
+ author: AtomAuthor = Field(
189
+ json_schema_extra={"name": "author", "type": "Element", "namespace": "http://www.w3.org/2005/Atom"}
190
+ )
191
+
192
+
193
+ class AtomFeed(BaseModel):
194
+ class Meta:
195
+ name = "feed"
196
+ namespace = "http://www.w3.org/2005/Atom"
197
+
198
+ title: str = Field(
199
+ json_schema_extra={"name": "title", "type": "Element", "namespace": "http://www.w3.org/2005/Atom"}
200
+ )
201
+ id: str = Field(
202
+ json_schema_extra={"name": "id", "type": "Element", "namespace": "http://www.w3.org/2005/Atom"}
203
+ )
204
+ updated: str = Field(
205
+ json_schema_extra={"name": "updated", "type": "Element", "namespace": "http://www.w3.org/2005/Atom"}
206
+ )
207
+ entries: list[AtomEntry] = Field(
208
+ default_factory=list,
209
+ json_schema_extra={"name": "entry", "type": "Element", "namespace": "http://www.w3.org/2005/Atom"},
210
+ )
211
+
212
+
213
+ def test_atom_feed_conformance():
214
+ xml_path = FIXTURES_DIR / "atom_feed.xml"
215
+ xml_text = xml_path.read_text(encoding="utf-8")
216
+
217
+ feed = polyxml.deserialize(xml_text, AtomFeed)
218
+ assert feed.title == "PolyXML Engineering Updates"
219
+ assert len(feed.entries) == 2
220
+ assert feed.entries[0].title == "Release v0.10.0"
221
+ assert feed.entries[0].author.name == "Bailey Nguyen"
222
+ assert feed.entries[1].title == "Performance Milestone"
223
+ assert feed.entries[1].author.email == "core@polyxml.org"
224
+
225
+ # Roundtrip serialize with default namespace
226
+ ns_map = {None: "http://www.w3.org/2005/Atom"}
227
+ serialized = polyxml.serialize(feed, indent=2, ns_map=ns_map)
228
+ xml_str = serialized.decode("utf-8")
229
+ assert 'xmlns="http://www.w3.org/2005/Atom"' in xml_str
230
+ assert "<feed" in xml_str
231
+
232
+ re_feed = polyxml.deserialize(serialized, AtomFeed)
233
+ assert re_feed.title == feed.title
234
+ assert len(re_feed.entries) == 2
235
+
236
+
237
+ # -------------------------------------------------------------
238
+ # 3. OASIS UBL 2.1 Invoice (Dataclasses)
239
+ # -------------------------------------------------------------
240
+
241
+
242
+ @dataclass
243
+ class UblParty:
244
+ class Meta:
245
+ name = "Party"
246
+ namespace = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
247
+
248
+ name: str = field(
249
+ metadata={
250
+ "name": "RegistrationName",
251
+ "type": "Element",
252
+ "namespace": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
253
+ },
254
+ default="",
255
+ )
256
+ company_id: str = field(
257
+ metadata={
258
+ "name": "CompanyID",
259
+ "type": "Element",
260
+ "namespace": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
261
+ },
262
+ default="",
263
+ )
264
+
265
+
266
+ @dataclass
267
+ class UblSupplier:
268
+ class Meta:
269
+ name = "AccountingSupplierParty"
270
+ namespace = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
271
+
272
+ party: UblParty = field(
273
+ metadata={
274
+ "name": "Party",
275
+ "type": "Element",
276
+ "namespace": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
277
+ },
278
+ default_factory=UblParty,
279
+ )
280
+
281
+
282
+ @dataclass
283
+ class UblMonetaryTotal:
284
+ class Meta:
285
+ name = "LegalMonetaryTotal"
286
+ namespace = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
287
+
288
+ payable_amount: float = field(
289
+ metadata={
290
+ "name": "PayableAmount",
291
+ "type": "Element",
292
+ "namespace": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
293
+ },
294
+ default=0.0,
295
+ )
296
+
297
+
298
+ @dataclass
299
+ class UblInvoice:
300
+ class Meta:
301
+ name = "Invoice"
302
+ namespace = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
303
+
304
+ id: str = field(
305
+ metadata={
306
+ "name": "ID",
307
+ "type": "Element",
308
+ "namespace": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
309
+ },
310
+ default="",
311
+ )
312
+ issue_date: str = field(
313
+ metadata={
314
+ "name": "IssueDate",
315
+ "type": "Element",
316
+ "namespace": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
317
+ },
318
+ default="",
319
+ )
320
+ currency: str = field(
321
+ metadata={
322
+ "name": "DocumentCurrencyCode",
323
+ "type": "Element",
324
+ "namespace": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
325
+ },
326
+ default="",
327
+ )
328
+ supplier: UblSupplier = field(
329
+ metadata={
330
+ "name": "AccountingSupplierParty",
331
+ "type": "Element",
332
+ "namespace": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
333
+ },
334
+ default_factory=UblSupplier,
335
+ )
336
+ totals: UblMonetaryTotal = field(
337
+ metadata={
338
+ "name": "LegalMonetaryTotal",
339
+ "type": "Element",
340
+ "namespace": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
341
+ },
342
+ default_factory=UblMonetaryTotal,
343
+ )
344
+
345
+
346
+ def test_ubl_invoice_conformance():
347
+ xml_path = FIXTURES_DIR / "ubl_invoice.xml"
348
+ xml_bytes = xml_path.read_bytes()
349
+
350
+ inv = polyxml.deserialize(xml_bytes, UblInvoice)
351
+ assert inv.id == "INV-2026-0042"
352
+ assert inv.issue_date == "2026-09-17"
353
+ assert inv.currency == "USD"
354
+ assert inv.supplier.party.name == "PolyXML Tech Inc."
355
+ assert inv.supplier.party.company_id == "US-987654321"
356
+ assert inv.totals.payable_amount == 1620.0
357
+
358
+ # Roundtrip serialize
359
+ ns_map = {
360
+ None: "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2",
361
+ "cac": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
362
+ "cbc": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
363
+ }
364
+ serialized = polyxml.serialize(inv, indent=2, ns_map=ns_map)
365
+ xml_str = serialized.decode("utf-8")
366
+ assert 'xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"' in xml_str
367
+ assert 'xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"' in xml_str
368
+ assert 'xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"' in xml_str
369
+
370
+ re_inv = polyxml.deserialize(serialized, UblInvoice)
371
+ assert re_inv.id == inv.id
372
+ assert re_inv.totals.payable_amount == 1620.0
373
+
374
+
375
+ # -------------------------------------------------------------
376
+ # 4. Streaming Catalog Iterparse Conformance
377
+ # -------------------------------------------------------------
378
+
379
+
380
+ @dataclass
381
+ class CatalogItem:
382
+ id: int = field(metadata={"name": "id", "type": "Attribute"}, default=0)
383
+ name: str = field(metadata={"name": "Name", "type": "Element"}, default="")
384
+ price: float = field(metadata={"name": "Price", "type": "Element"}, default=0.0)
385
+ in_stock: bool = field(metadata={"name": "InStock", "type": "Element"}, default=False)
386
+
387
+
388
+ def test_streaming_catalog_iterparse_conformance():
389
+ xml_path = FIXTURES_DIR / "streaming_catalog.xml"
390
+
391
+ count = 0
392
+ first_item = None
393
+ mid_item = None
394
+ last_item = None
395
+
396
+ for item in polyxml.iterparse(xml_path, CatalogItem, tag="Item"):
397
+ count += 1
398
+ if count == 1:
399
+ first_item = item
400
+ elif count == 500:
401
+ mid_item = item
402
+ elif count == 1000:
403
+ last_item = item
404
+
405
+ assert count == 1000
406
+ assert first_item is not None
407
+ assert first_item.id == 1
408
+ assert first_item.name == "Widget #1"
409
+ assert first_item.price == 1.50
410
+ assert first_item.in_stock is False
411
+
412
+ assert mid_item is not None
413
+ assert mid_item.id == 500
414
+ assert mid_item.name == "Widget #500"
415
+ assert mid_item.price == 750.0
416
+ assert mid_item.in_stock is True
417
+
418
+ assert last_item is not None
419
+ assert last_item.id == 1000
420
+ assert last_item.name == "Widget #1000"
421
+ assert last_item.price == 1500.0
422
+ assert last_item.in_stock is True
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "polyxml"
7
- version = "0.10.0"
7
+ version = "0.11.1"
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