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.
- {polyxml-0.10.0 → polyxml-0.11.1}/Cargo.lock +4 -4
- {polyxml-0.10.0 → polyxml-0.11.1}/Cargo.toml +1 -1
- {polyxml-0.10.0 → polyxml-0.11.1}/PKG-INFO +1 -1
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/src/parser.rs +27 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/src/schema.rs +8 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/src/serializer.rs +14 -2
- polyxml-0.11.1/crates/polyxml-core/tests/test_conformance.rs +493 -0
- polyxml-0.11.1/crates/polyxml-python/tests/test_conformance.py +422 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/pyproject.toml +1 -1
- {polyxml-0.10.0 → polyxml-0.11.1}/README.md +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/Cargo.toml +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/README.md +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/src/converters.rs +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/src/error.rs +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/src/lib.rs +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/src/value.rs +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/tests/test_core.rs +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-core/tests/test_security.rs +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-python/Cargo.toml +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-python/README.md +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-python/src/lib.rs +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/crates/polyxml-python/tests/test_polyxml.py +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/python/polyxml/AGENT_GUIDE.md +0 -0
- {polyxml-0.10.0 → polyxml-0.11.1}/python/polyxml/__init__.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
428
|
+
version = "0.11.1"
|
|
429
429
|
dependencies = [
|
|
430
430
|
"lexical-core",
|
|
431
431
|
"polyxml",
|
|
@@ -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,
|
|
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,
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|