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.
Files changed (25) hide show
  1. {polyxml-0.8.1 → polyxml-0.10.0}/Cargo.lock +8 -8
  2. {polyxml-0.8.1 → polyxml-0.10.0}/Cargo.toml +1 -1
  3. {polyxml-0.8.1 → polyxml-0.10.0}/PKG-INFO +1 -1
  4. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/src/lib.rs +144 -0
  5. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/src/parser.rs +20 -2
  6. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/src/schema.rs +29 -0
  7. polyxml-0.10.0/crates/polyxml-core/src/serializer.rs +411 -0
  8. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-python/src/lib.rs +77 -16
  9. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-python/tests/test_polyxml.py +66 -0
  10. {polyxml-0.8.1 → polyxml-0.10.0}/pyproject.toml +1 -1
  11. {polyxml-0.8.1 → polyxml-0.10.0}/python/polyxml/__init__.py +13 -2
  12. polyxml-0.8.1/crates/polyxml-core/src/serializer.rs +0 -200
  13. {polyxml-0.8.1 → polyxml-0.10.0}/README.md +0 -0
  14. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/Cargo.toml +0 -0
  15. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/README.md +0 -0
  16. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
  17. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/src/converters.rs +0 -0
  18. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/src/error.rs +0 -0
  19. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/src/value.rs +0 -0
  20. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/tests/test_core.rs +0 -0
  21. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-core/tests/test_security.rs +0 -0
  22. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-python/Cargo.toml +0 -0
  23. {polyxml-0.8.1 → polyxml-0.10.0}/crates/polyxml-python/README.md +0 -0
  24. {polyxml-0.8.1 → polyxml-0.10.0}/python/polyxml/AGENT_GUIDE.md +0 -0
  25. {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.1"
323
+ version = "2.4.2"
324
324
  source = "registry+https://github.com/rust-lang/crates.io-index"
325
- checksum = "60fdf9b392c50e7c4170fa633bd909490ed7835cea4c046776d1a4dd8d2ae0ab"
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.8.1"
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.8.1"
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.8.1"
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.8.1"
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.0"
632
+ version = "1.16.1"
633
633
  source = "registry+https://github.com/rust-lang/crates.io-index"
634
- checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
634
+ checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
635
635
 
636
636
  [[package]]
637
637
  name = "syn"
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/polyxml-core", "crates/polyxml-python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.8.1"
6
+ version = "0.10.0"
7
7
  edition = "2021"
8
8
  rust-version = "1.80"
9
9
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyxml
3
- Version: 0.8.1
3
+ Version: 0.10.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -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() == self.target_tag.as_slice() {
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() == self.target_tag.as_slice() {
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
- builder = builder.field(FieldSchema::new(
254
- py_name,
255
- xml_name.as_bytes(),
256
- kind,
257
- val_type,
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
- builder = builder.field(FieldSchema::new(
319
- py_name,
320
- xml_name.as_bytes(),
321
- kind,
322
- val_type,
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 bytes = polyxml::serialize(root_name, &poly_val, &meta.schema, indent)
809
- .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
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
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "polyxml"
7
- version = "0.8.1"
7
+ version = "0.10.0"
8
8
  description = "High-performance, polyglot XML data-binding engine built in Rust"
9
9
  authors = [
10
10
  { name = "Bailey Nguyen", email = "bailey.tan.nguyen@gmail.com" }
@@ -104,17 +104,28 @@ def iterparse[T](
104
104
  return _iterparse(_to_bytes(source), target_type, tag)
105
105
 
106
106
 
107
- def serialize(obj: object, *, indent: int | None = None) -> bytes:
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