polyxml 0.8.1__tar.gz → 0.10.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.
- {polyxml-0.8.1 → polyxml-0.10.0}/Cargo.lock +8 -8
- {polyxml-0.8.1 → polyxml-0.10.0}/Cargo.toml +1 -1
- {polyxml-0.8.1 → polyxml-0.10.0}/PKG-INFO +1 -1
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/src/lib.rs +144 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/src/parser.rs +20 -2
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/src/schema.rs +29 -0
- polyxml-0.10.0/crates/polyxml-core/src/serializer.rs +411 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-python/src/lib.rs +77 -16
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-python/tests/test_polyxml.py +66 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/pyproject.toml +1 -1
- {polyxml-0.8.1 → polyxml-0.10.0}/python/polyxml/__init__.py +13 -2
- polyxml-0.8.1/crates/polyxml-core/src/serializer.rs +0 -200
- {polyxml-0.8.1 → polyxml-0.10.0}/README.md +0 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/Cargo.toml +0 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/README.md +0 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/src/converters.rs +0 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/src/error.rs +0 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/src/value.rs +0 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/tests/test_core.rs +0 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/tests/test_security.rs +0 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-python/Cargo.toml +0 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-python/README.md +0 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/python/polyxml/AGENT_GUIDE.md +0 -0
- {polyxml-0.8.1 → polyxml-0.10.0}/python/polyxml/py.typed +0 -0
|
@@ -320,9 +320,9 @@ dependencies = [
|
|
|
320
320
|
|
|
321
321
|
[[package]]
|
|
322
322
|
name = "napi-build"
|
|
323
|
-
version = "2.4.
|
|
323
|
+
version = "2.4.2"
|
|
324
324
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
325
|
-
checksum = "
|
|
325
|
+
checksum = "860e7c40864f95cfb83cde99f9ebadd88ef3d9bdccd7dd2cee0cc96a2dd4ffa7"
|
|
326
326
|
|
|
327
327
|
[[package]]
|
|
328
328
|
name = "napi-derive"
|
|
@@ -395,7 +395,7 @@ dependencies = [
|
|
|
395
395
|
|
|
396
396
|
[[package]]
|
|
397
397
|
name = "polyxml"
|
|
398
|
-
version = "0.
|
|
398
|
+
version = "0.10.0"
|
|
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.10.0"
|
|
412
412
|
dependencies = [
|
|
413
413
|
"polyxml",
|
|
414
414
|
]
|
|
415
415
|
|
|
416
416
|
[[package]]
|
|
417
417
|
name = "polyxml-js"
|
|
418
|
-
version = "0.
|
|
418
|
+
version = "0.10.0"
|
|
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.10.0"
|
|
429
429
|
dependencies = [
|
|
430
430
|
"lexical-core",
|
|
431
431
|
"polyxml",
|
|
@@ -629,9 +629,9 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
|
|
629
629
|
|
|
630
630
|
[[package]]
|
|
631
631
|
name = "smallvec"
|
|
632
|
-
version = "1.16.
|
|
632
|
+
version = "1.16.1"
|
|
633
633
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
634
|
-
checksum = "
|
|
634
|
+
checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
|
|
635
635
|
|
|
636
636
|
[[package]]
|
|
637
637
|
name = "syn"
|
|
@@ -37,6 +37,25 @@ pub fn serialize(
|
|
|
37
37
|
XmlSerializer::serialize(root_name, value, schema, indent)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/// High-level function to serialize a PolyValue with optional namespace support and custom prefix mapping.
|
|
41
|
+
pub fn serialize_with_options(
|
|
42
|
+
root_name: &str,
|
|
43
|
+
value: &PolyValue,
|
|
44
|
+
schema: &ModelSchema,
|
|
45
|
+
indent: Option<usize>,
|
|
46
|
+
enable_namespaces: Option<bool>,
|
|
47
|
+
ns_map: Option<&std::collections::HashMap<String, String>>,
|
|
48
|
+
) -> Result<Vec<u8>> {
|
|
49
|
+
XmlSerializer::serialize_with_options(
|
|
50
|
+
root_name,
|
|
51
|
+
value,
|
|
52
|
+
schema,
|
|
53
|
+
indent,
|
|
54
|
+
enable_namespaces,
|
|
55
|
+
ns_map,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
#[cfg(test)]
|
|
41
60
|
mod tests {
|
|
42
61
|
use super::*;
|
|
@@ -212,4 +231,129 @@ mod tests {
|
|
|
212
231
|
let item4 = stream.next_item().unwrap();
|
|
213
232
|
assert!(item4.is_none());
|
|
214
233
|
}
|
|
234
|
+
|
|
235
|
+
#[test]
|
|
236
|
+
fn test_namespaced_serialization() {
|
|
237
|
+
use std::collections::HashMap;
|
|
238
|
+
|
|
239
|
+
let schema = ModelSchema::builder("Item")
|
|
240
|
+
.xml_name(b"item")
|
|
241
|
+
.namespace("http://example.com/ns1")
|
|
242
|
+
.field(
|
|
243
|
+
FieldSchema::new(
|
|
244
|
+
"title",
|
|
245
|
+
b"title",
|
|
246
|
+
FieldKind::Element,
|
|
247
|
+
ValueType::Scalar(ScalarType::String),
|
|
248
|
+
)
|
|
249
|
+
.namespace("http://example.com/ns1"),
|
|
250
|
+
)
|
|
251
|
+
.field(
|
|
252
|
+
FieldSchema::new(
|
|
253
|
+
"sku",
|
|
254
|
+
b"sku",
|
|
255
|
+
FieldKind::Attribute,
|
|
256
|
+
ValueType::Scalar(ScalarType::String),
|
|
257
|
+
)
|
|
258
|
+
.namespace("http://example.com/ns2"),
|
|
259
|
+
)
|
|
260
|
+
.field(FieldSchema::new(
|
|
261
|
+
"unprefixed_attr",
|
|
262
|
+
b"unprefixed_attr",
|
|
263
|
+
FieldKind::Attribute,
|
|
264
|
+
ValueType::Scalar(ScalarType::Int),
|
|
265
|
+
))
|
|
266
|
+
.build();
|
|
267
|
+
|
|
268
|
+
let mut obj = HashMap::new();
|
|
269
|
+
obj.insert("title".into(), PolyValue::String("Laptop".into()));
|
|
270
|
+
obj.insert("sku".into(), PolyValue::String("SKU123".into()));
|
|
271
|
+
obj.insert("unprefixed_attr".into(), PolyValue::Int(42));
|
|
272
|
+
let poly_val = PolyValue::Object(obj);
|
|
273
|
+
|
|
274
|
+
// 1. Auto-detected namespaces
|
|
275
|
+
let xml_bytes = serialize("item", &poly_val, &schema, None).unwrap();
|
|
276
|
+
let xml_str = std::str::from_utf8(&xml_bytes).unwrap();
|
|
277
|
+
|
|
278
|
+
assert!(xml_str.contains("xmlns:ns0=\"http://example.com/ns1\""));
|
|
279
|
+
assert!(xml_str.contains("xmlns:ns1=\"http://example.com/ns2\""));
|
|
280
|
+
assert!(xml_str.starts_with("<ns0:item"));
|
|
281
|
+
assert!(xml_str.contains("ns1:sku=\"SKU123\""));
|
|
282
|
+
assert!(xml_str.contains("unprefixed_attr=\"42\""));
|
|
283
|
+
assert!(xml_str.contains("<ns0:title>Laptop</ns0:title>"));
|
|
284
|
+
assert!(xml_str.ends_with("</ns0:item>"));
|
|
285
|
+
|
|
286
|
+
// 2. Custom ns_map with default namespace for ns1
|
|
287
|
+
let mut custom_map = HashMap::new();
|
|
288
|
+
custom_map.insert("".into(), "http://example.com/ns1".into());
|
|
289
|
+
custom_map.insert("inv".into(), "http://example.com/ns2".into());
|
|
290
|
+
|
|
291
|
+
let custom_xml_bytes = serialize_with_options(
|
|
292
|
+
"item",
|
|
293
|
+
&poly_val,
|
|
294
|
+
&schema,
|
|
295
|
+
None,
|
|
296
|
+
Some(true),
|
|
297
|
+
Some(&custom_map),
|
|
298
|
+
)
|
|
299
|
+
.unwrap();
|
|
300
|
+
let custom_str = std::str::from_utf8(&custom_xml_bytes).unwrap();
|
|
301
|
+
|
|
302
|
+
assert!(custom_str.contains("xmlns=\"http://example.com/ns1\""));
|
|
303
|
+
assert!(custom_str.contains("xmlns:inv=\"http://example.com/ns2\""));
|
|
304
|
+
assert!(custom_str.starts_with("<item"));
|
|
305
|
+
assert!(custom_str.contains("inv:sku=\"SKU123\""));
|
|
306
|
+
assert!(custom_str.contains("<title>Laptop</title>"));
|
|
307
|
+
assert!(custom_str.ends_with("</item>"));
|
|
308
|
+
|
|
309
|
+
// 3. Explicitly disabled namespaces (fast-path)
|
|
310
|
+
let raw_xml_bytes =
|
|
311
|
+
serialize_with_options("item", &poly_val, &schema, None, Some(false), None).unwrap();
|
|
312
|
+
let raw_str = std::str::from_utf8(&raw_xml_bytes).unwrap();
|
|
313
|
+
assert!(!raw_str.contains("xmlns"));
|
|
314
|
+
assert!(raw_str.starts_with("<item"));
|
|
315
|
+
assert!(raw_str.contains("sku=\"SKU123\""));
|
|
316
|
+
assert!(raw_str.contains("<title>Laptop</title>"));
|
|
317
|
+
|
|
318
|
+
// 4. Verify deserialization roundtrip ignores xmlns attributes and handles prefixed elements
|
|
319
|
+
let roundtrip_val = deserialize(&xml_bytes, Arc::clone(&schema)).unwrap();
|
|
320
|
+
assert_eq!(
|
|
321
|
+
roundtrip_val.get("title"),
|
|
322
|
+
Some(&PolyValue::String("Laptop".into()))
|
|
323
|
+
);
|
|
324
|
+
assert_eq!(
|
|
325
|
+
roundtrip_val.get("sku"),
|
|
326
|
+
Some(&PolyValue::String("SKU123".into()))
|
|
327
|
+
);
|
|
328
|
+
assert_eq!(
|
|
329
|
+
roundtrip_val.get("unprefixed_attr"),
|
|
330
|
+
Some(&PolyValue::Int(42))
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
#[test]
|
|
335
|
+
fn test_deserialization_does_not_confuse_xmlns_with_attributes() {
|
|
336
|
+
// Schema has an attribute named "prefix"
|
|
337
|
+
let schema = ModelSchema::builder("Item")
|
|
338
|
+
.field(FieldSchema::new(
|
|
339
|
+
"prefix",
|
|
340
|
+
b"prefix",
|
|
341
|
+
FieldKind::Attribute,
|
|
342
|
+
ValueType::Scalar(ScalarType::String),
|
|
343
|
+
))
|
|
344
|
+
.build();
|
|
345
|
+
|
|
346
|
+
// XML defines xmlns:prefix="http://example.com" and an actual attribute prefix="actual_val"
|
|
347
|
+
let xml = br#"<Item xmlns:prefix="http://example.com" prefix="actual_val" />"#;
|
|
348
|
+
let val = deserialize(xml, Arc::clone(&schema)).unwrap();
|
|
349
|
+
assert_eq!(
|
|
350
|
+
val.get("prefix"),
|
|
351
|
+
Some(&PolyValue::String("actual_val".into()))
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// XML defines xmlns:prefix="http://example.com" but NO actual attribute prefix
|
|
355
|
+
let xml2 = br#"<Item xmlns:prefix="http://example.com" />"#;
|
|
356
|
+
let val2 = deserialize(xml2, Arc::clone(&schema)).unwrap();
|
|
357
|
+
assert_eq!(val2.get("prefix"), None);
|
|
358
|
+
}
|
|
215
359
|
}
|
|
@@ -94,6 +94,10 @@ pub struct XmlDeserializer;
|
|
|
94
94
|
|
|
95
95
|
fn is_nil_element(e: &BytesStart) -> bool {
|
|
96
96
|
for attr in e.attributes().flatten() {
|
|
97
|
+
let raw_key = attr.key.as_ref();
|
|
98
|
+
if raw_key == "xmlns" || raw_key.starts_with("xmlns:") {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
97
101
|
let key = attr.key.local_name();
|
|
98
102
|
if (key.as_ref() == "nil" || key.as_ref() == "xsi:nil")
|
|
99
103
|
&& (attr.value.as_ref() == "true" || attr.value.as_ref() == "1")
|
|
@@ -368,6 +372,10 @@ impl XmlDeserializer {
|
|
|
368
372
|
|
|
369
373
|
pub(crate) fn parse_attributes(e: &BytesStart, frame: &mut StackFrame) -> Result<()> {
|
|
370
374
|
for attr in e.attributes().flatten() {
|
|
375
|
+
let raw_key = attr.key.as_ref();
|
|
376
|
+
if raw_key == "xmlns" || raw_key.starts_with("xmlns:") {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
371
379
|
let key = attr.key.local_name();
|
|
372
380
|
if let Some(&field_idx) = frame.schema.attribute_map.get(key.as_ref().as_bytes()) {
|
|
373
381
|
let field = &frame.schema.fields[field_idx];
|
|
@@ -407,12 +415,20 @@ impl<R: std::io::BufRead> XmlItemStream<R> {
|
|
|
407
415
|
}
|
|
408
416
|
|
|
409
417
|
pub fn next_item(&mut self) -> Result<Option<PolyValue>> {
|
|
418
|
+
let target_local = if let Some(pos) = self.target_tag.iter().position(|&b| b == b':') {
|
|
419
|
+
&self.target_tag[pos + 1..]
|
|
420
|
+
} else {
|
|
421
|
+
self.target_tag.as_slice()
|
|
422
|
+
};
|
|
423
|
+
|
|
410
424
|
loop {
|
|
411
425
|
self.buf.clear();
|
|
412
426
|
match self.reader.read_event_into(&mut self.buf) {
|
|
413
427
|
Ok(Event::Start(ref e)) => {
|
|
414
428
|
let local = e.local_name();
|
|
415
|
-
if local.as_ref().as_bytes() ==
|
|
429
|
+
if local.as_ref().as_bytes() == target_local
|
|
430
|
+
|| e.name().as_ref().as_bytes() == self.target_tag.as_slice()
|
|
431
|
+
{
|
|
416
432
|
let item = XmlDeserializer::parse_sub_tree(
|
|
417
433
|
&mut self.reader,
|
|
418
434
|
Arc::clone(&self.schema),
|
|
@@ -424,7 +440,9 @@ impl<R: std::io::BufRead> XmlItemStream<R> {
|
|
|
424
440
|
}
|
|
425
441
|
Ok(Event::Empty(ref e)) => {
|
|
426
442
|
let local = e.local_name();
|
|
427
|
-
if local.as_ref().as_bytes() ==
|
|
443
|
+
if local.as_ref().as_bytes() == target_local
|
|
444
|
+
|| e.name().as_ref().as_bytes() == self.target_tag.as_slice()
|
|
445
|
+
{
|
|
428
446
|
let mut frame = StackFrame::new(Arc::clone(&self.schema));
|
|
429
447
|
XmlDeserializer::parse_attributes(e, &mut frame)?;
|
|
430
448
|
return Ok(Some(frame.finish()?));
|
|
@@ -33,6 +33,7 @@ pub enum ValueType {
|
|
|
33
33
|
pub struct FieldSchema {
|
|
34
34
|
pub name: String,
|
|
35
35
|
pub xml_name: Vec<u8>,
|
|
36
|
+
pub namespace: Option<String>,
|
|
36
37
|
pub kind: FieldKind,
|
|
37
38
|
pub val_type: ValueType,
|
|
38
39
|
pub required: bool,
|
|
@@ -48,12 +49,18 @@ impl FieldSchema {
|
|
|
48
49
|
Self {
|
|
49
50
|
name: name.into(),
|
|
50
51
|
xml_name: xml_name.to_vec(),
|
|
52
|
+
namespace: None,
|
|
51
53
|
kind,
|
|
52
54
|
val_type,
|
|
53
55
|
required: false,
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
pub fn namespace(mut self, ns: impl Into<String>) -> Self {
|
|
60
|
+
self.namespace = Some(ns.into());
|
|
61
|
+
self
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
pub fn required(mut self) -> Self {
|
|
58
65
|
self.required = true;
|
|
59
66
|
self
|
|
@@ -63,6 +70,8 @@ impl FieldSchema {
|
|
|
63
70
|
#[derive(Debug, Clone)]
|
|
64
71
|
pub struct ModelSchema {
|
|
65
72
|
pub name: String,
|
|
73
|
+
pub xml_name: Vec<u8>,
|
|
74
|
+
pub namespace: Option<String>,
|
|
66
75
|
pub fields: Vec<FieldSchema>,
|
|
67
76
|
pub element_map: HashMap<Vec<u8>, usize>,
|
|
68
77
|
pub attribute_map: HashMap<Vec<u8>, usize>,
|
|
@@ -77,6 +86,8 @@ impl ModelSchema {
|
|
|
77
86
|
|
|
78
87
|
pub struct ModelSchemaBuilder {
|
|
79
88
|
name: String,
|
|
89
|
+
xml_name: Option<Vec<u8>>,
|
|
90
|
+
namespace: Option<String>,
|
|
80
91
|
fields: Vec<FieldSchema>,
|
|
81
92
|
}
|
|
82
93
|
|
|
@@ -84,10 +95,22 @@ impl ModelSchemaBuilder {
|
|
|
84
95
|
pub fn new(name: impl Into<String>) -> Self {
|
|
85
96
|
Self {
|
|
86
97
|
name: name.into(),
|
|
98
|
+
xml_name: None,
|
|
99
|
+
namespace: None,
|
|
87
100
|
fields: Vec::new(),
|
|
88
101
|
}
|
|
89
102
|
}
|
|
90
103
|
|
|
104
|
+
pub fn xml_name(mut self, xml_name: &[u8]) -> Self {
|
|
105
|
+
self.xml_name = Some(xml_name.to_vec());
|
|
106
|
+
self
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pub fn namespace(mut self, ns: impl Into<String>) -> Self {
|
|
110
|
+
self.namespace = Some(ns.into());
|
|
111
|
+
self
|
|
112
|
+
}
|
|
113
|
+
|
|
91
114
|
pub fn field(mut self, field: FieldSchema) -> Self {
|
|
92
115
|
self.fields.push(field);
|
|
93
116
|
self
|
|
@@ -119,8 +142,14 @@ impl ModelSchemaBuilder {
|
|
|
119
142
|
}
|
|
120
143
|
}
|
|
121
144
|
|
|
145
|
+
let xml_name = self
|
|
146
|
+
.xml_name
|
|
147
|
+
.unwrap_or_else(|| self.name.as_bytes().to_vec());
|
|
148
|
+
|
|
122
149
|
Arc::new(ModelSchema {
|
|
123
150
|
name: self.name,
|
|
151
|
+
xml_name,
|
|
152
|
+
namespace: self.namespace,
|
|
124
153
|
fields: self.fields,
|
|
125
154
|
element_map,
|
|
126
155
|
attribute_map,
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
use std::borrow::Cow;
|
|
2
|
+
use std::collections::HashMap;
|
|
3
|
+
use std::io::Cursor;
|
|
4
|
+
|
|
5
|
+
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
|
|
6
|
+
use quick_xml::Writer;
|
|
7
|
+
|
|
8
|
+
use crate::error::{PolyXmlError, Result};
|
|
9
|
+
use crate::schema::{FieldKind, ModelSchema, ValueType};
|
|
10
|
+
use crate::value::PolyValue;
|
|
11
|
+
|
|
12
|
+
#[derive(Debug, Clone)]
|
|
13
|
+
pub struct NamespaceContext {
|
|
14
|
+
pub uri_to_prefix: HashMap<String, String>,
|
|
15
|
+
pub prefix_to_uri: Vec<(String, String)>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
impl NamespaceContext {
|
|
19
|
+
pub fn build(
|
|
20
|
+
root_schema: &ModelSchema,
|
|
21
|
+
user_ns_map: Option<&HashMap<String, String>>,
|
|
22
|
+
) -> Option<Self> {
|
|
23
|
+
let mut uris = Vec::new();
|
|
24
|
+
Self::collect_namespaces(root_schema, &mut uris);
|
|
25
|
+
|
|
26
|
+
if uris.is_empty() && user_ns_map.is_none_or(|m| m.is_empty()) {
|
|
27
|
+
return None;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let mut uri_to_prefix = HashMap::new();
|
|
31
|
+
let mut prefix_to_uri = Vec::new();
|
|
32
|
+
|
|
33
|
+
// 1. Process user ns_map
|
|
34
|
+
if let Some(map) = user_ns_map {
|
|
35
|
+
for (k, v) in map {
|
|
36
|
+
let (prefix, uri) = if v.contains("://") || v.starts_with("urn:") {
|
|
37
|
+
(k.clone(), v.clone())
|
|
38
|
+
} else if k.contains("://") || k.starts_with("urn:") {
|
|
39
|
+
(v.clone(), k.clone())
|
|
40
|
+
} else {
|
|
41
|
+
(k.clone(), v.clone())
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let clean_prefix = if prefix == "None" {
|
|
45
|
+
String::new()
|
|
46
|
+
} else {
|
|
47
|
+
prefix
|
|
48
|
+
};
|
|
49
|
+
if !uri.is_empty() && !uri_to_prefix.contains_key(&uri) {
|
|
50
|
+
uri_to_prefix.insert(uri.clone(), clean_prefix.clone());
|
|
51
|
+
prefix_to_uri.push((clean_prefix, uri));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. Assign prefixes for remaining schema URIs
|
|
57
|
+
let mut ns_counter = 0;
|
|
58
|
+
for uri in uris {
|
|
59
|
+
if !uri_to_prefix.contains_key(&uri) {
|
|
60
|
+
let prefix = match uri.as_str() {
|
|
61
|
+
"http://www.w3.org/2001/XMLSchema-instance" => "xsi".to_string(),
|
|
62
|
+
"http://www.w3.org/2001/XMLSchema" => "xs".to_string(),
|
|
63
|
+
"http://www.w3.org/XML/1998/namespace" => "xml".to_string(),
|
|
64
|
+
_ => loop {
|
|
65
|
+
let candidate = format!("ns{}", ns_counter);
|
|
66
|
+
ns_counter += 1;
|
|
67
|
+
if !prefix_to_uri.iter().any(|(p, _)| p == &candidate) {
|
|
68
|
+
break candidate;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
uri_to_prefix.insert(uri.clone(), prefix.clone());
|
|
74
|
+
prefix_to_uri.push((prefix, uri));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Some(Self {
|
|
79
|
+
uri_to_prefix,
|
|
80
|
+
prefix_to_uri,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn collect_namespaces(schema: &ModelSchema, uris: &mut Vec<String>) {
|
|
85
|
+
if let Some(ref ns) = schema.namespace {
|
|
86
|
+
if !ns.is_empty() && !uris.contains(ns) {
|
|
87
|
+
uris.push(ns.clone());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for field in &schema.fields {
|
|
91
|
+
if let Some(ref ns) = field.namespace {
|
|
92
|
+
if !ns.is_empty() && !uris.contains(ns) {
|
|
93
|
+
uris.push(ns.clone());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
match &field.val_type {
|
|
97
|
+
ValueType::Nested(nested) => Self::collect_namespaces(nested, uris),
|
|
98
|
+
ValueType::List(inner) => {
|
|
99
|
+
if let ValueType::Nested(nested) = inner.as_ref() {
|
|
100
|
+
Self::collect_namespaces(nested, uris);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
_ => {}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pub fn qualify_element<'a>(&'a self, local_name: &'a str, ns: Option<&str>) -> Cow<'a, str> {
|
|
109
|
+
if let Some(ns_uri) = ns {
|
|
110
|
+
if let Some(prefix) = self.uri_to_prefix.get(ns_uri) {
|
|
111
|
+
if !prefix.is_empty() {
|
|
112
|
+
return Cow::Owned(format!("{}:{}", prefix, local_name));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
Cow::Borrowed(local_name)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pub fn qualify_attribute<'a>(&'a self, local_name: &'a str, ns: Option<&str>) -> Cow<'a, str> {
|
|
120
|
+
if let Some(ns_uri) = ns {
|
|
121
|
+
if let Some(prefix) = self.uri_to_prefix.get(ns_uri) {
|
|
122
|
+
if !prefix.is_empty() {
|
|
123
|
+
return Cow::Owned(format!("{}:{}", prefix, local_name));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
Cow::Borrowed(local_name)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
pub struct XmlSerializer;
|
|
132
|
+
|
|
133
|
+
impl XmlSerializer {
|
|
134
|
+
pub fn serialize(
|
|
135
|
+
root_name: &str,
|
|
136
|
+
value: &PolyValue,
|
|
137
|
+
schema: &ModelSchema,
|
|
138
|
+
indent: Option<usize>,
|
|
139
|
+
) -> Result<Vec<u8>> {
|
|
140
|
+
Self::serialize_with_options(root_name, value, schema, indent, None, None)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
pub fn serialize_with_options(
|
|
144
|
+
root_name: &str,
|
|
145
|
+
value: &PolyValue,
|
|
146
|
+
schema: &ModelSchema,
|
|
147
|
+
indent: Option<usize>,
|
|
148
|
+
enable_namespaces: Option<bool>,
|
|
149
|
+
ns_map: Option<&HashMap<String, String>>,
|
|
150
|
+
) -> Result<Vec<u8>> {
|
|
151
|
+
let mut buffer = Cursor::new(Vec::with_capacity(512));
|
|
152
|
+
let mut writer = match indent {
|
|
153
|
+
Some(spaces) => Writer::new_with_indent(&mut buffer, b' ', spaces),
|
|
154
|
+
None => Writer::new(&mut buffer),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
let ns_ctx = match enable_namespaces {
|
|
158
|
+
Some(false) => None,
|
|
159
|
+
_ => NamespaceContext::build(schema, ns_map),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
Self::write_model(
|
|
163
|
+
&mut writer,
|
|
164
|
+
root_name.as_bytes(),
|
|
165
|
+
value,
|
|
166
|
+
schema,
|
|
167
|
+
ns_ctx.as_ref(),
|
|
168
|
+
true,
|
|
169
|
+
schema.namespace.as_deref(),
|
|
170
|
+
)?;
|
|
171
|
+
|
|
172
|
+
Ok(buffer.into_inner())
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fn format_scalar_to<'a>(
|
|
176
|
+
val: &'a PolyValue,
|
|
177
|
+
buf: &'a mut [u8; lexical_core::BUFFER_SIZE],
|
|
178
|
+
) -> Option<&'a str> {
|
|
179
|
+
match val {
|
|
180
|
+
PolyValue::String(s) => Some(s.as_str()),
|
|
181
|
+
PolyValue::Int(i) => {
|
|
182
|
+
let bytes = lexical_core::write(*i, buf);
|
|
183
|
+
std::str::from_utf8(bytes).ok()
|
|
184
|
+
}
|
|
185
|
+
PolyValue::Float(f) => {
|
|
186
|
+
let bytes = lexical_core::write(*f, buf);
|
|
187
|
+
std::str::from_utf8(bytes).ok()
|
|
188
|
+
}
|
|
189
|
+
PolyValue::Bool(b) => Some(if *b { "true" } else { "false" }),
|
|
190
|
+
_ => None,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn write_model<W: std::io::Write>(
|
|
195
|
+
writer: &mut Writer<W>,
|
|
196
|
+
tag_name: &[u8],
|
|
197
|
+
value: &PolyValue,
|
|
198
|
+
schema: &ModelSchema,
|
|
199
|
+
ns_ctx: Option<&NamespaceContext>,
|
|
200
|
+
is_root: bool,
|
|
201
|
+
element_ns: Option<&str>,
|
|
202
|
+
) -> Result<()> {
|
|
203
|
+
let obj = match value {
|
|
204
|
+
PolyValue::Object(o) => o,
|
|
205
|
+
_ => {
|
|
206
|
+
return Err(PolyXmlError::SerializationError(
|
|
207
|
+
"Expected Object value for model".into(),
|
|
208
|
+
))
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
let local_tag = std::str::from_utf8(tag_name)?;
|
|
213
|
+
let qualified_tag = if let Some(ctx) = ns_ctx {
|
|
214
|
+
ctx.qualify_element(local_tag, element_ns)
|
|
215
|
+
} else {
|
|
216
|
+
Cow::Borrowed(local_tag)
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
let mut elem = BytesStart::new(qualified_tag.as_ref());
|
|
220
|
+
|
|
221
|
+
// Emit xmlns declarations on root element
|
|
222
|
+
if is_root {
|
|
223
|
+
if let Some(ctx) = ns_ctx {
|
|
224
|
+
for (prefix, uri) in &ctx.prefix_to_uri {
|
|
225
|
+
if prefix.is_empty() {
|
|
226
|
+
elem.push_attribute(("xmlns", uri.as_str()));
|
|
227
|
+
} else {
|
|
228
|
+
let attr_name = format!("xmlns:{}", prefix);
|
|
229
|
+
elem.push_attribute((attr_name.as_str(), uri.as_str()));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 1. Collect and write attributes
|
|
236
|
+
for field in &schema.fields {
|
|
237
|
+
if field.kind == FieldKind::Attribute {
|
|
238
|
+
if let Some(val) = obj.get(&field.name) {
|
|
239
|
+
if !val.is_null() {
|
|
240
|
+
let local_attr = std::str::from_utf8(&field.xml_name)?;
|
|
241
|
+
let attr_name = if let Some(ctx) = ns_ctx {
|
|
242
|
+
ctx.qualify_attribute(local_attr, field.namespace.as_deref())
|
|
243
|
+
} else {
|
|
244
|
+
Cow::Borrowed(local_attr)
|
|
245
|
+
};
|
|
246
|
+
let mut buf = [0u8; lexical_core::BUFFER_SIZE];
|
|
247
|
+
if let Some(attr_str) = Self::format_scalar_to(val, &mut buf) {
|
|
248
|
+
elem.push_attribute((attr_name.as_ref(), attr_str));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
writer
|
|
256
|
+
.write_event(Event::Start(elem))
|
|
257
|
+
.map_err(|e| PolyXmlError::SerializationError(e.to_string()))?;
|
|
258
|
+
|
|
259
|
+
// 2. Write text content if present
|
|
260
|
+
if let Some(text_idx) = schema.text_field {
|
|
261
|
+
let field = &schema.fields[text_idx];
|
|
262
|
+
if let Some(val) = obj.get(&field.name) {
|
|
263
|
+
let mut buf = [0u8; lexical_core::BUFFER_SIZE];
|
|
264
|
+
if let Some(text_content) = Self::format_scalar_to(val, &mut buf) {
|
|
265
|
+
if !text_content.is_empty() {
|
|
266
|
+
writer
|
|
267
|
+
.write_event(Event::Text(BytesText::new(text_content)))
|
|
268
|
+
.map_err(|e| PolyXmlError::SerializationError(e.to_string()))?;
|
|
269
|
+
}
|
|
270
|
+
} else if let PolyValue::List(items) = val {
|
|
271
|
+
for item in items {
|
|
272
|
+
let mut item_buf = [0u8; lexical_core::BUFFER_SIZE];
|
|
273
|
+
if let Some(text_content) = Self::format_scalar_to(item, &mut item_buf) {
|
|
274
|
+
if !text_content.is_empty() {
|
|
275
|
+
writer
|
|
276
|
+
.write_event(Event::Text(BytesText::new(text_content)))
|
|
277
|
+
.map_err(|e| PolyXmlError::SerializationError(e.to_string()))?;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 3. Write child elements
|
|
286
|
+
for field in &schema.fields {
|
|
287
|
+
if field.kind == FieldKind::Element {
|
|
288
|
+
if let Some(val) = obj.get(&field.name) {
|
|
289
|
+
if val.is_null() {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
let child_element_ns =
|
|
293
|
+
field.namespace.as_deref().or(schema.namespace.as_deref());
|
|
294
|
+
|
|
295
|
+
match &field.val_type {
|
|
296
|
+
ValueType::Scalar(_) => {
|
|
297
|
+
let mut buf = [0u8; lexical_core::BUFFER_SIZE];
|
|
298
|
+
if let Some(text) = Self::format_scalar_to(val, &mut buf) {
|
|
299
|
+
let local_child = std::str::from_utf8(&field.xml_name)?;
|
|
300
|
+
let child_tag = if let Some(ctx) = ns_ctx {
|
|
301
|
+
ctx.qualify_element(local_child, child_element_ns)
|
|
302
|
+
} else {
|
|
303
|
+
Cow::Borrowed(local_child)
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
writer
|
|
307
|
+
.write_event(Event::Start(BytesStart::new(child_tag.as_ref())))
|
|
308
|
+
.map_err(|e| PolyXmlError::SerializationError(e.to_string()))?;
|
|
309
|
+
writer
|
|
310
|
+
.write_event(Event::Text(BytesText::new(text)))
|
|
311
|
+
.map_err(|e| PolyXmlError::SerializationError(e.to_string()))?;
|
|
312
|
+
writer
|
|
313
|
+
.write_event(Event::End(BytesEnd::new(child_tag.as_ref())))
|
|
314
|
+
.map_err(|e| PolyXmlError::SerializationError(e.to_string()))?;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
ValueType::List(inner) => {
|
|
318
|
+
if let PolyValue::List(items) = val {
|
|
319
|
+
for item in items {
|
|
320
|
+
match inner.as_ref() {
|
|
321
|
+
ValueType::Scalar(_) => {
|
|
322
|
+
let mut buf = [0u8; lexical_core::BUFFER_SIZE];
|
|
323
|
+
if let Some(text) =
|
|
324
|
+
Self::format_scalar_to(item, &mut buf)
|
|
325
|
+
{
|
|
326
|
+
let local_child =
|
|
327
|
+
std::str::from_utf8(&field.xml_name)?;
|
|
328
|
+
let child_tag = if let Some(ctx) = ns_ctx {
|
|
329
|
+
ctx.qualify_element(
|
|
330
|
+
local_child,
|
|
331
|
+
child_element_ns,
|
|
332
|
+
)
|
|
333
|
+
} else {
|
|
334
|
+
Cow::Borrowed(local_child)
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
writer
|
|
338
|
+
.write_event(Event::Start(BytesStart::new(
|
|
339
|
+
child_tag.as_ref(),
|
|
340
|
+
)))
|
|
341
|
+
.map_err(|e| {
|
|
342
|
+
PolyXmlError::SerializationError(
|
|
343
|
+
e.to_string(),
|
|
344
|
+
)
|
|
345
|
+
})?;
|
|
346
|
+
writer
|
|
347
|
+
.write_event(Event::Text(BytesText::new(text)))
|
|
348
|
+
.map_err(|e| {
|
|
349
|
+
PolyXmlError::SerializationError(
|
|
350
|
+
e.to_string(),
|
|
351
|
+
)
|
|
352
|
+
})?;
|
|
353
|
+
writer
|
|
354
|
+
.write_event(Event::End(BytesEnd::new(
|
|
355
|
+
child_tag.as_ref(),
|
|
356
|
+
)))
|
|
357
|
+
.map_err(|e| {
|
|
358
|
+
PolyXmlError::SerializationError(
|
|
359
|
+
e.to_string(),
|
|
360
|
+
)
|
|
361
|
+
})?;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
ValueType::Nested(nested_schema) => {
|
|
365
|
+
let nested_ns = field
|
|
366
|
+
.namespace
|
|
367
|
+
.as_deref()
|
|
368
|
+
.or(nested_schema.namespace.as_deref());
|
|
369
|
+
Self::write_model(
|
|
370
|
+
writer,
|
|
371
|
+
&field.xml_name,
|
|
372
|
+
item,
|
|
373
|
+
nested_schema,
|
|
374
|
+
ns_ctx,
|
|
375
|
+
false,
|
|
376
|
+
nested_ns,
|
|
377
|
+
)?;
|
|
378
|
+
}
|
|
379
|
+
ValueType::List(_) => {}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
ValueType::Nested(nested_schema) => {
|
|
385
|
+
let nested_ns = field
|
|
386
|
+
.namespace
|
|
387
|
+
.as_deref()
|
|
388
|
+
.or(nested_schema.namespace.as_deref());
|
|
389
|
+
Self::write_model(
|
|
390
|
+
writer,
|
|
391
|
+
&field.xml_name,
|
|
392
|
+
val,
|
|
393
|
+
nested_schema,
|
|
394
|
+
ns_ctx,
|
|
395
|
+
false,
|
|
396
|
+
nested_ns,
|
|
397
|
+
)?;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Close element
|
|
405
|
+
writer
|
|
406
|
+
.write_event(Event::End(BytesEnd::new(qualified_tag.as_ref())))
|
|
407
|
+
.map_err(|e| PolyXmlError::SerializationError(e.to_string()))?;
|
|
408
|
+
|
|
409
|
+
Ok(())
|
|
410
|
+
}
|
|
411
|
+
}
|
|
@@ -188,6 +188,24 @@ fn extract_schema_from_class<'py>(
|
|
|
188
188
|
let py = cls.py();
|
|
189
189
|
let class_name: String = cls.getattr("__name__")?.extract()?;
|
|
190
190
|
let mut builder = ModelSchema::builder(class_name);
|
|
191
|
+
|
|
192
|
+
if let Ok(meta_cls) = cls.getattr("Meta") {
|
|
193
|
+
if let Ok(name_val) = meta_cls.getattr("name") {
|
|
194
|
+
if let Ok(name_str) = name_val.extract::<String>() {
|
|
195
|
+
if !name_str.is_empty() {
|
|
196
|
+
builder = builder.xml_name(name_str.as_bytes());
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if let Ok(ns_val) = meta_cls.getattr("namespace") {
|
|
201
|
+
if let Ok(ns_str) = ns_val.extract::<String>() {
|
|
202
|
+
if !ns_str.is_empty() {
|
|
203
|
+
builder = builder.namespace(ns_str);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
191
209
|
let mut cached_fields = Vec::new();
|
|
192
210
|
|
|
193
211
|
let type_hints: Option<Bound<'py, PyDict>> = py
|
|
@@ -204,6 +222,7 @@ fn extract_schema_from_class<'py>(
|
|
|
204
222
|
let py_string = PyString::new(py, &py_name).unbind();
|
|
205
223
|
let mut xml_name = py_name.clone();
|
|
206
224
|
let mut kind = FieldKind::Element;
|
|
225
|
+
let mut field_ns: Option<String> = None;
|
|
207
226
|
|
|
208
227
|
let meta = field_obj
|
|
209
228
|
.getattr("xsdata_metadata")
|
|
@@ -229,6 +248,13 @@ fn extract_schema_from_class<'py>(
|
|
|
229
248
|
if let Ok(n) = m.get_item("name") {
|
|
230
249
|
xml_name = n.extract().unwrap_or(py_name.clone());
|
|
231
250
|
}
|
|
251
|
+
if let Ok(ns) = m.get_item("namespace") {
|
|
252
|
+
if let Ok(ns_str) = ns.extract::<String>() {
|
|
253
|
+
if !ns_str.is_empty() {
|
|
254
|
+
field_ns = Some(ns_str);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
232
258
|
}
|
|
233
259
|
|
|
234
260
|
let field_type = if let Some(ref hints) = type_hints {
|
|
@@ -250,12 +276,11 @@ fn extract_schema_from_class<'py>(
|
|
|
250
276
|
py_type: Some(unwrapped_type.into_any().unbind()),
|
|
251
277
|
});
|
|
252
278
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
));
|
|
279
|
+
let mut field_schema = FieldSchema::new(py_name, xml_name.as_bytes(), kind, val_type);
|
|
280
|
+
if let Some(ns) = field_ns {
|
|
281
|
+
field_schema = field_schema.namespace(ns);
|
|
282
|
+
}
|
|
283
|
+
builder = builder.field(field_schema);
|
|
259
284
|
}
|
|
260
285
|
} else if cls.hasattr("__dataclass_fields__")? {
|
|
261
286
|
let fields: Bound<'py, PyDict> = cls.getattr("__dataclass_fields__")?.cast_into()?;
|
|
@@ -264,6 +289,7 @@ fn extract_schema_from_class<'py>(
|
|
|
264
289
|
let py_string = PyString::new(py, &py_name).unbind();
|
|
265
290
|
let mut xml_name = py_name.clone();
|
|
266
291
|
let mut kind = FieldKind::Element;
|
|
292
|
+
let mut field_ns: Option<String> = None;
|
|
267
293
|
|
|
268
294
|
if let Ok(meta) = field_obj.getattr("metadata") {
|
|
269
295
|
if let Ok(t) = meta.get_item("type") {
|
|
@@ -284,6 +310,13 @@ fn extract_schema_from_class<'py>(
|
|
|
284
310
|
if let Ok(n) = meta.get_item("name") {
|
|
285
311
|
xml_name = n.extract().unwrap_or(py_name.clone());
|
|
286
312
|
}
|
|
313
|
+
if let Ok(ns) = meta.get_item("namespace") {
|
|
314
|
+
if let Ok(ns_str) = ns.extract::<String>() {
|
|
315
|
+
if !ns_str.is_empty() {
|
|
316
|
+
field_ns = Some(ns_str);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
287
320
|
}
|
|
288
321
|
|
|
289
322
|
let is_init = field_obj
|
|
@@ -315,12 +348,11 @@ fn extract_schema_from_class<'py>(
|
|
|
315
348
|
py_type: Some(unwrapped_type.into_any().unbind()),
|
|
316
349
|
});
|
|
317
350
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
));
|
|
351
|
+
let mut field_schema = FieldSchema::new(py_name, xml_name.as_bytes(), kind, val_type);
|
|
352
|
+
if let Some(ns) = field_ns {
|
|
353
|
+
field_schema = field_schema.namespace(ns);
|
|
354
|
+
}
|
|
355
|
+
builder = builder.field(field_schema);
|
|
324
356
|
}
|
|
325
357
|
}
|
|
326
358
|
|
|
@@ -793,20 +825,49 @@ impl XmlIterator {
|
|
|
793
825
|
}
|
|
794
826
|
|
|
795
827
|
#[pyfunction]
|
|
796
|
-
#[pyo3(signature = (obj, indent=None))]
|
|
828
|
+
#[pyo3(signature = (obj, indent=None, namespaces=None, ns_map=None))]
|
|
797
829
|
fn serialize<'py>(
|
|
798
830
|
py: Python<'py>,
|
|
799
831
|
obj: Bound<'py, PyAny>,
|
|
800
832
|
indent: Option<usize>,
|
|
833
|
+
namespaces: Option<bool>,
|
|
834
|
+
ns_map: Option<Bound<'py, PyDict>>,
|
|
801
835
|
) -> PyResult<Bound<'py, PyBytes>> {
|
|
802
836
|
let cls = obj.get_type();
|
|
803
837
|
let meta = get_or_create_schema_meta(&cls)?;
|
|
804
838
|
|
|
805
839
|
let poly_val = py_to_poly_value(py, &obj, &meta.schema, Some(&meta))?;
|
|
806
|
-
let root_name = meta.schema.name.as_str();
|
|
840
|
+
let root_name = std::str::from_utf8(&meta.schema.xml_name).unwrap_or(meta.schema.name.as_str());
|
|
807
841
|
|
|
808
|
-
let
|
|
809
|
-
|
|
842
|
+
let rust_ns_map = if let Some(dict) = ns_map {
|
|
843
|
+
let mut map = std::collections::HashMap::new();
|
|
844
|
+
for (k, v) in dict.iter() {
|
|
845
|
+
let k_str = if k.is_none() {
|
|
846
|
+
String::new()
|
|
847
|
+
} else {
|
|
848
|
+
k.extract::<String>().unwrap_or_default()
|
|
849
|
+
};
|
|
850
|
+
let v_str = if v.is_none() {
|
|
851
|
+
String::new()
|
|
852
|
+
} else {
|
|
853
|
+
v.extract::<String>().unwrap_or_default()
|
|
854
|
+
};
|
|
855
|
+
map.insert(k_str, v_str);
|
|
856
|
+
}
|
|
857
|
+
Some(map)
|
|
858
|
+
} else {
|
|
859
|
+
None
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
let bytes = polyxml::serialize_with_options(
|
|
863
|
+
root_name,
|
|
864
|
+
&poly_val,
|
|
865
|
+
&meta.schema,
|
|
866
|
+
indent,
|
|
867
|
+
namespaces,
|
|
868
|
+
rust_ns_map.as_ref(),
|
|
869
|
+
)
|
|
870
|
+
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
|
|
810
871
|
|
|
811
872
|
Ok(PyBytes::new(py, &bytes))
|
|
812
873
|
}
|
|
@@ -561,3 +561,69 @@ def test_mixed_content_wildcard_dataclass():
|
|
|
561
561
|
serialized = polyxml.serialize(decoded)
|
|
562
562
|
assert b"Sample Text Content" in serialized
|
|
563
563
|
assert b'lang="en"' in serialized
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def test_namespaced_dataclass_serialization():
|
|
567
|
+
@dataclass
|
|
568
|
+
class Item:
|
|
569
|
+
class Meta:
|
|
570
|
+
name = "item"
|
|
571
|
+
namespace = "http://example.com/ns1"
|
|
572
|
+
|
|
573
|
+
title: str = field(metadata={"type": "Element", "namespace": "http://example.com/ns1"})
|
|
574
|
+
sku: str = field(metadata={"type": "Attribute", "namespace": "http://example.com/ns2"})
|
|
575
|
+
local_attr: int = field(default=99, metadata={"type": "Attribute"})
|
|
576
|
+
|
|
577
|
+
item = Item(title="Smartphone", sku="SKU-888", local_attr=99)
|
|
578
|
+
|
|
579
|
+
# 1. Auto-detected namespaces
|
|
580
|
+
xml_bytes = polyxml.serialize(item)
|
|
581
|
+
xml_str = xml_bytes.decode("utf-8")
|
|
582
|
+
assert 'xmlns:ns0="http://example.com/ns1"' in xml_str
|
|
583
|
+
assert 'xmlns:ns1="http://example.com/ns2"' in xml_str
|
|
584
|
+
assert xml_str.startswith("<ns0:item")
|
|
585
|
+
assert 'ns1:sku="SKU-888"' in xml_str
|
|
586
|
+
assert 'local_attr="99"' in xml_str
|
|
587
|
+
assert "<ns0:title>Smartphone</ns0:title>" in xml_str
|
|
588
|
+
assert xml_str.endswith("</ns0:item>")
|
|
589
|
+
|
|
590
|
+
# 2. Custom ns_map with default namespace
|
|
591
|
+
ns_map = {None: "http://example.com/ns1", "inv": "http://example.com/ns2"}
|
|
592
|
+
xml_custom = polyxml.serialize(item, ns_map=ns_map).decode("utf-8")
|
|
593
|
+
assert 'xmlns="http://example.com/ns1"' in xml_custom
|
|
594
|
+
assert 'xmlns:inv="http://example.com/ns2"' in xml_custom
|
|
595
|
+
assert xml_custom.startswith("<item")
|
|
596
|
+
assert 'inv:sku="SKU-888"' in xml_custom
|
|
597
|
+
assert "<title>Smartphone</title>" in xml_custom
|
|
598
|
+
assert xml_custom.endswith("</item>")
|
|
599
|
+
|
|
600
|
+
# 3. Explicitly disabled namespaces (toggle=False)
|
|
601
|
+
xml_raw = polyxml.serialize(item, namespaces=False).decode("utf-8")
|
|
602
|
+
assert "xmlns" not in xml_raw
|
|
603
|
+
assert xml_raw.startswith("<item")
|
|
604
|
+
assert 'sku="SKU-888"' in xml_raw
|
|
605
|
+
assert "<title>Smartphone</title>" in xml_raw
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def test_namespaced_pydantic_serialization():
|
|
609
|
+
class PydanticItem(BaseModel):
|
|
610
|
+
class Meta:
|
|
611
|
+
name = "product"
|
|
612
|
+
namespace = "http://example.com/prod"
|
|
613
|
+
|
|
614
|
+
name: str = Field(
|
|
615
|
+
json_schema_extra={"type": "Element", "namespace": "http://example.com/prod"}
|
|
616
|
+
)
|
|
617
|
+
code: str = Field(
|
|
618
|
+
json_schema_extra={"type": "Attribute", "namespace": "http://example.com/meta"}
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
prod = PydanticItem(name="Tablet", code="TAB-1")
|
|
622
|
+
xml_bytes = polyxml.serialize(prod, namespaces=True)
|
|
623
|
+
xml_str = xml_bytes.decode("utf-8")
|
|
624
|
+
|
|
625
|
+
assert 'xmlns:ns0="http://example.com/prod"' in xml_str
|
|
626
|
+
assert 'xmlns:ns1="http://example.com/meta"' in xml_str
|
|
627
|
+
assert xml_str.startswith("<ns0:product")
|
|
628
|
+
assert 'ns1:code="TAB-1"' in xml_str
|
|
629
|
+
assert "<ns0:name>Tablet</ns0:name>" in xml_str
|
|
@@ -104,17 +104,28 @@ def iterparse[T](
|
|
|
104
104
|
return _iterparse(_to_bytes(source), target_type, tag)
|
|
105
105
|
|
|
106
106
|
|
|
107
|
-
def serialize(
|
|
107
|
+
def serialize(
|
|
108
|
+
obj: object,
|
|
109
|
+
*,
|
|
110
|
+
indent: int | None = None,
|
|
111
|
+
namespaces: bool | None = None,
|
|
112
|
+
ns_map: dict[str, str] | None = None,
|
|
113
|
+
) -> bytes:
|
|
108
114
|
"""Serialize a strongly-typed model instance back into XML bytes.
|
|
109
115
|
|
|
110
116
|
Args:
|
|
111
117
|
obj: Python dataclass or Pydantic model instance.
|
|
112
118
|
indent: Optional indentation size in spaces for pretty-printing.
|
|
119
|
+
namespaces: Optional boolean toggle to enable or disable XML namespace prefix
|
|
120
|
+
resolution and root xmlns attribute generation. When None (default),
|
|
121
|
+
namespaces are automatically enabled if the model or fields define
|
|
122
|
+
namespaces or if ns_map is provided.
|
|
123
|
+
ns_map: Optional prefix-to-URI or URI-to-prefix mapping dictionary for XML namespaces.
|
|
113
124
|
|
|
114
125
|
Returns:
|
|
115
126
|
UTF-8 encoded XML bytes representing the model instance.
|
|
116
127
|
"""
|
|
117
|
-
return _serialize(obj, indent=indent)
|
|
128
|
+
return _serialize(obj, indent=indent, namespaces=namespaces, ns_map=ns_map)
|
|
118
129
|
|
|
119
130
|
|
|
120
131
|
try:
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
use std::io::Cursor;
|
|
2
|
-
|
|
3
|
-
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
|
|
4
|
-
use quick_xml::Writer;
|
|
5
|
-
|
|
6
|
-
use crate::error::{PolyXmlError, Result};
|
|
7
|
-
use crate::schema::{FieldKind, ModelSchema, ValueType};
|
|
8
|
-
use crate::value::PolyValue;
|
|
9
|
-
|
|
10
|
-
pub struct XmlSerializer;
|
|
11
|
-
|
|
12
|
-
impl XmlSerializer {
|
|
13
|
-
pub fn serialize(
|
|
14
|
-
root_name: &str,
|
|
15
|
-
value: &PolyValue,
|
|
16
|
-
schema: &ModelSchema,
|
|
17
|
-
indent: Option<usize>,
|
|
18
|
-
) -> Result<Vec<u8>> {
|
|
19
|
-
let mut buffer = Cursor::new(Vec::with_capacity(512));
|
|
20
|
-
let mut writer = match indent {
|
|
21
|
-
Some(spaces) => Writer::new_with_indent(&mut buffer, b' ', spaces),
|
|
22
|
-
None => Writer::new(&mut buffer),
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
Self::write_model(&mut writer, root_name.as_bytes(), value, schema)?;
|
|
26
|
-
|
|
27
|
-
Ok(buffer.into_inner())
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
fn format_scalar_to<'a>(
|
|
31
|
-
val: &'a PolyValue,
|
|
32
|
-
buf: &'a mut [u8; lexical_core::BUFFER_SIZE],
|
|
33
|
-
) -> Option<&'a str> {
|
|
34
|
-
match val {
|
|
35
|
-
PolyValue::String(s) => Some(s.as_str()),
|
|
36
|
-
PolyValue::Int(i) => {
|
|
37
|
-
let bytes = lexical_core::write(*i, buf);
|
|
38
|
-
std::str::from_utf8(bytes).ok()
|
|
39
|
-
}
|
|
40
|
-
PolyValue::Float(f) => {
|
|
41
|
-
let bytes = lexical_core::write(*f, buf);
|
|
42
|
-
std::str::from_utf8(bytes).ok()
|
|
43
|
-
}
|
|
44
|
-
PolyValue::Bool(b) => Some(if *b { "true" } else { "false" }),
|
|
45
|
-
_ => None,
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
fn write_model<W: std::io::Write>(
|
|
50
|
-
writer: &mut Writer<W>,
|
|
51
|
-
tag_name: &[u8],
|
|
52
|
-
value: &PolyValue,
|
|
53
|
-
schema: &ModelSchema,
|
|
54
|
-
) -> Result<()> {
|
|
55
|
-
let obj = match value {
|
|
56
|
-
PolyValue::Object(o) => o,
|
|
57
|
-
_ => {
|
|
58
|
-
return Err(PolyXmlError::SerializationError(
|
|
59
|
-
"Expected Object value for model".into(),
|
|
60
|
-
))
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
let mut elem = BytesStart::new(std::str::from_utf8(tag_name)?);
|
|
65
|
-
|
|
66
|
-
// 1. Collect and write attributes
|
|
67
|
-
for field in &schema.fields {
|
|
68
|
-
if field.kind == FieldKind::Attribute {
|
|
69
|
-
if let Some(val) = obj.get(&field.name) {
|
|
70
|
-
if !val.is_null() {
|
|
71
|
-
let attr_name = std::str::from_utf8(&field.xml_name)?;
|
|
72
|
-
let mut buf = [0u8; lexical_core::BUFFER_SIZE];
|
|
73
|
-
if let Some(attr_str) = Self::format_scalar_to(val, &mut buf) {
|
|
74
|
-
elem.push_attribute((attr_name, attr_str));
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
writer
|
|
82
|
-
.write_event(Event::Start(elem))
|
|
83
|
-
.map_err(|e| PolyXmlError::SerializationError(e.to_string()))?;
|
|
84
|
-
|
|
85
|
-
// 2. Write text content if present
|
|
86
|
-
if let Some(text_idx) = schema.text_field {
|
|
87
|
-
let field = &schema.fields[text_idx];
|
|
88
|
-
if let Some(val) = obj.get(&field.name) {
|
|
89
|
-
let mut buf = [0u8; lexical_core::BUFFER_SIZE];
|
|
90
|
-
if let Some(text_content) = Self::format_scalar_to(val, &mut buf) {
|
|
91
|
-
if !text_content.is_empty() {
|
|
92
|
-
writer
|
|
93
|
-
.write_event(Event::Text(BytesText::new(text_content)))
|
|
94
|
-
.map_err(|e| PolyXmlError::SerializationError(e.to_string()))?;
|
|
95
|
-
}
|
|
96
|
-
} else if let PolyValue::List(items) = val {
|
|
97
|
-
for item in items {
|
|
98
|
-
let mut item_buf = [0u8; lexical_core::BUFFER_SIZE];
|
|
99
|
-
if let Some(text_content) = Self::format_scalar_to(item, &mut item_buf) {
|
|
100
|
-
if !text_content.is_empty() {
|
|
101
|
-
writer
|
|
102
|
-
.write_event(Event::Text(BytesText::new(text_content)))
|
|
103
|
-
.map_err(|e| PolyXmlError::SerializationError(e.to_string()))?;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// 3. Write child elements
|
|
112
|
-
for field in &schema.fields {
|
|
113
|
-
if field.kind == FieldKind::Element {
|
|
114
|
-
if let Some(val) = obj.get(&field.name) {
|
|
115
|
-
if val.is_null() {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
match &field.val_type {
|
|
119
|
-
ValueType::Scalar(_) => {
|
|
120
|
-
let mut buf = [0u8; lexical_core::BUFFER_SIZE];
|
|
121
|
-
if let Some(text) = Self::format_scalar_to(val, &mut buf) {
|
|
122
|
-
let child_tag = std::str::from_utf8(&field.xml_name)?;
|
|
123
|
-
writer
|
|
124
|
-
.write_event(Event::Start(BytesStart::new(child_tag)))
|
|
125
|
-
.map_err(|e| PolyXmlError::SerializationError(e.to_string()))?;
|
|
126
|
-
writer
|
|
127
|
-
.write_event(Event::Text(BytesText::new(text)))
|
|
128
|
-
.map_err(|e| PolyXmlError::SerializationError(e.to_string()))?;
|
|
129
|
-
writer
|
|
130
|
-
.write_event(Event::End(BytesEnd::new(child_tag)))
|
|
131
|
-
.map_err(|e| PolyXmlError::SerializationError(e.to_string()))?;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
ValueType::List(inner) => {
|
|
135
|
-
if let PolyValue::List(items) = val {
|
|
136
|
-
for item in items {
|
|
137
|
-
match inner.as_ref() {
|
|
138
|
-
ValueType::Scalar(_) => {
|
|
139
|
-
let mut buf = [0u8; lexical_core::BUFFER_SIZE];
|
|
140
|
-
if let Some(text) =
|
|
141
|
-
Self::format_scalar_to(item, &mut buf)
|
|
142
|
-
{
|
|
143
|
-
let child_tag =
|
|
144
|
-
std::str::from_utf8(&field.xml_name)?;
|
|
145
|
-
writer
|
|
146
|
-
.write_event(Event::Start(BytesStart::new(
|
|
147
|
-
child_tag,
|
|
148
|
-
)))
|
|
149
|
-
.map_err(|e| {
|
|
150
|
-
PolyXmlError::SerializationError(
|
|
151
|
-
e.to_string(),
|
|
152
|
-
)
|
|
153
|
-
})?;
|
|
154
|
-
writer
|
|
155
|
-
.write_event(Event::Text(BytesText::new(text)))
|
|
156
|
-
.map_err(|e| {
|
|
157
|
-
PolyXmlError::SerializationError(
|
|
158
|
-
e.to_string(),
|
|
159
|
-
)
|
|
160
|
-
})?;
|
|
161
|
-
writer
|
|
162
|
-
.write_event(Event::End(BytesEnd::new(
|
|
163
|
-
child_tag,
|
|
164
|
-
)))
|
|
165
|
-
.map_err(|e| {
|
|
166
|
-
PolyXmlError::SerializationError(
|
|
167
|
-
e.to_string(),
|
|
168
|
-
)
|
|
169
|
-
})?;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
ValueType::Nested(nested_schema) => {
|
|
173
|
-
Self::write_model(
|
|
174
|
-
writer,
|
|
175
|
-
&field.xml_name,
|
|
176
|
-
item,
|
|
177
|
-
nested_schema,
|
|
178
|
-
)?;
|
|
179
|
-
}
|
|
180
|
-
ValueType::List(_) => {}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
ValueType::Nested(nested_schema) => {
|
|
186
|
-
Self::write_model(writer, &field.xml_name, val, nested_schema)?;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Close element
|
|
194
|
-
writer
|
|
195
|
-
.write_event(Event::End(BytesEnd::new(std::str::from_utf8(tag_name)?)))
|
|
196
|
-
.map_err(|e| PolyXmlError::SerializationError(e.to_string()))?;
|
|
197
|
-
|
|
198
|
-
Ok(())
|
|
199
|
-
}
|
|
200
|
-
}
|
|
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
|