hotchpotch 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.
- hotchpotch-0.1.0/.github/workflows/CI.yml +186 -0
- hotchpotch-0.1.0/.gitignore +72 -0
- hotchpotch-0.1.0/Cargo.lock +172 -0
- hotchpotch-0.1.0/Cargo.toml +13 -0
- hotchpotch-0.1.0/PKG-INFO +53 -0
- hotchpotch-0.1.0/README.md +34 -0
- hotchpotch-0.1.0/pyproject.toml +27 -0
- hotchpotch-0.1.0/src/config.rs +127 -0
- hotchpotch-0.1.0/src/lib.rs +127 -0
- hotchpotch-0.1.0/src/parser.rs +165 -0
- hotchpotch-0.1.0/src/serializer.rs +70 -0
- hotchpotch-0.1.0/src/value.rs +13 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# This file is autogenerated by maturin v1.12.6
|
|
2
|
+
# To update, run
|
|
3
|
+
#
|
|
4
|
+
# maturin generate-ci github
|
|
5
|
+
#
|
|
6
|
+
name: CI
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches:
|
|
11
|
+
- main
|
|
12
|
+
- master
|
|
13
|
+
tags:
|
|
14
|
+
- '*'
|
|
15
|
+
pull_request:
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
linux:
|
|
23
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
24
|
+
strategy:
|
|
25
|
+
matrix:
|
|
26
|
+
platform:
|
|
27
|
+
- runner: ubuntu-22.04
|
|
28
|
+
target: x86_64
|
|
29
|
+
- runner: ubuntu-22.04
|
|
30
|
+
target: x86
|
|
31
|
+
- runner: ubuntu-22.04
|
|
32
|
+
target: aarch64
|
|
33
|
+
- runner: ubuntu-22.04
|
|
34
|
+
target: armv7
|
|
35
|
+
- runner: ubuntu-22.04
|
|
36
|
+
target: s390x
|
|
37
|
+
- runner: ubuntu-22.04
|
|
38
|
+
target: ppc64le
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v6
|
|
41
|
+
- uses: actions/setup-python@v6
|
|
42
|
+
with:
|
|
43
|
+
python-version: 3.x
|
|
44
|
+
- name: Build wheels
|
|
45
|
+
uses: PyO3/maturin-action@v1
|
|
46
|
+
with:
|
|
47
|
+
target: ${{ matrix.platform.target }}
|
|
48
|
+
args: --release --out dist --find-interpreter
|
|
49
|
+
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
50
|
+
manylinux: auto
|
|
51
|
+
- name: Upload wheels
|
|
52
|
+
uses: actions/upload-artifact@v6
|
|
53
|
+
with:
|
|
54
|
+
name: wheels-linux-${{ matrix.platform.target }}
|
|
55
|
+
path: dist
|
|
56
|
+
|
|
57
|
+
musllinux:
|
|
58
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
59
|
+
strategy:
|
|
60
|
+
matrix:
|
|
61
|
+
platform:
|
|
62
|
+
- runner: ubuntu-22.04
|
|
63
|
+
target: x86_64
|
|
64
|
+
- runner: ubuntu-22.04
|
|
65
|
+
target: x86
|
|
66
|
+
- runner: ubuntu-22.04
|
|
67
|
+
target: aarch64
|
|
68
|
+
- runner: ubuntu-22.04
|
|
69
|
+
target: armv7
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/checkout@v6
|
|
72
|
+
- uses: actions/setup-python@v6
|
|
73
|
+
with:
|
|
74
|
+
python-version: 3.x
|
|
75
|
+
- name: Build wheels
|
|
76
|
+
uses: PyO3/maturin-action@v1
|
|
77
|
+
with:
|
|
78
|
+
target: ${{ matrix.platform.target }}
|
|
79
|
+
args: --release --out dist --find-interpreter
|
|
80
|
+
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
81
|
+
manylinux: musllinux_1_2
|
|
82
|
+
- name: Upload wheels
|
|
83
|
+
uses: actions/upload-artifact@v6
|
|
84
|
+
with:
|
|
85
|
+
name: wheels-musllinux-${{ matrix.platform.target }}
|
|
86
|
+
path: dist
|
|
87
|
+
|
|
88
|
+
windows:
|
|
89
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
90
|
+
strategy:
|
|
91
|
+
matrix:
|
|
92
|
+
platform:
|
|
93
|
+
- runner: windows-latest
|
|
94
|
+
target: x64
|
|
95
|
+
python_arch: x64
|
|
96
|
+
- runner: windows-latest
|
|
97
|
+
target: x86
|
|
98
|
+
python_arch: x86
|
|
99
|
+
- runner: windows-11-arm
|
|
100
|
+
target: aarch64
|
|
101
|
+
python_arch: arm64
|
|
102
|
+
steps:
|
|
103
|
+
- uses: actions/checkout@v6
|
|
104
|
+
- uses: actions/setup-python@v6
|
|
105
|
+
with:
|
|
106
|
+
python-version: 3.13
|
|
107
|
+
architecture: ${{ matrix.platform.python_arch }}
|
|
108
|
+
- name: Build wheels
|
|
109
|
+
uses: PyO3/maturin-action@v1
|
|
110
|
+
with:
|
|
111
|
+
target: ${{ matrix.platform.target }}
|
|
112
|
+
args: --release --out dist --find-interpreter
|
|
113
|
+
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
114
|
+
- name: Upload wheels
|
|
115
|
+
uses: actions/upload-artifact@v6
|
|
116
|
+
with:
|
|
117
|
+
name: wheels-windows-${{ matrix.platform.target }}
|
|
118
|
+
path: dist
|
|
119
|
+
|
|
120
|
+
macos:
|
|
121
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
122
|
+
strategy:
|
|
123
|
+
matrix:
|
|
124
|
+
platform:
|
|
125
|
+
- runner: macos-15-intel
|
|
126
|
+
target: x86_64
|
|
127
|
+
- runner: macos-latest
|
|
128
|
+
target: aarch64
|
|
129
|
+
steps:
|
|
130
|
+
- uses: actions/checkout@v6
|
|
131
|
+
- uses: actions/setup-python@v6
|
|
132
|
+
with:
|
|
133
|
+
python-version: 3.x
|
|
134
|
+
- name: Build wheels
|
|
135
|
+
uses: PyO3/maturin-action@v1
|
|
136
|
+
with:
|
|
137
|
+
target: ${{ matrix.platform.target }}
|
|
138
|
+
args: --release --out dist --find-interpreter
|
|
139
|
+
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
140
|
+
- name: Upload wheels
|
|
141
|
+
uses: actions/upload-artifact@v6
|
|
142
|
+
with:
|
|
143
|
+
name: wheels-macos-${{ matrix.platform.target }}
|
|
144
|
+
path: dist
|
|
145
|
+
|
|
146
|
+
sdist:
|
|
147
|
+
runs-on: ubuntu-latest
|
|
148
|
+
steps:
|
|
149
|
+
- uses: actions/checkout@v6
|
|
150
|
+
- name: Build sdist
|
|
151
|
+
uses: PyO3/maturin-action@v1
|
|
152
|
+
with:
|
|
153
|
+
command: sdist
|
|
154
|
+
args: --out dist
|
|
155
|
+
- name: Upload sdist
|
|
156
|
+
uses: actions/upload-artifact@v6
|
|
157
|
+
with:
|
|
158
|
+
name: wheels-sdist
|
|
159
|
+
path: dist
|
|
160
|
+
|
|
161
|
+
release:
|
|
162
|
+
name: Release
|
|
163
|
+
runs-on: ubuntu-latest
|
|
164
|
+
if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
|
|
165
|
+
needs: [linux, musllinux, windows, macos, sdist]
|
|
166
|
+
permissions:
|
|
167
|
+
# Use to sign the release artifacts
|
|
168
|
+
id-token: write
|
|
169
|
+
# Used to upload release artifacts
|
|
170
|
+
contents: write
|
|
171
|
+
# Used to generate artifact attestation
|
|
172
|
+
attestations: write
|
|
173
|
+
steps:
|
|
174
|
+
- uses: actions/download-artifact@v7
|
|
175
|
+
- name: Generate artifact attestation
|
|
176
|
+
uses: actions/attest-build-provenance@v3
|
|
177
|
+
with:
|
|
178
|
+
subject-path: 'wheels-*/*'
|
|
179
|
+
- name: Install uv
|
|
180
|
+
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
181
|
+
uses: astral-sh/setup-uv@v7
|
|
182
|
+
- name: Publish to PyPI
|
|
183
|
+
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
184
|
+
run: uv publish 'wheels-*/*'
|
|
185
|
+
env:
|
|
186
|
+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/target
|
|
2
|
+
|
|
3
|
+
# Byte-compiled / optimized / DLL files
|
|
4
|
+
__pycache__/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
|
|
8
|
+
# C extensions
|
|
9
|
+
*.so
|
|
10
|
+
|
|
11
|
+
# Distribution / packaging
|
|
12
|
+
.Python
|
|
13
|
+
.venv/
|
|
14
|
+
env/
|
|
15
|
+
bin/
|
|
16
|
+
build/
|
|
17
|
+
develop-eggs/
|
|
18
|
+
dist/
|
|
19
|
+
eggs/
|
|
20
|
+
lib/
|
|
21
|
+
lib64/
|
|
22
|
+
parts/
|
|
23
|
+
sdist/
|
|
24
|
+
var/
|
|
25
|
+
include/
|
|
26
|
+
man/
|
|
27
|
+
venv/
|
|
28
|
+
*.egg-info/
|
|
29
|
+
.installed.cfg
|
|
30
|
+
*.egg
|
|
31
|
+
|
|
32
|
+
# Installer logs
|
|
33
|
+
pip-log.txt
|
|
34
|
+
pip-delete-this-directory.txt
|
|
35
|
+
pip-selfcheck.json
|
|
36
|
+
|
|
37
|
+
# Unit test / coverage reports
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.coverage
|
|
41
|
+
.cache
|
|
42
|
+
nosetests.xml
|
|
43
|
+
coverage.xml
|
|
44
|
+
|
|
45
|
+
# Translations
|
|
46
|
+
*.mo
|
|
47
|
+
|
|
48
|
+
# Mr Developer
|
|
49
|
+
.mr.developer.cfg
|
|
50
|
+
.project
|
|
51
|
+
.pydevproject
|
|
52
|
+
|
|
53
|
+
# Rope
|
|
54
|
+
.ropeproject
|
|
55
|
+
|
|
56
|
+
# Django stuff:
|
|
57
|
+
*.log
|
|
58
|
+
*.pot
|
|
59
|
+
|
|
60
|
+
.DS_Store
|
|
61
|
+
|
|
62
|
+
# Sphinx documentation
|
|
63
|
+
docs/_build/
|
|
64
|
+
|
|
65
|
+
# PyCharm
|
|
66
|
+
.idea/
|
|
67
|
+
|
|
68
|
+
# VSCode
|
|
69
|
+
.vscode/
|
|
70
|
+
|
|
71
|
+
# Pyenv
|
|
72
|
+
.python-version
|
|
@@ -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 = "hotchpotch"
|
|
19
|
+
version = "0.1.0"
|
|
20
|
+
dependencies = [
|
|
21
|
+
"pyo3",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[[package]]
|
|
25
|
+
name = "indoc"
|
|
26
|
+
version = "2.0.7"
|
|
27
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
28
|
+
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
|
29
|
+
dependencies = [
|
|
30
|
+
"rustversion",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[[package]]
|
|
34
|
+
name = "libc"
|
|
35
|
+
version = "0.2.182"
|
|
36
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
37
|
+
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
|
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.1"
|
|
57
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
58
|
+
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
|
59
|
+
|
|
60
|
+
[[package]]
|
|
61
|
+
name = "proc-macro2"
|
|
62
|
+
version = "1.0.106"
|
|
63
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
64
|
+
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
|
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.44"
|
|
133
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
134
|
+
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
|
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.117"
|
|
148
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
149
|
+
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
|
150
|
+
dependencies = [
|
|
151
|
+
"proc-macro2",
|
|
152
|
+
"quote",
|
|
153
|
+
"unicode-ident",
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
[[package]]
|
|
157
|
+
name = "target-lexicon"
|
|
158
|
+
version = "0.13.5"
|
|
159
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
160
|
+
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
161
|
+
|
|
162
|
+
[[package]]
|
|
163
|
+
name = "unicode-ident"
|
|
164
|
+
version = "1.0.24"
|
|
165
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
166
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
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,13 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "hotchpotch"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
|
|
7
|
+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
8
|
+
[lib]
|
|
9
|
+
name = "hotchpotch"
|
|
10
|
+
crate-type = ["cdylib"]
|
|
11
|
+
|
|
12
|
+
[dependencies]
|
|
13
|
+
pyo3 = { version = "0.27.0", features = ["extension-module", "abi3-py39"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hotchpotch
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
7
|
+
Classifier: Topic :: Text Processing :: General
|
|
8
|
+
Classifier: Programming Language :: Rust
|
|
9
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
10
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
11
|
+
Summary: Fast Rust-powered serialization format for embedding Python objects in CSV fields
|
|
12
|
+
Keywords: csv,serialization,parser,rust,pyo3
|
|
13
|
+
License-Expression: MIT
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
16
|
+
Project-URL: Homepage, https://github.com/gebz97/hotchpotch
|
|
17
|
+
Project-URL: Issues, https://github.com/gebz97/hotchpotch/issues
|
|
18
|
+
Project-URL: Repository, https://github.com/gebz97/hotchpotch
|
|
19
|
+
|
|
20
|
+
# hotchpotch
|
|
21
|
+
|
|
22
|
+
Fast Rust-powered serialization for embedding Python objects in CSV fields.
|
|
23
|
+
|
|
24
|
+
## Format
|
|
25
|
+
```
|
|
26
|
+
name=adam;hobbies=[cycling|rowing|chess];height=175cm;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Supports: strings, ints, floats, bools, None, lists, nested dicts.
|
|
30
|
+
Special characters are backslash-escaped. All delimiters are configurable.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
```bash
|
|
34
|
+
pip install hotchpotch
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
```python
|
|
39
|
+
import hotchpotch
|
|
40
|
+
|
|
41
|
+
cfg = hotchpotch.FormatConfig()
|
|
42
|
+
|
|
43
|
+
s = hotchpotch.dumps({"name": "adam", "hobbies": ["cycling", "rowing"], "age": 30}, cfg)
|
|
44
|
+
# → "age=30;hobbies=[cycling|rowing];name=adam;"
|
|
45
|
+
|
|
46
|
+
data = hotchpotch.loads(s, cfg)
|
|
47
|
+
# → {"age": 30, "hobbies": ["cycling", "rowing"], "name": "adam"}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Custom delimiters
|
|
51
|
+
```python
|
|
52
|
+
cfg = hotchpotch.FormatConfig(field_sep='&', kv_sep=':', list_sep=',')
|
|
53
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# hotchpotch
|
|
2
|
+
|
|
3
|
+
Fast Rust-powered serialization for embedding Python objects in CSV fields.
|
|
4
|
+
|
|
5
|
+
## Format
|
|
6
|
+
```
|
|
7
|
+
name=adam;hobbies=[cycling|rowing|chess];height=175cm;
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Supports: strings, ints, floats, bools, None, lists, nested dicts.
|
|
11
|
+
Special characters are backslash-escaped. All delimiters are configurable.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
```bash
|
|
15
|
+
pip install hotchpotch
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
```python
|
|
20
|
+
import hotchpotch
|
|
21
|
+
|
|
22
|
+
cfg = hotchpotch.FormatConfig()
|
|
23
|
+
|
|
24
|
+
s = hotchpotch.dumps({"name": "adam", "hobbies": ["cycling", "rowing"], "age": 30}, cfg)
|
|
25
|
+
# → "age=30;hobbies=[cycling|rowing];name=adam;"
|
|
26
|
+
|
|
27
|
+
data = hotchpotch.loads(s, cfg)
|
|
28
|
+
# → {"age": 30, "hobbies": ["cycling", "rowing"], "name": "adam"}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Custom delimiters
|
|
32
|
+
```python
|
|
33
|
+
cfg = hotchpotch.FormatConfig(field_sep='&', kv_sep=':', list_sep=',')
|
|
34
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["maturin>=1.12,<2.0"]
|
|
3
|
+
build-backend = "maturin"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hotchpotch"
|
|
7
|
+
requires-python = ">=3.8"
|
|
8
|
+
keywords = ["csv", "serialization", "parser", "rust", "pyo3"]
|
|
9
|
+
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
14
|
+
"Topic :: Text Processing :: General",
|
|
15
|
+
"Programming Language :: Rust",
|
|
16
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
17
|
+
"Programming Language :: Python :: Implementation :: PyPy",
|
|
18
|
+
]
|
|
19
|
+
dynamic = ["version"]
|
|
20
|
+
description = "Fast Rust-powered serialization format for embedding Python objects in CSV fields"
|
|
21
|
+
readme = "README.md"
|
|
22
|
+
license = "MIT"
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/gebz97/hotchpotch"
|
|
26
|
+
Repository = "https://github.com/gebz97/hotchpotch"
|
|
27
|
+
Issues = "https://github.com/gebz97/hotchpotch/issues"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
use pyo3::prelude::*;
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
All delimiters that define the serialization format.
|
|
5
|
+
Defaults match the example: name=adam;hobbies=[cycling|rowing|chess];height=175cm;
|
|
6
|
+
*/
|
|
7
|
+
#[pyclass]
|
|
8
|
+
#[derive(Clone, Debug)]
|
|
9
|
+
pub struct FormatConfig {
|
|
10
|
+
// between key=value pairs → ';'
|
|
11
|
+
#[pyo3(get, set)]
|
|
12
|
+
pub field_sep: char,
|
|
13
|
+
// between key and value → '='
|
|
14
|
+
#[pyo3(get, set)]
|
|
15
|
+
pub kv_sep: char,
|
|
16
|
+
// list open bracket → '['
|
|
17
|
+
#[pyo3(get, set)]
|
|
18
|
+
pub list_open: char,
|
|
19
|
+
// list close bracket → ']'
|
|
20
|
+
#[pyo3(get, set)]
|
|
21
|
+
pub list_close: char,
|
|
22
|
+
// between list items → '|'
|
|
23
|
+
#[pyo3(get, set)]
|
|
24
|
+
pub list_sep: char,
|
|
25
|
+
// nested object open → '{'
|
|
26
|
+
#[pyo3(get, set)]
|
|
27
|
+
pub obj_open: char,
|
|
28
|
+
// nested object close → '}'
|
|
29
|
+
#[pyo3(get, set)]
|
|
30
|
+
pub obj_close: char,
|
|
31
|
+
// escape character → '\'
|
|
32
|
+
#[pyo3(get, set)]
|
|
33
|
+
pub escape: char,
|
|
34
|
+
// null representation → 'null'
|
|
35
|
+
#[pyo3(get, set)]
|
|
36
|
+
pub null_str: String,
|
|
37
|
+
// bool true representation → 'true'
|
|
38
|
+
#[pyo3(get, set)]
|
|
39
|
+
pub true_str: String,
|
|
40
|
+
// bool false representation→ 'false'
|
|
41
|
+
#[pyo3(get, set)]
|
|
42
|
+
pub false_str: String,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[pymethods]
|
|
46
|
+
impl FormatConfig {
|
|
47
|
+
#[new]
|
|
48
|
+
#[pyo3(signature = (
|
|
49
|
+
field_sep = ';',
|
|
50
|
+
kv_sep = '=',
|
|
51
|
+
list_open = '[',
|
|
52
|
+
list_close = ']',
|
|
53
|
+
list_sep = '|',
|
|
54
|
+
obj_open = '{',
|
|
55
|
+
obj_close = '}',
|
|
56
|
+
escape = '\\',
|
|
57
|
+
null_str = "null".to_string(),
|
|
58
|
+
true_str = "true".to_string(),
|
|
59
|
+
false_str = "false".to_string(),
|
|
60
|
+
))]
|
|
61
|
+
pub fn new(
|
|
62
|
+
field_sep: char,
|
|
63
|
+
kv_sep: char,
|
|
64
|
+
list_open: char,
|
|
65
|
+
list_close: char,
|
|
66
|
+
list_sep: char,
|
|
67
|
+
obj_open: char,
|
|
68
|
+
obj_close: char,
|
|
69
|
+
escape: char,
|
|
70
|
+
null_str: String,
|
|
71
|
+
true_str: String,
|
|
72
|
+
false_str: String,
|
|
73
|
+
) -> Self {
|
|
74
|
+
FormatConfig {
|
|
75
|
+
field_sep,
|
|
76
|
+
kv_sep,
|
|
77
|
+
list_open,
|
|
78
|
+
list_close,
|
|
79
|
+
list_sep,
|
|
80
|
+
obj_open,
|
|
81
|
+
obj_close,
|
|
82
|
+
escape,
|
|
83
|
+
null_str,
|
|
84
|
+
true_str,
|
|
85
|
+
false_str,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fn __repr__(&self) -> String {
|
|
90
|
+
format!(
|
|
91
|
+
"FormatConfig(field_sep={:?}, kv_sep={:?}, list_sep={:?})",
|
|
92
|
+
self.field_sep, self.kv_sep, self.list_sep
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Returns the set of all characters that must be escaped in scalar values
|
|
97
|
+
pub fn special_chars(&self) -> Vec<char> {
|
|
98
|
+
vec![
|
|
99
|
+
self.field_sep,
|
|
100
|
+
self.kv_sep,
|
|
101
|
+
self.list_open,
|
|
102
|
+
self.list_close,
|
|
103
|
+
self.list_sep,
|
|
104
|
+
self.obj_open,
|
|
105
|
+
self.obj_close,
|
|
106
|
+
self.escape,
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
impl Default for FormatConfig {
|
|
112
|
+
fn default() -> Self {
|
|
113
|
+
FormatConfig::new(
|
|
114
|
+
';',
|
|
115
|
+
'=',
|
|
116
|
+
'[',
|
|
117
|
+
']',
|
|
118
|
+
'|',
|
|
119
|
+
'{',
|
|
120
|
+
'}',
|
|
121
|
+
'\\',
|
|
122
|
+
"null".into(),
|
|
123
|
+
"true".into(),
|
|
124
|
+
"false".into(),
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
mod config;
|
|
2
|
+
mod parser;
|
|
3
|
+
mod serializer;
|
|
4
|
+
mod value;
|
|
5
|
+
|
|
6
|
+
use pyo3::exceptions::PyValueError;
|
|
7
|
+
use pyo3::prelude::*;
|
|
8
|
+
use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyList, PyString};
|
|
9
|
+
|
|
10
|
+
use config::FormatConfig;
|
|
11
|
+
use value::Value;
|
|
12
|
+
|
|
13
|
+
// ── Python → internal Value ───────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
fn py_to_value(obj: &Bound<'_, PyAny>) -> PyResult<Value> {
|
|
16
|
+
// Order matters: Bool must come before Int (bool is subclass of int in Python)
|
|
17
|
+
if obj.is_none() {
|
|
18
|
+
Ok(Value::Null)
|
|
19
|
+
} else if obj.is_instance_of::<PyBool>() {
|
|
20
|
+
Ok(Value::Bool(obj.extract::<bool>()?))
|
|
21
|
+
} else if obj.is_instance_of::<PyInt>() {
|
|
22
|
+
Ok(Value::Int(obj.extract::<i64>()?))
|
|
23
|
+
} else if obj.is_instance_of::<PyFloat>() {
|
|
24
|
+
Ok(Value::Float(obj.extract::<f64>()?))
|
|
25
|
+
} else if obj.is_instance_of::<PyString>() {
|
|
26
|
+
Ok(Value::Str(obj.extract::<String>()?))
|
|
27
|
+
} else if obj.is_instance_of::<PyList>() {
|
|
28
|
+
let list = obj.cast::<PyList>()?;
|
|
29
|
+
let items: PyResult<Vec<Value>> = list.iter().map(|x| py_to_value(&x)).collect();
|
|
30
|
+
Ok(Value::List(items?))
|
|
31
|
+
} else if obj.is_instance_of::<PyDict>() {
|
|
32
|
+
let dict = obj.cast::<PyDict>()?;
|
|
33
|
+
let mut map = std::collections::BTreeMap::new();
|
|
34
|
+
for (k, v) in dict.iter() {
|
|
35
|
+
let key: String = k.extract()?;
|
|
36
|
+
map.insert(key, py_to_value(&v)?);
|
|
37
|
+
}
|
|
38
|
+
Ok(Value::Object(map))
|
|
39
|
+
} else {
|
|
40
|
+
// Fallback: convert to string via __str__
|
|
41
|
+
Ok(Value::Str(obj.str()?.extract::<String>()?))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── internal Value → Python ───────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
fn value_to_py<'py>(val: &Value, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
|
|
48
|
+
match val {
|
|
49
|
+
Value::Null => Ok(py.None().into_bound(py)),
|
|
50
|
+
|
|
51
|
+
// .to_owned() converts Borrowed<'_, '_, T> → Bound<'py, T> before into_any()
|
|
52
|
+
Value::Bool(b) => Ok(b.into_pyobject(py)?.to_owned().into_any()),
|
|
53
|
+
Value::Int(i) => Ok(i.into_pyobject(py)?.to_owned().into_any()),
|
|
54
|
+
Value::Float(f) => Ok(f.into_pyobject(py)?.to_owned().into_any()),
|
|
55
|
+
|
|
56
|
+
// String already returns an owned Bound, no .to_owned() needed
|
|
57
|
+
Value::Str(s) => Ok(s.clone().into_pyobject(py)?.into_any()),
|
|
58
|
+
|
|
59
|
+
Value::List(items) => {
|
|
60
|
+
let list = PyList::empty(py);
|
|
61
|
+
for item in items {
|
|
62
|
+
list.append(value_to_py(item, py)?)?;
|
|
63
|
+
}
|
|
64
|
+
Ok(list.into_any())
|
|
65
|
+
}
|
|
66
|
+
Value::Object(map) => {
|
|
67
|
+
let dict = PyDict::new(py);
|
|
68
|
+
for (k, v) in map {
|
|
69
|
+
dict.set_item(k, value_to_py(v, py)?)?;
|
|
70
|
+
}
|
|
71
|
+
Ok(dict.into_any())
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Exposed Python functions ──────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/// Serialize a Python dict into the custom format string.
|
|
79
|
+
///
|
|
80
|
+
/// Example:
|
|
81
|
+
/// cfg = FormatConfig()
|
|
82
|
+
/// dumps({"name": "adam", "hobbies": ["cycling", "rowing"], "age": 30}, cfg)
|
|
83
|
+
/// # → "name=adam;hobbies=[cycling|rowing];age=30;"
|
|
84
|
+
#[pyfunction]
|
|
85
|
+
#[pyo3(signature = (obj, config=None))]
|
|
86
|
+
fn dumps(obj: &Bound<'_, PyDict>, config: Option<&FormatConfig>) -> PyResult<String> {
|
|
87
|
+
let cfg = config.cloned().unwrap_or_default();
|
|
88
|
+
let mut fields: Vec<(String, Value)> = Vec::new();
|
|
89
|
+
|
|
90
|
+
for (k, v) in obj.iter() {
|
|
91
|
+
let key: String = k.extract()?;
|
|
92
|
+
let val = py_to_value(&v)?;
|
|
93
|
+
fields.push((key, val));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
Ok(serializer::serialize_object(&fields, &cfg))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Deserialize a custom format string into a Python dict.
|
|
100
|
+
///
|
|
101
|
+
/// Example:
|
|
102
|
+
/// cfg = FormatConfig()
|
|
103
|
+
/// loads("name=adam;hobbies=[cycling|rowing];age=30;", cfg)
|
|
104
|
+
/// # → {"name": "adam", "hobbies": ["cycling", "rowing"], "age": 30}
|
|
105
|
+
#[pyfunction]
|
|
106
|
+
#[pyo3(signature = (s, config=None))]
|
|
107
|
+
fn loads(py: Python<'_>, s: &str, config: Option<&FormatConfig>) -> PyResult<Py<PyAny>> {
|
|
108
|
+
let cfg = config.cloned().unwrap_or_default();
|
|
109
|
+
|
|
110
|
+
let fields = parser::parse_record_str(s, &cfg).map_err(|e| PyValueError::new_err(e))?;
|
|
111
|
+
|
|
112
|
+
let dict = PyDict::new(py);
|
|
113
|
+
for (k, v) in &fields {
|
|
114
|
+
dict.set_item(k, value_to_py(v, py)?)?;
|
|
115
|
+
}
|
|
116
|
+
Ok(dict.into())
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Module ────────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
#[pymodule]
|
|
122
|
+
fn hotchpotch(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
123
|
+
m.add_class::<FormatConfig>()?;
|
|
124
|
+
m.add_function(wrap_pyfunction!(dumps, m)?)?;
|
|
125
|
+
m.add_function(wrap_pyfunction!(loads, m)?)?;
|
|
126
|
+
Ok(())
|
|
127
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
use std::collections::BTreeMap;
|
|
2
|
+
use crate::config::FormatConfig;
|
|
3
|
+
use crate::value::Value;
|
|
4
|
+
|
|
5
|
+
pub struct Parser<'a> {
|
|
6
|
+
input: &'a [char],
|
|
7
|
+
pos: usize,
|
|
8
|
+
cfg: &'a FormatConfig,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
impl<'a> Parser<'a> {
|
|
12
|
+
pub fn new(chars: &'a [char], cfg: &'a FormatConfig) -> Self {
|
|
13
|
+
Parser { input: chars, pos: 0, cfg }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fn peek(&self) -> Option<char> {
|
|
17
|
+
self.input.get(self.pos).copied()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fn advance(&mut self) -> Option<char> {
|
|
21
|
+
let ch = self.input.get(self.pos).copied();
|
|
22
|
+
self.pos += 1;
|
|
23
|
+
ch
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn expect(&mut self, ch: char) -> Result<(), String> {
|
|
27
|
+
match self.advance() {
|
|
28
|
+
Some(c) if c == ch => Ok(()),
|
|
29
|
+
Some(c) => Err(format!("Expected {:?} but got {:?} at pos {}", ch, c, self.pos - 1)),
|
|
30
|
+
None => Err(format!("Expected {:?} but got EOF", ch)),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Read a scalar string, handling escape sequences, stopping at any
|
|
35
|
+
/// unescaped special character in `stop_at`.
|
|
36
|
+
fn read_scalar(&mut self, stop_at: &[char]) -> String {
|
|
37
|
+
let mut out = String::new();
|
|
38
|
+
loop {
|
|
39
|
+
match self.peek() {
|
|
40
|
+
None => break,
|
|
41
|
+
Some(ch) if ch == self.cfg.escape => {
|
|
42
|
+
self.advance(); // consume escape char
|
|
43
|
+
if let Some(next) = self.advance() {
|
|
44
|
+
out.push(next); // take the literal next char
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
Some(ch) if stop_at.contains(&ch) => break,
|
|
48
|
+
Some(ch) => { self.advance(); out.push(ch); }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
out
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Parse a value: list, nested object, or scalar.
|
|
55
|
+
pub fn parse_value(&mut self) -> Result<Value, String> {
|
|
56
|
+
match self.peek() {
|
|
57
|
+
Some(ch) if ch == self.cfg.list_open => self.parse_list(),
|
|
58
|
+
Some(ch) if ch == self.cfg.obj_open => self.parse_nested_object(),
|
|
59
|
+
_ => self.parse_scalar(),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fn parse_list(&mut self) -> Result<Value, String> {
|
|
64
|
+
self.expect(self.cfg.list_open)?;
|
|
65
|
+
let mut items = Vec::new();
|
|
66
|
+
|
|
67
|
+
// Handle empty list []
|
|
68
|
+
if self.peek() == Some(self.cfg.list_close) {
|
|
69
|
+
self.advance();
|
|
70
|
+
return Ok(Value::List(items));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
loop {
|
|
74
|
+
items.push(self.parse_value()?);
|
|
75
|
+
match self.peek() {
|
|
76
|
+
Some(ch) if ch == self.cfg.list_sep => { self.advance(); }
|
|
77
|
+
Some(ch) if ch == self.cfg.list_close => { self.advance(); break; }
|
|
78
|
+
Some(ch) => return Err(format!("Unexpected {:?} in list at pos {}", ch, self.pos)),
|
|
79
|
+
None => return Err("Unterminated list".into()),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
Ok(Value::List(items))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn parse_nested_object(&mut self) -> Result<Value, String> {
|
|
86
|
+
self.expect(self.cfg.obj_open)?;
|
|
87
|
+
let mut map = BTreeMap::new();
|
|
88
|
+
|
|
89
|
+
if self.peek() == Some(self.cfg.obj_close) {
|
|
90
|
+
self.advance();
|
|
91
|
+
return Ok(Value::Object(map));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
loop {
|
|
95
|
+
// Stop chars for key: kv_sep
|
|
96
|
+
let key = self.read_scalar(&[self.cfg.kv_sep, self.cfg.obj_close]);
|
|
97
|
+
self.expect(self.cfg.kv_sep)?;
|
|
98
|
+
|
|
99
|
+
// Stop chars for value inside nested obj: field_sep or obj_close
|
|
100
|
+
let val = self.parse_value()?;
|
|
101
|
+
map.insert(key, val);
|
|
102
|
+
|
|
103
|
+
match self.peek() {
|
|
104
|
+
Some(ch) if ch == self.cfg.field_sep => { self.advance(); }
|
|
105
|
+
Some(ch) if ch == self.cfg.obj_close => { self.advance(); break; }
|
|
106
|
+
Some(ch) => return Err(format!("Unexpected {:?} in object at pos {}", ch, self.pos)),
|
|
107
|
+
None => return Err("Unterminated object".into()),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
Ok(Value::Object(map))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn parse_scalar(&mut self) -> Result<Value, String> {
|
|
114
|
+
// Stop at anything that could end a scalar in any context
|
|
115
|
+
let stop = vec![
|
|
116
|
+
self.cfg.field_sep, self.cfg.kv_sep,
|
|
117
|
+
self.cfg.list_sep, self.cfg.list_close,
|
|
118
|
+
self.cfg.obj_close,
|
|
119
|
+
];
|
|
120
|
+
let s = self.read_scalar(&stop);
|
|
121
|
+
Ok(coerce_scalar(s, self.cfg))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Parse a full top-level record: key=val;key=val;
|
|
125
|
+
pub fn parse_record(&mut self) -> Result<Vec<(String, Value)>, String> {
|
|
126
|
+
let mut fields = Vec::new();
|
|
127
|
+
|
|
128
|
+
while self.pos < self.input.len() {
|
|
129
|
+
// Skip trailing field_sep at end of input
|
|
130
|
+
if self.peek() == Some(self.cfg.field_sep) {
|
|
131
|
+
self.advance();
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let key = self.read_scalar(&[self.cfg.kv_sep]);
|
|
136
|
+
if key.is_empty() { break; }
|
|
137
|
+
|
|
138
|
+
self.expect(self.cfg.kv_sep)?;
|
|
139
|
+
let val = self.parse_value()?;
|
|
140
|
+
fields.push((key, val));
|
|
141
|
+
|
|
142
|
+
// consume trailing field_sep after value
|
|
143
|
+
if self.peek() == Some(self.cfg.field_sep) {
|
|
144
|
+
self.advance();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
Ok(fields)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Infer the type of a scalar string (int → float → bool → null → str)
|
|
152
|
+
fn coerce_scalar(s: String, cfg: &FormatConfig) -> Value {
|
|
153
|
+
if s == cfg.null_str { return Value::Null; }
|
|
154
|
+
if s == cfg.true_str { return Value::Bool(true); }
|
|
155
|
+
if s == cfg.false_str { return Value::Bool(false); }
|
|
156
|
+
if let Ok(i) = s.parse::<i64>() { return Value::Int(i); }
|
|
157
|
+
if let Ok(f) = s.parse::<f64>() { return Value::Float(f); }
|
|
158
|
+
Value::Str(s)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
pub fn parse_record_str(s: &str, cfg: &FormatConfig) -> Result<Vec<(String, Value)>, String> {
|
|
162
|
+
let chars: Vec<char> = s.chars().collect();
|
|
163
|
+
let mut parser = Parser::new(&chars, cfg);
|
|
164
|
+
parser.parse_record()
|
|
165
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
use crate::config::FormatConfig;
|
|
2
|
+
use crate::value::Value;
|
|
3
|
+
|
|
4
|
+
/// Escape a string scalar: prepend `config.escape` before any special char.
|
|
5
|
+
pub fn escape_str(s: &str, cfg: &FormatConfig) -> String {
|
|
6
|
+
let specials = cfg.special_chars();
|
|
7
|
+
let mut out = String::with_capacity(s.len());
|
|
8
|
+
for ch in s.chars() {
|
|
9
|
+
if specials.contains(&ch) {
|
|
10
|
+
out.push(cfg.escape);
|
|
11
|
+
}
|
|
12
|
+
out.push(ch);
|
|
13
|
+
}
|
|
14
|
+
out
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Serialize a [`Value`] into a string using `cfg`.
|
|
18
|
+
pub fn serialize_value(val: &Value, cfg: &FormatConfig) -> String {
|
|
19
|
+
match val {
|
|
20
|
+
Value::Null => cfg.null_str.clone(),
|
|
21
|
+
Value::Bool(b) => if *b { cfg.true_str.clone() } else { cfg.false_str.clone() },
|
|
22
|
+
Value::Int(i) => i.to_string(),
|
|
23
|
+
Value::Float(f) => {
|
|
24
|
+
// Always include decimal point so floats are unambiguous from ints
|
|
25
|
+
if f.fract() == 0.0 { format!("{:.1}", f) } else { f.to_string() }
|
|
26
|
+
}
|
|
27
|
+
Value::Str(s) => escape_str(s, cfg),
|
|
28
|
+
|
|
29
|
+
Value::List(items) => {
|
|
30
|
+
let inner: Vec<String> = items.iter().map(|v| serialize_value(v, cfg)).collect();
|
|
31
|
+
format!(
|
|
32
|
+
"{}{}{}",
|
|
33
|
+
cfg.list_open,
|
|
34
|
+
inner.join(&cfg.list_sep.to_string()),
|
|
35
|
+
cfg.list_close,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Value::Object(map) => {
|
|
40
|
+
let inner: Vec<String> = map
|
|
41
|
+
.iter()
|
|
42
|
+
.map(|(k, v)| format!(
|
|
43
|
+
"{}{}{}",
|
|
44
|
+
escape_str(k, cfg),
|
|
45
|
+
cfg.kv_sep,
|
|
46
|
+
serialize_value(v, cfg)
|
|
47
|
+
))
|
|
48
|
+
.collect();
|
|
49
|
+
format!(
|
|
50
|
+
"{}{}{}",
|
|
51
|
+
cfg.obj_open,
|
|
52
|
+
inner.join(&cfg.field_sep.to_string()),
|
|
53
|
+
cfg.obj_close,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Serialize a top-level object (dict of key→value) into the full format.
|
|
60
|
+
/// Produces: key1=val1;key2=val2;
|
|
61
|
+
pub fn serialize_object(map: &[(String, Value)], cfg: &FormatConfig) -> String {
|
|
62
|
+
let mut out = String::new();
|
|
63
|
+
for (k, v) in map {
|
|
64
|
+
out.push_str(&escape_str(k, cfg));
|
|
65
|
+
out.push(cfg.kv_sep);
|
|
66
|
+
out.push_str(&serialize_value(v, cfg));
|
|
67
|
+
out.push(cfg.field_sep);
|
|
68
|
+
}
|
|
69
|
+
out
|
|
70
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
use std::collections::BTreeMap; // BTreeMap keeps key order deterministic
|
|
2
|
+
|
|
3
|
+
/// Internal representation of any serializable value.
|
|
4
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
5
|
+
pub enum Value {
|
|
6
|
+
Null,
|
|
7
|
+
Bool(bool),
|
|
8
|
+
Int(i64),
|
|
9
|
+
Float(f64),
|
|
10
|
+
Str(String),
|
|
11
|
+
List(Vec<Value>),
|
|
12
|
+
Object(BTreeMap<String, Value>), // preserves insertion order via BTree sort
|
|
13
|
+
}
|