polyxml 0.19.1__tar.gz → 0.19.2__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 (55) hide show
  1. {polyxml-0.19.1 → polyxml-0.19.2}/Cargo.lock +5 -5
  2. {polyxml-0.19.1 → polyxml-0.19.2}/Cargo.toml +1 -1
  3. {polyxml-0.19.1 → polyxml-0.19.2}/PKG-INFO +1 -1
  4. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/json.rs +20 -3
  5. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/parser.rs +25 -20
  6. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/serializer.rs +17 -9
  7. polyxml-0.19.2/crates/polyxml-core/src/value.rs +167 -0
  8. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-python/src/lib.rs +294 -37
  9. {polyxml-0.19.1 → polyxml-0.19.2}/pyproject.toml +1 -1
  10. polyxml-0.19.1/crates/polyxml-core/src/value.rs +0 -90
  11. {polyxml-0.19.1 → polyxml-0.19.2}/README.md +0 -0
  12. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/Cargo.toml +0 -0
  13. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/README.md +0 -0
  14. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
  15. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/codegen/cpp/mod.rs +0 -0
  16. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/codegen/csharp/mod.rs +0 -0
  17. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/codegen/go/mod.rs +0 -0
  18. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/codegen/java/mod.rs +0 -0
  19. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/codegen/mod.rs +0 -0
  20. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/codegen/python/mod.rs +0 -0
  21. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/codegen/rust/mod.rs +0 -0
  22. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/codegen/typescript/mod.rs +0 -0
  23. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/converters.rs +0 -0
  24. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/error.rs +0 -0
  25. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/ir/mod.rs +0 -0
  26. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/ir/tarjan.rs +0 -0
  27. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/lib.rs +0 -0
  28. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/schema.rs +0 -0
  29. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/schema_parser/mod.rs +0 -0
  30. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/src/transcoder.rs +0 -0
  31. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/tests/test_conformance.rs +0 -0
  32. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/tests/test_core.rs +0 -0
  33. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/tests/test_cpp_codegen.rs +0 -0
  34. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/tests/test_csharp_codegen.rs +0 -0
  35. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/tests/test_go_codegen.rs +0 -0
  36. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/tests/test_java_codegen.rs +0 -0
  37. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/tests/test_json.rs +0 -0
  38. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/tests/test_python_codegen.rs +0 -0
  39. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/tests/test_rust_codecs.rs +0 -0
  40. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/tests/test_rust_codegen.rs +0 -0
  41. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/tests/test_schema_ir.rs +0 -0
  42. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/tests/test_security.rs +0 -0
  43. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/tests/test_transcoder.rs +0 -0
  44. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-core/tests/test_ts_codegen.rs +0 -0
  45. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-python/Cargo.toml +0 -0
  46. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-python/README.md +0 -0
  47. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-python/tests/test_conformance.py +0 -0
  48. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-python/tests/test_generated_models.py +0 -0
  49. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-python/tests/test_json.py +0 -0
  50. {polyxml-0.19.1 → polyxml-0.19.2}/crates/polyxml-python/tests/test_polyxml.py +0 -0
  51. {polyxml-0.19.1 → polyxml-0.19.2}/python/polyxml/AGENT_GUIDE.md +0 -0
  52. {polyxml-0.19.1 → polyxml-0.19.2}/python/polyxml/__init__.py +0 -0
  53. {polyxml-0.19.1 → polyxml-0.19.2}/python/polyxml/compat/__init__.py +0 -0
  54. {polyxml-0.19.1 → polyxml-0.19.2}/python/polyxml/compat/xsdata.py +0 -0
  55. {polyxml-0.19.1 → polyxml-0.19.2}/python/polyxml/py.typed +0 -0
