From 154b529adebe9cffd3363c04b1f37321babed1dc Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 21 Jul 2025 14:47:16 -0500 Subject: [PATCH 1/2] feat(json): Add json-write --- Cargo.lock | 440 ++++++++++++++++++++++++++++- crates/json-write/CHANGELOG.md | 13 + crates/json-write/Cargo.toml | 42 +++ crates/json-write/LICENSE-APACHE | 1 + crates/json-write/LICENSE-MIT | 1 + crates/json-write/README.md | 22 ++ crates/json-write/src/key.rs | 53 ++++ crates/json-write/src/lib.rs | 56 ++++ crates/json-write/src/value.rs | 393 ++++++++++++++++++++++++++ crates/json-write/src/write.rs | 60 ++++ crates/json-write/tests/float.rs | 120 ++++++++ crates/json-write/tests/integer.rs | 59 ++++ crates/json-write/tests/string.rs | 288 +++++++++++++++++++ 13 files changed, 1538 insertions(+), 10 deletions(-) create mode 100644 crates/json-write/CHANGELOG.md create mode 100644 crates/json-write/Cargo.toml create mode 120000 crates/json-write/LICENSE-APACHE create mode 120000 crates/json-write/LICENSE-MIT create mode 100644 crates/json-write/README.md create mode 100644 crates/json-write/src/key.rs create mode 100644 crates/json-write/src/lib.rs create mode 100644 crates/json-write/src/value.rs create mode 100644 crates/json-write/src/write.rs create mode 100644 crates/json-write/tests/float.rs create mode 100644 crates/json-write/tests/integer.rs create mode 100644 crates/json-write/tests/string.rs diff --git a/Cargo.lock b/Cargo.lock index 755628f..d9ae961 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,7 +37,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -47,9 +47,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.48.0", ] +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + [[package]] name = "cfg-if" version = "1.0.0" @@ -80,6 +107,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "escargot" version = "0.5.14" @@ -92,6 +129,30 @@ dependencies = [ "serde_json", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + [[package]] name = "hashbrown" version = "0.15.4" @@ -114,6 +175,22 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "json-write" +version = "0.0.1" +dependencies = [ + "proptest", + "serde", + "serde_json", + "snapbox", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "lexarg" version = "0.0.1" @@ -133,6 +210,12 @@ dependencies = [ name = "lexarg-parser" version = "0.0.1" +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + [[package]] name = "libtest-json" version = "0.0.1" @@ -187,6 +270,12 @@ dependencies = [ "snapbox", ] +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "log" version = "0.4.17" @@ -208,6 +297,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -226,6 +324,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -235,6 +342,32 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.40" @@ -244,6 +377,50 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core", +] + [[package]] name = "ref-cast" version = "1.0.24" @@ -264,6 +441,37 @@ dependencies = [ "syn", ] +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.13" @@ -387,6 +595,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.8" @@ -399,13 +626,49 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", ] [[package]] @@ -414,13 +677,45 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] [[package]] @@ -429,38 +724,163 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/crates/json-write/CHANGELOG.md b/crates/json-write/CHANGELOG.md new file mode 100644 index 0000000..7f428b0 --- /dev/null +++ b/crates/json-write/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +The format is based on [Keep a Changelog]. + +[Keep a Changelog]: http://keepachangelog.com/en/1.0.0/ + + +## [Unreleased] - ReleaseDate + +Initial release + + +[Unreleased]: https://github.com/toml-rs/toml/compare/06332279463f17447e1218c834078535b5ed9ebd...HEAD diff --git a/crates/json-write/Cargo.toml b/crates/json-write/Cargo.toml new file mode 100644 index 0000000..6c12b4f --- /dev/null +++ b/crates/json-write/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "json-write" +version = "0.0.1" +description = """ +A low-level interface for writing out JSON +""" +categories = ["encoding"] +keywords = ["encoding", "json", "no_std"] +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true +include.workspace = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"] + +[package.metadata.release] +pre-release-replacements = [ + {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, + {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, + {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, + {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, + {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/assert-rs/libtest2/compare/{{tag_name}}...HEAD", exactly=1}, +] + +[features] +default = ["std"] +std = ["alloc"] +alloc = [] + +[dependencies] + +[dev-dependencies] +proptest = "1.6.0" +serde = { version = "1.0.160", features = ["derive"] } +serde_json = { version = "1.0.96" } +snapbox = "0.6.0" + +[lints] +workspace = true diff --git a/crates/json-write/LICENSE-APACHE b/crates/json-write/LICENSE-APACHE new file mode 120000 index 0000000..1cd601d --- /dev/null +++ b/crates/json-write/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/json-write/LICENSE-MIT b/crates/json-write/LICENSE-MIT new file mode 120000 index 0000000..b2cfbdc --- /dev/null +++ b/crates/json-write/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/json-write/README.md b/crates/json-write/README.md new file mode 100644 index 0000000..8b6b57f --- /dev/null +++ b/crates/json-write/README.md @@ -0,0 +1,22 @@ +# toml_writer + +[![Latest Version](https://img.shields.io/crates/v/toml.svg)](https://crates.io/crates/toml) +[![Documentation](https://docs.rs/toml/badge.svg)](https://docs.rs/toml) + +A low-level interface for writing out TOML + +## License + +Licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual-licensed as above, without any additional terms or +conditions. diff --git a/crates/json-write/src/key.rs b/crates/json-write/src/key.rs new file mode 100644 index 0000000..60c469f --- /dev/null +++ b/crates/json-write/src/key.rs @@ -0,0 +1,53 @@ +#[cfg(feature = "alloc")] +use alloc::borrow::Cow; +#[cfg(feature = "alloc")] +use alloc::string::String; + +use crate::JsonWrite; + +#[cfg(feature = "alloc")] +pub trait ToJsonKey { + fn to_json_key(&self) -> String; +} + +#[cfg(feature = "alloc")] +impl ToJsonKey for T +where + T: WriteJsonKey + ?Sized, +{ + fn to_json_key(&self) -> String { + let mut result = String::new(); + let _ = self.write_json_key(&mut result); + result + } +} + +pub trait WriteJsonKey { + fn write_json_key(&self, writer: &mut W) -> core::fmt::Result; +} + +impl WriteJsonKey for str { + fn write_json_key(&self, writer: &mut W) -> core::fmt::Result { + crate::value::write_json_str(self, writer) + } +} + +#[cfg(feature = "alloc")] +impl WriteJsonKey for String { + fn write_json_key(&self, writer: &mut W) -> core::fmt::Result { + self.as_str().write_json_key(writer) + } +} + +#[cfg(feature = "alloc")] +impl WriteJsonKey for Cow<'_, str> { + fn write_json_key(&self, writer: &mut W) -> core::fmt::Result { + self.as_ref().write_json_key(writer) + } +} + +impl WriteJsonKey for &V { + fn write_json_key(&self, writer: &mut W) -> core::fmt::Result { + (*self).write_json_key(writer) + } +} diff --git a/crates/json-write/src/lib.rs b/crates/json-write/src/lib.rs new file mode 100644 index 0000000..0ec7e9f --- /dev/null +++ b/crates/json-write/src/lib.rs @@ -0,0 +1,56 @@ +//! A low-level interface for writing out JSON +//! +//! # Example +//! +//! ```rust +//! use json_write::JsonWrite as _; +//! +//! # fn main() -> std::fmt::Result { +//! let mut output = String::new(); +//! output.open_object()?; +//! output.newline()?; +//! +//! output.space()?; +//! output.space()?; +//! output.key("key")?; +//! output.keyval_sep()?; +//! output.space()?; +//! output.value("value")?; +//! output.newline()?; +//! +//! output.close_object()?; +//! output.newline()?; +//! +//! assert_eq!(output, r#"{ +//! "key": "value" +//! } +//! "#); +//! # Ok(()) +//! # } +//! ``` + +#![cfg_attr(all(not(feature = "std"), not(test)), no_std)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![warn(clippy::std_instead_of_core)] +#![warn(clippy::std_instead_of_alloc)] +#![warn(clippy::print_stderr)] +#![warn(clippy::print_stdout)] + +#[cfg(feature = "alloc")] +extern crate alloc; + +mod key; +mod value; +mod write; + +#[cfg(feature = "alloc")] +pub use key::ToJsonKey; +pub use key::WriteJsonKey; +#[cfg(feature = "alloc")] +pub use value::ToJsonValue; +pub use value::WriteJsonValue; +pub use write::JsonWrite; + +#[doc = include_str!("../README.md")] +#[cfg(doctest)] +pub struct ReadmeDoctests; diff --git a/crates/json-write/src/value.rs b/crates/json-write/src/value.rs new file mode 100644 index 0000000..2f54981 --- /dev/null +++ b/crates/json-write/src/value.rs @@ -0,0 +1,393 @@ +#[cfg(feature = "alloc")] +use alloc::borrow::Cow; +#[cfg(feature = "alloc")] +use alloc::string::String; +#[cfg(feature = "alloc")] +use alloc::vec::Vec; + +use crate::JsonWrite; +use crate::WriteJsonKey; + +#[cfg(feature = "alloc")] +pub trait ToJsonValue { + fn to_json_value(&self) -> String; +} + +#[cfg(feature = "alloc")] +impl ToJsonValue for T +where + T: WriteJsonValue + ?Sized, +{ + fn to_json_value(&self) -> String { + let mut result = String::new(); + let _ = self.write_json_value(&mut result); + result + } +} + +pub trait WriteJsonValue { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result; +} + +impl WriteJsonValue for bool { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + write!(writer, "{self}") + } +} + +impl WriteJsonValue for u8 { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + write!(writer, "{self}") + } +} + +impl WriteJsonValue for i8 { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + write!(writer, "{self}") + } +} + +impl WriteJsonValue for u16 { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + write!(writer, "{self}") + } +} + +impl WriteJsonValue for i16 { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + write!(writer, "{self}") + } +} + +impl WriteJsonValue for u32 { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + write!(writer, "{self}") + } +} + +impl WriteJsonValue for i32 { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + write!(writer, "{self}") + } +} + +impl WriteJsonValue for u64 { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + write!(writer, "{self}") + } +} + +impl WriteJsonValue for i64 { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + write!(writer, "{self}") + } +} + +impl WriteJsonValue for u128 { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + write!(writer, "{self}") + } +} + +impl WriteJsonValue for i128 { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + write!(writer, "{self}") + } +} + +impl WriteJsonValue for f32 { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + if self.is_nan() || self.is_infinite() { + None::.write_json_value(writer) + } else { + if self % 1.0 == 0.0 { + write!(writer, "{self}.0") + } else { + write!(writer, "{self}") + } + } + } +} + +impl WriteJsonValue for f64 { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + if self.is_nan() || self.is_infinite() { + None::.write_json_value(writer) + } else { + if self % 1.0 == 0.0 { + write!(writer, "{self}.0") + } else { + write!(writer, "{self}") + } + } + } +} + +impl WriteJsonValue for char { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + let mut buf = [0; 4]; + let v = self.encode_utf8(&mut buf); + v.write_json_value(writer) + } +} + +impl WriteJsonValue for str { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + write_json_str(self, writer) + } +} + +#[cfg(feature = "alloc")] +impl WriteJsonValue for String { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + self.as_str().write_json_value(writer) + } +} + +#[cfg(feature = "alloc")] +impl WriteJsonValue for Cow<'_, str> { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + self.as_ref().write_json_value(writer) + } +} + +impl WriteJsonValue for Option { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + match self { + Some(v) => v.write_json_value(writer), + None => write_json_null(writer), + } + } +} + +impl WriteJsonValue for [V] { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + writer.open_array()?; + let mut iter = self.iter(); + if let Some(v) = iter.next() { + writer.value(v)?; + } + for v in iter { + writer.val_sep()?; + writer.space()?; + writer.value(v)?; + } + writer.close_array()?; + Ok(()) + } +} + +impl WriteJsonValue for [V; N] { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + self.as_slice().write_json_value(writer) + } +} + +#[cfg(feature = "alloc")] +impl WriteJsonValue for Vec { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + self.as_slice().write_json_value(writer) + } +} + +#[cfg(feature = "alloc")] +impl WriteJsonValue for alloc::collections::BTreeMap { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + write_json_object(self.iter(), writer) + } +} + +#[cfg(feature = "std")] +impl WriteJsonValue for std::collections::HashMap { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + write_json_object(self.iter(), writer) + } +} + +impl WriteJsonValue for &V { + fn write_json_value(&self, writer: &mut W) -> core::fmt::Result { + (*self).write_json_value(writer) + } +} + +pub(crate) fn write_json_null(writer: &mut W) -> core::fmt::Result { + write!(writer, "null") +} + +pub(crate) fn write_json_str( + value: &str, + writer: &mut W, +) -> core::fmt::Result { + write!(writer, "\"")?; + format_escaped_str_contents(writer, value)?; + write!(writer, "\"")?; + Ok(()) +} + +fn format_escaped_str_contents(writer: &mut W, value: &str) -> core::fmt::Result +where + W: ?Sized + JsonWrite, +{ + let mut bytes = value.as_bytes(); + + let mut i = 0; + while i < bytes.len() { + let (string_run, rest) = bytes.split_at(i); + let (&byte, rest) = rest.split_first().unwrap(); + + let escape = ESCAPE[byte as usize]; + + i += 1; + if escape == 0 { + continue; + } + + bytes = rest; + i = 0; + + // Safety: string_run is a valid utf8 string, since we only split on ascii sequences + let string_run = unsafe { core::str::from_utf8_unchecked(string_run) }; + if !string_run.is_empty() { + write!(writer, "{string_run}")?; + } + + let char_escape = match escape { + BB => CharEscape::Backspace, + TT => CharEscape::Tab, + NN => CharEscape::LineFeed, + FF => CharEscape::FormFeed, + RR => CharEscape::CarriageReturn, + QU => CharEscape::Quote, + BS => CharEscape::ReverseSolidus, + UU => CharEscape::AsciiControl(byte), + // Safety: the escape table does not contain any other type of character. + _ => unsafe { core::hint::unreachable_unchecked() }, + }; + write_char_escape(writer, char_escape)?; + } + + // Safety: bytes is a valid utf8 string, since we only split on ascii sequences + let string_run = unsafe { core::str::from_utf8_unchecked(bytes) }; + if string_run.is_empty() { + return Ok(()); + } + + write!(writer, "{string_run}")?; + Ok(()) +} + +const BB: u8 = b'b'; // \x08 +const TT: u8 = b't'; // \x09 +const NN: u8 = b'n'; // \x0A +const FF: u8 = b'f'; // \x0C +const RR: u8 = b'r'; // \x0D +const QU: u8 = b'"'; // \x22 +const BS: u8 = b'\\'; // \x5C +const UU: u8 = b'u'; // \x00...\x1F except the ones above +const __: u8 = 0; + +// Lookup table of escape sequences. A value of b'x' at index i means that byte +// i is escaped as "\x" in JSON. A value of 0 means that byte i is not escaped. +static ESCAPE: [u8; 256] = [ + // 1 2 3 4 5 6 7 8 9 A B C D E F + UU, UU, UU, UU, UU, UU, UU, UU, BB, TT, NN, UU, FF, RR, UU, UU, // 0 + UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, // 1 + __, __, QU, __, __, __, __, __, __, __, __, __, __, __, __, __, // 2 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 3 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 4 + __, __, __, __, __, __, __, __, __, __, __, __, BS, __, __, __, // 5 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 6 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 7 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 8 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 9 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // A + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // B + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // C + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // D + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // E + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // F +]; + +/// Represents a character escape code in a type-safe manner. +enum CharEscape { + /// An escaped quote `"` + Quote, + /// An escaped reverse solidus `\` + ReverseSolidus, + /// An escaped backspace character (usually escaped as `\b`) + Backspace, + /// An escaped form feed character (usually escaped as `\f`) + FormFeed, + /// An escaped line feed character (usually escaped as `\n`) + LineFeed, + /// An escaped carriage return character (usually escaped as `\r`) + CarriageReturn, + /// An escaped tab character (usually escaped as `\t`) + Tab, + /// An escaped ASCII plane control character (usually escaped as + /// `\u00XX` where `XX` are two hex characters) + AsciiControl(u8), +} + +fn write_char_escape(writer: &mut W, char_escape: CharEscape) -> core::fmt::Result +where + W: ?Sized + JsonWrite, +{ + let escape_char = match char_escape { + CharEscape::Quote => '"', + CharEscape::ReverseSolidus => '\\', + CharEscape::Backspace => 'b', + CharEscape::FormFeed => 'f', + CharEscape::LineFeed => 'n', + CharEscape::CarriageReturn => 'r', + CharEscape::Tab => 't', + CharEscape::AsciiControl(_) => 'u', + }; + + match char_escape { + CharEscape::AsciiControl(byte) => { + static HEX_DIGITS: [u8; 16] = *b"0123456789abcdef"; + let first = HEX_DIGITS[(byte >> 4) as usize] as char; + let second = HEX_DIGITS[(byte & 0xF) as usize] as char; + write!(writer, "\\{escape_char}00{first}{second}") + } + _ => { + write!(writer, "\\{escape_char}") + } + } +} + +fn write_json_object< + 'i, + I: Iterator, + K: WriteJsonKey + 'i, + V: WriteJsonValue + 'i, + W: JsonWrite + ?Sized, +>( + mut iter: I, + writer: &mut W, +) -> core::fmt::Result { + writer.open_object()?; + let mut trailing_space = false; + if let Some((key, value)) = iter.next() { + writer.space()?; + writer.key(key)?; + writer.keyval_sep()?; + writer.space()?; + writer.value(value)?; + trailing_space = true; + } + for (key, value) in iter { + writer.val_sep()?; + writer.space()?; + writer.key(key)?; + writer.keyval_sep()?; + writer.space()?; + writer.value(value)?; + } + if trailing_space { + writer.space()?; + } + writer.close_object()?; + Ok(()) +} diff --git a/crates/json-write/src/write.rs b/crates/json-write/src/write.rs new file mode 100644 index 0000000..08ee785 --- /dev/null +++ b/crates/json-write/src/write.rs @@ -0,0 +1,60 @@ +pub trait JsonWrite: core::fmt::Write { + fn open_object(&mut self) -> core::fmt::Result { + write!(self, "{{") + } + fn close_object(&mut self) -> core::fmt::Result { + write!(self, "}}") + } + + fn open_array(&mut self) -> core::fmt::Result { + write!(self, "[") + } + fn close_array(&mut self) -> core::fmt::Result { + write!(self, "]") + } + + fn keyval_sep(&mut self) -> core::fmt::Result { + write!(self, ":") + } + + /// Write an encoded JSON key + fn key(&mut self, value: impl crate::WriteJsonKey) -> core::fmt::Result { + value.write_json_key(self) + } + + /// Write an encoded JSON scalar value + /// + ///
+ /// + /// For floats, this preserves the sign bit for [`f32::NAN`] / [`f64::NAN`] for the sake of + /// format-preserving editing. + /// However, in most cases the sign bit is indeterminate and outputting signed NANs can be a + /// cause of non-repeatable behavior. + /// + /// For general serialization, you should discard the sign bit. For example: + /// ``` + /// # let mut v = f64::NAN; + /// if v.is_nan() { + /// v = v.copysign(1.0); + /// } + /// ``` + /// + ///
+ fn value(&mut self, value: impl crate::WriteJsonValue) -> core::fmt::Result { + value.write_json_value(self) + } + + fn val_sep(&mut self) -> core::fmt::Result { + write!(self, ",") + } + + fn space(&mut self) -> core::fmt::Result { + write!(self, " ") + } + + fn newline(&mut self) -> core::fmt::Result { + writeln!(self) + } +} + +impl JsonWrite for W where W: core::fmt::Write {} diff --git a/crates/json-write/tests/float.rs b/crates/json-write/tests/float.rs new file mode 100644 index 0000000..a2cfc08 --- /dev/null +++ b/crates/json-write/tests/float.rs @@ -0,0 +1,120 @@ +#![cfg(feature = "alloc")] +#![allow(clippy::dbg_macro)] // unsure why config isn't working + +use snapbox::prelude::*; +use snapbox::str; + +use json_write::ToJsonKey; +use json_write::ToJsonValue; + +#[track_caller] +fn t(value: N, expected: impl IntoData) { + let key = "key".to_json_key(); + let string = value.to_json_value(); + let object = format!("{{ {key}: {string} }}"); + let parsed = format!("{:?}", object.parse::()); + let results = Results { + value, + string, + parsed, + }; + snapbox::assert_data_eq!(results.to_debug(), expected.raw()); +} + +#[derive(Debug)] +#[allow(dead_code)] +struct Results { + value: N, + string: String, + parsed: String, +} + +#[test] +fn zero() { + t( + 0.0f64, + str![[r#" +Results { + value: 0.0, + string: "0.0", + parsed: "Ok(Object {\"key\": Number(0.0)})", +} + +"#]], + ); +} + +#[test] +fn neg_zero() { + t( + -0.0f64, + str![[r#" +Results { + value: -0.0, + string: "-0.0", + parsed: "Ok(Object {\"key\": Number(-0.0)})", +} + +"#]], + ); +} + +#[test] +fn inf() { + t( + f64::INFINITY, + str![[r#" +Results { + value: inf, + string: "null", + parsed: "Ok(Object {\"key\": Null})", +} + +"#]], + ); +} + +#[test] +fn neg_inf() { + t( + f64::NEG_INFINITY, + str![[r#" +Results { + value: -inf, + string: "null", + parsed: "Ok(Object {\"key\": Null})", +} + +"#]], + ); +} + +#[test] +fn nan() { + t( + f64::NAN.copysign(1.0), + str![[r#" +Results { + value: NaN, + string: "null", + parsed: "Ok(Object {\"key\": Null})", +} + +"#]], + ); +} + +#[test] +fn neg_nan() { + t( + f64::NAN.copysign(-1.0), + str![[r#" +Results { + value: NaN, + string: "null", + parsed: "Ok(Object {\"key\": Null})", +} + +"#]], + ); +} diff --git a/crates/json-write/tests/integer.rs b/crates/json-write/tests/integer.rs new file mode 100644 index 0000000..e826256 --- /dev/null +++ b/crates/json-write/tests/integer.rs @@ -0,0 +1,59 @@ +#![cfg(feature = "alloc")] + +use snapbox::prelude::*; +use snapbox::str; + +use json_write::ToJsonKey; +use json_write::ToJsonValue; + +#[track_caller] +fn t(value: N, expected: impl IntoData) { + let key = "key".to_json_key(); + let string = value.to_json_value(); + let object = format!("{{ {key}: {string} }}"); + let parsed = format!("{:?}", object.parse::()); + let results = Results { + value, + string, + parsed, + }; + snapbox::assert_data_eq!(results.to_debug(), expected.raw()); +} + +#[derive(Debug)] +#[allow(dead_code)] +struct Results { + value: N, + string: String, + parsed: String, +} + +#[test] +fn positive() { + t( + 42, + str![[r#" +Results { + value: 42, + string: "42", + parsed: "Ok(Object {\"key\": Number(42)})", +} + +"#]], + ); +} + +#[test] +fn negative() { + t( + -42, + str![[r#" +Results { + value: -42, + string: "-42", + parsed: "Ok(Object {\"key\": Number(-42)})", +} + +"#]], + ); +} diff --git a/crates/json-write/tests/string.rs b/crates/json-write/tests/string.rs new file mode 100644 index 0000000..da40586 --- /dev/null +++ b/crates/json-write/tests/string.rs @@ -0,0 +1,288 @@ +#![cfg(feature = "alloc")] +#![allow(clippy::dbg_macro)] // unsure why config isn't working + +use snapbox::prelude::*; +use snapbox::str; + +use json_write::ToJsonKey; +use json_write::ToJsonValue; + +#[track_caller] +fn t(decoded: &str, expected: impl IntoData) { + let key = decoded.to_json_key(); + let string = decoded.to_json_value(); + let object = format!("{{ {key}: {string} }}"); + let parsed = format!("{:?}", object.parse::()); + let results = Results { + decoded, + key, + string, + parsed, + }; + snapbox::assert_data_eq!(results.to_debug(), expected.raw()); +} + +#[derive(Debug)] +#[allow(dead_code)] +struct Results<'i> { + decoded: &'i str, + key: String, + string: String, + parsed: String, +} + +#[test] +fn empty() { + t( + "", + str![[r#" +Results { + decoded: "", + key: "\"\"", + string: "\"\"", + parsed: "Ok(Object {\"\": String(\"\")})", +} + +"#]], + ); +} + +#[test] +fn alpha() { + t( + "helloworld", + str![[r#" +Results { + decoded: "helloworld", + key: "\"helloworld\"", + string: "\"helloworld\"", + parsed: "Ok(Object {\"helloworld\": String(\"helloworld\")})", +} + +"#]], + ); +} + +#[test] +fn ident() { + t( + "_hello-world_", + str![[r#" +Results { + decoded: "_hello-world_", + key: "\"_hello-world_\"", + string: "\"_hello-world_\"", + parsed: "Ok(Object {\"_hello-world_\": String(\"_hello-world_\")})", +} + +"#]], + ); +} + +#[test] +fn one_single_quote() { + t( + "'hello'world'", + str![[r#" +Results { + decoded: "'hello'world'", + key: "\"'hello'world'\"", + string: "\"'hello'world'\"", + parsed: "Ok(Object {\"'hello'world'\": String(\"'hello'world'\")})", +} + +"#]], + ); +} + +#[test] +fn two_single_quote() { + t( + "''hello''world''", + str![[r#" +Results { + decoded: "''hello''world''", + key: "\"''hello''world''\"", + string: "\"''hello''world''\"", + parsed: "Ok(Object {\"''hello''world''\": String(\"''hello''world''\")})", +} + +"#]], + ); +} + +#[test] +fn three_single_quote() { + t( + "'''hello'''world'''", + str![[r#" +Results { + decoded: "'''hello'''world'''", + key: "\"'''hello'''world'''\"", + string: "\"'''hello'''world'''\"", + parsed: "Ok(Object {\"'''hello'''world'''\": String(\"'''hello'''world'''\")})", +} + +"#]], + ); +} + +#[test] +fn one_double_quote() { + t( + r#""hello"world""#, + str![[r#" +Results { + decoded: "\"hello\"world\"", + key: "\"\\\"hello\\\"world\\\"\"", + string: "\"\\\"hello\\\"world\\\"\"", + parsed: "Ok(Object {\"\\\"hello\\\"world\\\"\": String(\"\\\"hello\\\"world\\\"\")})", +} + +"#]], + ); +} + +#[test] +fn two_double_quote() { + t( + r#"""hello""world"""#, + str![[r#" +Results { + decoded: "\"\"hello\"\"world\"\"", + key: "\"\\\"\\\"hello\\\"\\\"world\\\"\\\"\"", + string: "\"\\\"\\\"hello\\\"\\\"world\\\"\\\"\"", + parsed: "Ok(Object {\"\\\"\\\"hello\\\"\\\"world\\\"\\\"\": String(\"\\\"\\\"hello\\\"\\\"world\\\"\\\"\")})", +} + +"#]], + ); +} + +#[test] +fn three_double_quote() { + t( + r#""""hello"""world""""#, + str![[r#" +Results { + decoded: "\"\"\"hello\"\"\"world\"\"\"", + key: "\"\\\"\\\"\\\"hello\\\"\\\"\\\"world\\\"\\\"\\\"\"", + string: "\"\\\"\\\"\\\"hello\\\"\\\"\\\"world\\\"\\\"\\\"\"", + parsed: "Ok(Object {\"\\\"\\\"\\\"hello\\\"\\\"\\\"world\\\"\\\"\\\"\": String(\"\\\"\\\"\\\"hello\\\"\\\"\\\"world\\\"\\\"\\\"\")})", +} + +"#]], + ); +} + +#[test] +fn mixed_quote_1() { + t( + r#""'"#, + str![[r#" +Results { + decoded: "\"'", + key: "\"\\\"'\"", + string: "\"\\\"'\"", + parsed: "Ok(Object {\"\\\"'\": String(\"\\\"'\")})", +} + +"#]], + ); +} + +#[test] +fn mixed_quote_2() { + t( + r#"mixed quoted \"start\" 'end'' mixed quote"#, + str![[r#" +Results { + decoded: "mixed quoted \\\"start\\\" 'end'' mixed quote", + key: "\"mixed quoted \\\\\\\"start\\\\\\\" 'end'' mixed quote\"", + string: "\"mixed quoted \\\\\\\"start\\\\\\\" 'end'' mixed quote\"", + parsed: "Ok(Object {\"mixed quoted \\\\\\\"start\\\\\\\" 'end'' mixed quote\": String(\"mixed quoted \\\\\\\"start\\\\\\\" 'end'' mixed quote\")})", +} + +"#]], + ); +} + +#[test] +fn escape() { + t( + r#"\windows\system32\"#, + str![[r#" +Results { + decoded: "\\windows\\system32\\", + key: "\"\\\\windows\\\\system32\\\\\"", + string: "\"\\\\windows\\\\system32\\\\\"", + parsed: "Ok(Object {\"\\\\windows\\\\system32\\\\\": String(\"\\\\windows\\\\system32\\\\\")})", +} + +"#]], + ); +} + +#[test] +fn cr() { + t( + "\rhello\rworld\r", + str![[r#" +Results { + decoded: "\rhello\rworld\r", + key: "\"\\rhello\\rworld\\r\"", + string: "\"\\rhello\\rworld\\r\"", + parsed: "Ok(Object {\"\\rhello\\rworld\\r\": String(\"\\rhello\\rworld\\r\")})", +} + +"#]], + ); +} + +#[test] +fn lf() { + t( + "\nhello\nworld\n", + str![[r#" +Results { + decoded: "\nhello\nworld\n", + key: "\"\\nhello\\nworld\\n\"", + string: "\"\\nhello\\nworld\\n\"", + parsed: "Ok(Object {\"\\nhello\\nworld\\n\": String(\"\\nhello\\nworld\\n\")})", +} + +"#]], + ); +} + +#[test] +fn crlf() { + t( + "\r\nhello\r\nworld\r\n", + str![[r#" +Results { + decoded: "\r\nhello\r\nworld\r\n", + key: "\"\\r\\nhello\\r\\nworld\\r\\n\"", + string: "\"\\r\\nhello\\r\\nworld\\r\\n\"", + parsed: "Ok(Object {\"\\r\\nhello\\r\\nworld\\r\\n\": String(\"\\r\\nhello\\r\\nworld\\r\\n\")})", +} + +"#]], + ); +} + +#[test] +fn tab() { + t( + "\thello\tworld\t", + str![[r#" +Results { + decoded: "\thello\tworld\t", + key: "\"\\thello\\tworld\\t\"", + string: "\"\\thello\\tworld\\t\"", + parsed: "Ok(Object {\"\\thello\\tworld\\t\": String(\"\\thello\\tworld\\t\")})", +} + +"#]], + ); +} From 278d8a3a1ce758cb01491c0ecb14c4165ef42383 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 21 Jul 2025 17:04:11 -0500 Subject: [PATCH 2/2] perf: Allow writing json without serde --- Cargo.lock | 1 + crates/libtest-json/Cargo.toml | 3 +- crates/libtest-json/src/event.rs | 129 ++++++++++++++++++++++++++++++- 3 files changed, 131 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9ae961..505d327 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,6 +220,7 @@ checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" name = "libtest-json" version = "0.0.1" dependencies = [ + "json-write", "schemars", "serde", "serde_json", diff --git a/crates/libtest-json/Cargo.toml b/crates/libtest-json/Cargo.toml index 9943063..6a6afd7 100644 --- a/crates/libtest-json/Cargo.toml +++ b/crates/libtest-json/Cargo.toml @@ -26,13 +26,14 @@ pre-release-replacements = [ [features] default = [] serde = ["dep:serde"] -json = ["dep:serde_json", "serde"] +json = ["dep:json-write"] unstable-schema = ["serde", "dep:schemars", "dep:serde_json"] [dependencies] serde = { version = "1.0.160", features = ["derive"], optional = true } serde_json = { version = "1.0.96", optional = true } schemars = { version = "1.0.0-alpha.17", features = ["preserve_order", "semver1"], optional = true } +json-write = { version = "0.0.1", path = "../json-write", optional = true } [dev-dependencies] snapbox = "0.6.21" diff --git a/crates/libtest-json/src/event.rs b/crates/libtest-json/src/event.rs index e8bdc46..26f54d8 100644 --- a/crates/libtest-json/src/event.rs +++ b/crates/libtest-json/src/event.rs @@ -66,7 +66,125 @@ pub enum Event { impl Event { #[cfg(feature = "json")] pub fn to_jsonline(&self) -> String { - serde_json::to_string(self).expect("always valid json") + use json_write::JsonWrite as _; + + let mut buffer = String::new(); + buffer.open_object().unwrap(); + match self { + Self::DiscoverStart => { + buffer.key("event").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value("discover_start").unwrap(); + } + Self::DiscoverCase { name, mode, run } => { + buffer.key("event").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value("discover_case").unwrap(); + + buffer.val_sep().unwrap(); + buffer.key("name").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value(name).unwrap(); + + if !mode.is_default() { + buffer.val_sep().unwrap(); + buffer.key("mode").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value(mode.as_str()).unwrap(); + } + + if !run { + buffer.val_sep().unwrap(); + buffer.key("run").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value(run).unwrap(); + } + } + Self::DiscoverComplete { elapsed_s } => { + buffer.key("event").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value("discover_complete").unwrap(); + if let Some(elapsed_s) = elapsed_s { + buffer.val_sep().unwrap(); + buffer.key("elapsed_s").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value(String::from(*elapsed_s)).unwrap(); + } + } + Self::SuiteStart => { + buffer.key("event").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value("suite_start").unwrap(); + } + Self::CaseStart { name } => { + buffer.key("event").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value("case_start").unwrap(); + + buffer.val_sep().unwrap(); + buffer.key("name").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value(name).unwrap(); + } + Self::CaseComplete { + name, + mode, + status, + message, + elapsed_s, + } => { + buffer.key("event").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value("case_complete").unwrap(); + + buffer.val_sep().unwrap(); + buffer.key("name").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value(name).unwrap(); + + if !mode.is_default() { + buffer.val_sep().unwrap(); + buffer.key("mode").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value(mode.as_str()).unwrap(); + } + + if let Some(status) = status { + buffer.val_sep().unwrap(); + buffer.key("status").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value(status.as_str()).unwrap(); + } + + if let Some(message) = message { + buffer.val_sep().unwrap(); + buffer.key("message").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value(message).unwrap(); + } + + if let Some(elapsed_s) = elapsed_s { + buffer.val_sep().unwrap(); + buffer.key("elapsed_s").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value(String::from(*elapsed_s)).unwrap(); + } + } + Self::SuiteComplete { elapsed_s } => { + buffer.key("event").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value("suite_complete").unwrap(); + if let Some(elapsed_s) = elapsed_s { + buffer.val_sep().unwrap(); + buffer.key("elapsed_s").unwrap(); + buffer.keyval_sep().unwrap(); + buffer.value(String::from(*elapsed_s)).unwrap(); + } + } + } + buffer.close_object().unwrap(); + + buffer } } @@ -110,6 +228,15 @@ pub enum RunStatus { Failed, } +impl RunStatus { + pub fn as_str(&self) -> &str { + match self { + Self::Ignored => "ignored", + Self::Failed => "failed", + } + } +} + #[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] #[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]