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.
- json2xml_rs-0.1.0/Cargo.lock +172 -0
- json2xml_rs-0.1.0/Cargo.toml +19 -0
- json2xml_rs-0.1.0/PKG-INFO +118 -0
- json2xml_rs-0.1.0/README.md +106 -0
- json2xml_rs-0.1.0/pyproject.toml +20 -0
- json2xml_rs-0.1.0/src/lib.rs +668 -0
- json2xml_rs-0.1.0/uv.lock +7 -0
|
@@ -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("&"),
|
|
18
|
+
'"' => result.push_str("""),
|
|
19
|
+
'\'' => result.push_str("'"),
|
|
20
|
+
'<' => result.push_str("<"),
|
|
21
|
+
'>' => result.push_str(">"),
|
|
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
|
+
}
|