├── .gitignore ├── rust-toolchain.toml ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── enhancement_suggestion.md │ ├── documentation_issue.md │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── build.yml │ └── publish.yml ├── SECURITY.md ├── tests ├── test_lib │ ├── test_data.json │ └── mod.rs ├── writer_test.rs ├── transfer_test.rs ├── reader_test.rs ├── writer_alloc_test.rs ├── serde_serialize_test.rs ├── reader_alloc_test.rs ├── serde_deserialize_test.rs ├── custom_json_writer.rs ├── simple_writer.rs └── partial_reader.rs ├── ReleaseProcess.md ├── LICENSE-MIT ├── benches ├── serde_serialize_benchmark.rs ├── writer_struct_benchmark.rs ├── serde_deserialize_benchmark.rs ├── reader_struct_benchmark.rs ├── writer_benchmark.rs └── reader_benchmark.rs ├── Cargo.toml ├── Makefile.toml ├── src ├── serde │ └── mod.rs ├── utf8.rs ├── json_number.rs └── lib.rs ├── LICENSE-APACHE ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # IntelliJ / RustRover 4 | /.idea 5 | 6 | # VS Code 7 | /.vscode/settings.json 8 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.91.1" 3 | profile = "minimal" 4 | components = ["clippy", "rustfmt", "rust-docs"] 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Please report vulnerabilities privately through GitHub's [vulnerability reporting](https://github.com/Marcono1234/struson/security/advisories/new). 4 | If you are unsure whether an issue represents a vulnerability, report it privately nonetheless. We can then discuss it and decide whether to create a public bug report instead. 5 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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: "rust-toolchain" 6 | directory: "/" 7 | schedule: 8 | interval: "monthly" 9 | cooldown: 10 | default-days: 14 11 | 12 | - package-ecosystem: "cargo" 13 | directory: "/" 14 | schedule: 15 | interval: "monthly" 16 | cooldown: 17 | default-days: 14 18 | 19 | - package-ecosystem: "github-actions" 20 | directory: "/" 21 | schedule: 22 | interval: "monthly" 23 | cooldown: 24 | default-days: 14 25 | ignore: 26 | # Ignore patch updates for 'taiki-e/install-action' because they seem to only update the tool 27 | # versions; however GitHub workflow here has intentionally pinned tool version 28 | - dependency-name: "taiki-e/install-action" 29 | update-types: ["version-update:semver-patch"] 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 29 | 30 | - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 31 | 32 | - name: Install cargo-make 33 | uses: taiki-e/install-action@7fe7b8c79a3c5569643cf27dfc32456883a4cd4d #v2.62.16 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@7fe7b8c79a3c5569643cf27dfc32456883a4cd4d #v2.62.16 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 | -------------------------------------------------------------------------------- /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::{JsonEvent, get_expected_events, get_test_data_file_path}; 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 | -------------------------------------------------------------------------------- /tests/transfer_test.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fs::read_to_string}; 2 | 3 | use struson::{ 4 | reader::{JsonReader, JsonStreamReader, json_path::json_path}, 5 | writer::{JsonStreamWriter, JsonWriter, WriterSettings}, 6 | }; 7 | 8 | use crate::test_lib::get_test_data_file_path; 9 | 10 | #[expect( 11 | dead_code, 12 | reason = "this test does not use all functions from `test_lib`" 13 | )] 14 | mod test_lib; 15 | 16 | #[test] 17 | fn transfer_test() -> Result<(), Box> { 18 | let expected_json = read_to_string(get_test_data_file_path())?; 19 | // Normalize JSON document string 20 | let expected_json = expected_json.replace('\r', ""); 21 | let expected_json = expected_json.trim_end(); 22 | 23 | let mut json_reader = JsonStreamReader::new(expected_json.as_bytes()); 24 | 25 | let mut writer = Vec::::new(); 26 | let mut json_writer = JsonStreamWriter::new_custom( 27 | &mut writer, 28 | WriterSettings { 29 | pretty_print: true, 30 | ..Default::default() 31 | }, 32 | ); 33 | 34 | // First wrap and transfer JSON document 35 | json_writer.begin_object()?; 36 | json_writer.name("nested")?; 37 | json_writer.begin_array()?; 38 | 39 | json_reader.transfer_to(&mut json_writer)?; 40 | json_reader.consume_trailing_whitespace()?; 41 | 42 | json_writer.end_array()?; 43 | json_writer.end_object()?; 44 | json_writer.finish_document()?; 45 | 46 | let intermediate_json = String::from_utf8(writer)?; 47 | 48 | let mut json_reader = JsonStreamReader::new(intermediate_json.as_bytes()); 49 | 50 | let mut writer = Vec::::new(); 51 | let mut json_writer = JsonStreamWriter::new_custom( 52 | &mut writer, 53 | WriterSettings { 54 | pretty_print: true, 55 | ..Default::default() 56 | }, 57 | ); 58 | 59 | // Then unwrap it again 60 | json_reader.seek_to(&json_path!["nested", 0])?; 61 | json_reader.transfer_to(&mut json_writer)?; 62 | json_reader.skip_to_top_level()?; 63 | json_reader.consume_trailing_whitespace()?; 64 | 65 | json_writer.finish_document()?; 66 | 67 | assert_eq!(expected_json, String::from_utf8(writer)?); 68 | 69 | Ok(()) 70 | } 71 | -------------------------------------------------------------------------------- /benches/serde_serialize_benchmark.rs: -------------------------------------------------------------------------------- 1 | use criterion::{Criterion, criterion_group, criterion_main}; 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 writer = Vec::new(); 13 | let mut json_writer = JsonStreamWriter::new(&mut writer); 14 | let mut serializer = JsonWriterSerializer::new(&mut json_writer); 15 | value.serialize(&mut serializer).unwrap(); 16 | json_writer.finish_document().unwrap(); 17 | writer 18 | }); 19 | }); 20 | group.bench_with_input("serde-json", &value, |b, value| { 21 | b.iter(|| { 22 | let mut writer = Vec::new(); 23 | serde_json::to_writer(&mut writer, value).unwrap(); 24 | writer 25 | }); 26 | }); 27 | 28 | group.finish(); 29 | } 30 | 31 | fn benchmark_number_vec(c: &mut Criterion) { 32 | let value = (0..10) 33 | .map(|x| (0..10).map(|y| x * y).collect()) 34 | .collect::>>(); 35 | 36 | bench_compare(c, "serde-serialize-number-vec", value); 37 | } 38 | 39 | fn benchmark_structs(c: &mut Criterion) { 40 | #[derive(Serialize)] 41 | struct Nested { 42 | my_field: String, 43 | another_one: u32, 44 | } 45 | 46 | #[derive(Serialize)] 47 | enum Enum { 48 | VariantA, 49 | Other(u32), 50 | AndOneMore { value: bool }, 51 | } 52 | 53 | #[derive(Serialize)] 54 | struct Struct { 55 | name: String, 56 | some_value: u32, 57 | optional: Option, 58 | nested: Nested, 59 | enum_value: Enum, 60 | } 61 | 62 | let value = (0..30) 63 | .map(|i| Struct { 64 | name: format!("some name {i}"), 65 | some_value: i * 256, 66 | optional: if i % 5 == 0 { 67 | None 68 | } else { 69 | Some(i as f64 / 123.0) 70 | }, 71 | nested: Nested { 72 | my_field: "abcd".repeat((5 + i % 10) as usize), 73 | another_one: i * i, 74 | }, 75 | enum_value: match i % 3 { 76 | 0 => Enum::VariantA, 77 | 1 => Enum::Other(i * 2), 78 | _ => Enum::AndOneMore { value: i % 2 == 0 }, 79 | }, 80 | }) 81 | .collect::>(); 82 | 83 | bench_compare(c, "serde-serialize-structs", value); 84 | } 85 | 86 | criterion_group!( 87 | benches, 88 | // Benchmark functions 89 | benchmark_number_vec, 90 | benchmark_structs 91 | ); 92 | criterion_main!(benches); 93 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "struson" 3 | version = "0.6.0" 4 | authors = ["Marcono1234"] 5 | edition = "2024" 6 | rust-version = "1.85.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.17" 17 | strum = { version = "0.27.2", features = ["derive"] } 18 | duplicate = "2.0.1" 19 | 20 | serde_core = { version = "1.0.228", optional = true } 21 | 22 | [dev-dependencies] 23 | criterion = { version = "0.7.0", features = ["html_reports"] } 24 | # Serde is used for comparison in benchmarks and for tests 25 | serde = "1.0.228" 26 | # When updating serde_json, adjust Struson Serde integration behavior to match serde_json 27 | serde_json = "1.0.145" 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_core"] 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 | -------------------------------------------------------------------------------- /benches/writer_struct_benchmark.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use criterion::{Criterion, criterion_group, criterion_main}; 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 = std::iter::repeat_n( 18 | StructValue { 19 | bool_value: true, 20 | integer: 123456, 21 | float: 123.4567, 22 | string: "a string value with some text", 23 | }, 24 | 10_000, 25 | ) 26 | .collect(); 27 | 28 | fn struson_write( 29 | mut json_writer: impl JsonWriter, 30 | values: &Vec, 31 | ) -> Result<(), Box> { 32 | // Hopefully this is a fair comparison with how Serde behaves 33 | json_writer.begin_array()?; 34 | for value in values { 35 | json_writer.begin_object()?; 36 | 37 | json_writer.name("bool_value")?; 38 | json_writer.bool_value(value.bool_value)?; 39 | 40 | json_writer.name("integer")?; 41 | json_writer.number_value(value.integer)?; 42 | 43 | json_writer.name("float")?; 44 | json_writer.fp_number_value(value.float)?; 45 | 46 | json_writer.name("string")?; 47 | json_writer.string_value(value.string)?; 48 | 49 | json_writer.end_object()?; 50 | } 51 | json_writer.end_array()?; 52 | 53 | json_writer.finish_document()?; 54 | Ok(()) 55 | } 56 | 57 | group.bench_with_input("struson", &values, |b, values| { 58 | b.iter(|| { 59 | let mut writer = Vec::new(); 60 | let json_writer = JsonStreamWriter::new(&mut writer); 61 | struson_write(json_writer, values).unwrap(); 62 | writer 63 | }) 64 | }); 65 | group.bench_with_input("struson (pretty)", &values, |b, values| { 66 | b.iter(|| { 67 | let mut writer = Vec::new(); 68 | let json_writer = JsonStreamWriter::new_custom( 69 | &mut writer, 70 | WriterSettings { 71 | pretty_print: true, 72 | ..Default::default() 73 | }, 74 | ); 75 | struson_write(json_writer, values).unwrap(); 76 | writer 77 | }) 78 | }); 79 | 80 | group.bench_with_input("serde", &values, |b, values| { 81 | b.iter(|| { 82 | let mut writer = Vec::new(); 83 | serde_json::to_writer(&mut writer, &values).unwrap(); 84 | writer 85 | }) 86 | }); 87 | group.bench_with_input("serde (pretty)", &values, |b, values| { 88 | b.iter(|| { 89 | let mut writer = Vec::new(); 90 | serde_json::to_writer_pretty(&mut writer, &values).unwrap(); 91 | writer 92 | }) 93 | }); 94 | 95 | group.finish(); 96 | } 97 | 98 | criterion_group!( 99 | benches, 100 | // Benchmark functions 101 | benchmark_struct 102 | ); 103 | criterion_main!(benches); 104 | -------------------------------------------------------------------------------- /benches/serde_deserialize_benchmark.rs: -------------------------------------------------------------------------------- 1 | use std::hint::black_box; 2 | 3 | use criterion::{Criterion, criterion_group, criterion_main}; 4 | use serde::{Deserialize, Serialize, de::DeserializeOwned}; 5 | use struson::{ 6 | reader::{JsonReader, JsonStreamReader, ReaderSettings}, 7 | serde::JsonReaderDeserializer, 8 | }; 9 | 10 | fn bench_compare(c: &mut Criterion, name: &str, json: &str) { 11 | let bytes = json.as_bytes(); 12 | let mut group = c.benchmark_group(name); 13 | group.bench_with_input("struson", bytes, |b, bytes| { 14 | b.iter(|| { 15 | let mut json_reader = JsonStreamReader::new_custom( 16 | bytes, 17 | ReaderSettings { 18 | // Disable path tracking for fairer comparison, because Serde JSON does not seem to track JSON path either 19 | track_path: false, 20 | ..Default::default() 21 | }, 22 | ); 23 | let mut deserializer = JsonReaderDeserializer::new(&mut json_reader); 24 | black_box(D::deserialize(&mut deserializer).unwrap()); 25 | json_reader.consume_trailing_whitespace().unwrap(); 26 | }); 27 | }); 28 | group.bench_with_input("serde-json", bytes, |b, bytes| { 29 | b.iter(|| serde_json::from_reader::<_, D>(bytes).unwrap()); 30 | }); 31 | 32 | group.finish(); 33 | } 34 | 35 | fn benchmark_number_vec(c: &mut Criterion) { 36 | // Prepare JSON input 37 | let value = (0..10) 38 | .map(|x| (0..10).map(|y| x * y).collect()) 39 | .collect::>>(); 40 | 41 | let json = serde_json::to_string(&value).unwrap(); 42 | bench_compare::>>(c, "serde-deserialize-number-vec", &json); 43 | } 44 | 45 | fn benchmark_structs(c: &mut Criterion) { 46 | #[derive(Serialize, Deserialize)] 47 | struct Nested { 48 | my_field: String, 49 | another_one: u32, 50 | } 51 | 52 | #[derive(Serialize, Deserialize)] 53 | enum Enum { 54 | VariantA, 55 | Other(u32), 56 | AndOneMore { value: bool }, 57 | } 58 | 59 | #[derive(Serialize, Deserialize)] 60 | struct Struct { 61 | name: String, 62 | some_value: u32, 63 | optional: Option, 64 | nested: Nested, 65 | enum_value: Enum, 66 | } 67 | 68 | // Prepare JSON input 69 | let value = (0..30) 70 | .map(|i| Struct { 71 | name: format!("some name {i}"), 72 | some_value: i * 256, 73 | optional: if i % 5 == 0 { 74 | None 75 | } else { 76 | Some(i as f64 / 123.0) 77 | }, 78 | nested: Nested { 79 | my_field: "abcd".repeat((5 + i % 10) as usize), 80 | another_one: i * i, 81 | }, 82 | enum_value: match i % 3 { 83 | 0 => Enum::VariantA, 84 | 1 => Enum::Other(i * 2), 85 | _ => Enum::AndOneMore { value: i % 2 == 0 }, 86 | }, 87 | }) 88 | .collect::>(); 89 | 90 | let json = serde_json::to_string(&value).unwrap(); 91 | bench_compare::>(c, "serde-deserialize-structs", &json); 92 | } 93 | 94 | criterion_group!( 95 | benches, 96 | // Benchmark functions 97 | benchmark_number_vec, 98 | benchmark_structs 99 | ); 100 | criterion_main!(benches); 101 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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::{JsonEvent, get_expected_events, get_test_data_file_path}; 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 | -------------------------------------------------------------------------------- /.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 | permissions: 31 | id-token: write # Required for OIDC token exchange for trusted publishing, see below 32 | steps: 33 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 34 | 35 | - name: Update project version 36 | shell: bash 37 | # https://github.com/killercup/cargo-edit 38 | run: | 39 | cargo install --no-default-features --features set-version cargo-edit@0.13.0 40 | cargo set-version --bump ${{ inputs.versionBump }} 41 | 42 | # There is currently no easy way to get the new version number (see also https://github.com/killercup/cargo-edit/issues/524), 43 | # so have to get it by other means 44 | - name: Get new version 45 | id: get-new-version 46 | shell: bash 47 | # See https://stackoverflow.com/a/75023425 48 | # and https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter 49 | run: | 50 | VERSION=$(cargo metadata --format-version=1 --no-deps | jq --compact-output --raw-output --exit-status '.packages[0].version') 51 | echo "New version: $VERSION" 52 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 53 | 54 | - name: Commit version update 55 | shell: bash 56 | run: | 57 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 58 | git config user.name "github-actions[bot]" 59 | git add . 60 | git commit -m "Release version ${{ steps.get-new-version.outputs.VERSION }}" 61 | git tag -a "v${{ steps.get-new-version.outputs.VERSION }}" -m "Release ${{ steps.get-new-version.outputs.VERSION }}" 62 | 63 | - name: Install cargo-make 64 | uses: taiki-e/install-action@7fe7b8c79a3c5569643cf27dfc32456883a4cd4d #v2.62.16 65 | with: 66 | tool: cargo-make@0.37.23 67 | 68 | # Perform full build to make sure there are no issues 69 | - name: Build project 70 | shell: bash 71 | run: cargo make 72 | 73 | # TODO: Once this project is more stable, maybe include SemVer checks, e.g. 74 | # https://github.com/rust-lang/rust-semverver or https://github.com/obi1kenobi/cargo-semver-checks 75 | 76 | # Clean up results from build to not affect publish in any way 77 | - name: Clean 78 | shell: bash 79 | run: cargo clean 80 | 81 | # Uses trusted publishing, see https://crates.io/docs/trusted-publishing 82 | - name: crates.io auth 83 | uses: rust-lang/crates-io-auth-action@b7e9a28eded4986ec6b1fa40eeee8f8f165559ec #v1.0.3 84 | id: auth 85 | - name: Publish 86 | shell: bash 87 | # TODO: Fail on any warnings (e.g. incorrect Cargo.toml values); not yet available, see https://github.com/rust-lang/cargo/issues/8424 88 | run: cargo publish --all-features 89 | env: 90 | CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} 91 | 92 | - name: Push Git changes 93 | shell: bash 94 | run: git push --follow-tags 95 | -------------------------------------------------------------------------------- /benches/reader_struct_benchmark.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, hint::black_box}; 2 | 3 | use criterion::{Criterion, criterion_group, criterion_main}; 4 | use serde::Deserialize; 5 | use serde_json::{ 6 | StreamDeserializer, 7 | de::{IoRead, Read, StrRead}, 8 | }; 9 | use struson::reader::{JsonReader, JsonStreamReader, ReaderSettings}; 10 | 11 | #[derive(Deserialize)] 12 | #[expect(dead_code, reason = "benchmark does 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 | .inspect(|r| { 90 | black_box(r); 91 | }) 92 | .count(); 93 | assert_eq!(COUNT, count); 94 | } 95 | 96 | group.bench_with_input("serde (reader)", &json, |b, json| { 97 | b.iter(|| serde_read(IoRead::new(json.as_bytes()))) 98 | }); 99 | group.bench_with_input("serde (string)", &json, |b, json| { 100 | b.iter(|| serde_read(StrRead::new(json))) 101 | }); 102 | 103 | group.finish(); 104 | } 105 | 106 | criterion_group!( 107 | benches, 108 | // Benchmark functions 109 | benchmark_struct 110 | ); 111 | criterion_main!(benches); 112 | -------------------------------------------------------------------------------- /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_core::ser::Serialize) 4 | //! types to be written to a [`JsonWriter`](crate::writer::JsonWriter) and [`Deserialize`](serde_core::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 | -------------------------------------------------------------------------------- /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/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 | #[expect(dead_code, reason = "field is skipped during serialization")] 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/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 | -------------------------------------------------------------------------------- /benches/writer_benchmark.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use criterion::{Criterion, criterion_group, criterion_main}; 4 | use serde::ser::Serializer; 5 | use struson::writer::{JsonStreamWriter, JsonWriter, WriterSettings}; 6 | 7 | fn bench_compare>) -> Result<(), Box>>( 8 | c: &mut Criterion, 9 | name: &str, 10 | include_pretty: bool, 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 writer = Vec::new(); 17 | let mut json_writer = JsonStreamWriter::new(&mut writer); 18 | write_function(&mut json_writer).unwrap(); 19 | json_writer.finish_document().unwrap(); 20 | writer 21 | }) 22 | }); 23 | if include_pretty { 24 | group.bench_with_input( 25 | "struson-write (pretty)", 26 | &struson_function, 27 | |b, write_function| { 28 | b.iter(|| { 29 | let mut writer = Vec::new(); 30 | let mut json_writer = JsonStreamWriter::new_custom( 31 | &mut writer, 32 | WriterSettings { 33 | pretty_print: true, 34 | ..Default::default() 35 | }, 36 | ); 37 | write_function(&mut json_writer).unwrap(); 38 | json_writer.finish_document().unwrap(); 39 | writer 40 | }) 41 | }, 42 | ); 43 | } 44 | 45 | // TODO: Maybe also try to support Serde, but Serializer API cannot be easily used for arbitrary data? 46 | // Could test against serde_json's Formatter, but that might be too low level (especially string value writing)? 47 | 48 | group.finish(); 49 | } 50 | 51 | fn benchmark_large_array(c: &mut Criterion) { 52 | bench_compare(c, "write-large-array", true, |json_writer| { 53 | json_writer.begin_array()?; 54 | 55 | for _ in 0..1000 { 56 | json_writer.bool_value(true)?; 57 | json_writer.number_value(123456)?; 58 | json_writer.fp_number_value(1234.56)?; 59 | json_writer.string_value("string value")?; 60 | } 61 | 62 | json_writer.end_array()?; 63 | 64 | Ok(()) 65 | }); 66 | } 67 | 68 | fn benchmark_nested_object(c: &mut Criterion) { 69 | bench_compare(c, "write-nested-object", true, |json_writer| { 70 | let count = 1000; 71 | 72 | for _ in 0..count { 73 | json_writer.begin_object()?; 74 | json_writer.name("member name")?; 75 | } 76 | 77 | json_writer.null_value()?; 78 | 79 | for _ in 0..count { 80 | json_writer.end_object()?; 81 | } 82 | 83 | Ok(()) 84 | }); 85 | } 86 | 87 | fn bench_compare_string_writing(c: &mut Criterion, name: &str, string_value: &str) { 88 | let mut group = c.benchmark_group(name); 89 | 90 | group.bench_with_input("struson", string_value, |b, string_value| { 91 | b.iter(|| { 92 | let mut writer = Vec::new(); 93 | let mut json_writer = JsonStreamWriter::new(&mut writer); 94 | json_writer.string_value(string_value).unwrap(); 95 | json_writer.finish_document().unwrap(); 96 | writer 97 | }) 98 | }); 99 | 100 | group.bench_with_input("serde", string_value, |b, string_value| { 101 | b.iter(|| { 102 | let mut writer = Vec::new(); 103 | let mut serializer = serde_json::ser::Serializer::new(&mut writer); 104 | serializer.serialize_str(string_value).unwrap(); 105 | writer 106 | }) 107 | }); 108 | 109 | group.finish(); 110 | } 111 | 112 | fn benchmark_large_ascii_string(c: &mut Criterion) { 113 | let string_value = "this is a test string".repeat(10_000); 114 | bench_compare( 115 | c, 116 | "write-large-ascii-string", 117 | // Pretty printing makes no difference since this is just one top-level value 118 | false, 119 | |json_writer| { 120 | json_writer.string_value(&string_value)?; 121 | 122 | Ok(()) 123 | }, 124 | ); 125 | bench_compare_string_writing( 126 | c, 127 | "write-large-ascii-string (string writing)", 128 | &string_value, 129 | ); 130 | } 131 | 132 | fn benchmark_large_unicode_string(c: &mut Criterion) { 133 | let string_value = "ab\u{0080}cd\u{0800}ef\u{1234}gh\u{10FFFF}".repeat(10_000); 134 | bench_compare( 135 | c, 136 | "write-large-unicode-string", 137 | // Pretty printing makes no difference since this is just one top-level value 138 | false, 139 | |json_writer| { 140 | json_writer.string_value(&string_value)?; 141 | 142 | Ok(()) 143 | }, 144 | ); 145 | bench_compare_string_writing( 146 | c, 147 | "write-large-unicode-string (string writing)", 148 | &string_value, 149 | ); 150 | } 151 | 152 | fn benchmark_escapes_string(c: &mut Criterion) { 153 | let string_value = r#"a\nb\tc\\d\"e\u0000f\u0080g\u0800h\u1234i\uD800\uDC00"#.repeat(10_000); 154 | bench_compare( 155 | c, 156 | "write-large-escapes-string", 157 | // Pretty printing makes no difference since this is just one top-level value 158 | false, 159 | |json_writer| { 160 | json_writer.string_value(&string_value)?; 161 | 162 | Ok(()) 163 | }, 164 | ); 165 | bench_compare_string_writing( 166 | c, 167 | "write-large-escapes-string (string writing)", 168 | &string_value, 169 | ); 170 | } 171 | 172 | criterion_group!( 173 | benches, 174 | // Benchmark functions 175 | benchmark_large_array, 176 | benchmark_nested_object, 177 | benchmark_large_ascii_string, 178 | benchmark_large_unicode_string, 179 | benchmark_escapes_string 180 | ); 181 | criterion_main!(benches); 182 | -------------------------------------------------------------------------------- /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!( 91 | "Unexpected: Invalid UTF-8 bytes detected, report this to the Struson maintainers: {e:?}; bytes: {bytes:02X?}" 92 | ) 93 | } 94 | } 95 | } 96 | 97 | /// Converts bytes to a `str`, possibly without validating that the bytes are valid UTF-8 data 98 | /// 99 | /// Must only be called if UTF-8 validation on the bytes has already been performed manually. 100 | pub(crate) fn to_str_unchecked(bytes: &[u8]) -> &str { 101 | debug_assert_valid_utf8(bytes); 102 | // TODO: Once confident enough that UTF-8 validation in this crate is correct, use `std::str::from_utf8_unchecked` instead 103 | std::str::from_utf8(bytes).unwrap() 104 | } 105 | 106 | /// Converts bytes to a `String`, possibly without validating that the bytes are valid UTF-8 data 107 | /// 108 | /// Must only be called if UTF-8 validation on the bytes has already been performed manually. 109 | pub(crate) fn to_string_unchecked(bytes: Vec) -> String { 110 | debug_assert_valid_utf8(&bytes); 111 | // TODO: Once confident enough that UTF-8 validation in this crate is correct, use `String::from_utf8_unchecked` instead 112 | String::from_utf8(bytes).unwrap() 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use super::*; 118 | use std::panic::UnwindSafe; 119 | 120 | #[must_use] // caller must perform assertion on panic message 121 | fn assert_panics(f: impl FnOnce() -> R + UnwindSafe) -> String { 122 | if let Err(panic_value) = std::panic::catch_unwind(f) { 123 | match panic_value.downcast::() { 124 | Ok(message) => *message, 125 | Err(panic_value) => { 126 | panic!("Panic value should have been a String, but is: {panic_value:?}") 127 | } 128 | } 129 | } else { 130 | panic!("Expression should have panicked"); 131 | } 132 | } 133 | 134 | #[cfg(debug_assertions)] // validation is only performed when debug assertions are enabled 135 | #[test] 136 | fn to_str_unchecked_invalid() { 137 | // Overlong UTF-8 encoding for two bytes 138 | let message = assert_panics(|| to_str_unchecked(b"\xC1\xBF")); 139 | // Check prefix and suffix but ignore the error message from Rust in the middle 140 | assert!( 141 | message.starts_with( 142 | "Unexpected: Invalid UTF-8 bytes detected, report this to the Struson maintainers: " 143 | ), 144 | "Unexpected prefix for message: {message}" 145 | ); 146 | assert!( 147 | message.ends_with("; bytes: [C1, BF]"), 148 | "Unexpected suffix for message: {message}" 149 | ); 150 | } 151 | 152 | #[cfg(debug_assertions)] // validation is only performed when debug assertions are enabled 153 | #[test] 154 | fn to_string_unchecked_invalid() { 155 | // Overlong UTF-8 encoding for two bytes 156 | let message = assert_panics(|| to_string_unchecked(b"\xC1\xBF".to_vec())); 157 | // Check prefix and suffix but ignore the error message from Rust in the middle 158 | assert!( 159 | message.starts_with( 160 | "Unexpected: Invalid UTF-8 bytes detected, report this to the Struson maintainers: " 161 | ), 162 | "Unexpected prefix for message: {message}" 163 | ); 164 | assert!( 165 | message.ends_with("; bytes: [C1, BF]"), 166 | "Unexpected suffix for message: {message}" 167 | ); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/serde_deserialize_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "serde")] 2 | 3 | use std::{collections::HashMap, fmt::Debug}; 4 | 5 | use serde::{Deserialize, de::DeserializeOwned}; 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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]\ 10 | > 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.\ 11 | > Feedback and suggestions for improvements are welcome! 12 | 13 | ## Why? 14 | 15 | 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. 16 | 17 | - 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.\ 18 | 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). 19 | 20 | - 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. 21 | 22 | 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. 23 | 24 | ## Main features 25 | 26 | - Low level streaming API, no implicit value conversion 27 | - Strong enforcement of correct API usage 28 | - Panics only for incorrect API usage\ 29 | Malformed JSON and unexpected JSON structure only causes errors 30 | - API does not require recursion for JSON arrays and objects\ 31 | Can theoretically read and write arbitrarily deeply nested JSON data 32 | - Read and write arbitrarily precise JSON numbers as string\ 33 | ([`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)) 34 | - Seek to specific location in JSON data ([`JsonReader::seek_to`](https://docs.rs/struson/latest/struson/reader/trait.JsonReader.html#tymethod.seek_to)) 35 | - 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)) 36 | - Read and write arbitrarily large JSON string values\ 37 | ([`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)) 38 | - Optional [Serde integration](#serde-integration) 39 | 40 | ## Usage examples 41 | 42 | Two variants of the API are provided: 43 | 44 | - simple: ensures correct API usage at compile-time 45 | - advanced: ensures correct API usage only at runtime (by panicking); more flexible and 46 | provides more functionality 47 | 48 | ### Simple API 49 | 50 | **🔬 Experimental**\ 51 | The simple API and its naming is currently experimental, please provide feedback [here](https://github.com/Marcono1234/struson/issues/34). 52 | It has to be enabled by specifying the `simple-api` feature in `Cargo.toml`: 53 | 54 | ```toml 55 | [dependencies] 56 | struson = { version = "...", features = ["simple-api"] } 57 | ``` 58 | 59 | Any feedback is appreciated! 60 | 61 | #### Reading 62 | 63 | See [`SimpleJsonReader`](https://docs.rs/struson/latest/struson/reader/simple/struct.SimpleJsonReader.html). 64 | 65 | ```rust 66 | use struson::reader::simple::*; 67 | 68 | // In this example JSON data comes from a string; 69 | // normally it would come from a file or a network connection 70 | let json_reader = SimpleJsonReader::new(r#"["a", "short", "example"]"#.as_bytes()); 71 | let mut words = Vec::::new(); 72 | json_reader.read_array_items(|item_reader| { 73 | let word = item_reader.read_string()?; 74 | words.push(word); 75 | Ok(()) 76 | })?; 77 | assert_eq!(words, vec!["a", "short", "example"]); 78 | ``` 79 | 80 | For reading nested values, the methods [`read_seeked`](https://docs.rs/struson/latest/struson/reader/simple/trait.ValueReader.html#tymethod.read_seeked) 81 | and [`read_seeked_multi`](https://docs.rs/struson/latest/struson/reader/simple/trait.ValueReader.html#tymethod.read_seeked_multi) 82 | can be used: 83 | 84 | ```rust 85 | use struson::reader::simple::*; 86 | use struson::reader::simple::multi_json_path::multi_json_path; 87 | 88 | // In this example JSON data comes from a string; 89 | // normally it would come from a file or a network connection 90 | let json = r#"{ 91 | "users": [ 92 | {"name": "John", "age": 32}, 93 | {"name": "Jane", "age": 41} 94 | ] 95 | }"#; 96 | let json_reader = SimpleJsonReader::new(json.as_bytes()); 97 | 98 | let mut ages = Vec::::new(); 99 | // Select the ages of all users 100 | let json_path = multi_json_path!["users", [*], "age"]; 101 | json_reader.read_seeked_multi(&json_path, false, |value_reader| { 102 | let age = value_reader.read_number()??; 103 | ages.push(age); 104 | Ok(()) 105 | })?; 106 | assert_eq!(ages, vec![32, 41]); 107 | ``` 108 | 109 | #### Writing 110 | 111 | See [`SimpleJsonWriter`](https://docs.rs/struson/latest/struson/writer/simple/struct.SimpleJsonWriter.html). 112 | 113 | ```rust 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 | 130 | ### Advanced API 131 | 132 | #### Reading 133 | 134 | See [`JsonStreamReader`](https://docs.rs/struson/latest/struson/reader/struct.JsonStreamReader.html). 135 | 136 | ```rust 137 | use struson::reader::*; 138 | 139 | // In this example JSON data comes from a string; 140 | // normally it would come from a file or a network connection 141 | let json = r#"{"a": [1, true]}"#; 142 | let mut json_reader = JsonStreamReader::new(json.as_bytes()); 143 | 144 | json_reader.begin_object()?; 145 | assert_eq!(json_reader.next_name()?, "a"); 146 | 147 | json_reader.begin_array()?; 148 | assert_eq!(json_reader.next_number::()??, 1); 149 | assert_eq!(json_reader.next_bool()?, true); 150 | json_reader.end_array()?; 151 | 152 | json_reader.end_object()?; 153 | // Ensures that there is no trailing data 154 | json_reader.consume_trailing_whitespace()?; 155 | ``` 156 | 157 | #### Writing 158 | 159 | See [`JsonStreamWriter`](https://docs.rs/struson/latest/struson/writer/struct.JsonStreamWriter.html). 160 | 161 | ```rust 162 | use struson::writer::*; 163 | 164 | // In this example JSON bytes are stored in a Vec; 165 | // normally they would be written to a file or network connection 166 | let mut writer = Vec::::new(); 167 | let mut json_writer = JsonStreamWriter::new(&mut writer); 168 | 169 | json_writer.begin_object()?; 170 | json_writer.name("a")?; 171 | 172 | json_writer.begin_array()?; 173 | json_writer.number_value(1)?; 174 | json_writer.bool_value(true)?; 175 | json_writer.end_array()?; 176 | 177 | json_writer.end_object()?; 178 | // Ensures that the JSON document is complete and flushes the buffer 179 | json_writer.finish_document()?; 180 | 181 | let json = String::from_utf8(writer)?; 182 | assert_eq!(json, r#"{"a":[1,true]}"#); 183 | ``` 184 | 185 | ## Serde integration 186 | 187 | 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. 188 | 189 | ## Changelog 190 | 191 | See [GitHub releases](https://github.com/Marcono1234/struson/releases). 192 | 193 | ## Building 194 | 195 | This project uses [cargo-make](https://github.com/sagiegurari/cargo-make) for building: 196 | 197 | ```sh 198 | cargo make 199 | ``` 200 | 201 | If you don't want to install cargo-make, you can instead manually run the tasks declared in the [`Makefile.toml`](Makefile.toml). 202 | 203 | ## Similar projects 204 | 205 | - 206 | > A streaming JSON parser/emitter library for rust 207 | - 208 | > JSON Tokenizer written in Rust 209 | - 210 | > JSON serialization/deserialization (full-featured, modern, streaming, direct into struct/enum) 211 | - 212 | 213 | > Rust re-implementation of the Python streaming JSON parser [ijson](https://github.com/isagalaev/ijson) 214 | - 215 | > A zero-copy json-lexer, filters and serializer. 216 | - 217 | > High-fidelity JSON lexer and parser 218 | - 's `justjson::parser::Tokenizer` 219 | > A JSON tokenizer, which converts JSON source to a series of Tokens 220 | - 221 | > Json pull parser 222 | - 223 | > Simple and fast crate for writing JSON to a string without creating intermediate objects 224 | - 225 | > A queryable, streaming, JSON pull-parser with low allocation overhead. 226 | - 227 | > Actson is a low-level JSON parser for reactive applications and non-blocking I/O. 228 | - [rustc-serialize `Parser`](https://docs.rs/rustc-serialize/latest/rustc_serialize/json/struct.Parser.html) (deprecated) 229 | > A streaming JSON parser implemented as an iterator of JsonEvent, consuming an iterator of char. 230 | 231 | ## License 232 | 233 | Licensed under either of 234 | 235 | - [Apache License, Version 2.0](LICENSE-APACHE) 236 | - [MIT License](LICENSE-MIT) 237 | 238 | at your option. 239 | 240 | All contributions you make to this project are licensed implicitly under both licenses mentioned above, without any additional terms or conditions. 241 | 242 | 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). 243 | -------------------------------------------------------------------------------- /benches/reader_benchmark.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, hint::black_box}; 2 | 3 | use criterion::{Criterion, criterion_group, criterion_main}; 4 | use serde::{Deserializer, de::Visitor}; 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, include_no_path_tracking: bool) { 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 | if include_no_path_tracking { 31 | group.bench_with_input("struson-skip (no path tracking)", json, |b, json| { 32 | b.iter(|| { 33 | call_unwrap(|| { 34 | let mut json_reader = JsonStreamReader::new_custom( 35 | json.as_bytes(), 36 | ReaderSettings { 37 | track_path: false, 38 | max_nesting_depth: None, 39 | ..Default::default() 40 | }, 41 | ); 42 | json_reader.skip_value()?; 43 | json_reader.consume_trailing_whitespace()?; 44 | Ok(()) 45 | }); 46 | }) 47 | }); 48 | } 49 | 50 | fn struson_read( 51 | mut json_reader: JsonStreamReader, 52 | ) -> Result<(), Box> { 53 | enum StackValue { 54 | Array, 55 | Object, 56 | } 57 | 58 | let mut stack = Vec::new(); 59 | loop { 60 | if !stack.is_empty() { 61 | match stack.last().unwrap() { 62 | StackValue::Array => { 63 | if !json_reader.has_next()? { 64 | stack.pop(); 65 | json_reader.end_array()?; 66 | 67 | if stack.is_empty() { 68 | break; 69 | } else { 70 | continue; 71 | } 72 | } 73 | } 74 | StackValue::Object => { 75 | if json_reader.has_next()? { 76 | black_box(json_reader.next_name()?); 77 | // fall through to value reading 78 | } else { 79 | stack.pop(); 80 | json_reader.end_object()?; 81 | 82 | if stack.is_empty() { 83 | break; 84 | } else { 85 | continue; 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | match json_reader.peek()? { 93 | ValueType::Array => { 94 | json_reader.begin_array()?; 95 | stack.push(StackValue::Array) 96 | } 97 | ValueType::Object => { 98 | json_reader.begin_object()?; 99 | stack.push(StackValue::Object) 100 | } 101 | ValueType::String => { 102 | black_box(json_reader.next_str()?); 103 | } 104 | ValueType::Number => { 105 | black_box(json_reader.next_number_as_str()?); 106 | } 107 | ValueType::Boolean => { 108 | black_box(json_reader.next_bool()?); 109 | } 110 | ValueType::Null => json_reader.next_null()?, 111 | } 112 | 113 | if stack.is_empty() { 114 | break; 115 | } 116 | } 117 | json_reader.consume_trailing_whitespace()?; 118 | Ok(()) 119 | } 120 | 121 | group.bench_with_input("struson-read", json, |b, json| { 122 | b.iter(|| { 123 | call_unwrap(|| { 124 | let json_reader = JsonStreamReader::new_custom( 125 | json.as_bytes(), 126 | ReaderSettings { 127 | max_nesting_depth: None, 128 | ..Default::default() 129 | }, 130 | ); 131 | struson_read(json_reader) 132 | }); 133 | }) 134 | }); 135 | 136 | if include_no_path_tracking { 137 | group.bench_with_input("struson-read (no path tracking)", json, |b, json| { 138 | b.iter(|| { 139 | call_unwrap(|| { 140 | let json_reader = JsonStreamReader::new_custom( 141 | json.as_bytes(), 142 | ReaderSettings { 143 | track_path: false, 144 | max_nesting_depth: None, 145 | ..Default::default() 146 | }, 147 | ); 148 | struson_read(json_reader) 149 | }); 150 | }) 151 | }); 152 | } 153 | 154 | fn serde_skip<'a, R: Read<'a>>(read: R) { 155 | struct UnitVisitor; 156 | 157 | impl Visitor<'_> for UnitVisitor { 158 | type Value = (); 159 | 160 | fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 161 | write!(formatter, "unit") 162 | } 163 | 164 | fn visit_unit(self) -> Result { 165 | Ok(()) 166 | } 167 | } 168 | 169 | let mut deserializer = serde_json::de::Deserializer::new(read); 170 | // TODO: Is this correct? 171 | deserializer.deserialize_ignored_any(UnitVisitor).unwrap(); 172 | deserializer.end().unwrap(); 173 | } 174 | 175 | group.bench_with_input("serde-skip (reader)", json, |b, json| { 176 | b.iter(|| { 177 | serde_skip(IoRead::new(json.as_bytes())); 178 | }) 179 | }); 180 | 181 | group.bench_with_input("serde-skip (string)", json, |b, json| { 182 | b.iter(|| { 183 | serde_skip(StrRead::new(json)); 184 | }) 185 | }); 186 | 187 | // TODO: Is there a way to also write a serde_json benchmark which just reads the JSON values, 188 | // without deserializing them into something? 189 | 190 | group.finish(); 191 | } 192 | 193 | fn benchmark_large_array(c: &mut Criterion) { 194 | let json = format!( 195 | "[{}true]", 196 | "true, false, null, 12345689.123e12, \"abcdabcdabcdabcd\",".repeat(1000) 197 | ); 198 | bench_compare(c, "read-large-array", &json, true); 199 | } 200 | 201 | fn benchmark_nested_object(c: &mut Criterion) { 202 | let count = 1000; 203 | let json = r#"{"member name":"#.repeat(count) + "true" + "}".repeat(count).as_str(); 204 | bench_compare(c, "read-nested-object", &json, true); 205 | } 206 | 207 | fn benchmark_nested_object_pretty(c: &mut Criterion) { 208 | let count = 1000; 209 | let mut json = "{".to_owned(); 210 | 211 | for i in 1..=count { 212 | json.push('\n'); 213 | json.push_str(" ".repeat(i).as_str()); 214 | json.push_str(r#""member name": {"#); 215 | } 216 | for i in (0..=count).rev() { 217 | json.push('\n'); 218 | json.push_str(" ".repeat(i).as_str()); 219 | json.push('}'); 220 | } 221 | 222 | bench_compare(c, "read-nested-object-pretty", &json, true); 223 | } 224 | 225 | fn bench_compare_string_reading(c: &mut Criterion, name: &str, json: &str) { 226 | let mut group = c.benchmark_group(name); 227 | 228 | group.bench_with_input("struson", json, |b, json| { 229 | b.iter(|| { 230 | call_unwrap(|| { 231 | let mut json_reader = JsonStreamReader::new(json.as_bytes()); 232 | black_box(json_reader.next_str()?); 233 | json_reader.consume_trailing_whitespace()?; 234 | 235 | Ok(()) 236 | }); 237 | }) 238 | }); 239 | 240 | struct StringVisitor; 241 | 242 | impl<'de> Visitor<'de> for StringVisitor { 243 | type Value = (); 244 | 245 | fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 246 | write!(formatter, "a string") 247 | } 248 | 249 | fn visit_borrowed_str(self, v: &'de str) -> Result { 250 | black_box(v); 251 | Ok(()) 252 | } 253 | 254 | fn visit_str(self, v: &str) -> Result { 255 | black_box(v); 256 | Ok(()) 257 | } 258 | 259 | fn visit_string(self, v: String) -> Result { 260 | black_box(v); 261 | Ok(()) 262 | } 263 | } 264 | 265 | fn serde_read<'a, R: Read<'a>, F: FnOnce(&mut serde_json::de::Deserializer)>( 266 | read: R, 267 | read_function: F, 268 | ) { 269 | let mut deserializer = serde_json::de::Deserializer::new(read); 270 | read_function(&mut deserializer); 271 | deserializer.end().unwrap(); 272 | } 273 | 274 | // TODO: Are really all of these Serde benchmarks necessary? 275 | group.bench_with_input("serde-str (reader)", json, |b, json| { 276 | b.iter(|| { 277 | serde_read(IoRead::new(json.as_bytes()), |deserializer| { 278 | deserializer.deserialize_str(StringVisitor).unwrap() 279 | }); 280 | }) 281 | }); 282 | 283 | group.bench_with_input("serde-string (reader)", json, |b, json| { 284 | b.iter(|| { 285 | serde_read(IoRead::new(json.as_bytes()), |deserializer| { 286 | deserializer.deserialize_string(StringVisitor).unwrap() 287 | }); 288 | }) 289 | }); 290 | 291 | group.bench_with_input("serde-str (string)", json, |b, json| { 292 | b.iter(|| { 293 | serde_read(StrRead::new(json), |deserializer| { 294 | deserializer.deserialize_str(StringVisitor).unwrap() 295 | }); 296 | }) 297 | }); 298 | 299 | group.bench_with_input("serde-string (string)", json, |b, json| { 300 | b.iter(|| { 301 | serde_read(StrRead::new(json), |deserializer| { 302 | deserializer.deserialize_string(StringVisitor).unwrap() 303 | }); 304 | }) 305 | }); 306 | 307 | group.finish(); 308 | } 309 | 310 | fn benchmark_large_ascii_string(c: &mut Criterion) { 311 | let json = format!("\"{}\"", "this is a test string".repeat(10_000)); 312 | bench_compare( 313 | c, 314 | "read-large-ascii-string", 315 | &json, 316 | // Path tracking makes no difference since this is just one top-level value 317 | false, 318 | ); 319 | bench_compare_string_reading(c, "read-large-ascii-string (string reading)", &json); 320 | } 321 | 322 | fn benchmark_large_unicode_string(c: &mut Criterion) { 323 | let json = format!( 324 | "\"{}\"", 325 | "ab\u{0080}cd\u{0800}ef\u{1234}gh\u{10FFFF}".repeat(10_000) 326 | ); 327 | bench_compare( 328 | c, 329 | "read-large-unicode-string", 330 | &json, 331 | // Path tracking makes no difference since this is just one top-level value 332 | false, 333 | ); 334 | bench_compare_string_reading(c, "read-large-unicode-string (string reading)", &json); 335 | } 336 | 337 | fn benchmark_escapes_string(c: &mut Criterion) { 338 | let json = format!( 339 | "\"{}\"", 340 | r#"a\nb\tc\\d\"e\u0000f\u0080g\u0800h\u1234i\uD800\uDC00"#.repeat(10_000) 341 | ); 342 | bench_compare( 343 | c, 344 | "read-large-escapes-string", 345 | &json, 346 | // Path tracking makes no difference since this is just one top-level value 347 | false, 348 | ); 349 | bench_compare_string_reading(c, "read-large-escapes-string (string reading)", &json); 350 | } 351 | 352 | criterion_group!( 353 | benches, 354 | // Benchmark functions 355 | benchmark_large_array, 356 | benchmark_nested_object, 357 | benchmark_nested_object_pretty, 358 | benchmark_large_ascii_string, 359 | benchmark_large_unicode_string, 360 | benchmark_escapes_string 361 | ); 362 | criterion_main!(benches); 363 | -------------------------------------------------------------------------------- /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!( 132 | "Incorrect writer usage: Member name has already been written; expecting value" 133 | ); 134 | } 135 | self.pending_name = Some(name.to_owned()); 136 | Ok(()) 137 | } else { 138 | panic!("Incorrect writer usage: Cannot write name; not inside object"); 139 | } 140 | } 141 | 142 | fn null_value(&mut self) -> Result<(), IoError> { 143 | self.check_before_value(); 144 | self.add_value(Value::Null); 145 | Ok(()) 146 | } 147 | 148 | fn bool_value(&mut self, value: bool) -> Result<(), IoError> { 149 | self.check_before_value(); 150 | self.add_value(Value::Bool(value)); 151 | Ok(()) 152 | } 153 | 154 | fn string_value(&mut self, value: &str) -> Result<(), IoError> { 155 | self.check_before_value(); 156 | self.add_value(Value::String(value.to_owned())); 157 | Ok(()) 158 | } 159 | 160 | fn string_value_writer(&mut self) -> Result { 161 | self.check_before_value(); 162 | self.is_string_value_writer_active = true; 163 | Ok(StringValueWriterImpl { 164 | buf: Vec::new(), 165 | json_writer: self, 166 | }) 167 | } 168 | 169 | fn number_value_from_string(&mut self, value: &str) -> Result<(), JsonNumberError> { 170 | self.check_before_value(); 171 | // TODO: `parse::` might not match JSON number string format (might allow more / less than allowed by JSON)? 172 | let f = value 173 | .parse::() 174 | .map_err(|e| JsonNumberError::InvalidNumber(e.to_string()))?; 175 | self.add_value(Value::Number(serde_number_from_f64(f)?)); 176 | Ok(()) 177 | } 178 | 179 | fn number_value(&mut self, value: N) -> Result<(), IoError> { 180 | let number = value 181 | .as_u64() 182 | .map(Number::from) 183 | .or_else(|| value.as_i64().map(Number::from)); 184 | 185 | if let Some(n) = number { 186 | self.check_before_value(); 187 | self.add_value(Value::Number(n)); 188 | Ok(()) 189 | } else { 190 | value.use_json_number(|number_str| { 191 | self.number_value_from_string(number_str) 192 | .map_err(|e| match e { 193 | JsonNumberError::InvalidNumber(e) => { 194 | panic!( 195 | "Unexpected: Writer rejected finite number '{number_str}': {e}" 196 | ) 197 | } 198 | JsonNumberError::IoError(e) => IoError::other(e), 199 | }) 200 | }) 201 | } 202 | } 203 | 204 | fn fp_number_value( 205 | &mut self, 206 | value: N, 207 | ) -> Result<(), JsonNumberError> { 208 | let number = if let Some(n) = value.as_f64() { 209 | Some(serde_number_from_f64(n)?) 210 | } else { 211 | None 212 | }; 213 | 214 | if let Some(n) = number { 215 | self.check_before_value(); 216 | self.add_value(Value::Number(n)); 217 | Ok(()) 218 | } else { 219 | // TODO: Cannot match over possible implementations? Therefore have to use string representation 220 | value.use_json_number(|number_str| { 221 | self.number_value_from_string(number_str).map_err(|e| { 222 | match e { 223 | // `use_json_number` should have verified that value is valid finite JSON number 224 | JsonNumberError::InvalidNumber(e) => { 225 | panic!( 226 | "Unexpected: Writer rejected finite number '{number_str}': {e}" 227 | ) 228 | } 229 | JsonNumberError::IoError(e) => IoError::other(e), 230 | } 231 | }) 232 | }) 233 | } 234 | } 235 | 236 | #[cfg(feature = "serde")] 237 | fn serialize_value( 238 | &mut self, 239 | value: &S, 240 | ) -> Result<(), struson::serde::SerializerError> { 241 | self.check_before_value(); 242 | let mut serializer = struson::serde::JsonWriterSerializer::new(self); 243 | value.serialize(&mut serializer) 244 | // TODO: Verify that value was properly serialized (only single value; no incomplete array or object) 245 | // might not be necessary because Serde's Serialize API enforces this 246 | } 247 | 248 | fn finish_document(self) -> Result { 249 | self.verify_string_writer_inactive(); 250 | if let Some(value) = self.final_value { 251 | Ok(value) 252 | } else { 253 | panic!("Incorrect writer usage: Top-level value is incomplete") 254 | } 255 | } 256 | } 257 | 258 | struct StringValueWriterImpl<'j> { 259 | buf: Vec, 260 | json_writer: &'j mut JsonValueWriter, 261 | } 262 | impl Write for StringValueWriterImpl<'_> { 263 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 264 | self.buf.extend_from_slice(buf); 265 | Ok(buf.len()) 266 | } 267 | 268 | fn flush(&mut self) -> std::io::Result<()> { 269 | // No-op 270 | Ok(()) 271 | } 272 | } 273 | impl StringValueWriter for StringValueWriterImpl<'_> { 274 | fn finish_value(self) -> Result<(), IoError> { 275 | let string = 276 | String::from_utf8(self.buf).map_err(|e| IoError::new(ErrorKind::InvalidData, e))?; 277 | self.json_writer.add_value(Value::String(string)); 278 | self.json_writer.is_string_value_writer_active = false; 279 | Ok(()) 280 | } 281 | } 282 | } 283 | 284 | #[test] 285 | fn write() -> Result<(), Box> { 286 | fn assert_invalid_number(expected_message: Option<&str>, result: Result<(), JsonNumberError>) { 287 | match result { 288 | Err(JsonNumberError::InvalidNumber(message)) => { 289 | if let Some(expected_message) = expected_message { 290 | assert_eq!(expected_message, message) 291 | } 292 | } 293 | _ => panic!("Unexpected result: {result:?}"), 294 | } 295 | } 296 | 297 | let mut json_writer = JsonValueWriter::new(); 298 | 299 | json_writer.begin_array()?; 300 | 301 | json_writer.begin_object()?; 302 | json_writer.name("name1")?; 303 | json_writer.begin_array()?; 304 | json_writer.bool_value(true)?; 305 | json_writer.end_array()?; 306 | json_writer.name("name2")?; 307 | json_writer.bool_value(false)?; 308 | json_writer.end_object()?; 309 | 310 | json_writer.null_value()?; 311 | json_writer.bool_value(true)?; 312 | json_writer.bool_value(false)?; 313 | json_writer.string_value("string")?; 314 | 315 | let mut string_writer = json_writer.string_value_writer()?; 316 | string_writer.write_all("first ".as_bytes())?; 317 | string_writer.write_all("second".as_bytes())?; 318 | string_writer.finish_value()?; 319 | 320 | json_writer.number_value_from_string("123")?; 321 | assert_invalid_number( 322 | Some(&format!("non-finite number: {}", f64::INFINITY)), 323 | json_writer.number_value_from_string("Infinity"), 324 | ); 325 | // Don't check for exact error message because it is created by Rust and might change in the future 326 | assert_invalid_number(None, json_writer.number_value_from_string("test")); 327 | json_writer.number_value(45)?; 328 | json_writer.number_value(-67)?; 329 | json_writer.fp_number_value(8.9)?; 330 | assert_invalid_number( 331 | Some(&format!("non-finite number: {}", f64::INFINITY)), 332 | json_writer.fp_number_value(f64::INFINITY), 333 | ); 334 | 335 | json_writer.end_array()?; 336 | let written_value = json_writer.finish_document()?; 337 | 338 | let expected_json = json!([ 339 | { 340 | "name1": [true], 341 | "name2": false, 342 | }, 343 | null, 344 | true, 345 | false, 346 | "string", 347 | "first second", 348 | // Current number from string implementation always writes f64 349 | 123.0, 350 | 45, 351 | -67, 352 | 8.9, 353 | ]); 354 | assert_eq!(expected_json, written_value); 355 | 356 | Ok(()) 357 | } 358 | 359 | #[test] 360 | fn transfer() -> Result<(), Box> { 361 | let mut json_writer = JsonValueWriter::new(); 362 | 363 | let mut json_reader = JsonStreamReader::new( 364 | "[true, 123, {\"name1\": \"value1\", \"name2\": null}, false]".as_bytes(), 365 | ); 366 | json_reader.transfer_to(&mut json_writer)?; 367 | json_reader.consume_trailing_whitespace()?; 368 | 369 | let written_value = json_writer.finish_document()?; 370 | 371 | let expected_json = json!([ 372 | true, 373 | // Current number from string implementation always writes f64 374 | 123.0, 375 | { 376 | "name1": "value1", 377 | "name2": null, 378 | }, 379 | false, 380 | ]); 381 | assert_eq!(expected_json, written_value); 382 | 383 | Ok(()) 384 | } 385 | 386 | #[cfg(feature = "serde")] 387 | #[test] 388 | fn serialize() -> Result<(), Box> { 389 | use serde::Serialize; 390 | 391 | #[derive(Serialize)] 392 | struct CustomStruct { 393 | a: u32, 394 | b: &'static str, 395 | } 396 | 397 | let mut json_writer = JsonValueWriter::new(); 398 | json_writer.serialize_value(&CustomStruct { a: 123, b: "test" })?; 399 | let written_value = json_writer.finish_document()?; 400 | 401 | let expected_json = json!({ 402 | "a": 123, 403 | "b": "test", 404 | }); 405 | assert_eq!(expected_json, written_value); 406 | 407 | Ok(()) 408 | } 409 | -------------------------------------------------------------------------------- /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::{ErrorKind, Sink, Write, sink}, 12 | }; 13 | 14 | use struson::writer::{ 15 | JsonStreamWriter, 16 | simple::{SimpleJsonWriter, ValueWriter}, 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 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 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.11" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 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 = "bumpalo" 72 | version = "3.14.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 75 | 76 | [[package]] 77 | name = "cast" 78 | version = "0.3.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 81 | 82 | [[package]] 83 | name = "cc" 84 | version = "1.0.83" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 87 | dependencies = [ 88 | "libc", 89 | ] 90 | 91 | [[package]] 92 | name = "cfg-if" 93 | version = "1.0.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 96 | 97 | [[package]] 98 | name = "ciborium" 99 | version = "0.2.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" 102 | dependencies = [ 103 | "ciborium-io", 104 | "ciborium-ll", 105 | "serde", 106 | ] 107 | 108 | [[package]] 109 | name = "ciborium-io" 110 | version = "0.2.1" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" 113 | 114 | [[package]] 115 | name = "ciborium-ll" 116 | version = "0.2.1" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" 119 | dependencies = [ 120 | "ciborium-io", 121 | "half", 122 | ] 123 | 124 | [[package]] 125 | name = "clap" 126 | version = "4.5.40" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" 129 | dependencies = [ 130 | "clap_builder", 131 | ] 132 | 133 | [[package]] 134 | name = "clap_builder" 135 | version = "4.5.40" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" 138 | dependencies = [ 139 | "anstyle", 140 | "clap_lex", 141 | ] 142 | 143 | [[package]] 144 | name = "clap_lex" 145 | version = "0.7.5" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 148 | 149 | [[package]] 150 | name = "criterion" 151 | version = "0.7.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" 154 | dependencies = [ 155 | "anes", 156 | "cast", 157 | "ciborium", 158 | "clap", 159 | "criterion-plot", 160 | "itertools", 161 | "num-traits", 162 | "oorandom", 163 | "plotters", 164 | "rayon", 165 | "regex", 166 | "serde", 167 | "serde_json", 168 | "tinytemplate", 169 | "walkdir", 170 | ] 171 | 172 | [[package]] 173 | name = "criterion-plot" 174 | version = "0.6.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" 177 | dependencies = [ 178 | "cast", 179 | "itertools", 180 | ] 181 | 182 | [[package]] 183 | name = "crossbeam-deque" 184 | version = "0.8.3" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" 187 | dependencies = [ 188 | "cfg-if", 189 | "crossbeam-epoch", 190 | "crossbeam-utils", 191 | ] 192 | 193 | [[package]] 194 | name = "crossbeam-epoch" 195 | version = "0.9.15" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" 198 | dependencies = [ 199 | "autocfg", 200 | "cfg-if", 201 | "crossbeam-utils", 202 | "memoffset", 203 | "scopeguard", 204 | ] 205 | 206 | [[package]] 207 | name = "crossbeam-utils" 208 | version = "0.8.16" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" 211 | dependencies = [ 212 | "cfg-if", 213 | ] 214 | 215 | [[package]] 216 | name = "duplicate" 217 | version = "2.0.1" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24" 220 | dependencies = [ 221 | "heck", 222 | "proc-macro2", 223 | "proc-macro2-diagnostics", 224 | ] 225 | 226 | [[package]] 227 | name = "either" 228 | version = "1.9.0" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 231 | 232 | [[package]] 233 | name = "gimli" 234 | version = "0.28.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 237 | 238 | [[package]] 239 | name = "half" 240 | version = "1.8.2" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" 243 | 244 | [[package]] 245 | name = "heck" 246 | version = "0.5.0" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 249 | 250 | [[package]] 251 | name = "itertools" 252 | version = "0.13.0" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 255 | dependencies = [ 256 | "either", 257 | ] 258 | 259 | [[package]] 260 | name = "itoa" 261 | version = "1.0.9" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 264 | 265 | [[package]] 266 | name = "js-sys" 267 | version = "0.3.64" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" 270 | dependencies = [ 271 | "wasm-bindgen", 272 | ] 273 | 274 | [[package]] 275 | name = "libc" 276 | version = "0.2.148" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" 279 | 280 | [[package]] 281 | name = "log" 282 | version = "0.4.20" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 285 | 286 | [[package]] 287 | name = "memchr" 288 | version = "2.6.3" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 291 | 292 | [[package]] 293 | name = "memoffset" 294 | version = "0.9.0" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" 297 | dependencies = [ 298 | "autocfg", 299 | ] 300 | 301 | [[package]] 302 | name = "miniz_oxide" 303 | version = "0.7.1" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 306 | dependencies = [ 307 | "adler", 308 | ] 309 | 310 | [[package]] 311 | name = "num-traits" 312 | version = "0.2.16" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" 315 | dependencies = [ 316 | "autocfg", 317 | ] 318 | 319 | [[package]] 320 | name = "object" 321 | version = "0.32.1" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 324 | dependencies = [ 325 | "memchr", 326 | ] 327 | 328 | [[package]] 329 | name = "once_cell" 330 | version = "1.18.0" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 333 | 334 | [[package]] 335 | name = "oorandom" 336 | version = "11.1.3" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" 339 | 340 | [[package]] 341 | name = "plotters" 342 | version = "0.3.5" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" 345 | dependencies = [ 346 | "num-traits", 347 | "plotters-backend", 348 | "plotters-svg", 349 | "wasm-bindgen", 350 | "web-sys", 351 | ] 352 | 353 | [[package]] 354 | name = "plotters-backend" 355 | version = "0.3.5" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" 358 | 359 | [[package]] 360 | name = "plotters-svg" 361 | version = "0.3.5" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" 364 | dependencies = [ 365 | "plotters-backend", 366 | ] 367 | 368 | [[package]] 369 | name = "proc-macro2" 370 | version = "1.0.92" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 373 | dependencies = [ 374 | "unicode-ident", 375 | ] 376 | 377 | [[package]] 378 | name = "proc-macro2-diagnostics" 379 | version = "0.10.1" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" 382 | dependencies = [ 383 | "proc-macro2", 384 | "quote", 385 | "syn", 386 | "version_check", 387 | ] 388 | 389 | [[package]] 390 | name = "quote" 391 | version = "1.0.35" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 394 | dependencies = [ 395 | "proc-macro2", 396 | ] 397 | 398 | [[package]] 399 | name = "rayon" 400 | version = "1.8.0" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" 403 | dependencies = [ 404 | "either", 405 | "rayon-core", 406 | ] 407 | 408 | [[package]] 409 | name = "rayon-core" 410 | version = "1.12.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" 413 | dependencies = [ 414 | "crossbeam-deque", 415 | "crossbeam-utils", 416 | ] 417 | 418 | [[package]] 419 | name = "regex" 420 | version = "1.9.5" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" 423 | dependencies = [ 424 | "aho-corasick", 425 | "memchr", 426 | "regex-automata", 427 | "regex-syntax", 428 | ] 429 | 430 | [[package]] 431 | name = "regex-automata" 432 | version = "0.3.8" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" 435 | dependencies = [ 436 | "aho-corasick", 437 | "memchr", 438 | "regex-syntax", 439 | ] 440 | 441 | [[package]] 442 | name = "regex-syntax" 443 | version = "0.7.5" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" 446 | 447 | [[package]] 448 | name = "rustc-demangle" 449 | version = "0.1.23" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 452 | 453 | [[package]] 454 | name = "rustversion" 455 | version = "1.0.14" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 458 | 459 | [[package]] 460 | name = "ryu" 461 | version = "1.0.15" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 464 | 465 | [[package]] 466 | name = "same-file" 467 | version = "1.0.6" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 470 | dependencies = [ 471 | "winapi-util", 472 | ] 473 | 474 | [[package]] 475 | name = "scopeguard" 476 | version = "1.2.0" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 479 | 480 | [[package]] 481 | name = "serde" 482 | version = "1.0.228" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 485 | dependencies = [ 486 | "serde_core", 487 | "serde_derive", 488 | ] 489 | 490 | [[package]] 491 | name = "serde_core" 492 | version = "1.0.228" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 495 | dependencies = [ 496 | "serde_derive", 497 | ] 498 | 499 | [[package]] 500 | name = "serde_derive" 501 | version = "1.0.228" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 504 | dependencies = [ 505 | "proc-macro2", 506 | "quote", 507 | "syn", 508 | ] 509 | 510 | [[package]] 511 | name = "serde_json" 512 | version = "1.0.145" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 515 | dependencies = [ 516 | "itoa", 517 | "memchr", 518 | "ryu", 519 | "serde", 520 | "serde_core", 521 | ] 522 | 523 | [[package]] 524 | name = "strum" 525 | version = "0.27.2" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" 528 | dependencies = [ 529 | "strum_macros", 530 | ] 531 | 532 | [[package]] 533 | name = "strum_macros" 534 | version = "0.27.1" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" 537 | dependencies = [ 538 | "heck", 539 | "proc-macro2", 540 | "quote", 541 | "rustversion", 542 | "syn", 543 | ] 544 | 545 | [[package]] 546 | name = "struson" 547 | version = "0.6.0" 548 | dependencies = [ 549 | "assert_no_alloc", 550 | "criterion", 551 | "duplicate", 552 | "serde", 553 | "serde_core", 554 | "serde_json", 555 | "strum", 556 | "thiserror", 557 | ] 558 | 559 | [[package]] 560 | name = "syn" 561 | version = "2.0.90" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" 564 | dependencies = [ 565 | "proc-macro2", 566 | "quote", 567 | "unicode-ident", 568 | ] 569 | 570 | [[package]] 571 | name = "thiserror" 572 | version = "2.0.17" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 575 | dependencies = [ 576 | "thiserror-impl", 577 | ] 578 | 579 | [[package]] 580 | name = "thiserror-impl" 581 | version = "2.0.17" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 584 | dependencies = [ 585 | "proc-macro2", 586 | "quote", 587 | "syn", 588 | ] 589 | 590 | [[package]] 591 | name = "tinytemplate" 592 | version = "1.2.1" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 595 | dependencies = [ 596 | "serde", 597 | "serde_json", 598 | ] 599 | 600 | [[package]] 601 | name = "unicode-ident" 602 | version = "1.0.12" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 605 | 606 | [[package]] 607 | name = "version_check" 608 | version = "0.9.4" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 611 | 612 | [[package]] 613 | name = "walkdir" 614 | version = "2.4.0" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 617 | dependencies = [ 618 | "same-file", 619 | "winapi-util", 620 | ] 621 | 622 | [[package]] 623 | name = "wasm-bindgen" 624 | version = "0.2.87" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" 627 | dependencies = [ 628 | "cfg-if", 629 | "wasm-bindgen-macro", 630 | ] 631 | 632 | [[package]] 633 | name = "wasm-bindgen-backend" 634 | version = "0.2.87" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" 637 | dependencies = [ 638 | "bumpalo", 639 | "log", 640 | "once_cell", 641 | "proc-macro2", 642 | "quote", 643 | "syn", 644 | "wasm-bindgen-shared", 645 | ] 646 | 647 | [[package]] 648 | name = "wasm-bindgen-macro" 649 | version = "0.2.87" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" 652 | dependencies = [ 653 | "quote", 654 | "wasm-bindgen-macro-support", 655 | ] 656 | 657 | [[package]] 658 | name = "wasm-bindgen-macro-support" 659 | version = "0.2.87" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" 662 | dependencies = [ 663 | "proc-macro2", 664 | "quote", 665 | "syn", 666 | "wasm-bindgen-backend", 667 | "wasm-bindgen-shared", 668 | ] 669 | 670 | [[package]] 671 | name = "wasm-bindgen-shared" 672 | version = "0.2.87" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" 675 | 676 | [[package]] 677 | name = "web-sys" 678 | version = "0.3.64" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" 681 | dependencies = [ 682 | "js-sys", 683 | "wasm-bindgen", 684 | ] 685 | 686 | [[package]] 687 | name = "winapi" 688 | version = "0.3.9" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 691 | dependencies = [ 692 | "winapi-i686-pc-windows-gnu", 693 | "winapi-x86_64-pc-windows-gnu", 694 | ] 695 | 696 | [[package]] 697 | name = "winapi-i686-pc-windows-gnu" 698 | version = "0.4.0" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 701 | 702 | [[package]] 703 | name = "winapi-util" 704 | version = "0.1.6" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 707 | dependencies = [ 708 | "winapi", 709 | ] 710 | 711 | [[package]] 712 | name = "winapi-x86_64-pc-windows-gnu" 713 | version = "0.4.0" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 716 | -------------------------------------------------------------------------------- /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 | #[expect(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 | #[test] 441 | fn test() { 442 | #[derive(Debug, Deserialize, Clone, PartialEq)] 443 | #[serde(default)] 444 | struct Outer { 445 | a: u32, 446 | b: bool, 447 | c: Option, 448 | d: Vec, 449 | } 450 | impl Default for Outer { 451 | fn default() -> Self { 452 | Self { 453 | a: Default::default(), 454 | b: Default::default(), 455 | c: Some(1), // Use something other than `None` to test JSON null handling 456 | d: Default::default(), 457 | } 458 | } 459 | } 460 | 461 | #[derive(Debug, Default, Deserialize, Clone, PartialEq)] 462 | #[serde(default)] 463 | struct Inner { 464 | e: String, 465 | f: f32, 466 | } 467 | 468 | let full_json = r#"{"a":2,"b":true,"c":null,"d":[{"e":"str\"","f":1.2e3}]}"#; 469 | let mut json = String::new(); 470 | let mut outer = Outer::default(); 471 | 472 | // Test handling of empty JSON 473 | let result = deserialize_partial!("".as_bytes(), |d| Outer::deserialize_in_place( 474 | d, &mut outer 475 | )); 476 | match result { 477 | Err(DeserializerError::ReaderError(ReaderError::SyntaxError(JsonSyntaxError { 478 | kind: SyntaxErrorKind::IncompleteDocument, 479 | .. 480 | }))) => {} 481 | r => panic!("Unexpected result: {r:?}"), 482 | } 483 | 484 | let mut expected_deserialized = Vec::::new(); 485 | expected_deserialized.extend_from_slice(&vec![Outer::default(); 7]); 486 | expected_deserialized.extend_from_slice(&vec![ 487 | Outer { 488 | a: 2, 489 | ..Default::default() 490 | }; 491 | 7 492 | ]); 493 | expected_deserialized.extend_from_slice(&vec![ 494 | Outer { 495 | a: 2, 496 | b: true, 497 | ..Default::default() 498 | }; 499 | 9 500 | ]); 501 | expected_deserialized.extend_from_slice(&vec![ 502 | Outer { 503 | a: 2, 504 | b: true, 505 | c: None, 506 | ..Default::default() 507 | }; 508 | 7 509 | ]); 510 | expected_deserialized.extend_from_slice(&vec![ 511 | Outer { 512 | a: 2, 513 | b: true, 514 | c: None, 515 | d: vec![Inner::default()] 516 | }; 517 | 11 518 | ]); 519 | expected_deserialized.extend_from_slice(&vec![ 520 | Outer { 521 | a: 2, 522 | b: true, 523 | c: None, 524 | d: vec![Inner { 525 | e: "str\"".to_owned(), 526 | ..Default::default() 527 | }] 528 | }; 529 | 11 530 | ]); 531 | expected_deserialized.extend_from_slice(&vec![ 532 | Outer { 533 | a: 2, 534 | b: true, 535 | c: None, 536 | d: vec![Inner { 537 | e: "str\"".to_owned(), 538 | f: 1.2e3 539 | }] 540 | }; 541 | 3 542 | ]); 543 | // Verify that test is properly implemented and number of expected values is equal to chars 544 | assert_eq!(full_json.chars().count(), expected_deserialized.len()); 545 | 546 | for (index, c) in full_json.char_indices() { 547 | json.push(c); 548 | deserialize_partial!(json.as_bytes(), |d| Outer::deserialize_in_place( 549 | d, &mut outer 550 | )) 551 | .unwrap(); 552 | assert_eq!( 553 | expected_deserialized[index], outer, 554 | "For char index {index}, JSON: {json}" 555 | ); 556 | } 557 | } 558 | 559 | #[test] 560 | fn unexpected_value() -> Result<(), Box> { 561 | let json = "true"; 562 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 563 | match json_reader.next_number_as_str() { 564 | Err(ReaderError::UnexpectedValueType { 565 | expected: ValueType::Number, 566 | actual: ValueType::Boolean, 567 | location, 568 | }) => { 569 | assert_eq!( 570 | JsonReaderPosition { 571 | path: None, 572 | line_pos: Some(LinePosition { line: 0, column: 0 }), 573 | data_pos: Some(0) 574 | }, 575 | location 576 | ); 577 | } 578 | r => panic!("unexpected result: {r:?}"), 579 | } 580 | assert_eq!(true, json_reader.next_bool()?); 581 | 582 | let json = "true"; 583 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 584 | match json_reader.begin_array() { 585 | Err(ReaderError::UnexpectedValueType { 586 | expected: ValueType::Array, 587 | actual: ValueType::Boolean, 588 | location, 589 | }) => { 590 | assert_eq!( 591 | JsonReaderPosition { 592 | path: None, 593 | line_pos: Some(LinePosition { line: 0, column: 0 }), 594 | data_pos: Some(0) 595 | }, 596 | location 597 | ); 598 | } 599 | r => panic!("unexpected result: {r:?}"), 600 | } 601 | assert_eq!(true, json_reader.next_bool()?); 602 | 603 | let json = "[true]"; 604 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 605 | match json_reader.next_number_as_str() { 606 | Err(ReaderError::UnexpectedValueType { 607 | expected: ValueType::Number, 608 | actual: ValueType::Array, 609 | location, 610 | }) => { 611 | assert_eq!( 612 | JsonReaderPosition { 613 | path: None, 614 | line_pos: Some(LinePosition { line: 0, column: 0 }), 615 | data_pos: Some(0) 616 | }, 617 | location 618 | ); 619 | } 620 | r => panic!("unexpected result: {r:?}"), 621 | } 622 | json_reader.begin_array()?; 623 | assert_eq!(true, json_reader.next_bool()?); 624 | json_reader.end_array()?; 625 | 626 | Ok(()) 627 | } 628 | 629 | #[test] 630 | fn end_incomplete() -> Result<(), Box> { 631 | let json = "["; 632 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 633 | json_reader.begin_array()?; 634 | // Directly calls `end_array()` without preceding `has_next()` 635 | json_reader.end_array()?; 636 | json_reader.consume_trailing_whitespace()?; 637 | 638 | let json = "{"; 639 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 640 | json_reader.begin_object()?; 641 | // Directly calls `end_object()` without preceding `has_next()` 642 | json_reader.end_object()?; 643 | json_reader.consume_trailing_whitespace()?; 644 | 645 | Ok(()) 646 | } 647 | 648 | #[test] 649 | fn unexpected_structure() -> Result<(), Box> { 650 | let json = "[]"; 651 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 652 | json_reader.begin_array()?; 653 | match json_reader.peek() { 654 | Err(ReaderError::UnexpectedStructure { 655 | kind: UnexpectedStructureKind::FewerElementsThanExpected, 656 | location, 657 | }) => { 658 | assert_eq!( 659 | JsonReaderPosition { 660 | path: None, 661 | line_pos: Some(LinePosition { line: 0, column: 1 }), 662 | data_pos: Some(1) 663 | }, 664 | location 665 | ); 666 | } 667 | r => panic!("unexpected result: {r:?}"), 668 | } 669 | json_reader.end_array()?; 670 | 671 | let json = "[true]"; 672 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 673 | json_reader.begin_array()?; 674 | match json_reader.end_array() { 675 | Err(ReaderError::UnexpectedStructure { 676 | kind: UnexpectedStructureKind::MoreElementsThanExpected, 677 | location, 678 | }) => { 679 | assert_eq!( 680 | JsonReaderPosition { 681 | path: None, 682 | line_pos: Some(LinePosition { line: 0, column: 1 }), 683 | data_pos: Some(1) 684 | }, 685 | location 686 | ); 687 | } 688 | r => panic!("unexpected result: {r:?}"), 689 | } 690 | assert_eq!(true, json_reader.next_bool()?); 691 | json_reader.end_array()?; 692 | 693 | let json = "{}"; 694 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 695 | json_reader.begin_object()?; 696 | match json_reader.next_name() { 697 | Err(ReaderError::UnexpectedStructure { 698 | kind: UnexpectedStructureKind::FewerElementsThanExpected, 699 | location, 700 | }) => { 701 | assert_eq!( 702 | JsonReaderPosition { 703 | path: None, 704 | line_pos: Some(LinePosition { line: 0, column: 1 }), 705 | data_pos: Some(1) 706 | }, 707 | location 708 | ); 709 | } 710 | r => panic!("unexpected result: {r:?}"), 711 | } 712 | json_reader.end_object()?; 713 | 714 | let json = "{\"a\": true}"; 715 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 716 | json_reader.begin_object()?; 717 | match json_reader.end_object() { 718 | Err(ReaderError::UnexpectedStructure { 719 | kind: UnexpectedStructureKind::MoreElementsThanExpected, 720 | location, 721 | }) => { 722 | assert_eq!( 723 | JsonReaderPosition { 724 | path: None, 725 | line_pos: Some(LinePosition { line: 0, column: 1 }), 726 | data_pos: Some(1) 727 | }, 728 | location 729 | ); 730 | } 731 | r => panic!("unexpected result: {r:?}"), 732 | } 733 | assert_eq!("a", json_reader.next_name()?); 734 | assert_eq!(true, json_reader.next_bool()?); 735 | json_reader.end_object()?; 736 | 737 | Ok(()) 738 | } 739 | 740 | #[test] 741 | fn current_position() -> Result<(), Box> { 742 | let json = r#" [ 1 , { "a" : [] , "b" : "#; 743 | let mut json_reader = PartialJsonReader::new(JsonStreamReader::new(json.as_bytes())); 744 | 745 | fn assert_pos(json_reader: &PartialJsonReader, expected_column: u64) { 746 | let position = json_reader.current_position(true); 747 | assert_eq!( 748 | JsonReaderPosition { 749 | path: None, 750 | line_pos: Some(LinePosition { 751 | line: 0, 752 | column: expected_column, 753 | }), 754 | // Assume input is ASCII only on single line; treat column as byte pos 755 | data_pos: Some(expected_column) 756 | }, 757 | position 758 | ); 759 | } 760 | 761 | assert_pos(&json_reader, 0); 762 | assert_eq!(ValueType::Array, json_reader.peek()?); 763 | assert_pos(&json_reader, 1); 764 | json_reader.begin_array()?; 765 | assert_pos(&json_reader, 2); 766 | assert!(json_reader.has_next()?); 767 | assert_pos(&json_reader, 3); 768 | assert_eq!("1", json_reader.next_number_as_str()?); 769 | assert_pos(&json_reader, 7); 770 | assert!(json_reader.has_next()?); 771 | assert_pos(&json_reader, 7); 772 | json_reader.begin_object()?; 773 | assert_pos(&json_reader, 8); 774 | assert!(json_reader.has_next()?); 775 | assert_pos(&json_reader, 9); 776 | assert_eq!("a", json_reader.next_name()?); 777 | assert_pos(&json_reader, 15); 778 | assert_eq!(ValueType::Array, json_reader.peek()?); 779 | assert_pos(&json_reader, 15); 780 | json_reader.begin_array()?; 781 | assert_pos(&json_reader, 16); 782 | assert!(!json_reader.has_next()?); 783 | assert_pos(&json_reader, 16); 784 | json_reader.end_array()?; 785 | assert_pos(&json_reader, 17); 786 | // Here the end of valid JSON is reached 787 | assert!(!json_reader.has_next()?); 788 | assert_pos(&json_reader, 17); 789 | json_reader.end_object()?; 790 | assert_pos(&json_reader, 17); 791 | assert!(!json_reader.has_next()?); 792 | assert_pos(&json_reader, 17); 793 | json_reader.end_array()?; 794 | assert_pos(&json_reader, 17); 795 | 796 | Ok(()) 797 | } 798 | --------------------------------------------------------------------------------