├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── documentation_issue.md │ └── enhancement_suggestion.md ├── dependabot.yml └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile.toml ├── README.md ├── ReleaseProcess.md ├── benches ├── reader_benchmark.rs ├── reader_struct_benchmark.rs ├── serde_deserialize_benchmark.rs ├── serde_serialize_benchmark.rs ├── writer_benchmark.rs └── writer_struct_benchmark.rs ├── rust-toolchain.toml ├── src ├── json_number.rs ├── lib.rs ├── reader │ ├── mod.rs │ ├── simple.rs │ └── stream_reader.rs ├── serde │ ├── de.rs │ ├── mod.rs │ └── ser.rs ├── utf8.rs └── writer │ ├── mod.rs │ ├── simple.rs │ └── stream_writer.rs └── tests ├── custom_json_reader.rs ├── custom_json_writer.rs ├── partial_reader.rs ├── reader_alloc_test.rs ├── reader_test.rs ├── serde_deserialize_test.rs ├── serde_serialize_test.rs ├── simple_reader.rs ├── simple_writer.rs ├── test_lib ├── mod.rs └── test_data.json ├── transfer_test.rs ├── writer_alloc_test.rs └── writer_test.rs /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a Struson bug. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Struson version 11 | 12 | 13 | 14 | ### Description 15 | 16 | 17 | 18 | ### Expected behavior 19 | 20 | 21 | 22 | ### Actual behavior 23 | 24 | 25 | 26 | ### Reproduction steps 27 | 28 | 29 | 30 | 1. ... 31 | 2. ... 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Usage question & discussion 3 | url: https://github.com/Marcono1234/struson/discussions 4 | about: Ask usage questions and discuss Struson in GitHub Discussions. 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation issue 3 | about: Report an issue with the Struson documentation, e.g. incorrect or incomplete information. 4 | title: '' 5 | labels: documentation 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Struson version 11 | 12 | 13 | 14 | ### Location 15 | 16 | 17 | 18 | ### Description 19 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement_suggestion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement suggestion 3 | about: Suggest an enhancement for Struson. 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Problem solved by the enhancement 11 | 12 | 13 | 14 | ### Enhancement description 15 | 16 | 17 | 18 | ### Alternatives / workarounds 19 | 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "cargo" 6 | directory: "/" 7 | schedule: 8 | interval: "monthly" 9 | 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "monthly" 14 | ignore: 15 | # Ignore patch updates for 'taiki-e/install-action' because they seem to only update the tool 16 | # versions; however GitHub workflow here has intentionally pinned tool version 17 | - dependency-name: "taiki-e/install-action" 18 | update-types: ["version-update:semver-patch"] 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | # Ignore Dependabot branches because it will also open a pull request, which would cause the 7 | # workflow to redundantly run twice 8 | - dependabot/** 9 | pull_request: 10 | 11 | 12 | permissions: 13 | contents: read # to fetch code (actions/checkout) 14 | 15 | env: 16 | # Enable colored terminal output, see https://doc.rust-lang.org/cargo/reference/config.html#termcolor 17 | CARGO_TERM_COLOR: always 18 | 19 | jobs: 20 | build: 21 | strategy: 22 | matrix: 23 | os: [ubuntu-latest, windows-latest] 24 | runs-on: ${{ matrix.os }} 25 | name: Build (${{ matrix.os }}) 26 | 27 | steps: 28 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 29 | 30 | - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 31 | 32 | - name: Install cargo-make 33 | uses: taiki-e/install-action@33734a118689b0b418824fb78ea2bf18e970b43b #v2.50.4 34 | with: 35 | tool: cargo-make@0.37.23 36 | 37 | - name: Build 38 | run: cargo make 39 | 40 | - name: Install cargo-hack 41 | uses: taiki-e/install-action@33734a118689b0b418824fb78ea2bf18e970b43b #v2.50.4 42 | with: 43 | tool: cargo-hack@0.6.28 44 | # See https://doc.rust-lang.org/cargo/guide/continuous-integration.html#verifying-rust-version 45 | - name: Check 'rust-version' compatibility 46 | run: cargo hack check --rust-version --workspace --all-targets --all-features 47 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | # Only run when manually triggered 5 | workflow_dispatch: 6 | inputs: 7 | versionBump: 8 | description: 'Part of the SemVer project version to bump for release (..)' 9 | required: true 10 | type: choice 11 | options: 12 | - major 13 | - minor 14 | - patch 15 | 16 | permissions: 17 | contents: write # read repository content and push updated version and tag 18 | 19 | env: 20 | # Enable colored terminal output, see https://doc.rust-lang.org/cargo/reference/config.html#termcolor 21 | CARGO_TERM_COLOR: always 22 | 23 | # TODO: Maybe switch to https://github.com/crate-ci/cargo-release in the future, and if possible 24 | # let it check API SemVer compliance, see also https://github.com/crate-ci/cargo-release/issues/62 25 | 26 | jobs: 27 | publish: 28 | runs-on: ubuntu-latest 29 | environment: publishing 30 | steps: 31 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 32 | 33 | - name: Update project version 34 | shell: bash 35 | # https://github.com/killercup/cargo-edit 36 | run: | 37 | cargo install --no-default-features --features set-version cargo-edit@0.13.0 38 | cargo set-version --bump ${{ inputs.versionBump }} 39 | 40 | # There is currently no easy way to get the new version number (see also https://github.com/killercup/cargo-edit/issues/524), 41 | # so have to get it by other means 42 | - name: Get new version 43 | id: get-new-version 44 | shell: bash 45 | # See https://stackoverflow.com/a/75023425 46 | # and https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter 47 | run: | 48 | VERSION=$(cargo metadata --format-version=1 --no-deps | jq --compact-output --raw-output --exit-status '.packages[0].version') 49 | echo "New version: $VERSION" 50 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 51 | 52 | - name: Commit version update 53 | shell: bash 54 | run: | 55 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 56 | git config user.name "github-actions[bot]" 57 | git add . 58 | git commit -m "Release version ${{ steps.get-new-version.outputs.VERSION }}" 59 | git tag -a "v${{ steps.get-new-version.outputs.VERSION }}" -m "Release ${{ steps.get-new-version.outputs.VERSION }}" 60 | 61 | - name: Install cargo-make 62 | uses: taiki-e/install-action@33734a118689b0b418824fb78ea2bf18e970b43b #v2.50.4 63 | with: 64 | tool: cargo-make@0.37.23 65 | 66 | # Perform full build to make sure there are no issues 67 | - name: Build project 68 | shell: bash 69 | run: cargo make 70 | 71 | # TODO: Once this project is more stable, maybe include SemVer checks, e.g. 72 | # https://github.com/rust-lang/rust-semverver or https://github.com/obi1kenobi/cargo-semver-checks 73 | 74 | # Clean up results from build to not affect publish in any way 75 | - name: Clean 76 | shell: bash 77 | run: cargo clean 78 | 79 | - name: Publish 80 | shell: bash 81 | # TODO: Fail on any warnings (e.g. incorrect Cargo.toml values); not yet available, see https://github.com/rust-lang/cargo/issues/8424 82 | run: cargo publish --all-features 83 | env: 84 | # CRATES_IO_TOKEN has to be set as GitHub environment secret 85 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 86 | 87 | - name: Push Git changes 88 | shell: bash 89 | run: git push --follow-tags 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # IntelliJ / RustRover 4 | /.idea 5 | 6 | # VS Code 7 | /.vscode/settings.json 8 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anes" 31 | version = "0.1.6" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.3" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" 40 | 41 | [[package]] 42 | name = "assert_no_alloc" 43 | version = "1.1.2" 44 | source = "git+https://github.com/Windfisch/rust-assert-no-alloc.git?rev=d31f2d5f550ce339d1c2f0c1ab7da951224b20df#d31f2d5f550ce339d1c2f0c1ab7da951224b20df" 45 | dependencies = [ 46 | "backtrace", 47 | ] 48 | 49 | [[package]] 50 | name = "autocfg" 51 | version = "1.1.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 54 | 55 | [[package]] 56 | name = "backtrace" 57 | version = "0.3.69" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 60 | dependencies = [ 61 | "addr2line", 62 | "cc", 63 | "cfg-if", 64 | "libc", 65 | "miniz_oxide", 66 | "object", 67 | "rustc-demangle", 68 | ] 69 | 70 | [[package]] 71 | name = "bitflags" 72 | version = "2.4.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" 75 | 76 | [[package]] 77 | name = "bumpalo" 78 | version = "3.14.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 81 | 82 | [[package]] 83 | name = "cast" 84 | version = "0.3.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 87 | 88 | [[package]] 89 | name = "cc" 90 | version = "1.0.83" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 93 | dependencies = [ 94 | "libc", 95 | ] 96 | 97 | [[package]] 98 | name = "cfg-if" 99 | version = "1.0.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 102 | 103 | [[package]] 104 | name = "ciborium" 105 | version = "0.2.1" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" 108 | dependencies = [ 109 | "ciborium-io", 110 | "ciborium-ll", 111 | "serde", 112 | ] 113 | 114 | [[package]] 115 | name = "ciborium-io" 116 | version = "0.2.1" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" 119 | 120 | [[package]] 121 | name = "ciborium-ll" 122 | version = "0.2.1" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" 125 | dependencies = [ 126 | "ciborium-io", 127 | "half", 128 | ] 129 | 130 | [[package]] 131 | name = "clap" 132 | version = "4.4.4" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" 135 | dependencies = [ 136 | "clap_builder", 137 | ] 138 | 139 | [[package]] 140 | name = "clap_builder" 141 | version = "4.4.4" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" 144 | dependencies = [ 145 | "anstyle", 146 | "clap_lex", 147 | ] 148 | 149 | [[package]] 150 | name = "clap_lex" 151 | version = "0.5.1" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" 154 | 155 | [[package]] 156 | name = "criterion" 157 | version = "0.5.1" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" 160 | dependencies = [ 161 | "anes", 162 | "cast", 163 | "ciborium", 164 | "clap", 165 | "criterion-plot", 166 | "is-terminal", 167 | "itertools", 168 | "num-traits", 169 | "once_cell", 170 | "oorandom", 171 | "plotters", 172 | "rayon", 173 | "regex", 174 | "serde", 175 | "serde_derive", 176 | "serde_json", 177 | "tinytemplate", 178 | "walkdir", 179 | ] 180 | 181 | [[package]] 182 | name = "criterion-plot" 183 | version = "0.5.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" 186 | dependencies = [ 187 | "cast", 188 | "itertools", 189 | ] 190 | 191 | [[package]] 192 | name = "crossbeam-deque" 193 | version = "0.8.3" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" 196 | dependencies = [ 197 | "cfg-if", 198 | "crossbeam-epoch", 199 | "crossbeam-utils", 200 | ] 201 | 202 | [[package]] 203 | name = "crossbeam-epoch" 204 | version = "0.9.15" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" 207 | dependencies = [ 208 | "autocfg", 209 | "cfg-if", 210 | "crossbeam-utils", 211 | "memoffset", 212 | "scopeguard", 213 | ] 214 | 215 | [[package]] 216 | name = "crossbeam-utils" 217 | version = "0.8.16" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" 220 | dependencies = [ 221 | "cfg-if", 222 | ] 223 | 224 | [[package]] 225 | name = "duplicate" 226 | version = "2.0.0" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "97af9b5f014e228b33e77d75ee0e6e87960124f0f4b16337b586a6bec91867b1" 229 | dependencies = [ 230 | "heck", 231 | "proc-macro2", 232 | "proc-macro2-diagnostics", 233 | ] 234 | 235 | [[package]] 236 | name = "either" 237 | version = "1.9.0" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 240 | 241 | [[package]] 242 | name = "errno" 243 | version = "0.3.3" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" 246 | dependencies = [ 247 | "errno-dragonfly", 248 | "libc", 249 | "windows-sys", 250 | ] 251 | 252 | [[package]] 253 | name = "errno-dragonfly" 254 | version = "0.1.2" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 257 | dependencies = [ 258 | "cc", 259 | "libc", 260 | ] 261 | 262 | [[package]] 263 | name = "gimli" 264 | version = "0.28.0" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 267 | 268 | [[package]] 269 | name = "half" 270 | version = "1.8.2" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" 273 | 274 | [[package]] 275 | name = "heck" 276 | version = "0.5.0" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 279 | 280 | [[package]] 281 | name = "hermit-abi" 282 | version = "0.3.3" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 285 | 286 | [[package]] 287 | name = "is-terminal" 288 | version = "0.4.9" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" 291 | dependencies = [ 292 | "hermit-abi", 293 | "rustix", 294 | "windows-sys", 295 | ] 296 | 297 | [[package]] 298 | name = "itertools" 299 | version = "0.10.5" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 302 | dependencies = [ 303 | "either", 304 | ] 305 | 306 | [[package]] 307 | name = "itoa" 308 | version = "1.0.9" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 311 | 312 | [[package]] 313 | name = "js-sys" 314 | version = "0.3.64" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" 317 | dependencies = [ 318 | "wasm-bindgen", 319 | ] 320 | 321 | [[package]] 322 | name = "libc" 323 | version = "0.2.148" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" 326 | 327 | [[package]] 328 | name = "linux-raw-sys" 329 | version = "0.4.10" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" 332 | 333 | [[package]] 334 | name = "log" 335 | version = "0.4.20" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 338 | 339 | [[package]] 340 | name = "memchr" 341 | version = "2.6.3" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 344 | 345 | [[package]] 346 | name = "memoffset" 347 | version = "0.9.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" 350 | dependencies = [ 351 | "autocfg", 352 | ] 353 | 354 | [[package]] 355 | name = "miniz_oxide" 356 | version = "0.7.1" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 359 | dependencies = [ 360 | "adler", 361 | ] 362 | 363 | [[package]] 364 | name = "num-traits" 365 | version = "0.2.16" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" 368 | dependencies = [ 369 | "autocfg", 370 | ] 371 | 372 | [[package]] 373 | name = "object" 374 | version = "0.32.1" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 377 | dependencies = [ 378 | "memchr", 379 | ] 380 | 381 | [[package]] 382 | name = "once_cell" 383 | version = "1.18.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 386 | 387 | [[package]] 388 | name = "oorandom" 389 | version = "11.1.3" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" 392 | 393 | [[package]] 394 | name = "plotters" 395 | version = "0.3.5" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" 398 | dependencies = [ 399 | "num-traits", 400 | "plotters-backend", 401 | "plotters-svg", 402 | "wasm-bindgen", 403 | "web-sys", 404 | ] 405 | 406 | [[package]] 407 | name = "plotters-backend" 408 | version = "0.3.5" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" 411 | 412 | [[package]] 413 | name = "plotters-svg" 414 | version = "0.3.5" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" 417 | dependencies = [ 418 | "plotters-backend", 419 | ] 420 | 421 | [[package]] 422 | name = "proc-macro2" 423 | version = "1.0.92" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 426 | dependencies = [ 427 | "unicode-ident", 428 | ] 429 | 430 | [[package]] 431 | name = "proc-macro2-diagnostics" 432 | version = "0.10.1" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" 435 | dependencies = [ 436 | "proc-macro2", 437 | "quote", 438 | "syn", 439 | "version_check", 440 | "yansi", 441 | ] 442 | 443 | [[package]] 444 | name = "quote" 445 | version = "1.0.35" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 448 | dependencies = [ 449 | "proc-macro2", 450 | ] 451 | 452 | [[package]] 453 | name = "rayon" 454 | version = "1.8.0" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" 457 | dependencies = [ 458 | "either", 459 | "rayon-core", 460 | ] 461 | 462 | [[package]] 463 | name = "rayon-core" 464 | version = "1.12.0" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" 467 | dependencies = [ 468 | "crossbeam-deque", 469 | "crossbeam-utils", 470 | ] 471 | 472 | [[package]] 473 | name = "regex" 474 | version = "1.9.5" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" 477 | dependencies = [ 478 | "aho-corasick", 479 | "memchr", 480 | "regex-automata", 481 | "regex-syntax", 482 | ] 483 | 484 | [[package]] 485 | name = "regex-automata" 486 | version = "0.3.8" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" 489 | dependencies = [ 490 | "aho-corasick", 491 | "memchr", 492 | "regex-syntax", 493 | ] 494 | 495 | [[package]] 496 | name = "regex-syntax" 497 | version = "0.7.5" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" 500 | 501 | [[package]] 502 | name = "rustc-demangle" 503 | version = "0.1.23" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 506 | 507 | [[package]] 508 | name = "rustix" 509 | version = "0.38.20" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" 512 | dependencies = [ 513 | "bitflags", 514 | "errno", 515 | "libc", 516 | "linux-raw-sys", 517 | "windows-sys", 518 | ] 519 | 520 | [[package]] 521 | name = "rustversion" 522 | version = "1.0.14" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 525 | 526 | [[package]] 527 | name = "ryu" 528 | version = "1.0.15" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 531 | 532 | [[package]] 533 | name = "same-file" 534 | version = "1.0.6" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 537 | dependencies = [ 538 | "winapi-util", 539 | ] 540 | 541 | [[package]] 542 | name = "scopeguard" 543 | version = "1.2.0" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 546 | 547 | [[package]] 548 | name = "serde" 549 | version = "1.0.219" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 552 | dependencies = [ 553 | "serde_derive", 554 | ] 555 | 556 | [[package]] 557 | name = "serde_derive" 558 | version = "1.0.219" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 561 | dependencies = [ 562 | "proc-macro2", 563 | "quote", 564 | "syn", 565 | ] 566 | 567 | [[package]] 568 | name = "serde_json" 569 | version = "1.0.140" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 572 | dependencies = [ 573 | "itoa", 574 | "memchr", 575 | "ryu", 576 | "serde", 577 | ] 578 | 579 | [[package]] 580 | name = "strum" 581 | version = "0.27.1" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" 584 | dependencies = [ 585 | "strum_macros", 586 | ] 587 | 588 | [[package]] 589 | name = "strum_macros" 590 | version = "0.27.1" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" 593 | dependencies = [ 594 | "heck", 595 | "proc-macro2", 596 | "quote", 597 | "rustversion", 598 | "syn", 599 | ] 600 | 601 | [[package]] 602 | name = "struson" 603 | version = "0.6.0" 604 | dependencies = [ 605 | "assert_no_alloc", 606 | "criterion", 607 | "duplicate", 608 | "serde", 609 | "serde_json", 610 | "strum", 611 | "thiserror", 612 | ] 613 | 614 | [[package]] 615 | name = "syn" 616 | version = "2.0.90" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" 619 | dependencies = [ 620 | "proc-macro2", 621 | "quote", 622 | "unicode-ident", 623 | ] 624 | 625 | [[package]] 626 | name = "thiserror" 627 | version = "2.0.12" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 630 | dependencies = [ 631 | "thiserror-impl", 632 | ] 633 | 634 | [[package]] 635 | name = "thiserror-impl" 636 | version = "2.0.12" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 639 | dependencies = [ 640 | "proc-macro2", 641 | "quote", 642 | "syn", 643 | ] 644 | 645 | [[package]] 646 | name = "tinytemplate" 647 | version = "1.2.1" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 650 | dependencies = [ 651 | "serde", 652 | "serde_json", 653 | ] 654 | 655 | [[package]] 656 | name = "unicode-ident" 657 | version = "1.0.12" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 660 | 661 | [[package]] 662 | name = "version_check" 663 | version = "0.9.4" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 666 | 667 | [[package]] 668 | name = "walkdir" 669 | version = "2.4.0" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 672 | dependencies = [ 673 | "same-file", 674 | "winapi-util", 675 | ] 676 | 677 | [[package]] 678 | name = "wasm-bindgen" 679 | version = "0.2.87" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" 682 | dependencies = [ 683 | "cfg-if", 684 | "wasm-bindgen-macro", 685 | ] 686 | 687 | [[package]] 688 | name = "wasm-bindgen-backend" 689 | version = "0.2.87" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" 692 | dependencies = [ 693 | "bumpalo", 694 | "log", 695 | "once_cell", 696 | "proc-macro2", 697 | "quote", 698 | "syn", 699 | "wasm-bindgen-shared", 700 | ] 701 | 702 | [[package]] 703 | name = "wasm-bindgen-macro" 704 | version = "0.2.87" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" 707 | dependencies = [ 708 | "quote", 709 | "wasm-bindgen-macro-support", 710 | ] 711 | 712 | [[package]] 713 | name = "wasm-bindgen-macro-support" 714 | version = "0.2.87" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" 717 | dependencies = [ 718 | "proc-macro2", 719 | "quote", 720 | "syn", 721 | "wasm-bindgen-backend", 722 | "wasm-bindgen-shared", 723 | ] 724 | 725 | [[package]] 726 | name = "wasm-bindgen-shared" 727 | version = "0.2.87" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" 730 | 731 | [[package]] 732 | name = "web-sys" 733 | version = "0.3.64" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" 736 | dependencies = [ 737 | "js-sys", 738 | "wasm-bindgen", 739 | ] 740 | 741 | [[package]] 742 | name = "winapi" 743 | version = "0.3.9" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 746 | dependencies = [ 747 | "winapi-i686-pc-windows-gnu", 748 | "winapi-x86_64-pc-windows-gnu", 749 | ] 750 | 751 | [[package]] 752 | name = "winapi-i686-pc-windows-gnu" 753 | version = "0.4.0" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 756 | 757 | [[package]] 758 | name = "winapi-util" 759 | version = "0.1.6" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 762 | dependencies = [ 763 | "winapi", 764 | ] 765 | 766 | [[package]] 767 | name = "winapi-x86_64-pc-windows-gnu" 768 | version = "0.4.0" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 771 | 772 | [[package]] 773 | name = "windows-sys" 774 | version = "0.48.0" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 777 | dependencies = [ 778 | "windows-targets", 779 | ] 780 | 781 | [[package]] 782 | name = "windows-targets" 783 | version = "0.48.5" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 786 | dependencies = [ 787 | "windows_aarch64_gnullvm", 788 | "windows_aarch64_msvc", 789 | "windows_i686_gnu", 790 | "windows_i686_msvc", 791 | "windows_x86_64_gnu", 792 | "windows_x86_64_gnullvm", 793 | "windows_x86_64_msvc", 794 | ] 795 | 796 | [[package]] 797 | name = "windows_aarch64_gnullvm" 798 | version = "0.48.5" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 801 | 802 | [[package]] 803 | name = "windows_aarch64_msvc" 804 | version = "0.48.5" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 807 | 808 | [[package]] 809 | name = "windows_i686_gnu" 810 | version = "0.48.5" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 813 | 814 | [[package]] 815 | name = "windows_i686_msvc" 816 | version = "0.48.5" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 819 | 820 | [[package]] 821 | name = "windows_x86_64_gnu" 822 | version = "0.48.5" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 825 | 826 | [[package]] 827 | name = "windows_x86_64_gnullvm" 828 | version = "0.48.5" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 831 | 832 | [[package]] 833 | name = "windows_x86_64_msvc" 834 | version = "0.48.5" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 837 | 838 | [[package]] 839 | name = "yansi" 840 | version = "1.0.1" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 843 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "struson" 3 | version = "0.6.0" 4 | authors = ["Marcono1234"] 5 | edition = "2021" 6 | rust-version = "1.75.0" 7 | description = "A low-level streaming JSON reader and writer" 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/Marcono1234/struson" 10 | keywords = ["json", "streaming", "parser"] 11 | categories = ["parser-implementations"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | thiserror = "2.0.12" 17 | strum = { version = "0.27.1", features = ["derive"] } 18 | duplicate = "2.0.0" 19 | 20 | serde = { version = "1.0.219", optional = true } 21 | 22 | [dev-dependencies] 23 | criterion = { version = "0.5.1", features = ["html_reports"] } 24 | # Serde is used for comparison in benchmarks and for tests 25 | serde = "1.0.219" 26 | # When updating serde_json, adjust Struson Serde integration behavior to match serde_json 27 | serde_json = "1.0.140" 28 | # Used for verifying in allocation tests that no allocations occur in certain situations 29 | # Specify Git revision because version with "backtrace" feature has not been released yet 30 | assert_no_alloc = { git = "https://github.com/Windfisch/rust-assert-no-alloc.git", rev = "d31f2d5f550ce339d1c2f0c1ab7da951224b20df", features = [ 31 | "backtrace", 32 | ] } 33 | 34 | [features] 35 | # Optional Serde integration 36 | serde = ["dep:serde"] 37 | # Optional simple JSON reader and writer API 38 | simple-api = [] 39 | 40 | [lints.rust] 41 | unsafe_code = "forbid" 42 | # The documentation discourages omitting `'_` in paths, but this lint is "allow" by default, 43 | # see https://doc.rust-lang.org/reference/lifetime-elision.html#lifetime-elision-in-functions 44 | elided_lifetimes_in_paths = "warn" 45 | 46 | [lints.clippy] 47 | # Allow needless `return` because that makes it sometimes more obvious that 48 | # an expression is the result of the function 49 | needless_return = "allow" 50 | # Allow `assert_eq!(true, ...)` because in some cases it is used to check a bool 51 | # value and not a 'flag' / 'state', and `assert_eq!` makes that more explicit 52 | bool_assert_comparison = "allow" 53 | 54 | # docs.rs specific configuration 55 | [package.metadata.docs.rs] 56 | # Document all features 57 | all-features = true 58 | # Set configuration flag to enable docs.rs specific doc attributes, such as `doc_auto_cfg` 59 | # See https://stackoverflow.com/q/61417452 60 | rustdoc-args = ["--cfg", "docsrs"] 61 | 62 | # Cargo Profile for manual performance profiling 63 | [profile.release-debug] 64 | inherits = "release" 65 | debug = true 66 | 67 | 68 | # For all these benchmarks disable default harness (`harness = false`) because Criterion creates its own 69 | 70 | [[bench]] 71 | name = "reader_benchmark" 72 | harness = false 73 | 74 | [[bench]] 75 | name = "reader_struct_benchmark" 76 | harness = false 77 | 78 | [[bench]] 79 | name = "writer_benchmark" 80 | harness = false 81 | 82 | [[bench]] 83 | name = "writer_struct_benchmark" 84 | harness = false 85 | 86 | [[bench]] 87 | name = "serde_serialize_benchmark" 88 | harness = false 89 | required-features = ["serde"] 90 | 91 | [[bench]] 92 | name = "serde_deserialize_benchmark" 93 | harness = false 94 | required-features = ["serde"] 95 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Marcono1234 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | # Uses https://github.com/sagiegurari/cargo-make 2 | 3 | [config] 4 | # Skip core tasks; they overlap with some of the tasks below 5 | skip_core_tasks = true 6 | 7 | [env] 8 | RUSTFLAGS = "--deny warnings" 9 | RUSTDOCFLAGS = "--deny warnings" 10 | 11 | 12 | # The following differentiates between default features and Serde interoperability feature 13 | # to detect build issues when Serde feature is not enabled 14 | 15 | # Default features 16 | [tasks.build-default] 17 | command = "cargo" 18 | args = ["build", "--all-targets"] 19 | 20 | [tasks.test-default] 21 | command = "cargo" 22 | # Important: Don't use `--all-targets` here; that would exclude doc tests (see https://github.com/rust-lang/cargo/issues/6669) 23 | # Benchmark tests are performed further below; could include them here for this execution in the future though 24 | args = ["test"] 25 | dependencies = ["build-default"] 26 | 27 | 28 | # Serde interoperability feature 29 | [tasks.build-serde-interop] 30 | command = "cargo" 31 | args = ["build", "--all-targets", "--features", "serde"] 32 | 33 | [tasks.test-serde-interop] 34 | command = "cargo" 35 | # Important: Don't use `--all-targets` here; that would exclude doc tests (see https://github.com/rust-lang/cargo/issues/6669) 36 | # Benchmark tests are performed further below; could include them here for this execution in the future though 37 | args = ["test", "--features", "serde"] 38 | dependencies = ["build-serde-interop"] 39 | 40 | 41 | # All features 42 | [tasks.build-all-features] 43 | command = "cargo" 44 | args = ["build", "--all-targets", "--all-features"] 45 | 46 | [tasks.test-all-features] 47 | command = "cargo" 48 | # Important: Don't use `--all-targets` here; that would exclude doc tests (see https://github.com/rust-lang/cargo/issues/6669) 49 | # Benchmark tests are performed further below; could include them here for this execution in the future though 50 | args = ["test", "--all-features"] 51 | dependencies = ["build-all-features"] 52 | 53 | 54 | [tasks.test-benches] 55 | command = "cargo" 56 | # Run all benchmarks as tests to make sure they don't encounter any errors, e.g. due to malformed JSON 57 | # Unfortunately this seems to rerun library tests, see https://github.com/rust-lang/cargo/issues/6454 58 | args = ["test", "--benches", "--all-features"] 59 | dependencies = ["test-default", "test-serde-interop", "test-all-features"] 60 | 61 | [tasks.build] 62 | dependencies = [ 63 | "build-default", 64 | "build-serde-interop", 65 | "build-all-features", 66 | ] 67 | 68 | [tasks.test] 69 | dependencies = [ 70 | "test-default", 71 | "test-serde-interop", 72 | "test-all-features", 73 | "test-benches", 74 | ] 75 | 76 | # Note: Running Clippy should hopefully suffice, no need to run `cargo check`, see https://stackoverflow.com/q/57449356 77 | [tasks.clippy] 78 | command = "cargo" 79 | args = ["clippy", "--all-targets", "--all-features", "--", "--deny", "warnings"] 80 | 81 | [tasks.doc] 82 | command = "cargo" 83 | args = ["doc", "--all-features", "--no-deps"] 84 | 85 | [tasks.format-check] 86 | command = "cargo" 87 | args = ["fmt", "--all", "--check"] 88 | 89 | 90 | # Note: 'default' task is called when `cargo make` is used without explicit task name 91 | [tasks.default] 92 | # Dependencies here are ordered by 'severity'; first fail for build errors and eventually 93 | # fail in case of format check errors 94 | dependencies = [ 95 | "build", 96 | "test", 97 | "clippy", 98 | "doc", 99 | "format-check", 100 | ] 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #
Struson
[![crates.io](https://img.shields.io/crates/v/struson)](https://crates.io/crates/struson) [![docs.rs](https://img.shields.io/docsrs/struson?label=docs.rs)](https://docs.rs/struson)
2 | 3 | Struson is an [RFC 8259](https://www.rfc-editor.org/rfc/rfc8259.html) compliant streaming JSON reader and writer. 4 | 5 | Its main purpose is to allow reading and writing JSON documents in a memory efficient way without having to store the complete JSON document structure in memory. 6 | 7 | The API of Struson was inspired by the streaming API of the Java library [Gson](https://github.com/google/gson) (classes `JsonReader` and `JsonWriter`). It is rather low-level and its methods correspond to the elements of a JSON document, with little abstraction on top of it, allowing to read and write any valid JSON document regardless of its structure or content. 8 | 9 | **Note:** This library is still experimental. The performance is not very good yet and the API might be changed in future versions; releases < 1.0.0 might not follow [Semantic Versioning](https://doc.rust-lang.org/cargo/reference/semver.html), breaking changes may occur.\ 10 | Feedback and suggestions for improvements are welcome! 11 | 12 | ## Why? 13 | 14 | The most popular JSON Rust crates [Serde JSON (`serde_json`)](https://github.com/serde-rs/json) and [json-rust (`json`)](https://github.com/maciejhirsz/json-rust) mainly provide high level APIs for working with JSON. 15 | 16 | - Serde JSON provides an API for converting JSON into DOM like structures (module `serde_json::value`) and object mapper functionality by converting structs to JSON and vice versa. Both requires the complete value to be present in memory. The trait `serde_json::ser::Formatter` actually allows writing JSON in a streaming way, but its API is arguably too low level and inconvenient to use: You have to handle string escaping yourself, and you always have to provide the writer as argument for every method call.\ 17 | Note however, that Serde JSON's [`StreamDeserializer`](https://docs.rs/serde_json/latest/serde_json/struct.StreamDeserializer.html) allows reading multiple top-level values in a streaming way, and that certain streaming use cases can be solved with custom `Visitor` implementations, see the documentation for examples of [streaming an array](https://serde.rs/stream-array.html) and [discarding data](https://serde.rs/ignored-any.html). 18 | 19 | - json-rust provides an API for converting JSON into DOM like structures (enum `json::JsonValue`), this requires the complete value to be present in memory. The trait `json::codegen::Generator` offers a partial API for writing JSON in a streaming way, however it misses methods for writing JSON arrays and objects in a streaming way. 20 | 21 | If you need to process JSON in a DOM like way or want object mapper functionality to convert structs to JSON and vice versa, then Struson is _not_ suited for your use case and you should instead use one of the libraries above. 22 | 23 | ## Main features 24 | 25 | - Low level streaming API, no implicit value conversion 26 | - Strong enforcement of correct API usage 27 | - Panics only for incorrect API usage\ 28 | Malformed JSON and unexpected JSON structure only causes errors 29 | - API does not require recursion for JSON arrays and objects\ 30 | Can theoretically read and write arbitrarily deeply nested JSON data 31 | - Read and write arbitrarily precise JSON numbers as string\ 32 | ([`JsonReader::next_number_as_str`](https://docs.rs/struson/latest/struson/reader/trait.JsonReader.html#tymethod.next_number_as_str) and [`JsonWriter::number_value_from_string`](https://docs.rs/struson/latest/struson/writer/trait.JsonWriter.html#tymethod.number_value_from_string)) 33 | - Seek to specific location in JSON data ([`JsonReader::seek_to`](https://docs.rs/struson/latest/struson/reader/trait.JsonReader.html#tymethod.seek_to)) 34 | - Transfer JSON data from a reader to a writer ([`JsonReader::transfer_to`](https://docs.rs/struson/latest/struson/reader/trait.JsonReader.html#tymethod.transfer_to)) 35 | - Read and write arbitrarily large JSON string values\ 36 | ([`JsonReader::next_string_reader`](https://docs.rs/struson/latest/struson/reader/trait.JsonReader.html#tymethod.next_string_reader) and [`JsonWriter::string_value_writer`](https://docs.rs/struson/latest/struson/writer/trait.JsonWriter.html#tymethod.string_value_writer)) 37 | - Optional [Serde integration](#serde-integration) 38 | 39 | ## Usage examples 40 | 41 | Two variants of the API are provided: 42 | 43 | - simple: ensures correct API usage at compile-time 44 | - advanced: ensures correct API usage only at runtime (by panicking); more flexible and 45 | provides more functionality 46 | 47 | ### Simple API 48 | 49 | **🔬 Experimental**\ 50 | The simple API and its naming is currently experimental, please provide feedback [here](https://github.com/Marcono1234/struson/issues/34). 51 | It has to be enabled by specifying the `simple-api` feature in `Cargo.toml`: 52 | 53 | ```toml 54 | [dependencies] 55 | struson = { version = "...", features = ["simple-api"] } 56 | ``` 57 | 58 | Any feedback is appreciated! 59 | 60 | #### Reading 61 | 62 | See [`SimpleJsonReader`](https://docs.rs/struson/latest/struson/reader/simple/struct.SimpleJsonReader.html). 63 | 64 | ```rust 65 | use struson::reader::simple::*; 66 | 67 | // In this example JSON data comes from a string; 68 | // normally it would come from a file or a network connection 69 | let json_reader = SimpleJsonReader::new(r#"["a", "short", "example"]"#.as_bytes()); 70 | let mut words = Vec::::new(); 71 | json_reader.read_array_items(|item_reader| { 72 | let word = item_reader.read_string()?; 73 | words.push(word); 74 | Ok(()) 75 | })?; 76 | assert_eq!(words, vec!["a", "short", "example"]); 77 | ``` 78 | 79 | For reading nested values, the methods [`read_seeked`](https://docs.rs/struson/latest/struson/reader/simple/trait.ValueReader.html#tymethod.read_seeked) 80 | and [`read_seeked_multi`](https://docs.rs/struson/latest/struson/reader/simple/trait.ValueReader.html#tymethod.read_seeked_multi) 81 | can be used: 82 | 83 | ```rust 84 | use struson::reader::simple::*; 85 | use struson::reader::simple::multi_json_path::multi_json_path; 86 | 87 | // In this example JSON data comes from a string; 88 | // normally it would come from a file or a network connection 89 | let json = r#"{ 90 | "users": [ 91 | {"name": "John", "age": 32}, 92 | {"name": "Jane", "age": 41} 93 | ] 94 | }"#; 95 | let json_reader = SimpleJsonReader::new(json.as_bytes()); 96 | 97 | let mut ages = Vec::::new(); 98 | // Select the ages of all users 99 | let json_path = multi_json_path!["users", [*], "age"]; 100 | json_reader.read_seeked_multi(&json_path, false, |value_reader| { 101 | let age = value_reader.read_number()??; 102 | ages.push(age); 103 | Ok(()) 104 | })?; 105 | assert_eq!(ages, vec![32, 41]); 106 | ``` 107 | 108 | #### Writing 109 | 110 | See [`SimpleJsonWriter`](https://docs.rs/struson/latest/struson/writer/simple/struct.SimpleJsonWriter.html). 111 | 112 | ```rust 113 | use struson::writer::simple::*; 114 | 115 | // In this example JSON bytes are stored in a Vec; 116 | // normally they would be written to a file or network connection 117 | let mut writer = Vec::::new(); 118 | let json_writer = SimpleJsonWriter::new(&mut writer); 119 | json_writer.write_object(|object_writer| { 120 | object_writer.write_number_member("a", 1)?; 121 | object_writer.write_bool_member("b", true)?; 122 | Ok(()) 123 | })?; 124 | 125 | let json = String::from_utf8(writer)?; 126 | assert_eq!(json, r#"{"a":1,"b":true}"#); 127 | ``` 128 | 129 | ### Advanced API 130 | 131 | #### Reading 132 | 133 | See [`JsonStreamReader`](https://docs.rs/struson/latest/struson/reader/struct.JsonStreamReader.html). 134 | 135 | ```rust 136 | use struson::reader::*; 137 | 138 | // In this example JSON data comes from a string; 139 | // normally it would come from a file or a network connection 140 | let json = r#"{"a": [1, true]}"#; 141 | let mut json_reader = JsonStreamReader::new(json.as_bytes()); 142 | 143 | json_reader.begin_object()?; 144 | assert_eq!(json_reader.next_name()?, "a"); 145 | 146 | json_reader.begin_array()?; 147 | assert_eq!(json_reader.next_number::()??, 1); 148 | assert_eq!(json_reader.next_bool()?, true); 149 | json_reader.end_array()?; 150 | 151 | json_reader.end_object()?; 152 | // Ensures that there is no trailing data 153 | json_reader.consume_trailing_whitespace()?; 154 | ``` 155 | 156 | #### Writing 157 | 158 | See [`JsonStreamWriter`](https://docs.rs/struson/latest/struson/writer/struct.JsonStreamWriter.html). 159 | 160 | ```rust 161 | use struson::writer::*; 162 | 163 | // In this example JSON bytes are stored in a Vec; 164 | // normally they would be written to a file or network connection 165 | let mut writer = Vec::::new(); 166 | let mut json_writer = JsonStreamWriter::new(&mut writer); 167 | 168 | json_writer.begin_object()?; 169 | json_writer.name("a")?; 170 | 171 | json_writer.begin_array()?; 172 | json_writer.number_value(1)?; 173 | json_writer.bool_value(true)?; 174 | json_writer.end_array()?; 175 | 176 | json_writer.end_object()?; 177 | // Ensures that the JSON document is complete and flushes the buffer 178 | json_writer.finish_document()?; 179 | 180 | let json = String::from_utf8(writer)?; 181 | assert_eq!(json, r#"{"a":[1,true]}"#); 182 | ``` 183 | 184 | ## Serde integration 185 | 186 | Optional integration with [Serde](https://docs.rs/serde/latest/serde/) exists to allow writing a `Serialize` to a `JsonWriter` and reading a `Deserialize` from a `JsonReader`. See the [`serde` module](https://docs.rs/struson/latest/struson/serde/index.html) of this crate for more information. 187 | 188 | ## Changelog 189 | 190 | See [GitHub releases](https://github.com/Marcono1234/struson/releases). 191 | 192 | ## Building 193 | 194 | This project uses [cargo-make](https://github.com/sagiegurari/cargo-make) for building: 195 | 196 | ```sh 197 | cargo make 198 | ``` 199 | 200 | If you don't want to install cargo-make, you can instead manually run the tasks declared in the [`Makefile.toml`](Makefile.toml). 201 | 202 | ## Similar projects 203 | 204 | - 205 | > A streaming JSON parser/emitter library for rust 206 | - 207 | > JSON Tokenizer written in Rust 208 | - 209 | > JSON serialization/deserialization (full-featured, modern, streaming, direct into struct/enum) 210 | - 211 | 212 | > Rust re-implementation of the Python streaming JSON parser [ijson](https://github.com/isagalaev/ijson) 213 | - 214 | > A zero-copy json-lexer, filters and serializer. 215 | - 216 | > High-fidelity JSON lexer and parser 217 | - 's `justjson::parser::Tokenizer` 218 | > A JSON tokenizer, which converts JSON source to a series of Tokens 219 | - 220 | > Json pull parser 221 | - 222 | > Simple and fast crate for writing JSON to a string without creating intermediate objects 223 | - 224 | > A queryable, streaming, JSON pull-parser with low allocation overhead. 225 | - 226 | > Actson is a low-level JSON parser for reactive applications and non-blocking I/O. 227 | - [rustc-serialize `Parser`](https://docs.rs/rustc-serialize/latest/rustc_serialize/json/struct.Parser.html) (deprecated) 228 | > A streaming JSON parser implemented as an iterator of JsonEvent, consuming an iterator of char. 229 | 230 | ## License 231 | 232 | Licensed under either of 233 | 234 | - [Apache License, Version 2.0](LICENSE-APACHE) 235 | - [MIT License](LICENSE-MIT) 236 | 237 | at your option. 238 | 239 | All contributions you make to this project are licensed implicitly under both licenses mentioned above, without any additional terms or conditions. 240 | 241 | Note: This dual-licensing is the same you see for the majority of Rust projects, see also the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/necessities.html#crate-and-its-dependencies-have-a-permissive-license-c-permissive). 242 | -------------------------------------------------------------------------------- /ReleaseProcess.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | > [!TIP]\ 4 | > During development already create a draft GitHub Release and write down all relevant changes, so that 5 | > on release you don't have to go in detail through all commits again. 6 | 7 | 1. Determine the changes since last release, and based on that choose the corresponding SemVer version change 8 | 2. Run the ["Publish" workflow](https://github.com/Marcono1234/struson/actions/workflows/publish.yml) on GitHub\ 9 | This will perform the following things: 10 | - Update the project version 11 | - Git commit & tag the changes 12 | - Publish the version to `crates.io` 13 | - Push the Git changes 14 | 3. Create a [GitHub Release](https://github.com/Marcono1234/struson/releases) (respectively publish the existing draft release)\ 15 | Select the newly created Git tag for that. 16 | 4. Optional: In case issues reported by other users were fixed, inform them that a new release including the fix has been published. 17 | -------------------------------------------------------------------------------- /benches/reader_benchmark.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use criterion::{criterion_group, criterion_main, Criterion}; 4 | use serde::{de::Visitor, Deserializer}; 5 | use serde_json::de::{IoRead, Read, StrRead}; 6 | use struson::reader::*; 7 | 8 | fn call_unwrap Result<(), Box>>(f: F) { 9 | f().unwrap(); 10 | } 11 | 12 | fn bench_compare(c: &mut Criterion, name: &str, json: &str) { 13 | let mut group = c.benchmark_group(name); 14 | group.bench_with_input("struson-skip", json, |b, json| { 15 | b.iter(|| { 16 | call_unwrap(|| { 17 | let mut json_reader = JsonStreamReader::new_custom( 18 | json.as_bytes(), 19 | ReaderSettings { 20 | max_nesting_depth: None, 21 | ..Default::default() 22 | }, 23 | ); 24 | json_reader.skip_value()?; 25 | json_reader.consume_trailing_whitespace()?; 26 | Ok(()) 27 | }); 28 | }) 29 | }); 30 | group.bench_with_input("struson-skip (no path tracking)", json, |b, json| { 31 | b.iter(|| { 32 | call_unwrap(|| { 33 | let mut json_reader = JsonStreamReader::new_custom( 34 | json.as_bytes(), 35 | ReaderSettings { 36 | track_path: false, 37 | max_nesting_depth: None, 38 | ..Default::default() 39 | }, 40 | ); 41 | json_reader.skip_value()?; 42 | json_reader.consume_trailing_whitespace()?; 43 | Ok(()) 44 | }); 45 | }) 46 | }); 47 | 48 | fn struson_read( 49 | mut json_reader: JsonStreamReader, 50 | ) -> Result<(), Box> { 51 | enum StackValue { 52 | Array, 53 | Object, 54 | } 55 | 56 | let mut stack = Vec::new(); 57 | loop { 58 | if !stack.is_empty() { 59 | match stack.last().unwrap() { 60 | StackValue::Array => { 61 | if !json_reader.has_next()? { 62 | stack.pop(); 63 | json_reader.end_array()?; 64 | 65 | if stack.is_empty() { 66 | break; 67 | } else { 68 | continue; 69 | } 70 | } 71 | } 72 | StackValue::Object => { 73 | if json_reader.has_next()? { 74 | json_reader.next_name()?; 75 | // fall through to value reading 76 | } else { 77 | stack.pop(); 78 | json_reader.end_object()?; 79 | 80 | if stack.is_empty() { 81 | break; 82 | } else { 83 | continue; 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | match json_reader.peek()? { 91 | ValueType::Array => { 92 | json_reader.begin_array()?; 93 | stack.push(StackValue::Array) 94 | } 95 | ValueType::Object => { 96 | json_reader.begin_object()?; 97 | stack.push(StackValue::Object) 98 | } 99 | ValueType::String => { 100 | json_reader.next_str()?; 101 | } 102 | ValueType::Number => { 103 | json_reader.next_number_as_str()?; 104 | } 105 | ValueType::Boolean => { 106 | json_reader.next_bool()?; 107 | } 108 | ValueType::Null => json_reader.next_null()?, 109 | } 110 | 111 | if stack.is_empty() { 112 | break; 113 | } 114 | } 115 | json_reader.consume_trailing_whitespace()?; 116 | Ok(()) 117 | } 118 | 119 | group.bench_with_input("struson-read", json, |b, json| { 120 | b.iter(|| { 121 | call_unwrap(|| { 122 | let json_reader = JsonStreamReader::new_custom( 123 | json.as_bytes(), 124 | ReaderSettings { 125 | max_nesting_depth: None, 126 | ..Default::default() 127 | }, 128 | ); 129 | struson_read(json_reader) 130 | }); 131 | }) 132 | }); 133 | 134 | group.bench_with_input("struson-read (no path tracking)", json, |b, json| { 135 | b.iter(|| { 136 | call_unwrap(|| { 137 | let json_reader = JsonStreamReader::new_custom( 138 | json.as_bytes(), 139 | ReaderSettings { 140 | track_path: false, 141 | max_nesting_depth: None, 142 | ..Default::default() 143 | }, 144 | ); 145 | struson_read(json_reader) 146 | }); 147 | }) 148 | }); 149 | 150 | fn serde_skip<'a, R: Read<'a>>(read: R) { 151 | struct UnitVisitor; 152 | 153 | impl Visitor<'_> for UnitVisitor { 154 | type Value = (); 155 | 156 | fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 157 | write!(formatter, "unit") 158 | } 159 | 160 | fn visit_unit(self) -> Result { 161 | Ok(()) 162 | } 163 | } 164 | 165 | let mut deserializer = serde_json::de::Deserializer::new(read); 166 | // TODO: Is this correct? 167 | deserializer.deserialize_ignored_any(UnitVisitor).unwrap(); 168 | deserializer.end().unwrap(); 169 | } 170 | 171 | group.bench_with_input("serde-skip (reader)", json, |b, json| { 172 | b.iter(|| { 173 | serde_skip(IoRead::new(json.as_bytes())); 174 | }) 175 | }); 176 | 177 | group.bench_with_input("serde-skip (string)", json, |b, json| { 178 | b.iter(|| { 179 | serde_skip(StrRead::new(json)); 180 | }) 181 | }); 182 | 183 | // TODO: Is there a way to also write a serde_json benchmark which just reads the JSON values, 184 | // without deserializing them into something? 185 | 186 | group.finish(); 187 | } 188 | 189 | fn benchmark_large_array(c: &mut Criterion) { 190 | let json = format!( 191 | "[{}true]", 192 | "true, false, null, 12345689.123e12, \"abcdabcdabcdabcd\",".repeat(1000) 193 | ); 194 | bench_compare(c, "read-large-array", &json); 195 | } 196 | 197 | fn benchmark_nested_object(c: &mut Criterion) { 198 | let count = 1000; 199 | let json = r#"{"member name":"#.repeat(count) + "true" + "}".repeat(count).as_str(); 200 | bench_compare(c, "read-nested-object", &json); 201 | } 202 | 203 | fn benchmark_nested_object_pretty(c: &mut Criterion) { 204 | let count = 1000; 205 | let mut json = "{".to_owned(); 206 | 207 | for i in 1..=count { 208 | json.push('\n'); 209 | json.push_str(" ".repeat(i).as_str()); 210 | json.push_str(r#""member name": {"#); 211 | } 212 | for i in (0..=count).rev() { 213 | json.push('\n'); 214 | json.push_str(" ".repeat(i).as_str()); 215 | json.push('}'); 216 | } 217 | 218 | bench_compare(c, "read-nested-object-pretty", &json); 219 | } 220 | 221 | fn bench_compare_string_reading(c: &mut Criterion, name: &str, json: &str) { 222 | let mut group = c.benchmark_group(name); 223 | 224 | group.bench_with_input("struson", json, |b, json| { 225 | b.iter(|| { 226 | call_unwrap(|| { 227 | let mut json_reader = JsonStreamReader::new(json.as_bytes()); 228 | json_reader.next_str()?; 229 | json_reader.consume_trailing_whitespace()?; 230 | 231 | Ok(()) 232 | }); 233 | }) 234 | }); 235 | 236 | struct StringVisitor; 237 | 238 | impl<'de> Visitor<'de> for StringVisitor { 239 | type Value = (); 240 | 241 | fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 242 | write!(formatter, "a string") 243 | } 244 | 245 | fn visit_borrowed_str(self, _: &'de str) -> Result { 246 | Ok(()) 247 | } 248 | 249 | fn visit_str(self, _: &str) -> Result { 250 | Ok(()) 251 | } 252 | 253 | fn visit_string(self, _: String) -> Result { 254 | Ok(()) 255 | } 256 | } 257 | 258 | fn serde_read<'a, R: Read<'a>, F: FnOnce(&mut serde_json::de::Deserializer)>( 259 | read: R, 260 | read_function: F, 261 | ) { 262 | let mut deserializer = serde_json::de::Deserializer::new(read); 263 | read_function(&mut deserializer); 264 | deserializer.end().unwrap(); 265 | } 266 | 267 | // TODO: Are really all of these Serde benchmarks necessary? 268 | group.bench_with_input("serde-str (reader)", json, |b, json| { 269 | b.iter(|| { 270 | serde_read(IoRead::new(json.as_bytes()), |deserializer| { 271 | deserializer.deserialize_str(StringVisitor).unwrap() 272 | }); 273 | }) 274 | }); 275 | 276 | group.bench_with_input("serde-string (reader)", json, |b, json| { 277 | b.iter(|| { 278 | serde_read(IoRead::new(json.as_bytes()), |deserializer| { 279 | deserializer.deserialize_str(StringVisitor).unwrap() 280 | }); 281 | }) 282 | }); 283 | 284 | group.bench_with_input("serde-str (string)", json, |b, json| { 285 | b.iter(|| { 286 | serde_read(StrRead::new(json), |deserializer| { 287 | deserializer.deserialize_string(StringVisitor).unwrap() 288 | }); 289 | }) 290 | }); 291 | 292 | group.bench_with_input("serde-string (string)", json, |b, json| { 293 | b.iter(|| { 294 | serde_read(StrRead::new(json), |deserializer| { 295 | deserializer.deserialize_string(StringVisitor).unwrap() 296 | }); 297 | }) 298 | }); 299 | 300 | group.finish(); 301 | } 302 | 303 | fn benchmark_large_ascii_string(c: &mut Criterion) { 304 | let json = format!("\"{}\"", "this is a test string".repeat(10_000)); 305 | bench_compare(c, "read-large-ascii-string", &json); 306 | bench_compare_string_reading(c, "read-large-ascii-string (string reading)", &json); 307 | } 308 | 309 | fn benchmark_large_unicode_string(c: &mut Criterion) { 310 | let json = format!( 311 | "\"{}\"", 312 | "ab\u{0080}cd\u{0800}ef\u{1234}gh\u{10FFFF}".repeat(10_000) 313 | ); 314 | bench_compare(c, "read-large-unicode-string", &json); 315 | bench_compare_string_reading(c, "read-large-unicode-string (string reading)", &json); 316 | } 317 | 318 | fn benchmark_escapes_string(c: &mut Criterion) { 319 | let json = format!( 320 | "\"{}\"", 321 | r#"a\nb\tc\\d\"e\u0000f\u0080g\u0800h\u1234i\uD800\uDC00"#.repeat(10_000) 322 | ); 323 | bench_compare(c, "read-large-escapes-string", &json); 324 | bench_compare_string_reading(c, "read-large-escapes-string (string reading)", &json); 325 | } 326 | 327 | criterion_group!( 328 | benches, 329 | // Benchmark functions 330 | benchmark_large_array, 331 | benchmark_nested_object, 332 | benchmark_nested_object_pretty, 333 | benchmark_large_ascii_string, 334 | benchmark_large_unicode_string, 335 | benchmark_escapes_string 336 | ); 337 | criterion_main!(benches); 338 | -------------------------------------------------------------------------------- /benches/reader_struct_benchmark.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use serde::Deserialize; 5 | use serde_json::{ 6 | de::{IoRead, Read, StrRead}, 7 | StreamDeserializer, 8 | }; 9 | use struson::reader::{JsonReader, JsonStreamReader, ReaderSettings}; 10 | 11 | #[derive(Deserialize)] 12 | #[allow(dead_code)] // suppress warnings for not read fields 13 | struct StructValue { 14 | bool_value: bool, 15 | integer: u32, 16 | float: f64, 17 | string: String, 18 | } 19 | 20 | fn benchmark_struct(c: &mut Criterion) { 21 | let mut group = c.benchmark_group("read-structs"); 22 | 23 | static COUNT: usize = 5000; 24 | let json = r#"{"bool_value": true, "integer": 123456, "float": 1234.56, "string": "this is a string value"}"#.repeat(COUNT); 25 | 26 | group.bench_with_input("struson", &json, |b, json| { 27 | b.iter(|| { 28 | let mut json_reader = JsonStreamReader::new_custom( 29 | json.as_bytes(), 30 | ReaderSettings { 31 | allow_multiple_top_level: true, 32 | ..Default::default() 33 | }, 34 | ); 35 | 36 | let mut count = 0; 37 | // Hopefully this is a fair comparison with how Serde behaves 38 | 39 | || -> Result<(), Box> { 40 | loop { 41 | let mut bool_value = None; 42 | let mut integer = None; 43 | let mut float = None; 44 | let mut string = None; 45 | 46 | json_reader.begin_object()?; 47 | while json_reader.has_next()? { 48 | match json_reader.next_name()? { 49 | "bool_value" => { 50 | bool_value = Some(json_reader.next_bool()?); 51 | } 52 | "integer" => { 53 | integer = Some(json_reader.next_number::()??); 54 | } 55 | "float" => { 56 | float = Some(json_reader.next_number::()??); 57 | } 58 | "string" => { 59 | string = Some(json_reader.next_string()?); 60 | } 61 | name => panic!("Unknown name '{name}'"), 62 | } 63 | } 64 | json_reader.end_object()?; 65 | // Discard created struct 66 | black_box(StructValue { 67 | bool_value: bool_value.unwrap(), 68 | integer: integer.unwrap(), 69 | float: float.unwrap(), 70 | string: string.unwrap(), 71 | }); 72 | count += 1; 73 | 74 | if !json_reader.has_next()? { 75 | break; 76 | } 77 | } 78 | assert_eq!(COUNT, count); 79 | 80 | Ok(()) 81 | }() 82 | .unwrap(); 83 | }) 84 | }); 85 | 86 | fn serde_read<'a, R: Read<'a>>(read: R) { 87 | let count = StreamDeserializer::::new(read) 88 | .map(Result::unwrap) 89 | .count(); 90 | assert_eq!(COUNT, count); 91 | } 92 | 93 | group.bench_with_input("serde (reader)", &json, |b, json| { 94 | b.iter(|| serde_read(IoRead::new(json.as_bytes()))) 95 | }); 96 | group.bench_with_input("serde (string)", &json, |b, json| { 97 | b.iter(|| serde_read(StrRead::new(json))) 98 | }); 99 | 100 | group.finish(); 101 | } 102 | 103 | criterion_group!( 104 | benches, 105 | // Benchmark functions 106 | benchmark_struct 107 | ); 108 | criterion_main!(benches); 109 | -------------------------------------------------------------------------------- /benches/serde_deserialize_benchmark.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 3 | use struson::{ 4 | reader::{JsonReader, JsonStreamReader, ReaderSettings}, 5 | serde::JsonReaderDeserializer, 6 | }; 7 | 8 | fn bench_compare(c: &mut Criterion, name: &str, json: &str) { 9 | let bytes = json.as_bytes(); 10 | let mut group = c.benchmark_group(name); 11 | group.bench_with_input("struson", bytes, |b, bytes| { 12 | b.iter(|| { 13 | let mut json_reader = JsonStreamReader::new_custom( 14 | bytes, 15 | ReaderSettings { 16 | // Disable path tracking for fairer comparison, because Serde JSON does not seem to track JSON path either 17 | track_path: false, 18 | ..Default::default() 19 | }, 20 | ); 21 | let mut deserializer = JsonReaderDeserializer::new(&mut json_reader); 22 | D::deserialize(&mut deserializer).unwrap(); 23 | json_reader.consume_trailing_whitespace().unwrap(); 24 | }); 25 | }); 26 | group.bench_with_input("serde-json", bytes, |b, bytes| { 27 | b.iter(|| { 28 | serde_json::from_reader::<_, D>(bytes).unwrap(); 29 | }); 30 | }); 31 | 32 | group.finish(); 33 | } 34 | 35 | fn benchmark_number_vec(c: &mut Criterion) { 36 | let value = (0..10) 37 | .map(|x| (0..10).map(|y| x * y).collect()) 38 | .collect::>>(); 39 | 40 | let json = serde_json::to_string(&value).unwrap(); 41 | bench_compare::>>(c, "serde-deserialize-number-vec", &json); 42 | } 43 | 44 | fn benchmark_structs(c: &mut Criterion) { 45 | #[derive(Serialize, Deserialize)] 46 | struct Nested { 47 | my_field: String, 48 | another_one: u32, 49 | } 50 | 51 | #[derive(Serialize, Deserialize)] 52 | enum Enum { 53 | VariantA, 54 | Other(u32), 55 | AndOneMore { value: bool }, 56 | } 57 | 58 | #[derive(Serialize, Deserialize)] 59 | struct Struct { 60 | name: String, 61 | some_value: u32, 62 | optional: Option, 63 | nested: Nested, 64 | enum_value: Enum, 65 | } 66 | 67 | let value = (0..30) 68 | .map(|i| Struct { 69 | name: format!("some name {i}"), 70 | some_value: i * 256, 71 | optional: if i % 5 == 0 { 72 | None 73 | } else { 74 | Some(i as f64 / 123.0) 75 | }, 76 | nested: Nested { 77 | my_field: "abcd".repeat((5 + i % 10) as usize), 78 | another_one: i * i, 79 | }, 80 | enum_value: match i % 3 { 81 | 0 => Enum::VariantA, 82 | 1 => Enum::Other(i * 2), 83 | _ => Enum::AndOneMore { value: i % 2 == 0 }, 84 | }, 85 | }) 86 | .collect::>(); 87 | 88 | let json = serde_json::to_string(&value).unwrap(); 89 | bench_compare::>(c, "serde-deserialize-structs", &json); 90 | } 91 | 92 | criterion_group!( 93 | benches, 94 | // Benchmark functions 95 | benchmark_number_vec, 96 | benchmark_structs 97 | ); 98 | criterion_main!(benches); 99 | -------------------------------------------------------------------------------- /benches/serde_serialize_benchmark.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use serde::Serialize; 3 | use struson::{ 4 | serde::JsonWriterSerializer, 5 | writer::{JsonStreamWriter, JsonWriter}, 6 | }; 7 | 8 | fn bench_compare(c: &mut Criterion, name: &str, value: S) { 9 | let mut group = c.benchmark_group(name); 10 | group.bench_with_input("struson", &value, |b, value| { 11 | b.iter(|| { 12 | let mut json_writer = JsonStreamWriter::new(std::io::sink()); 13 | let mut serializer = JsonWriterSerializer::new(&mut json_writer); 14 | value.serialize(&mut serializer).unwrap(); 15 | json_writer.finish_document().unwrap(); 16 | }); 17 | }); 18 | group.bench_with_input("serde-json", &value, |b, value| { 19 | b.iter(|| { 20 | serde_json::to_writer(std::io::sink(), value).unwrap(); 21 | }); 22 | }); 23 | 24 | group.finish(); 25 | } 26 | 27 | fn benchmark_number_vec(c: &mut Criterion) { 28 | let value = (0..10) 29 | .map(|x| (0..10).map(|y| x * y).collect()) 30 | .collect::>>(); 31 | 32 | bench_compare(c, "serde-serialize-number-vec", value); 33 | } 34 | 35 | fn benchmark_structs(c: &mut Criterion) { 36 | #[derive(Serialize)] 37 | struct Nested { 38 | my_field: String, 39 | another_one: u32, 40 | } 41 | 42 | #[derive(Serialize)] 43 | enum Enum { 44 | VariantA, 45 | Other(u32), 46 | AndOneMore { value: bool }, 47 | } 48 | 49 | #[derive(Serialize)] 50 | struct Struct { 51 | name: String, 52 | some_value: u32, 53 | optional: Option, 54 | nested: Nested, 55 | enum_value: Enum, 56 | } 57 | 58 | let value = (0..30) 59 | .map(|i| Struct { 60 | name: format!("some name {i}"), 61 | some_value: i * 256, 62 | optional: if i % 5 == 0 { 63 | None 64 | } else { 65 | Some(i as f64 / 123.0) 66 | }, 67 | nested: Nested { 68 | my_field: "abcd".repeat((5 + i % 10) as usize), 69 | another_one: i * i, 70 | }, 71 | enum_value: match i % 3 { 72 | 0 => Enum::VariantA, 73 | 1 => Enum::Other(i * 2), 74 | _ => Enum::AndOneMore { value: i % 2 == 0 }, 75 | }, 76 | }) 77 | .collect::>(); 78 | 79 | bench_compare(c, "serde-serialize-structs", value); 80 | } 81 | 82 | criterion_group!( 83 | benches, 84 | // Benchmark functions 85 | benchmark_number_vec, 86 | benchmark_structs 87 | ); 88 | criterion_main!(benches); 89 | -------------------------------------------------------------------------------- /benches/writer_benchmark.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, io::Sink}; 2 | 3 | use criterion::{criterion_group, criterion_main, Criterion}; 4 | use struson::writer::{JsonStreamWriter, JsonWriter, WriterSettings}; 5 | 6 | use serde::ser::Serializer; 7 | 8 | fn bench_compare) -> Result<(), Box>>( 9 | c: &mut Criterion, 10 | name: &str, 11 | struson_function: SF, 12 | ) { 13 | let mut group = c.benchmark_group(name); 14 | group.bench_with_input("struson-write", &struson_function, |b, write_function| { 15 | b.iter(|| { 16 | let mut json_writer = JsonStreamWriter::new(std::io::sink()); 17 | write_function(&mut json_writer).unwrap(); 18 | json_writer.finish_document().unwrap(); 19 | }) 20 | }); 21 | group.bench_with_input( 22 | "struson-write (pretty)", 23 | &struson_function, 24 | |b, write_function| { 25 | b.iter(|| { 26 | let mut json_writer = JsonStreamWriter::new_custom( 27 | std::io::sink(), 28 | WriterSettings { 29 | pretty_print: true, 30 | ..Default::default() 31 | }, 32 | ); 33 | write_function(&mut json_writer).unwrap(); 34 | json_writer.finish_document().unwrap(); 35 | }) 36 | }, 37 | ); 38 | 39 | // TODO: Maybe also try to support Serde, but Serializer API cannot be easily used for arbitrary data? 40 | // Could test against serde_json's Formatter, but that might be too low level (especially string value writing)? 41 | 42 | group.finish(); 43 | } 44 | 45 | fn benchmark_large_array(c: &mut Criterion) { 46 | bench_compare(c, "write-large-array", |json_writer| { 47 | json_writer.begin_array()?; 48 | 49 | for _ in 0..1000 { 50 | json_writer.bool_value(true)?; 51 | json_writer.number_value(123456)?; 52 | json_writer.fp_number_value(1234.56)?; 53 | json_writer.string_value("string value")?; 54 | } 55 | 56 | json_writer.end_array()?; 57 | 58 | Ok(()) 59 | }); 60 | } 61 | 62 | fn benchmark_nested_object(c: &mut Criterion) { 63 | bench_compare(c, "write-nested-object", |json_writer| { 64 | let count = 1000; 65 | 66 | for _ in 0..count { 67 | json_writer.begin_object()?; 68 | json_writer.name("member name")?; 69 | } 70 | 71 | json_writer.null_value()?; 72 | 73 | for _ in 0..count { 74 | json_writer.end_object()?; 75 | } 76 | 77 | Ok(()) 78 | }); 79 | } 80 | 81 | fn bench_compare_string_writing(c: &mut Criterion, name: &str, string_value: &str) { 82 | let mut group = c.benchmark_group(name); 83 | 84 | group.bench_with_input("struson", string_value, |b, string_value| { 85 | b.iter(|| { 86 | let mut json_writer = JsonStreamWriter::new(std::io::sink()); 87 | json_writer.string_value(string_value).unwrap(); 88 | json_writer.finish_document().unwrap(); 89 | }) 90 | }); 91 | 92 | group.bench_with_input("serde", string_value, |b, string_value| { 93 | b.iter(|| { 94 | let mut serializer = serde_json::ser::Serializer::new(std::io::sink()); 95 | serializer.serialize_str(string_value).unwrap(); 96 | }) 97 | }); 98 | 99 | group.finish(); 100 | } 101 | 102 | fn benchmark_large_ascii_string(c: &mut Criterion) { 103 | let string_value = "this is a test string".repeat(10_000); 104 | bench_compare(c, "write-large-ascii-string", |json_writer| { 105 | json_writer.string_value(&string_value)?; 106 | 107 | Ok(()) 108 | }); 109 | bench_compare_string_writing( 110 | c, 111 | "write-large-ascii-string (string writing)", 112 | &string_value, 113 | ); 114 | } 115 | 116 | fn benchmark_large_unicode_string(c: &mut Criterion) { 117 | let string_value = "ab\u{0080}cd\u{0800}ef\u{1234}gh\u{10FFFF}".repeat(10_000); 118 | bench_compare(c, "write-large-unicode-string", |json_writer| { 119 | json_writer.string_value(&string_value)?; 120 | 121 | Ok(()) 122 | }); 123 | bench_compare_string_writing( 124 | c, 125 | "write-large-unicode-string (string writing)", 126 | &string_value, 127 | ); 128 | } 129 | 130 | fn benchmark_escapes_string(c: &mut Criterion) { 131 | let string_value = r#"a\nb\tc\\d\"e\u0000f\u0080g\u0800h\u1234i\uD800\uDC00"#.repeat(10_000); 132 | bench_compare(c, "write-large-escapes-string", |json_writer| { 133 | json_writer.string_value(&string_value)?; 134 | 135 | Ok(()) 136 | }); 137 | bench_compare_string_writing( 138 | c, 139 | "write-large-escapes-string (string writing)", 140 | &string_value, 141 | ); 142 | } 143 | 144 | criterion_group!( 145 | benches, 146 | // Benchmark functions 147 | benchmark_large_array, 148 | benchmark_nested_object, 149 | benchmark_large_ascii_string, 150 | benchmark_large_unicode_string, 151 | benchmark_escapes_string 152 | ); 153 | criterion_main!(benches); 154 | -------------------------------------------------------------------------------- /benches/writer_struct_benchmark.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, io::Sink, iter}; 2 | 3 | use criterion::{criterion_group, criterion_main, Criterion}; 4 | use serde::Serialize; 5 | use struson::writer::{JsonStreamWriter, JsonWriter, WriterSettings}; 6 | 7 | #[derive(Serialize, Clone)] 8 | struct StructValue { 9 | bool_value: bool, 10 | integer: u32, 11 | float: f64, 12 | string: &'static str, 13 | } 14 | 15 | fn benchmark_struct(c: &mut Criterion) { 16 | let mut group = c.benchmark_group("write-structs"); 17 | let values: Vec = iter::repeat(StructValue { 18 | bool_value: true, 19 | integer: 123456, 20 | float: 123.4567, 21 | string: "a string value with some text", 22 | }) 23 | .take(10_000) 24 | .collect(); 25 | 26 | fn struson_write( 27 | mut json_writer: JsonStreamWriter, 28 | values: &Vec, 29 | ) -> Result<(), Box> { 30 | // Hopefully this is a fair comparison with how Serde behaves 31 | json_writer.begin_array()?; 32 | for value in values { 33 | json_writer.begin_object()?; 34 | 35 | json_writer.name("bool_value")?; 36 | json_writer.bool_value(value.bool_value)?; 37 | 38 | json_writer.name("integer")?; 39 | json_writer.number_value(value.integer)?; 40 | 41 | json_writer.name("float")?; 42 | json_writer.fp_number_value(value.float)?; 43 | 44 | json_writer.name("string")?; 45 | json_writer.string_value(value.string)?; 46 | 47 | json_writer.end_object()?; 48 | } 49 | json_writer.end_array()?; 50 | 51 | json_writer.finish_document()?; 52 | Ok(()) 53 | } 54 | 55 | group.bench_with_input("struson", &values, |b, values| { 56 | b.iter(|| { 57 | let json_writer = JsonStreamWriter::new(std::io::sink()); 58 | struson_write(json_writer, values).unwrap() 59 | }) 60 | }); 61 | group.bench_with_input("struson (pretty)", &values, |b, values| { 62 | b.iter(|| { 63 | let json_writer = JsonStreamWriter::new_custom( 64 | std::io::sink(), 65 | WriterSettings { 66 | pretty_print: true, 67 | ..Default::default() 68 | }, 69 | ); 70 | struson_write(json_writer, values).unwrap() 71 | }) 72 | }); 73 | 74 | group.bench_with_input("serde", &values, |b, values| { 75 | b.iter(|| { 76 | serde_json::to_writer(std::io::sink(), &values).unwrap(); 77 | }) 78 | }); 79 | group.bench_with_input("serde (pretty)", &values, |b, values| { 80 | b.iter(|| { 81 | serde_json::to_writer_pretty(std::io::sink(), &values).unwrap(); 82 | }) 83 | }); 84 | 85 | group.finish(); 86 | } 87 | 88 | criterion_group!( 89 | benches, 90 | // Benchmark functions 91 | benchmark_struct 92 | ); 93 | criterion_main!(benches); 94 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.87.0" 3 | profile = "minimal" 4 | components = ["clippy", "rustfmt", "rust-docs"] 5 | -------------------------------------------------------------------------------- /src/json_number.rs: -------------------------------------------------------------------------------- 1 | //! Internal module for parsing / validating JSON numbers 2 | 3 | pub(crate) trait NumberBytesProvider { 4 | /// Consumes the byte which is currently processed, and peeks at the next. 5 | /// 6 | /// Returns `None` if the end of the input has been reached. 7 | fn consume_current_peek_next(&mut self) -> Result, E>; 8 | } 9 | 10 | /// Returns `None` if the number is invalid and `Some(exponent_digits_count)` if 11 | /// the number is valid. The `exponent_digits_count` is the number of exponent 12 | /// digits, without sign and without leading 0s. 13 | pub(crate) fn consume_json_number>( 14 | reader: &mut R, 15 | first_byte: u8, 16 | ) -> Result, E> { 17 | let mut byte = first_byte; 18 | 19 | if byte == b'-' { 20 | if let Some(b) = reader.consume_current_peek_next()? { 21 | byte = b; 22 | } else { 23 | // Invalid number (missing integer part) 24 | return Ok(None); 25 | } 26 | } 27 | 28 | // Consume integer part, but treat 0 specially, because leading 0 before integer part is disallowed 29 | if matches!(byte, b'1'..=b'9') { 30 | loop { 31 | if let Some(b) = reader.consume_current_peek_next()? { 32 | byte = b; 33 | } else { 34 | // Valid number with 0 exponent digits 35 | return Ok(Some(0)); 36 | } 37 | 38 | if !byte.is_ascii_digit() { 39 | break; 40 | } 41 | } 42 | } else if byte == b'0' { 43 | if let Some(b) = reader.consume_current_peek_next()? { 44 | byte = b; 45 | } else { 46 | // Valid number with 0 exponent digits 47 | return Ok(Some(0)); 48 | } 49 | } else { 50 | // Invalid number (invalid integer part) 51 | return Ok(None); 52 | } 53 | 54 | // Fraction part 55 | if byte == b'.' { 56 | if let Some(b) = reader.consume_current_peek_next()? { 57 | byte = b; 58 | } else { 59 | // Invalid number (missing decimal part) 60 | return Ok(None); 61 | } 62 | 63 | if !byte.is_ascii_digit() { 64 | // Invalid number (invalid decimal part) 65 | return Ok(None); 66 | } 67 | 68 | loop { 69 | if let Some(b) = reader.consume_current_peek_next()? { 70 | byte = b; 71 | } else { 72 | // Valid number with 0 exponent digits 73 | return Ok(Some(0)); 74 | } 75 | 76 | if !byte.is_ascii_digit() { 77 | break; 78 | } 79 | } 80 | } 81 | 82 | // Exponent part 83 | let mut exponent_digits_count = 0; 84 | if matches!(byte, b'e' | b'E') { 85 | if let Some(b) = reader.consume_current_peek_next()? { 86 | byte = b; 87 | } else { 88 | // Invalid number (missing exponent number) 89 | return Ok(None); 90 | } 91 | 92 | if matches!(byte, b'-' | b'+') { 93 | if let Some(b) = reader.consume_current_peek_next()? { 94 | byte = b; 95 | } else { 96 | // Invalid number (missing exponent number) 97 | return Ok(None); 98 | } 99 | } 100 | 101 | // Check for '1'..='9' to ignore leading 0s for exponent digits count 102 | if matches!(byte, b'1'..=b'9') { 103 | exponent_digits_count += 1; 104 | } else if byte != b'0' { 105 | // Invalid number (invalid exponent number) 106 | return Ok(None); 107 | } 108 | 109 | loop { 110 | if let Some(b) = reader.consume_current_peek_next()? { 111 | byte = b; 112 | } else { 113 | // Valid number 114 | return Ok(Some(exponent_digits_count)); 115 | } 116 | 117 | if byte.is_ascii_digit() { 118 | // Ignore leading 0s for exponent digits count 119 | if byte != b'0' || exponent_digits_count > 0 { 120 | exponent_digits_count += 1; 121 | } 122 | } else { 123 | break; 124 | } 125 | } 126 | } 127 | 128 | if byte.is_ascii_digit() || matches!(byte, b'-' | b'+' | b'.' | b'e' | b'E') { 129 | // If character after number (which is not part of number) is a number char, treat it as invalid 130 | // For example `01`, `1.2.3` or `1-` 131 | Ok(None) 132 | } else { 133 | // Valid number 134 | Ok(Some(exponent_digits_count)) 135 | } 136 | } 137 | 138 | struct BytesSliceNumberBytesProvider<'a> { 139 | bytes: &'a [u8], 140 | index: usize, 141 | } 142 | impl NumberBytesProvider<()> for BytesSliceNumberBytesProvider<'_> { 143 | fn consume_current_peek_next(&mut self) -> Result, ()> { 144 | self.index += 1; 145 | if self.index < self.bytes.len() { 146 | Ok(Some(self.bytes[self.index])) 147 | } else { 148 | Ok(None) 149 | } 150 | } 151 | } 152 | 153 | pub(crate) fn is_valid_json_number(value: &str) -> bool { 154 | if value.is_empty() { 155 | return false; 156 | } 157 | 158 | let value_bytes = value.as_bytes(); 159 | let mut bytes_provider = BytesSliceNumberBytesProvider { 160 | bytes: value_bytes, 161 | index: 0, 162 | }; 163 | let is_valid = consume_json_number(&mut bytes_provider, value_bytes[0]) 164 | .unwrap() 165 | // Just check that number is valid, ignore exponent digits count 166 | .is_some(); 167 | 168 | // Is valid and complete string was consumed 169 | is_valid && bytes_provider.index >= value_bytes.len() 170 | } 171 | 172 | #[cfg(test)] 173 | mod tests { 174 | use super::*; 175 | 176 | #[test] 177 | fn number_validation() { 178 | assert!(is_valid_json_number("0")); 179 | assert!(is_valid_json_number("-0")); 180 | assert!(is_valid_json_number("1230.1")); 181 | assert!(is_valid_json_number("1.01e1")); 182 | assert!(is_valid_json_number("12.120e+01")); 183 | assert!(is_valid_json_number("12.120e-10")); 184 | 185 | assert_eq!(false, is_valid_json_number("00")); 186 | assert_eq!(false, is_valid_json_number("-00")); 187 | assert_eq!(false, is_valid_json_number("+1")); 188 | assert_eq!(false, is_valid_json_number(".1")); 189 | assert_eq!(false, is_valid_json_number("1.-1")); 190 | assert_eq!(false, is_valid_json_number("1e")); 191 | assert_eq!(false, is_valid_json_number("1e+-1")); 192 | assert_eq!(false, is_valid_json_number("1e.1")); 193 | 194 | assert_eq!(false, is_valid_json_number("")); 195 | assert_eq!(false, is_valid_json_number("1a")); 196 | assert_eq!(false, is_valid_json_number("NaN")); 197 | assert_eq!(false, is_valid_json_number("nan")); 198 | assert_eq!(false, is_valid_json_number("NAN")); 199 | assert_eq!(false, is_valid_json_number(&f32::NAN.to_string())); 200 | assert_eq!(false, is_valid_json_number("Infinity")); 201 | assert_eq!(false, is_valid_json_number("-Infinity")); 202 | assert_eq!(false, is_valid_json_number(&f32::INFINITY.to_string())); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | // Enable 'unused' warnings for doc tests (are disabled by default) 3 | #![doc(test(no_crate_inject))] 4 | #![doc(test(attr(warn(unused))))] 5 | // Fail on warnings in doc tests 6 | #![doc(test(attr(deny(warnings))))] 7 | // When `docsrs` configuration flag is set enable banner for features in documentation 8 | // See https://stackoverflow.com/q/61417452 9 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 10 | 11 | //! Struson is an [RFC 8259](https://www.rfc-editor.org/rfc/rfc8259.html) compliant streaming JSON reader and writer. 12 | //! 13 | //! Its main purpose is allowing to read and write JSON data in a memory efficient way without having to store the 14 | //! complete JSON document structure in memory. It is however *not* an object mapper which converts structs 15 | //! to JSON and vice versa, a dedicated library such as [Serde](https://github.com/serde-rs/json) should be 16 | //! used for that. 17 | //! 18 | //! The API of Struson was inspired by the streaming API of the Java library [Gson](https://github.com/google/gson). 19 | //! It is rather low-level and corresponds to the elements of a JSON document, with little 20 | //! abstraction on top of it, allowing to read and write any valid JSON document regardless 21 | //! of its structure or content. 22 | //! 23 | //! # Terminology 24 | //! 25 | //! This crate uses the same terminology as the JSON specification: 26 | //! 27 | //! - *object*: `{ ... }` 28 | //! - *member*: Entry in an object. For example the JSON object `{"a": 1}` has the member 29 | //! `"a": 1` where `"a"` is the member *name* and `1` is the member *value*. 30 | //! - *array*: `[ ... ]` 31 | //! - *literal*: 32 | //! - *boolean*: `true` or `false` 33 | //! - `null` 34 | //! - *number*: number value, for example `123.4e+10` 35 | //! - *string*: string value, for example `"text in \"quotes\""` 36 | //! 37 | //! # Usage examples 38 | //! 39 | //! Two variants of the API are provided: 40 | //! - simple: ensures correct API usage at compile-time 41 | //! - advanced: ensures correct API usage only at runtime (by panicking); more flexible and 42 | //! provides more functionality 43 | //! 44 | //! ## Simple API 45 | //! 46 | //! **🔬 Experimental**\ 47 | //! The simple API and its naming is currently experimental, please provide feedback [here](https://github.com/Marcono1234/struson/issues/34). 48 | //! It has to be enabled by specifying the `simple-api` feature in `Cargo.toml`: 49 | //! ```toml 50 | //! [dependencies] 51 | //! struson = { version = "...", features = ["simple-api"] } 52 | //! ``` 53 | //! Any feedback is appreciated! 54 | //! 55 | //! ### Reading 56 | //! See [`SimpleJsonReader`](crate::reader::simple::SimpleJsonReader). 57 | //! 58 | //! ``` 59 | //! # #[cfg(feature = "simple-api")] 60 | //! # { 61 | //! use struson::reader::simple::*; 62 | //! 63 | //! // In this example JSON data comes from a string; 64 | //! // normally it would come from a file or a network connection 65 | //! let json_reader = SimpleJsonReader::new(r#"["a", "short", "example"]"#.as_bytes()); 66 | //! let mut words = Vec::::new(); 67 | //! json_reader.read_array_items(|item_reader| { 68 | //! let word = item_reader.read_string()?; 69 | //! words.push(word); 70 | //! Ok(()) 71 | //! })?; 72 | //! assert_eq!(words, vec!["a", "short", "example"]); 73 | //! # } 74 | //! # Ok::<(), Box>(()) 75 | //! ``` 76 | //! 77 | //! For reading nested values, the methods [`read_seeked`](crate::reader::simple::ValueReader::read_seeked) 78 | //! and [`read_seeked_multi`](crate::reader::simple::ValueReader::read_seeked_multi) can be used: 79 | //! ``` 80 | //! # #[cfg(feature = "simple-api")] 81 | //! # { 82 | //! use struson::reader::simple::*; 83 | //! use struson::reader::simple::multi_json_path::multi_json_path; 84 | //! 85 | //! // In this example JSON data comes from a string; 86 | //! // normally it would come from a file or a network connection 87 | //! let json = r#"{ 88 | //! "users": [ 89 | //! {"name": "John", "age": 32}, 90 | //! {"name": "Jane", "age": 41} 91 | //! ] 92 | //! }"#; 93 | //! let json_reader = SimpleJsonReader::new(json.as_bytes()); 94 | //! 95 | //! let mut ages = Vec::::new(); 96 | //! // Select the ages of all users 97 | //! let json_path = multi_json_path!["users", [*], "age"]; 98 | //! json_reader.read_seeked_multi(&json_path, false, |value_reader| { 99 | //! let age = value_reader.read_number()??; 100 | //! ages.push(age); 101 | //! Ok(()) 102 | //! })?; 103 | //! assert_eq!(ages, vec![32, 41]); 104 | //! # } 105 | //! # Ok::<(), Box>(()) 106 | //! ``` 107 | //! 108 | //! ### Writing 109 | //! See [`SimpleJsonWriter`](crate::writer::simple::SimpleJsonWriter). 110 | //! 111 | //! ``` 112 | //! # #[cfg(feature = "simple-api")] 113 | //! # { 114 | //! use struson::writer::simple::*; 115 | //! 116 | //! // In this example JSON bytes are stored in a Vec; 117 | //! // normally they would be written to a file or network connection 118 | //! let mut writer = Vec::::new(); 119 | //! let json_writer = SimpleJsonWriter::new(&mut writer); 120 | //! json_writer.write_object(|object_writer| { 121 | //! object_writer.write_number_member("a", 1)?; 122 | //! object_writer.write_bool_member("b", true)?; 123 | //! Ok(()) 124 | //! })?; 125 | //! 126 | //! let json = String::from_utf8(writer)?; 127 | //! assert_eq!(json, r#"{"a":1,"b":true}"#); 128 | //! # } 129 | //! # Ok::<(), Box>(()) 130 | //! ``` 131 | //! 132 | //! ## Advanced API 133 | //! 134 | //! ### Reading 135 | //! See [`JsonStreamReader`](crate::reader::JsonStreamReader). 136 | //! 137 | //! ``` 138 | //! use struson::reader::*; 139 | //! 140 | //! // In this example JSON data comes from a string; 141 | //! // normally it would come from a file or a network connection 142 | //! let json = r#"{"a": [1, true]}"#; 143 | //! let mut json_reader = JsonStreamReader::new(json.as_bytes()); 144 | //! 145 | //! json_reader.begin_object()?; 146 | //! assert_eq!(json_reader.next_name()?, "a"); 147 | //! 148 | //! json_reader.begin_array()?; 149 | //! assert_eq!(json_reader.next_number::()??, 1); 150 | //! assert_eq!(json_reader.next_bool()?, true); 151 | //! json_reader.end_array()?; 152 | //! 153 | //! json_reader.end_object()?; 154 | //! // Ensures that there is no trailing data 155 | //! json_reader.consume_trailing_whitespace()?; 156 | //! # Ok::<(), Box>(()) 157 | //! ``` 158 | //! 159 | //! ### Writing 160 | //! See [`JsonStreamWriter`](crate::writer::JsonStreamWriter). 161 | //! 162 | //! ``` 163 | //! use struson::writer::*; 164 | //! 165 | //! // In this example JSON bytes are stored in a Vec; 166 | //! // normally they would be written to a file or network connection 167 | //! let mut writer = Vec::::new(); 168 | //! let mut json_writer = JsonStreamWriter::new(&mut writer); 169 | //! 170 | //! json_writer.begin_object()?; 171 | //! json_writer.name("a")?; 172 | //! 173 | //! json_writer.begin_array()?; 174 | //! json_writer.number_value(1)?; 175 | //! json_writer.bool_value(true)?; 176 | //! json_writer.end_array()?; 177 | //! 178 | //! json_writer.end_object()?; 179 | //! // Ensures that the JSON document is complete and flushes the buffer 180 | //! json_writer.finish_document()?; 181 | //! 182 | //! let json = String::from_utf8(writer)?; 183 | //! assert_eq!(json, r#"{"a":[1,true]}"#); 184 | //! # Ok::<(), Box>(()) 185 | //! ``` 186 | //! 187 | //! # Serde integration 188 | //! Optional integration with [Serde](https://docs.rs/serde/latest/serde/) exists to 189 | //! allow writing a `Serialize` to a `JsonWriter` and reading a `Deserialize` from 190 | //! a `JsonReader`. See the [`serde` module](crate::serde) of this crate for more information. 191 | 192 | pub mod reader; 193 | pub mod writer; 194 | 195 | #[cfg(feature = "serde")] 196 | pub mod serde; 197 | 198 | mod json_number; 199 | mod utf8; 200 | -------------------------------------------------------------------------------- /src/serde/mod.rs: -------------------------------------------------------------------------------- 1 | //! Provides integration with [Serde](https://docs.rs/serde/latest/serde/) 2 | //! 3 | //! This module provides optional integration with Serde by allowing [`Serialize`](serde::ser::Serialize) 4 | //! types to be written to a [`JsonWriter`](crate::writer::JsonWriter) and [`Deserialize`](serde::de::Deserialize) 5 | //! types to be read from a [`JsonReader`](crate::reader::JsonReader). It is intended for use cases 6 | //! where code is using a `JsonWriter` or `JsonReader` in the first place, to provide convenience methods 7 | //! to directly write or read a `Serialize` or `Deserialize` in the middle of the JSON document. 8 | //! For compatibility this module tries to match Serde JSON's behavior, but there might be small 9 | //! differences. 10 | //! 11 | //! This module is _not_ intended as replacement for [Serde JSON](https://docs.rs/serde_json/latest/serde_json/index.html). 12 | //! That crate provides greater functionality when working with JSON in the context of Serde, and likely 13 | //! also offers better performance. 14 | //! 15 | //! To enable this optional integration, specify the `serde` feature in your `Cargo.toml` file 16 | //! for the dependency on this crate: 17 | //! ```toml 18 | //! [dependencies] 19 | //! struson = { version = "...", features = ["serde"] } 20 | //! ``` 21 | //! 22 | //! The most convenient way to use the Serde integration is by using [`JsonWriter::serialize_value`](crate::writer::JsonWriter::serialize_value) 23 | //! and [`JsonReader::deserialize_next`](crate::reader::JsonReader::deserialize_next).\ 24 | //! Alternatively [`JsonWriterSerializer`] and [`JsonReaderDeserializer`] 25 | //! can be used directly, but that is rarely necessary. 26 | //! 27 | //! # Usage examples 28 | //! 29 | //! ## Serialization 30 | //! ``` 31 | //! # use struson::writer::*; 32 | //! # use serde::*; 33 | //! // In this example JSON bytes are stored in a Vec; 34 | //! // normally they would be written to a file or network connection 35 | //! let mut writer = Vec::::new(); 36 | //! let mut json_writer = JsonStreamWriter::new(&mut writer); 37 | //! 38 | //! // Start writing the enclosing data using the regular JsonWriter methods 39 | //! json_writer.begin_object()?; 40 | //! json_writer.name("outer")?; 41 | //! 42 | //! #[derive(Serialize)] 43 | //! struct MyStruct { 44 | //! text: String, 45 | //! number: u64, 46 | //! } 47 | //! 48 | //! let value = MyStruct { 49 | //! text: "some text".to_owned(), 50 | //! number: 5, 51 | //! }; 52 | //! // Serialize the value as next JSON value 53 | //! json_writer.serialize_value(&value)?; 54 | //! 55 | //! // Write the remainder of the enclosing data 56 | //! json_writer.end_object()?; 57 | //! 58 | //! // Ensures that the JSON document is complete and flushes the buffer 59 | //! json_writer.finish_document()?; 60 | //! 61 | //! let json = String::from_utf8(writer)?; 62 | //! assert_eq!( 63 | //! json, 64 | //! r#"{"outer":{"text":"some text","number":5}}"# 65 | //! ); 66 | //! # Ok::<(), Box>(()) 67 | //! ``` 68 | //! 69 | //! # Deserialization 70 | //! ``` 71 | //! # use struson::reader::*; 72 | //! # use struson::reader::json_path::*; 73 | //! # use serde::*; 74 | //! // In this example JSON data comes from a string; 75 | //! // normally it would come from a file or a network connection 76 | //! let json = r#"{"outer": {"text": "some text", "number": 5}}"#; 77 | //! let mut json_reader = JsonStreamReader::new(json.as_bytes()); 78 | //! 79 | //! // Skip outer data using the regular JsonReader methods 80 | //! json_reader.seek_to(&json_path!["outer"])?; 81 | //! 82 | //! #[derive(Deserialize, PartialEq, Debug)] 83 | //! struct MyStruct { 84 | //! text: String, 85 | //! number: u64, 86 | //! } 87 | //! 88 | //! let value: MyStruct = json_reader.deserialize_next()?; 89 | //! 90 | //! // Skip the remainder of the JSON document 91 | //! json_reader.skip_to_top_level()?; 92 | //! 93 | //! // Ensures that there is no trailing data 94 | //! json_reader.consume_trailing_whitespace()?; 95 | //! 96 | //! assert_eq!( 97 | //! value, 98 | //! MyStruct { text: "some text".to_owned(), number: 5 } 99 | //! ); 100 | //! # Ok::<(), Box>(()) 101 | //! ``` 102 | 103 | // Re-export everything directly under `serde`; mainly because the sub-modules do not 104 | // contain many structs and enums and to flatten documentation hierarchy 105 | mod de; 106 | pub use de::*; 107 | mod ser; 108 | pub use ser::*; 109 | -------------------------------------------------------------------------------- /src/utf8.rs: -------------------------------------------------------------------------------- 1 | //! Utility module for UTF-8 data handling 2 | 3 | /// Maximum number of UTF-8 bytes needed to encode one Unicode `char` 4 | pub(crate) const MAX_BYTES_PER_CHAR: usize = 4; 5 | 6 | /// Whether the byte is a 1 byte UTF-8 encoded char; that means the byte itself represents an ASCII character 7 | pub(crate) fn is_1byte(b: u8) -> bool { 8 | b <= 0x7F 9 | } 10 | 11 | const _2BYTE_MASK: u8 = 0b1110_0000; 12 | /// Bit mask which matches the value bits of the 2 byte start 13 | const _2BYTE_MASK_VAL: u8 = !_2BYTE_MASK; 14 | 15 | /// Whether the byte is the start of a 2 byte UTF-8 encoded char 16 | pub(crate) fn is_2byte_start(b: u8) -> bool { 17 | // 110x_xxxx 18 | (b & _2BYTE_MASK) == 0b1100_0000 19 | } 20 | 21 | const _3BYTE_MASK: u8 = 0b1111_0000; 22 | /// Bit mask which matches the value bits of the 3 byte start 23 | const _3BYTE_MASK_VAL: u8 = !_3BYTE_MASK; 24 | 25 | /// Whether the byte is the start of a 3 byte UTF-8 encoded char 26 | pub(crate) fn is_3byte_start(b: u8) -> bool { 27 | // 1110_xxxx 28 | (b & _3BYTE_MASK) == 0b1110_0000 29 | } 30 | 31 | const _4BYTE_MASK: u8 = 0b1111_1000; 32 | /// Bit mask which matches the value bits of the 4 byte start 33 | const _4BYTE_MASK_VAL: u8 = !_4BYTE_MASK; 34 | 35 | /// Whether the byte is the start of a 4 byte UTF-8 encoded char 36 | pub(crate) fn is_4byte_start(b: u8) -> bool { 37 | // 1111_0xxx 38 | (b & _4BYTE_MASK) == 0b1111_0000 39 | } 40 | 41 | const CONT_MASK: u8 = 0b1100_0000; 42 | /// Bit mask which matches the value bits of the continuation byte 43 | const CONT_MASK_VAL: u8 = !CONT_MASK; 44 | 45 | /// Whether the byte is a continuation byte of a char which is UTF-8 encoded as 2, 3 or 4 bytes; 46 | /// that means it is not the first byte for those multi-byte UTF-8 chars 47 | pub(crate) fn is_continuation(b: u8) -> bool { 48 | // 10xx_xxxx 49 | (b & CONT_MASK) == 0b1000_0000 50 | } 51 | 52 | /// Whether the 2 bytes UTF-8 encoding is valid 53 | pub(crate) fn is_valid_2bytes(b0: u8, b1: u8) -> bool { 54 | // caller should have verified this 55 | debug_assert!(is_2byte_start(b0) && is_continuation(b1)); 56 | let code_point = (u32::from(b0 & _2BYTE_MASK_VAL) << 6) | u32::from(b1 & CONT_MASK_VAL); 57 | // Verify no 'overlong encoding' of lower code points 58 | code_point >= 0x80 59 | } 60 | 61 | /// Whether the 3 bytes UTF-8 encoding is valid 62 | pub(crate) fn is_valid_3bytes(b0: u8, b1: u8, b2: u8) -> bool { 63 | // caller should have verified this 64 | debug_assert!(is_3byte_start(b0) && is_continuation(b1) && is_continuation(b2)); 65 | let code_point = (u32::from(b0 & _3BYTE_MASK_VAL) << 12) 66 | | (u32::from(b1 & CONT_MASK_VAL) << 6) 67 | | u32::from(b2 & CONT_MASK_VAL); 68 | // Verify no 'overlong encoding' of lower code points, and no UTF-16 surrogate chars encoded in UTF-8 69 | code_point >= 0x800 && !matches!(code_point, 0xD800..=0xDFFF) 70 | } 71 | 72 | /// Whether the 4 bytes UTF-8 encoding is valid 73 | pub(crate) fn is_valid_4bytes(b0: u8, b1: u8, b2: u8, b3: u8) -> bool { 74 | // caller should have verified this 75 | debug_assert!( 76 | is_4byte_start(b0) && is_continuation(b1) && is_continuation(b2) && is_continuation(b3) 77 | ); 78 | let code_point = (u32::from(b0 & _4BYTE_MASK_VAL) << 18) 79 | | (u32::from(b1 & CONT_MASK_VAL) << 12) 80 | | (u32::from(b2 & CONT_MASK_VAL) << 6) 81 | | u32::from(b3 & CONT_MASK_VAL); 82 | 83 | // Verify no 'overlong encoding' of lower code points, and no invalid code point > U+10FFFF 84 | matches!(code_point, 0x10000..=0x10FFFF) 85 | } 86 | 87 | fn debug_assert_valid_utf8(bytes: &[u8]) { 88 | if cfg!(debug_assertions) { 89 | if let Err(e) = std::str::from_utf8(bytes) { 90 | panic!("Unexpected: Invalid UTF-8 bytes detected, report this to the Struson maintainers: {e:?}; bytes: {bytes:02X?}") 91 | } 92 | } 93 | } 94 | 95 | /// Converts bytes to a `str`, possibly without validating that the bytes are valid UTF-8 data 96 | /// 97 | /// Must only be called if UTF-8 validation on the bytes has already been performed manually. 98 | pub(crate) fn to_str_unchecked(bytes: &[u8]) -> &str { 99 | debug_assert_valid_utf8(bytes); 100 | // TODO: Once confident enough that UTF-8 validation in this crate is correct, use `std::str::from_utf8_unchecked` instead 101 | std::str::from_utf8(bytes).unwrap() 102 | } 103 | 104 | /// Converts bytes to a `String`, possibly without validating that the bytes are valid UTF-8 data 105 | /// 106 | /// Must only be called if UTF-8 validation on the bytes has already been performed manually. 107 | pub(crate) fn to_string_unchecked(bytes: Vec) -> String { 108 | debug_assert_valid_utf8(&bytes); 109 | // TODO: Once confident enough that UTF-8 validation in this crate is correct, use `String::from_utf8_unchecked` instead 110 | String::from_utf8(bytes).unwrap() 111 | } 112 | 113 | #[cfg(test)] 114 | mod tests { 115 | use super::*; 116 | use std::panic::UnwindSafe; 117 | 118 | #[must_use] // caller must perform assertion on panic message 119 | fn assert_panics(f: impl FnOnce() -> R + UnwindSafe) -> String { 120 | if let Err(panic_value) = std::panic::catch_unwind(f) { 121 | match panic_value.downcast::() { 122 | Ok(message) => *message, 123 | Err(panic_value) => { 124 | panic!("Panic value should have been a String, but is: {panic_value:?}") 125 | } 126 | } 127 | } else { 128 | panic!("Expression should have panicked"); 129 | } 130 | } 131 | 132 | #[cfg(debug_assertions)] // validation is only performed when debug assertions are enabled 133 | #[test] 134 | fn to_str_unchecked_invalid() { 135 | // Overlong UTF-8 encoding for two bytes 136 | let message = assert_panics(|| to_str_unchecked(b"\xC1\xBF")); 137 | // Check prefix and suffix but ignore the error message from Rust in the middle 138 | assert!( 139 | message.starts_with("Unexpected: Invalid UTF-8 bytes detected, report this to the Struson maintainers: "), 140 | "Unexpected prefix for message: {message}" 141 | ); 142 | assert!( 143 | message.ends_with("; bytes: [C1, BF]"), 144 | "Unexpected suffix for message: {message}" 145 | ); 146 | } 147 | 148 | #[cfg(debug_assertions)] // validation is only performed when debug assertions are enabled 149 | #[test] 150 | fn to_string_unchecked_invalid() { 151 | // Overlong UTF-8 encoding for two bytes 152 | let message = assert_panics(|| to_string_unchecked(b"\xC1\xBF".to_vec())); 153 | // Check prefix and suffix but ignore the error message from Rust in the middle 154 | assert!(message.starts_with( 155 | "Unexpected: Invalid UTF-8 bytes detected, report this to the Struson maintainers: "), 156 | "Unexpected prefix for message: {message}" 157 | ); 158 | assert!( 159 | message.ends_with("; bytes: [C1, BF]"), 160 | "Unexpected suffix for message: {message}" 161 | ); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /tests/custom_json_writer.rs: -------------------------------------------------------------------------------- 1 | //! Integration test for a custom `JsonWriter` implementation 2 | //! 3 | //! The `JsonWriter` implementation here is built on top of serde_json's `Value`. 4 | //! This ensures that the `JsonWriter` trait can be implemented by users, and does 5 | //! not depend on something which is only accessible within Struson. 6 | //! 7 | //! **Important:** This code is only for integration test and demonstration purposes; 8 | //! it is not intended to be used in production code. 9 | 10 | use custom_writer::JsonValueWriter; 11 | use serde_json::json; 12 | use std::io::Write; 13 | use struson::{ 14 | reader::{JsonReader, JsonStreamReader}, 15 | writer::{JsonNumberError, JsonWriter, StringValueWriter}, 16 | }; 17 | 18 | mod custom_writer { 19 | use serde_json::{Map, Number, Value}; 20 | use std::io::{ErrorKind, Write}; 21 | use struson::writer::{ 22 | FiniteNumber, FloatingPointNumber, JsonNumberError, JsonWriter, StringValueWriter, 23 | }; 24 | 25 | type IoError = std::io::Error; 26 | 27 | enum StackValue { 28 | Array(Vec), 29 | Object(Map), 30 | } 31 | 32 | pub struct JsonValueWriter { 33 | stack: Vec, 34 | pending_name: Option, 35 | is_string_value_writer_active: bool, 36 | /// Holds the final value until `finish_document` is called 37 | final_value: Option, 38 | } 39 | impl JsonValueWriter { 40 | pub fn new() -> Self { 41 | JsonValueWriter { 42 | stack: Vec::new(), 43 | pending_name: None, 44 | is_string_value_writer_active: false, 45 | final_value: None, 46 | } 47 | } 48 | } 49 | 50 | impl JsonValueWriter { 51 | fn verify_string_writer_inactive(&self) { 52 | if self.is_string_value_writer_active { 53 | panic!("Incorrect writer usage: String value writer is active"); 54 | } 55 | } 56 | 57 | fn check_before_value(&self) { 58 | self.verify_string_writer_inactive(); 59 | if self.final_value.is_some() { 60 | panic!("Incorrect writer usage: Top-level value has already been written") 61 | } 62 | if let Some(StackValue::Object(_)) = self.stack.last() { 63 | if self.pending_name.is_none() { 64 | panic!("Incorrect writer usage: Member name is expected"); 65 | } 66 | } 67 | } 68 | 69 | fn add_value(&mut self, value: Value) { 70 | if let Some(stack_value) = self.stack.last_mut() { 71 | match stack_value { 72 | StackValue::Array(array) => array.push(value), 73 | StackValue::Object(object) => { 74 | object.insert(self.pending_name.take().unwrap(), value); 75 | } 76 | }; 77 | } else { 78 | debug_assert!( 79 | self.final_value.is_none(), 80 | "caller should have verified that final value is not set yet" 81 | ); 82 | self.final_value = Some(value); 83 | } 84 | } 85 | } 86 | 87 | fn serde_number_from_f64(f: f64) -> Result { 88 | Number::from_f64(f) 89 | .ok_or_else(|| JsonNumberError::InvalidNumber(format!("non-finite number: {f}"))) 90 | } 91 | 92 | impl JsonWriter for JsonValueWriter { 93 | type WriterResult = Value; 94 | 95 | fn begin_object(&mut self) -> Result<(), IoError> { 96 | self.check_before_value(); 97 | self.stack.push(StackValue::Object(Map::new())); 98 | Ok(()) 99 | } 100 | 101 | fn end_object(&mut self) -> Result<(), IoError> { 102 | self.verify_string_writer_inactive(); 103 | if let Some(StackValue::Object(map)) = self.stack.pop() { 104 | self.add_value(Value::Object(map)); 105 | Ok(()) 106 | } else { 107 | panic!("Incorrect writer usage: Cannot end object; not inside object"); 108 | } 109 | } 110 | 111 | fn begin_array(&mut self) -> Result<(), IoError> { 112 | self.check_before_value(); 113 | self.stack.push(StackValue::Array(Vec::new())); 114 | Ok(()) 115 | } 116 | 117 | fn end_array(&mut self) -> Result<(), IoError> { 118 | self.verify_string_writer_inactive(); 119 | if let Some(StackValue::Array(vec)) = self.stack.pop() { 120 | self.add_value(Value::Array(vec)); 121 | Ok(()) 122 | } else { 123 | panic!("Incorrect writer usage: Cannot end array; not inside array"); 124 | } 125 | } 126 | 127 | fn name(&mut self, name: &str) -> Result<(), IoError> { 128 | self.verify_string_writer_inactive(); 129 | if let Some(StackValue::Object(_)) = self.stack.last() { 130 | if self.pending_name.is_some() { 131 | panic!("Incorrect writer usage: Member name has already been written; expecting value"); 132 | } 133 | self.pending_name = Some(name.to_owned()); 134 | Ok(()) 135 | } else { 136 | panic!("Incorrect writer usage: Cannot write name; not inside object"); 137 | } 138 | } 139 | 140 | fn null_value(&mut self) -> Result<(), IoError> { 141 | self.check_before_value(); 142 | self.add_value(Value::Null); 143 | Ok(()) 144 | } 145 | 146 | fn bool_value(&mut self, value: bool) -> Result<(), IoError> { 147 | self.check_before_value(); 148 | self.add_value(Value::Bool(value)); 149 | Ok(()) 150 | } 151 | 152 | fn string_value(&mut self, value: &str) -> Result<(), IoError> { 153 | self.check_before_value(); 154 | self.add_value(Value::String(value.to_owned())); 155 | Ok(()) 156 | } 157 | 158 | fn string_value_writer(&mut self) -> Result { 159 | self.check_before_value(); 160 | self.is_string_value_writer_active = true; 161 | Ok(StringValueWriterImpl { 162 | buf: Vec::new(), 163 | json_writer: self, 164 | }) 165 | } 166 | 167 | fn number_value_from_string(&mut self, value: &str) -> Result<(), JsonNumberError> { 168 | self.check_before_value(); 169 | // TODO: `parse::` might not match JSON number string format (might allow more / less than allowed by JSON)? 170 | let f = value 171 | .parse::() 172 | .map_err(|e| JsonNumberError::InvalidNumber(e.to_string()))?; 173 | self.add_value(Value::Number(serde_number_from_f64(f)?)); 174 | Ok(()) 175 | } 176 | 177 | fn number_value(&mut self, value: N) -> Result<(), IoError> { 178 | let number = value 179 | .as_u64() 180 | .map(Number::from) 181 | .or_else(|| value.as_i64().map(Number::from)); 182 | 183 | if let Some(n) = number { 184 | self.check_before_value(); 185 | self.add_value(Value::Number(n)); 186 | Ok(()) 187 | } else { 188 | value.use_json_number(|number_str| { 189 | self.number_value_from_string(number_str) 190 | .map_err(|e| match e { 191 | JsonNumberError::InvalidNumber(e) => { 192 | panic!( 193 | "Unexpected: Writer rejected finite number '{number_str}': {e}" 194 | ) 195 | } 196 | JsonNumberError::IoError(e) => IoError::other(e), 197 | }) 198 | }) 199 | } 200 | } 201 | 202 | fn fp_number_value( 203 | &mut self, 204 | value: N, 205 | ) -> Result<(), JsonNumberError> { 206 | let number = if let Some(n) = value.as_f64() { 207 | Some(serde_number_from_f64(n)?) 208 | } else { 209 | None 210 | }; 211 | 212 | if let Some(n) = number { 213 | self.check_before_value(); 214 | self.add_value(Value::Number(n)); 215 | Ok(()) 216 | } else { 217 | // TODO: Cannot match over possible implementations? Therefore have to use string representation 218 | value.use_json_number(|number_str| { 219 | self.number_value_from_string(number_str).map_err(|e| { 220 | match e { 221 | // `use_json_number` should have verified that value is valid finite JSON number 222 | JsonNumberError::InvalidNumber(e) => { 223 | panic!( 224 | "Unexpected: Writer rejected finite number '{number_str}': {e}" 225 | ) 226 | } 227 | JsonNumberError::IoError(e) => IoError::other(e), 228 | } 229 | }) 230 | }) 231 | } 232 | } 233 | 234 | #[cfg(feature = "serde")] 235 | fn serialize_value( 236 | &mut self, 237 | value: &S, 238 | ) -> Result<(), struson::serde::SerializerError> { 239 | self.check_before_value(); 240 | let mut serializer = struson::serde::JsonWriterSerializer::new(self); 241 | value.serialize(&mut serializer) 242 | // TODO: Verify that value was properly serialized (only single value; no incomplete array or object) 243 | // might not be necessary because Serde's Serialize API enforces this 244 | } 245 | 246 | fn finish_document(self) -> Result { 247 | self.verify_string_writer_inactive(); 248 | if let Some(value) = self.final_value { 249 | Ok(value) 250 | } else { 251 | panic!("Incorrect writer usage: Top-level value is incomplete") 252 | } 253 | } 254 | } 255 | 256 | struct StringValueWriterImpl<'j> { 257 | buf: Vec, 258 | json_writer: &'j mut JsonValueWriter, 259 | } 260 | impl Write for StringValueWriterImpl<'_> { 261 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 262 | self.buf.extend_from_slice(buf); 263 | Ok(buf.len()) 264 | } 265 | 266 | fn flush(&mut self) -> std::io::Result<()> { 267 | // No-op 268 | Ok(()) 269 | } 270 | } 271 | impl StringValueWriter for StringValueWriterImpl<'_> { 272 | fn finish_value(self) -> Result<(), IoError> { 273 | let string = 274 | String::from_utf8(self.buf).map_err(|e| IoError::new(ErrorKind::InvalidData, e))?; 275 | self.json_writer.add_value(Value::String(string)); 276 | self.json_writer.is_string_value_writer_active = false; 277 | Ok(()) 278 | } 279 | } 280 | } 281 | 282 | #[test] 283 | fn write() -> Result<(), Box> { 284 | fn assert_invalid_number(expected_message: Option<&str>, result: Result<(), JsonNumberError>) { 285 | match result { 286 | Err(JsonNumberError::InvalidNumber(message)) => { 287 | if let Some(expected_message) = expected_message { 288 | assert_eq!(expected_message, message) 289 | } 290 | } 291 | _ => panic!("Unexpected result: {result:?}"), 292 | } 293 | } 294 | 295 | let mut json_writer = JsonValueWriter::new(); 296 | 297 | json_writer.begin_array()?; 298 | 299 | json_writer.begin_object()?; 300 | json_writer.name("name1")?; 301 | json_writer.begin_array()?; 302 | json_writer.bool_value(true)?; 303 | json_writer.end_array()?; 304 | json_writer.name("name2")?; 305 | json_writer.bool_value(false)?; 306 | json_writer.end_object()?; 307 | 308 | json_writer.null_value()?; 309 | json_writer.bool_value(true)?; 310 | json_writer.bool_value(false)?; 311 | json_writer.string_value("string")?; 312 | 313 | let mut string_writer = json_writer.string_value_writer()?; 314 | string_writer.write_all("first ".as_bytes())?; 315 | string_writer.write_all("second".as_bytes())?; 316 | string_writer.finish_value()?; 317 | 318 | json_writer.number_value_from_string("123")?; 319 | assert_invalid_number( 320 | Some(&format!("non-finite number: {}", f64::INFINITY)), 321 | json_writer.number_value_from_string("Infinity"), 322 | ); 323 | // Don't check for exact error message because it is created by Rust and might change in the future 324 | assert_invalid_number(None, json_writer.number_value_from_string("test")); 325 | json_writer.number_value(45)?; 326 | json_writer.number_value(-67)?; 327 | json_writer.fp_number_value(8.9)?; 328 | assert_invalid_number( 329 | Some(&format!("non-finite number: {}", f64::INFINITY)), 330 | json_writer.fp_number_value(f64::INFINITY), 331 | ); 332 | 333 | json_writer.end_array()?; 334 | let written_value = json_writer.finish_document()?; 335 | 336 | let expected_json = json!([ 337 | { 338 | "name1": [true], 339 | "name2": false, 340 | }, 341 | null, 342 | true, 343 | false, 344 | "string", 345 | "first second", 346 | // Current number from string implementation always writes f64 347 | 123.0, 348 | 45, 349 | -67, 350 | 8.9, 351 | ]); 352 | assert_eq!(expected_json, written_value); 353 | 354 | Ok(()) 355 | } 356 | 357 | #[test] 358 | fn transfer() -> Result<(), Box> { 359 | let mut json_writer = JsonValueWriter::new(); 360 | 361 | let mut json_reader = JsonStreamReader::new( 362 | "[true, 123, {\"name1\": \"value1\", \"name2\": null}, false]".as_bytes(), 363 | ); 364 | json_reader.transfer_to(&mut json_writer)?; 365 | json_reader.consume_trailing_whitespace()?; 366 | 367 | let written_value = json_writer.finish_document()?; 368 | 369 | let expected_json = json!([ 370 | true, 371 | // Current number from string implementation always writes f64 372 | 123.0, 373 | { 374 | "name1": "value1", 375 | "name2": null, 376 | }, 377 | false, 378 | ]); 379 | assert_eq!(expected_json, written_value); 380 | 381 | Ok(()) 382 | } 383 | 384 | #[cfg(feature = "serde")] 385 | #[test] 386 | fn serialize() -> Result<(), Box> { 387 | use serde::Serialize; 388 | 389 | #[derive(Serialize)] 390 | struct CustomStruct { 391 | a: u32, 392 | b: &'static str, 393 | } 394 | 395 | let mut json_writer = JsonValueWriter::new(); 396 | json_writer.serialize_value(&CustomStruct { a: 123, b: "test" })?; 397 | let written_value = json_writer.finish_document()?; 398 | 399 | let expected_json = json!({ 400 | "a": 123, 401 | "b": "test", 402 | }); 403 | assert_eq!(expected_json, written_value); 404 | 405 | Ok(()) 406 | } 407 | -------------------------------------------------------------------------------- /tests/partial_reader.rs: -------------------------------------------------------------------------------- 1 | //! Integration test for a custom `JsonReader` implementation which reads 2 | //! partial / incomplete JSON data 3 | //! 4 | //! This code was originally created for https://github.com/Marcono1234/struson/discussions/19#discussioncomment-7415830 5 | //! 6 | //! **Important:** This code is only for integration test and demonstration purposes; 7 | //! it is not intended to be used in production code. 8 | 9 | #![cfg(feature = "serde")] 10 | 11 | use serde::Deserialize; 12 | use struson::{ 13 | reader::{ 14 | JsonReader, JsonReaderPosition, JsonStreamReader, JsonSyntaxError, LinePosition, 15 | ReaderError, SyntaxErrorKind, TransferError, UnexpectedStructureKind, ValueType, 16 | }, 17 | serde::{DeserializerError, JsonReaderDeserializer}, 18 | writer::JsonWriter, 19 | }; 20 | 21 | #[derive(Debug, PartialEq)] 22 | enum PeekedValue { 23 | Null, 24 | Bool(bool), 25 | Number(String), 26 | String(String), 27 | /// Peeked array start, but has not been consumed yet 28 | PeekedArray, 29 | /// Peeked object start, but has not been consumed yet 30 | PeekedObject, 31 | } 32 | impl PeekedValue { 33 | fn get_value_type(&self) -> ValueType { 34 | match self { 35 | PeekedValue::Null => ValueType::Null, 36 | PeekedValue::Bool(_) => ValueType::Boolean, 37 | PeekedValue::Number(_) => ValueType::Number, 38 | PeekedValue::String(_) => ValueType::String, 39 | PeekedValue::PeekedArray => ValueType::Array, 40 | PeekedValue::PeekedObject => ValueType::Object, 41 | } 42 | } 43 | } 44 | 45 | struct PartialJsonReader { 46 | delegate: J, 47 | reached_eof: bool, 48 | /// Stack which is expanded every time an array or object is opened; 49 | /// values are `true` if object, `false` if array 50 | is_in_object: Vec, 51 | /// Temporarily holding string value or name to allow returning reference to it 52 | string_buf: String, 53 | peeked_name_pos: Option, 54 | peeked_name: Option, 55 | peeked_value_pos: Option, 56 | peeked_value: Option, 57 | after_peeked_pos: JsonReaderPosition, 58 | } 59 | 60 | impl PartialJsonReader { 61 | pub fn new(delegate: J) -> Self { 62 | let initial_pos = delegate.current_position(false); 63 | PartialJsonReader { 64 | delegate, 65 | reached_eof: false, 66 | is_in_object: Vec::new(), 67 | string_buf: String::new(), 68 | peeked_name_pos: None, 69 | peeked_name: None, 70 | peeked_value_pos: None, 71 | peeked_value: None, 72 | after_peeked_pos: initial_pos, 73 | } 74 | } 75 | } 76 | 77 | impl PartialJsonReader { 78 | fn provident_current_position(&mut self) -> JsonReaderPosition { 79 | // For now don't include path for better performance since this method is called providently 80 | // even if peeking value succeeds and position will be discarded 81 | let include_path = false; 82 | self.delegate.current_position(include_path) 83 | } 84 | 85 | fn peek_value(&mut self) -> Result { 86 | let peeked = self.delegate.peek()?; 87 | self.peeked_value_pos = Some(self.provident_current_position()); 88 | 89 | self.peeked_value = Some(match peeked { 90 | ValueType::Array => PeekedValue::PeekedArray, 91 | ValueType::Object => PeekedValue::PeekedObject, 92 | ValueType::String => { 93 | let v = PeekedValue::String(self.delegate.next_string()?); 94 | self.after_peeked_pos = self.provident_current_position(); 95 | v 96 | } 97 | ValueType::Number => { 98 | let v = PeekedValue::Number(self.delegate.next_number_as_string()?); 99 | // For number must make sure complete number was processed; for example 100 | // `1` might actually be `1.2` or `12` 101 | 102 | // Only works for non-top-level value; for top-level value cannot know if number is complete 103 | if !self.is_in_object.is_empty() { 104 | // Trigger EOF error in case nothing follows after last char of number 105 | let _ = self.delegate.has_next()?; 106 | } 107 | self.after_peeked_pos = self.provident_current_position(); 108 | v 109 | } 110 | ValueType::Boolean => { 111 | let v = PeekedValue::Bool(self.delegate.next_bool()?); 112 | self.after_peeked_pos = self.provident_current_position(); 113 | v 114 | } 115 | ValueType::Null => { 116 | self.delegate.next_null()?; 117 | self.after_peeked_pos = self.provident_current_position(); 118 | PeekedValue::Null 119 | } 120 | }); 121 | Ok(peeked) 122 | } 123 | 124 | fn has_next_impl(&mut self) -> Result { 125 | if self.delegate.has_next()? { 126 | // Must peek next array item / object member 127 | 128 | if let Some(true) = self.is_in_object.last() { 129 | self.peeked_name_pos = Some(self.provident_current_position()); 130 | self.peeked_name = Some(self.delegate.next_name_owned()?); 131 | } 132 | 133 | self.peek_value()?; 134 | Ok(true) 135 | } else { 136 | Ok(false) 137 | } 138 | } 139 | } 140 | 141 | macro_rules! consume_expected_value { 142 | ($self:ident, $expected:pat_param => $consumer:expr, $expected_type:ident) => {{ 143 | // Populate `self.peeked_value` (or fail if there is no next value) 144 | let _ = $self.peek()?; 145 | 146 | let p = $self.peeked_value.take().unwrap(); 147 | if let $expected = p { 148 | Ok($consumer) 149 | } else { 150 | let actual_type = p.get_value_type(); 151 | 152 | // Put back unexpected value 153 | $self.peeked_value = Some(p); 154 | 155 | Err(ReaderError::UnexpectedValueType { 156 | expected: ValueType::$expected_type, 157 | actual: actual_type, 158 | location: $self.peeked_value_pos.clone().unwrap(), 159 | }) 160 | } 161 | }}; 162 | } 163 | 164 | /* 165 | * This implementation is incomplete: 166 | * - multiple methods contain `unimplemented!()` 167 | * - correct API usage is not properly enforced, e.g. it might be possible to consume 168 | * an object member value before its name 169 | * - retrying on any error type may cause unspecified behavior (even the ones for which JsonReader says 170 | * it is safe to retry) 171 | */ 172 | impl JsonReader for PartialJsonReader { 173 | fn peek(&mut self) -> Result { 174 | // If called for top-level value and value has not peeked yet, peek at it here 175 | if self.is_in_object.is_empty() && self.peeked_value.is_none() { 176 | return self.peek_value(); 177 | } 178 | 179 | if self.has_next()? { 180 | let p = self.peeked_value.as_ref().unwrap(); 181 | Ok(p.get_value_type()) 182 | } else { 183 | Err(ReaderError::UnexpectedStructure { 184 | kind: UnexpectedStructureKind::FewerElementsThanExpected, 185 | location: self.current_position(true), 186 | }) 187 | } 188 | } 189 | 190 | fn begin_object(&mut self) -> Result<(), ReaderError> { 191 | consume_expected_value!( 192 | self, 193 | PeekedValue::PeekedObject => { 194 | self.is_in_object.push(true); 195 | self.delegate.begin_object()?; 196 | self.after_peeked_pos = self.provident_current_position(); 197 | }, 198 | Object 199 | ) 200 | } 201 | 202 | fn end_object(&mut self) -> Result<(), ReaderError> { 203 | match self.is_in_object.last() { 204 | Some(true) => {} 205 | // Covers `None` (neither in array nor object), and `Some(false)` (in array) 206 | _ => panic!("not inside object"), 207 | } 208 | 209 | if self.has_next()? { 210 | return Err(ReaderError::UnexpectedStructure { 211 | kind: UnexpectedStructureKind::MoreElementsThanExpected, 212 | location: self.current_position(true), 213 | }); 214 | } 215 | 216 | if self.reached_eof { 217 | self.is_in_object.pop(); 218 | Ok(()) 219 | } else { 220 | self.delegate.end_object()?; 221 | // Only pop after delegate `end_object()` was successful, to allow retry if it fails with MoreElementsThanExpected 222 | self.is_in_object.pop(); 223 | self.after_peeked_pos = self.provident_current_position(); 224 | Ok(()) 225 | } 226 | } 227 | 228 | fn begin_array(&mut self) -> Result<(), ReaderError> { 229 | consume_expected_value!( 230 | self, 231 | PeekedValue::PeekedArray => { 232 | self.is_in_object.push(false); 233 | self.delegate.begin_array()?; 234 | self.after_peeked_pos = self.provident_current_position(); 235 | }, 236 | Array 237 | ) 238 | } 239 | 240 | fn end_array(&mut self) -> Result<(), ReaderError> { 241 | match self.is_in_object.last() { 242 | Some(false) => {} 243 | // Covers `None` (neither in array nor object), and `Some(true)` (in object) 244 | _ => panic!("not inside array"), 245 | } 246 | 247 | if self.has_next()? { 248 | return Err(ReaderError::UnexpectedStructure { 249 | kind: UnexpectedStructureKind::MoreElementsThanExpected, 250 | location: self.current_position(true), 251 | }); 252 | } 253 | 254 | if self.reached_eof { 255 | self.is_in_object.pop(); 256 | Ok(()) 257 | } else { 258 | self.delegate.end_array()?; 259 | // Only pop after delegate `end_array()` was successful, to allow retry if it fails with MoreElementsThanExpected 260 | self.is_in_object.pop(); 261 | self.after_peeked_pos = self.provident_current_position(); 262 | Ok(()) 263 | } 264 | } 265 | 266 | fn has_next(&mut self) -> Result { 267 | if self.reached_eof { 268 | Ok(false) 269 | } else if self.peeked_name.is_some() || self.peeked_value.is_some() { 270 | Ok(true) 271 | } else { 272 | match self.has_next_impl() { 273 | // JsonStreamReader currently reports not only `SyntaxErrorKind::IncompleteDocument` 274 | // on unexpected EOF, but also other errors, such as `InvalidLiteral` 275 | Err(ReaderError::SyntaxError(JsonSyntaxError { .. })) => { 276 | self.reached_eof = true; 277 | // Clear the peeked name, if any, to avoid accidentally consuming it despite the member 278 | // value being missing 279 | self.peeked_name.take(); 280 | Ok(false) 281 | } 282 | // Propagate any other errors, or success result 283 | r => r, 284 | } 285 | } 286 | } 287 | 288 | fn next_name(&mut self) -> Result<&str, ReaderError> { 289 | self.string_buf = self.next_name_owned()?; 290 | Ok(&self.string_buf) 291 | } 292 | 293 | fn next_name_owned(&mut self) -> Result { 294 | match self.is_in_object.last() { 295 | Some(true) => {} 296 | // Covers `None` (neither in array nor object), and `Some(false)` (in array) 297 | _ => panic!("not inside object"), 298 | } 299 | 300 | if self.has_next()? { 301 | Ok(self.peeked_name.take().unwrap()) 302 | } else { 303 | Err(ReaderError::UnexpectedStructure { 304 | kind: UnexpectedStructureKind::FewerElementsThanExpected, 305 | location: self.current_position(true), 306 | }) 307 | } 308 | } 309 | 310 | fn next_str(&mut self) -> Result<&str, ReaderError> { 311 | self.string_buf = self.next_string()?; 312 | Ok(&self.string_buf) 313 | } 314 | 315 | fn next_string(&mut self) -> Result { 316 | consume_expected_value!( 317 | self, 318 | PeekedValue::String(s) => s, 319 | String 320 | ) 321 | } 322 | 323 | fn next_string_reader(&mut self) -> Result { 324 | unimplemented!(); 325 | // Unreachable; allow the compiler to infer the type of `impl std::io::Read` 326 | #[allow(unreachable_code)] 327 | Ok(std::io::empty()) 328 | } 329 | 330 | fn next_number_as_str(&mut self) -> Result<&str, ReaderError> { 331 | self.string_buf = self.next_number_as_string()?; 332 | Ok(&self.string_buf) 333 | } 334 | 335 | fn next_number_as_string(&mut self) -> Result { 336 | consume_expected_value!( 337 | self, 338 | PeekedValue::Number(s) => s, 339 | Number 340 | ) 341 | } 342 | 343 | fn next_bool(&mut self) -> Result { 344 | consume_expected_value!( 345 | self, 346 | PeekedValue::Bool(b) => b, 347 | Boolean 348 | ) 349 | } 350 | 351 | fn next_null(&mut self) -> Result<(), ReaderError> { 352 | consume_expected_value!( 353 | self, 354 | PeekedValue::Null => (), 355 | Null 356 | ) 357 | } 358 | 359 | fn deserialize_next<'de, D: Deserialize<'de>>(&mut self) -> Result { 360 | let mut deserializer = JsonReaderDeserializer::new(self); 361 | D::deserialize(&mut deserializer) 362 | } 363 | 364 | fn skip_name(&mut self) -> Result<(), ReaderError> { 365 | let _ = self.next_name()?; 366 | Ok(()) 367 | } 368 | 369 | // Important: This is implemented recursively; could lead to stack overflow for deeply nested JSON 370 | fn skip_value(&mut self) -> Result<(), ReaderError> { 371 | // Populate `self.peeked_value` (or fail if there is no next value) 372 | let _ = self.peek()?; 373 | 374 | match self.peeked_value.as_ref().unwrap() { 375 | // For array and object need to manually skip value here by delegating to other 376 | // methods to handle EOF properly; cannot delegate to underlying JSON reader 377 | PeekedValue::PeekedArray => { 378 | self.begin_array()?; 379 | while self.has_next()? { 380 | self.skip_value()?; 381 | } 382 | self.end_array() 383 | } 384 | PeekedValue::PeekedObject => { 385 | self.begin_object()?; 386 | while self.has_next()? { 387 | self.skip_name()?; 388 | self.skip_value()?; 389 | } 390 | self.end_object() 391 | } 392 | _ => { 393 | self.peeked_value.take(); 394 | Ok(()) 395 | } 396 | } 397 | } 398 | 399 | fn skip_to_top_level(&mut self) -> Result<(), ReaderError> { 400 | unimplemented!() 401 | } 402 | 403 | fn transfer_to(&mut self, _json_writer: &mut W) -> Result<(), TransferError> { 404 | unimplemented!() 405 | } 406 | 407 | fn consume_trailing_whitespace(self) -> Result<(), ReaderError> { 408 | if self.reached_eof { 409 | Ok(()) 410 | } else { 411 | self.delegate.consume_trailing_whitespace() 412 | } 413 | } 414 | 415 | fn current_position(&self, _include_path: bool) -> JsonReaderPosition { 416 | if self.peeked_name.is_some() { 417 | self.peeked_name_pos.clone().unwrap() 418 | } else if self.peeked_value.is_some() { 419 | self.peeked_value_pos.clone().unwrap() 420 | } else { 421 | // Use stored position instead of directly obtaining position from delegate 422 | // since its position might already be at the end of the partial JSON data, 423 | // even though the trailing JSON value is incomplete and won't be returned 424 | // by this reader 425 | self.after_peeked_pos.clone() 426 | } 427 | } 428 | } 429 | 430 | macro_rules! deserialize_partial { 431 | ($reader:expr, |$deserializer:ident| $deserializing_function:expr) => {{ 432 | let delegate = JsonStreamReader::new($reader); 433 | let mut json_reader = PartialJsonReader::new(delegate); 434 | let mut d = JsonReaderDeserializer::new(&mut json_reader); 435 | let $deserializer = &mut d; 436 | $deserializing_function 437 | }}; 438 | } 439 | 440 | #[allow(clippy::useless_vec)] // https://github.com/rust-lang/rust-clippy/issues/11958 441 | #[test] 442 | fn test() { 443 | #[derive(Debug, Deserialize, Clone, PartialEq)] 444 | #[serde(default)] 445 | struct Outer { 446 | a: u32, 447 | b: bool, 448 | c: Option, 449 | d: Vec, 450 | } 451 | impl Default for Outer { 452 | fn default() -> Self { 453 | Self { 454 | a: Default::default(), 455 | b: Default::default(), 456 | c: Some(1), // Use something other than `None` to test JSON null handling 457 | d: Default::default(), 458 | } 459 | } 460 | } 461 | 462 | #[derive(Debug, Default, Deserialize, Clone, PartialEq)] 463 | #[serde(default)] 464 | struct Inner { 465 | e: String, 466 | f: f32, 467 | } 468 | 469 | let full_json = r#"{"a":2,"b":true,"c":null,"d":[{"e":"str\"","f":1.2e3}]}"#; 470 | let mut json = String::new(); 471 | let mut outer = Outer::default(); 472 | 473 | // Test handling of empty JSON 474 | let result = deserialize_partial!("".as_bytes(), |d| Outer::deserialize_in_place( 475 | d, &mut outer 476 | )); 477 | match result { 478 | Err(DeserializerError::ReaderError(ReaderError::SyntaxError(JsonSyntaxError { 479 | kind: SyntaxErrorKind::IncompleteDocument, 480 | .. 481 | }))) => {} 482 | r => panic!("Unexpected result: {r:?}"), 483 | } 484 | 485 | let mut expected_deserialized = Vec::::new(); 486 | expected_deserialized.extend_from_slice(&vec![Outer::default(); 7]); 487 | expected_deserialized.extend_from_slice(&vec![ 488 | Outer { 489 | a: 2, 490 | ..Default::default() 491 | }; 492 | 7 493 | ]); 494 | expected_deserialized.extend_from_slice(&vec![ 495 | Outer { 496 | a: 2, 497 | b: true, 498 | ..Default::default() 499 | }; 500 | 9 501 | ]); 502 | expected_deserialized.extend_from_slice(&vec![ 503 | Outer { 504 | a: 2, 505 | b: true, 506 | c: None, 507 | ..Default::default() 508 | }; 509 | 7 510 | ]); 511 | expected_deserialized.extend_from_slice(&vec![ 512 | Outer { 513 | a: 2, 514 | b: true, 515 | c: None, 516 | d: vec![Inner::default()] 517 | }; 518 | 11 519 | ]); 520 | expected_deserialized.extend_from_slice(&vec![ 521 | Outer { 522 | a: 2, 523 | b: true, 524 | c: None, 525 | d: vec![Inner { 526 | e: "str\"".to_owned(), 527 | ..Default::default() 528 | }] 529 | }; 530 | 11 531 | ]); 532 | expected_deserialized.extend_from_slice(&vec![ 533 | Outer { 534 | a: 2, 535 | b: true, 536 | c: None, 537 | d: vec![Inner { 538 | e: "str\"".to_owned(), 539 | f: 1.2e3 540 | }] 541 | }; 542 | 3 543 | ]); 544 | // Verify that test is properly implemented and number of expected values is equal to chars 545 | assert_eq!(full_json.chars().count(), expected_deserialized.len()); 546 | 547 | for (index, c) in full_json.char_indices() { 548 | json.push(c); 549 | deserialize_partial!(json.as_bytes(), |d| Outer::deserialize_in_place( 550 | d, &mut outer 551 | )) 552 | .unwrap(); 553 | assert_eq!( 554 | expected_deserialized[index], outer, 555 | "For char index {index}, JSON: {json}" 556 | ); 557 | } 558 | } 559 | 560 | #[test] 561 | fn unexpected_value() -> Result<(), Box> { 562 | let json = "true"; 563 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 564 | match json_reader.next_number_as_str() { 565 | Err(ReaderError::UnexpectedValueType { 566 | expected: ValueType::Number, 567 | actual: ValueType::Boolean, 568 | location, 569 | }) => { 570 | assert_eq!( 571 | JsonReaderPosition { 572 | path: None, 573 | line_pos: Some(LinePosition { line: 0, column: 0 }), 574 | data_pos: Some(0) 575 | }, 576 | location 577 | ); 578 | } 579 | r => panic!("unexpected result: {r:?}"), 580 | } 581 | assert_eq!(true, json_reader.next_bool()?); 582 | 583 | let json = "true"; 584 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 585 | match json_reader.begin_array() { 586 | Err(ReaderError::UnexpectedValueType { 587 | expected: ValueType::Array, 588 | actual: ValueType::Boolean, 589 | location, 590 | }) => { 591 | assert_eq!( 592 | JsonReaderPosition { 593 | path: None, 594 | line_pos: Some(LinePosition { line: 0, column: 0 }), 595 | data_pos: Some(0) 596 | }, 597 | location 598 | ); 599 | } 600 | r => panic!("unexpected result: {r:?}"), 601 | } 602 | assert_eq!(true, json_reader.next_bool()?); 603 | 604 | let json = "[true]"; 605 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 606 | match json_reader.next_number_as_str() { 607 | Err(ReaderError::UnexpectedValueType { 608 | expected: ValueType::Number, 609 | actual: ValueType::Array, 610 | location, 611 | }) => { 612 | assert_eq!( 613 | JsonReaderPosition { 614 | path: None, 615 | line_pos: Some(LinePosition { line: 0, column: 0 }), 616 | data_pos: Some(0) 617 | }, 618 | location 619 | ); 620 | } 621 | r => panic!("unexpected result: {r:?}"), 622 | } 623 | json_reader.begin_array()?; 624 | assert_eq!(true, json_reader.next_bool()?); 625 | json_reader.end_array()?; 626 | 627 | Ok(()) 628 | } 629 | 630 | #[test] 631 | fn end_incomplete() -> Result<(), Box> { 632 | let json = "["; 633 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 634 | json_reader.begin_array()?; 635 | // Directly calls `end_array()` without preceding `has_next()` 636 | json_reader.end_array()?; 637 | json_reader.consume_trailing_whitespace()?; 638 | 639 | let json = "{"; 640 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 641 | json_reader.begin_object()?; 642 | // Directly calls `end_object()` without preceding `has_next()` 643 | json_reader.end_object()?; 644 | json_reader.consume_trailing_whitespace()?; 645 | 646 | Ok(()) 647 | } 648 | 649 | #[test] 650 | fn unexpected_structure() -> Result<(), Box> { 651 | let json = "[]"; 652 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 653 | json_reader.begin_array()?; 654 | match json_reader.peek() { 655 | Err(ReaderError::UnexpectedStructure { 656 | kind: UnexpectedStructureKind::FewerElementsThanExpected, 657 | location, 658 | }) => { 659 | assert_eq!( 660 | JsonReaderPosition { 661 | path: None, 662 | line_pos: Some(LinePosition { line: 0, column: 1 }), 663 | data_pos: Some(1) 664 | }, 665 | location 666 | ); 667 | } 668 | r => panic!("unexpected result: {r:?}"), 669 | } 670 | json_reader.end_array()?; 671 | 672 | let json = "[true]"; 673 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 674 | json_reader.begin_array()?; 675 | match json_reader.end_array() { 676 | Err(ReaderError::UnexpectedStructure { 677 | kind: UnexpectedStructureKind::MoreElementsThanExpected, 678 | location, 679 | }) => { 680 | assert_eq!( 681 | JsonReaderPosition { 682 | path: None, 683 | line_pos: Some(LinePosition { line: 0, column: 1 }), 684 | data_pos: Some(1) 685 | }, 686 | location 687 | ); 688 | } 689 | r => panic!("unexpected result: {r:?}"), 690 | } 691 | assert_eq!(true, json_reader.next_bool()?); 692 | json_reader.end_array()?; 693 | 694 | let json = "{}"; 695 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 696 | json_reader.begin_object()?; 697 | match json_reader.next_name() { 698 | Err(ReaderError::UnexpectedStructure { 699 | kind: UnexpectedStructureKind::FewerElementsThanExpected, 700 | location, 701 | }) => { 702 | assert_eq!( 703 | JsonReaderPosition { 704 | path: None, 705 | line_pos: Some(LinePosition { line: 0, column: 1 }), 706 | data_pos: Some(1) 707 | }, 708 | location 709 | ); 710 | } 711 | r => panic!("unexpected result: {r:?}"), 712 | } 713 | json_reader.end_object()?; 714 | 715 | let json = "{\"a\": true}"; 716 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 717 | json_reader.begin_object()?; 718 | match json_reader.end_object() { 719 | Err(ReaderError::UnexpectedStructure { 720 | kind: UnexpectedStructureKind::MoreElementsThanExpected, 721 | location, 722 | }) => { 723 | assert_eq!( 724 | JsonReaderPosition { 725 | path: None, 726 | line_pos: Some(LinePosition { line: 0, column: 1 }), 727 | data_pos: Some(1) 728 | }, 729 | location 730 | ); 731 | } 732 | r => panic!("unexpected result: {r:?}"), 733 | } 734 | assert_eq!("a", json_reader.next_name()?); 735 | assert_eq!(true, json_reader.next_bool()?); 736 | json_reader.end_object()?; 737 | 738 | Ok(()) 739 | } 740 | 741 | #[test] 742 | fn current_position() -> Result<(), Box> { 743 | let json = r#" [ 1 , { "a" : [] , "b" : "#; 744 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 745 | 746 | fn assert_pos(json_reader: &PartialJsonReader, expected_column: u64) { 747 | let position = json_reader.current_position(true); 748 | assert_eq!( 749 | JsonReaderPosition { 750 | path: None, 751 | line_pos: Some(LinePosition { 752 | line: 0, 753 | column: expected_column, 754 | }), 755 | // Assume input is ASCII only on single line; treat column as byte pos 756 | data_pos: Some(expected_column) 757 | }, 758 | position 759 | ); 760 | } 761 | 762 | assert_pos(&json_reader, 0); 763 | assert_eq!(ValueType::Array, json_reader.peek()?); 764 | assert_pos(&json_reader, 1); 765 | json_reader.begin_array()?; 766 | assert_pos(&json_reader, 2); 767 | assert!(json_reader.has_next()?); 768 | assert_pos(&json_reader, 3); 769 | assert_eq!("1", json_reader.next_number_as_str()?); 770 | assert_pos(&json_reader, 7); 771 | assert!(json_reader.has_next()?); 772 | assert_pos(&json_reader, 7); 773 | json_reader.begin_object()?; 774 | assert_pos(&json_reader, 8); 775 | assert!(json_reader.has_next()?); 776 | assert_pos(&json_reader, 9); 777 | assert_eq!("a", json_reader.next_name()?); 778 | assert_pos(&json_reader, 15); 779 | assert_eq!(ValueType::Array, json_reader.peek()?); 780 | assert_pos(&json_reader, 15); 781 | json_reader.begin_array()?; 782 | assert_pos(&json_reader, 16); 783 | assert!(!json_reader.has_next()?); 784 | assert_pos(&json_reader, 16); 785 | json_reader.end_array()?; 786 | assert_pos(&json_reader, 17); 787 | // Here the end of valid JSON is reached 788 | assert!(!json_reader.has_next()?); 789 | assert_pos(&json_reader, 17); 790 | json_reader.end_object()?; 791 | assert_pos(&json_reader, 17); 792 | assert!(!json_reader.has_next()?); 793 | assert_pos(&json_reader, 17); 794 | json_reader.end_array()?; 795 | assert_pos(&json_reader, 17); 796 | 797 | Ok(()) 798 | } 799 | -------------------------------------------------------------------------------- /tests/reader_alloc_test.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, io::Read}; 2 | 3 | use assert_no_alloc::permit_alloc; 4 | // Only use import when creating debug builds, see also configuration below 5 | #[cfg(debug_assertions)] 6 | use assert_no_alloc::AllocDisabler; 7 | use struson::{ 8 | json_path, 9 | reader::{JsonReader, JsonStreamReader, ReaderSettings}, 10 | writer::{JsonStreamWriter, JsonWriter}, 11 | }; 12 | 13 | // Only enable when creating debug builds 14 | #[cfg(debug_assertions)] 15 | #[global_allocator] 16 | static A: AllocDisabler = AllocDisabler; 17 | 18 | fn assert_no_alloc Result<(), Box>>(func: F) { 19 | assert_no_alloc::assert_no_alloc(func).unwrap() 20 | } 21 | 22 | fn permit_dealloc T>(func: F) -> T { 23 | // TODO: Permitting only dealloc is not possible yet, see https://github.com/Windfisch/rust-assert-no-alloc/issues/15 24 | permit_alloc(func) 25 | } 26 | 27 | fn new_reader(json: &str) -> JsonStreamReader<&[u8]> { 28 | JsonStreamReader::new_custom( 29 | json.as_bytes(), 30 | ReaderSettings { 31 | // Disable path tracking because that causes allocations 32 | track_path: false, 33 | ..Default::default() 34 | }, 35 | ) 36 | } 37 | 38 | #[test] 39 | fn skip() { 40 | let json = 41 | r#"{"a": [{"b": 1, "c": [[], [2, {"d": 3, "e": "some string value"}]]}], "f": true}"#; 42 | let mut json_reader = new_reader(json); 43 | 44 | assert_no_alloc(|| { 45 | json_reader.skip_value()?; 46 | 47 | permit_dealloc(|| json_reader.consume_trailing_whitespace())?; 48 | Ok(()) 49 | }); 50 | } 51 | 52 | #[test] 53 | fn read_values() { 54 | let json = "{\"a\": [\"string\", \"\u{1234}\u{10FFFF}\", 1234.5e+6, true, false, null]}"; 55 | let mut json_reader = new_reader(json); 56 | 57 | assert_no_alloc(|| { 58 | json_reader.begin_object()?; 59 | assert_eq!("a", json_reader.next_name()?); 60 | json_reader.begin_array()?; 61 | 62 | assert_eq!("string", json_reader.next_str()?); 63 | assert_eq!("\u{1234}\u{10FFFF}", json_reader.next_str()?); 64 | assert_eq!("1234.5e+6", json_reader.next_number_as_str()?); 65 | assert_eq!(true, json_reader.next_bool()?); 66 | assert_eq!(false, json_reader.next_bool()?); 67 | json_reader.next_null()?; 68 | 69 | json_reader.end_array()?; 70 | json_reader.end_object()?; 71 | permit_dealloc(|| json_reader.consume_trailing_whitespace())?; 72 | Ok(()) 73 | }); 74 | } 75 | 76 | #[test] 77 | fn read_string_escape_sequences() { 78 | let json = r#"["\n", "\t a", "a \u1234", "a \uDBFF\uDFFF b"]"#; 79 | let mut json_reader = new_reader(json); 80 | 81 | assert_no_alloc(|| { 82 | json_reader.begin_array()?; 83 | // These don't cause allocation because the internal value buffer has already 84 | // been allocated when the JSON reader was created, and is then reused 85 | assert_eq!("\n", json_reader.next_str()?); 86 | assert_eq!("\t a", json_reader.next_str()?); 87 | assert_eq!("a \u{1234}", json_reader.next_str()?); 88 | assert_eq!("a \u{10FFFF} b", json_reader.next_str()?); 89 | 90 | json_reader.end_array()?; 91 | permit_dealloc(|| json_reader.consume_trailing_whitespace())?; 92 | Ok(()) 93 | }); 94 | } 95 | 96 | #[test] 97 | fn string_value_reader() -> Result<(), Box> { 98 | let repetition_count = 100; 99 | let json_string_value = "\\n \\t a \\u1234 \\uDBFF\\uDFFF \u{10FFFF}".repeat(repetition_count); 100 | let expected_string_value = "\n \t a \u{1234} \u{10FFFF} \u{10FFFF}".repeat(repetition_count); 101 | let json = format!("\"{json_string_value}\""); 102 | let mut json_reader = new_reader(&json); 103 | 104 | // Pre-allocate with expected size to avoid allocations during test execution 105 | let mut string_output = String::with_capacity(expected_string_value.len()); 106 | 107 | assert_no_alloc(|| { 108 | let mut string_value_reader = json_reader.next_string_reader()?; 109 | string_value_reader.read_to_string(&mut string_output)?; 110 | drop(string_value_reader); 111 | 112 | permit_dealloc(|| json_reader.consume_trailing_whitespace())?; 113 | Ok(()) 114 | }); 115 | 116 | assert_eq!(expected_string_value, string_output); 117 | Ok(()) 118 | } 119 | 120 | #[test] 121 | fn transfer_to() -> Result<(), Box> { 122 | let inner_json = r#"{"a":[{"b":1,"c":[[],[2,{"d":3,"e":"some string value"}]]}],"f":true}"#; 123 | let json = "{\"outer-ignored\": 1, \"outer\":[\"ignored\", ".to_owned() + inner_json + "]}"; 124 | let mut json_reader = new_reader(&json); 125 | 126 | // Pre-allocate with expected size to avoid allocations during test execution 127 | let mut writer = Vec::::with_capacity(inner_json.len()); 128 | let mut json_writer = JsonStreamWriter::new(&mut writer); 129 | 130 | let json_path = json_path!["outer", 1]; 131 | 132 | assert_no_alloc(|| { 133 | json_reader.seek_to(&json_path)?; 134 | 135 | json_reader.transfer_to(&mut json_writer)?; 136 | permit_dealloc(|| json_writer.finish_document())?; 137 | 138 | json_reader.skip_to_top_level()?; 139 | permit_dealloc(|| json_reader.consume_trailing_whitespace())?; 140 | Ok(()) 141 | }); 142 | 143 | assert_eq!(inner_json, String::from_utf8(writer)?); 144 | Ok(()) 145 | } 146 | -------------------------------------------------------------------------------- /tests/reader_test.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt::Debug, fs::File}; 2 | 3 | use struson::reader::{JsonReader, JsonStreamReader, ValueType}; 4 | 5 | use crate::test_lib::{get_expected_events, get_test_data_file_path, JsonEvent}; 6 | 7 | mod test_lib; 8 | 9 | /// Assertion slices for slices which provides more useful error messages than `assert_eq!` 10 | fn assert_slice_eq(left: &[T], right: &[T]) { 11 | let iter_len = left.len().min(right.len()); 12 | 13 | for i in 0..iter_len { 14 | assert_eq!(left[i], right[i], "Elements at index {i} don't match"); 15 | } 16 | 17 | // Only check length mismatch afterwards, to detect mismatching items (if any) first 18 | assert_eq!(left.len(), right.len(), "Slices have different lengths"); 19 | } 20 | 21 | #[test] 22 | fn reader_test() -> Result<(), Box> { 23 | let mut json_reader = JsonStreamReader::new(File::open(get_test_data_file_path())?); 24 | let mut events = Vec::new(); 25 | 26 | enum StackValue { 27 | Array, 28 | Object, 29 | } 30 | 31 | let mut stack = Vec::new(); 32 | loop { 33 | if !stack.is_empty() { 34 | match stack.last().unwrap() { 35 | StackValue::Array => { 36 | if !json_reader.has_next()? { 37 | stack.pop(); 38 | json_reader.end_array()?; 39 | events.push(JsonEvent::ArrayEnd); 40 | 41 | if stack.is_empty() { 42 | break; 43 | } else { 44 | continue; 45 | } 46 | } 47 | } 48 | StackValue::Object => { 49 | if json_reader.has_next()? { 50 | events.push(JsonEvent::MemberName(json_reader.next_name_owned()?)); 51 | // fall through to value reading 52 | } else { 53 | stack.pop(); 54 | json_reader.end_object()?; 55 | events.push(JsonEvent::ObjectEnd); 56 | 57 | if stack.is_empty() { 58 | break; 59 | } else { 60 | continue; 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | match json_reader.peek()? { 68 | ValueType::Array => { 69 | json_reader.begin_array()?; 70 | stack.push(StackValue::Array); 71 | events.push(JsonEvent::ArrayStart); 72 | } 73 | ValueType::Object => { 74 | json_reader.begin_object()?; 75 | stack.push(StackValue::Object); 76 | events.push(JsonEvent::ObjectStart); 77 | } 78 | ValueType::String => { 79 | events.push(JsonEvent::StringValue(json_reader.next_string()?)); 80 | } 81 | ValueType::Number => { 82 | events.push(JsonEvent::NumberValue(json_reader.next_number_as_string()?)); 83 | } 84 | ValueType::Boolean => { 85 | events.push(JsonEvent::BoolValue(json_reader.next_bool()?)); 86 | } 87 | ValueType::Null => { 88 | json_reader.next_null()?; 89 | events.push(JsonEvent::NullValue); 90 | } 91 | } 92 | 93 | if stack.is_empty() { 94 | break; 95 | } 96 | } 97 | json_reader.consume_trailing_whitespace()?; 98 | 99 | assert_slice_eq(&get_expected_events(), &events); 100 | 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /tests/serde_deserialize_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "serde")] 2 | 3 | use std::{collections::HashMap, fmt::Debug}; 4 | 5 | use serde::{de::DeserializeOwned, Deserialize}; 6 | use struson::{ 7 | reader::{JsonReader, JsonStreamReader, ReaderError, UnexpectedStructureKind, ValueType}, 8 | serde::{DeserializerError, JsonReaderDeserializer}, 9 | }; 10 | 11 | fn assert_deserialized( 12 | json: &str, 13 | expected_deserialized: D, 14 | ) { 15 | let mut json_reader = JsonStreamReader::new(json.as_bytes()); 16 | let mut deserializer = JsonReaderDeserializer::new(&mut json_reader); 17 | let value = D::deserialize(&mut deserializer).unwrap(); 18 | json_reader.consume_trailing_whitespace().unwrap(); 19 | assert_eq!(expected_deserialized, value); 20 | 21 | let value_serde_json = serde_json::from_str(json).unwrap(); 22 | assert_eq!( 23 | expected_deserialized, value_serde_json, 24 | "Serde JSON deserialized value does not match expected value" 25 | ); 26 | } 27 | 28 | macro_rules! assert_deserialize_error { 29 | ($json:expr, $type:ident, $expected_pattern:pat_param => $assertion:expr, $serde_error:ident => $assertion_serde:expr) => { 30 | let mut json_reader = JsonStreamReader::new($json.as_bytes()); 31 | let mut deserializer = JsonReaderDeserializer::new(&mut json_reader); 32 | match $type::deserialize(&mut deserializer) { 33 | Err($expected_pattern) => $assertion, 34 | r => panic!("Unexpected result for '{}': {r:?}", $json), 35 | } 36 | 37 | match serde_json::from_str::<$type>($json) { 38 | Err($serde_error) => $assertion_serde, 39 | r => panic!("Unexpected result for Serde JSON for '{}': {r:?}", $json), 40 | } 41 | }; 42 | } 43 | 44 | #[test] 45 | fn deserialize_enum() { 46 | #[derive(Deserialize, PartialEq, Debug)] 47 | enum E { 48 | A, // unit 49 | B(u8), // newtype 50 | C(i16, String), // tuple 51 | D { 52 | // struct 53 | a: f32, 54 | b: Vec, 55 | }, 56 | } 57 | 58 | assert_deserialized("\"A\"", E::A); 59 | assert_deserialized(r#"{"A": null}"#, E::A); 60 | 61 | assert_deserialized(r#"{"B": 5}"#, E::B(5)); 62 | assert_deserialize_error!( 63 | "\"B\"", 64 | E, 65 | DeserializerError::Custom(message) => assert_eq!("invalid type: unit variant, expected newtype variant", message), 66 | serde_error => assert_eq!("invalid type: unit variant, expected newtype variant", serde_error.to_string()) 67 | ); 68 | 69 | assert_deserialized(r#"{"C": [-7, "test"]}"#, E::C(-7, "test".to_owned())); 70 | assert_deserialize_error!( 71 | r#"{"C": 5}"#, 72 | E, 73 | DeserializerError::ReaderError(ReaderError::UnexpectedValueType { expected, actual, .. }) => { 74 | assert_eq!(ValueType::Array, expected); 75 | assert_eq!(ValueType::Number, actual); 76 | }, 77 | serde_error => assert_eq!("invalid type: integer `5`, expected tuple variant E::C at line 1 column 7", serde_error.to_string()) 78 | ); 79 | 80 | assert_deserialized( 81 | r#"{"D": {"a": 1.5, "b": [true]}}"#, 82 | E::D { 83 | a: 1.5, 84 | b: vec![true], 85 | }, 86 | ); 87 | assert_deserialized( 88 | r#"{"D": [3.2, [false]]}"#, 89 | E::D { 90 | a: 3.2, 91 | b: vec![false], 92 | }, 93 | ); 94 | assert_deserialize_error!( 95 | r#"{"D": 1.2}"#, 96 | E, 97 | DeserializerError::Custom(message) => assert_eq!("invalid type: number, expected struct variant E::D", message), 98 | serde_error => assert_eq!("invalid type: floating point `1.2`, expected struct variant E::D at line 1 column 9", serde_error.to_string()) 99 | ); 100 | 101 | assert_deserialize_error!( 102 | r#"{"E": 1}"#, 103 | E, 104 | DeserializerError::Custom(message) => assert_eq!("unknown variant `E`, expected one of `A`, `B`, `C`, `D`", message), 105 | serde_error => assert_eq!("unknown variant `E`, expected one of `A`, `B`, `C`, `D` at line 1 column 4", serde_error.to_string()) 106 | ); 107 | } 108 | 109 | #[test] 110 | fn deserialize_map() { 111 | assert_deserialized("{}", HashMap::::new()); 112 | assert_deserialized(r#"{"": 1}"#, HashMap::from([("".to_owned(), 1)])); 113 | assert_deserialized(r#"{"a": 1}"#, HashMap::from([("a".to_owned(), 1)])); 114 | assert_deserialized( 115 | r#"{"1": true, "2": false}"#, 116 | HashMap::from([(1, true), (2, false)]), 117 | ); 118 | assert_deserialized( 119 | r#"{"a": [1, 2], "b": [3, 4]}"#, 120 | HashMap::from([("a".to_owned(), vec![1, 2]), ("b".to_owned(), vec![3, 4])]), 121 | ); 122 | 123 | // Duplicate key 124 | assert_deserialized(r#"{"a": 1, "a": 2}"#, HashMap::from([("a".to_owned(), 2)])); 125 | } 126 | 127 | #[test] 128 | fn deserialize_unit_struct() { 129 | #[derive(Deserialize, PartialEq, Debug)] 130 | struct S; 131 | 132 | assert_deserialized("null", S); 133 | } 134 | 135 | #[test] 136 | fn deserialize_newtype_struct() { 137 | #[derive(Deserialize, PartialEq, Debug)] 138 | struct S(u8); 139 | 140 | assert_deserialized("5", S(5)); 141 | } 142 | 143 | #[test] 144 | fn deserialize_tuple_struct() { 145 | #[derive(Deserialize, PartialEq, Debug)] 146 | struct S(u8, String); 147 | 148 | assert_deserialized("[8, \"test\"]", S(8, "test".to_owned())); 149 | 150 | assert_deserialize_error!( 151 | "[1]", 152 | S, 153 | DeserializerError::Custom(message) => assert_eq!("invalid length 1, expected tuple struct S with 2 elements", message), 154 | serde_error => assert_eq!("invalid length 1, expected tuple struct S with 2 elements at line 1 column 3", serde_error.to_string()) 155 | ); 156 | assert_deserialize_error!( 157 | "[1, \"test\", 2]", 158 | S, 159 | DeserializerError::ReaderError(ReaderError::UnexpectedStructure { kind, .. }) => assert_eq!(UnexpectedStructureKind::MoreElementsThanExpected, kind), 160 | serde_error => assert_eq!("trailing characters at line 1 column 13", serde_error.to_string()) 161 | ); 162 | } 163 | 164 | #[test] 165 | fn deserialize_struct() { 166 | #[derive(Deserialize, PartialEq, Debug)] 167 | struct S { 168 | a: f32, 169 | b: Vec, 170 | } 171 | 172 | assert_deserialized( 173 | r#"{"a": 4.1, "b": [false]}"#, 174 | S { 175 | a: 4.1, 176 | b: vec![false], 177 | }, 178 | ); 179 | assert_deserialized( 180 | "[1.2, [true]]", 181 | S { 182 | a: 1.2, 183 | b: vec![true], 184 | }, 185 | ); 186 | 187 | assert_deserialize_error!( 188 | r#"{"a": 1, "a": 2}"#, 189 | S, 190 | DeserializerError::Custom(message) => assert_eq!("duplicate field `a`", message), 191 | serde_error => assert_eq!("duplicate field `a` at line 1 column 12", serde_error.to_string()) 192 | ); 193 | 194 | assert_deserialize_error!( 195 | "[1]", 196 | S, 197 | DeserializerError::Custom(message) => assert_eq!("invalid length 1, expected struct S with 2 elements", message), 198 | serde_error => assert_eq!("invalid length 1, expected struct S with 2 elements at line 1 column 3", serde_error.to_string()) 199 | ); 200 | assert_deserialize_error!( 201 | "[1, [true], false]", 202 | S, 203 | DeserializerError::ReaderError(ReaderError::UnexpectedStructure { kind, .. }) => assert_eq!(UnexpectedStructureKind::MoreElementsThanExpected, kind), 204 | serde_error => assert_eq!("trailing characters at line 1 column 13", serde_error.to_string()) 205 | ); 206 | } 207 | 208 | #[test] 209 | fn deserialize_option() { 210 | assert_deserialized("5", Some(5)); 211 | assert_deserialized("null", None::); 212 | 213 | // Ambiguity for values which are themselves serialized as `null` 214 | assert_deserialized("null", None::<()>); 215 | } 216 | 217 | #[test] 218 | fn deserialize_seq() { 219 | assert_deserialized("[]", Vec::::new()); 220 | assert_deserialized("[null]", vec![()]); 221 | assert_deserialized("[1, -3, 4.5]", vec![1.0, -3.0, 4.5]); 222 | } 223 | 224 | #[test] 225 | fn deserialize_string() { 226 | assert_deserialized( 227 | "\"a \\u0000 \\uD852\\uDF62 \u{10FFFF}\"", 228 | "a \0 \u{24B62} \u{10FFFF}".to_owned(), 229 | ); 230 | } 231 | -------------------------------------------------------------------------------- /tests/serde_serialize_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "serde")] 2 | 3 | use std::collections::{BTreeMap, HashMap}; 4 | 5 | use serde::Serialize; 6 | use struson::{ 7 | serde::JsonWriterSerializer, 8 | writer::{JsonStreamWriter, JsonWriter}, 9 | }; 10 | 11 | fn assert_serialized(s: S, expected_json: &str) { 12 | let mut writer = Vec::::new(); 13 | let mut json_writer = JsonStreamWriter::new(&mut writer); 14 | let mut serializer = JsonWriterSerializer::new(&mut json_writer); 15 | s.serialize(&mut serializer).unwrap(); 16 | json_writer.finish_document().unwrap(); 17 | let json = String::from_utf8(writer).unwrap(); 18 | assert_eq!(expected_json, json); 19 | 20 | let serde_json = serde_json::to_string(&s).unwrap(); 21 | assert_eq!( 22 | expected_json, serde_json, 23 | "Serde JSON output does not match expected JSON" 24 | ); 25 | } 26 | 27 | #[test] 28 | fn serialize_map() { 29 | // Use BTreeMap for consistent entry order 30 | assert_serialized( 31 | BTreeMap::from([("a", vec![1, 2]), ("b", vec![3, 4])]), 32 | r#"{"a":[1,2],"b":[3,4]}"#, 33 | ); 34 | 35 | assert_serialized(HashMap::from([(1, 2)]), r#"{"1":2}"#); 36 | } 37 | 38 | #[test] 39 | fn serialize_newtype_struct() { 40 | #[derive(Serialize)] 41 | struct S(Vec); 42 | 43 | assert_serialized(S(vec![1, 2]), "[1,2]"); 44 | } 45 | 46 | #[test] 47 | fn serialize_newtype_variant() { 48 | #[derive(Serialize)] 49 | enum E { 50 | A(Vec), 51 | } 52 | 53 | assert_serialized(E::A(vec![1, 2]), r#"{"A":[1,2]}"#); 54 | } 55 | 56 | #[test] 57 | fn serialize_option() { 58 | assert_serialized(vec![None, Some(1)], "[null,1]"); 59 | 60 | // Ambiguity for values which are themselves serialized as `null` 61 | assert_serialized(vec![None, Some(())], "[null,null]"); 62 | } 63 | 64 | #[test] 65 | fn serialize_struct() { 66 | #[derive(Serialize)] 67 | struct S { 68 | a: u64, 69 | b: bool, 70 | c: Vec, 71 | } 72 | 73 | assert_serialized( 74 | S { 75 | a: 1, 76 | b: true, 77 | c: vec![S { 78 | a: 2, 79 | b: false, 80 | c: Vec::new(), 81 | }], 82 | }, 83 | r#"{"a":1,"b":true,"c":[{"a":2,"b":false,"c":[]}]}"#, 84 | ); 85 | } 86 | 87 | #[test] 88 | fn serialize_struct_variant() { 89 | #[derive(Serialize)] 90 | enum E { 91 | A { a: i8, b: String }, 92 | } 93 | 94 | assert_serialized( 95 | E::A { 96 | a: -3, 97 | b: "test".to_owned(), 98 | }, 99 | r#"{"A":{"a":-3,"b":"test"}}"#, 100 | ); 101 | } 102 | 103 | #[test] 104 | fn serialize_tuple() { 105 | assert_serialized((true, 1, "test"), r#"[true,1,"test"]"#); 106 | } 107 | 108 | #[test] 109 | fn serialize_tuple_struct() { 110 | #[derive(Serialize)] 111 | struct S(bool, Vec, &'static str); 112 | 113 | assert_serialized(S(true, vec![1], "test"), r#"[true,[1],"test"]"#); 114 | } 115 | 116 | #[test] 117 | fn serialize_tuple_variant() { 118 | #[derive(Serialize)] 119 | enum E { 120 | A(bool, i8), 121 | } 122 | 123 | assert_serialized(E::A(true, -3), r#"{"A":[true,-3]}"#); 124 | } 125 | 126 | #[test] 127 | fn serialize_unit() { 128 | assert_serialized((), "null"); 129 | } 130 | 131 | #[test] 132 | fn serialize_unit_struct() { 133 | #[derive(Serialize)] 134 | struct S; 135 | 136 | assert_serialized(S, "null"); 137 | } 138 | 139 | #[test] 140 | fn serialize_unit_variant() { 141 | #[derive(Serialize)] 142 | enum E { 143 | A, 144 | } 145 | 146 | assert_serialized(E::A, "\"A\""); 147 | } 148 | 149 | #[test] 150 | fn serialize_skipped_field() { 151 | #[derive(Serialize)] 152 | struct S { 153 | a: u32, 154 | #[allow(dead_code)] 155 | #[serde(skip)] 156 | b: u32, 157 | } 158 | 159 | assert_serialized(S { a: 1, b: 2 }, r#"{"a":1}"#); 160 | } 161 | 162 | #[test] 163 | fn serialize_conditional_skipped_field() { 164 | #[derive(Serialize)] 165 | struct S { 166 | a: u32, 167 | #[serde(skip_serializing_if = "Option::is_none")] 168 | b: Option, 169 | } 170 | 171 | assert_serialized(S { a: 1, b: Some(2) }, r#"{"a":1,"b":2}"#); 172 | assert_serialized(S { a: 1, b: None }, r#"{"a":1}"#); 173 | 174 | #[derive(Serialize)] 175 | enum E { 176 | S { 177 | a: u32, 178 | #[serde(skip_serializing_if = "Option::is_none")] 179 | b: Option, 180 | }, 181 | } 182 | assert_serialized(E::S { a: 1, b: Some(2) }, r#"{"S":{"a":1,"b":2}}"#); 183 | assert_serialized(E::S { a: 1, b: None }, r#"{"S":{"a":1}}"#); 184 | } 185 | -------------------------------------------------------------------------------- /tests/simple_writer.rs: -------------------------------------------------------------------------------- 1 | //! Tests for [`struson::writer::simple`] 2 | 3 | #![cfg(feature = "simple-api")] 4 | #![cfg(feature = "serde")] 5 | 6 | use std::{ 7 | cmp::min, 8 | collections::HashMap, 9 | error::Error, 10 | fmt::{Debug, Display}, 11 | io::{sink, ErrorKind, Sink, Write}, 12 | }; 13 | 14 | use struson::writer::{ 15 | simple::{SimpleJsonWriter, ValueWriter}, 16 | JsonStreamWriter, 17 | }; 18 | 19 | fn assert_written( 20 | f: impl FnOnce(SimpleJsonWriter>>) -> Result<(), E>, 21 | expected_json: &str, 22 | ) { 23 | let mut writer = Vec::new(); 24 | let json_writer = SimpleJsonWriter::new(&mut writer); 25 | f(json_writer).unwrap(); 26 | 27 | let json = String::from_utf8(writer).unwrap(); 28 | assert_eq!(expected_json, json); 29 | } 30 | 31 | #[test] 32 | fn write_simple() { 33 | assert_written(|j| j.write_null(), "null"); 34 | assert_written(|j| j.write_bool(true), "true"); 35 | assert_written(|j| j.write_string("test"), "\"test\""); 36 | assert_written(|j| j.write_number(1_u64), "1"); 37 | assert_written(|j| j.write_fp_number(2.3_f64), "2.3"); 38 | assert_written(|j| j.write_number_string("4.5e6"), "4.5e6"); 39 | assert_written(|j| j.write_serialize(&"serde"), "\"serde\""); 40 | } 41 | 42 | #[test] 43 | fn write_array() { 44 | assert_written( 45 | |json_writer| { 46 | json_writer.write_array(|array_writer| { 47 | array_writer.write_null()?; 48 | array_writer.write_bool(true)?; 49 | array_writer.write_string("string")?; 50 | array_writer 51 | .write_string_with_writer(|mut w| Ok(w.write_all(b"string-writer")?))?; 52 | array_writer.write_number(1_u64)?; 53 | array_writer.write_fp_number(2.3_f64)?; 54 | array_writer.write_number_string("4.5e6")?; 55 | array_writer.write_serialize(&"serde")?; 56 | 57 | array_writer.write_array(|array_writer| { 58 | array_writer.write_bool(false)?; 59 | Ok(()) 60 | })?; 61 | 62 | array_writer.write_object(|object_writer| { 63 | object_writer.write_bool_member("a", false)?; 64 | Ok(()) 65 | })?; 66 | 67 | Ok(()) 68 | })?; 69 | Ok::<(), Box>(()) 70 | }, 71 | "[null,true,\"string\",\"string-writer\",1,2.3,4.5e6,\"serde\",[false],{\"a\":false}]", 72 | ); 73 | } 74 | 75 | #[test] 76 | fn write_object() { 77 | assert_written( 78 | |json_writer| { 79 | json_writer.write_object(|object_writer| { 80 | object_writer.write_member("a", |value_writer| { 81 | value_writer.write_bool(true)?; 82 | Ok(()) 83 | })?; 84 | object_writer.write_member("b", |_| { 85 | // Writing no value will cause JSON null to be written 86 | Ok(()) 87 | })?; 88 | Ok(()) 89 | }) 90 | }, 91 | r#"{"a":true,"b":null}"#, 92 | ); 93 | 94 | assert_written( 95 | |json_writer| { 96 | json_writer.write_object(|object_writer| { 97 | object_writer.write_null_member("a")?; 98 | object_writer.write_bool_member("b", true)?; 99 | object_writer.write_string_member("c", "string")?; 100 | object_writer.write_string_member_with_writer("d", |mut w| { 101 | Ok(w.write_all(b"string-writer")?) 102 | })?; 103 | object_writer.write_number_member("e", 1_u64)?; 104 | object_writer.write_fp_number_member("f", 2.3_f64)?; 105 | object_writer.write_number_string_member("g", "4.5e6")?; 106 | object_writer.write_serialized_member("h", &"serde")?; 107 | 108 | object_writer.write_array_member("i", |array_writer| { 109 | array_writer.write_bool(false)?; 110 | Ok(()) 111 | })?; 112 | 113 | object_writer.write_object_member("j", |object_writer| { 114 | object_writer.write_bool_member("nested", true)?; 115 | Ok(()) 116 | })?; 117 | 118 | Ok(()) 119 | }) 120 | }, 121 | r#"{"a":null,"b":true,"c":"string","d":"string-writer","e":1,"f":2.3,"g":4.5e6,"h":"serde","i":[false],"j":{"nested":true}}"#, 122 | ); 123 | } 124 | 125 | #[test] 126 | fn write_string_with_writer() -> Result<(), Box> { 127 | assert_written( 128 | |f| { 129 | f.write_string_with_writer(|_| { 130 | // Write nothing 131 | Ok(()) 132 | }) 133 | }, 134 | "\"\"", 135 | ); 136 | 137 | assert_written( 138 | |f| { 139 | f.write_string_with_writer(|mut w| { 140 | w.write_all(b"test")?; 141 | Ok(()) 142 | }) 143 | }, 144 | "\"test\"", 145 | ); 146 | 147 | assert_written( 148 | |f| { 149 | f.write_string_with_writer(|mut w| { 150 | w.write_str("test \u{1F600}")?; 151 | Ok(()) 152 | }) 153 | }, 154 | "\"test \u{1F600}\"", 155 | ); 156 | 157 | assert_written( 158 | |f| { 159 | f.write_string_with_writer(|mut w| { 160 | w.write_all(b"first \"")?; 161 | w.write_all(b" second")?; 162 | w.write_str(" third \"")?; 163 | w.write_all(b" fourth")?; 164 | Ok(()) 165 | }) 166 | }, 167 | "\"first \\\" second third \\\" fourth\"", 168 | ); 169 | 170 | let json_writer = SimpleJsonWriter::new(std::io::sink()); 171 | let result = json_writer.write_string_with_writer(|mut w| { 172 | // Write malformed UTF-8 data 173 | w.write_all(b"\xFF")?; 174 | Ok(()) 175 | }); 176 | match result { 177 | Err(e) => assert_eq!("invalid UTF-8 data", e.to_string()), 178 | _ => panic!("unexpected result: {result:?}"), 179 | }; 180 | 181 | let json_writer = SimpleJsonWriter::new(std::io::sink()); 182 | let result = json_writer.write_string_with_writer(|mut w| { 183 | // Write incomplete UTF-8 data 184 | w.write_all(b"\xF0")?; 185 | Ok(()) 186 | }); 187 | match result { 188 | Err(e) => assert_eq!("incomplete multi-byte UTF-8 data", e.to_string()), 189 | _ => panic!("unexpected result: {result:?}"), 190 | }; 191 | 192 | #[derive(Debug, PartialEq)] 193 | enum WriterAction { 194 | Flush, 195 | Write(Vec), 196 | } 197 | struct TrackingWriter { 198 | actions: Vec, 199 | } 200 | impl Write for TrackingWriter { 201 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 202 | self.actions.push(WriterAction::Write(buf.to_vec())); 203 | Ok(buf.len()) 204 | } 205 | 206 | fn flush(&mut self) -> std::io::Result<()> { 207 | self.actions.push(WriterAction::Flush); 208 | Ok(()) 209 | } 210 | } 211 | 212 | let mut tracking_writer = TrackingWriter { 213 | actions: Vec::new(), 214 | }; 215 | let json_writer = SimpleJsonWriter::new(&mut tracking_writer); 216 | // Test usage of `flush()` 217 | json_writer.write_string_with_writer(|mut string_writer| { 218 | string_writer.flush()?; 219 | string_writer.write_all(b"first")?; 220 | string_writer.flush()?; 221 | string_writer.write_str(" second")?; 222 | string_writer.write_str(" third")?; 223 | string_writer.flush()?; 224 | string_writer.flush()?; 225 | string_writer.write_str(" fourth")?; 226 | Ok(()) 227 | })?; 228 | assert_eq!( 229 | // Note: This depends a bit on the implementation details of `JsonStreamWriter` 230 | vec![ 231 | WriterAction::Write(b"\"".to_vec()), 232 | WriterAction::Flush, 233 | WriterAction::Write(b"first".to_vec()), 234 | WriterAction::Flush, 235 | WriterAction::Write(b" second".to_vec()), 236 | WriterAction::Write(b" third".to_vec()), 237 | WriterAction::Flush, 238 | WriterAction::Flush, 239 | WriterAction::Write(b" fourth".to_vec()), 240 | WriterAction::Write(b"\"".to_vec()), 241 | WriterAction::Flush, 242 | ], 243 | tracking_writer.actions 244 | ); 245 | 246 | Ok(()) 247 | } 248 | 249 | /// Verifies that errors returned by closures are propagated and abort processing 250 | /// 251 | /// Especially after the closure returned an error no further `JsonWriter` methods should be 252 | /// called since that could cause a panic. 253 | #[test] 254 | fn closure_error_propagation() { 255 | let message = "custom-message"; 256 | fn assert_error( 257 | f: impl FnOnce(SimpleJsonWriter>>) -> Result, 258 | partial_json: &str, 259 | ) { 260 | let mut writer = Vec::new(); 261 | let json_writer = SimpleJsonWriter::new(&mut writer); 262 | let result = f(json_writer); 263 | match result { 264 | Err(e) => assert_eq!("custom-message", e.to_string()), 265 | _ => panic!("unexpected result: {result:?}"), 266 | }; 267 | 268 | // Check partial written JSON data to make sure no additional data (e.g. closing `]`) was 269 | // written after error was propagated 270 | assert_eq!(partial_json, String::from_utf8(writer).unwrap()); 271 | } 272 | 273 | // --- write_string_with_writer --- 274 | assert_error( 275 | |json_writer| json_writer.write_string_with_writer(|_| Err(message.into())), 276 | "\"", 277 | ); 278 | 279 | // --- write_array --- 280 | assert_error( 281 | |json_writer| json_writer.write_array(|_| Err(message.into())), 282 | "[", 283 | ); 284 | 285 | assert_error( 286 | |json_writer| { 287 | json_writer 288 | .write_array(|array_writer| array_writer.write_array(|_| Err(message.into()))) 289 | }, 290 | "[[", 291 | ); 292 | 293 | assert_error( 294 | |json_writer| { 295 | json_writer 296 | .write_array(|array_writer| array_writer.write_object(|_| Err(message.into()))) 297 | }, 298 | "[{", 299 | ); 300 | 301 | // --- write_object --- 302 | assert_error( 303 | |json_writer| json_writer.write_object(|_| Err(message.into())), 304 | "{", 305 | ); 306 | 307 | assert_error( 308 | |json_writer| { 309 | json_writer.write_object(|object_writer| { 310 | object_writer.write_member("name", |_| Err(message.into())) 311 | }) 312 | }, 313 | "{\"name\":", 314 | ); 315 | 316 | assert_error( 317 | |json_writer| { 318 | json_writer.write_object(|object_writer| { 319 | object_writer.write_array_member("name", |_| Err(message.into())) 320 | }) 321 | }, 322 | "{\"name\":[", 323 | ); 324 | 325 | assert_error( 326 | |json_writer| { 327 | json_writer.write_object(|object_writer| { 328 | object_writer.write_object_member("name", |_| Err(message.into())) 329 | }) 330 | }, 331 | "{\"name\":{", 332 | ); 333 | } 334 | 335 | /// Tests behavior when a user-provided closure encounters an `Err` from the writer, 336 | /// but instead of propagating it, returns `Ok` 337 | #[test] 338 | fn discarded_error_handling() { 339 | fn new_writer() -> SimpleJsonWriter> { 340 | SimpleJsonWriter::new(sink()) 341 | } 342 | 343 | let json_writer = new_writer(); 344 | let result = json_writer.write_array(|array_writer| { 345 | array_writer.write_fp_number(f32::NAN).unwrap_err(); 346 | Ok(()) 347 | }); 348 | assert_eq!( 349 | format!( 350 | "previous error '{}': non-finite number: {}", 351 | ErrorKind::Other, 352 | f32::NAN 353 | ), 354 | result.unwrap_err().to_string() 355 | ); 356 | 357 | let json_writer = new_writer(); 358 | let result = json_writer.write_object(|object_writer| { 359 | object_writer 360 | .write_fp_number_member("name", f32::NAN) 361 | .unwrap_err(); 362 | Ok(()) 363 | }); 364 | assert_eq!( 365 | format!( 366 | "previous error '{}': non-finite number: {}", 367 | ErrorKind::Other, 368 | f32::NAN 369 | ), 370 | result.unwrap_err().to_string() 371 | ); 372 | 373 | let json_writer = new_writer(); 374 | let result = json_writer.write_object(|object_writer| { 375 | object_writer.write_member("name", |value_writer| { 376 | value_writer.write_fp_number(f32::NAN).unwrap_err(); 377 | Ok(()) 378 | })?; 379 | Ok(()) 380 | }); 381 | assert_eq!( 382 | format!( 383 | "previous error '{}': non-finite number: {}", 384 | ErrorKind::Other, 385 | f32::NAN 386 | ), 387 | result.unwrap_err().to_string() 388 | ); 389 | 390 | let json_writer = new_writer(); 391 | let result = json_writer.write_array(|array_writer| { 392 | array_writer.write_number_string("invalid").unwrap_err(); 393 | Ok(()) 394 | }); 395 | assert_eq!( 396 | format!( 397 | "previous error '{}': invalid JSON number: invalid", 398 | ErrorKind::Other 399 | ), 400 | result.unwrap_err().to_string() 401 | ); 402 | 403 | let json_writer = new_writer(); 404 | let result = json_writer.write_array(|array_writer| { 405 | array_writer.write_serialize(&f32::NAN).unwrap_err(); 406 | Ok(()) 407 | }); 408 | assert_eq!( 409 | format!( 410 | "previous error '{}': invalid number: non-finite number: {}", 411 | ErrorKind::Other, 412 | f32::NAN 413 | ), 414 | result.unwrap_err().to_string() 415 | ); 416 | 417 | let json_writer = new_writer(); 418 | let result = json_writer.write_array(|array_writer| { 419 | let value = HashMap::from([(vec![1, 2], true)]); 420 | array_writer.write_serialize(&value).unwrap_err(); 421 | Ok(()) 422 | }); 423 | assert_eq!( 424 | format!( 425 | "previous error '{}': map key cannot be converted to string", 426 | ErrorKind::Other 427 | ), 428 | result.unwrap_err().to_string() 429 | ); 430 | 431 | /// Writer which only permits a certain amount of bytes, returning an error afterwards 432 | struct MaxCapacityWriter { 433 | remaining_capacity: usize, 434 | } 435 | impl Write for MaxCapacityWriter { 436 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 437 | if self.remaining_capacity == 0 { 438 | return Err(std::io::Error::new(ErrorKind::WouldBlock, "custom-error")); 439 | } 440 | 441 | let write_count = min(self.remaining_capacity, buf.len()); 442 | self.remaining_capacity -= write_count; 443 | Ok(write_count) 444 | } 445 | 446 | fn flush(&mut self) -> std::io::Result<()> { 447 | // Do nothing 448 | Ok(()) 449 | } 450 | } 451 | let json_writer = SimpleJsonWriter::new(MaxCapacityWriter { 452 | remaining_capacity: 3, 453 | }); 454 | let result = json_writer.write_array(|array_writer| { 455 | array_writer.write_string("test").unwrap_err(); 456 | Ok(()) 457 | }); 458 | assert_eq!( 459 | format!("previous error '{}': custom-error", ErrorKind::WouldBlock), 460 | result.unwrap_err().to_string() 461 | ); 462 | 463 | let json_writer = new_writer(); 464 | let result = json_writer.write_string_with_writer(|mut writer| { 465 | // Malformed UTF-8 466 | writer.write_all(b"\"\xFF\"").unwrap_err(); 467 | Ok(()) 468 | }); 469 | assert_eq!( 470 | format!( 471 | "previous error '{}': invalid UTF-8 data", 472 | ErrorKind::InvalidData 473 | ), 474 | result.unwrap_err().to_string() 475 | ); 476 | 477 | let json_writer = new_writer(); 478 | let result = json_writer.write_string_with_writer(|mut writer| { 479 | // Malformed UTF-8 480 | writer.write_all(b"\"\xFF\"").unwrap_err(); 481 | let result = writer.write_all(b"\"\xFF\""); 482 | assert_eq!( 483 | format!( 484 | "previous error '{}': invalid UTF-8 data", 485 | ErrorKind::InvalidData 486 | ), 487 | result.unwrap_err().to_string() 488 | ); 489 | Ok(()) 490 | }); 491 | assert_eq!( 492 | format!( 493 | "previous error '{}': invalid UTF-8 data", 494 | ErrorKind::InvalidData 495 | ), 496 | result.unwrap_err().to_string() 497 | ); 498 | 499 | let json_writer = SimpleJsonWriter::new(MaxCapacityWriter { 500 | remaining_capacity: 3, 501 | }); 502 | let result = json_writer.write_string_with_writer(|mut writer| { 503 | writer.write_str("test").unwrap_err(); 504 | Ok(()) 505 | }); 506 | assert_eq!( 507 | format!("previous error '{}': custom-error", ErrorKind::WouldBlock), 508 | result.unwrap_err().to_string() 509 | ); 510 | 511 | /// Writer which returns an error when `flush()` is called 512 | struct FlushErrorWriter; 513 | impl Write for FlushErrorWriter { 514 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 515 | // Do nothing 516 | Ok(buf.len()) 517 | } 518 | 519 | fn flush(&mut self) -> std::io::Result<()> { 520 | Err(std::io::Error::new(ErrorKind::WouldBlock, "custom-error")) 521 | } 522 | } 523 | let json_writer = SimpleJsonWriter::new(FlushErrorWriter); 524 | let result = json_writer.write_string_with_writer(|mut writer| { 525 | let error = writer.flush().unwrap_err(); 526 | assert_eq!(ErrorKind::WouldBlock, error.kind()); 527 | assert_eq!("custom-error", error.to_string()); 528 | Ok(()) 529 | }); 530 | assert_eq!( 531 | format!("previous error '{}': custom-error", ErrorKind::WouldBlock), 532 | result.unwrap_err().to_string() 533 | ); 534 | 535 | let json_writer = SimpleJsonWriter::new(sink()); 536 | let result = json_writer.write_array(|array_writer| { 537 | array_writer 538 | .write_string_with_writer(|mut writer| { 539 | // Malformed UTF-8 540 | writer.write_all(b"\"\xFF\"").unwrap_err(); 541 | Ok(()) 542 | }) 543 | .unwrap_err(); 544 | Ok(()) 545 | }); 546 | assert_eq!( 547 | format!( 548 | "previous error '{}': invalid UTF-8 data", 549 | ErrorKind::InvalidData 550 | ), 551 | result.unwrap_err().to_string() 552 | ); 553 | 554 | let json_writer = SimpleJsonWriter::new(sink()); 555 | let result = json_writer.write_object(|object_writer| { 556 | object_writer 557 | .write_string_member_with_writer("name", |mut writer| { 558 | // Malformed UTF-8 559 | writer.write_all(b"\"\xFF\"").unwrap_err(); 560 | Ok(()) 561 | }) 562 | .unwrap_err(); 563 | Ok(()) 564 | }); 565 | assert_eq!( 566 | format!( 567 | "previous error '{}': invalid UTF-8 data", 568 | ErrorKind::InvalidData 569 | ), 570 | result.unwrap_err().to_string() 571 | ); 572 | } 573 | -------------------------------------------------------------------------------- /tests/test_lib/mod.rs: -------------------------------------------------------------------------------- 1 | //! Common library module for integration tests 2 | // See https://doc.rust-lang.org/book/ch11-03-test-organization.html#submodules-in-integration-tests 3 | 4 | use std::path::PathBuf; 5 | 6 | pub fn get_test_data_file_path() -> PathBuf { 7 | // Get path of test file, see https://stackoverflow.com/a/30004252 8 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 9 | path.push("tests/test_lib/test_data.json"); 10 | path 11 | } 12 | 13 | #[derive(PartialEq, Eq, Debug)] 14 | pub enum JsonEvent { 15 | ArrayStart, 16 | ArrayEnd, 17 | ObjectStart, 18 | ObjectEnd, 19 | MemberName(String), 20 | 21 | StringValue(String), 22 | // Contains string representation of number value 23 | NumberValue(String), 24 | BoolValue(bool), 25 | NullValue, 26 | } 27 | 28 | /// Gets the events expected for the JSON document at the path returned by [`get_test_data_file_path`] 29 | pub fn get_expected_events() -> Vec { 30 | vec![ 31 | JsonEvent::ArrayStart, 32 | // Arrays 33 | JsonEvent::ArrayStart, 34 | JsonEvent::ArrayEnd, 35 | // Array with single item 36 | JsonEvent::ArrayStart, 37 | JsonEvent::NumberValue("1".to_owned()), 38 | JsonEvent::ArrayEnd, 39 | // Array with multiple items 40 | JsonEvent::ArrayStart, 41 | JsonEvent::NumberValue("1".to_owned()), 42 | JsonEvent::StringValue("a".to_owned()), 43 | JsonEvent::BoolValue(true), 44 | JsonEvent::ObjectStart, 45 | JsonEvent::MemberName("nested".to_owned()), 46 | JsonEvent::ArrayStart, 47 | JsonEvent::ObjectStart, 48 | JsonEvent::MemberName("nested2".to_owned()), 49 | JsonEvent::ArrayStart, 50 | JsonEvent::NumberValue("2".to_owned()), 51 | JsonEvent::ArrayEnd, 52 | JsonEvent::ObjectEnd, 53 | JsonEvent::ArrayEnd, 54 | JsonEvent::ObjectEnd, 55 | JsonEvent::ArrayEnd, 56 | // Objects 57 | JsonEvent::ObjectStart, 58 | JsonEvent::ObjectEnd, 59 | // Object with single member 60 | JsonEvent::ObjectStart, 61 | JsonEvent::MemberName("name".to_owned()), 62 | JsonEvent::NumberValue("1".to_owned()), 63 | JsonEvent::ObjectEnd, 64 | // Object with multiple members 65 | JsonEvent::ObjectStart, 66 | JsonEvent::MemberName("name1".to_owned()), 67 | JsonEvent::BoolValue(false), 68 | JsonEvent::MemberName("name2".to_owned()), 69 | JsonEvent::StringValue("value".to_owned()), 70 | JsonEvent::MemberName("name1".to_owned()), 71 | JsonEvent::NumberValue("2".to_owned()), 72 | JsonEvent::MemberName("".to_owned()), 73 | JsonEvent::NumberValue("3".to_owned()), 74 | JsonEvent::ObjectEnd, 75 | // Strings 76 | JsonEvent::StringValue("string value".to_owned()), 77 | JsonEvent::StringValue("\0 test \n\t \\ \"".to_owned()), 78 | JsonEvent::StringValue("unicode § ಀ ᠅ 𝄆".to_owned()), 79 | // Numbers 80 | JsonEvent::NumberValue("0".to_owned()), 81 | JsonEvent::NumberValue("-1234".to_owned()), 82 | JsonEvent::NumberValue("567.89".to_owned()), 83 | JsonEvent::NumberValue("100e-10".to_owned()), 84 | JsonEvent::NumberValue("6.070e+05".to_owned()), 85 | // Booleans 86 | JsonEvent::BoolValue(true), 87 | JsonEvent::BoolValue(false), 88 | JsonEvent::NullValue, 89 | JsonEvent::ArrayEnd, 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /tests/test_lib/test_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | [], 3 | [ 4 | 1 5 | ], 6 | [ 7 | 1, 8 | "a", 9 | true, 10 | { 11 | "nested": [ 12 | { 13 | "nested2": [ 14 | 2 15 | ] 16 | } 17 | ] 18 | } 19 | ], 20 | {}, 21 | { 22 | "name": 1 23 | }, 24 | { 25 | "name1": false, 26 | "name2": "value", 27 | "name1": 2, 28 | "": 3 29 | }, 30 | "string value", 31 | "\u0000 test \n\t \\ \"", 32 | "unicode § ಀ ᠅ 𝄆", 33 | 0, 34 | -1234, 35 | 567.89, 36 | 100e-10, 37 | 6.070e+05, 38 | true, 39 | false, 40 | null 41 | ] 42 | -------------------------------------------------------------------------------- /tests/transfer_test.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fs::read_to_string}; 2 | 3 | use struson::{ 4 | reader::{json_path::json_path, JsonReader, JsonStreamReader}, 5 | writer::{JsonStreamWriter, JsonWriter, WriterSettings}, 6 | }; 7 | 8 | use crate::test_lib::get_test_data_file_path; 9 | 10 | // Ignore dead code warnings because this test does not use all functions from `test_lib` 11 | #[allow(dead_code)] 12 | mod test_lib; 13 | 14 | #[test] 15 | fn transfer_test() -> Result<(), Box> { 16 | let expected_json = read_to_string(get_test_data_file_path())?; 17 | // Normalize JSON document string 18 | let expected_json = expected_json.replace('\r', ""); 19 | let expected_json = expected_json.trim_end(); 20 | 21 | let mut json_reader = JsonStreamReader::new(expected_json.as_bytes()); 22 | 23 | let mut writer = Vec::::new(); 24 | let mut json_writer = JsonStreamWriter::new_custom( 25 | &mut writer, 26 | WriterSettings { 27 | pretty_print: true, 28 | ..Default::default() 29 | }, 30 | ); 31 | 32 | // First wrap and transfer JSON document 33 | json_writer.begin_object()?; 34 | json_writer.name("nested")?; 35 | json_writer.begin_array()?; 36 | 37 | json_reader.transfer_to(&mut json_writer)?; 38 | json_reader.consume_trailing_whitespace()?; 39 | 40 | json_writer.end_array()?; 41 | json_writer.end_object()?; 42 | json_writer.finish_document()?; 43 | 44 | let intermediate_json = String::from_utf8(writer)?; 45 | 46 | let mut json_reader = JsonStreamReader::new(intermediate_json.as_bytes()); 47 | 48 | let mut writer = Vec::::new(); 49 | let mut json_writer = JsonStreamWriter::new_custom( 50 | &mut writer, 51 | WriterSettings { 52 | pretty_print: true, 53 | ..Default::default() 54 | }, 55 | ); 56 | 57 | // Then unwrap it again 58 | json_reader.seek_to(&json_path!["nested", 0])?; 59 | json_reader.transfer_to(&mut json_writer)?; 60 | json_reader.skip_to_top_level()?; 61 | json_reader.consume_trailing_whitespace()?; 62 | 63 | json_writer.finish_document()?; 64 | 65 | assert_eq!(expected_json, String::from_utf8(writer)?); 66 | 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /tests/writer_alloc_test.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, io::Write}; 2 | 3 | use assert_no_alloc::permit_alloc; 4 | // Only use import when creating debug builds, see also configuration below 5 | #[cfg(debug_assertions)] 6 | use assert_no_alloc::AllocDisabler; 7 | use struson::writer::{JsonStreamWriter, JsonWriter, StringValueWriter, WriterSettings}; 8 | 9 | // Only enable when creating debug builds 10 | #[cfg(debug_assertions)] 11 | #[global_allocator] 12 | static A: AllocDisabler = AllocDisabler; 13 | 14 | fn assert_no_alloc Result<(), Box>>(func: F) { 15 | assert_no_alloc::assert_no_alloc(func).unwrap() 16 | } 17 | 18 | fn permit_dealloc T>(func: F) -> T { 19 | // TODO: Permitting only dealloc is not possible yet, see https://github.com/Windfisch/rust-assert-no-alloc/issues/15 20 | permit_alloc(func) 21 | } 22 | 23 | fn new_byte_writer() -> Vec { 24 | // Pre-allocate to avoid allocations during test execution 25 | Vec::with_capacity(4096) 26 | } 27 | 28 | #[test] 29 | fn write_values() { 30 | let mut writer = new_byte_writer(); 31 | let mut json_writer = JsonStreamWriter::new_custom( 32 | &mut writer, 33 | WriterSettings { 34 | // To test creation of surrogate pair escape sequences for supplementary code points 35 | escape_all_non_ascii: true, 36 | ..Default::default() 37 | }, 38 | ); 39 | 40 | let large_string = "abcd".repeat(500); 41 | 42 | assert_no_alloc(|| { 43 | json_writer.begin_object()?; 44 | json_writer.name("a")?; 45 | 46 | json_writer.begin_array()?; 47 | // Write string which has to be escaped 48 | json_writer.string_value("\0\n\t \u{10FFFF}")?; 49 | json_writer.string_value(&large_string)?; 50 | // Note: Cannot use non-string number methods because they perform allocation 51 | json_writer.number_value_from_string("1234.56e-7")?; 52 | json_writer.bool_value(true)?; 53 | json_writer.bool_value(false)?; 54 | json_writer.null_value()?; 55 | json_writer.end_array()?; 56 | 57 | // Write string which has to be escaped 58 | json_writer.name("\0\n\t \u{10FFFF}")?; 59 | json_writer.bool_value(true)?; 60 | 61 | json_writer.end_object()?; 62 | 63 | permit_dealloc(|| json_writer.finish_document())?; 64 | Ok(()) 65 | }); 66 | 67 | let expected_json = "{\"a\":[\"\\u0000\\n\\t \\uDBFF\\uDFFF\",\"".to_owned() 68 | + &large_string 69 | + "\",1234.56e-7,true,false,null],\"\\u0000\\n\\t \\uDBFF\\uDFFF\":true}"; 70 | assert_eq!(expected_json, String::from_utf8(writer).unwrap()); 71 | } 72 | 73 | #[test] 74 | fn pretty_print() { 75 | let mut writer = new_byte_writer(); 76 | let mut json_writer = JsonStreamWriter::new_custom( 77 | &mut writer, 78 | WriterSettings { 79 | pretty_print: true, 80 | ..Default::default() 81 | }, 82 | ); 83 | 84 | assert_no_alloc(|| { 85 | json_writer.begin_object()?; 86 | json_writer.name("a")?; 87 | json_writer.begin_array()?; 88 | 89 | json_writer.begin_array()?; 90 | json_writer.end_array()?; 91 | json_writer.begin_object()?; 92 | json_writer.end_object()?; 93 | json_writer.bool_value(true)?; 94 | 95 | json_writer.end_array()?; 96 | json_writer.end_object()?; 97 | 98 | permit_dealloc(|| json_writer.finish_document())?; 99 | Ok(()) 100 | }); 101 | 102 | let expected_json = "{\n \"a\": [\n [],\n {},\n true\n ]\n}"; 103 | assert_eq!(expected_json, String::from_utf8(writer).unwrap()); 104 | } 105 | 106 | #[test] 107 | fn string_value_writer() -> Result<(), Box> { 108 | let mut writer = new_byte_writer(); 109 | let mut json_writer = JsonStreamWriter::new(&mut writer); 110 | let large_string = "abcd".repeat(500); 111 | 112 | assert_no_alloc(|| { 113 | let mut string_value_writer = json_writer.string_value_writer()?; 114 | string_value_writer.write_all(b"a")?; 115 | string_value_writer.write_all(b"\0")?; 116 | string_value_writer.write_all(b"\n\t")?; 117 | string_value_writer.write_all(large_string.as_bytes())?; 118 | string_value_writer.write_all(b"test")?; 119 | string_value_writer.finish_value()?; 120 | 121 | permit_dealloc(|| json_writer.finish_document())?; 122 | Ok(()) 123 | }); 124 | 125 | let expected_json = format!("\"a\\u0000\\n\\t{large_string}test\""); 126 | assert_eq!(expected_json, String::from_utf8(writer).unwrap()); 127 | Ok(()) 128 | } 129 | -------------------------------------------------------------------------------- /tests/writer_test.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fs::read_to_string}; 2 | 3 | use struson::writer::{JsonStreamWriter, JsonWriter, WriterSettings}; 4 | 5 | use crate::test_lib::{get_expected_events, get_test_data_file_path, JsonEvent}; 6 | 7 | mod test_lib; 8 | 9 | #[test] 10 | fn writer_test() -> Result<(), Box> { 11 | let expected_json = read_to_string(get_test_data_file_path())?; 12 | // Normalize JSON document string 13 | let expected_json = expected_json.replace('\r', ""); 14 | let expected_json = expected_json.trim_end(); 15 | 16 | let mut writer = Vec::::new(); 17 | let mut json_writer = JsonStreamWriter::new_custom( 18 | &mut writer, 19 | WriterSettings { 20 | pretty_print: true, 21 | ..Default::default() 22 | }, 23 | ); 24 | 25 | for event in get_expected_events() { 26 | match event { 27 | JsonEvent::ArrayStart => { 28 | json_writer.begin_array()?; 29 | } 30 | JsonEvent::ArrayEnd => { 31 | json_writer.end_array()?; 32 | } 33 | JsonEvent::ObjectStart => { 34 | json_writer.begin_object()?; 35 | } 36 | JsonEvent::ObjectEnd => { 37 | json_writer.end_object()?; 38 | } 39 | JsonEvent::MemberName(name) => { 40 | json_writer.name(&name)?; 41 | } 42 | JsonEvent::StringValue(value) => { 43 | json_writer.string_value(&value)?; 44 | } 45 | JsonEvent::NumberValue(value) => { 46 | json_writer.number_value_from_string(&value)?; 47 | } 48 | JsonEvent::BoolValue(value) => { 49 | json_writer.bool_value(value)?; 50 | } 51 | JsonEvent::NullValue => { 52 | json_writer.null_value()?; 53 | } 54 | } 55 | } 56 | 57 | json_writer.finish_document()?; 58 | 59 | assert_eq!(expected_json, String::from_utf8(writer)?); 60 | 61 | Ok(()) 62 | } 63 | --------------------------------------------------------------------------------