feat: add display UI with splash screen, dashboard, and RSSI sparkline

Implement full SSD1306 OLED display support:
- Splash screen on boot (1.5s) with project name, version, board
- Dashboard showing radio state, config, counters, RSSI/SNR
- RSSI history sparkline (64-sample ring buffer, 2px-wide bars)
- DisplayOn/DisplayOff host commands routed via dedicated channel
- Pure render functions against generic DrawTarget (no hardware coupling)
- Display on/off control bypasses radio task entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

+1
View File
@@ -0,0 +1 @@
{"files":{".cargo_vcs_info.json":"72692ac658d7a52aa23c651f3b480ef4e0e94ab83a6aec620ea36ea40a972253",".github/workflows/ci.yml":"5a0da1f9728e84c8353b3099bf952f06177024877d6a00b088fa05b53e5a7065",".taplo.toml":"378370a906f719c8dc33bee3083b8ca62b6b11b42209f31ab354f246d9d59dd3","Cargo.lock":"113516ab8136995c417856edd53364ed8df08f565c06d900512e61e97ae79486","Cargo.toml":"b3308bdf41b05098e30cd18a45acacbbe4a92e0c6e1405898615978a1c61e5f7","Cargo.toml.orig":"a664c4dca28108ea2e8977735b91140e7281eb1cabdfffe750c29a6f2d646c26","LICENSE-APACHE":"62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a","LICENSE-MIT":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3","README.md":"5619ae83205466278a91e52d0d0ff050440754a847acef802edfd8ed9ab73e0a","examples/decode.rs":"c6b00634e2d59589f47777b9012672856e8a257be84b081fb88f2ec4e4e41cc8","src/de.rs":"95385155e7f1425d086b5fef2941b4df28038fc21743d03871a29c2f2a9d6938","src/error.rs":"59798d362c2794420801a9dd951916f58163312ec133ab48a2c1ecd4c445b5d7","src/lib.rs":"453e8dc2a7f17829f68009ddcec79c49fefc921bfcac4f2ec48805b217a7da5a","src/ser.rs":"5d2b9cc71dcf7b072095a9c63d923384f351bb1b1f869be4afbe03ac65666013","src/tokens.rs":"700d844e20e678d9ead1ae9ddcf427f7249c66d803730deebc39583098dbd22c","tests/README.md":"3e7d07d64c34e117c3d862ee12e4947af8c133fb0b53f6f0de43cc3bfa5057b8","tests/datetime.rs":"b2effdea59ad0b1f856bb67ad3b855e34fea2ef3ddd2453d48d6d30f2b62104a","tests/de-errors.rs":"3ad1abe2153645facfb504dfa1cbfcca2db8cbd9cb0261b98aa20f99c5a5d366","tests/display-tricky.rs":"240f050d9bdf4693d2dec5b64c8ee616c2fb1acff3addaad4da582bf9ea03295","tests/enum_external_deserialize.rs":"1ea8bd4585d76c03ca15136d7ffa44348e235a57ca846180cf20466e5c8edf22","tests/float.rs":"7fd288dbf32764b0456038ca802386b2830bb27687a089c88d51b8861776594b","tests/formatting.rs":"d6904ef6d7689beee0751da6d4d6b74fec4d4ce469234894f4a06b345b7ae620","tests/invalid-encoder/array-mixed-types-ints-and-floats.json":"206d2a353153efbcee74e7ee7b0f852e7a948cfb88417f74607b3ad6f020e373","tests/invalid-misc.rs":"49b5d2664647a6dc31d662565d5972da3d9253a1fb5146584ea79b9c0b6379a6","tests/invalid.rs":"17ada3847954c45a56907d125aafe381bf2fb8cb3d6586fc7a10055b3857b2df","tests/invalid/datetime-malformed-no-leads.toml":"1fa6bf72ea0b96be835ac031408f7acfd0b8d18d5f54b4c0fce8136aad27d22e","tests/invalid/datetime-malformed-no-secs.toml":"b9c30b7fa1847c80e381d68cc79834eefbb8813cd0eff465fb7cbe0733df429b","tests/invalid/datetime-malformed-no-t.toml":"490a46d2e707ef4f87987ec12a22afa5a71c97c5eaa0f4201a174c47846c6a4a","tests/invalid/datetime-malformed-with-milli.toml":"62bfd0a6645bcb3f78211523e5673a3d1fa726d9e942c1643df243d6fba474c8","tests/invalid/duplicate-key-table.toml":"a896ea2180d16fcdd4f6c30aa27529b5b29e7e195a4e7a9944343028783602e9","tests/invalid/duplicate-keys.toml":"4bb0a65c22e7704893a2a51334eb2518af702850ef59aeecd5226e7b26bc07ec","tests/invalid/duplicate-table.toml":"449a36439d737320604c96b2fb86e796195f4801446dfd294c32dcb6ab3a7add","tests/invalid/duplicate-tables.toml":"23b16ce26e1155ee6bf1fff559701dce86475c6a2878536b61f6b7e68be340de","tests/invalid/empty-implicit-table.toml":"d6026110dc0dee7babd69592218819328caa425f48cc879e895b29e48550c06c","tests/invalid/empty-table.toml":"37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570","tests/invalid/float-no-leading-zero.toml":"159654461094c938574ba2d2d09baa3d3c387dd6ed024fd411530c0573a1ec42","tests/invalid/float-no-suffix.toml":"4d6ce54a312e2da26b89c47e19e2c9002ce5fcfe59ec1201f17e240369429d46","tests/invalid/float-no-trailing-digits.toml":"64e4f336186cd096be2804532dbd694dd50ea551d292a9cdbf0bef2abf227101","tests/invalid/key-after-array.toml":"314af33770170b53bf2ec3be43ea1609d981c81d62c968052499b85ed54ccce8","tests/invalid/key-after-table.toml":"ed0dcf38f003d184dd18d1518702da0115cbfb05a5a28cbcf42de2f9bdee05fa","tests/invalid/key-empty.toml":"4303477abf4c0b920b42e96edd61caecf9c1f2d5d97f56af876854cd725aff3c","tests/invalid/key-hash.toml":"cd2a2eba6032d32c829633d3cd2778aeba81f5ff554e69f62af6557d1dc712f6","tests/invalid/key-newline.toml":"06a313a6bd70c2db6a1f5bda479d854d8f87e037e3cabf18fb5db822466ffcac","tests/invalid/key-open-bracket.toml":"52dea939022949290e3a19f1291d15605429344dce3cd1aa1f1568ecad8ca365","tests/invalid/key-single-open-bracket.toml":"245843abef9e72e7efac30138a994bf6301e7e1d7d7042a33d42e863d2638811","tests/invalid/key-space.toml":"b4e336d07c27fb3d0f0a6e50b733e1546202dfd58aaf2f7956f56fd6f075b0a1","tests/invalid/key-start-bracket.toml":"3bd3748a9df1d25ab2661330a3da187bd4da3958292bbf0e8b59d7963634dd87","tests/invalid/key-two-equals.toml":"3ac0c4e339d47c86e57564e43147b772ae83933b78083dc78d0ef77e231df3f1","tests/invalid/string-bad-byte-escape.toml":"c665dcec7c02f442c4fdc80423698eed2376ce65779cf855371772293bec2927","tests/invalid/string-bad-escape.toml":"eeca691fbba3d270f58ae2953d2d1369a773e619e39d8c11f38d6bf6f8472e82","tests/invalid/string-bad-line-ending-escape.toml":"ffa1673378cf4c4153d7f139ea8e4628c1f0aaa721c94b8766a1a3822fb080e4","tests/invalid/string-byte-Line truncated
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "c54c9bd3ade1a609f5da6e31cf6ee72d6c7552fb"
},
"path_in_vcs": ""
}
+104
View File
@@ -0,0 +1,104 @@
name: CI
on:
push:
pull_request:
workflow_dispatch:
schedule: [cron: "40 1 * * *"]
permissions:
contents: read
env:
RUSTFLAGS: -Dwarnings
jobs:
pre_ci:
uses: dtolnay/.github/.github/workflows/pre_ci.yml@master
test:
name: Rust ${{matrix.rust}}
needs: pre_ci
if: needs.pre_ci.outputs.continue
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
rust: [nightly, beta, stable, 1.56.0]
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{matrix.rust}}
- name: Enable type layout randomization
run: echo RUSTFLAGS=${RUSTFLAGS}\ -Zrandomize-layout >> $GITHUB_ENV
if: matrix.rust == 'nightly'
- run: cargo test
- uses: actions/upload-artifact@v4
if: matrix.rust == 'nightly' && always()
with:
name: Cargo.lock
path: Cargo.lock
continue-on-error: true
minimal:
name: Minimal versions
needs: pre_ci
if: needs.pre_ci.outputs.continue
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- run: cargo generate-lockfile -Z minimal-versions
- run: cargo check --locked
doc:
name: Documentation
needs: pre_ci
if: needs.pre_ci.outputs.continue
runs-on: ubuntu-latest
timeout-minutes: 45
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: dtolnay/install@cargo-docs-rs
- run: cargo docs-rs
clippy:
name: Clippy
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@clippy
- run: cargo clippy --tests -- -Dclippy::all -Dclippy::pedantic
miri:
name: Miri
needs: pre_ci
if: needs.pre_ci.outputs.continue
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@miri
- run: cargo miri setup
- run: cargo miri test
env:
MIRIFLAGS: -Zmiri-strict-provenance
outdated:
name: Outdated
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/install@cargo-outdated
- run: cargo outdated --workspace --exit-code 1
+4
View File
@@ -0,0 +1,4 @@
exclude = ["tests/**"]
[formatting]
array_auto_expand = false
+107
View File
@@ -0,0 +1,107 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "basic-toml"
version = "0.1.10"
dependencies = [
"semver",
"serde",
"serde_derive",
"serde_json",
]
[[package]]
name = "itoa"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "proc-macro2"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "semver"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
dependencies = [
"serde",
]
[[package]]
name = "serde"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
+117
View File
@@ -0,0 +1,117 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "basic-toml"
version = "0.1.10"
authors = [
"Alex Crichton <alex@alexcrichton.com>",
"David Tolnay <dtolnay@gmail.com>",
]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Minimal TOML library with few dependencies"
documentation = "https://docs.rs/basic-toml"
readme = "README.md"
keywords = [
"toml",
"serde",
]
categories = [
"config",
"encoding",
"parser-implementations",
]
license = "MIT OR Apache-2.0"
repository = "https://github.com/dtolnay/basic-toml"
[package.metadata.docs.rs]
rustdoc-args = [
"--generate-link-to-definition",
"--extern-html-root-url=core=https://doc.rust-lang.org",
"--extern-html-root-url=alloc=https://doc.rust-lang.org",
"--extern-html-root-url=std=https://doc.rust-lang.org",
]
targets = ["x86_64-unknown-linux-gnu"]
[lib]
name = "basic_toml"
path = "src/lib.rs"
[[example]]
name = "decode"
path = "examples/decode.rs"
[[test]]
name = "datetime"
path = "tests/datetime.rs"
[[test]]
name = "de-errors"
path = "tests/de-errors.rs"
[[test]]
name = "display-tricky"
path = "tests/display-tricky.rs"
[[test]]
name = "enum_external_deserialize"
path = "tests/enum_external_deserialize.rs"
[[test]]
name = "float"
path = "tests/float.rs"
[[test]]
name = "formatting"
path = "tests/formatting.rs"
[[test]]
name = "invalid"
path = "tests/invalid.rs"
[[test]]
name = "invalid-misc"
path = "tests/invalid-misc.rs"
[[test]]
name = "parser"
path = "tests/parser.rs"
[[test]]
name = "tokens"
path = "tests/tokens.rs"
[[test]]
name = "valid"
path = "tests/valid.rs"
[dependencies.serde]
version = "1.0.194"
[dev-dependencies.semver]
version = "1.0.17"
features = ["serde"]
[dev-dependencies.serde]
version = "1.0.194"
features = ["derive"]
[dev-dependencies.serde_derive]
version = "1.0.194"
[dev-dependencies.serde_json]
version = "1.0.99"
+176
View File
@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+23
View File
@@ -0,0 +1,23 @@
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+36
View File
@@ -0,0 +1,36 @@
# basic-toml
[<img alt="github" src="https://img.shields.io/badge/github-dtolnay/basic--toml-8da0cb?style=for-the-badge&labelColor=555555&logo=github" height="20">](https://github.com/dtolnay/basic-toml)
[<img alt="crates.io" src="https://img.shields.io/crates/v/basic-toml.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/basic-toml)
[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-basic--toml-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs" height="20">](https://docs.rs/basic-toml)
[<img alt="build status" src="https://img.shields.io/github/actions/workflow/status/dtolnay/basic-toml/ci.yml?branch=master&style=for-the-badge" height="20">](https://github.com/dtolnay/basic-toml/actions?query=branch%3Amaster)
A library for parsing and producing data in [TOML] format using [Serde].
This crate is a stripped down fork of version 0.5 of the `toml` crate (from
before the `toml_edit` rewrite).
[TOML]: https://toml.io
[Serde]: https://serde.rs
```toml
[dependencies]
basic-toml = "0.1"
```
<br>
#### License
<sup>
Licensed under either of <a href="LICENSE-APACHE">Apache License, Version
2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option.
</sup>
<br>
<sub>
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in this crate by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.
</sub>
+54
View File
@@ -0,0 +1,54 @@
//! An example showing off the usage of `Deserialize` to automatically decode
//! TOML into a Rust `struct`
#![deny(warnings)]
#![allow(dead_code)]
use serde_derive::Deserialize;
/// This is what we're going to decode into. Each field is optional, meaning
/// that it doesn't have to be present in TOML.
#[derive(Debug, Deserialize)]
struct Config {
global_string: Option<String>,
global_integer: Option<u64>,
server: Option<ServerConfig>,
peers: Option<Vec<PeerConfig>>,
}
/// Sub-structs are decoded from tables, so this will decode from the `[server]`
/// table.
///
/// Again, each field is optional, meaning they don't have to be present.
#[derive(Debug, Deserialize)]
struct ServerConfig {
ip: Option<String>,
port: Option<u64>,
}
#[derive(Debug, Deserialize)]
struct PeerConfig {
ip: Option<String>,
port: Option<u64>,
}
fn main() {
let toml_str = r#"
global_string = "test"
global_integer = 5
[server]
ip = "127.0.0.1"
port = 80
[[peers]]
ip = "127.0.0.1"
port = 8080
[[peers]]
ip = "127.0.0.1"
"#;
let decoded: Config = basic_toml::from_str(toml_str).unwrap();
println!("{:#?}", decoded);
}
+1694
View File
File diff suppressed because it is too large. Load diff
+54
View File
@@ -0,0 +1,54 @@
use std::fmt::{self, Debug, Display};
/// Errors that can occur when serializing or deserializing TOML.
pub struct Error(Box<ErrorInner>);
pub(crate) enum ErrorInner {
Ser(crate::ser::Error),
De(crate::de::Error),
}
impl Error {
/// Produces a (line, column) pair of the position of the error if
/// available.
///
/// All indexes are 0-based.
pub fn line_col(&self) -> Option<(usize, usize)> {
match &*self.0 {
ErrorInner::Ser(_) => None,
ErrorInner::De(error) => error.line_col(),
}
}
}
impl From<crate::ser::Error> for Error {
fn from(error: crate::ser::Error) -> Self {
Error(Box::new(ErrorInner::Ser(error)))
}
}
impl From<crate::de::Error> for Error {
fn from(error: crate::de::Error) -> Self {
Error(Box::new(ErrorInner::De(error)))
}
}
impl Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match &*self.0 {
ErrorInner::Ser(error) => Display::fmt(error, formatter),
ErrorInner::De(error) => Display::fmt(error, formatter),
}
}
}
impl Debug for Error {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match &*self.0 {
ErrorInner::Ser(error) => Debug::fmt(error, formatter),
ErrorInner::De(error) => Debug::fmt(error, formatter),
}
}
}
impl std::error::Error for Error {}
+144
View File
@@ -0,0 +1,144 @@
//! [![github]](https://github.com/dtolnay/basic-toml)&ensp;[![crates-io]](https://crates.io/crates/basic-toml)&ensp;[![docs-rs]](https://docs.rs/basic-toml)
//!
//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
//!
//! <br>
//!
//! A library for parsing and producing data in [TOML] format using [Serde].
//!
//! TOML is designed to be "a config file format for humans": minimal and easy
//! to read due to obvious semantics.
//!
//! ```toml
//! [package]
//! name = "basic-toml"
#![doc = concat!("version = \"", env!("CARGO_PKG_VERSION_MAJOR"), ".", env!("CARGO_PKG_VERSION_MINOR"), ".", env!("CARGO_PKG_VERSION_PATCH"), "\"")]
//! authors = ["Alex Crichton <alex@alexcrichton.com>"]
//!
//! [dependencies]
//! serde = "1.0"
//! ```
//!
//! The TOML format is widely used throughout the Rust community for
//! configuration, notably being used by [Cargo], Rust's package manager.
//!
//! [TOML]: https://toml.io
//! [Serde]: https://serde.rs
//! [Cargo]: https://crates.io
//!
//! # Deserialization
//!
//! ```
//! use semver::{Version, VersionReq};
//! use serde_derive::Deserialize;
//! use std::collections::BTreeMap as Map;
//!
//! #[derive(Deserialize)]
//! struct Manifest {
//! package: Package,
//! #[serde(default)]
//! dependencies: Map<String, VersionReq>,
//! }
//!
//! #[derive(Deserialize)]
//! struct Package {
//! name: String,
//! version: Version,
//! #[serde(default)]
//! authors: Vec<String>,
//! }
//!
//! fn main() {
//! let manifest: Manifest = basic_toml::from_str(r#"
//! [package]
//! name = "basic-toml"
#![doc = concat!(" version = \"", env!("CARGO_PKG_VERSION_MAJOR"), ".", env!("CARGO_PKG_VERSION_MINOR"), ".", env!("CARGO_PKG_VERSION_PATCH"), "\"")]
//! authors = ["Alex Crichton <alex@alexcrichton.com>"]
//!
//! [dependencies]
//! serde = "^1.0"
//! "#).unwrap();
//!
//! assert_eq!(manifest.package.name, "basic-toml");
#![doc = concat!(" assert_eq!(manifest.package.version, Version::new(", env!("CARGO_PKG_VERSION_MAJOR"), ", ", env!("CARGO_PKG_VERSION_MINOR"), ", ", env!("CARGO_PKG_VERSION_PATCH"), "));")]
//! assert_eq!(manifest.package.authors, ["Alex Crichton <alex@alexcrichton.com>"]);
//! assert_eq!(manifest.dependencies["serde"].to_string(), "^1.0");
//! }
//! ```
//!
//! # Serialization
//!
//! ```
//! use semver::{Version, VersionReq};
//! use serde_derive::Serialize;
//! use std::collections::BTreeMap as Map;
//!
//! #[derive(Serialize)]
//! struct Manifest {
//! package: Package,
//! dependencies: Map<String, VersionReq>,
//! }
//!
//! #[derive(Serialize)]
//! struct Package {
//! name: String,
//! version: Version,
//! authors: Vec<String>,
//! }
//!
//! fn main() {
//! let manifest = Manifest {
//! package: Package {
//! name: "basic-toml".to_owned(),
#![doc = concat!(" version: Version::new(", env!("CARGO_PKG_VERSION_MAJOR"), ", ", env!("CARGO_PKG_VERSION_MINOR"), ", ", env!("CARGO_PKG_VERSION_PATCH"), "),")]
//! authors: vec!["Alex Crichton <alex@alexcrichton.com>".to_owned()],
//! },
//! dependencies: {
//! let mut dependencies = Map::new();
//! dependencies.insert("serde".to_owned(), "^1.0".parse().unwrap());
//! dependencies
//! },
//! };
//!
//! let toml = basic_toml::to_string(&manifest).unwrap();
//! print!("{}", toml);
//! }
//! ```
//!
//! # Spec compatibility
//!
//! TOML v0.5.0.
//!
//! TOML's date and time syntax are not supported.
#![doc(html_root_url = "https://docs.rs/basic-toml/0.1.10")]
#![deny(missing_docs)]
#![allow(
clippy::bool_to_int_with_if,
clippy::elidable_lifetime_names,
clippy::let_underscore_untyped,
clippy::manual_let_else,
clippy::manual_range_contains,
clippy::match_like_matches_macro,
clippy::missing_errors_doc,
clippy::must_use_candidate,
clippy::needless_doctest_main,
clippy::needless_lifetimes,
clippy::needless_pass_by_value,
clippy::similar_names,
clippy::type_complexity,
clippy::uninlined_format_args,
clippy::unnecessary_box_returns,
clippy::unwrap_or_default
)]
mod de;
mod error;
mod ser;
mod tokens;
pub use crate::de::{from_slice, from_str};
pub use crate::error::Error;
pub use crate::ser::to_string;
+833
View File
@@ -0,0 +1,833 @@
use serde::ser::{self, Serialize};
use std::cell::Cell;
use std::error;
use std::fmt::{self, Display, Write};
/// Serialize the given data structure as a String of TOML.
///
/// Serialization can fail if `T`'s implementation of `Serialize` decides to
/// fail, if `T` contains a map with non-string keys, or if `T` attempts to
/// serialize an unsupported datatype such as an enum, tuple, or tuple struct.
pub fn to_string<T>(value: &T) -> Result<String, crate::Error>
where
T: ?Sized + Serialize,
{
let mut dst = String::with_capacity(128);
value.serialize(&mut Serializer::new(&mut dst))?;
Ok(dst)
}
#[derive(Debug)]
pub(crate) enum Error {
/// Indicates that a Rust type was requested to be serialized but it was not
/// supported.
///
/// Currently the TOML format does not support serializing types such as
/// enums, tuples and tuple structs.
UnsupportedType,
/// The key of all TOML maps must be strings, but serialization was
/// attempted where the key of a map was not a string.
KeyNotString,
/// All values in a TOML table must be emitted before further tables are
/// emitted. If a value is emitted *after* a table then this error is
/// generated.
ValueAfterTable,
/// None was attempted to be serialized, but it's not supported.
UnsupportedNone,
/// A custom error which could be generated when serializing a particular
/// type.
Custom(String),
}
struct Serializer<'a> {
dst: &'a mut String,
state: State<'a>,
}
#[derive(Debug, Copy, Clone)]
enum ArrayState {
Started,
StartedAsATable,
}
#[derive(Debug, Clone)]
enum State<'a> {
Table {
key: &'a str,
parent: &'a State<'a>,
first: &'a Cell<bool>,
table_emitted: &'a Cell<bool>,
},
Array {
parent: &'a State<'a>,
first: &'a Cell<bool>,
type_: &'a Cell<Option<ArrayState>>,
len: Option<usize>,
},
End,
}
struct SerializeSeq<'a, 'b> {
ser: &'b mut Serializer<'a>,
first: Cell<bool>,
type_: Cell<Option<ArrayState>>,
len: Option<usize>,
}
struct SerializeTable<'a, 'b> {
ser: &'b mut Serializer<'a>,
key: String,
first: Cell<bool>,
table_emitted: Cell<bool>,
}
impl<'a> Serializer<'a> {
fn new(dst: &'a mut String) -> Serializer<'a> {
Serializer {
dst,
state: State::End,
}
}
fn display<T: Display>(&mut self, t: T, type_: ArrayState) -> Result<(), Error> {
self.emit_key(type_)?;
write!(self.dst, "{}", t).map_err(ser::Error::custom)?;
if let State::Table { .. } = self.state {
self.dst.push('\n');
}
Ok(())
}
fn emit_key(&mut self, type_: ArrayState) -> Result<(), Error> {
self.array_type(type_);
let state = self.state.clone();
self.do_emit_key(&state)
}
// recursive implementation of `emit_key` above
fn do_emit_key(&mut self, state: &State) -> Result<(), Error> {
match *state {
State::End => Ok(()),
State::Array {
parent,
first,
type_,
len,
} => {
assert!(type_.get().is_some());
if first.get() {
self.do_emit_key(parent)?;
}
self.emit_array(first, len);
Ok(())
}
State::Table {
parent,
first,
table_emitted,
key,
} => {
if table_emitted.get() {
return Err(Error::ValueAfterTable);
}
if first.get() {
self.emit_table_header(parent)?;
first.set(false);
}
self.escape_key(key)?;
self.dst.push_str(" = ");
Ok(())
}
}
}
fn emit_array(&mut self, first: &Cell<bool>, _len: Option<usize>) {
if first.get() {
self.dst.push('[');
} else {
self.dst.push_str(", ");
}
}
fn array_type(&mut self, type_: ArrayState) {
let prev = match self.state {
State::Array { type_, .. } => type_,
_ => return,
};
if prev.get().is_none() {
prev.set(Some(type_));
}
}
fn escape_key(&mut self, key: &str) -> Result<(), Error> {
let ok = !key.is_empty()
&& key.chars().all(|c| match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => true,
_ => false,
});
if ok {
write!(self.dst, "{}", key).map_err(ser::Error::custom)?;
} else {
self.emit_str(key)?;
}
Ok(())
}
fn emit_str(&mut self, value: &str) -> Result<(), Error> {
self.dst.push('"');
for ch in value.chars() {
match ch {
'\u{8}' => self.dst.push_str("\\b"),
'\u{9}' => self.dst.push_str("\\t"),
'\u{a}' => self.dst.push_str("\\n"),
'\u{c}' => self.dst.push_str("\\f"),
'\u{d}' => self.dst.push_str("\\r"),
'\u{22}' => self.dst.push_str("\\\""),
'\u{5c}' => self.dst.push_str("\\\\"),
c if c <= '\u{1f}' || c == '\u{7f}' => {
write!(self.dst, "\\u{:04X}", ch as u32).map_err(ser::Error::custom)?;
}
ch => self.dst.push(ch),
}
}
self.dst.push('"');
Ok(())
}
fn emit_table_header(&mut self, state: &State) -> Result<(), Error> {
let array_of_tables = match *state {
State::End => return Ok(()),
State::Array { .. } => true,
State::Table { .. } => false,
};
// Unlike [..]s, we can't omit [[..]] ancestors, so be sure to emit
// table headers for them.
let mut p = state;
if let State::Array { first, parent, .. } = *state {
if first.get() {
p = parent;
}
}
while let State::Table { first, parent, .. } = *p {
p = parent;
if !first.get() {
break;
}
if let State::Array {
parent: &State::Table { .. },
..
} = *parent
{
self.emit_table_header(parent)?;
break;
}
}
match *state {
State::Table { first, .. } => {
if !first.get() {
// Newline if we are a table that is not the first table in
// the document.
self.dst.push('\n');
}
}
State::Array { parent, first, .. } => {
if !first.get() {
// Always newline if we are not the first item in the
// table-array
self.dst.push('\n');
} else if let State::Table { first, .. } = *parent {
if !first.get() {
// Newline if we are not the first item in the document
self.dst.push('\n');
}
}
}
State::End => {}
}
self.dst.push('[');
if array_of_tables {
self.dst.push('[');
}
self.emit_key_part(state)?;
if array_of_tables {
self.dst.push(']');
}
self.dst.push_str("]\n");
Ok(())
}
fn emit_key_part(&mut self, key: &State) -> Result<bool, Error> {
match *key {
State::Array { parent, .. } => self.emit_key_part(parent),
State::End => Ok(true),
State::Table {
key,
parent,
table_emitted,
..
} => {
table_emitted.set(true);
let first = self.emit_key_part(parent)?;
if !first {
self.dst.push('.');
}
self.escape_key(key)?;
Ok(false)
}
}
}
}
macro_rules! serialize_float {
($this:expr, $v:expr) => {{
$this.emit_key(ArrayState::Started)?;
match ($v.is_sign_negative(), $v.is_nan(), $v == 0.0) {
(_, true, _) => write!($this.dst, "nan"),
(true, false, true) => write!($this.dst, "-0.0"),
(false, false, true) => write!($this.dst, "0.0"),
(_, false, false) => write!($this.dst, "{}", $v).and_then(|()| {
if $v % 1.0 == 0.0 {
write!($this.dst, ".0")
} else {
Ok(())
}
}),
}
.map_err(ser::Error::custom)?;
if let State::Table { .. } = $this.state {
$this.dst.push_str("\n");
}
return Ok(());
}};
}
impl<'a, 'b> ser::Serializer for &'b mut Serializer<'a> {
type Ok = ();
type Error = Error;
type SerializeSeq = SerializeSeq<'a, 'b>;
type SerializeTuple = SerializeSeq<'a, 'b>;
type SerializeTupleStruct = SerializeSeq<'a, 'b>;
type SerializeTupleVariant = ser::Impossible<(), Error>;
type SerializeMap = SerializeTable<'a, 'b>;
type SerializeStruct = SerializeTable<'a, 'b>;
type SerializeStructVariant = ser::Impossible<(), Error>;
fn serialize_bool(self, v: bool) -> Result<(), Self::Error> {
self.display(v, ArrayState::Started)
}
fn serialize_i8(self, v: i8) -> Result<(), Self::Error> {
self.display(v, ArrayState::Started)
}
fn serialize_i16(self, v: i16) -> Result<(), Self::Error> {
self.display(v, ArrayState::Started)
}
fn serialize_i32(self, v: i32) -> Result<(), Self::Error> {
self.display(v, ArrayState::Started)
}
fn serialize_i64(self, v: i64) -> Result<(), Self::Error> {
self.display(v, ArrayState::Started)
}
fn serialize_u8(self, v: u8) -> Result<(), Self::Error> {
self.display(v, ArrayState::Started)
}
fn serialize_u16(self, v: u16) -> Result<(), Self::Error> {
self.display(v, ArrayState::Started)
}
fn serialize_u32(self, v: u32) -> Result<(), Self::Error> {
self.display(v, ArrayState::Started)
}
fn serialize_u64(self, v: u64) -> Result<(), Self::Error> {
self.display(v, ArrayState::Started)
}
fn serialize_f32(self, v: f32) -> Result<(), Self::Error> {
serialize_float!(self, v)
}
fn serialize_f64(self, v: f64) -> Result<(), Self::Error> {
serialize_float!(self, v)
}
fn serialize_char(self, v: char) -> Result<(), Self::Error> {
let mut buf = [0; 4];
self.serialize_str(v.encode_utf8(&mut buf))
}
fn serialize_str(self, value: &str) -> Result<(), Self::Error> {
self.emit_key(ArrayState::Started)?;
self.emit_str(value)?;
if let State::Table { .. } = self.state {
self.dst.push('\n');
}
Ok(())
}
fn serialize_bytes(self, value: &[u8]) -> Result<(), Self::Error> {
value.serialize(self)
}
fn serialize_none(self) -> Result<(), Self::Error> {
Err(Error::UnsupportedNone)
}
fn serialize_some<T>(self, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(self)
}
fn serialize_unit(self) -> Result<(), Self::Error> {
Err(Error::UnsupportedType)
}
fn serialize_unit_struct(self, _name: &'static str) -> Result<(), Self::Error> {
Err(Error::UnsupportedType)
}
fn serialize_unit_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
) -> Result<(), Self::Error> {
self.serialize_str(variant)
}
fn serialize_newtype_struct<T>(self, _name: &'static str, value: &T) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(self)
}
fn serialize_newtype_variant<T>(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_value: &T,
) -> Result<(), Self::Error>
where
T: ?Sized + Serialize,
{
Err(Error::UnsupportedType)
}
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
self.array_type(ArrayState::Started);
Ok(SerializeSeq {
ser: self,
first: Cell::new(true),
type_: Cell::new(None),
len,
})
}
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error> {
self.serialize_seq(Some(len))
}
fn serialize_tuple_struct(
self,
_name: &'static str,
len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error> {
self.serialize_seq(Some(len))
}
fn serialize_tuple_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
Err(Error::UnsupportedType)
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
self.array_type(ArrayState::StartedAsATable);
Ok(SerializeTable {
ser: self,
key: String::new(),
first: Cell::new(true),
table_emitted: Cell::new(false),
})
}
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
self.array_type(ArrayState::StartedAsATable);
Ok(SerializeTable {
ser: self,
key: String::new(),
first: Cell::new(true),
table_emitted: Cell::new(false),
})
}
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
Err(Error::UnsupportedType)
}
}
impl<'a, 'b> ser::SerializeSeq for SerializeSeq<'a, 'b> {
type Ok = ();
type Error = Error;
fn serialize_element<T>(&mut self, value: &T) -> Result<(), Error>
where
T: ?Sized + Serialize,
{
value.serialize(&mut Serializer {
dst: &mut *self.ser.dst,
state: State::Array {
parent: &self.ser.state,
first: &self.first,
type_: &self.type_,
len: self.len,
},
})?;
self.first.set(false);
Ok(())
}
fn end(self) -> Result<(), Error> {
match self.type_.get() {
Some(ArrayState::StartedAsATable) => return Ok(()),
Some(ArrayState::Started) => self.ser.dst.push(']'),
None => {
assert!(self.first.get());
self.ser.emit_key(ArrayState::Started)?;
self.ser.dst.push_str("[]");
}
}
if let State::Table { .. } = self.ser.state {
self.ser.dst.push('\n');
}
Ok(())
}
}
impl<'a, 'b> ser::SerializeTuple for SerializeSeq<'a, 'b> {
type Ok = ();
type Error = Error;
fn serialize_element<T>(&mut self, value: &T) -> Result<(), Error>
where
T: ?Sized + Serialize,
{
ser::SerializeSeq::serialize_element(self, value)
}
fn end(self) -> Result<(), Error> {
ser::SerializeSeq::end(self)
}
}
impl<'a, 'b> ser::SerializeTupleStruct for SerializeSeq<'a, 'b> {
type Ok = ();
type Error = Error;
fn serialize_field<T>(&mut self, value: &T) -> Result<(), Error>
where
T: ?Sized + Serialize,
{
ser::SerializeSeq::serialize_element(self, value)
}
fn end(self) -> Result<(), Error> {
ser::SerializeSeq::end(self)
}
}
impl<'a, 'b> ser::SerializeMap for SerializeTable<'a, 'b> {
type Ok = ();
type Error = Error;
fn serialize_key<T>(&mut self, input: &T) -> Result<(), Error>
where
T: ?Sized + Serialize,
{
self.key = input.serialize(StringExtractor)?;
Ok(())
}
fn serialize_value<T>(&mut self, value: &T) -> Result<(), Error>
where
T: ?Sized + Serialize,
{
let res = value.serialize(&mut Serializer {
dst: &mut *self.ser.dst,
state: State::Table {
key: &self.key,
parent: &self.ser.state,
first: &self.first,
table_emitted: &self.table_emitted,
},
});
match res {
Ok(()) => self.first.set(false),
Err(Error::UnsupportedNone) => {}
Err(e) => return Err(e),
}
Ok(())
}
fn end(self) -> Result<(), Error> {
if self.first.get() {
let state = self.ser.state.clone();
self.ser.emit_table_header(&state)?;
}
Ok(())
}
}
impl<'a, 'b> ser::SerializeStruct for SerializeTable<'a, 'b> {
type Ok = ();
type Error = Error;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error>
where
T: ?Sized + Serialize,
{
let res = value.serialize(&mut Serializer {
dst: &mut *self.ser.dst,
state: State::Table {
key,
parent: &self.ser.state,
first: &self.first,
table_emitted: &self.table_emitted,
},
});
match res {
Ok(()) => self.first.set(false),
Err(Error::UnsupportedNone) => {}
Err(e) => return Err(e),
}
Ok(())
}
fn end(self) -> Result<(), Error> {
if self.first.get() {
let state = self.ser.state.clone();
self.ser.emit_table_header(&state)?;
}
Ok(())
}
}
struct StringExtractor;
impl ser::Serializer for StringExtractor {
type Ok = String;
type Error = Error;
type SerializeSeq = ser::Impossible<String, Error>;
type SerializeTuple = ser::Impossible<String, Error>;
type SerializeTupleStruct = ser::Impossible<String, Error>;
type SerializeTupleVariant = ser::Impossible<String, Error>;
type SerializeMap = ser::Impossible<String, Error>;
type SerializeStruct = ser::Impossible<String, Error>;
type SerializeStructVariant = ser::Impossible<String, Error>;
fn serialize_bool(self, _v: bool) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_i8(self, _v: i8) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_i16(self, _v: i16) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_i32(self, _v: i32) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_i64(self, _v: i64) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_u8(self, _v: u8) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_u16(self, _v: u16) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_u32(self, _v: u32) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_u64(self, _v: u64) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_f32(self, _v: f32) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_f64(self, _v: f64) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_char(self, _v: char) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_str(self, value: &str) -> Result<String, Self::Error> {
Ok(value.to_string())
}
fn serialize_bytes(self, _value: &[u8]) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_none(self) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_some<T>(self, _value: &T) -> Result<String, Self::Error>
where
T: ?Sized + Serialize,
{
Err(Error::KeyNotString)
}
fn serialize_unit(self) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_unit_struct(self, _name: &'static str) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_unit_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
) -> Result<String, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_newtype_struct<T>(
self,
_name: &'static str,
value: &T,
) -> Result<String, Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(self)
}
fn serialize_newtype_variant<T>(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_value: &T,
) -> Result<String, Self::Error>
where
T: ?Sized + Serialize,
{
Err(Error::KeyNotString)
}
fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_tuple_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_tuple_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
Err(Error::KeyNotString)
}
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
Err(Error::KeyNotString)
}
}
impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Error::UnsupportedType => "unsupported Rust type".fmt(f),
Error::KeyNotString => "map key was not a string".fmt(f),
Error::ValueAfterTable => "values must be emitted before tables".fmt(f),
Error::UnsupportedNone => "unsupported None value".fmt(f),
Error::Custom(ref s) => s.fmt(f),
}
}
}
impl error::Error for Error {}
impl ser::Error for Error {
fn custom<T: Display>(msg: T) -> Error {
Error::Custom(msg.to_string())
}
}
+545
View File
@@ -0,0 +1,545 @@
use std::borrow::Cow;
use std::char;
use std::str;
/// A span, designating a range of bytes where a token is located.
#[derive(Eq, PartialEq, Debug, Clone, Copy)]
pub struct Span {
/// The start of the range.
pub start: usize,
/// The end of the range (exclusive).
pub end: usize,
}
impl From<Span> for (usize, usize) {
fn from(Span { start, end }: Span) -> (usize, usize) {
(start, end)
}
}
#[derive(Eq, PartialEq, Debug)]
pub enum Token<'a> {
Whitespace(&'a str),
Newline,
Comment(&'a str),
Equals,
Period,
Comma,
Colon,
Plus,
LeftBrace,
RightBrace,
LeftBracket,
RightBracket,
Keylike(&'a str),
String {
src: &'a str,
val: Cow<'a, str>,
multiline: bool,
},
}
#[derive(Eq, PartialEq, Debug)]
pub enum Error {
InvalidCharInString(usize, char),
InvalidEscape(usize, char),
InvalidHexEscape(usize, char),
InvalidEscapeValue(usize, u32),
NewlineInString(usize),
Unexpected(usize, char),
UnterminatedString(usize),
NewlineInTableKey(usize),
MultilineStringKey(usize),
Wanted {
at: usize,
expected: &'static str,
found: &'static str,
},
}
#[derive(Clone)]
pub struct Tokenizer<'a> {
input: &'a str,
chars: CrlfFold<'a>,
}
#[derive(Clone)]
struct CrlfFold<'a> {
chars: str::CharIndices<'a>,
}
#[derive(Debug)]
enum MaybeString {
NotEscaped(usize),
Owned(String),
}
impl<'a> Tokenizer<'a> {
pub fn new(input: &'a str) -> Tokenizer<'a> {
let mut t = Tokenizer {
input,
chars: CrlfFold {
chars: input.char_indices(),
},
};
// Eat utf-8 BOM
t.eatc('\u{feff}');
t
}
pub fn next(&mut self) -> Result<Option<(Span, Token<'a>)>, Error> {
let (start, token) = match self.one() {
Some((start, '\n')) => (start, Token::Newline),
Some((start, ' ' | '\t')) => (start, self.whitespace_token(start)),
Some((start, '#')) => (start, self.comment_token(start)),
Some((start, '=')) => (start, Token::Equals),
Some((start, '.')) => (start, Token::Period),
Some((start, ',')) => (start, Token::Comma),
Some((start, ':')) => (start, Token::Colon),
Some((start, '+')) => (start, Token::Plus),
Some((start, '{')) => (start, Token::LeftBrace),
Some((start, '}')) => (start, Token::RightBrace),
Some((start, '[')) => (start, Token::LeftBracket),
Some((start, ']')) => (start, Token::RightBracket),
Some((start, '\'')) => {
return self
.literal_string(start)
.map(|t| Some((self.step_span(start), t)))
}
Some((start, '"')) => {
return self
.basic_string(start)
.map(|t| Some((self.step_span(start), t)))
}
Some((start, ch)) if is_keylike(ch) => (start, self.keylike(start)),
Some((start, ch)) => return Err(Error::Unexpected(start, ch)),
None => return Ok(None),
};
let span = self.step_span(start);
Ok(Some((span, token)))
}
pub fn peek(&mut self) -> Result<Option<(Span, Token<'a>)>, Error> {
self.clone().next()
}
pub fn eat(&mut self, expected: Token<'a>) -> Result<bool, Error> {
self.eat_spanned(expected).map(|s| s.is_some())
}
/// Eat a value, returning it's span if it was consumed.
pub fn eat_spanned(&mut self, expected: Token<'a>) -> Result<Option<Span>, Error> {
let span = match self.peek()? {
Some((span, ref found)) if expected == *found => span,
Some(_) | None => return Ok(None),
};
drop(self.next());
Ok(Some(span))
}
pub fn expect(&mut self, expected: Token<'a>) -> Result<(), Error> {
// ignore span
let _ = self.expect_spanned(expected)?;
Ok(())
}
/// Expect the given token returning its span.
pub fn expect_spanned(&mut self, expected: Token<'a>) -> Result<Span, Error> {
let current = self.current();
match self.next()? {
Some((span, found)) => {
if expected == found {
Ok(span)
} else {
Err(Error::Wanted {
at: current,
expected: expected.describe(),
found: found.describe(),
})
}
}
None => Err(Error::Wanted {
at: self.input.len(),
expected: expected.describe(),
found: "eof",
}),
}
}
pub fn table_key(&mut self) -> Result<(Span, Cow<'a, str>), Error> {
let current = self.current();
match self.next()? {
Some((span, Token::Keylike(k))) => Ok((span, k.into())),
Some((
span,
Token::String {
src,
val,
multiline,
},
)) => {
let offset = self.substr_offset(src);
if multiline {
return Err(Error::MultilineStringKey(offset));
}
match src.find('\n') {
None => Ok((span, val)),
Some(i) => Err(Error::NewlineInTableKey(offset + i)),
}
}
Some((_, other)) => Err(Error::Wanted {
at: current,
expected: "a table key",
found: other.describe(),
}),
None => Err(Error::Wanted {
at: self.input.len(),
expected: "a table key",
found: "eof",
}),
}
}
pub fn eat_whitespace(&mut self) {
while self.eatc(' ') || self.eatc('\t') {
// ...
}
}
pub fn eat_comment(&mut self) -> Result<bool, Error> {
if !self.eatc('#') {
return Ok(false);
}
drop(self.comment_token(0));
self.eat_newline_or_eof().map(|()| true)
}
pub fn eat_newline_or_eof(&mut self) -> Result<(), Error> {
let current = self.current();
match self.next()? {
None | Some((_, Token::Newline)) => Ok(()),
Some((_, other)) => Err(Error::Wanted {
at: current,
expected: "newline",
found: other.describe(),
}),
}
}
pub fn skip_to_newline(&mut self) {
loop {
match self.one() {
Some((_, '\n')) | None => break,
_ => {}
}
}
}
fn eatc(&mut self, ch: char) -> bool {
match self.chars.clone().next() {
Some((_, ch2)) if ch == ch2 => {
self.one();
true
}
_ => false,
}
}
pub fn current(&mut self) -> usize {
match self.chars.clone().next() {
Some(i) => i.0,
None => self.input.len(),
}
}
fn whitespace_token(&mut self, start: usize) -> Token<'a> {
while self.eatc(' ') || self.eatc('\t') {
// ...
}
Token::Whitespace(&self.input[start..self.current()])
}
fn comment_token(&mut self, start: usize) -> Token<'a> {
while let Some((_, ch)) = self.chars.clone().next() {
if ch != '\t' && (ch < '\u{20}' || ch > '\u{10ffff}') {
break;
}
self.one();
}
Token::Comment(&self.input[start..self.current()])
}
fn read_string(
&mut self,
delim: char,
start: usize,
new_ch: &mut dyn FnMut(
&mut Tokenizer,
&mut MaybeString,
bool,
usize,
char,
) -> Result<(), Error>,
) -> Result<Token<'a>, Error> {
let mut multiline = false;
if self.eatc(delim) {
if self.eatc(delim) {
multiline = true;
} else {
return Ok(Token::String {
src: &self.input[start..start + 2],
val: Cow::Borrowed(""),
multiline: false,
});
}
}
let mut val = MaybeString::NotEscaped(self.current());
let mut n = 0;
loop {
n += 1;
match self.one() {
Some((i, '\n')) => {
if multiline {
if self.input.as_bytes()[i] == b'\r' {
val.make_owned(&self.input[..i]);
}
if n == 1 {
val = MaybeString::NotEscaped(self.current());
} else {
val.push('\n');
}
} else {
return Err(Error::NewlineInString(i));
}
}
Some((mut i, ch)) if ch == delim => {
if multiline {
if !self.eatc(delim) {
val.push(delim);
continue;
}
if !self.eatc(delim) {
val.push(delim);
val.push(delim);
continue;
}
if self.eatc(delim) {
val.push(delim);
i += 1;
}
if self.eatc(delim) {
val.push(delim);
i += 1;
}
}
return Ok(Token::String {
src: &self.input[start..self.current()],
val: val.into_cow(&self.input[..i]),
multiline,
});
}
Some((i, c)) => new_ch(self, &mut val, multiline, i, c)?,
None => return Err(Error::UnterminatedString(start)),
}
}
}
fn literal_string(&mut self, start: usize) -> Result<Token<'a>, Error> {
self.read_string('\'', start, &mut |_me, val, _multi, i, ch| {
if ch == '\u{09}' || ('\u{20}' <= ch && ch <= '\u{10ffff}' && ch != '\u{7f}') {
val.push(ch);
Ok(())
} else {
Err(Error::InvalidCharInString(i, ch))
}
})
}
fn basic_string(&mut self, start: usize) -> Result<Token<'a>, Error> {
self.read_string('"', start, &mut |me, val, multi, i, ch| match ch {
'\\' => {
val.make_owned(&me.input[..i]);
match me.chars.next() {
Some((_, '"')) => val.push('"'),
Some((_, '\\')) => val.push('\\'),
Some((_, 'b')) => val.push('\u{8}'),
Some((_, 'f')) => val.push('\u{c}'),
Some((_, 'n')) => val.push('\n'),
Some((_, 'r')) => val.push('\r'),
Some((_, 't')) => val.push('\t'),
Some((i, c @ ('u' | 'U'))) => {
let len = if c == 'u' { 4 } else { 8 };
val.push(me.hex(start, i, len)?);
}
Some((i, c @ (' ' | '\t' | '\n'))) if multi => {
if c != '\n' {
while let Some((_, ch)) = me.chars.clone().next() {
match ch {
' ' | '\t' => {
me.chars.next();
}
'\n' => {
me.chars.next();
break;
}
_ => return Err(Error::InvalidEscape(i, c)),
}
}
}
while let Some((_, ch)) = me.chars.clone().next() {
match ch {
' ' | '\t' | '\n' => {
me.chars.next();
}
_ => break,
}
}
}
Some((i, c)) => return Err(Error::InvalidEscape(i, c)),
None => return Err(Error::UnterminatedString(start)),
}
Ok(())
}
ch if ch == '\u{09}' || ('\u{20}' <= ch && ch <= '\u{10ffff}' && ch != '\u{7f}') => {
val.push(ch);
Ok(())
}
_ => Err(Error::InvalidCharInString(i, ch)),
})
}
fn hex(&mut self, start: usize, i: usize, len: usize) -> Result<char, Error> {
let mut buf = String::with_capacity(len);
for _ in 0..len {
match self.one() {
Some((_, ch)) if ch as u32 <= 0x7F && ch.is_ascii_hexdigit() => buf.push(ch),
Some((i, ch)) => return Err(Error::InvalidHexEscape(i, ch)),
None => return Err(Error::UnterminatedString(start)),
}
}
let val = u32::from_str_radix(&buf, 16).unwrap();
match char::from_u32(val) {
Some(ch) => Ok(ch),
None => Err(Error::InvalidEscapeValue(i, val)),
}
}
fn keylike(&mut self, start: usize) -> Token<'a> {
while let Some((_, ch)) = self.peek_one() {
if !is_keylike(ch) {
break;
}
self.one();
}
Token::Keylike(&self.input[start..self.current()])
}
pub fn substr_offset(&self, s: &'a str) -> usize {
assert!(s.len() <= self.input.len());
let a = self.input.as_ptr() as usize;
let b = s.as_ptr() as usize;
assert!(a <= b);
b - a
}
/// Calculate the span of a single character.
fn step_span(&mut self, start: usize) -> Span {
let end = match self.peek_one() {
Some(t) => t.0,
None => self.input.len(),
};
Span { start, end }
}
/// Peek one char without consuming it.
fn peek_one(&mut self) -> Option<(usize, char)> {
self.chars.clone().next()
}
/// Take one char.
pub fn one(&mut self) -> Option<(usize, char)> {
self.chars.next()
}
}
impl<'a> Iterator for CrlfFold<'a> {
type Item = (usize, char);
fn next(&mut self) -> Option<(usize, char)> {
self.chars.next().map(|(i, c)| {
if c == '\r' {
let mut attempt = self.chars.clone();
if let Some((_, '\n')) = attempt.next() {
self.chars = attempt;
return (i, '\n');
}
}
(i, c)
})
}
}
impl MaybeString {
fn push(&mut self, ch: char) {
match *self {
MaybeString::NotEscaped(..) => {}
MaybeString::Owned(ref mut s) => s.push(ch),
}
}
fn make_owned(&mut self, input: &str) {
match *self {
MaybeString::NotEscaped(start) => {
*self = MaybeString::Owned(input[start..].to_owned());
}
MaybeString::Owned(..) => {}
}
}
fn into_cow(self, input: &str) -> Cow<str> {
match self {
MaybeString::NotEscaped(start) => Cow::Borrowed(&input[start..]),
MaybeString::Owned(s) => Cow::Owned(s),
}
}
}
fn is_keylike(ch: char) -> bool {
('A' <= ch && ch <= 'Z')
|| ('a' <= ch && ch <= 'z')
|| ('0' <= ch && ch <= '9')
|| ch == '-'
|| ch == '_'
}
impl<'a> Token<'a> {
pub fn describe(&self) -> &'static str {
match *self {
Token::Keylike(_) => "an identifier",
Token::Equals => "an equals",
Token::Period => "a period",
Token::Comment(_) => "a comment",
Token::Newline => "a newline",
Token::Whitespace(_) => "whitespace",
Token::Comma => "a comma",
Token::RightBrace => "a right brace",
Token::LeftBrace => "a left brace",
Token::RightBracket => "a right bracket",
Token::LeftBracket => "a left bracket",
Token::String { multiline, .. } => {
if multiline {
"a multiline string"
} else {
"a string"
}
}
Token::Colon => "a colon",
Token::Plus => "a plus",
}
}
}
+1
View File
@@ -0,0 +1 @@
Tests are from https://github.com/BurntSushi/toml-test
+142
View File
@@ -0,0 +1,142 @@
use serde_json::Value;
macro_rules! bad {
($toml:expr, $msg:expr) => {
match basic_toml::from_str::<Value>($toml) {
Ok(s) => panic!("parsed to: {:#?}", s),
Err(e) => assert_eq!(e.to_string(), $msg),
}
};
}
#[test]
fn times() {
fn multi_bad(s: &str, msg: &str) {
bad!(s, msg);
bad!(&s.replace('T', " "), msg);
bad!(&s.replace('T', "t"), msg);
bad!(&s.replace('Z', "z"), msg);
}
multi_bad(
"foo = 1997-09-09T09:09:09Z",
"invalid number at line 1 column 7",
);
multi_bad(
"foo = 1997-09-09T09:09:09+09:09",
"invalid number at line 1 column 7",
);
multi_bad(
"foo = 1997-09-09T09:09:09-09:09",
"invalid number at line 1 column 7",
);
multi_bad(
"foo = 1997-09-09T09:09:09",
"invalid number at line 1 column 7",
);
multi_bad("foo = 1997-09-09", "invalid number at line 1 column 7");
bad!("foo = 1997-09-09 ", "invalid number at line 1 column 7");
bad!(
"foo = 1997-09-09 # comment",
"invalid number at line 1 column 7"
);
multi_bad("foo = 09:09:09", "invalid number at line 1 column 8");
multi_bad(
"foo = 1997-09-09T09:09:09.09Z",
"invalid number at line 1 column 7",
);
multi_bad(
"foo = 1997-09-09T09:09:09.09+09:09",
"invalid number at line 1 column 7",
);
multi_bad(
"foo = 1997-09-09T09:09:09.09-09:09",
"invalid number at line 1 column 7",
);
multi_bad(
"foo = 1997-09-09T09:09:09.09",
"invalid number at line 1 column 7",
);
multi_bad("foo = 09:09:09.09", "invalid number at line 1 column 8");
}
#[test]
fn bad_times() {
bad!("foo = 199-09-09", "invalid number at line 1 column 7");
bad!("foo = 199709-09", "invalid number at line 1 column 7");
bad!("foo = 1997-9-09", "invalid number at line 1 column 7");
bad!("foo = 1997-09-9", "invalid number at line 1 column 7");
bad!(
"foo = 1997-09-0909:09:09",
"invalid number at line 1 column 7"
);
bad!(
"foo = 1997-09-09T09:09:09.",
"invalid number at line 1 column 7"
);
bad!(
"foo = T",
"invalid TOML value, did you mean to use a quoted string? at line 1 column 7"
);
bad!(
"foo = T.",
"invalid TOML value, did you mean to use a quoted string? at line 1 column 7"
);
bad!(
"foo = TZ",
"invalid TOML value, did you mean to use a quoted string? at line 1 column 7"
);
bad!(
"foo = 1997-09-09T09:09:09.09+",
"invalid number at line 1 column 7"
);
bad!(
"foo = 1997-09-09T09:09:09.09+09",
"invalid number at line 1 column 7"
);
bad!(
"foo = 1997-09-09T09:09:09.09+09:9",
"invalid number at line 1 column 7"
);
bad!(
"foo = 1997-09-09T09:09:09.09+0909",
"invalid number at line 1 column 7"
);
bad!(
"foo = 1997-09-09T09:09:09.09-",
"invalid number at line 1 column 7"
);
bad!(
"foo = 1997-09-09T09:09:09.09-09",
"invalid number at line 1 column 7"
);
bad!(
"foo = 1997-09-09T09:09:09.09-09:9",
"invalid number at line 1 column 7"
);
bad!(
"foo = 1997-09-09T09:09:09.09-0909",
"invalid number at line 1 column 7"
);
bad!(
"foo = 1997-00-09T09:09:09.09Z",
"invalid number at line 1 column 7"
);
bad!(
"foo = 1997-09-00T09:09:09.09Z",
"invalid number at line 1 column 7"
);
bad!(
"foo = 1997-09-09T30:09:09.09Z",
"invalid number at line 1 column 7"
);
bad!(
"foo = 1997-09-09T12:69:09.09Z",
"invalid number at line 1 column 7"
);
bad!(
"foo = 1997-09-09T12:09:69.09Z",
"invalid number at line 1 column 7"
);
}
+354
View File
@@ -0,0 +1,354 @@
#![allow(
clippy::elidable_lifetime_names,
clippy::needless_lifetimes,
clippy::too_many_lines
)]
use serde::{de, Deserialize};
use std::fmt;
macro_rules! bad {
($toml:expr, $ty:ty, $msg:expr) => {
match basic_toml::from_str::<$ty>($toml) {
Ok(s) => panic!("parsed to: {:#?}", s),
Err(e) => assert_eq!(e.to_string(), $msg),
}
};
}
#[derive(Debug, Deserialize, PartialEq)]
struct Parent<T> {
p_a: T,
p_b: Vec<Child<T>>,
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
struct Child<T> {
c_a: T,
c_b: T,
}
#[derive(Debug, PartialEq)]
enum CasedString {
Lowercase(String),
Uppercase(String),
}
impl<'de> de::Deserialize<'de> for CasedString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct CasedStringVisitor;
impl<'de> de::Visitor<'de> for CasedStringVisitor {
type Value = CasedString;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
if s.is_empty() {
Err(de::Error::invalid_length(0, &"a non-empty string"))
} else if s.chars().all(|x| x.is_ascii_lowercase()) {
Ok(CasedString::Lowercase(s.to_string()))
} else if s.chars().all(|x| x.is_ascii_uppercase()) {
Ok(CasedString::Uppercase(s.to_string()))
} else {
Err(de::Error::invalid_value(
de::Unexpected::Str(s),
&"all lowercase or all uppercase",
))
}
}
}
deserializer.deserialize_any(CasedStringVisitor)
}
}
#[test]
fn custom_errors() {
basic_toml::from_str::<Parent<CasedString>>(
"
p_a = 'a'
p_b = [{c_a = 'a', c_b = 'c'}]
",
)
.unwrap();
// Custom error at p_b value.
bad!(
"
p_a = ''
# ^
",
Parent<CasedString>,
"invalid length 0, expected a non-empty string for key `p_a` at line 2 column 19"
);
// Missing field in table.
bad!(
"
p_a = 'a'
# ^
",
Parent<CasedString>,
"missing field `p_b` at line 1 column 1"
);
// Invalid type in p_b.
bad!(
"
p_a = 'a'
p_b = 1
# ^
",
Parent<CasedString>,
"invalid type: integer `1`, expected a sequence for key `p_b` at line 3 column 19"
);
// Sub-table in Vec is missing a field.
bad!(
"
p_a = 'a'
p_b = [
{c_a = 'a'}
# ^
]
",
Parent<CasedString>,
"missing field `c_b` for key `p_b` at line 4 column 17"
);
// Sub-table in Vec has a field with a bad value.
bad!(
"
p_a = 'a'
p_b = [
{c_a = 'a', c_b = '*'}
# ^
]
",
Parent<CasedString>,
"invalid value: string \"*\", expected all lowercase or all uppercase for key `p_b` at line 4 column 35"
);
// Sub-table in Vec is missing a field.
bad!(
"
p_a = 'a'
p_b = [
{c_a = 'a', c_b = 'b'},
{c_a = 'aa'}
# ^
]
",
Parent<CasedString>,
"missing field `c_b` for key `p_b` at line 5 column 17"
);
// Sub-table in the middle of a Vec is missing a field.
bad!(
"
p_a = 'a'
p_b = [
{c_a = 'a', c_b = 'b'},
{c_a = 'aa'},
# ^
{c_a = 'aaa', c_b = 'bbb'},
]
",
Parent<CasedString>,
"missing field `c_b` for key `p_b` at line 5 column 17"
);
// Sub-table in the middle of a Vec has a field with a bad value.
bad!(
"
p_a = 'a'
p_b = [
{c_a = 'a', c_b = 'b'},
{c_a = 'aa', c_b = 1},
# ^
{c_a = 'aaa', c_b = 'bbb'},
]
",
Parent<CasedString>,
"invalid type: integer `1`, expected a string for key `p_b` at line 5 column 36"
);
// Sub-table in the middle of a Vec has an extra field.
// FIXME: This location could be better.
bad!(
"
p_a = 'a'
p_b = [
{c_a = 'a', c_b = 'b'},
{c_a = 'aa', c_b = 'bb', c_d = 'd'},
# ^
{c_a = 'aaa', c_b = 'bbb'},
{c_a = 'aaaa', c_b = 'bbbb'},
]
",
Parent<CasedString>,
"unknown field `c_d`, expected `c_a` or `c_b` for key `p_b` at line 5 column 17"
);
// Sub-table in the middle of a Vec is missing a field.
// FIXME: This location is pretty off.
bad!(
"
p_a = 'a'
[[p_b]]
c_a = 'a'
c_b = 'b'
[[p_b]]
c_a = 'aa'
# c_b = 'bb' # <- missing field
[[p_b]]
c_a = 'aaa'
c_b = 'bbb'
[[p_b]]
# ^
c_a = 'aaaa'
c_b = 'bbbb'
",
Parent<CasedString>,
"missing field `c_b` for key `p_b` at line 12 column 13"
);
// Sub-table in the middle of a Vec has a field with a bad value.
bad!(
"
p_a = 'a'
[[p_b]]
c_a = 'a'
c_b = 'b'
[[p_b]]
c_a = 'aa'
c_b = '*'
# ^
[[p_b]]
c_a = 'aaa'
c_b = 'bbb'
",
Parent<CasedString>,
"invalid value: string \"*\", expected all lowercase or all uppercase for key `p_b.c_b` at line 8 column 19"
);
// Sub-table in the middle of a Vec has an extra field.
// FIXME: This location is pretty off.
bad!(
"
p_a = 'a'
[[p_b]]
c_a = 'a'
c_b = 'b'
[[p_b]]
c_a = 'aa'
c_d = 'dd' # unknown field
[[p_b]]
c_a = 'aaa'
c_b = 'bbb'
[[p_b]]
# ^
c_a = 'aaaa'
c_b = 'bbbb'
",
Parent<CasedString>,
"unknown field `c_d`, expected `c_a` or `c_b` for key `p_b` at line 12 column 13"
);
}
#[test]
fn serde_derive_deserialize_errors() {
bad!(
"
p_a = ''
# ^
",
Parent<String>,
"missing field `p_b` at line 1 column 1"
);
bad!(
"
p_a = ''
p_b = [
{c_a = ''}
# ^
]
",
Parent<String>,
"missing field `c_b` for key `p_b` at line 4 column 17"
);
bad!(
"
p_a = ''
p_b = [
{c_a = '', c_b = 1}
# ^
]
",
Parent<String>,
"invalid type: integer `1`, expected a string for key `p_b` at line 4 column 34"
);
// FIXME: This location could be better.
bad!(
"
p_a = ''
p_b = [
{c_a = '', c_b = '', c_d = ''},
# ^
]
",
Parent<String>,
"unknown field `c_d`, expected `c_a` or `c_b` for key `p_b` at line 4 column 17"
);
bad!(
"
p_a = 'a'
p_b = [
{c_a = '', c_b = 1, c_d = ''},
# ^
]
",
Parent<String>,
"invalid type: integer `1`, expected a string for key `p_b` at line 4 column 34"
);
}
#[test]
fn error_handles_crlf() {
bad!(
"\r\n\
[t1]\r\n\
[t2]\r\n\
a = 1\r\n\
. = 2\r\n\
",
serde_json::Value,
"expected a table key, found a period at line 5 column 1"
);
// Should be the same as above.
bad!(
"\n\
[t1]\n\
[t2]\n\
a = 1\n\
. = 2\n\
",
serde_json::Value,
"expected a table key, found a period at line 5 column 1"
);
}
+53
View File
@@ -0,0 +1,53 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Recipe {
pub name: String,
pub description: Option<String>,
#[serde(default)]
pub modules: Vec<Modules>,
#[serde(default)]
pub packages: Vec<Packages>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Modules {
pub name: String,
pub version: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Packages {
pub name: String,
pub version: Option<String>,
}
#[test]
fn both_ends() {
let recipe_works = basic_toml::from_str::<Recipe>(
r#"
name = "testing"
description = "example"
modules = []
[[packages]]
name = "base"
"#,
)
.unwrap();
basic_toml::to_string(&recipe_works).unwrap();
let recipe_fails = basic_toml::from_str::<Recipe>(
r#"
name = "testing"
description = "example"
packages = []
[[modules]]
name = "base"
"#,
)
.unwrap();
let err = basic_toml::to_string(&recipe_fails).unwrap_err();
assert_eq!(err.to_string(), "values must be emitted before tables");
}
+30
View File
@@ -0,0 +1,30 @@
#![allow(clippy::wildcard_imports)]
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq)]
struct Struct {
value: Enum,
}
#[derive(Debug, Deserialize, PartialEq)]
enum Enum {
Variant,
}
#[test]
fn unknown_variant() {
let error = basic_toml::from_str::<Struct>("value = \"NonExistent\"").unwrap_err();
assert_eq!(
error.to_string(),
"unknown variant `NonExistent`, expected `Variant` for key `value` at line 1 column 1"
);
}
#[test]
fn from_str() {
let s = basic_toml::from_str::<Struct>("value = \"Variant\"").unwrap();
assert_eq!(Enum::Variant, s.value);
}
+81
View File
@@ -0,0 +1,81 @@
#![allow(clippy::float_cmp)]
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[rustfmt::skip] // appears to be a bug in rustfmt to make this converge...
macro_rules! float_inf_tests {
($ty:ty) => {{
#[derive(Serialize, Deserialize)]
struct S {
sf1: $ty,
sf2: $ty,
sf3: $ty,
sf4: $ty,
sf5: $ty,
sf6: $ty,
sf7: $ty,
sf8: $ty,
}
let inf: S = basic_toml::from_str(
"
# infinity
sf1 = inf # positive infinity
sf2 = +inf # positive infinity
sf3 = -inf # negative infinity
# not a number
sf4 = nan # actual sNaN/qNaN encoding is implementation specific
sf5 = +nan # same as `nan`
sf6 = -nan # valid, actual encoding is implementation specific
# zero
sf7 = +0.0
sf8 = -0.0
",
)
.expect("Parse infinities.");
assert!(inf.sf1.is_infinite());
assert!(inf.sf1.is_sign_positive());
assert!(inf.sf2.is_infinite());
assert!(inf.sf2.is_sign_positive());
assert!(inf.sf3.is_infinite());
assert!(inf.sf3.is_sign_negative());
assert!(inf.sf4.is_nan());
assert!(inf.sf4.is_sign_positive());
assert!(inf.sf5.is_nan());
assert!(inf.sf5.is_sign_positive());
assert!(inf.sf6.is_nan());
assert!(inf.sf6.is_sign_negative()); // NOTE: but serializes to just `nan`
assert_eq!(inf.sf7, 0.0);
assert!(inf.sf7.is_sign_positive());
assert_eq!(inf.sf8, 0.0);
assert!(inf.sf8.is_sign_negative());
let s = basic_toml::to_string(&inf).unwrap();
assert_eq!(
s,
"\
sf1 = inf
sf2 = inf
sf3 = -inf
sf4 = nan
sf5 = nan
sf6 = nan
sf7 = 0.0
sf8 = -0.0
"
);
basic_toml::from_str::<Value>(&s).expect("roundtrip");
}};
}
#[test]
fn float_inf() {
float_inf_tests!(f32);
float_inf_tests!(f64);
}
+53
View File
@@ -0,0 +1,53 @@
use basic_toml::to_string;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
struct User {
pub name: String,
pub surname: String,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
struct Users {
pub user: Vec<User>,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
struct TwoUsers {
pub user0: User,
pub user1: User,
}
#[test]
fn no_unnecessary_newlines_array() {
assert!(!to_string(&Users {
user: vec![
User {
name: "John".to_string(),
surname: "Doe".to_string(),
},
User {
name: "Jane".to_string(),
surname: "Dough".to_string(),
},
],
})
.unwrap()
.starts_with('\n'));
}
#[test]
fn no_unnecessary_newlines_table() {
assert!(!to_string(&TwoUsers {
user0: User {
name: "John".to_string(),
surname: "Doe".to_string(),
},
user1: User {
name: "Jane".to_string(),
surname: "Dough".to_string(),
},
})
.unwrap()
.starts_with('\n'));
}
@@ -0,0 +1,15 @@
{
"ints-and-floats": {
"type": "array",
"value": [
{
"type": "integer",
"value": "1"
},
{
"type": "float",
"value": "1.1"
}
]
}
}
+48
View File
@@ -0,0 +1,48 @@
use serde_json::Value;
macro_rules! bad {
($toml:expr, $msg:expr) => {
match basic_toml::from_str::<Value>($toml) {
Ok(s) => panic!("parsed to: {:#?}", s),
Err(e) => assert_eq!(e.to_string(), $msg),
}
};
}
#[test]
fn bad() {
bad!("a = 01", "invalid number at line 1 column 6");
bad!("a = 1__1", "invalid number at line 1 column 5");
bad!("a = 1_", "invalid number at line 1 column 5");
bad!("''", "expected an equals, found eof at line 1 column 3");
bad!("a = 9e99999", "invalid number at line 1 column 5");
bad!(
"a = \"\u{7f}\"",
"invalid character in string: `\\u{7f}` at line 1 column 6"
);
bad!(
"a = '\u{7f}'",
"invalid character in string: `\\u{7f}` at line 1 column 6"
);
bad!("a = -0x1", "invalid number at line 1 column 5");
bad!("a = 0x-1", "invalid number at line 1 column 7");
// Dotted keys.
bad!(
"a.b.c = 1
a.b = 2
",
"duplicate key: `b` for key `a` at line 2 column 12"
);
bad!(
"a = 1
a.b = 2",
"dotted key attempted to extend non-table type at line 1 column 5"
);
bad!(
"a = {k1 = 1, k1.name = \"joe\"}",
"dotted key attempted to extend non-table type at line 1 column 11"
);
}
+226
View File
@@ -0,0 +1,226 @@
use serde_json::Value;
macro_rules! bad {
($toml:expr, $msg:expr) => {
match basic_toml::from_str::<Value>($toml) {
Ok(s) => panic!("parsed to: {:#?}", s),
Err(e) => assert_eq!(e.to_string(), $msg),
}
};
}
macro_rules! test( ($name:ident, $s:expr, $msg:expr) => (
#[test]
fn $name() { bad!($s, $msg); }
) );
test!(
datetime_malformed_no_leads,
include_str!("invalid/datetime-malformed-no-leads.toml"),
"invalid number at line 1 column 12"
);
test!(
datetime_malformed_no_secs,
include_str!("invalid/datetime-malformed-no-secs.toml"),
"invalid number at line 1 column 11"
);
test!(
datetime_malformed_no_t,
include_str!("invalid/datetime-malformed-no-t.toml"),
"invalid number at line 1 column 8"
);
test!(
datetime_malformed_with_milli,
include_str!("invalid/datetime-malformed-with-milli.toml"),
"invalid number at line 1 column 14"
);
test!(
duplicate_key_table,
include_str!("invalid/duplicate-key-table.toml"),
"duplicate key: `type` for key `fruit` at line 4 column 8"
);
test!(
duplicate_keys,
include_str!("invalid/duplicate-keys.toml"),
"duplicate key: `dupe` at line 2 column 1"
);
test!(
duplicate_table,
include_str!("invalid/duplicate-table.toml"),
"redefinition of table `dependencies` for key `dependencies` at line 7 column 1"
);
test!(
duplicate_tables,
include_str!("invalid/duplicate-tables.toml"),
"redefinition of table `a` for key `a` at line 2 column 1"
);
test!(
empty_implicit_table,
include_str!("invalid/empty-implicit-table.toml"),
"expected a table key, found a period at line 1 column 10"
);
test!(
empty_table,
include_str!("invalid/empty-table.toml"),
"expected a table key, found a right bracket at line 1 column 2"
);
test!(
float_no_leading_zero,
include_str!("invalid/float-no-leading-zero.toml"),
"expected a value, found a period at line 1 column 10"
);
test!(
float_no_suffix,
include_str!("invalid/float-no-suffix.toml"),
"invalid number at line 1 column 5"
);
test!(
float_no_trailing_digits,
include_str!("invalid/float-no-trailing-digits.toml"),
"invalid number at line 1 column 12"
);
test!(
key_after_array,
include_str!("invalid/key-after-array.toml"),
"expected newline, found an identifier at line 1 column 14"
);
test!(
key_after_table,
include_str!("invalid/key-after-table.toml"),
"expected newline, found an identifier at line 1 column 11"
);
test!(
key_empty,
include_str!("invalid/key-empty.toml"),
"expected a table key, found an equals at line 1 column 2"
);
test!(
key_hash,
include_str!("invalid/key-hash.toml"),
"expected an equals, found a comment at line 1 column 2"
);
test!(
key_newline,
include_str!("invalid/key-newline.toml"),
"expected an equals, found a newline at line 1 column 2"
);
test!(
key_open_bracket,
include_str!("invalid/key-open-bracket.toml"),
"expected a right bracket, found an equals at line 1 column 6"
);
test!(
key_single_open_bracket,
include_str!("invalid/key-single-open-bracket.toml"),
"expected a table key, found eof at line 1 column 2"
);
test!(
key_space,
include_str!("invalid/key-space.toml"),
"expected an equals, found an identifier at line 1 column 3"
);
test!(
key_start_bracket,
include_str!("invalid/key-start-bracket.toml"),
"expected a right bracket, found an equals at line 2 column 6"
);
test!(
key_two_equals,
include_str!("invalid/key-two-equals.toml"),
"expected a value, found an equals at line 1 column 6"
);
test!(
string_bad_byte_escape,
include_str!("invalid/string-bad-byte-escape.toml"),
"invalid escape character in string: `x` at line 1 column 13"
);
test!(
string_bad_escape,
include_str!("invalid/string-bad-escape.toml"),
"invalid escape character in string: `a` at line 1 column 42"
);
test!(
string_bad_line_ending_escape,
include_str!("invalid/string-bad-line-ending-escape.toml"),
"invalid escape character in string: ` ` at line 2 column 79"
);
test!(
string_byte_escapes,
include_str!("invalid/string-byte-escapes.toml"),
"invalid escape character in string: `x` at line 1 column 12"
);
test!(
string_no_close,
include_str!("invalid/string-no-close.toml"),
"newline in string found at line 1 column 42"
);
test!(
table_array_implicit,
include_str!("invalid/table-array-implicit.toml"),
"table redefined as array for key `albums` at line 13 column 1"
);
test!(
table_array_malformed_bracket,
include_str!("invalid/table-array-malformed-bracket.toml"),
"expected a right bracket, found a newline at line 1 column 10"
);
test!(
table_array_malformed_empty,
include_str!("invalid/table-array-malformed-empty.toml"),
"expected a table key, found a right bracket at line 1 column 3"
);
test!(
table_empty,
include_str!("invalid/table-empty.toml"),
"expected a table key, found a right bracket at line 1 column 2"
);
test!(
table_nested_brackets_close,
include_str!("invalid/table-nested-brackets-close.toml"),
"expected newline, found an identifier at line 1 column 4"
);
test!(
table_nested_brackets_open,
include_str!("invalid/table-nested-brackets-open.toml"),
"expected a right bracket, found a left bracket at line 1 column 3"
);
test!(
table_whitespace,
include_str!("invalid/table-whitespace.toml"),
"expected a right bracket, found an identifier at line 1 column 10"
);
test!(
table_with_pound,
include_str!("invalid/table-with-pound.toml"),
"expected a right bracket, found a comment at line 1 column 5"
);
test!(
text_after_array_entries,
include_str!("invalid/text-after-array-entries.toml"),
"invalid TOML value, did you mean to use a quoted string? at line 2 column 46"
);
test!(
text_after_integer,
include_str!("invalid/text-after-integer.toml"),
"expected newline, found an identifier at line 1 column 13"
);
test!(
text_after_string,
include_str!("invalid/text-after-string.toml"),
"expected newline, found an identifier at line 1 column 41"
);
test!(
text_after_table,
include_str!("invalid/text-after-table.toml"),
"expected newline, found an identifier at line 1 column 9"
);
test!(
text_before_array_separator,
include_str!("invalid/text-before-array-separator.toml"),
"expected a right bracket, found an identifier at line 2 column 46"
);
test!(
text_in_array,
include_str!("invalid/text-in-array.toml"),
"invalid TOML value, did you mean to use a quoted string? at line 3 column 3"
);
@@ -0,0 +1 @@
no-leads = 1987-7-05T17:45:00Z
@@ -0,0 +1 @@
no-secs = 1987-07-05T17:45Z
@@ -0,0 +1 @@
no-t = 1987-07-0517:45:00Z
@@ -0,0 +1 @@
with-milli = 1987-07-5T17:45:00.12Z
@@ -0,0 +1,5 @@
[fruit]
type = "apple"
[fruit.type]
apple = "yes"
+2
View File
@@ -0,0 +1,2 @@
dupe = false
dupe = true
+8
View File
@@ -0,0 +1,8 @@
[dependencies.openssl-sys]
version = "0.5.2"
[dependencies]
libc = "0.1"
[dependencies]
bitflags = "0.1.1"
+2
View File
@@ -0,0 +1,2 @@
[a]
[a]
@@ -0,0 +1 @@
[naughty..naughty]
+1
View File
@@ -0,0 +1 @@
[]
@@ -0,0 +1,2 @@
answer = .12345
neganswer = -.12345
+1
View File
@@ -0,0 +1 @@
a = 1.2f
@@ -0,0 +1,2 @@
answer = 1.
neganswer = -1.
+1
View File
@@ -0,0 +1 @@
[[agencies]] owner = "S Cjelli"
+1
View File
@@ -0,0 +1 @@
[history] guard = "sleeping"
+1
View File
@@ -0,0 +1 @@
= 1
+1
View File
@@ -0,0 +1 @@
a# = 1
+2
View File
@@ -0,0 +1,2 @@
a
= 1
+1
View File
@@ -0,0 +1 @@
[abc = 1
@@ -0,0 +1 @@
[
+1
View File
@@ -0,0 +1 @@
a b = 1
@@ -0,0 +1,3 @@
[a]
[xyz = 5
[b]
+1
View File
@@ -0,0 +1 @@
key= = 1
@@ -0,0 +1 @@
naughty = "\xAg"
@@ -0,0 +1 @@
invalid-escape = "This string has a bad \a escape character."
@@ -0,0 +1,3 @@
invalid-escape = """\
This string has a non whitespace-character after the line ending escape. \ a
"""
@@ -0,0 +1 @@
answer = "\x33"
+1
View File
@@ -0,0 +1 @@
no-ending-quote = "One time, at band camp
@@ -0,0 +1,14 @@
# This test is a bit tricky. It should fail because the first use of
# `[[albums.songs]]` without first declaring `albums` implies that `albums`
# must be a table. The alternative would be quite weird. Namely, it wouldn't
# comply with the TOML spec: "Each double-bracketed sub-table will belong to
# the most *recently* defined table element *above* it."
#
# This is in contrast to the *valid* test, table-array-implicit where
# `[[albums.songs]]` works by itself, so long as `[[albums]]` isn't declared
# later. (Although, `[albums]` could be.)
[[albums.songs]]
name = "Glory Days"
[[albums]]
name = "Born in the USA"
@@ -0,0 +1,2 @@
[[albums]
name = "Born to Run"
@@ -0,0 +1,2 @@
[[]]
name = "Born to Run"
+1
View File
@@ -0,0 +1 @@
[]
@@ -0,0 +1,2 @@
[a]b]
zyx = 42
@@ -0,0 +1,2 @@
[a[b]
zyx = 42
+1
View File
@@ -0,0 +1 @@
[invalid key]
+2
View File
@@ -0,0 +1,2 @@
[key#group]
answer = 42
@@ -0,0 +1,4 @@
array = [
"Is there life after an array separator?", No
"Entry"
]
@@ -0,0 +1 @@
answer = 42 the ultimate answer?
@@ -0,0 +1 @@
string = "Is there life after strings?" No.
+1
View File
@@ -0,0 +1 @@
[error] this shouldn't be here
@@ -0,0 +1,4 @@
array = [
"Is there life before an array separator?" No,
"Entry"
]
+5
View File
@@ -0,0 +1,5 @@
array = [
"Entry 1",
I don't belong,
"Entry 2",
]
+687
View File
@@ -0,0 +1,687 @@
#![allow(clippy::let_underscore_untyped, clippy::uninlined_format_args)]
use serde_json::{json, Value};
macro_rules! bad {
($toml:expr, $msg:expr) => {
match basic_toml::from_str::<Value>($toml) {
Ok(s) => panic!("parsed to: {:#?}", s),
Err(e) => assert_eq!(e.to_string(), $msg),
}
};
}
#[test]
fn crlf() {
let toml = "\
[project]\r\n\
\r\n\
name = \"splay\"\r\n\
version = \"0.1.0\"\r\n\
authors = [\"alex@crichton.co\"]\r\n\
\r\n\
[[lib]]\r\n\
\r\n\
path = \"lib.rs\"\r\n\
name = \"splay\"\r\n\
description = \"\"\"\
A Rust implementation of a TAR file reader and writer. This library does not\r\n\
currently handle compression, but it is abstract over all I/O readers and\r\n\
writers. Additionally, great lengths are taken to ensure that the entire\r\n\
contents are never required to be entirely resident in memory all at once.\r\n\
\"\"\"\
";
basic_toml::from_str::<Value>(toml).unwrap();
}
#[test]
fn fun_with_strings() {
let toml = r#"
bar = "\U00000000"
key1 = "One\nTwo"
key2 = """One\nTwo"""
key3 = """
One
Two"""
key4 = "The quick brown fox jumps over the lazy dog."
key5 = """
The quick brown \
fox jumps over \
the lazy dog."""
key6 = """\
The quick brown \
fox jumps over \
the lazy dog.\
"""
# What you see is what you get.
winpath = 'C:\Users\nodejs\templates'
winpath2 = '\\ServerX\admin$\system32\'
quoted = 'Tom "Dubs" Preston-Werner'
regex = '<\i\c*\s*>'
regex2 = '''I [dw]on't need \d{2} apples'''
lines = '''
The first newline is
trimmed in raw strings.
All other whitespace
is preserved.
'''
"#;
let table: Value = basic_toml::from_str(toml).unwrap();
assert_eq!(table["bar"], json!("\0"));
assert_eq!(table["key1"], json!("One\nTwo"));
assert_eq!(table["key2"], json!("One\nTwo"));
assert_eq!(table["key3"], json!("One\nTwo"));
let msg = "The quick brown fox jumps over the lazy dog.";
assert_eq!(table["key4"], json!(msg));
assert_eq!(table["key5"], json!(msg));
assert_eq!(table["key6"], json!(msg));
assert_eq!(table["winpath"], json!(r"C:\Users\nodejs\templates"));
assert_eq!(table["winpath2"], json!(r"\\ServerX\admin$\system32\"));
assert_eq!(table["quoted"], json!(r#"Tom "Dubs" Preston-Werner"#));
assert_eq!(table["regex"], json!(r"<\i\c*\s*>"));
assert_eq!(table["regex2"], json!(r"I [dw]on't need \d{2} apples"));
assert_eq!(
table["lines"],
json!(
"The first newline is\n\
trimmed in raw strings.\n\
All other whitespace\n\
is preserved.\n"
)
);
}
#[test]
fn tables_in_arrays() {
let toml = "
[[foo]]
#…
[foo.bar]
#…
[[foo]] # ...
#…
[foo.bar]
#...
";
let table: Value = basic_toml::from_str(toml).unwrap();
table["foo"][0]["bar"].as_object().unwrap();
table["foo"][1]["bar"].as_object().unwrap();
}
#[test]
fn empty_table() {
let toml = "
[foo]";
let table: Value = basic_toml::from_str(toml).unwrap();
table["foo"].as_object().unwrap();
}
#[test]
fn fruit() {
let toml = r#"
[[fruit]]
name = "apple"
[fruit.physical]
color = "red"
shape = "round"
[[fruit.variety]]
name = "red delicious"
[[fruit.variety]]
name = "granny smith"
[[fruit]]
name = "banana"
[[fruit.variety]]
name = "plantain"
"#;
let table: Value = basic_toml::from_str(toml).unwrap();
assert_eq!(table["fruit"][0]["name"], json!("apple"));
assert_eq!(table["fruit"][0]["physical"]["color"], json!("red"));
assert_eq!(table["fruit"][0]["physical"]["shape"], json!("round"));
assert_eq!(
table["fruit"][0]["variety"][0]["name"],
json!("red delicious")
);
assert_eq!(
table["fruit"][0]["variety"][1]["name"],
json!("granny smith")
);
assert_eq!(table["fruit"][1]["name"], json!("banana"));
assert_eq!(table["fruit"][1]["variety"][0]["name"], json!("plantain"));
}
#[test]
fn stray_cr() {
bad!("\r", "unexpected character found: `\\r` at line 1 column 1");
bad!(
"a = [ \r ]",
"unexpected character found: `\\r` at line 1 column 7"
);
bad!(
"a = \"\"\"\r\"\"\"",
"invalid character in string: `\\r` at line 1 column 8"
);
bad!(
"a = \"\"\"\\ \r \"\"\"",
"invalid escape character in string: ` ` at line 1 column 9"
);
bad!(
"a = '''\r'''",
"invalid character in string: `\\r` at line 1 column 8"
);
bad!(
"a = '\r'",
"invalid character in string: `\\r` at line 1 column 6"
);
bad!(
"a = \"\r\"",
"invalid character in string: `\\r` at line 1 column 6"
);
}
#[test]
fn blank_literal_string() {
let table: Value = basic_toml::from_str("foo = ''").unwrap();
assert_eq!(table["foo"], json!(""));
}
#[test]
fn many_blank() {
let table: Value = basic_toml::from_str("foo = \"\"\"\n\n\n\"\"\"").unwrap();
assert_eq!(table["foo"], json!("\n\n"));
}
#[test]
fn literal_eats_crlf() {
let toml = "
foo = \"\"\"\\\r\n\"\"\"
bar = \"\"\"\\\r\n \r\n \r\n a\"\"\"
";
let table: Value = basic_toml::from_str(toml).unwrap();
assert_eq!(table["foo"], json!(""));
assert_eq!(table["bar"], json!("a"));
}
#[test]
fn string_no_newline() {
bad!("a = \"\n\"", "newline in string found at line 1 column 6");
bad!("a = '\n'", "newline in string found at line 1 column 6");
}
#[test]
fn bad_leading_zeros() {
bad!("a = 00", "invalid number at line 1 column 6");
bad!("a = -00", "invalid number at line 1 column 7");
bad!("a = +00", "invalid number at line 1 column 7");
bad!("a = 00.0", "invalid number at line 1 column 6");
bad!("a = -00.0", "invalid number at line 1 column 7");
bad!("a = +00.0", "invalid number at line 1 column 7");
bad!(
"a = 9223372036854775808",
"invalid number at line 1 column 5"
);
bad!(
"a = -9223372036854775809",
"invalid number at line 1 column 5"
);
}
#[test]
fn bad_floats() {
bad!("a = 0.", "invalid number at line 1 column 7");
bad!("a = 0.e", "invalid number at line 1 column 7");
bad!("a = 0.E", "invalid number at line 1 column 7");
bad!("a = 0.0E", "invalid number at line 1 column 5");
bad!("a = 0.0e", "invalid number at line 1 column 5");
bad!("a = 0.0e-", "invalid number at line 1 column 9");
bad!("a = 0.0e+", "invalid number at line 1 column 5");
}
#[test]
fn floats() {
macro_rules! t {
($actual:expr, $expected:expr) => {{
let f = format!("foo = {}", $actual);
println!("{}", f);
let a: Value = basic_toml::from_str(&f).unwrap();
assert_eq!(a["foo"], json!($expected));
}};
}
t!("1.0", 1.0);
t!("1.0e0", 1.0);
t!("1.0e+0", 1.0);
t!("1.0e-0", 1.0);
t!("1E-0", 1.0);
t!("1.001e-0", 1.001);
t!("2e10", 2e10);
t!("2e+10", 2e10);
t!("2e-10", 2e-10);
t!("2_0.0", 20.0);
t!("2_0.0_0e1_0", 20.0e10);
t!("2_0.1_0e1_0", 20.1e10);
}
#[test]
fn bare_key_names() {
let toml = "
foo = 3
foo_3 = 3
foo_-2--3--r23f--4-f2-4 = 3
_ = 3
- = 3
8 = 8
\"a\" = 3
\"!\" = 3
\"a^b\" = 3
\"\\\"\" = 3
\"character encoding\" = \"value\"
'ʎǝʞ' = \"value\"
";
let a: Value = basic_toml::from_str(toml).unwrap();
let _ = &a["foo"];
let _ = &a["-"];
let _ = &a["_"];
let _ = &a["8"];
let _ = &a["foo_3"];
let _ = &a["foo_-2--3--r23f--4-f2-4"];
let _ = &a["a"];
let _ = &a["!"];
let _ = &a["\""];
let _ = &a["character encoding"];
let _ = &a["ʎǝʞ"];
}
#[test]
fn bad_keys() {
bad!(
"key\n=3",
"expected an equals, found a newline at line 1 column 4"
);
bad!(
"key=\n3",
"expected a value, found a newline at line 1 column 5"
);
bad!(
"key|=3",
"unexpected character found: `|` at line 1 column 4"
);
bad!(
"=3",
"expected a table key, found an equals at line 1 column 1"
);
bad!(
"\"\"|=3",
"unexpected character found: `|` at line 1 column 3"
);
bad!("\"\n\"|=3", "newline in string found at line 1 column 2");
bad!(
"\"\r\"|=3",
"invalid character in string: `\\r` at line 1 column 2"
);
bad!(
"''''''=3",
"multiline strings are not allowed for key at line 1 column 1"
);
bad!(
"\"\"\"\"\"\"=3",
"multiline strings are not allowed for key at line 1 column 1"
);
bad!(
"'''key'''=3",
"multiline strings are not allowed for key at line 1 column 1"
);
bad!(
"\"\"\"key\"\"\"=3",
"multiline strings are not allowed for key at line 1 column 1"
);
}
#[test]
fn bad_table_names() {
bad!(
"[]",
"expected a table key, found a right bracket at line 1 column 2"
);
bad!(
"[.]",
"expected a table key, found a period at line 1 column 2"
);
bad!(
"[a.]",
"expected a table key, found a right bracket at line 1 column 4"
);
bad!("[!]", "unexpected character found: `!` at line 1 column 2");
bad!("[\"\n\"]", "newline in string found at line 1 column 3");
bad!(
"[a.b]\n[a.\"b\"]",
"redefinition of table `a.b` for key `a.b` at line 2 column 1"
);
bad!("[']", "unterminated string at line 1 column 2");
bad!("[''']", "unterminated string at line 1 column 2");
bad!(
"['''''']",
"multiline strings are not allowed for key at line 1 column 2"
);
bad!(
"['''foo''']",
"multiline strings are not allowed for key at line 1 column 2"
);
bad!(
"[\"\"\"bar\"\"\"]",
"multiline strings are not allowed for key at line 1 column 2"
);
bad!("['\n']", "newline in string found at line 1 column 3");
bad!("['\r\n']", "newline in string found at line 1 column 3");
}
#[test]
fn table_names() {
let toml = "
[a.\"b\"]
[\"f f\"]
[\"f.f\"]
[\"\\\"\"]
['a.a']
['\"\"']
";
let a: Value = basic_toml::from_str(toml).unwrap();
println!("{:?}", a);
let _ = &a["a"]["b"];
let _ = &a["f f"];
let _ = &a["f.f"];
let _ = &a["\""];
let _ = &a["\"\""];
}
#[test]
fn invalid_bare_numeral() {
bad!("4", "expected an equals, found eof at line 1 column 2");
}
#[test]
fn inline_tables() {
basic_toml::from_str::<Value>("a = {}").unwrap();
basic_toml::from_str::<Value>("a = {b=1}").unwrap();
basic_toml::from_str::<Value>("a = { b = 1 }").unwrap();
basic_toml::from_str::<Value>("a = {a=1,b=2}").unwrap();
basic_toml::from_str::<Value>("a = {a=1,b=2,c={}}").unwrap();
bad!(
"a = {a=1,}",
"expected a table key, found a right brace at line 1 column 10"
);
bad!(
"a = {,}",
"expected a table key, found a comma at line 1 column 6"
);
bad!(
"a = {a=1,a=1}",
"duplicate key: `a` for key `a` at line 1 column 10"
);
bad!(
"a = {\n}",
"expected a table key, found a newline at line 1 column 6"
);
bad!(
"a = {",
"expected a table key, found eof at line 1 column 6"
);
basic_toml::from_str::<Value>("a = {a=[\n]}").unwrap();
basic_toml::from_str::<Value>("a = {\"a\"=[\n]}").unwrap();
basic_toml::from_str::<Value>("a = [\n{},\n{},\n]").unwrap();
}
#[test]
fn number_underscores() {
macro_rules! t {
($actual:expr, $expected:expr) => {{
let f = format!("foo = {}", $actual);
let table: Value = basic_toml::from_str(&f).unwrap();
assert_eq!(table["foo"], json!($expected));
}};
}
t!("1_0", 10);
t!("1_0_0", 100);
t!("1_000", 1000);
t!("+1_000", 1000);
t!("-1_000", -1000);
}
#[test]
fn bad_underscores() {
bad!("foo = 0_", "invalid number at line 1 column 7");
bad!("foo = 0__0", "invalid number at line 1 column 7");
bad!(
"foo = __0",
"invalid TOML value, did you mean to use a quoted string? at line 1 column 7"
);
bad!("foo = 1_0_", "invalid number at line 1 column 7");
}
#[test]
fn bad_unicode_codepoint() {
bad!(
"foo = \"\\uD800\"",
"invalid escape value: `55296` at line 1 column 9"
);
}
#[test]
fn bad_strings() {
bad!(
"foo = \"\\uxx\"",
"invalid hex escape character in string: `x` at line 1 column 10"
);
bad!(
"foo = \"\\u\"",
"invalid hex escape character in string: `\\\"` at line 1 column 10"
);
bad!("foo = \"\\", "unterminated string at line 1 column 7");
bad!("foo = '", "unterminated string at line 1 column 7");
}
#[test]
fn empty_string() {
let table: Value = basic_toml::from_str::<Value>("foo = \"\"").unwrap();
assert_eq!(table["foo"], json!(""));
}
#[test]
fn booleans() {
let table: Value = basic_toml::from_str("foo = true").unwrap();
assert_eq!(table["foo"], json!(true));
let table: Value = basic_toml::from_str("foo = false").unwrap();
assert_eq!(table["foo"], json!(false));
bad!(
"foo = true2",
"invalid TOML value, did you mean to use a quoted string? at line 1 column 7"
);
bad!(
"foo = false2",
"invalid TOML value, did you mean to use a quoted string? at line 1 column 7"
);
bad!(
"foo = t1",
"invalid TOML value, did you mean to use a quoted string? at line 1 column 7"
);
bad!(
"foo = f2",
"invalid TOML value, did you mean to use a quoted string? at line 1 column 7"
);
}
#[test]
fn bad_nesting() {
bad!(
"
a = [2]
[[a]]
b = 5
",
"duplicate key: `a` at line 3 column 11"
);
bad!(
"
a = 1
[a.b]
",
"duplicate key: `a` at line 3 column 10"
);
bad!(
"
a = []
[a.b]
",
"duplicate key: `a` at line 3 column 10"
);
bad!(
"
a = []
[[a.b]]
",
"duplicate key: `a` at line 3 column 11"
);
bad!(
"
[a]
b = { c = 2, d = {} }
[a.b]
c = 2
",
"duplicate key: `b` for key `a` at line 4 column 12"
);
}
#[test]
fn bad_table_redefine() {
bad!(
"
[a]
foo=\"bar\"
[a.b]
foo=\"bar\"
[a]
",
"redefinition of table `a` for key `a` at line 6 column 9"
);
bad!(
"
[a]
foo=\"bar\"
b = { foo = \"bar\" }
[a]
",
"redefinition of table `a` for key `a` at line 5 column 9"
);
bad!(
"
[a]
b = {}
[a.b]
",
"duplicate key: `b` for key `a` at line 4 column 12"
);
bad!(
"
[a]
b = {}
[a]
",
"redefinition of table `a` for key `a` at line 4 column 9"
);
}
#[test]
fn datetimes() {
bad!(
"foo = 2016-09-09T09:09:09Z",
"invalid number at line 1 column 7"
);
bad!(
"foo = 2016-09-09T09:09:09.1Z",
"invalid number at line 1 column 7"
);
bad!(
"foo = 2016-09-09T09:09:09.2+10:00",
"invalid number at line 1 column 7"
);
bad!(
"foo = 2016-09-09T09:09:09.123456789-02:00",
"invalid number at line 1 column 7"
);
bad!(
"foo = 2016-09-09T09:09:09.Z",
"invalid number at line 1 column 7"
);
bad!(
"foo = 2016-9-09T09:09:09Z",
"invalid number at line 1 column 7"
);
bad!(
"foo = 2016-09-09T09:09:09+2:00",
"invalid number at line 1 column 7"
);
bad!(
"foo = 2016-09-09T09:09:09-2:00",
"invalid number at line 1 column 7"
);
bad!(
"foo = 2016-09-09T09:09:09Z-2:00",
"invalid number at line 1 column 7"
);
}
#[test]
fn require_newline_after_value() {
bad!("0=0r=false", "invalid number at line 1 column 3");
bad!(
r#"
0=""o=""m=""r=""00="0"q="""0"""e="""0"""
"#,
"expected newline, found an identifier at line 2 column 5"
);
bad!(
r#"
[[0000l0]]
0="0"[[0000l0]]
0="0"[[0000l0]]
0="0"l="0"
"#,
"expected newline, found a left bracket at line 3 column 6"
);
bad!(
r#"
0=[0]00=[0,0,0]t=["0","0","0"]s=[1000-00-00T00:00:00Z,2000-00-00T00:00:00Z]
"#,
"expected newline, found an identifier at line 2 column 6"
);
bad!(
"
0=0r0=0r=false
",
"invalid number at line 2 column 3"
);
bad!(
"
0=0r0=0r=falsefal=false
",
"invalid number at line 2 column 3"
);
}
+190
View File
@@ -0,0 +1,190 @@
#![allow(
clippy::elidable_lifetime_names,
clippy::let_underscore_untyped,
clippy::manual_range_contains,
clippy::needless_lifetimes,
clippy::needless_pass_by_value,
clippy::type_complexity
)]
#[path = "../src/tokens.rs"]
#[allow(dead_code)]
mod tokens;
use crate::tokens::{Error, Token, Tokenizer};
use std::borrow::Cow;
fn err(input: &str, err: Error) {
let mut t = Tokenizer::new(input);
let token = t.next().unwrap_err();
assert_eq!(token, err);
assert!(t.next().unwrap().is_none());
}
#[test]
fn literal_strings() {
fn t(input: &str, val: &str, multiline: bool) {
let mut t = Tokenizer::new(input);
let (_, token) = t.next().unwrap().unwrap();
assert_eq!(
token,
Token::String {
src: input,
val: Cow::Borrowed(val),
multiline,
}
);
assert!(t.next().unwrap().is_none());
}
t("''", "", false);
t("''''''", "", true);
t("'''\n'''", "", true);
t("'a'", "a", false);
t("'\"a'", "\"a", false);
t("''''a'''", "'a", true);
t("'''\n'a\n'''", "'a\n", true);
t("'''a\n'a\r\n'''", "a\n'a\n", true);
}
#[test]
fn basic_strings() {
fn t(input: &str, val: &str, multiline: bool) {
let mut t = Tokenizer::new(input);
let (_, token) = t.next().unwrap().unwrap();
assert_eq!(
token,
Token::String {
src: input,
val: Cow::Borrowed(val),
multiline,
}
);
assert!(t.next().unwrap().is_none());
}
t(r#""""#, "", false);
t(r#""""""""#, "", true);
t(r#""a""#, "a", false);
t(r#""""a""""#, "a", true);
t(r#""\t""#, "\t", false);
t(r#""\u0000""#, "\0", false);
t(r#""\U00000000""#, "\0", false);
t(r#""\U000A0000""#, "\u{A0000}", false);
t(r#""\\t""#, "\\t", false);
t("\"\t\"", "\t", false);
t("\"\"\"\n\t\"\"\"", "\t", true);
t("\"\"\"\\\n\"\"\"", "", true);
t(
"\"\"\"\\\n \t \t \\\r\n \t \n \t \r\n\"\"\"",
"",
true,
);
t(r#""\r""#, "\r", false);
t(r#""\n""#, "\n", false);
t(r#""\b""#, "\u{8}", false);
t(r#""a\fa""#, "a\u{c}a", false);
t(r#""\"a""#, "\"a", false);
t("\"\"\"\na\"\"\"", "a", true);
t("\"\"\"\n\"\"\"", "", true);
t(r#""""a\"""b""""#, "a\"\"\"b", true);
err(r#""\a"#, Error::InvalidEscape(2, 'a'));
err("\"\\\n", Error::InvalidEscape(2, '\n'));
err("\"\\\r\n", Error::InvalidEscape(2, '\n'));
err("\"\\", Error::UnterminatedString(0));
err("\"\u{0}", Error::InvalidCharInString(1, '\u{0}'));
err(r#""\U00""#, Error::InvalidHexEscape(5, '"'));
err(r#""\U00"#, Error::UnterminatedString(0));
err(r#""\uD800"#, Error::InvalidEscapeValue(2, 0xd800));
err(r#""\UFFFFFFFF"#, Error::InvalidEscapeValue(2, 0xffff_ffff));
}
#[test]
fn keylike() {
fn t(input: &str) {
let mut t = Tokenizer::new(input);
let (_, token) = t.next().unwrap().unwrap();
assert_eq!(token, Token::Keylike(input));
assert!(t.next().unwrap().is_none());
}
t("foo");
t("0bar");
t("bar0");
t("1234");
t("a-b");
t("a_B");
t("-_-");
t("___");
}
#[test]
fn all() {
fn t(input: &str, expected: &[((usize, usize), Token, &str)]) {
let mut tokens = Tokenizer::new(input);
let mut actual: Vec<((usize, usize), Token, &str)> = Vec::new();
while let Some((span, token)) = tokens.next().unwrap() {
actual.push((span.into(), token, &input[span.start..span.end]));
}
for (a, b) in actual.iter().zip(expected) {
assert_eq!(a, b);
}
assert_eq!(actual.len(), expected.len());
}
t(
" a ",
&[
((0, 1), Token::Whitespace(" "), " "),
((1, 2), Token::Keylike("a"), "a"),
((2, 3), Token::Whitespace(" "), " "),
],
);
t(
" a\t [[]] \t [] {} , . =\n# foo \r\n#foo \n ",
&[
((0, 1), Token::Whitespace(" "), " "),
((1, 2), Token::Keylike("a"), "a"),
((2, 4), Token::Whitespace("\t "), "\t "),
((4, 5), Token::LeftBracket, "["),
((5, 6), Token::LeftBracket, "["),
((6, 7), Token::RightBracket, "]"),
((7, 8), Token::RightBracket, "]"),
((8, 11), Token::Whitespace(" \t "), " \t "),
((11, 12), Token::LeftBracket, "["),
((12, 13), Token::RightBracket, "]"),
((13, 14), Token::Whitespace(" "), " "),
((14, 15), Token::LeftBrace, "{"),
((15, 16), Token::RightBrace, "}"),
((16, 17), Token::Whitespace(" "), " "),
((17, 18), Token::Comma, ","),
((18, 19), Token::Whitespace(" "), " "),
((19, 20), Token::Period, "."),
((20, 21), Token::Whitespace(" "), " "),
((21, 22), Token::Equals, "="),
((22, 23), Token::Newline, "\n"),
((23, 29), Token::Comment("# foo "), "# foo "),
((29, 31), Token::Newline, "\r\n"),
((31, 36), Token::Comment("#foo "), "#foo "),
((36, 37), Token::Newline, "\n"),
((37, 38), Token::Whitespace(" "), " "),
],
);
}
#[test]
fn bare_cr_bad() {
err("\r", Error::Unexpected(0, '\r'));
err("'\n", Error::NewlineInString(1));
err("'\u{0}", Error::InvalidCharInString(1, '\u{0}'));
err("'", Error::UnterminatedString(0));
err("\u{0}", Error::Unexpected(0, '\u{0}'));
}
#[test]
fn bad_comment() {
let mut t = Tokenizer::new("#\u{0}");
t.next().unwrap().unwrap();
assert_eq!(t.next(), Err(Error::Unexpected(1, '\u{0}')));
assert!(t.next().unwrap().is_none());
}
+368
View File
@@ -0,0 +1,368 @@
#![allow(
clippy::match_like_matches_macro,
clippy::needless_pass_by_value,
clippy::uninlined_format_args
)]
use serde_json::{json, Value};
fn to_json(toml: Value) -> Value {
fn doit(s: &str, json: Value) -> Value {
json!({ "type": s, "value": json })
}
match toml {
Value::Null => unreachable!(),
Value::String(s) => doit("string", Value::String(s)),
Value::Number(n) => {
let repr = n.to_string();
if repr.contains('.') {
let float: f64 = repr.parse().unwrap();
let mut repr = format!("{:.15}", float);
repr.truncate(repr.trim_end_matches('0').len());
if repr.ends_with('.') {
repr.push('0');
}
doit("float", Value::String(repr))
} else {
doit("integer", Value::String(repr))
}
}
Value::Bool(b) => doit("bool", Value::String(format!("{}", b))),
Value::Array(arr) => {
let is_table = match arr.first() {
Some(&Value::Object(_)) => true,
_ => false,
};
let json = Value::Array(arr.into_iter().map(to_json).collect());
if is_table {
json
} else {
doit("array", json)
}
}
Value::Object(table) => {
let mut map = serde_json::Map::new();
for (k, v) in table {
map.insert(k, to_json(v));
}
Value::Object(map)
}
}
}
fn run(toml_raw: &str, json_raw: &str) {
println!("parsing:\n{}", toml_raw);
let toml: Value = basic_toml::from_str(toml_raw).unwrap();
let json: Value = serde_json::from_str(json_raw).unwrap();
// Assert toml == json
let toml_json = to_json(toml.clone());
assert!(
json == toml_json,
"expected\n{}\ngot\n{}\n",
serde_json::to_string_pretty(&json).unwrap(),
serde_json::to_string_pretty(&toml_json).unwrap()
);
// Assert round trip
println!("round trip parse: {}", toml);
let toml2: Value = basic_toml::from_str(&basic_toml::to_string(&toml).unwrap()).unwrap();
assert_eq!(toml, toml2);
}
macro_rules! test( ($name:ident, $toml:expr, $json:expr) => (
#[test]
fn $name() { run($toml, $json); }
) );
test!(
array_empty,
include_str!("valid/array-empty.toml"),
include_str!("valid/array-empty.json")
);
test!(
array_nospaces,
include_str!("valid/array-nospaces.toml"),
include_str!("valid/array-nospaces.json")
);
test!(
arrays_hetergeneous,
include_str!("valid/arrays-hetergeneous.toml"),
include_str!("valid/arrays-hetergeneous.json")
);
#[cfg(any())]
test!(
arrays,
include_str!("valid/arrays.toml"),
include_str!("valid/arrays.json")
);
test!(
arrays_nested,
include_str!("valid/arrays-nested.toml"),
include_str!("valid/arrays-nested.json")
);
test!(
array_mixed_types_ints_and_floats,
include_str!("valid/array-mixed-types-ints-and-floats.toml"),
include_str!("valid/array-mixed-types-ints-and-floats.json")
);
test!(
array_mixed_types_arrays_and_ints,
include_str!("valid/array-mixed-types-arrays-and-ints.toml"),
include_str!("valid/array-mixed-types-arrays-and-ints.json")
);
test!(
array_mixed_types_strings_and_ints,
include_str!("valid/array-mixed-types-strings-and-ints.toml"),
include_str!("valid/array-mixed-types-strings-and-ints.json")
);
test!(
empty,
include_str!("valid/empty.toml"),
include_str!("valid/empty.json")
);
test!(
bool,
include_str!("valid/bool.toml"),
include_str!("valid/bool.json")
);
test!(
comments_everywhere,
include_str!("valid/comments-everywhere.toml"),
include_str!("valid/comments-everywhere.json")
);
#[cfg(any())]
test!(
datetime,
include_str!("valid/datetime.toml"),
include_str!("valid/datetime.json")
);
#[cfg(any())]
test!(
example,
include_str!("valid/example.toml"),
include_str!("valid/example.json")
);
test!(
float,
include_str!("valid/float.toml"),
include_str!("valid/float.json")
);
#[cfg(any())]
test!(
implicit_and_explicit_after,
include_str!("valid/implicit-and-explicit-after.toml"),
include_str!("valid/implicit-and-explicit-after.json")
);
#[cfg(any())]
test!(
implicit_and_explicit_before,
include_str!("valid/implicit-and-explicit-before.toml"),
include_str!("valid/implicit-and-explicit-before.json")
);
test!(
implicit_groups,
include_str!("valid/implicit-groups.toml"),
include_str!("valid/implicit-groups.json")
);
test!(
integer,
include_str!("valid/integer.toml"),
include_str!("valid/integer.json")
);
test!(
key_equals_nospace,
include_str!("valid/key-equals-nospace.toml"),
include_str!("valid/key-equals-nospace.json")
);
test!(
key_space,
include_str!("valid/key-space.toml"),
include_str!("valid/key-space.json")
);
test!(
key_special_chars,
include_str!("valid/key-special-chars.toml"),
include_str!("valid/key-special-chars.json")
);
test!(
key_with_pound,
include_str!("valid/key-with-pound.toml"),
include_str!("valid/key-with-pound.json")
);
test!(
key_empty,
include_str!("valid/key-empty.toml"),
include_str!("valid/key-empty.json")
);
test!(
long_float,
include_str!("valid/long-float.toml"),
include_str!("valid/long-float.json")
);
test!(
long_integer,
include_str!("valid/long-integer.toml"),
include_str!("valid/long-integer.json")
);
test!(
multiline_string,
include_str!("valid/multiline-string.toml"),
include_str!("valid/multiline-string.json")
);
test!(
raw_multiline_string,
include_str!("valid/raw-multiline-string.toml"),
include_str!("valid/raw-multiline-string.json")
);
test!(
raw_string,
include_str!("valid/raw-string.toml"),
include_str!("valid/raw-string.json")
);
test!(
string_empty,
include_str!("valid/string-empty.toml"),
include_str!("valid/string-empty.json")
);
test!(
string_escapes,
include_str!("valid/string-escapes.toml"),
include_str!("valid/string-escapes.json")
);
test!(
string_simple,
include_str!("valid/string-simple.toml"),
include_str!("valid/string-simple.json")
);
test!(
string_with_pound,
include_str!("valid/string-with-pound.toml"),
include_str!("valid/string-with-pound.json")
);
test!(
table_array_implicit,
include_str!("valid/table-array-implicit.toml"),
include_str!("valid/table-array-implicit.json")
);
test!(
table_array_many,
include_str!("valid/table-array-many.toml"),
include_str!("valid/table-array-many.json")
);
test!(
table_array_nest,
include_str!("valid/table-array-nest.toml"),
include_str!("valid/table-array-nest.json")
);
test!(
table_array_one,
include_str!("valid/table-array-one.toml"),
include_str!("valid/table-array-one.json")
);
test!(
table_empty,
include_str!("valid/table-empty.toml"),
include_str!("valid/table-empty.json")
);
test!(
table_sub_empty,
include_str!("valid/table-sub-empty.toml"),
include_str!("valid/table-sub-empty.json")
);
test!(
table_multi_empty,
include_str!("valid/table-multi-empty.toml"),
include_str!("valid/table-multi-empty.json")
);
test!(
table_whitespace,
include_str!("valid/table-whitespace.toml"),
include_str!("valid/table-whitespace.json")
);
test!(
table_with_pound,
include_str!("valid/table-with-pound.toml"),
include_str!("valid/table-with-pound.json")
);
test!(
unicode_escape,
include_str!("valid/unicode-escape.toml"),
include_str!("valid/unicode-escape.json")
);
test!(
unicode_literal,
include_str!("valid/unicode-literal.toml"),
include_str!("valid/unicode-literal.json")
);
#[cfg(any())]
test!(
hard_example,
include_str!("valid/hard_example.toml"),
include_str!("valid/hard_example.json")
);
#[cfg(any())]
test!(
example2,
include_str!("valid/example2.toml"),
include_str!("valid/example2.json")
);
#[cfg(any())]
test!(
example3,
include_str!("valid/example-v0.3.0.toml"),
include_str!("valid/example-v0.3.0.json")
);
#[cfg(any())]
test!(
example4,
include_str!("valid/example-v0.4.0.toml"),
include_str!("valid/example-v0.4.0.json")
);
#[cfg(any())]
test!(
example_bom,
include_str!("valid/example-bom.toml"),
include_str!("valid/example.json")
);
#[cfg(any())]
test!(
datetime_truncate,
include_str!("valid/datetime-truncate.toml"),
include_str!("valid/datetime-truncate.json")
);
test!(
key_quote_newline,
include_str!("valid/key-quote-newline.toml"),
include_str!("valid/key-quote-newline.json")
);
test!(
table_array_nest_no_keys,
include_str!("valid/table-array-nest-no-keys.toml"),
include_str!("valid/table-array-nest-no-keys.json")
);
test!(
dotted_keys,
include_str!("valid/dotted-keys.toml"),
include_str!("valid/dotted-keys.json")
);
test!(
quote_surrounded_value,
include_str!("valid/quote-surrounded-value.toml"),
include_str!("valid/quote-surrounded-value.json")
);
test!(
float_exponent,
include_str!("valid/float-exponent.toml"),
include_str!("valid/float-exponent.json")
);
test!(
string_delim_end,
include_str!("valid/string-delim-end.toml"),
include_str!("valid/string-delim-end.json")
);
+11
View File
@@ -0,0 +1,11 @@
{
"thevoid": { "type": "array", "value": [
{"type": "array", "value": [
{"type": "array", "value": [
{"type": "array", "value": [
{"type": "array", "value": []}
]}
]}
]}
]}
}
+1
View File
@@ -0,0 +1 @@
thevoid = [[[[[]]]]]
@@ -0,0 +1,11 @@
{
"arrays-and-ints": {
"type": "array",
"value": [
{"type": "integer", "value": "1"},
{"type": "array", "value": [
{ "type": "string", "value":"Arrays are not integers."}
]}
]
}
}
@@ -0,0 +1 @@
arrays-and-ints = [1, ["Arrays are not integers."]]
@@ -0,0 +1,9 @@
{
"ints-and-floats": {
"type": "array",
"value": [
{"type": "integer", "value": "1"},
{"type": "float", "value": "1.1"}
]
}
}
@@ -0,0 +1 @@
ints-and-floats = [1, 1.1]
@@ -0,0 +1,9 @@
{
"strings-and-ints": {
"type": "array",
"value": [
{"type": "string", "value": "hi"},
{"type": "integer", "value": "42"}
]
}
}
@@ -0,0 +1 @@
strings-and-ints = ["hi", 42]
+10
View File
@@ -0,0 +1,10 @@
{
"ints": {
"type": "array",
"value": [
{"type": "integer", "value": "1"},
{"type": "integer", "value": "2"},
{"type": "integer", "value": "3"}
]
}
}
+1
View File
@@ -0,0 +1 @@
ints = [1,2,3]
+19
View File
@@ -0,0 +1,19 @@
{
"mixed": {
"type": "array",
"value": [
{"type": "array", "value": [
{"type": "integer", "value": "1"},
{"type": "integer", "value": "2"}
]},
{"type": "array", "value": [
{"type": "string", "value": "a"},
{"type": "string", "value": "b"}
]},
{"type": "array", "value": [
{"type": "float", "value": "1.1"},
{"type": "float", "value": "2.1"}
]}
]
}
}
@@ -0,0 +1 @@
mixed = [[1, 2], ["a", "b"], [1.1, 2.1]]
+13
View File
@@ -0,0 +1,13 @@
{
"nest": {
"type": "array",
"value": [
{"type": "array", "value": [
{"type": "string", "value": "a"}
]},
{"type": "array", "value": [
{"type": "string", "value": "b"}
]}
]
}
}
+1
View File
@@ -0,0 +1 @@
nest = [["a"], ["b"]]
+34
View File
@@ -0,0 +1,34 @@
{
"ints": {
"type": "array",
"value": [
{"type": "integer", "value": "1"},
{"type": "integer", "value": "2"},
{"type": "integer", "value": "3"}
]
},
"floats": {
"type": "array",
"value": [
{"type": "float", "value": "1.1"},
{"type": "float", "value": "2.1"},
{"type": "float", "value": "3.1"}
]
},
"strings": {
"type": "array",
"value": [
{"type": "string", "value": "a"},
{"type": "string", "value": "b"},
{"type": "string", "value": "c"}
]
},
"dates": {
"type": "array",
"value": [
{"type": "datetime", "value": "1987-07-05T17:45:00Z"},
{"type": "datetime", "value": "1979-05-27T07:32:00Z"},
{"type": "datetime", "value": "2006-06-01T11:00:00Z"}
]
}
}
+8
View File
@@ -0,0 +1,8 @@
ints = [1, 2, 3]
floats = [1.1, 2.1, 3.1]
strings = ["a", "b", "c"]
dates = [
1987-07-05T17:45:00Z,
1979-05-27T07:32:00Z,
2006-06-01T11:00:00Z,
]
+4
View File
@@ -0,0 +1,4 @@
{
"f": {"type": "bool", "value": "false"},
"t": {"type": "bool", "value": "true"}
}
+2
View File
@@ -0,0 +1,2 @@
t = true
f = false
+12
View File
@@ -0,0 +1,12 @@
{
"group": {
"answer": {"type": "integer", "value": "42"},
"more": {
"type": "array",
"value": [
{"type": "integer", "value": "42"},
{"type": "integer", "value": "42"}
]
}
}
}
+24
View File
@@ -0,0 +1,24 @@
# Top comment.
# Top comment.
# Top comment.
# [no-extraneous-groups-please]
[group] # Comment
answer = 42 # Comment
# no-extraneous-keys-please = 999
# In between comment.
more = [ # Comment
# What about multiple # comments?
# Can you handle it?
#
# Evil.
# Evil.
42, 42, # Comments within arrays are fun.
# What about multiple # comments?
# Can you handle it?
#
# Evil.
# Evil.
# ] Did I fool you?
] # Hopefully not.
+6
View File
@@ -0,0 +1,6 @@
{
"bestdayever": {
"type": "datetime",
"value": "1987-07-05T17:45:00.123456789Z"
}
}
+1
View File
@@ -0,0 +1 @@
bestdayever = 1987-07-05T17:45:00.123456789012345Z
+3
View File
@@ -0,0 +1,3 @@
{
"bestdayever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"}
}
+1
View File
@@ -0,0 +1 @@
bestdayever = 1987-07-05T17:45:00Z
+34
View File
@@ -0,0 +1,34 @@
{
"a": {
"b": {
"type": "integer",
"value": "123"
}
},
"table": {
"a": {
"b": {
"c": {
"type": "integer",
"value": "1"
},
"d": {
"type": "integer",
"value": "2"
}
}
},
"in": {
"type": {
"color": {
"type": "string",
"value": "blue"
},
"name": {
"type": "string",
"value": "cat"
}
}
}
}
}
+7
View File
@@ -0,0 +1,7 @@
a.b = 123
[table]
a.b.c = 1
a . b . d = 2
in = { type.name = "cat", type.color = "blue" }
+1
View File
@@ -0,0 +1 @@
{}
View File
Whitespace-only changes.
+5
View File
@@ -0,0 +1,5 @@
best-day-ever = 1987-07-05T17:45:00Z
[numtheory]
boring = false
perfection = [6, 28, 496]
+1
View File
@@ -0,0 +1 @@
{"Array":{"key1":{"type":"array","value":[{"type":"integer","value":"1"},{"type":"integer","value":"2"},{"type":"integer","value":"3"}]},"key2":{"type":"array","value":[{"type":"string","value":"red"},{"type":"string","value":"yellow"},{"type":"string","value":"green"}]},"key3":{"type":"array","value":[{"type":"array","value":[{"type":"integer","value":"1"},{"type":"integer","value":"2"}]},{"type":"array","value":[{"type":"integer","value":"3"},{"type":"integer","value":"4"},{"type":"integer","value":"5"}]}]},"key4":{"type":"array","value":[{"type":"array","value":[{"type":"integer","value":"1"},{"type":"integer","value":"2"}]},{"type":"array","value":[{"type":"string","value":"a"},{"type":"string","value":"b"},{"type":"string","value":"c"}]}]},"key5":{"type":"array","value":[{"type":"integer","value":"1"},{"type":"integer","value":"2"},{"type":"integer","value":"3"}]},"key6":{"type":"array","value":[{"type":"integer","value":"1"},{"type":"integer","value":"2"}]}},"Booleans":{"False":{"type":"bool","value":"false"},"True":{"type":"bool","value":"true"}},"Datetime":{"key1":{"type":"datetime","value":"1979-05-27T07:32:00Z"}},"Float":{"both":{},"exponent":{},"fractional":{"key1":{"type":"float","value":"1.0"},"key2":{"type":"float","value":"3.1415"},"key3":{"type":"float","value":"-0.01"}}},"Integer":{"key1":{"type":"integer","value":"99"},"key2":{"type":"integer","value":"42"},"key3":{"type":"integer","value":"0"},"key4":{"type":"integer","value":"-17"}},"String":{"Literal":{"Multiline":{"lines":{"type":"string","value":"The first newline is\ntrimmed in raw strings.\n All other whitespace\n is preserved.\n"},"regex2":{"type":"string","value":"I [dw]on't need \\d{2} apples"}},"quoted":{"type":"string","value":"Tom \"Dubs\" Preston-Werner"},"regex":{"type":"string","value":"\u003c\\i\\c*\\s*\u003e"},"winpath":{"type":"string","value":"C:\\Users\\nodejs\\templates"},"winpath2":{"type":"string","value":"\\\\ServerX\\admin$\\system32\\"}},"Multiline":{"key1":{"type":"string","value":"One\nTwo"},"key2":{"type":"string","value":"One\nTwo"},"key3":{"type":"string","value":"One\nTwo"}},"Multilined":{"Singleline":{"key1":{"type":"string","value":"The quick brown fox jumps over the lazy dog."},"key2":{"type":"string","value":"The quick brown fox jumps over the lazy dog."},"key3":{"type":"string","value":"The quick brown fox jumps over the lazy dog."}}},"basic":{"type":"string","value":"I'm a string. \"You can quote me\". Name\u0009José\nLocation\u0009SF."}},"Table":{"key":{"type":"string","value":"value"}},"dog":{"tater":{"type":{"type":"string","value":"pug"}}},"fruit":[{"name":{"type":"string","value":"apple"},"physical":{"color":{"type":"string","value":"red"},"shape":{"type":"string","value":"round"}},"variety":[{"name":{"type":"string","value":"red delicious"}},{"name":{"type":"string","value":"granny smith"}}]},{"name":{"type":"string","value":"banana"},"variety":[{"name":{"type":"string","value":"plantain"}}]}],"products":[{"name":{"type":"string","value":"Hammer"},"sku":{"type":"integer","value":"738594937"}},{},{"color":{"type":"string","value":"gray"},"name":{"type":"string","value":"Nail"},"sku":{"type":"integer","value":"284758393"}}],"x":{"y":{"z":{"w":{}}}}}
Loaded 100 of 17624 files, more files were not shown because too many files have changed in this diff. Show more