@@ -549,7 +549,7 @@ dependencies = [
549
549
 
550
550
  [[package]]
551
551
  name = "polyxml"
552
- version = "0.19.1"
552
+ version = "0.19.2"
553
553
  dependencies = [
554
554
  "criterion",
555
555
  "heck",
@@ -567,14 +567,14 @@ dependencies = [
567
567
 
568
568
  [[package]]
569
569
  name = "polyxml-c"
570
- version = "0.19.1"
570
+ version = "0.19.2"
571
571
  dependencies = [
572
572
  "polyxml",
573
573
  ]
574
574
 
575
575
  [[package]]
576
576
  name = "polyxml-cli"
577
- version = "0.19.1"
577
+ version = "0.19.2"
578
578
  dependencies = [
579
579
  "clap",
580
580
  "glob",
@@ -588,7 +588,7 @@ dependencies = [
588
588
 
589
589
  [[package]]
590
590
  name = "polyxml-js"
591
- version = "0.19.1"
591
+ version = "0.19.2"
592
592
  dependencies = [
593
593
  "napi",
594
594
  "napi-build",
@@ -598,7 +598,7 @@ dependencies = [
598
598
 
599
599
  [[package]]
600
600
  name = "polyxml-python"
601
- version = "0.19.1"
601
+ version = "0.19.2"
602
602
  dependencies = [
603
603
  "lexical-core",
604
604
  "polyxml",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/polyxml-core", "crates/polyxml-python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.19.1"
6
+ version = "0.19.2"
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.19.1
3
+ Version: 0.19.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -27,6 +27,15 @@ impl From<&PolyValue> for JsonValue {
27
27
  }
28
28
  JsonValue::Object(map)
29
29
  }
30
+ PolyValue::Record { schema, values } => {
31
+ let mut map = serde_json::Map::with_capacity(schema.fields.len());
32
+ for (idx, field) in schema.fields.iter().enumerate() {
33
+ if let Some(Some(v)) = values.get(idx) {
34
+ map.insert(field.name.clone(), JsonValue::from(v));
35
+ }
36
+ }
37
+ JsonValue::Object(map)
38
+ }
30
39
  }
31
40
  }
32
41
  }
@@ -67,17 +76,25 @@ pub fn poly_value_to_json_value(
67
76
  schema: &ModelSchema,
68
77
  by_alias: bool,
69
78
  ) -> JsonValue {
79
+ let get_field = |idx: usize, name: &str| -> Option<&PolyValue> {
80
+ match val {
81
+ PolyValue::Record { values, .. } => values.get(idx).and_then(|v| v.as_ref()),
82
+ PolyValue::Object(map) => map.get(name),
83
+ _ => None,
84
+ }
85
+ };
86
+
70
87
  match val {
71
- PolyValue::Object(map) => {
88
+ PolyValue::Record { .. } | PolyValue::Object(_) => {
72
89
  let mut json_map = serde_json::Map::with_capacity(schema.fields.len());
73
- for field in &schema.fields {
90
+ for (idx, field) in schema.fields.iter().enumerate() {
74
91
  let key = if by_alias {
75
92
  std::str::from_utf8(&field.xml_name).unwrap_or(field.name.as_str())
76
93
  } else {
77
94
  field.name.as_str()
78
95
  };
79
96
 
80
- if let Some(field_val) = map.get(&field.name) {
97
+ if let Some(field_val) = get_field(idx, &field.name) {
81
98
  let converted = match &field.val_type {
82
99
  ValueType::Nested(nested_schema) => {
83
100
  if field_val.is_null() {
@@ -1,4 +1,3 @@
1
- use std::collections::HashMap;
2
1
  use std::sync::Arc;
3
2
 
4
3
  use quick_xml::events::{BytesStart, Event};
@@ -48,19 +47,10 @@ impl StackFrame {
48
47
  }
49
48
 
50
49
  fn finish(mut self) -> Result<PolyValue> {
51
- let mut obj = HashMap::with_capacity(self.schema.fields.len());
52
-
53
- for (idx, field) in self.schema.fields.iter().enumerate() {
54
- if let Some(val) = self.values.get_mut(idx).and_then(|v| v.take()) {
55
- obj.insert(field.name.clone(), val);
56
- }
57
- }
58
-
59
50
  if !self.list_values.is_empty() {
60
51
  for (idx, list_opt) in self.list_values.into_iter().enumerate() {
61
52
  if let Some(list) = list_opt {
62
- let field = &self.schema.fields[idx];
63
- obj.insert(field.name.clone(), PolyValue::List(list));
53
+ self.values[idx] = Some(PolyValue::List(list));
64
54
  }
65
55
  }
66
56
  }
@@ -72,12 +62,12 @@ impl StackFrame {
72
62
  match &field.val_type {
73
63
  ValueType::Scalar(st) => {
74
64
  let val = ValueConverter::parse_scalar(st, text_buf, &field.name)?;
75
- obj.insert(field.name.clone(), val);
65
+ self.values[text_idx] = Some(val);
76
66
  }
77
67
  ValueType::List(inner) => {
78
68
  if let ValueType::Scalar(st) = &**inner {
79
69
  let val = ValueConverter::parse_scalar(st, text_buf, &field.name)?;
80
- obj.insert(field.name.clone(), PolyValue::List(vec![val]));
70
+ self.values[text_idx] = Some(PolyValue::List(vec![val]));
81
71
  }
82
72
  }
83
73
  _ => {}
@@ -86,7 +76,10 @@ impl StackFrame {
86
76
  }
87
77
  }
88
78
 
89
- Ok(PolyValue::Object(obj))
79
+ Ok(PolyValue::Record {
80
+ schema: self.schema,
81
+ values: self.values.into_vec().into_boxed_slice(),
82
+ })
90
83
  }
91
84
  }
92
85
 
@@ -313,12 +306,24 @@ impl XmlDeserializer {
313
306
  if unknown_depth > 0 {
314
307
  continue;
315
308
  }
316
- let unescaped = quick_xml::escape::unescape(e.as_ref())?;
317
- if active_scalar_field.is_some() {
318
- text_buf.extend_from_slice(unescaped.as_bytes());
319
- } else if let Some(frame) = stack.last_mut() {
320
- if let Some(ref mut tb) = frame.frame_text_buf {
321
- tb.extend_from_slice(unescaped.as_bytes());
309
+ let raw = e.as_ref();
310
+ let raw_bytes = raw.as_bytes();
311
+ if memchr::memchr(b'&', raw_bytes).is_none() {
312
+ if active_scalar_field.is_some() {
313
+ text_buf.extend_from_slice(raw_bytes);
314
+ } else if let Some(frame) = stack.last_mut() {
315
+ if let Some(ref mut tb) = frame.frame_text_buf {
316
+ tb.extend_from_slice(raw_bytes);
317
+ }
318
+ }
319
+ } else {
320
+ let unescaped = quick_xml::escape::unescape(raw)?;
321
+ if active_scalar_field.is_some() {
322
+ text_buf.extend_from_slice(unescaped.as_bytes());
323
+ } else if let Some(frame) = stack.last_mut() {
324
+ if let Some(ref mut tb) = frame.frame_text_buf {
325
+ tb.extend_from_slice(unescaped.as_bytes());
326
+ }
322
327
  }
323
328
  }
324
329
  }
@@ -212,12 +212,20 @@ impl XmlSerializer {
212
212
  is_root: bool,
213
213
  element_ns: Option<&str>,
214
214
  ) -> Result<()> {
215
- let obj = match value {
216
- PolyValue::Object(o) => o,
215
+ match value {
216
+ PolyValue::Record { .. } | PolyValue::Object(_) => {}
217
217
  _ => {
218
218
  return Err(PolyXmlError::SerializationError(
219
- "Expected Object value for model".into(),
220
- ))
219
+ "Expected Object or Record value for model".into(),
220
+ ));
221
+ }
222
+ }
223
+
224
+ let get_field = |idx: usize, name: &str| -> Option<&PolyValue> {
225
+ match value {
226
+ PolyValue::Record { values, .. } => values.get(idx).and_then(|v| v.as_ref()),
227
+ PolyValue::Object(o) => o.get(name),
228
+ _ => None,
221
229
  }
222
230
  };
223
231
 
@@ -245,9 +253,9 @@ impl XmlSerializer {
245
253
  }
246
254
 
247
255
  // 1. Collect and write attributes
248
- for field in &schema.fields {
256
+ for (idx, field) in schema.fields.iter().enumerate() {
249
257
  if field.kind == FieldKind::Attribute {
250
- if let Some(val) = obj.get(&field.name) {
258
+ if let Some(val) = get_field(idx, &field.name) {
251
259
  if !val.is_null() {
252
260
  let local_attr = std::str::from_utf8(&field.xml_name)?;
253
261
  let attr_name = if let Some(ctx) = ns_ctx {
@@ -271,7 +279,7 @@ impl XmlSerializer {
271
279
  // 2. Write text content if present
272
280
  if let Some(text_idx) = schema.text_field {
273
281
  let field = &schema.fields[text_idx];
274
- if let Some(val) = obj.get(&field.name) {
282
+ if let Some(val) = get_field(text_idx, &field.name) {
275
283
  let mut buf = [0u8; lexical_core::BUFFER_SIZE];
276
284
  if let Some(text_content) = Self::format_scalar_to(val, &mut buf) {
277
285
  if !text_content.is_empty() {
@@ -295,9 +303,9 @@ impl XmlSerializer {
295
303
  }
296
304
 
297
305
  // 3. Write child elements
298
- for field in &schema.fields {
306
+ for (idx, field) in schema.fields.iter().enumerate() {
299
307
  if field.kind == FieldKind::Element {
300
- if let Some(val) = obj.get(&field.name) {
308
+ if let Some(val) = get_field(idx, &field.name) {
301
309
  if val.is_null() {
302
310
  continue;
303
311
  }
@@ -0,0 +1,167 @@
1
+ use std::collections::HashMap;
2
+ use std::sync::Arc;
3
+
4
+ use crate::schema::ModelSchema;
5
+
6
+ #[derive(Debug, Clone)]
7
+ pub enum PolyValue {
8
+ Null,
9
+ Bool(bool),
10
+ Int(i64),
11
+ Float(f64),
12
+ String(String),
13
+ List(Vec<PolyValue>),
14
+ Object(HashMap<String, PolyValue>),
15
+ Record {
16
+ schema: Arc<ModelSchema>,
17
+ values: Box<[Option<PolyValue>]>,
18
+ },
19
+ }
20
+
21
+ impl PartialEq for PolyValue {
22
+ fn eq(&self, other: &Self) -> bool {
23
+ match (self, other) {
24
+ (PolyValue::Null, PolyValue::Null) => true,
25
+ (PolyValue::Bool(a), PolyValue::Bool(b)) => a == b,
26
+ (PolyValue::Int(a), PolyValue::Int(b)) => a == b,
27
+ (PolyValue::Float(a), PolyValue::Float(b)) => a == b,
28
+ (PolyValue::String(a), PolyValue::String(b)) => a == b,
29
+ (PolyValue::List(a), PolyValue::List(b)) => a == b,
30
+ (PolyValue::Object(a), PolyValue::Object(b)) => a == b,
31
+ (
32
+ PolyValue::Record {
33
+ schema: s1,
34
+ values: v1,
35
+ },
36
+ PolyValue::Record {
37
+ schema: s2,
38
+ values: v2,
39
+ },
40
+ ) => (Arc::ptr_eq(s1, s2) || s1.name == s2.name) && v1 == v2,
41
+ (PolyValue::Record { schema, values }, PolyValue::Object(map))
42
+ | (PolyValue::Object(map), PolyValue::Record { schema, values }) => {
43
+ for (idx, field) in schema.fields.iter().enumerate() {
44
+ let rec_val = values.get(idx).and_then(|v| v.as_ref());
45
+ let map_val = map.get(&field.name);
46
+ if rec_val != map_val {
47
+ return false;
48
+ }
49
+ }
50
+ true
51
+ }
52
+ _ => false,
53
+ }
54
+ }
55
+ }
56
+
57
+ impl PolyValue {
58
+ pub fn is_null(&self) -> bool {
59
+ matches!(self, PolyValue::Null)
60
+ }
61
+
62
+ pub fn as_str(&self) -> Option<&str> {
63
+ match self {
64
+ PolyValue::String(s) => Some(s.as_str()),
65
+ _ => None,
66
+ }
67
+ }
68
+
69
+ pub fn as_i64(&self) -> Option<i64> {
70
+ match self {
71
+ PolyValue::Int(i) => Some(*i),
72
+ _ => None,
73
+ }
74
+ }
75
+
76
+ pub fn as_f64(&self) -> Option<f64> {
77
+ match self {
78
+ PolyValue::Float(f) => Some(*f),
79
+ _ => None,
80
+ }
81
+ }
82
+
83
+ pub fn as_bool(&self) -> Option<bool> {
84
+ match self {
85
+ PolyValue::Bool(b) => Some(*b),
86
+ _ => None,
87
+ }
88
+ }
89
+
90
+ pub fn as_list(&self) -> Option<&[PolyValue]> {
91
+ match self {
92
+ PolyValue::List(l) => Some(l.as_slice()),
93
+ _ => None,
94
+ }
95
+ }
96
+
97
+ pub fn as_object(&self) -> Option<&HashMap<String, PolyValue>> {
98
+ match self {
99
+ PolyValue::Object(o) => Some(o),
100
+ _ => None,
101
+ }
102
+ }
103
+
104
+ pub fn as_record(&self) -> Option<(&Arc<ModelSchema>, &[Option<PolyValue>])> {
105
+ match self {
106
+ PolyValue::Record { schema, values } => Some((schema, values.as_ref())),
107
+ _ => None,
108
+ }
109
+ }
110
+
111
+ pub fn get_by_index(&self, idx: usize) -> Option<&PolyValue> {
112
+ match self {
113
+ PolyValue::Record { values, .. } => values.get(idx).and_then(|v| v.as_ref()),
114
+ _ => None,
115
+ }
116
+ }
117
+
118
+ pub fn get(&self, key: &str) -> Option<&PolyValue> {
119
+ match self {
120
+ PolyValue::Object(o) => o.get(key),
121
+ PolyValue::Record { schema, values } => {
122
+ let idx = schema.fields.iter().position(|f| f.name == key)?;
123
+ values.get(idx).and_then(|v| v.as_ref())
124
+ }
125
+ _ => None,
126
+ }
127
+ }
128
+
129
+ pub fn to_hash_map(&self) -> Option<HashMap<String, PolyValue>> {
130
+ match self {
131
+ PolyValue::Object(o) => Some(o.clone()),
132
+ PolyValue::Record { schema, values } => {
133
+ let mut map = HashMap::with_capacity(schema.fields.len());
134
+ for (idx, field) in schema.fields.iter().enumerate() {
135
+ if let Some(Some(val)) = values.get(idx) {
136
+ map.insert(field.name.clone(), val.clone());
137
+ }
138
+ }
139
+ Some(map)
140
+ }
141
+ _ => None,
142
+ }
143
+ }
144
+
145
+ /// Serialize this PolyValue into raw JSON bytes.
146
+ pub fn to_json(&self, indent: Option<usize>) -> crate::error::Result<Vec<u8>> {
147
+ let json_val = serde_json::Value::from(self);
148
+ match indent {
149
+ Some(spaces) if spaces > 0 => {
150
+ let indent_str = " ".repeat(spaces);
151
+ let formatter =
152
+ serde_json::ser::PrettyFormatter::with_indent(indent_str.as_bytes());
153
+ let mut buf = Vec::new();
154
+ let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
155
+ serde::Serialize::serialize(&json_val, &mut ser)?;
156
+ Ok(buf)
157
+ }
158
+ _ => Ok(serde_json::to_vec(&json_val)?),
159
+ }
160
+ }
161
+
162
+ /// Deserialize raw JSON bytes into a dynamic PolyValue without a schema.
163
+ pub fn from_json(json_bytes: &[u8]) -> crate::error::Result<Self> {
164
+ let json_val: serde_json::Value = serde_json::from_slice(json_bytes)?;
165
+ Ok(PolyValue::from(json_val))
166
+ }
167
+ }
@@ -12,11 +12,14 @@ use std::sync::{Arc, RwLock};
12
12
  use polyxml::schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType};
13
13
  use polyxml::value::PolyValue;
14
14
 
15
+ use smallvec::SmallVec;
16
+
15
17
  type PyObject = Py<PyAny>;
16
18
 
17
19
  struct CachedFieldMeta {
18
20
  py_string: Py<PyString>,
19
21
  is_init: bool,
22
+ #[allow(dead_code)]
20
23
  is_kw_only: bool,
21
24
  py_type: Option<PyObject>,
22
25
  }
@@ -25,6 +28,8 @@ struct CachedSchemaMeta {
25
28
  schema: Arc<ModelSchema>,
26
29
  py_cls: PyObject,
27
30
  is_dataclass: bool,
31
+ all_init: bool,
32
+ kwnames: Option<Py<PyTuple>>,
28
33
  fields: Vec<CachedFieldMeta>,
29
34
  }
30
35
 
@@ -394,10 +399,24 @@ fn get_or_create_schema_meta<'py>(cls: &Bound<'py, PyType>) -> PyResult<Arc<Cach
394
399
  let (schema, cached_fields) = extract_schema_from_class(cls)?;
395
400
  let py_cls_obj: PyObject = cls.clone().into_any().unbind();
396
401
 
402
+ let all_init =
403
+ is_dataclass && !cached_fields.is_empty() && cached_fields.iter().all(|f| f.is_init);
404
+ let kwnames = if all_init {
405
+ let py_strings: Vec<Bound<'py, PyString>> = cached_fields
406
+ .iter()
407
+ .map(|f| f.py_string.bind(cls.py()).clone())
408
+ .collect();
409
+ PyTuple::new(cls.py(), &py_strings).ok().map(|t| t.unbind())
410
+ } else {
411
+ None
412
+ };
413
+
397
414
  let meta = Arc::new(CachedSchemaMeta {
398
415
  schema: Arc::clone(&schema),
399
416
  py_cls: py_cls_obj,
400
417
  is_dataclass,
418
+ all_init,
419
+ kwnames,
401
420
  fields: cached_fields,
402
421
  });
403
422
 
@@ -559,7 +578,10 @@ fn poly_value_to_py<'py>(
559
578
  Ok(py_list.into_any().unbind())
560
579
  }
561
580
  }
562
- PolyValue::Object(map) => {
581
+ PolyValue::Record {
582
+ schema: rec_schema,
583
+ values,
584
+ } => {
563
585
  let meta_opt = if let ValueType::Nested(ref s) = val_type {
564
586
  lookup_cached_meta(&s.name)
565
587
  } else {
@@ -573,40 +595,194 @@ fn poly_value_to_py<'py>(
573
595
 
574
596
  if let Some(ref target_cls) = effective_cls {
575
597
  if let ValueType::Nested(ref schema) = val_type {
576
- // Method 1: Fast Positional Tuple Construction for Dataclasses when all fields are present AND all fields are is_init and not kw_only
598
+ // Method 1: Python 3.12+ Vectorcall for Dataclasses
577
599
  if let Some(ref meta) = meta_opt {
578
- if meta.is_dataclass
579
- && schema.fields.len() == map.len()
580
- && meta.fields.iter().all(|f| f.is_init && !f.is_kw_only)
581
- {
582
- let mut args_vec = Vec::with_capacity(schema.fields.len());
583
- let mut all_found = true;
584
- for (i, field) in schema.fields.iter().enumerate() {
585
- if let Some(field_val) = map.get(&field.name) {
586
- let field_cls = if let ValueType::Nested(ref s) = field.val_type
587
- {
588
- lookup_py_class(py, &s.name)
600
+ if meta.is_dataclass && meta.all_init && !meta.fields.is_empty() {
601
+ if let Some(ref kwnames) = meta.kwnames {
602
+ let mut args_ptrs: SmallVec<[*mut pyo3::ffi::PyObject; 16]> =
603
+ SmallVec::with_capacity(schema.fields.len());
604
+ let mut py_vals: SmallVec<[PyObject; 16]> =
605
+ SmallVec::with_capacity(schema.fields.len());
606
+ let mut all_found = true;
607
+
608
+ for (i, field) in schema.fields.iter().enumerate() {
609
+ if let Some(Some(field_val)) = values.get(i) {
610
+ let field_cls =
611
+ if let ValueType::Nested(ref s) = field.val_type {
612
+ lookup_py_class(py, &s.name)
613
+ } else {
614
+ None
615
+ };
616
+ let f_meta = meta.fields.get(i);
617
+ let py_val = poly_value_to_py(
618
+ py,
619
+ field_val,
620
+ &field.val_type,
621
+ field_cls.as_ref(),
622
+ f_meta,
623
+ )?;
624
+ args_ptrs.push(py_val.as_ptr());
625
+ py_vals.push(py_val);
589
626
  } else {
590
- None
591
- };
592
- let f_meta = meta.fields.get(i);
593
- let py_val = poly_value_to_py(
594
- py,
595
- field_val,
596
- &field.val_type,
597
- field_cls.as_ref(),
598
- f_meta,
599
- )?;
600
- args_vec.push(py_val);
627
+ all_found = false;
628
+ break;
629
+ }
630
+ }
631
+
632
+ if all_found && args_ptrs.len() == schema.fields.len() {
633
+ unsafe {
634
+ let res_ptr = pyo3::ffi::PyObject_Vectorcall(
635
+ target_cls.as_ptr(),
636
+ args_ptrs.as_ptr(),
637
+ 0,
638
+ kwnames.as_ptr(),
639
+ );
640
+ if !res_ptr.is_null() {
641
+ return Ok(Bound::from_owned_ptr(py, res_ptr).unbind());
642
+ } else {
643
+ return Err(PyErr::fetch(py));
644
+ }
645
+ }
646
+ }
647
+ }
648
+ }
649
+ }
650
+
651
+ // Method 2: Keyword arguments with init=False post-init support
652
+ let kwargs = PyDict::new(py);
653
+ let mut post_init_fields: Vec<(Py<PyString>, PyObject)> = Vec::new();
654
+
655
+ for (i, field) in schema.fields.iter().enumerate() {
656
+ if let Some(Some(field_val)) = values.get(i) {
657
+ let field_cls = if let ValueType::Nested(ref s) = field.val_type {
658
+ lookup_py_class(py, &s.name)
659
+ } else {
660
+ None
661
+ };
662
+ let f_meta = meta_opt.as_ref().and_then(|m| m.fields.get(i));
663
+ let is_init = f_meta.map(|f| f.is_init).unwrap_or(true);
664
+ let py_val = poly_value_to_py(
665
+ py,
666
+ field_val,
667
+ &field.val_type,
668
+ field_cls.as_ref(),
669
+ f_meta,
670
+ )?;
671
+ if let Some(meta) = f_meta {
672
+ if is_init {
673
+ kwargs.set_item(meta.py_string.bind(py), py_val)?;
601
674
  } else {
602
- all_found = false;
603
- break;
675
+ post_init_fields.push((meta.py_string.clone_ref(py), py_val));
604
676
  }
677
+ } else if is_init {
678
+ kwargs.set_item(&field.name, py_val)?;
679
+ } else {
680
+ post_init_fields
681
+ .push((PyString::new(py, &field.name).unbind(), py_val));
605
682
  }
606
- if all_found {
607
- let tuple = pyo3::types::PyTuple::new(py, &args_vec)?;
608
- if let Ok(instance) = target_cls.call1(tuple) {
609
- return Ok(instance.unbind());
683
+ }
684
+ }
685
+ let instance = target_cls.call((), Some(&kwargs))?;
686
+ for (attr_name, val) in post_init_fields {
687
+ instance.setattr(&attr_name, val)?;
688
+ }
689
+ return Ok(instance.unbind());
690
+ }
691
+
692
+ let dict = PyDict::new(py);
693
+ for (i, field) in rec_schema.fields.iter().enumerate() {
694
+ if let Some(Some(v)) = values.get(i) {
695
+ let py_v = poly_value_to_py(
696
+ py,
697
+ v,
698
+ &ValueType::Scalar(ScalarType::Any),
699
+ None,
700
+ None,
701
+ )?;
702
+ dict.set_item(&field.name, py_v)?;
703
+ }
704
+ }
705
+ let instance = target_cls.call((), Some(&dict))?;
706
+ Ok(instance.unbind())
707
+ } else {
708
+ let dict = PyDict::new(py);
709
+ for (i, field) in rec_schema.fields.iter().enumerate() {
710
+ if let Some(Some(v)) = values.get(i) {
711
+ let py_v = poly_value_to_py(
712
+ py,
713
+ v,
714
+ &ValueType::Scalar(ScalarType::Any),
715
+ None,
716
+ None,
717
+ )?;
718
+ dict.set_item(&field.name, py_v)?;
719
+ }
720
+ }
721
+ Ok(dict.into_any().unbind())
722
+ }
723
+ }
724
+ PolyValue::Object(map) => {
725
+ let meta_opt = if let ValueType::Nested(ref s) = val_type {
726
+ lookup_cached_meta(&s.name)
727
+ } else {
728
+ None
729
+ };
730
+
731
+ let effective_cls = match cls {
732
+ Some(c) => Some(c.clone()),
733
+ None => meta_opt.as_ref().map(|m| m.py_cls.bind(py).clone()),
734
+ };
735
+
736
+ if let Some(ref target_cls) = effective_cls {
737
+ if let ValueType::Nested(ref schema) = val_type {
738
+ // Method 1: Python 3.12+ Vectorcall for Dataclasses
739
+ if let Some(ref meta) = meta_opt {
740
+ if meta.is_dataclass && meta.all_init && !meta.fields.is_empty() {
741
+ if let Some(ref kwnames) = meta.kwnames {
742
+ let mut args_ptrs: SmallVec<[*mut pyo3::ffi::PyObject; 16]> =
743
+ SmallVec::with_capacity(schema.fields.len());
744
+ let mut py_vals: SmallVec<[PyObject; 16]> =
745
+ SmallVec::with_capacity(schema.fields.len());
746
+ let mut all_found = true;
747
+
748
+ for (i, field) in schema.fields.iter().enumerate() {
749
+ if let Some(field_val) = map.get(&field.name) {
750
+ let field_cls =
751
+ if let ValueType::Nested(ref s) = field.val_type {
752
+ lookup_py_class(py, &s.name)
753
+ } else {
754
+ None
755
+ };
756
+ let f_meta = meta.fields.get(i);
757
+ let py_val = poly_value_to_py(
758
+ py,
759
+ field_val,
760
+ &field.val_type,
761
+ field_cls.as_ref(),
762
+ f_meta,
763
+ )?;
764
+ args_ptrs.push(py_val.as_ptr());
765
+ py_vals.push(py_val);
766
+ } else {
767
+ all_found = false;
768
+ break;
769
+ }
770
+ }
771
+
772
+ if all_found && args_ptrs.len() == schema.fields.len() {
773
+ unsafe {
774
+ let res_ptr = pyo3::ffi::PyObject_Vectorcall(
775
+ target_cls.as_ptr(),
776
+ args_ptrs.as_ptr(),
777
+ 0,
778
+ kwnames.as_ptr(),
779
+ );
780
+ if !res_ptr.is_null() {
781
+ return Ok(Bound::from_owned_ptr(py, res_ptr).unbind());
782
+ } else {
783
+ return Err(PyErr::fetch(py));
784
+ }
785
+ }
610
786
  }
611
787
  }
612
788
  }
@@ -679,18 +855,99 @@ fn py_to_poly_value<'py>(
679
855
  schema: &ModelSchema,
680
856
  meta_opt: Option<&CachedSchemaMeta>,
681
857
  ) -> PyResult<PolyValue> {
682
- let mut map = HashMap::with_capacity(schema.fields.len());
858
+ if let Some(meta) = meta_opt {
859
+ let mut values: Vec<Option<PolyValue>> = vec![None; schema.fields.len()];
683
860
 
684
- for (i, field) in schema.fields.iter().enumerate() {
685
- let val_res = if let Some(meta) = meta_opt {
686
- if let Some(f_meta) = meta.fields.get(i) {
861
+ for (i, field) in schema.fields.iter().enumerate() {
862
+ let val_res = if let Some(f_meta) = meta.fields.get(i) {
687
863
  obj.getattr(f_meta.py_string.bind(py))
688
864
  } else {
689
865
  obj.getattr(field.name.as_str())
866
+ };
867
+
868
+ if let Ok(val) = val_res {
869
+ if val.is_none() {
870
+ values[i] = Some(PolyValue::Null);
871
+ continue;
872
+ }
873
+
874
+ match &field.val_type {
875
+ ValueType::Scalar(st) => match st {
876
+ ScalarType::Int => {
877
+ let int_val: i64 = if let Ok(enum_val) = val.getattr("value") {
878
+ enum_val.extract()?
879
+ } else {
880
+ val.extract()?
881
+ };
882
+ values[i] = Some(PolyValue::Int(int_val));
883
+ }
884
+ ScalarType::Float => {
885
+ let f: f64 = val.extract()?;
886
+ values[i] = Some(PolyValue::Float(f));
887
+ }
888
+ ScalarType::Bool => {
889
+ let b: bool = val.extract()?;
890
+ values[i] = Some(PolyValue::Bool(b));
891
+ }
892
+ _ => {
893
+ let s: String = if let Ok(enum_val) = val.getattr("value") {
894
+ enum_val.str()?.extract()?
895
+ } else {
896
+ val.str()?.extract()?
897
+ };
898
+ values[i] = Some(PolyValue::String(s));
899
+ }
900
+ },
901
+ ValueType::List(inner) => {
902
+ if let Ok(list) = val.cast::<PyList>() {
903
+ let mut poly_items = Vec::with_capacity(list.len());
904
+ let child_meta = if let ValueType::Nested(sub_schema) = inner.as_ref() {
905
+ lookup_cached_meta(&sub_schema.name)
906
+ } else {
907
+ None
908
+ };
909
+ for item in list.iter() {
910
+ if let ValueType::Nested(sub_schema) = inner.as_ref() {
911
+ poly_items.push(py_to_poly_value(
912
+ py,
913
+ &item,
914
+ sub_schema,
915
+ child_meta.as_deref(),
916
+ )?);
917
+ } else {
918
+ let s: String = if let Ok(enum_val) = item.getattr("value") {
919
+ enum_val.str()?.extract()?
920
+ } else {
921
+ item.str()?.extract()?
922
+ };
923
+ poly_items.push(PolyValue::String(s));
924
+ }
925
+ }
926
+ values[i] = Some(PolyValue::List(poly_items));
927
+ }
928
+ }
929
+ ValueType::Nested(sub_schema) => {
930
+ let child_meta = lookup_cached_meta(&sub_schema.name);
931
+ values[i] = Some(py_to_poly_value(
932
+ py,
933
+ &val,
934
+ sub_schema,
935
+ child_meta.as_deref(),
936
+ )?);
937
+ }
938
+ }
690
939
  }
691
- } else {
692
- obj.getattr(field.name.as_str())
693
- };
940
+ }
941
+ return Ok(PolyValue::Record {
942
+ schema: Arc::clone(&meta.schema),
943
+ values: values.into_boxed_slice(),
944
+ });
945
+ }
946
+
947
+ let mut map = HashMap::with_capacity(schema.fields.len());
948
+
949
+ for field in &schema.fields {
950
+ let val_res = obj.getattr(field.name.as_str());
694
951
 
695
952
  if let Ok(val) = val_res {
696
953
  if val.is_none() {
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "polyxml"
7
- version = "0.19.1"
7
+ version = "0.19.2"
8
8
  description = "High-performance, polyglot XML data-binding engine built in Rust"
9
9
  authors = [
10
10
  { name = "Bailey Nguyen", email = "bailey.tan.nguyen@gmail.com" }
@@ -1,90 +0,0 @@
1
- use std::collections::HashMap;
2
-
3
- #[derive(Debug, Clone, PartialEq)]
4
- pub enum PolyValue {
5
- Null,
6
- Bool(bool),
7
- Int(i64),
8
- Float(f64),
9
- String(String),
10
- List(Vec<PolyValue>),
11
- Object(HashMap<String, PolyValue>),
12
- }
13
-
14
- impl PolyValue {
15
- pub fn is_null(&self) -> bool {
16
- matches!(self, PolyValue::Null)
17
- }
18
-
19
- pub fn as_str(&self) -> Option<&str> {
20
- match self {
21
- PolyValue::String(s) => Some(s.as_str()),
22
- _ => None,
23
- }
24
- }
25
-
26
- pub fn as_i64(&self) -> Option<i64> {
27
- match self {
28
- PolyValue::Int(i) => Some(*i),
29
- _ => None,
30
- }
31
- }
32
-
33
- pub fn as_f64(&self) -> Option<f64> {
34
- match self {
35
- PolyValue::Float(f) => Some(*f),
36
- _ => None,
37
- }
38
- }
39
-
40
- pub fn as_bool(&self) -> Option<bool> {
41
- match self {
42
- PolyValue::Bool(b) => Some(*b),
43
- _ => None,
44
- }
45
- }
46
-
47
- pub fn as_list(&self) -> Option<&[PolyValue]> {
48
- match self {
49
- PolyValue::List(l) => Some(l.as_slice()),
50
- _ => None,
51
- }
52
- }
53
-
54
- pub fn as_object(&self) -> Option<&HashMap<String, PolyValue>> {
55
- match self {
56
- PolyValue::Object(o) => Some(o),
57
- _ => None,
58
- }
59
- }
60
-
61
- pub fn get(&self, key: &str) -> Option<&PolyValue> {
62
- match self {
63
- PolyValue::Object(o) => o.get(key),
64
- _ => None,
65
- }
66
- }
67
-
68
- /// Serialize this PolyValue into raw JSON bytes.
69
- pub fn to_json(&self, indent: Option<usize>) -> crate::error::Result<Vec<u8>> {
70
- let json_val = serde_json::Value::from(self);
71
- match indent {
72
- Some(spaces) if spaces > 0 => {
73
- let indent_str = " ".repeat(spaces);
74
- let formatter =
75
- serde_json::ser::PrettyFormatter::with_indent(indent_str.as_bytes());
76
- let mut buf = Vec::new();
77
- let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
78
- serde::Serialize::serialize(&json_val, &mut ser)?;
79
- Ok(buf)
80
- }
81
- _ => Ok(serde_json::to_vec(&json_val)?),
82
- }
83
- }
84
-
85
- /// Deserialize raw JSON bytes into a dynamic PolyValue without a schema.
86
- pub fn from_json(json_bytes: &[u8]) -> crate::error::Result<Self> {
87
- let json_val: serde_json::Value = serde_json::from_slice(json_bytes)?;
88
- Ok(PolyValue::from(json_val))
89
- }
90
- }
File without changes