json2xml-rs 0.1.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.
@@ -0,0 +1,172 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.5.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
10
+
11
+ [[package]]
12
+ name = "heck"
13
+ version = "0.5.0"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
16
+
17
+ [[package]]
18
+ name = "indoc"
19
+ version = "2.0.7"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
22
+ dependencies = [
23
+ "rustversion",
24
+ ]
25
+
26
+ [[package]]
27
+ name = "json2xml_rs"
28
+ version = "0.1.0"
29
+ dependencies = [
30
+ "pyo3",
31
+ ]
32
+
33
+ [[package]]
34
+ name = "libc"
35
+ version = "0.2.180"
36
+ source = "registry+https://github.com/rust-lang/crates.io-index"
37
+ checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
38
+
39
+ [[package]]
40
+ name = "memoffset"
41
+ version = "0.9.1"
42
+ source = "registry+https://github.com/rust-lang/crates.io-index"
43
+ checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
44
+ dependencies = [
45
+ "autocfg",
46
+ ]
47
+
48
+ [[package]]
49
+ name = "once_cell"
50
+ version = "1.21.3"
51
+ source = "registry+https://github.com/rust-lang/crates.io-index"
52
+ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
53
+
54
+ [[package]]
55
+ name = "portable-atomic"
56
+ version = "1.13.0"
57
+ source = "registry+https://github.com/rust-lang/crates.io-index"
58
+ checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
59
+
60
+ [[package]]
61
+ name = "proc-macro2"
62
+ version = "1.0.105"
63
+ source = "registry+https://github.com/rust-lang/crates.io-index"
64
+ checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
65
+ dependencies = [
66
+ "unicode-ident",
67
+ ]
68
+
69
+ [[package]]
70
+ name = "pyo3"
71
+ version = "0.27.2"
72
+ source = "registry+https://github.com/rust-lang/crates.io-index"
73
+ checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d"
74
+ dependencies = [
75
+ "indoc",
76
+ "libc",
77
+ "memoffset",
78
+ "once_cell",
79
+ "portable-atomic",
80
+ "pyo3-build-config",
81
+ "pyo3-ffi",
82
+ "pyo3-macros",
83
+ "unindent",
84
+ ]
85
+
86
+ [[package]]
87
+ name = "pyo3-build-config"
88
+ version = "0.27.2"
89
+ source = "registry+https://github.com/rust-lang/crates.io-index"
90
+ checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6"
91
+ dependencies = [
92
+ "target-lexicon",
93
+ ]
94
+
95
+ [[package]]
96
+ name = "pyo3-ffi"
97
+ version = "0.27.2"
98
+ source = "registry+https://github.com/rust-lang/crates.io-index"
99
+ checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089"
100
+ dependencies = [
101
+ "libc",
102
+ "pyo3-build-config",
103
+ ]
104
+
105
+ [[package]]
106
+ name = "pyo3-macros"
107
+ version = "0.27.2"
108
+ source = "registry+https://github.com/rust-lang/crates.io-index"
109
+ checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02"
110
+ dependencies = [
111
+ "proc-macro2",
112
+ "pyo3-macros-backend",
113
+ "quote",
114
+ "syn",
115
+ ]
116
+
117
+ [[package]]
118
+ name = "pyo3-macros-backend"
119
+ version = "0.27.2"
120
+ source = "registry+https://github.com/rust-lang/crates.io-index"
121
+ checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9"
122
+ dependencies = [
123
+ "heck",
124
+ "proc-macro2",
125
+ "pyo3-build-config",
126
+ "quote",
127
+ "syn",
128
+ ]
129
+
130
+ [[package]]
131
+ name = "quote"
132
+ version = "1.0.43"
133
+ source = "registry+https://github.com/rust-lang/crates.io-index"
134
+ checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
135
+ dependencies = [
136
+ "proc-macro2",
137
+ ]
138
+
139
+ [[package]]
140
+ name = "rustversion"
141
+ version = "1.0.22"
142
+ source = "registry+https://github.com/rust-lang/crates.io-index"
143
+ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
144
+
145
+ [[package]]
146
+ name = "syn"
147
+ version = "2.0.114"
148
+ source = "registry+https://github.com/rust-lang/crates.io-index"
149
+ checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
150
+ dependencies = [
151
+ "proc-macro2",
152
+ "quote",
153
+ "unicode-ident",
154
+ ]
155
+
156
+ [[package]]
157
+ name = "target-lexicon"
158
+ version = "0.13.4"
159
+ source = "registry+https://github.com/rust-lang/crates.io-index"
160
+ checksum = "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba"
161
+
162
+ [[package]]
163
+ name = "unicode-ident"
164
+ version = "1.0.22"
165
+ source = "registry+https://github.com/rust-lang/crates.io-index"
166
+ checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
167
+
168
+ [[package]]
169
+ name = "unindent"
170
+ version = "0.2.4"
171
+ source = "registry+https://github.com/rust-lang/crates.io-index"
172
+ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
@@ -0,0 +1,19 @@
1
+ [package]
2
+ name = "json2xml_rs"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Fast native JSON to XML conversion for Python"
6
+ license = "Apache-2.0"
7
+ readme = "README.md"
8
+
9
+ [lib]
10
+ name = "json2xml_rs"
11
+ crate-type = ["cdylib"]
12
+
13
+ [dependencies]
14
+ pyo3 = { version = "0.27", features = ["extension-module"] }
15
+
16
+ [profile.release]
17
+ lto = true
18
+ codegen-units = 1
19
+ opt-level = 3
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: json2xml_rs
3
+ Version: 0.1.0
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
7
+ Summary: Fast native JSON to XML conversion - Rust extension for json2xml
8
+ License: Apache-2.0
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
11
+
12
+ # json2xml_rs - Rust Extension for json2xml
13
+
14
+ A high-performance Rust implementation of the dicttoxml module using PyO3.
15
+
16
+ ## Building
17
+
18
+ ### Prerequisites
19
+
20
+ - Rust (1.70+)
21
+ - Python (3.9+)
22
+ - maturin (`pip install maturin`)
23
+
24
+ ### Development Build
25
+
26
+ ```bash
27
+ cd rust
28
+ maturin develop --release
29
+ ```
30
+
31
+ This builds the extension and installs it in your current Python environment.
32
+
33
+ ### Production Build
34
+
35
+ ```bash
36
+ cd rust
37
+ maturin build --release
38
+ ```
39
+
40
+ The wheel will be in `target/wheels/`.
41
+
42
+ ## Usage
43
+
44
+ ```python
45
+ # Direct usage
46
+ from json2xml_rs import dicttoxml
47
+
48
+ data = {"name": "John", "age": 30, "active": True}
49
+ xml_bytes = dicttoxml(data)
50
+ print(xml_bytes.decode())
51
+
52
+ # Or use the hybrid module that auto-selects the fastest backend
53
+ from json2xml import dicttoxml_fast
54
+ xml_bytes = dicttoxml_fast.dicttoxml(data)
55
+ ```
56
+
57
+ ## API
58
+
59
+ ### `dicttoxml(obj, root=True, custom_root="root", attr_type=True, item_wrap=True, cdata=False, list_headers=False) -> bytes`
60
+
61
+ Convert a Python dict or list to XML.
62
+
63
+ **Parameters:**
64
+ - `obj`: The Python object to convert (dict or list)
65
+ - `root`: Include XML declaration and root element (default: True)
66
+ - `custom_root`: Name of the root element (default: "root")
67
+ - `attr_type`: Include type attributes on elements (default: True)
68
+ - `item_wrap`: Wrap list items in `<item>` tags (default: True)
69
+ - `cdata`: Wrap string values in CDATA sections (default: False)
70
+ - `list_headers`: Repeat parent tag for each list item (default: False)
71
+
72
+ **Returns:** UTF-8 encoded XML as bytes
73
+
74
+ ### `escape_xml_py(s: str) -> str`
75
+
76
+ Escape special XML characters (&, ", ', <, >) in a string.
77
+
78
+ ### `wrap_cdata_py(s: str) -> str`
79
+
80
+ Wrap a string in a CDATA section.
81
+
82
+ ## Performance
83
+
84
+ The Rust implementation is expected to be 5-15x faster than pure Python for:
85
+
86
+ - String escaping (single-pass vs. multiple `.replace()` calls)
87
+ - Type dispatch (compiled match statements vs. `isinstance()` chains)
88
+ - String building (pre-allocated buffers vs. f-string concatenation)
89
+
90
+ ## Limitations
91
+
92
+ The Rust implementation currently does not support:
93
+
94
+ - `ids` parameter (unique IDs for elements)
95
+ - `item_func` parameter (custom item naming function)
96
+ - `xml_namespaces` parameter
97
+ - `xpath_format` parameter
98
+ - `@attrs`, `@val`, `@flat` special dict keys
99
+
100
+ For these features, fall back to the pure Python implementation.
101
+
102
+ ## Development
103
+
104
+ ### Running Tests
105
+
106
+ ```bash
107
+ cd rust
108
+ maturin develop
109
+ python -m pytest ../tests/
110
+ ```
111
+
112
+ ### Benchmarking
113
+
114
+ ```bash
115
+ cd ..
116
+ python benchmark_rust.py
117
+ ```
118
+
@@ -0,0 +1,106 @@
1
+ # json2xml_rs - Rust Extension for json2xml
2
+
3
+ A high-performance Rust implementation of the dicttoxml module using PyO3.
4
+
5
+ ## Building
6
+
7
+ ### Prerequisites
8
+
9
+ - Rust (1.70+)
10
+ - Python (3.9+)
11
+ - maturin (`pip install maturin`)
12
+
13
+ ### Development Build
14
+
15
+ ```bash
16
+ cd rust
17
+ maturin develop --release
18
+ ```
19
+
20
+ This builds the extension and installs it in your current Python environment.
21
+
22
+ ### Production Build
23
+
24
+ ```bash
25
+ cd rust
26
+ maturin build --release
27
+ ```
28
+
29
+ The wheel will be in `target/wheels/`.
30
+
31
+ ## Usage
32
+
33
+ ```python
34
+ # Direct usage
35
+ from json2xml_rs import dicttoxml
36
+
37
+ data = {"name": "John", "age": 30, "active": True}
38
+ xml_bytes = dicttoxml(data)
39
+ print(xml_bytes.decode())
40
+
41
+ # Or use the hybrid module that auto-selects the fastest backend
42
+ from json2xml import dicttoxml_fast
43
+ xml_bytes = dicttoxml_fast.dicttoxml(data)
44
+ ```
45
+
46
+ ## API
47
+
48
+ ### `dicttoxml(obj, root=True, custom_root="root", attr_type=True, item_wrap=True, cdata=False, list_headers=False) -> bytes`
49
+
50
+ Convert a Python dict or list to XML.
51
+
52
+ **Parameters:**
53
+ - `obj`: The Python object to convert (dict or list)
54
+ - `root`: Include XML declaration and root element (default: True)
55
+ - `custom_root`: Name of the root element (default: "root")
56
+ - `attr_type`: Include type attributes on elements (default: True)
57
+ - `item_wrap`: Wrap list items in `<item>` tags (default: True)
58
+ - `cdata`: Wrap string values in CDATA sections (default: False)
59
+ - `list_headers`: Repeat parent tag for each list item (default: False)
60
+
61
+ **Returns:** UTF-8 encoded XML as bytes
62
+
63
+ ### `escape_xml_py(s: str) -> str`
64
+
65
+ Escape special XML characters (&, ", ', <, >) in a string.
66
+
67
+ ### `wrap_cdata_py(s: str) -> str`
68
+
69
+ Wrap a string in a CDATA section.
70
+
71
+ ## Performance
72
+
73
+ The Rust implementation is expected to be 5-15x faster than pure Python for:
74
+
75
+ - String escaping (single-pass vs. multiple `.replace()` calls)
76
+ - Type dispatch (compiled match statements vs. `isinstance()` chains)
77
+ - String building (pre-allocated buffers vs. f-string concatenation)
78
+
79
+ ## Limitations
80
+
81
+ The Rust implementation currently does not support:
82
+
83
+ - `ids` parameter (unique IDs for elements)
84
+ - `item_func` parameter (custom item naming function)
85
+ - `xml_namespaces` parameter
86
+ - `xpath_format` parameter
87
+ - `@attrs`, `@val`, `@flat` special dict keys
88
+
89
+ For these features, fall back to the pure Python implementation.
90
+
91
+ ## Development
92
+
93
+ ### Running Tests
94
+
95
+ ```bash
96
+ cd rust
97
+ maturin develop
98
+ python -m pytest ../tests/
99
+ ```
100
+
101
+ ### Benchmarking
102
+
103
+ ```bash
104
+ cd ..
105
+ python benchmark_rust.py
106
+ ```
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.4,<2.0"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "json2xml_rs"
7
+ version = "0.1.0"
8
+ description = "Fast native JSON to XML conversion - Rust extension for json2xml"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "Apache-2.0"}
12
+ classifiers = [
13
+ "Programming Language :: Rust",
14
+ "Programming Language :: Python :: Implementation :: CPython",
15
+ "Programming Language :: Python :: Implementation :: PyPy",
16
+ ]
17
+
18
+ [tool.maturin]
19
+ features = ["pyo3/extension-module"]
20
+ module-name = "json2xml_rs"
@@ -0,0 +1,668 @@
1
+ //! Fast native JSON to XML conversion for Python
2
+ //!
3
+ //! This module provides a high-performance Rust implementation of dicttoxml
4
+ //! that can be used as a drop-in replacement for the pure Python version.
5
+
6
+ use pyo3::prelude::*;
7
+ use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyList, PyString};
8
+ use std::fmt::Write;
9
+
10
+ /// Escape special XML characters in a string.
11
+ /// This is one of the hottest paths - optimized for single-pass processing.
12
+ #[inline]
13
+ fn escape_xml(s: &str) -> String {
14
+ let mut result = String::with_capacity(s.len() + s.len() / 10);
15
+ for c in s.chars() {
16
+ match c {
17
+ '&' => result.push_str("&amp;"),
18
+ '"' => result.push_str("&quot;"),
19
+ '\'' => result.push_str("&apos;"),
20
+ '<' => result.push_str("&lt;"),
21
+ '>' => result.push_str("&gt;"),
22
+ _ => result.push(c),
23
+ }
24
+ }
25
+ result
26
+ }
27
+
28
+ /// Wrap content in CDATA section
29
+ #[inline]
30
+ fn wrap_cdata(s: &str) -> String {
31
+ let escaped = s.replace("]]>", "]]]]><![CDATA[>");
32
+ format!("<![CDATA[{}]]>", escaped)
33
+ }
34
+
35
+ /// Check if a key is a valid XML element name (simplified check)
36
+ /// Full validation would require XML parsing, but this catches common issues
37
+ fn is_valid_xml_name(key: &str) -> bool {
38
+ if key.is_empty() {
39
+ return false;
40
+ }
41
+
42
+ let mut chars = key.chars();
43
+
44
+ // First character must be letter or underscore
45
+ match chars.next() {
46
+ Some(c) if c.is_alphabetic() || c == '_' => {}
47
+ _ => return false,
48
+ }
49
+
50
+ // Remaining characters can be letters, digits, hyphens, underscores, or periods
51
+ for c in chars {
52
+ if !(c.is_alphanumeric() || c == '-' || c == '_' || c == '.' || c == ':') {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ // Names starting with "xml" (case-insensitive) are reserved
58
+ !key.to_lowercase().starts_with("xml")
59
+ }
60
+
61
+ /// Make a valid XML name from a key, returning the key and any attributes
62
+ fn make_valid_xml_name(key: &str) -> (String, Option<(String, String)>) {
63
+ let escaped = escape_xml(key);
64
+
65
+ // Already valid
66
+ if is_valid_xml_name(&escaped) {
67
+ return (escaped, None);
68
+ }
69
+
70
+ // Numeric key - prepend 'n'
71
+ if escaped.chars().all(|c| c.is_ascii_digit()) {
72
+ return (format!("n{}", escaped), None);
73
+ }
74
+
75
+ // Try replacing spaces with underscores
76
+ let with_underscores = escaped.replace(' ', "_");
77
+ if is_valid_xml_name(&with_underscores) {
78
+ return (with_underscores, None);
79
+ }
80
+
81
+ // Fall back to using "key" with name attribute
82
+ ("key".to_string(), Some(("name".to_string(), escaped)))
83
+ }
84
+
85
+ /// Build an attribute string from key-value pairs
86
+ fn make_attr_string(attrs: &[(String, String)]) -> String {
87
+ if attrs.is_empty() {
88
+ return String::new();
89
+ }
90
+ let mut result = String::new();
91
+ for (k, v) in attrs {
92
+ write!(result, " {}=\"{}\"", k, escape_xml(v)).unwrap();
93
+ }
94
+ result
95
+ }
96
+
97
+ /// Configuration for XML conversion
98
+ struct ConvertConfig {
99
+ attr_type: bool,
100
+ cdata: bool,
101
+ item_wrap: bool,
102
+ list_headers: bool,
103
+ }
104
+
105
+ /// Convert a Python value to XML string
106
+ fn convert_value(
107
+ py: Python<'_>,
108
+ obj: &Bound<'_, PyAny>,
109
+ parent: &str,
110
+ config: &ConvertConfig,
111
+ item_name: &str,
112
+ ) -> PyResult<String> {
113
+ // Handle None
114
+ if obj.is_none() {
115
+ return convert_none(item_name, config);
116
+ }
117
+
118
+ // Handle bool (must check before int since bool is subclass of int in Python)
119
+ if obj.is_instance_of::<PyBool>() {
120
+ let val: bool = obj.extract()?;
121
+ return convert_bool(item_name, val, config);
122
+ }
123
+
124
+ // Handle int - try i64 first, fall back to string for large integers
125
+ if obj.is_instance_of::<PyInt>() {
126
+ let val_str = match obj.extract::<i64>() {
127
+ Ok(val) => val.to_string(),
128
+ Err(_) => obj.str()?.extract::<String>()?, // Fall back for big ints
129
+ };
130
+ return convert_number(item_name, &val_str, "int", config);
131
+ }
132
+
133
+ // Handle float
134
+ if obj.is_instance_of::<PyFloat>() {
135
+ let val: f64 = obj.extract()?;
136
+ return convert_number(item_name, &val.to_string(), "float", config);
137
+ }
138
+
139
+ // Handle string
140
+ if obj.is_instance_of::<PyString>() {
141
+ let val: String = obj.extract()?;
142
+ return convert_string(item_name, &val, config);
143
+ }
144
+
145
+ // Handle dict
146
+ if obj.is_instance_of::<PyDict>() {
147
+ let dict: &Bound<'_, PyDict> = obj.cast()?;
148
+ return convert_dict(py, dict, parent, config);
149
+ }
150
+
151
+ // Handle list
152
+ if obj.is_instance_of::<PyList>() {
153
+ let list: &Bound<'_, PyList> = obj.cast()?;
154
+ return convert_list(py, list, parent, config);
155
+ }
156
+
157
+ // Handle other sequences (tuples, etc.) - check if iterable via try_iter
158
+ if let Ok(iter) = obj.try_iter() {
159
+ let items: Vec<Bound<'_, PyAny>> = iter.filter_map(|r| r.ok()).collect();
160
+ let list = PyList::new(py, &items)?;
161
+ return convert_list(py, &list, parent, config);
162
+ }
163
+
164
+ // Fallback: convert to string
165
+ let val: String = obj.str()?.extract()?;
166
+ convert_string(item_name, &val, config)
167
+ }
168
+
169
+ /// Convert a string value to XML
170
+ fn convert_string(key: &str, val: &str, config: &ConvertConfig) -> PyResult<String> {
171
+ let (xml_key, name_attr) = make_valid_xml_name(key);
172
+ let mut attrs = Vec::new();
173
+
174
+ if let Some((k, v)) = name_attr {
175
+ attrs.push((k, v));
176
+ }
177
+ if config.attr_type {
178
+ attrs.push(("type".to_string(), "str".to_string()));
179
+ }
180
+
181
+ let attr_string = make_attr_string(&attrs);
182
+ let content = if config.cdata {
183
+ wrap_cdata(val)
184
+ } else {
185
+ escape_xml(val)
186
+ };
187
+
188
+ Ok(format!(
189
+ "<{}{}>{}</{}>",
190
+ xml_key, attr_string, content, xml_key
191
+ ))
192
+ }
193
+
194
+ /// Convert a number value to XML
195
+ fn convert_number(
196
+ key: &str,
197
+ val: &str,
198
+ type_name: &str,
199
+ config: &ConvertConfig,
200
+ ) -> PyResult<String> {
201
+ let (xml_key, name_attr) = make_valid_xml_name(key);
202
+ let mut attrs = Vec::new();
203
+
204
+ if let Some((k, v)) = name_attr {
205
+ attrs.push((k, v));
206
+ }
207
+ if config.attr_type {
208
+ attrs.push(("type".to_string(), type_name.to_string()));
209
+ }
210
+
211
+ let attr_string = make_attr_string(&attrs);
212
+ Ok(format!("<{}{}>{}</{}>", xml_key, attr_string, val, xml_key))
213
+ }
214
+
215
+ /// Convert a boolean value to XML
216
+ fn convert_bool(key: &str, val: bool, config: &ConvertConfig) -> PyResult<String> {
217
+ let (xml_key, name_attr) = make_valid_xml_name(key);
218
+ let mut attrs = Vec::new();
219
+
220
+ if let Some((k, v)) = name_attr {
221
+ attrs.push((k, v));
222
+ }
223
+ if config.attr_type {
224
+ attrs.push(("type".to_string(), "bool".to_string()));
225
+ }
226
+
227
+ let attr_string = make_attr_string(&attrs);
228
+ let bool_str = if val { "true" } else { "false" };
229
+ Ok(format!(
230
+ "<{}{}>{}</{}>",
231
+ xml_key, attr_string, bool_str, xml_key
232
+ ))
233
+ }
234
+
235
+ /// Convert a None value to XML
236
+ fn convert_none(key: &str, config: &ConvertConfig) -> PyResult<String> {
237
+ let (xml_key, name_attr) = make_valid_xml_name(key);
238
+ let mut attrs = Vec::new();
239
+
240
+ if let Some((k, v)) = name_attr {
241
+ attrs.push((k, v));
242
+ }
243
+ if config.attr_type {
244
+ attrs.push(("type".to_string(), "null".to_string()));
245
+ }
246
+
247
+ let attr_string = make_attr_string(&attrs);
248
+ Ok(format!("<{}{}></{}>", xml_key, attr_string, xml_key))
249
+ }
250
+
251
+ /// Convert a dictionary to XML
252
+ fn convert_dict(
253
+ py: Python<'_>,
254
+ dict: &Bound<'_, PyDict>,
255
+ _parent: &str,
256
+ config: &ConvertConfig,
257
+ ) -> PyResult<String> {
258
+ let mut output = String::new();
259
+
260
+ for (key, val) in dict.iter() {
261
+ let key_str: String = key.str()?.extract()?;
262
+ let (xml_key, name_attr) = make_valid_xml_name(&key_str);
263
+
264
+ // Handle bool (must check before int)
265
+ if val.is_instance_of::<PyBool>() {
266
+ let bool_val: bool = val.extract()?;
267
+ let mut attrs = Vec::new();
268
+ if let Some((k, v)) = name_attr {
269
+ attrs.push((k, v));
270
+ }
271
+ if config.attr_type {
272
+ attrs.push(("type".to_string(), "bool".to_string()));
273
+ }
274
+ let attr_string = make_attr_string(&attrs);
275
+ let bool_str = if bool_val { "true" } else { "false" };
276
+ write!(
277
+ output,
278
+ "<{}{}>{}</{}>",
279
+ xml_key, attr_string, bool_str, xml_key
280
+ )
281
+ .unwrap();
282
+ }
283
+ // Handle int - try i64 first, fall back to string for large integers
284
+ else if val.is_instance_of::<PyInt>() {
285
+ let int_str = match val.extract::<i64>() {
286
+ Ok(v) => v.to_string(),
287
+ Err(_) => val.str()?.extract::<String>()?,
288
+ };
289
+ let mut attrs = Vec::new();
290
+ if let Some((k, v)) = name_attr {
291
+ attrs.push((k, v));
292
+ }
293
+ if config.attr_type {
294
+ attrs.push(("type".to_string(), "int".to_string()));
295
+ }
296
+ let attr_string = make_attr_string(&attrs);
297
+ write!(
298
+ output,
299
+ "<{}{}>{}</{}>",
300
+ xml_key, attr_string, int_str, xml_key
301
+ )
302
+ .unwrap();
303
+ }
304
+ // Handle float
305
+ else if val.is_instance_of::<PyFloat>() {
306
+ let float_val: f64 = val.extract()?;
307
+ let mut attrs = Vec::new();
308
+ if let Some((k, v)) = name_attr {
309
+ attrs.push((k, v));
310
+ }
311
+ if config.attr_type {
312
+ attrs.push(("type".to_string(), "float".to_string()));
313
+ }
314
+ let attr_string = make_attr_string(&attrs);
315
+ write!(
316
+ output,
317
+ "<{}{}>{}</{}>",
318
+ xml_key, attr_string, float_val, xml_key
319
+ )
320
+ .unwrap();
321
+ }
322
+ // Handle string
323
+ else if val.is_instance_of::<PyString>() {
324
+ let str_val: String = val.extract()?;
325
+ let mut attrs = Vec::new();
326
+ if let Some((k, v)) = name_attr {
327
+ attrs.push((k, v));
328
+ }
329
+ if config.attr_type {
330
+ attrs.push(("type".to_string(), "str".to_string()));
331
+ }
332
+ let attr_string = make_attr_string(&attrs);
333
+ let content = if config.cdata {
334
+ wrap_cdata(&str_val)
335
+ } else {
336
+ escape_xml(&str_val)
337
+ };
338
+ write!(
339
+ output,
340
+ "<{}{}>{}</{}>",
341
+ xml_key, attr_string, content, xml_key
342
+ )
343
+ .unwrap();
344
+ }
345
+ // Handle None
346
+ else if val.is_none() {
347
+ let mut attrs = Vec::new();
348
+ if let Some((k, v)) = name_attr {
349
+ attrs.push((k, v));
350
+ }
351
+ if config.attr_type {
352
+ attrs.push(("type".to_string(), "null".to_string()));
353
+ }
354
+ let attr_string = make_attr_string(&attrs);
355
+ write!(output, "<{}{}></{}>", xml_key, attr_string, xml_key).unwrap();
356
+ }
357
+ // Handle nested dict
358
+ else if val.is_instance_of::<PyDict>() {
359
+ let nested_dict: &Bound<'_, PyDict> = val.cast()?;
360
+ let mut attrs = Vec::new();
361
+ if let Some((k, v)) = name_attr {
362
+ attrs.push((k, v));
363
+ }
364
+ if config.attr_type {
365
+ attrs.push(("type".to_string(), "dict".to_string()));
366
+ }
367
+ let attr_string = make_attr_string(&attrs);
368
+ let inner = convert_dict(py, nested_dict, &xml_key, config)?;
369
+ write!(
370
+ output,
371
+ "<{}{}>{}</{}>",
372
+ xml_key, attr_string, inner, xml_key
373
+ )
374
+ .unwrap();
375
+ }
376
+ // Handle list
377
+ else if val.is_instance_of::<PyList>() {
378
+ let list: &Bound<'_, PyList> = val.cast()?;
379
+ let list_output = convert_list(py, list, &xml_key, config)?;
380
+
381
+ if config.item_wrap {
382
+ let mut attrs = Vec::new();
383
+ if let Some((k, v)) = name_attr {
384
+ attrs.push((k, v));
385
+ }
386
+ if config.attr_type {
387
+ attrs.push(("type".to_string(), "list".to_string()));
388
+ }
389
+ let attr_string = make_attr_string(&attrs);
390
+ write!(
391
+ output,
392
+ "<{}{}>{}</{}>",
393
+ xml_key, attr_string, list_output, xml_key
394
+ )
395
+ .unwrap();
396
+ } else {
397
+ output.push_str(&list_output);
398
+ }
399
+ }
400
+ // Fallback: convert to string
401
+ else {
402
+ let str_val: String = val.str()?.extract()?;
403
+ let mut attrs = Vec::new();
404
+ if let Some((k, v)) = name_attr {
405
+ attrs.push((k, v));
406
+ }
407
+ if config.attr_type {
408
+ attrs.push(("type".to_string(), "str".to_string()));
409
+ }
410
+ let attr_string = make_attr_string(&attrs);
411
+ let content = if config.cdata {
412
+ wrap_cdata(&str_val)
413
+ } else {
414
+ escape_xml(&str_val)
415
+ };
416
+ write!(
417
+ output,
418
+ "<{}{}>{}</{}>",
419
+ xml_key, attr_string, content, xml_key
420
+ )
421
+ .unwrap();
422
+ }
423
+ }
424
+
425
+ Ok(output)
426
+ }
427
+
428
+ /// Convert a list to XML
429
+ fn convert_list(
430
+ py: Python<'_>,
431
+ list: &Bound<'_, PyList>,
432
+ parent: &str,
433
+ config: &ConvertConfig,
434
+ ) -> PyResult<String> {
435
+ let mut output = String::new();
436
+ let item_name = "item";
437
+
438
+ for item in list.iter() {
439
+ let tag_name = if config.item_wrap || config.list_headers {
440
+ if config.list_headers {
441
+ parent
442
+ } else {
443
+ item_name
444
+ }
445
+ } else {
446
+ parent
447
+ };
448
+
449
+ // Handle bool (must check before int)
450
+ if item.is_instance_of::<PyBool>() {
451
+ let bool_val: bool = item.extract()?;
452
+ let mut attrs = Vec::new();
453
+ if config.attr_type {
454
+ attrs.push(("type".to_string(), "bool".to_string()));
455
+ }
456
+ let attr_string = make_attr_string(&attrs);
457
+ let bool_str = if bool_val { "true" } else { "false" };
458
+ write!(
459
+ output,
460
+ "<{}{}>{}</{}>",
461
+ tag_name, attr_string, bool_str, tag_name
462
+ )
463
+ .unwrap();
464
+ }
465
+ // Handle int - try i64 first, fall back to string for large integers
466
+ else if item.is_instance_of::<PyInt>() {
467
+ let int_str = match item.extract::<i64>() {
468
+ Ok(v) => v.to_string(),
469
+ Err(_) => item.str()?.extract::<String>()?,
470
+ };
471
+ let mut attrs = Vec::new();
472
+ if config.attr_type {
473
+ attrs.push(("type".to_string(), "int".to_string()));
474
+ }
475
+ let attr_string = make_attr_string(&attrs);
476
+ write!(
477
+ output,
478
+ "<{}{}>{}</{}>",
479
+ tag_name, attr_string, int_str, tag_name
480
+ )
481
+ .unwrap();
482
+ }
483
+ // Handle float
484
+ else if item.is_instance_of::<PyFloat>() {
485
+ let float_val: f64 = item.extract()?;
486
+ let mut attrs = Vec::new();
487
+ if config.attr_type {
488
+ attrs.push(("type".to_string(), "float".to_string()));
489
+ }
490
+ let attr_string = make_attr_string(&attrs);
491
+ write!(
492
+ output,
493
+ "<{}{}>{}</{}>",
494
+ tag_name, attr_string, float_val, tag_name
495
+ )
496
+ .unwrap();
497
+ }
498
+ // Handle string
499
+ else if item.is_instance_of::<PyString>() {
500
+ let str_val: String = item.extract()?;
501
+ let mut attrs = Vec::new();
502
+ if config.attr_type {
503
+ attrs.push(("type".to_string(), "str".to_string()));
504
+ }
505
+ let attr_string = make_attr_string(&attrs);
506
+ let content = if config.cdata {
507
+ wrap_cdata(&str_val)
508
+ } else {
509
+ escape_xml(&str_val)
510
+ };
511
+ write!(
512
+ output,
513
+ "<{}{}>{}</{}>",
514
+ tag_name, attr_string, content, tag_name
515
+ )
516
+ .unwrap();
517
+ }
518
+ // Handle None
519
+ else if item.is_none() {
520
+ let mut attrs = Vec::new();
521
+ if config.attr_type {
522
+ attrs.push(("type".to_string(), "null".to_string()));
523
+ }
524
+ let attr_string = make_attr_string(&attrs);
525
+ write!(output, "<{}{}></{}>", tag_name, attr_string, tag_name).unwrap();
526
+ }
527
+ // Handle nested dict
528
+ else if item.is_instance_of::<PyDict>() {
529
+ let nested_dict: &Bound<'_, PyDict> = item.cast()?;
530
+ let inner = convert_dict(py, nested_dict, tag_name, config)?;
531
+
532
+ if config.item_wrap || config.list_headers {
533
+ let mut attrs = Vec::new();
534
+ if config.attr_type {
535
+ attrs.push(("type".to_string(), "dict".to_string()));
536
+ }
537
+ let attr_string = make_attr_string(&attrs);
538
+ write!(
539
+ output,
540
+ "<{}{}>{}</{}>",
541
+ tag_name, attr_string, inner, tag_name
542
+ )
543
+ .unwrap();
544
+ } else {
545
+ output.push_str(&inner);
546
+ }
547
+ }
548
+ // Handle nested list
549
+ else if item.is_instance_of::<PyList>() {
550
+ let nested_list: &Bound<'_, PyList> = item.cast()?;
551
+ let inner = convert_list(py, nested_list, tag_name, config)?;
552
+
553
+ let mut attrs = Vec::new();
554
+ if config.attr_type {
555
+ attrs.push(("type".to_string(), "list".to_string()));
556
+ }
557
+ let attr_string = make_attr_string(&attrs);
558
+ write!(
559
+ output,
560
+ "<{}{}>{}</{}>",
561
+ tag_name, attr_string, inner, tag_name
562
+ )
563
+ .unwrap();
564
+ }
565
+ // Fallback
566
+ else {
567
+ let str_val: String = item.str()?.extract()?;
568
+ let mut attrs = Vec::new();
569
+ if config.attr_type {
570
+ attrs.push(("type".to_string(), "str".to_string()));
571
+ }
572
+ let attr_string = make_attr_string(&attrs);
573
+ let content = if config.cdata {
574
+ wrap_cdata(&str_val)
575
+ } else {
576
+ escape_xml(&str_val)
577
+ };
578
+ write!(
579
+ output,
580
+ "<{}{}>{}</{}>",
581
+ tag_name, attr_string, content, tag_name
582
+ )
583
+ .unwrap();
584
+ }
585
+ }
586
+
587
+ Ok(output)
588
+ }
589
+
590
+ /// Convert a Python dict/list to XML bytes.
591
+ ///
592
+ /// This is a high-performance Rust implementation of dicttoxml.
593
+ ///
594
+ /// Args:
595
+ /// obj: The Python object to convert (dict or list)
596
+ /// root: Whether to include XML declaration and root element (default: True)
597
+ /// custom_root: The name of the root element (default: "root")
598
+ /// attr_type: Whether to include type attributes (default: True)
599
+ /// item_wrap: Whether to wrap list items in <item> tags (default: True)
600
+ /// cdata: Whether to wrap string values in CDATA sections (default: False)
601
+ /// list_headers: Whether to repeat parent tag for each list item (default: False)
602
+ ///
603
+ /// Returns:
604
+ /// bytes: The XML representation of the input object
605
+ #[pyfunction]
606
+ #[pyo3(signature = (obj, root=true, custom_root="root", attr_type=true, item_wrap=true, cdata=false, list_headers=false))]
607
+ #[allow(clippy::too_many_arguments)]
608
+ fn dicttoxml(
609
+ py: Python<'_>,
610
+ obj: &Bound<'_, PyAny>,
611
+ root: bool,
612
+ custom_root: &str,
613
+ attr_type: bool,
614
+ item_wrap: bool,
615
+ cdata: bool,
616
+ list_headers: bool,
617
+ ) -> PyResult<Vec<u8>> {
618
+ let config = ConvertConfig {
619
+ attr_type,
620
+ cdata,
621
+ item_wrap,
622
+ list_headers,
623
+ };
624
+
625
+ let content = if obj.is_instance_of::<PyDict>() {
626
+ let dict: &Bound<'_, PyDict> = obj.cast()?;
627
+ convert_dict(py, dict, custom_root, &config)?
628
+ } else if obj.is_instance_of::<PyList>() {
629
+ let list: &Bound<'_, PyList> = obj.cast()?;
630
+ convert_list(py, list, custom_root, &config)?
631
+ } else {
632
+ convert_value(py, obj, custom_root, &config, custom_root)?
633
+ };
634
+
635
+ let output = if root {
636
+ format!(
637
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><{}>{}</{}>",
638
+ custom_root, content, custom_root
639
+ )
640
+ } else {
641
+ content
642
+ };
643
+
644
+ Ok(output.into_bytes())
645
+ }
646
+
647
+ /// Fast XML string escaping.
648
+ ///
649
+ /// Escapes &, ", ', <, > characters for XML.
650
+ #[pyfunction]
651
+ fn escape_xml_py(s: &str) -> String {
652
+ escape_xml(s)
653
+ }
654
+
655
+ /// Wrap a string in CDATA section.
656
+ #[pyfunction]
657
+ fn wrap_cdata_py(s: &str) -> String {
658
+ wrap_cdata(s)
659
+ }
660
+
661
+ /// A Python module implemented in Rust.
662
+ #[pymodule]
663
+ fn json2xml_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
664
+ m.add_function(wrap_pyfunction!(dicttoxml, m)?)?;
665
+ m.add_function(wrap_pyfunction!(escape_xml_py, m)?)?;
666
+ m.add_function(wrap_pyfunction!(wrap_cdata_py, m)?)?;
667
+ Ok(())
668
+ }
@@ -0,0 +1,7 @@
1
+ version = 1
2
+ requires-python = ">=3.9"
3
+
4
+ [[package]]
5
+ name = "json2xml-rs"
6
+ version = "0.1.0"
7
+ source = { editable = "." }