aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeongik Cha <jeongik@google.com>2023-09-14 16:35:19 +0900
committerJeongik Cha <jeongik@google.com>2023-09-27 03:09:12 +0900
commit4a17ad57d3426684d3c5335ad61b923eff0e91bb (patch)
tree40d4534a13f7679e3b351dd83923db6bc3422329
parent627e3353d06c6963fea9f849a062339b58ffd2d0 (diff)
downloadtoml_edit-4a17ad57d3426684d3c5335ad61b923eff0e91bb.tar.gz
Import toml_edit
Bug: 277909042 Test: build Change-Id: I493bcfcd368bcb2ffbf23ad7a36ddd4b1094b9e9
-rw-r--r--.cargo_vcs_info.json6
-rw-r--r--Android.bp24
-rw-r--r--Cargo.lock898
-rw-r--r--Cargo.toml156
-rw-r--r--Cargo.toml.orig68
-rw-r--r--LICENSE228
-rw-r--r--LICENSE-APACHE201
-rw-r--r--LICENSE-MIT21
-rw-r--r--METADATA20
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--MODULE_LICENSE_MIT0
-rw-r--r--OWNERS1
-rw-r--r--README.md59
-rw-r--r--examples/visit.rs284
-rw-r--r--src/array.rs403
-rw-r--r--src/array_of_tables.rs166
-rw-r--r--src/de/array.rs97
-rw-r--r--src/de/datetime.rs43
-rw-r--r--src/de/key.rs140
-rw-r--r--src/de/mod.rs289
-rw-r--r--src/de/spanned.rs70
-rw-r--r--src/de/table.rs213
-rw-r--r--src/de/table_enum.rs160
-rw-r--r--src/de/value.rs252
-rw-r--r--src/document.rs113
-rw-r--r--src/encode.rs569
-rw-r--r--src/index.rs156
-rw-r--r--src/inline_table.rs679
-rw-r--r--src/internal_string.rs183
-rw-r--r--src/item.rs393
-rw-r--r--src/key.rs344
-rw-r--r--src/lib.rs124
-rw-r--r--src/parser/array.rs146
-rw-r--r--src/parser/datetime.rs446
-rw-r--r--src/parser/document.rs141
-rw-r--r--src/parser/errors.rs316
-rw-r--r--src/parser/inline_table.rs181
-rw-r--r--src/parser/key.rs112
-rw-r--r--src/parser/mod.rs265
-rw-r--r--src/parser/numbers.rs397
-rw-r--r--src/parser/state.rs323
-rw-r--r--src/parser/strings.rs478
-rw-r--r--src/parser/table.rs89
-rw-r--r--src/parser/trivia.rs156
-rw-r--r--src/parser/value.rs155
-rw-r--r--src/raw_string.rs182
-rw-r--r--src/repr.rs253
-rw-r--r--src/ser/array.rs84
-rw-r--r--src/ser/key.rs173
-rw-r--r--src/ser/map.rs405
-rw-r--r--src/ser/mod.rs165
-rw-r--r--src/ser/pretty.rs45
-rw-r--r--src/ser/value.rs243
-rw-r--r--src/table.rs757
-rw-r--r--src/value.rs372
-rw-r--r--src/visit.rs236
-rw-r--r--src/visit_mut.rs252
-rw-r--r--tests/decoder.rs100
-rw-r--r--tests/decoder_compliance.rs17
-rw-r--r--tests/encoder.rs111
-rw-r--r--tests/encoder_compliance.rs14
-rw-r--r--tests/fixtures/invalid/array/double-comma-1.stderr6
-rw-r--r--tests/fixtures/invalid/array/double-comma-2.stderr6
-rw-r--r--tests/fixtures/invalid/array/extending-table.stderr6
-rw-r--r--tests/fixtures/invalid/array/missing-separator.stderr6
-rw-r--r--tests/fixtures/invalid/array/no-close-2.stderr6
-rw-r--r--tests/fixtures/invalid/array/no-close-table-2.stderr6
-rw-r--r--tests/fixtures/invalid/array/no-close-table.stderr6
-rw-r--r--tests/fixtures/invalid/array/no-close.stderr6
-rw-r--r--tests/fixtures/invalid/array/tables-1.stderr6
-rw-r--r--tests/fixtures/invalid/array/tables-2.stderr6
-rw-r--r--tests/fixtures/invalid/array/text-after-array-entries.stderr6
-rw-r--r--tests/fixtures/invalid/array/text-before-array-separator.stderr6
-rw-r--r--tests/fixtures/invalid/array/text-in-array.stderr6
-rw-r--r--tests/fixtures/invalid/bool/almost-false-with-extra.stderr6
-rw-r--r--tests/fixtures/invalid/bool/almost-false.stderr6
-rw-r--r--tests/fixtures/invalid/bool/almost-true-with-extra.stderr6
-rw-r--r--tests/fixtures/invalid/bool/almost-true.stderr6
-rw-r--r--tests/fixtures/invalid/bool/just-f.stderr6
-rw-r--r--tests/fixtures/invalid/bool/just-t.stderr6
-rw-r--r--tests/fixtures/invalid/bool/mixed-case.stderr6
-rw-r--r--tests/fixtures/invalid/bool/starting-same-false.stderr5
-rw-r--r--tests/fixtures/invalid/bool/starting-same-true.stderr5
-rw-r--r--tests/fixtures/invalid/bool/wrong-case-false.stderr6
-rw-r--r--tests/fixtures/invalid/bool/wrong-case-true.stderr6
-rw-r--r--tests/fixtures/invalid/control/bare-cr.stderr5
-rw-r--r--tests/fixtures/invalid/control/bare-formfeed.stderr6
-rw-r--r--tests/fixtures/invalid/control/bare-null.stderrbin0 -> 126 bytes
-rw-r--r--tests/fixtures/invalid/control/bare-vertical-tab.stderr6
-rw-r--r--tests/fixtures/invalid/control/comment-cr.stderr6
-rw-r--r--tests/fixtures/invalid/control/comment-del.stderr5
-rw-r--r--tests/fixtures/invalid/control/comment-lf.stderr5
-rw-r--r--tests/fixtures/invalid/control/comment-null.stderrbin0 -> 124 bytes
-rw-r--r--tests/fixtures/invalid/control/comment-us.stderr5
-rw-r--r--tests/fixtures/invalid/control/control.stderr6
-rw-r--r--tests/fixtures/invalid/control/multi-del.stderr5
-rw-r--r--tests/fixtures/invalid/control/multi-lf.stderr5
-rw-r--r--tests/fixtures/invalid/control/multi-null.stderrbin0 -> 128 bytes
-rw-r--r--tests/fixtures/invalid/control/multi-us.stderr5
-rw-r--r--tests/fixtures/invalid/control/rawmulti-del.stderr5
-rw-r--r--tests/fixtures/invalid/control/rawmulti-lf.stderr5
-rw-r--r--tests/fixtures/invalid/control/rawmulti-null.stderrbin0 -> 136 bytes
-rw-r--r--tests/fixtures/invalid/control/rawmulti-us.stderr5
-rw-r--r--tests/fixtures/invalid/control/rawstring-del.stderr5
-rw-r--r--tests/fixtures/invalid/control/rawstring-lf.stderr5
-rw-r--r--tests/fixtures/invalid/control/rawstring-null.stderrbin0 -> 122 bytes
-rw-r--r--tests/fixtures/invalid/control/rawstring-us.stderr5
-rw-r--r--tests/fixtures/invalid/control/string-bs.stderr5
-rw-r--r--tests/fixtures/invalid/control/string-del.stderr5
-rw-r--r--tests/fixtures/invalid/control/string-lf.stderr5
-rw-r--r--tests/fixtures/invalid/control/string-null.stderrbin0 -> 114 bytes
-rw-r--r--tests/fixtures/invalid/control/string-us.stderr5
-rw-r--r--tests/fixtures/invalid/datetime/hour-over.stderr5
-rw-r--r--tests/fixtures/invalid/datetime/mday-over.stderr6
-rw-r--r--tests/fixtures/invalid/datetime/mday-under.stderr6
-rw-r--r--tests/fixtures/invalid/datetime/minute-over.stderr6
-rw-r--r--tests/fixtures/invalid/datetime/month-over.stderr6
-rw-r--r--tests/fixtures/invalid/datetime/month-under.stderr6
-rw-r--r--tests/fixtures/invalid/datetime/no-leads-with-milli.stderr5
-rw-r--r--tests/fixtures/invalid/datetime/no-leads.stderr5
-rw-r--r--tests/fixtures/invalid/datetime/no-secs.stderr5
-rw-r--r--tests/fixtures/invalid/datetime/no-t.stderr5
-rw-r--r--tests/fixtures/invalid/datetime/second-over.stderr6
-rw-r--r--tests/fixtures/invalid/datetime/time-no-leads-2.stderr5
-rw-r--r--tests/fixtures/invalid/datetime/time-no-leads.stderr5
-rw-r--r--tests/fixtures/invalid/datetime/trailing-t.stderr5
-rw-r--r--tests/fixtures/invalid/encoding/bad-utf8-at-end.stderr1
-rw-r--r--tests/fixtures/invalid/encoding/bad-utf8-in-comment.stderr1
-rw-r--r--tests/fixtures/invalid/encoding/bad-utf8-in-multiline-literal.stderr1
-rw-r--r--tests/fixtures/invalid/encoding/bad-utf8-in-multiline.stderr1
-rw-r--r--tests/fixtures/invalid/encoding/bad-utf8-in-string-literal.stderr1
-rw-r--r--tests/fixtures/invalid/encoding/bad-utf8-in-string.stderr1
-rw-r--r--tests/fixtures/invalid/encoding/bom-not-at-start-1.stderr1
-rw-r--r--tests/fixtures/invalid/encoding/bom-not-at-start-2.stderr1
-rw-r--r--tests/fixtures/invalid/encoding/utf16-bom.stderr1
-rw-r--r--tests/fixtures/invalid/encoding/utf16.stderrbin0 -> 105 bytes
-rw-r--r--tests/fixtures/invalid/float/double-point-1.stderr6
-rw-r--r--tests/fixtures/invalid/float/double-point-2.stderr5
-rw-r--r--tests/fixtures/invalid/float/exp-double-e-1.stderr5
-rw-r--r--tests/fixtures/invalid/float/exp-double-e-2.stderr5
-rw-r--r--tests/fixtures/invalid/float/exp-double-us.stderr5
-rw-r--r--tests/fixtures/invalid/float/exp-leading-us.stderr5
-rw-r--r--tests/fixtures/invalid/float/exp-point-1.stderr5
-rw-r--r--tests/fixtures/invalid/float/exp-point-2.stderr6
-rw-r--r--tests/fixtures/invalid/float/exp-trailing-us.stderr5
-rw-r--r--tests/fixtures/invalid/float/float.stderr5
-rw-r--r--tests/fixtures/invalid/float/inf-incomplete-1.stderr6
-rw-r--r--tests/fixtures/invalid/float/inf-incomplete-2.stderr5
-rw-r--r--tests/fixtures/invalid/float/inf-incomplete-3.stderr5
-rw-r--r--tests/fixtures/invalid/float/inf_underscore.stderr6
-rw-r--r--tests/fixtures/invalid/float/leading-point-neg.stderr5
-rw-r--r--tests/fixtures/invalid/float/leading-point-plus.stderr5
-rw-r--r--tests/fixtures/invalid/float/leading-point.stderr6
-rw-r--r--tests/fixtures/invalid/float/leading-us.stderr6
-rw-r--r--tests/fixtures/invalid/float/leading-zero-neg.stderr5
-rw-r--r--tests/fixtures/invalid/float/leading-zero-plus.stderr5
-rw-r--r--tests/fixtures/invalid/float/leading-zero.stderr5
-rw-r--r--tests/fixtures/invalid/float/nan-incomplete-1.stderr6
-rw-r--r--tests/fixtures/invalid/float/nan-incomplete-2.stderr5
-rw-r--r--tests/fixtures/invalid/float/nan-incomplete-3.stderr5
-rw-r--r--tests/fixtures/invalid/float/nan_underscore.stderr6
-rw-r--r--tests/fixtures/invalid/float/trailing-point-min.stderr6
-rw-r--r--tests/fixtures/invalid/float/trailing-point-plus.stderr6
-rw-r--r--tests/fixtures/invalid/float/trailing-point.stderr6
-rw-r--r--tests/fixtures/invalid/float/trailing-us-exp.stderr6
-rw-r--r--tests/fixtures/invalid/float/trailing-us.stderr6
-rw-r--r--tests/fixtures/invalid/float/us-after-point.stderr6
-rw-r--r--tests/fixtures/invalid/float/us-before-point.stderr6
-rw-r--r--tests/fixtures/invalid/inline-table/add.stderr6
-rw-r--r--tests/fixtures/invalid/inline-table/double-comma.stderr6
-rw-r--r--tests/fixtures/invalid/inline-table/duplicate-key.stderr5
-rw-r--r--tests/fixtures/invalid/inline-table/empty.stderr6
-rw-r--r--tests/fixtures/invalid/inline-table/linebreak-1.stderr6
-rw-r--r--tests/fixtures/invalid/inline-table/linebreak-2.stderr6
-rw-r--r--tests/fixtures/invalid/inline-table/linebreak-3.stderr6
-rw-r--r--tests/fixtures/invalid/inline-table/linebreak-4.stderr6
-rw-r--r--tests/fixtures/invalid/inline-table/no-comma.stderr6
-rw-r--r--tests/fixtures/invalid/inline-table/overwrite.stderr5
-rw-r--r--tests/fixtures/invalid/inline-table/trailing-comma.stderr6
-rw-r--r--tests/fixtures/invalid/integer/capital-bin.stderr5
-rw-r--r--tests/fixtures/invalid/integer/capital-hex.stderr5
-rw-r--r--tests/fixtures/invalid/integer/capital-oct.stderr5
-rw-r--r--tests/fixtures/invalid/integer/double-sign-nex.stderr5
-rw-r--r--tests/fixtures/invalid/integer/double-sign-plus.stderr5
-rw-r--r--tests/fixtures/invalid/integer/double-us.stderr6
-rw-r--r--tests/fixtures/invalid/integer/incomplete-bin.stderr5
-rw-r--r--tests/fixtures/invalid/integer/incomplete-hex.stderr5
-rw-r--r--tests/fixtures/invalid/integer/incomplete-oct.stderr5
-rw-r--r--tests/fixtures/invalid/integer/integer.stderr5
-rw-r--r--tests/fixtures/invalid/integer/invalid-bin.stderr5
-rw-r--r--tests/fixtures/invalid/integer/invalid-hex.stderr5
-rw-r--r--tests/fixtures/invalid/integer/invalid-oct.stderr5
-rw-r--r--tests/fixtures/invalid/integer/leading-us-bin.stderr6
-rw-r--r--tests/fixtures/invalid/integer/leading-us-hex.stderr6
-rw-r--r--tests/fixtures/invalid/integer/leading-us-oct.stderr6
-rw-r--r--tests/fixtures/invalid/integer/leading-us.stderr6
-rw-r--r--tests/fixtures/invalid/integer/leading-zero-1.stderr5
-rw-r--r--tests/fixtures/invalid/integer/leading-zero-2.stderr5
-rw-r--r--tests/fixtures/invalid/integer/leading-zero-3.stderr5
-rw-r--r--tests/fixtures/invalid/integer/leading-zero-sign-1.stderr5
-rw-r--r--tests/fixtures/invalid/integer/leading-zero-sign-2.stderr5
-rw-r--r--tests/fixtures/invalid/integer/leading-zero-sign-3.stderr5
-rw-r--r--tests/fixtures/invalid/integer/negative-bin.stderr5
-rw-r--r--tests/fixtures/invalid/integer/negative-hex.stderr5
-rw-r--r--tests/fixtures/invalid/integer/negative-oct.stderr5
-rw-r--r--tests/fixtures/invalid/integer/positive-bin.stderr5
-rw-r--r--tests/fixtures/invalid/integer/positive-hex.stderr5
-rw-r--r--tests/fixtures/invalid/integer/positive-oct.stderr5
-rw-r--r--tests/fixtures/invalid/integer/text-after-integer.stderr5
-rw-r--r--tests/fixtures/invalid/integer/trailing-us-bin.stderr6
-rw-r--r--tests/fixtures/invalid/integer/trailing-us-hex.stderr6
-rw-r--r--tests/fixtures/invalid/integer/trailing-us-oct.stderr6
-rw-r--r--tests/fixtures/invalid/integer/trailing-us.stderr6
-rw-r--r--tests/fixtures/invalid/integer/us-after-bin.stderr5
-rw-r--r--tests/fixtures/invalid/integer/us-after-hex.stderr5
-rw-r--r--tests/fixtures/invalid/integer/us-after-oct.stderr5
-rw-r--r--tests/fixtures/invalid/key/after-array.stderr6
-rw-r--r--tests/fixtures/invalid/key/after-table.stderr6
-rw-r--r--tests/fixtures/invalid/key/after-value.stderr5
-rw-r--r--tests/fixtures/invalid/key/bare-invalid-character.stderr5
-rw-r--r--tests/fixtures/invalid/key/dotted-redefine-table.stderr5
-rw-r--r--tests/fixtures/invalid/key/duplicate-keys.stderr5
-rw-r--r--tests/fixtures/invalid/key/duplicate.stderr5
-rw-r--r--tests/fixtures/invalid/key/empty.stderr5
-rw-r--r--tests/fixtures/invalid/key/escape.stderr5
-rw-r--r--tests/fixtures/invalid/key/hash.stderr5
-rw-r--r--tests/fixtures/invalid/key/multiline.stderr5
-rw-r--r--tests/fixtures/invalid/key/newline.stderr5
-rw-r--r--tests/fixtures/invalid/key/no-eol.stderr5
-rw-r--r--tests/fixtures/invalid/key/open-bracket.stderr6
-rw-r--r--tests/fixtures/invalid/key/partial-quoted.stderr5
-rw-r--r--tests/fixtures/invalid/key/quoted-unclosed-1.stderr5
-rw-r--r--tests/fixtures/invalid/key/quoted-unclosed-2.stderr5
-rw-r--r--tests/fixtures/invalid/key/single-open-bracket.stderr5
-rw-r--r--tests/fixtures/invalid/key/space.stderr5
-rw-r--r--tests/fixtures/invalid/key/special-character.stderr5
-rw-r--r--tests/fixtures/invalid/key/start-bracket.stderr6
-rw-r--r--tests/fixtures/invalid/key/start-dot.stderr5
-rw-r--r--tests/fixtures/invalid/key/two-equals.stderr6
-rw-r--r--tests/fixtures/invalid/key/two-equals2.stderr6
-rw-r--r--tests/fixtures/invalid/key/two-equals3.stderr6
-rw-r--r--tests/fixtures/invalid/key/without-value-1.stderr5
-rw-r--r--tests/fixtures/invalid/key/without-value-2.stderr6
-rw-r--r--tests/fixtures/invalid/key/without-value-3.stderr5
-rw-r--r--tests/fixtures/invalid/key/without-value-4.stderr6
-rw-r--r--tests/fixtures/invalid/spec/inline-table-2-0.stderr5
-rw-r--r--tests/fixtures/invalid/spec/inline-table-3-0.stderr5
-rw-r--r--tests/fixtures/invalid/spec/key-value-pair-1.stderr6
-rw-r--r--tests/fixtures/invalid/spec/keys-2.stderr5
-rw-r--r--tests/fixtures/invalid/spec/string-4-0.stderr5
-rw-r--r--tests/fixtures/invalid/spec/string-7-0.stderr5
-rw-r--r--tests/fixtures/invalid/spec/table-9-0.stderr6
-rw-r--r--tests/fixtures/invalid/spec/table-9-1.stderr6
-rw-r--r--tests/fixtures/invalid/string/bad-byte-escape.stderr6
-rw-r--r--tests/fixtures/invalid/string/bad-codepoint.stderr6
-rw-r--r--tests/fixtures/invalid/string/bad-concat.stderr5
-rw-r--r--tests/fixtures/invalid/string/bad-escape-1.stderr6
-rw-r--r--tests/fixtures/invalid/string/bad-escape-2.stderr6
-rw-r--r--tests/fixtures/invalid/string/bad-hex-esc-1.stderr6
-rw-r--r--tests/fixtures/invalid/string/bad-hex-esc-2.stderr6
-rw-r--r--tests/fixtures/invalid/string/bad-hex-esc-3.stderr6
-rw-r--r--tests/fixtures/invalid/string/bad-hex-esc-4.stderr6
-rw-r--r--tests/fixtures/invalid/string/bad-hex-esc-5.stderr6
-rw-r--r--tests/fixtures/invalid/string/bad-hex-esc.stderr6
-rw-r--r--tests/fixtures/invalid/string/bad-multiline.stderr5
-rw-r--r--tests/fixtures/invalid/string/bad-slash-escape.stderr6
-rw-r--r--tests/fixtures/invalid/string/bad-uni-esc-1.stderr5
-rw-r--r--tests/fixtures/invalid/string/bad-uni-esc-2.stderr5
-rw-r--r--tests/fixtures/invalid/string/bad-uni-esc-3.stderr5
-rw-r--r--tests/fixtures/invalid/string/bad-uni-esc-4.stderr5
-rw-r--r--tests/fixtures/invalid/string/bad-uni-esc-5.stderr5
-rw-r--r--tests/fixtures/invalid/string/basic-byte-escapes.stderr6
-rw-r--r--tests/fixtures/invalid/string/basic-multiline-out-of-range-unicode-escape-1.stderr6
-rw-r--r--tests/fixtures/invalid/string/basic-multiline-out-of-range-unicode-escape-2.stderr6
-rw-r--r--tests/fixtures/invalid/string/basic-multiline-quotes.stderr5
-rw-r--r--tests/fixtures/invalid/string/basic-multiline-unknown-escape.stderr6
-rw-r--r--tests/fixtures/invalid/string/basic-out-of-range-unicode-escape-1.stderr6
-rw-r--r--tests/fixtures/invalid/string/basic-out-of-range-unicode-escape-2.stderr6
-rw-r--r--tests/fixtures/invalid/string/basic-unknown-escape.stderr6
-rw-r--r--tests/fixtures/invalid/string/literal-multiline-quotes-1.stderr5
-rw-r--r--tests/fixtures/invalid/string/literal-multiline-quotes-2.stderr5
-rw-r--r--tests/fixtures/invalid/string/missing-quotes.stderr6
-rw-r--r--tests/fixtures/invalid/string/multiline-bad-escape-1.stderr6
-rw-r--r--tests/fixtures/invalid/string/multiline-bad-escape-2.stderr6
-rw-r--r--tests/fixtures/invalid/string/multiline-bad-escape-3.stderr6
-rw-r--r--tests/fixtures/invalid/string/multiline-escape-space.stderr6
-rw-r--r--tests/fixtures/invalid/string/multiline-no-close-2.stderr5
-rw-r--r--tests/fixtures/invalid/string/multiline-no-close.stderr5
-rw-r--r--tests/fixtures/invalid/string/multiline-quotes-1.stderr5
-rw-r--r--tests/fixtures/invalid/string/no-close.stderr5
-rw-r--r--tests/fixtures/invalid/string/text-after-string.stderr5
-rw-r--r--tests/fixtures/invalid/string/wrong-close.stderr5
-rw-r--r--tests/fixtures/invalid/table/append-with-dotted-keys-1.stderr5
-rw-r--r--tests/fixtures/invalid/table/append-with-dotted-keys-2.stderr5
-rw-r--r--tests/fixtures/invalid/table/array-empty.stderr5
-rw-r--r--tests/fixtures/invalid/table/array-implicit.stderr6
-rw-r--r--tests/fixtures/invalid/table/array-missing-bracket.stderr6
-rw-r--r--tests/fixtures/invalid/table/duplicate-key-dotted-table.stderr6
-rw-r--r--tests/fixtures/invalid/table/duplicate-key-dotted-table2.stderr6
-rw-r--r--tests/fixtures/invalid/table/duplicate-key-table.stderr6
-rw-r--r--tests/fixtures/invalid/table/duplicate-table-array.stderr6
-rw-r--r--tests/fixtures/invalid/table/duplicate-table-array2.stderr6
-rw-r--r--tests/fixtures/invalid/table/duplicate.stderr6
-rw-r--r--tests/fixtures/invalid/table/empty-implicit-table.stderr6
-rw-r--r--tests/fixtures/invalid/table/empty.stderr5
-rw-r--r--tests/fixtures/invalid/table/equals-sign.stderr6
-rw-r--r--tests/fixtures/invalid/table/llbrace.stderr5
-rw-r--r--tests/fixtures/invalid/table/nested-brackets-close.stderr6
-rw-r--r--tests/fixtures/invalid/table/nested-brackets-open.stderr6
-rw-r--r--tests/fixtures/invalid/table/quoted-no-close.stderr5
-rw-r--r--tests/fixtures/invalid/table/redefine.stderr6
-rw-r--r--tests/fixtures/invalid/table/rrbrace.stderr6
-rw-r--r--tests/fixtures/invalid/table/text-after-table.stderr6
-rw-r--r--tests/fixtures/invalid/table/whitespace.stderr6
-rw-r--r--tests/fixtures/invalid/table/with-pound.stderr6
-rw-r--r--tests/invalid.rs26
-rw-r--r--tests/testsuite/convert.rs79
-rw-r--r--tests/testsuite/datetime.rs256
-rw-r--r--tests/testsuite/edit.rs855
-rw-r--r--tests/testsuite/invalid.rs211
-rw-r--r--tests/testsuite/main.rs8
-rw-r--r--tests/testsuite/parse.rs1490
-rw-r--r--tests/testsuite/stackoverflow.rs54
323 files changed, 17269 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644
index 0000000..fbf4047
--- /dev/null
+++ b/.cargo_vcs_info.json
@@ -0,0 +1,6 @@
+{
+ "git": {
+ "sha1": "5753e4fd8ddf78980c41aabaec78a935eaba93e1"
+ },
+ "path_in_vcs": "crates/toml_edit"
+} \ No newline at end of file
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..e023edc
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,24 @@
+// This file is generated by cargo2android.py --run --features default,serde.
+// Do not modify this file as changes will be overridden on upgrade.
+
+
+
+rust_library_host {
+ name: "libtoml_edit",
+ crate_name: "toml_edit",
+ cargo_env_compat: true,
+ cargo_pkg_version: "0.19.14",
+ srcs: ["src/lib.rs"],
+ edition: "2021",
+ features: [
+ "default",
+ "serde",
+ ],
+ rustlibs: [
+ "libindexmap",
+ "libserde",
+ "libserde_spanned",
+ "libtoml_datetime",
+ "libwinnow",
+ ],
+}
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..5dfc8c2
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,898 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6342bd4f5a1205d7f41e94a41a901f5647c938cdfa96036338e8533c9d6c2450"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is-terminal",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bstr"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "clap"
+version = "4.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39"
+dependencies = [
+ "bitflags",
+ "clap_derive",
+ "clap_lex",
+ "is-terminal",
+ "once_cell",
+ "strsim",
+ "termcolor",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.105",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
+
+[[package]]
+name = "errno"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "globset"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "fnv",
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
+[[package]]
+name = "ignore"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
+dependencies = [
+ "crossbeam-utils",
+ "globset",
+ "lazy_static",
+ "log",
+ "memchr",
+ "regex",
+ "same-file",
+ "thread_local",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "include_dir"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e"
+dependencies = [
+ "include_dir_macros",
+]
+
+[[package]]
+name = "include_dir_macros"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c"
+dependencies = [
+ "libc",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
+
+[[package]]
+name = "kstring"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747"
+dependencies = [
+ "static_assertions",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.142"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
+
+[[package]]
+name = "libtest-mimic"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7b603516767d1ab23d0de09d023e62966c3322f7148297c35cf3d97aa8b37fa"
+dependencies = [
+ "clap",
+ "termcolor",
+ "threadpool",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
+
+[[package]]
+name = "os_str_bytes"
+version = "6.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.105",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
+
+[[package]]
+name = "rustix"
+version = "0.37.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.160"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.160"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.96"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "similar"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803"
+
+[[package]]
+name = "snapbox"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6bccd62078347f89a914e3004d94582e13824d4e3d8a816317862884c423835"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "ignore",
+ "libtest-mimic",
+ "normalize-line-endings",
+ "similar",
+ "snapbox-macros",
+]
+
+[[package]]
+name = "snapbox-macros"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaaf09df9f0eeae82be96290918520214530e738a7fe5a351b0f24cf77c0ca31"
+dependencies = [
+ "anstream",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "threadpool"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
+dependencies = [
+ "num_cpus",
+]
+
+[[package]]
+name = "toml-test"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37351256790aa1dbd6d60f4ff08e55e7f372e292f3e9040d6e077463d9a779c3"
+dependencies = [
+ "chrono",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "toml-test-data"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93f351b6d6005ee802b0d4a53ca1cdf05636f441df4d299e62cba57f1da52646"
+dependencies = [
+ "include_dir",
+]
+
+[[package]]
+name = "toml-test-harness"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e00fda5710922fe6b3005bf6a5050c303d6f9625249c37b7386e8818f4af675"
+dependencies = [
+ "ignore",
+ "libtest-mimic",
+ "toml-test",
+ "toml-test-data",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.14"
+dependencies = [
+ "indexmap",
+ "kstring",
+ "libtest-mimic",
+ "serde",
+ "serde_json",
+ "serde_spanned",
+ "snapbox",
+ "toml-test-data",
+ "toml-test-harness",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.1",
+ "windows_aarch64_msvc 0.42.1",
+ "windows_i686_gnu 0.42.1",
+ "windows_i686_msvc 0.42.1",
+ "windows_x86_64_gnu 0.42.1",
+ "windows_x86_64_gnullvm 0.42.1",
+ "windows_x86_64_msvc 0.42.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.1",
+ "windows_aarch64_msvc 0.42.1",
+ "windows_i686_gnu 0.42.1",
+ "windows_i686_msvc 0.42.1",
+ "windows_x86_64_gnu 0.42.1",
+ "windows_x86_64_gnullvm 0.42.1",
+ "windows_x86_64_msvc 0.42.1",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
+name = "winnow"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7"
+dependencies = [
+ "memchr",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..26929ff
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,156 @@
+# 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"
+rust-version = "1.64.0"
+name = "toml_edit"
+version = "0.19.14"
+authors = [
+ "Andronik Ordian <write@reusable.software>",
+ "Ed Page <eopage@gmail.com>",
+]
+include = [
+ "build.rs",
+ "src/**/*",
+ "Cargo.toml",
+ "Cargo.lock",
+ "LICENSE*",
+ "README.md",
+ "benches/**/*",
+ "examples/**/*",
+ "tests/**/*",
+]
+description = "Yet another format-preserving TOML parser."
+readme = "README.md"
+keywords = [
+ "encoding",
+ "toml",
+]
+categories = [
+ "encoding",
+ "parser-implementations",
+ "parsing",
+ "config",
+]
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/toml-rs/toml"
+
+[package.metadata.docs.rs]
+features = ["serde"]
+rustdoc-args = [
+ "--cfg",
+ "docsrs",
+]
+
+[package.metadata.release]
+tag-name = "v{{version}}"
+
+[[package.metadata.release.pre-release-replacements]]
+file = "CHANGELOG.md"
+min = 1
+replace = "{{version}}"
+search = "Unreleased"
+
+[[package.metadata.release.pre-release-replacements]]
+exactly = 1
+file = "CHANGELOG.md"
+replace = "...{{tag_name}}"
+search = '\.\.\.HEAD'
+
+[[package.metadata.release.pre-release-replacements]]
+file = "CHANGELOG.md"
+min = 1
+replace = "{{date}}"
+search = "ReleaseDate"
+
+[[package.metadata.release.pre-release-replacements]]
+exactly = 1
+file = "CHANGELOG.md"
+replace = """
+<!-- next-header -->
+## [Unreleased] - ReleaseDate
+"""
+search = "<!-- next-header -->"
+
+[[package.metadata.release.pre-release-replacements]]
+exactly = 1
+file = "CHANGELOG.md"
+replace = """
+<!-- next-url -->
+[Unreleased]: https://github.com/toml-rs/toml/compare/{{tag_name}}...HEAD"""
+search = "<!-- next-url -->"
+
+[[example]]
+name = "visit"
+test = true
+
+[[test]]
+name = "decoder_compliance"
+harness = false
+
+[[test]]
+name = "encoder_compliance"
+harness = false
+
+[[test]]
+name = "invalid"
+harness = false
+
+[dependencies.indexmap]
+version = "2.0.0"
+features = ["std"]
+
+[dependencies.kstring]
+version = "2.0.0"
+features = ["max_inline"]
+optional = true
+
+[dependencies.serde]
+version = "1.0.145"
+optional = true
+
+[dependencies.serde_spanned]
+version = "0.6.3"
+features = ["serde"]
+optional = true
+
+[dependencies.toml_datetime]
+version = "0.6.3"
+
+[dependencies.winnow]
+version = "0.5.0"
+
+[dev-dependencies.libtest-mimic]
+version = "0.6.0"
+
+[dev-dependencies.serde_json]
+version = "1.0.96"
+
+[dev-dependencies.snapbox]
+version = "0.4.11"
+features = ["harness"]
+
+[dev-dependencies.toml-test-data]
+version = "1.3.0"
+
+[dev-dependencies.toml-test-harness]
+version = "0.4.3"
+
+[features]
+default = []
+perf = ["dep:kstring"]
+serde = [
+ "dep:serde",
+ "toml_datetime/serde",
+ "dep:serde_spanned",
+]
+unbounded = []
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644
index 0000000..cda679f
--- /dev/null
+++ b/Cargo.toml.orig
@@ -0,0 +1,68 @@
+[package]
+name = "toml_edit"
+version = "0.19.14"
+keywords = ["encoding", "toml"]
+categories = ["encoding", "parser-implementations", "parsing", "config"]
+description = "Yet another format-preserving TOML parser."
+authors = ["Andronik Ordian <write@reusable.software>", "Ed Page <eopage@gmail.com>"]
+repository.workspace = true
+license.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+include.workspace = true
+
+[package.metadata.docs.rs]
+rustdoc-args = ["--cfg", "docsrs"]
+features = ["serde"]
+
+[package.metadata.release]
+tag-name = "v{{version}}"
+pre-release-replacements = [
+ {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1},
+ {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1},
+ {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1},
+ {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## [Unreleased] - ReleaseDate\n", exactly=1},
+ {file="CHANGELOG.md", search="<!-- next-url -->", replace="<!-- next-url -->\n[Unreleased]: https://github.com/toml-rs/toml/compare/{{tag_name}}...HEAD", exactly=1},
+]
+
+[features]
+default = []
+perf = ["dep:kstring"]
+serde = ["dep:serde", "toml_datetime/serde", "dep:serde_spanned"]
+# Provide a method disable_recursion_limit to parse arbitrarily deep structures
+# without any consideration for overflowing the stack. Additionally you will
+# need to be careful around other recursive operations on the parsed result
+# which may overflow the stack after deserialization has completed, including,
+# but not limited to, Display and Debug and Drop impls.
+unbounded = []
+
+[dependencies]
+indexmap = { version = "2.0.0", features = ["std"] }
+winnow = "0.5.0"
+serde = { version = "1.0.145", optional = true }
+kstring = { version = "2.0.0", features = ["max_inline"], optional = true }
+toml_datetime = { version = "0.6.3", path = "../toml_datetime" }
+serde_spanned = { version = "0.6.3", path = "../serde_spanned", features = ["serde"], optional = true }
+
+[dev-dependencies]
+serde_json = "1.0.96"
+toml-test-harness = "0.4.3"
+toml-test-data = "1.3.0"
+libtest-mimic = "0.6.0"
+snapbox = { version = "0.4.11", features = ["harness"] }
+
+[[test]]
+name = "decoder_compliance"
+harness = false
+
+[[test]]
+name = "encoder_compliance"
+harness = false
+
+[[test]]
+name = "invalid"
+harness = false
+
+[[example]]
+name = "visit"
+test = true
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b73c718
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,228 @@
+ 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
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+---
+
+MIT License
+
+Copyright (c) 2017 Andronik Ordian
+
+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.
+
+---
+
diff --git a/LICENSE-APACHE b/LICENSE-APACHE
new file mode 100644
index 0000000..16fe87b
--- /dev/null
+++ b/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ 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
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/LICENSE-MIT b/LICENSE-MIT
new file mode 100644
index 0000000..b9e61a2
--- /dev/null
+++ b/LICENSE-MIT
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Andronik Ordian
+
+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.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..6a96df6
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,20 @@
+name: "toml_edit"
+description: "Yet another format-preserving TOML parser."
+third_party {
+ identifier {
+ type: "crates.io"
+ value: "https://crates.io/crates/toml_edit"
+ }
+ identifier {
+ type: "Archive"
+ value: "https://static.crates.io/crates/toml_edit/toml_edit-0.19.14.crate"
+ }
+ version: "0.19.14"
+ # Dual-licensed, using the least restrictive per go/thirdpartylicenses#same.
+ license_type: NOTICE
+ last_upgrade_date {
+ year: 2023
+ month: 8
+ day: 23
+ }
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/MODULE_LICENSE_MIT b/MODULE_LICENSE_MIT
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_MIT
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..45dc4dd
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1 @@
+include platform/prebuilts/rust:master:/OWNERS
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f1c74ee
--- /dev/null
+++ b/README.md
@@ -0,0 +1,59 @@
+# toml_edit
+
+[![Build Status](https://github.com/ordian/toml_edit/workflows/Continuous%20integration/badge.svg)](https://github.com/ordian/toml_edit/actions)
+[![codecov](https://codecov.io/gh/ordian/toml_edit/branch/master/graph/badge.svg)](https://codecov.io/gh/ordian/toml_edit)
+[![crates.io](https://img.shields.io/crates/v/toml_edit.svg)](https://crates.io/crates/toml_edit)
+[![docs](https://docs.rs/toml_edit/badge.svg)](https://docs.rs/toml_edit)
+[![Join the chat at https://gitter.im/toml_edit/Lobby](https://badges.gitter.im/a.svg)](https://gitter.im/toml_edit/Lobby)
+
+
+This crate allows you to parse and modify toml
+documents, while preserving comments, spaces *and
+relative order* or items.
+
+`toml_edit` is primarily tailored for [cargo-edit](https://github.com/killercup/cargo-edit/) needs.
+
+## Example
+
+```rust
+use toml_edit::{Document, value};
+
+fn main() {
+ let toml = r#"
+"hello" = 'toml!' # comment
+['a'.b]
+ "#;
+ let mut doc = toml.parse::<Document>().expect("invalid doc");
+ assert_eq!(doc.to_string(), toml);
+ // let's add a new key/value pair inside a.b: c = {d = "hello"}
+ doc["a"]["b"]["c"]["d"] = value("hello");
+ // autoformat inline table a.b.c: { d = "hello" }
+ doc["a"]["b"]["c"].as_inline_table_mut().map(|t| t.fmt());
+ let expected = r#"
+"hello" = 'toml!' # comment
+['a'.b]
+c = { d = "hello" }
+ "#;
+ assert_eq!(doc.to_string(), expected);
+}
+```
+
+## Limitations
+
+Things it does not preserve:
+
+* Scattered array of tables (tables are reordered by default, see [test]).
+* Order of dotted keys, see [issue](https://github.com/ordian/toml_edit/issues/163).
+
+## License
+
+Licensed under either of
+
+- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://apache.org/licenses/LICENSE-2.0)
+- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
+
+### Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
+
+[test]: https://github.com/ordian/toml_edit/blob/f09bd5d075fdb7d2ef8d9bb3270a34506c276753/tests/test_valid.rs#L84
diff --git a/examples/visit.rs b/examples/visit.rs
new file mode 100644
index 0000000..cd7f851
--- /dev/null
+++ b/examples/visit.rs
@@ -0,0 +1,284 @@
+//! Example for how to use `VisitMut` to iterate over a table.
+
+use std::collections::BTreeSet;
+use toml_edit::visit::*;
+use toml_edit::visit_mut::*;
+use toml_edit::{Array, Document, InlineTable, Item, KeyMut, Table, Value};
+
+/// This models the visit state for dependency keys in a `Cargo.toml`.
+///
+/// Dependencies can be specified as:
+///
+/// ```toml
+/// [dependencies]
+/// dep1 = "0.2"
+///
+/// [build-dependencies]
+/// dep2 = "0.3"
+///
+/// [dev-dependencies]
+/// dep3 = "0.4"
+///
+/// [target.'cfg(windows)'.dependencies]
+/// dep4 = "0.5"
+///
+/// # and target build- and dev-dependencies
+/// ```
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+enum VisitState {
+ /// Represents the root of the table.
+ Root,
+ /// Represents "dependencies", "build-dependencies" or "dev-dependencies", or the target
+ /// forms of these.
+ Dependencies,
+ /// A table within dependencies.
+ SubDependencies,
+ /// Represents "target".
+ Target,
+ /// "target.[TARGET]".
+ TargetWithSpec,
+ /// Represents some other state.
+ Other,
+}
+
+impl VisitState {
+ /// Figures out the next visit state, given the current state and the given key.
+ fn descend(self, key: &str) -> Self {
+ match (self, key) {
+ (
+ VisitState::Root | VisitState::TargetWithSpec,
+ "dependencies" | "build-dependencies" | "dev-dependencies",
+ ) => VisitState::Dependencies,
+ (VisitState::Root, "target") => VisitState::Target,
+ (VisitState::Root | VisitState::TargetWithSpec, _) => VisitState::Other,
+ (VisitState::Target, _) => VisitState::TargetWithSpec,
+ (VisitState::Dependencies, _) => VisitState::SubDependencies,
+ (VisitState::SubDependencies, _) => VisitState::SubDependencies,
+ (VisitState::Other, _) => VisitState::Other,
+ }
+ }
+}
+
+/// Collect the names of every dependency key.
+#[derive(Debug)]
+struct DependencyNameVisitor<'doc> {
+ state: VisitState,
+ names: BTreeSet<&'doc str>,
+}
+
+impl<'doc> Visit<'doc> for DependencyNameVisitor<'doc> {
+ fn visit_table_like_kv(&mut self, key: &'doc str, node: &'doc Item) {
+ if self.state == VisitState::Dependencies {
+ self.names.insert(key);
+ } else {
+ // Since we're only interested in collecting the top-level keys right under
+ // [dependencies], don't recurse unconditionally.
+
+ let old_state = self.state;
+
+ // Figure out the next state given the key.
+ self.state = self.state.descend(key);
+
+ // Recurse further into the document tree.
+ visit_table_like_kv(self, key, node);
+
+ // Restore the old state after it's done.
+ self.state = old_state;
+ }
+ }
+}
+
+/// Normalize all dependency tables into the format:
+///
+/// ```toml
+/// [dependencies]
+/// dep = { version = "1.0", features = ["foo", "bar"], ... }
+/// ```
+///
+/// leaving other tables untouched.
+#[derive(Debug)]
+struct NormalizeDependencyTablesVisitor {
+ state: VisitState,
+}
+
+impl VisitMut for NormalizeDependencyTablesVisitor {
+ fn visit_table_mut(&mut self, node: &mut Table) {
+ visit_table_mut(self, node);
+
+ // The conversion from regular tables into inline ones might leave some explicit parent
+ // tables hanging, so convert them to implicit.
+ if matches!(self.state, VisitState::Target | VisitState::TargetWithSpec) {
+ node.set_implicit(true);
+ }
+ }
+
+ fn visit_table_like_kv_mut(&mut self, mut key: KeyMut<'_>, node: &mut Item) {
+ let old_state = self.state;
+
+ // Figure out the next state given the key.
+ self.state = self.state.descend(key.get());
+
+ match self.state {
+ VisitState::Target | VisitState::TargetWithSpec | VisitState::Dependencies => {
+ // Top-level dependency row, or above: turn inline tables into regular ones.
+ if let Item::Value(Value::InlineTable(inline_table)) = node {
+ let inline_table = std::mem::replace(inline_table, InlineTable::new());
+ let table = inline_table.into_table();
+ key.fmt();
+ *node = Item::Table(table);
+ }
+ }
+ VisitState::SubDependencies => {
+ // Individual dependency: turn regular tables into inline ones.
+ if let Item::Table(table) = node {
+ // Turn the table into an inline table.
+ let table = std::mem::replace(table, Table::new());
+ let inline_table = table.into_inline_table();
+ key.fmt();
+ *node = Item::Value(Value::InlineTable(inline_table));
+ }
+ }
+ _ => {}
+ }
+
+ // Recurse further into the document tree.
+ visit_table_like_kv_mut(self, key, node);
+
+ // Restore the old state after it's done.
+ self.state = old_state;
+ }
+
+ fn visit_array_mut(&mut self, node: &mut Array) {
+ // Format any arrays within dependencies to be on the same line.
+ if matches!(
+ self.state,
+ VisitState::Dependencies | VisitState::SubDependencies
+ ) {
+ node.fmt();
+ }
+ }
+}
+
+/// This is the input provided to visit_mut_example.
+static INPUT: &str = r#"
+[package]
+name = "my-package"
+
+[package.metadata.foo]
+bar = 42
+
+[dependencies]
+atty = "0.2"
+cargo-platform = { path = "crates/cargo-platform", version = "0.1.2" }
+
+[dependencies.pretty_env_logger]
+version = "0.4"
+optional = true
+
+[target.'cfg(windows)'.dependencies]
+fwdansi = "1.1.0"
+
+[target.'cfg(windows)'.dependencies.winapi]
+version = "0.3"
+features = [
+"handleapi",
+"jobapi",
+]
+
+[target.'cfg(unix)']
+dev-dependencies = { miniz_oxide = "0.5" }
+
+[dev-dependencies.cargo-test-macro]
+path = "crates/cargo-test-macro"
+
+[build-dependencies.flate2]
+version = "0.4"
+"#;
+
+/// This is the output produced by visit_mut_example.
+#[cfg(test)]
+static VISIT_MUT_OUTPUT: &str = r#"
+[package]
+name = "my-package"
+
+[package.metadata.foo]
+bar = 42
+
+[dependencies]
+atty = "0.2"
+cargo-platform = { path = "crates/cargo-platform", version = "0.1.2" }
+pretty_env_logger = { version = "0.4", optional = true }
+
+[target.'cfg(windows)'.dependencies]
+fwdansi = "1.1.0"
+winapi = { version = "0.3", features = ["handleapi", "jobapi"] }
+
+[target.'cfg(unix)'.dev-dependencies]
+miniz_oxide = "0.5"
+
+[dev-dependencies]
+cargo-test-macro = { path = "crates/cargo-test-macro" }
+
+[build-dependencies]
+flate2 = { version = "0.4" }
+"#;
+
+fn visit_example(document: &Document) -> BTreeSet<&str> {
+ let mut visitor = DependencyNameVisitor {
+ state: VisitState::Root,
+ names: BTreeSet::new(),
+ };
+
+ visitor.visit_document(document);
+
+ visitor.names
+}
+
+fn visit_mut_example(document: &mut Document) {
+ let mut visitor = NormalizeDependencyTablesVisitor {
+ state: VisitState::Root,
+ };
+
+ visitor.visit_document_mut(document);
+}
+
+fn main() {
+ let mut document: Document = INPUT.parse().expect("input is valid TOML");
+
+ println!("** visit example");
+ println!("{:?}", visit_example(&document));
+
+ println!("** visit_mut example");
+ visit_mut_example(&mut document);
+ println!("{}", document);
+}
+
+#[cfg(test)]
+#[test]
+fn visit_correct() {
+ let document: Document = INPUT.parse().expect("input is valid TOML");
+
+ let names = visit_example(&document);
+ let expected = vec![
+ "atty",
+ "cargo-platform",
+ "pretty_env_logger",
+ "fwdansi",
+ "winapi",
+ "miniz_oxide",
+ "cargo-test-macro",
+ "flate2",
+ ]
+ .into_iter()
+ .collect();
+ assert_eq!(names, expected);
+}
+
+#[cfg(test)]
+#[test]
+fn visit_mut_correct() {
+ let mut document: Document = INPUT.parse().expect("input is valid TOML");
+
+ visit_mut_example(&mut document);
+ assert_eq!(format!("{}", document), VISIT_MUT_OUTPUT);
+}
diff --git a/src/array.rs b/src/array.rs
new file mode 100644
index 0000000..045b451
--- /dev/null
+++ b/src/array.rs
@@ -0,0 +1,403 @@
+use std::iter::FromIterator;
+use std::mem;
+
+use crate::repr::Decor;
+use crate::value::{DEFAULT_LEADING_VALUE_DECOR, DEFAULT_VALUE_DECOR};
+use crate::{Item, RawString, Value};
+
+/// Type representing a TOML array,
+/// payload of the `Value::Array` variant's value
+#[derive(Debug, Default, Clone)]
+pub struct Array {
+ // `trailing` represents whitespaces, newlines
+ // and comments in an empty array or after the trailing comma
+ trailing: RawString,
+ trailing_comma: bool,
+ // prefix before `[` and suffix after `]`
+ decor: Decor,
+ pub(crate) span: Option<std::ops::Range<usize>>,
+ // always Vec<Item::Value>
+ pub(crate) values: Vec<Item>,
+}
+
+/// An owned iterator type over `Table`'s key/value pairs.
+pub type ArrayIntoIter = Box<dyn Iterator<Item = Value>>;
+/// An iterator type over `Array`'s values.
+pub type ArrayIter<'a> = Box<dyn Iterator<Item = &'a Value> + 'a>;
+/// An iterator type over `Array`'s values.
+pub type ArrayIterMut<'a> = Box<dyn Iterator<Item = &'a mut Value> + 'a>;
+
+/// Constructors
+///
+/// See also `FromIterator`
+impl Array {
+ /// Create an empty `Array`
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// let mut arr = toml_edit::Array::new();
+ /// ```
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ pub(crate) fn with_vec(values: Vec<Item>) -> Self {
+ Self {
+ values,
+ ..Default::default()
+ }
+ }
+}
+
+/// Formatting
+impl Array {
+ /// Auto formats the array.
+ pub fn fmt(&mut self) {
+ decorate_array(self);
+ }
+
+ /// Set whether the array will use a trailing comma
+ pub fn set_trailing_comma(&mut self, yes: bool) {
+ self.trailing_comma = yes;
+ }
+
+ /// Whether the array will use a trailing comma
+ pub fn trailing_comma(&self) -> bool {
+ self.trailing_comma
+ }
+
+ /// Set whitespace after last element
+ pub fn set_trailing(&mut self, trailing: impl Into<RawString>) {
+ self.trailing = trailing.into();
+ }
+
+ /// Whitespace after last element
+ pub fn trailing(&self) -> &RawString {
+ &self.trailing
+ }
+
+ /// Returns the surrounding whitespace
+ pub fn decor_mut(&mut self) -> &mut Decor {
+ &mut self.decor
+ }
+
+ /// Returns the surrounding whitespace
+ pub fn decor(&self) -> &Decor {
+ &self.decor
+ }
+
+ /// Returns the location within the original document
+ pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+ self.span.clone()
+ }
+
+ pub(crate) fn despan(&mut self, input: &str) {
+ self.span = None;
+ self.decor.despan(input);
+ self.trailing.despan(input);
+ for value in &mut self.values {
+ value.despan(input);
+ }
+ }
+}
+
+impl Array {
+ /// Returns an iterator over all values.
+ pub fn iter(&self) -> ArrayIter<'_> {
+ Box::new(self.values.iter().filter_map(Item::as_value))
+ }
+
+ /// Returns an iterator over all values.
+ pub fn iter_mut(&mut self) -> ArrayIterMut<'_> {
+ Box::new(self.values.iter_mut().filter_map(Item::as_value_mut))
+ }
+
+ /// Returns the length of the underlying Vec.
+ ///
+ /// In some rare cases, placeholder elements will exist. For a more accurate count, call
+ /// `a.iter().count()`
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// let mut arr = toml_edit::Array::new();
+ /// arr.push(1);
+ /// arr.push("foo");
+ /// assert_eq!(arr.len(), 2);
+ /// ```
+ pub fn len(&self) -> usize {
+ self.values.len()
+ }
+
+ /// Return true iff `self.len() == 0`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// let mut arr = toml_edit::Array::new();
+ /// assert!(arr.is_empty());
+ ///
+ /// arr.push(1);
+ /// arr.push("foo");
+ /// assert!(! arr.is_empty());
+ /// ```
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// Clears the array, removing all values. Keeps the allocated memory for reuse.
+ pub fn clear(&mut self) {
+ self.values.clear()
+ }
+
+ /// Returns a reference to the value at the given index, or `None` if the index is out of
+ /// bounds.
+ pub fn get(&self, index: usize) -> Option<&Value> {
+ self.values.get(index).and_then(Item::as_value)
+ }
+
+ /// Returns a reference to the value at the given index, or `None` if the index is out of
+ /// bounds.
+ pub fn get_mut(&mut self, index: usize) -> Option<&mut Value> {
+ self.values.get_mut(index).and_then(Item::as_value_mut)
+ }
+
+ /// Appends a new value to the end of the array, applying default formatting to it.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// let mut arr = toml_edit::Array::new();
+ /// arr.push(1);
+ /// arr.push("foo");
+ /// ```
+ pub fn push<V: Into<Value>>(&mut self, v: V) {
+ self.value_op(v.into(), true, |items, value| {
+ items.push(Item::Value(value))
+ })
+ }
+
+ /// Appends a new, already formatted value to the end of the array.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// let formatted_value = "'literal'".parse::<toml_edit::Value>().unwrap();
+ /// let mut arr = toml_edit::Array::new();
+ /// arr.push_formatted(formatted_value);
+ /// ```
+ pub fn push_formatted(&mut self, v: Value) {
+ self.values.push(Item::Value(v));
+ }
+
+ /// Inserts an element at the given position within the array, applying default formatting to
+ /// it and shifting all values after it to the right.
+ ///
+ /// # Panics
+ ///
+ /// Panics if `index > len`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// let mut arr = toml_edit::Array::new();
+ /// arr.push(1);
+ /// arr.push("foo");
+ ///
+ /// arr.insert(0, "start");
+ /// ```
+ pub fn insert<V: Into<Value>>(&mut self, index: usize, v: V) {
+ self.value_op(v.into(), true, |items, value| {
+ items.insert(index, Item::Value(value))
+ })
+ }
+
+ /// Inserts an already formatted value at the given position within the array, shifting all
+ /// values after it to the right.
+ ///
+ /// # Panics
+ ///
+ /// Panics if `index > len`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// let mut arr = toml_edit::Array::new();
+ /// arr.push(1);
+ /// arr.push("foo");
+ ///
+ /// let formatted_value = "'start'".parse::<toml_edit::Value>().unwrap();
+ /// arr.insert_formatted(0, formatted_value);
+ /// ```
+ pub fn insert_formatted(&mut self, index: usize, v: Value) {
+ self.values.insert(index, Item::Value(v))
+ }
+
+ /// Replaces the element at the given position within the array, preserving existing formatting.
+ ///
+ /// # Panics
+ ///
+ /// Panics if `index >= len`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// let mut arr = toml_edit::Array::new();
+ /// arr.push(1);
+ /// arr.push("foo");
+ ///
+ /// arr.replace(0, "start");
+ /// ```
+ pub fn replace<V: Into<Value>>(&mut self, index: usize, v: V) -> Value {
+ // Read the existing value's decor and preserve it.
+ let existing_decor = self
+ .get(index)
+ .unwrap_or_else(|| panic!("index {} out of bounds (len = {})", index, self.len()))
+ .decor();
+ let mut value = v.into();
+ *value.decor_mut() = existing_decor.clone();
+ self.replace_formatted(index, value)
+ }
+
+ /// Replaces the element at the given position within the array with an already formatted value.
+ ///
+ /// # Panics
+ ///
+ /// Panics if `index >= len`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// let mut arr = toml_edit::Array::new();
+ /// arr.push(1);
+ /// arr.push("foo");
+ ///
+ /// let formatted_value = "'start'".parse::<toml_edit::Value>().unwrap();
+ /// arr.replace_formatted(0, formatted_value);
+ /// ```
+ pub fn replace_formatted(&mut self, index: usize, v: Value) -> Value {
+ match mem::replace(&mut self.values[index], Item::Value(v)) {
+ Item::Value(old_value) => old_value,
+ x => panic!("non-value item {:?} in an array", x),
+ }
+ }
+
+ /// Removes the value at the given index.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// let mut arr = toml_edit::Array::new();
+ /// arr.push(1);
+ /// arr.push("foo");
+ ///
+ /// arr.remove(0);
+ /// assert_eq!(arr.len(), 1);
+ /// ```
+ pub fn remove(&mut self, index: usize) -> Value {
+ let removed = self.values.remove(index);
+ match removed {
+ Item::Value(v) => v,
+ x => panic!("non-value item {:?} in an array", x),
+ }
+ }
+
+ /// Retains only the values specified by the `keep` predicate.
+ ///
+ /// In other words, remove all values for which `keep(&value)` returns `false`.
+ ///
+ /// This method operates in place, visiting each element exactly once in the
+ /// original order, and preserves the order of the retained elements.
+ pub fn retain<F>(&mut self, mut keep: F)
+ where
+ F: FnMut(&Value) -> bool,
+ {
+ self.values
+ .retain(|item| item.as_value().map(&mut keep).unwrap_or(false));
+ }
+
+ fn value_op<T>(
+ &mut self,
+ v: Value,
+ decorate: bool,
+ op: impl FnOnce(&mut Vec<Item>, Value) -> T,
+ ) -> T {
+ let mut value = v;
+ if !self.is_empty() && decorate {
+ value.decorate(" ", "");
+ } else if decorate {
+ value.decorate("", "");
+ }
+ op(&mut self.values, value)
+ }
+}
+
+impl std::fmt::Display for Array {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ crate::encode::Encode::encode(self, f, None, ("", ""))
+ }
+}
+
+impl<V: Into<Value>> Extend<V> for Array {
+ fn extend<T: IntoIterator<Item = V>>(&mut self, iter: T) {
+ for value in iter {
+ self.push_formatted(value.into());
+ }
+ }
+}
+
+impl<V: Into<Value>> FromIterator<V> for Array {
+ fn from_iter<I>(iter: I) -> Self
+ where
+ I: IntoIterator<Item = V>,
+ {
+ let v = iter.into_iter().map(|a| Item::Value(a.into()));
+ Array {
+ values: v.collect(),
+ ..Default::default()
+ }
+ }
+}
+
+impl IntoIterator for Array {
+ type Item = Value;
+ type IntoIter = ArrayIntoIter;
+
+ fn into_iter(self) -> Self::IntoIter {
+ Box::new(
+ self.values
+ .into_iter()
+ .filter(|v| v.is_value())
+ .map(|v| v.into_value().unwrap()),
+ )
+ }
+}
+
+impl<'s> IntoIterator for &'s Array {
+ type Item = &'s Value;
+ type IntoIter = ArrayIter<'s>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter()
+ }
+}
+
+fn decorate_array(array: &mut Array) {
+ for (i, value) in array
+ .values
+ .iter_mut()
+ .filter_map(Item::as_value_mut)
+ .enumerate()
+ {
+ // [value1, value2, value3]
+ if i == 0 {
+ value.decorate(DEFAULT_LEADING_VALUE_DECOR.0, DEFAULT_LEADING_VALUE_DECOR.1);
+ } else {
+ value.decorate(DEFAULT_VALUE_DECOR.0, DEFAULT_VALUE_DECOR.1);
+ }
+ }
+ // Since everything is now on the same line, remove trailing commas and whitespace.
+ array.set_trailing_comma(false);
+ array.set_trailing("");
+}
diff --git a/src/array_of_tables.rs b/src/array_of_tables.rs
new file mode 100644
index 0000000..c4d7194
--- /dev/null
+++ b/src/array_of_tables.rs
@@ -0,0 +1,166 @@
+use std::iter::FromIterator;
+
+use crate::{Array, Item, Table};
+
+/// Type representing a TOML array of tables
+#[derive(Clone, Debug, Default)]
+pub struct ArrayOfTables {
+ // Always Vec<Item::Table>, just `Item` to make `Index` work
+ pub(crate) span: Option<std::ops::Range<usize>>,
+ pub(crate) values: Vec<Item>,
+}
+
+/// Constructors
+///
+/// See also `FromIterator`
+impl ArrayOfTables {
+ /// Creates an empty array of tables.
+ pub fn new() -> Self {
+ Default::default()
+ }
+}
+
+/// Formatting
+impl ArrayOfTables {
+ /// Convert to an inline array
+ pub fn into_array(mut self) -> Array {
+ for value in self.values.iter_mut() {
+ value.make_value();
+ }
+ let mut a = Array::with_vec(self.values);
+ a.fmt();
+ a
+ }
+
+ /// Returns the location within the original document
+ pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+ self.span.clone()
+ }
+
+ pub(crate) fn despan(&mut self, input: &str) {
+ self.span = None;
+ for value in &mut self.values {
+ value.despan(input);
+ }
+ }
+}
+
+impl ArrayOfTables {
+ /// Returns an iterator over tables.
+ pub fn iter(&self) -> ArrayOfTablesIter<'_> {
+ Box::new(self.values.iter().filter_map(Item::as_table))
+ }
+
+ /// Returns an iterator over tables.
+ pub fn iter_mut(&mut self) -> ArrayOfTablesIterMut<'_> {
+ Box::new(self.values.iter_mut().filter_map(Item::as_table_mut))
+ }
+
+ /// Returns the length of the underlying Vec.
+ /// To get the actual number of items use `a.iter().count()`.
+ pub fn len(&self) -> usize {
+ self.values.len()
+ }
+
+ /// Returns true iff `self.len() == 0`.
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// Removes all the tables.
+ pub fn clear(&mut self) {
+ self.values.clear()
+ }
+
+ /// Returns an optional reference to the table.
+ pub fn get(&self, index: usize) -> Option<&Table> {
+ self.values.get(index).and_then(Item::as_table)
+ }
+
+ /// Returns an optional mutable reference to the table.
+ pub fn get_mut(&mut self, index: usize) -> Option<&mut Table> {
+ self.values.get_mut(index).and_then(Item::as_table_mut)
+ }
+
+ /// Appends a table to the array.
+ pub fn push(&mut self, table: Table) {
+ self.values.push(Item::Table(table));
+ }
+
+ /// Removes a table with the given index.
+ pub fn remove(&mut self, index: usize) {
+ self.values.remove(index);
+ }
+
+ /// Retains only the elements specified by the `keep` predicate.
+ ///
+ /// In other words, remove all tables for which `keep(&table)` returns `false`.
+ ///
+ /// This method operates in place, visiting each element exactly once in the
+ /// original order, and preserves the order of the retained elements.
+ pub fn retain<F>(&mut self, mut keep: F)
+ where
+ F: FnMut(&Table) -> bool,
+ {
+ self.values
+ .retain(|item| item.as_table().map(&mut keep).unwrap_or(false));
+ }
+}
+
+/// An iterator type over `ArrayOfTables`'s values.
+pub type ArrayOfTablesIter<'a> = Box<dyn Iterator<Item = &'a Table> + 'a>;
+/// An iterator type over `ArrayOfTables`'s values.
+pub type ArrayOfTablesIterMut<'a> = Box<dyn Iterator<Item = &'a mut Table> + 'a>;
+/// An iterator type over `ArrayOfTables`'s values.
+pub type ArrayOfTablesIntoIter = Box<dyn Iterator<Item = Table>>;
+
+impl Extend<Table> for ArrayOfTables {
+ fn extend<T: IntoIterator<Item = Table>>(&mut self, iter: T) {
+ for value in iter {
+ self.push(value);
+ }
+ }
+}
+
+impl FromIterator<Table> for ArrayOfTables {
+ fn from_iter<I>(iter: I) -> Self
+ where
+ I: IntoIterator<Item = Table>,
+ {
+ let v = iter.into_iter().map(Item::Table);
+ ArrayOfTables {
+ values: v.collect(),
+ span: None,
+ }
+ }
+}
+
+impl IntoIterator for ArrayOfTables {
+ type Item = Table;
+ type IntoIter = ArrayOfTablesIntoIter;
+
+ fn into_iter(self) -> Self::IntoIter {
+ Box::new(
+ self.values
+ .into_iter()
+ .filter(|v| v.is_table())
+ .map(|v| v.into_table().unwrap()),
+ )
+ }
+}
+
+impl<'s> IntoIterator for &'s ArrayOfTables {
+ type Item = &'s Table;
+ type IntoIter = ArrayOfTablesIter<'s>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter()
+ }
+}
+
+impl std::fmt::Display for ArrayOfTables {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ // HACK: Without the header, we don't really have a proper way of printing this
+ self.clone().into_array().fmt(f)
+ }
+}
diff --git a/src/de/array.rs b/src/de/array.rs
new file mode 100644
index 0000000..adc5401
--- /dev/null
+++ b/src/de/array.rs
@@ -0,0 +1,97 @@
+use crate::de::Error;
+
+pub(crate) struct ArrayDeserializer {
+ input: Vec<crate::Item>,
+ span: Option<std::ops::Range<usize>>,
+}
+
+impl ArrayDeserializer {
+ pub(crate) fn new(input: Vec<crate::Item>, span: Option<std::ops::Range<usize>>) -> Self {
+ Self { input, span }
+ }
+}
+
+// Note: this is wrapped by `ValueDeserializer` and any trait methods
+// implemented here need to be wrapped there
+impl<'de> serde::Deserializer<'de> for ArrayDeserializer {
+ type Error = Error;
+
+ fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ visitor.visit_seq(ArraySeqAccess::new(self.input))
+ }
+
+ fn deserialize_struct<V>(
+ self,
+ name: &'static str,
+ fields: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ if serde_spanned::__unstable::is_spanned(name, fields) {
+ if let Some(span) = self.span.clone() {
+ return visitor.visit_map(super::SpannedDeserializer::new(self, span));
+ }
+ }
+
+ self.deserialize_any(visitor)
+ }
+
+ serde::forward_to_deserialize_any! {
+ bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq
+ bytes byte_buf map option unit newtype_struct
+ ignored_any unit_struct tuple_struct tuple enum identifier
+ }
+}
+
+impl<'de> serde::de::IntoDeserializer<'de, crate::de::Error> for ArrayDeserializer {
+ type Deserializer = Self;
+
+ fn into_deserializer(self) -> Self::Deserializer {
+ self
+ }
+}
+
+impl crate::Array {
+ pub(crate) fn into_deserializer(self) -> ArrayDeserializer {
+ ArrayDeserializer::new(self.values, self.span)
+ }
+}
+
+impl crate::ArrayOfTables {
+ pub(crate) fn into_deserializer(self) -> ArrayDeserializer {
+ ArrayDeserializer::new(self.values, self.span)
+ }
+}
+
+pub(crate) struct ArraySeqAccess {
+ iter: std::vec::IntoIter<crate::Item>,
+}
+
+impl ArraySeqAccess {
+ pub(crate) fn new(input: Vec<crate::Item>) -> Self {
+ Self {
+ iter: input.into_iter(),
+ }
+ }
+}
+
+impl<'de> serde::de::SeqAccess<'de> for ArraySeqAccess {
+ type Error = Error;
+
+ fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
+ where
+ T: serde::de::DeserializeSeed<'de>,
+ {
+ match self.iter.next() {
+ Some(v) => seed
+ .deserialize(crate::de::ValueDeserializer::new(v))
+ .map(Some),
+ None => Ok(None),
+ }
+ }
+}
diff --git a/src/de/datetime.rs b/src/de/datetime.rs
new file mode 100644
index 0000000..14de28b
--- /dev/null
+++ b/src/de/datetime.rs
@@ -0,0 +1,43 @@
+use serde::de::value::BorrowedStrDeserializer;
+use serde::de::IntoDeserializer;
+
+use crate::de::Error;
+
+pub(crate) struct DatetimeDeserializer {
+ date: Option<crate::Datetime>,
+}
+
+impl DatetimeDeserializer {
+ pub(crate) fn new(date: crate::Datetime) -> Self {
+ Self { date: Some(date) }
+ }
+}
+
+impl<'de> serde::de::MapAccess<'de> for DatetimeDeserializer {
+ type Error = Error;
+
+ fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
+ where
+ K: serde::de::DeserializeSeed<'de>,
+ {
+ if self.date.is_some() {
+ seed.deserialize(BorrowedStrDeserializer::new(
+ toml_datetime::__unstable::FIELD,
+ ))
+ .map(Some)
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Error>
+ where
+ V: serde::de::DeserializeSeed<'de>,
+ {
+ if let Some(date) = self.date.take() {
+ seed.deserialize(date.to_string().into_deserializer())
+ } else {
+ panic!("next_value_seed called before next_key_seed")
+ }
+ }
+}
diff --git a/src/de/key.rs b/src/de/key.rs
new file mode 100644
index 0000000..3da41df
--- /dev/null
+++ b/src/de/key.rs
@@ -0,0 +1,140 @@
+use serde::de::IntoDeserializer;
+
+use super::Error;
+
+pub(crate) struct KeyDeserializer {
+ span: Option<std::ops::Range<usize>>,
+ key: crate::InternalString,
+}
+
+impl KeyDeserializer {
+ pub(crate) fn new(key: crate::InternalString, span: Option<std::ops::Range<usize>>) -> Self {
+ KeyDeserializer { span, key }
+ }
+}
+
+impl<'de> serde::de::IntoDeserializer<'de, Error> for KeyDeserializer {
+ type Deserializer = Self;
+
+ fn into_deserializer(self) -> Self::Deserializer {
+ self
+ }
+}
+
+impl<'de> serde::de::Deserializer<'de> for KeyDeserializer {
+ type Error = Error;
+
+ fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ self.key.into_deserializer().deserialize_any(visitor)
+ }
+
+ fn deserialize_enum<V>(
+ self,
+ name: &str,
+ variants: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ let _ = name;
+ let _ = variants;
+ visitor.visit_enum(self)
+ }
+
+ fn deserialize_struct<V>(
+ self,
+ name: &'static str,
+ fields: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ if serde_spanned::__unstable::is_spanned(name, fields) {
+ if let Some(span) = self.span.clone() {
+ return visitor.visit_map(super::SpannedDeserializer::new(self.key.as_str(), span));
+ }
+ }
+ self.deserialize_any(visitor)
+ }
+
+ serde::forward_to_deserialize_any! {
+ bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq
+ bytes byte_buf map option unit newtype_struct
+ ignored_any unit_struct tuple_struct tuple identifier
+ }
+}
+
+impl<'de> serde::de::EnumAccess<'de> for KeyDeserializer {
+ type Error = super::Error;
+ type Variant = UnitOnly<Self::Error>;
+
+ fn variant_seed<T>(self, seed: T) -> Result<(T::Value, Self::Variant), Self::Error>
+ where
+ T: serde::de::DeserializeSeed<'de>,
+ {
+ seed.deserialize(self).map(unit_only)
+ }
+}
+
+pub(crate) struct UnitOnly<E> {
+ marker: std::marker::PhantomData<E>,
+}
+
+fn unit_only<T, E>(t: T) -> (T, UnitOnly<E>) {
+ (
+ t,
+ UnitOnly {
+ marker: std::marker::PhantomData,
+ },
+ )
+}
+
+impl<'de, E> serde::de::VariantAccess<'de> for UnitOnly<E>
+where
+ E: serde::de::Error,
+{
+ type Error = E;
+
+ fn unit_variant(self) -> Result<(), Self::Error> {
+ Ok(())
+ }
+
+ fn newtype_variant_seed<T>(self, _seed: T) -> Result<T::Value, Self::Error>
+ where
+ T: serde::de::DeserializeSeed<'de>,
+ {
+ Err(serde::de::Error::invalid_type(
+ serde::de::Unexpected::UnitVariant,
+ &"newtype variant",
+ ))
+ }
+
+ fn tuple_variant<V>(self, _len: usize, _visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ Err(serde::de::Error::invalid_type(
+ serde::de::Unexpected::UnitVariant,
+ &"tuple variant",
+ ))
+ }
+
+ fn struct_variant<V>(
+ self,
+ _fields: &'static [&'static str],
+ _visitor: V,
+ ) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ Err(serde::de::Error::invalid_type(
+ serde::de::Unexpected::UnitVariant,
+ &"struct variant",
+ ))
+ }
+}
diff --git a/src/de/mod.rs b/src/de/mod.rs
new file mode 100644
index 0000000..09ea120
--- /dev/null
+++ b/src/de/mod.rs
@@ -0,0 +1,289 @@
+//! Deserializing TOML into Rust structures.
+//!
+//! This module contains all the Serde support for deserializing TOML documents into Rust structures.
+
+use serde::de::DeserializeOwned;
+
+mod array;
+mod datetime;
+mod key;
+mod spanned;
+mod table;
+mod table_enum;
+mod value;
+
+use array::ArrayDeserializer;
+use datetime::DatetimeDeserializer;
+use key::KeyDeserializer;
+use spanned::SpannedDeserializer;
+use table::TableMapAccess;
+use table_enum::TableEnumDeserializer;
+
+pub use value::ValueDeserializer;
+
+/// Errors that can occur when deserializing a type.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Error {
+ inner: crate::TomlError,
+}
+
+impl Error {
+ pub(crate) fn custom<T>(msg: T, span: Option<std::ops::Range<usize>>) -> Self
+ where
+ T: std::fmt::Display,
+ {
+ Error {
+ inner: crate::TomlError::custom(msg.to_string(), span),
+ }
+ }
+
+ /// Add key while unwinding
+ pub fn add_key(&mut self, key: String) {
+ self.inner.add_key(key)
+ }
+
+ /// What went wrong
+ pub fn message(&self) -> &str {
+ self.inner.message()
+ }
+
+ /// The start/end index into the original document where the error occurred
+ pub fn span(&self) -> Option<std::ops::Range<usize>> {
+ self.inner.span()
+ }
+
+ pub(crate) fn set_span(&mut self, span: Option<std::ops::Range<usize>>) {
+ self.inner.set_span(span);
+ }
+}
+
+impl serde::de::Error for Error {
+ fn custom<T>(msg: T) -> Self
+ where
+ T: std::fmt::Display,
+ {
+ Error::custom(msg, None)
+ }
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ self.inner.fmt(f)
+ }
+}
+
+impl From<crate::TomlError> for Error {
+ fn from(e: crate::TomlError) -> Error {
+ Self { inner: e }
+ }
+}
+
+impl From<Error> for crate::TomlError {
+ fn from(e: Error) -> crate::TomlError {
+ e.inner
+ }
+}
+
+impl std::error::Error for Error {}
+
+/// Convert a value into `T`.
+pub fn from_str<T>(s: &'_ str) -> Result<T, Error>
+where
+ T: DeserializeOwned,
+{
+ let de = s.parse::<Deserializer>()?;
+ T::deserialize(de)
+}
+
+/// Convert a value into `T`.
+pub fn from_slice<T>(s: &'_ [u8]) -> Result<T, Error>
+where
+ T: DeserializeOwned,
+{
+ let s = std::str::from_utf8(s).map_err(|e| Error::custom(e, None))?;
+ from_str(s)
+}
+
+/// Convert a document into `T`.
+pub fn from_document<T>(d: crate::Document) -> Result<T, Error>
+where
+ T: DeserializeOwned,
+{
+ let deserializer = Deserializer::new(d);
+ T::deserialize(deserializer)
+}
+
+/// Deserialization for TOML [documents][crate::Document].
+pub struct Deserializer {
+ input: crate::Document,
+}
+
+impl Deserializer {
+ /// Deserialization implementation for TOML.
+ pub fn new(input: crate::Document) -> Self {
+ Self { input }
+ }
+}
+
+impl std::str::FromStr for Deserializer {
+ type Err = Error;
+
+ /// Parses a document from a &str
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let d = crate::parser::parse_document(s).map_err(Error::from)?;
+ Ok(Self::new(d))
+ }
+}
+
+// Note: this is wrapped by `toml::de::Deserializer` and any trait methods
+// implemented here need to be wrapped there
+impl<'de> serde::Deserializer<'de> for Deserializer {
+ type Error = Error;
+
+ fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ let original = self.input.original;
+ self.input
+ .root
+ .into_deserializer()
+ .deserialize_any(visitor)
+ .map_err(|mut e: Self::Error| {
+ e.inner.set_original(original);
+ e
+ })
+ }
+
+ // `None` is interpreted as a missing field so be sure to implement `Some`
+ // as a present field.
+ fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ let original = self.input.original;
+ self.input
+ .root
+ .into_deserializer()
+ .deserialize_option(visitor)
+ .map_err(|mut e: Self::Error| {
+ e.inner.set_original(original);
+ e
+ })
+ }
+
+ fn deserialize_newtype_struct<V>(
+ self,
+ name: &'static str,
+ visitor: V,
+ ) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ let original = self.input.original;
+ self.input
+ .root
+ .into_deserializer()
+ .deserialize_newtype_struct(name, visitor)
+ .map_err(|mut e: Self::Error| {
+ e.inner.set_original(original);
+ e
+ })
+ }
+
+ fn deserialize_struct<V>(
+ self,
+ name: &'static str,
+ fields: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ let original = self.input.original;
+ self.input
+ .root
+ .into_deserializer()
+ .deserialize_struct(name, fields, visitor)
+ .map_err(|mut e: Self::Error| {
+ e.inner.set_original(original);
+ e
+ })
+ }
+
+ // Called when the type to deserialize is an enum, as opposed to a field in the type.
+ fn deserialize_enum<V>(
+ self,
+ name: &'static str,
+ variants: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ let original = self.input.original;
+ self.input
+ .root
+ .into_deserializer()
+ .deserialize_enum(name, variants, visitor)
+ .map_err(|mut e: Self::Error| {
+ e.inner.set_original(original);
+ e
+ })
+ }
+
+ serde::forward_to_deserialize_any! {
+ bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq
+ bytes byte_buf map unit
+ ignored_any unit_struct tuple_struct tuple identifier
+ }
+}
+
+impl<'de> serde::de::IntoDeserializer<'de, crate::de::Error> for Deserializer {
+ type Deserializer = Deserializer;
+
+ fn into_deserializer(self) -> Self::Deserializer {
+ self
+ }
+}
+
+impl<'de> serde::de::IntoDeserializer<'de, crate::de::Error> for crate::Document {
+ type Deserializer = Deserializer;
+
+ fn into_deserializer(self) -> Self::Deserializer {
+ Deserializer::new(self)
+ }
+}
+
+pub(crate) fn validate_struct_keys(
+ table: &crate::table::KeyValuePairs,
+ fields: &'static [&'static str],
+) -> Result<(), Error> {
+ let extra_fields = table
+ .iter()
+ .filter_map(|(key, val)| {
+ if !fields.contains(&key.as_str()) {
+ Some(val.clone())
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+
+ if extra_fields.is_empty() {
+ Ok(())
+ } else {
+ Err(Error::custom(
+ format!(
+ "unexpected keys in table: {}, available keys: {}",
+ extra_fields
+ .iter()
+ .map(|k| k.key.get())
+ .collect::<Vec<_>>()
+ .join(", "),
+ fields.join(", "),
+ ),
+ extra_fields[0].key.span(),
+ ))
+ }
+}
diff --git a/src/de/spanned.rs b/src/de/spanned.rs
new file mode 100644
index 0000000..7ce5864
--- /dev/null
+++ b/src/de/spanned.rs
@@ -0,0 +1,70 @@
+use serde::de::value::BorrowedStrDeserializer;
+use serde::de::IntoDeserializer as _;
+
+use super::Error;
+
+pub(crate) struct SpannedDeserializer<'de, T: serde::de::IntoDeserializer<'de, Error>> {
+ phantom_data: std::marker::PhantomData<&'de ()>,
+ start: Option<usize>,
+ end: Option<usize>,
+ value: Option<T>,
+}
+
+impl<'de, T> SpannedDeserializer<'de, T>
+where
+ T: serde::de::IntoDeserializer<'de, Error>,
+{
+ pub(crate) fn new(value: T, span: std::ops::Range<usize>) -> Self {
+ Self {
+ phantom_data: Default::default(),
+ start: Some(span.start),
+ end: Some(span.end),
+ value: Some(value),
+ }
+ }
+}
+
+impl<'de, T> serde::de::MapAccess<'de> for SpannedDeserializer<'de, T>
+where
+ T: serde::de::IntoDeserializer<'de, Error>,
+{
+ type Error = Error;
+ fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
+ where
+ K: serde::de::DeserializeSeed<'de>,
+ {
+ if self.start.is_some() {
+ seed.deserialize(BorrowedStrDeserializer::new(
+ serde_spanned::__unstable::START_FIELD,
+ ))
+ .map(Some)
+ } else if self.end.is_some() {
+ seed.deserialize(BorrowedStrDeserializer::new(
+ serde_spanned::__unstable::END_FIELD,
+ ))
+ .map(Some)
+ } else if self.value.is_some() {
+ seed.deserialize(BorrowedStrDeserializer::new(
+ serde_spanned::__unstable::VALUE_FIELD,
+ ))
+ .map(Some)
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Error>
+ where
+ V: serde::de::DeserializeSeed<'de>,
+ {
+ if let Some(start) = self.start.take() {
+ seed.deserialize(start.into_deserializer())
+ } else if let Some(end) = self.end.take() {
+ seed.deserialize(end.into_deserializer())
+ } else if let Some(value) = self.value.take() {
+ seed.deserialize(value.into_deserializer())
+ } else {
+ panic!("next_value_seed called before next_key_seed")
+ }
+ }
+}
diff --git a/src/de/table.rs b/src/de/table.rs
new file mode 100644
index 0000000..0b6183e
--- /dev/null
+++ b/src/de/table.rs
@@ -0,0 +1,213 @@
+use serde::de::IntoDeserializer;
+
+use crate::de::Error;
+
+pub(crate) struct TableDeserializer {
+ span: Option<std::ops::Range<usize>>,
+ items: crate::table::KeyValuePairs,
+}
+
+// Note: this is wrapped by `Deserializer` and `ValueDeserializer` and any trait methods
+// implemented here need to be wrapped there
+impl<'de> serde::Deserializer<'de> for TableDeserializer {
+ type Error = Error;
+
+ fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ visitor.visit_map(crate::de::TableMapAccess::new(self))
+ }
+
+ // `None` is interpreted as a missing field so be sure to implement `Some`
+ // as a present field.
+ fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ visitor.visit_some(self)
+ }
+
+ fn deserialize_newtype_struct<V>(
+ self,
+ _name: &'static str,
+ visitor: V,
+ ) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ visitor.visit_newtype_struct(self)
+ }
+
+ fn deserialize_struct<V>(
+ self,
+ name: &'static str,
+ fields: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ if serde_spanned::__unstable::is_spanned(name, fields) {
+ if let Some(span) = self.span.clone() {
+ return visitor.visit_map(super::SpannedDeserializer::new(self, span));
+ }
+ }
+
+ self.deserialize_any(visitor)
+ }
+
+ // Called when the type to deserialize is an enum, as opposed to a field in the type.
+ fn deserialize_enum<V>(
+ self,
+ _name: &'static str,
+ _variants: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ if self.items.is_empty() {
+ Err(crate::de::Error::custom(
+ "wanted exactly 1 element, found 0 elements",
+ self.span,
+ ))
+ } else if self.items.len() != 1 {
+ Err(crate::de::Error::custom(
+ "wanted exactly 1 element, more than 1 element",
+ self.span,
+ ))
+ } else {
+ visitor.visit_enum(crate::de::TableMapAccess::new(self))
+ }
+ }
+
+ serde::forward_to_deserialize_any! {
+ bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq
+ bytes byte_buf map unit
+ ignored_any unit_struct tuple_struct tuple identifier
+ }
+}
+
+impl<'de> serde::de::IntoDeserializer<'de, crate::de::Error> for TableDeserializer {
+ type Deserializer = TableDeserializer;
+
+ fn into_deserializer(self) -> Self::Deserializer {
+ self
+ }
+}
+
+impl crate::Table {
+ pub(crate) fn into_deserializer(self) -> TableDeserializer {
+ TableDeserializer {
+ span: self.span(),
+ items: self.items,
+ }
+ }
+}
+
+impl crate::InlineTable {
+ pub(crate) fn into_deserializer(self) -> TableDeserializer {
+ TableDeserializer {
+ span: self.span(),
+ items: self.items,
+ }
+ }
+}
+
+pub(crate) struct TableMapAccess {
+ iter: indexmap::map::IntoIter<crate::InternalString, crate::table::TableKeyValue>,
+ span: Option<std::ops::Range<usize>>,
+ value: Option<(crate::InternalString, crate::Item)>,
+}
+
+impl TableMapAccess {
+ pub(crate) fn new(input: TableDeserializer) -> Self {
+ Self {
+ iter: input.items.into_iter(),
+ span: input.span,
+ value: None,
+ }
+ }
+}
+
+impl<'de> serde::de::MapAccess<'de> for TableMapAccess {
+ type Error = Error;
+
+ fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
+ where
+ K: serde::de::DeserializeSeed<'de>,
+ {
+ match self.iter.next() {
+ Some((k, v)) => {
+ let ret = seed
+ .deserialize(super::KeyDeserializer::new(k, v.key.span()))
+ .map(Some)
+ .map_err(|mut e: Self::Error| {
+ if e.span().is_none() {
+ e.set_span(v.key.span());
+ }
+ e
+ });
+ self.value = Some((v.key.into(), v.value));
+ ret
+ }
+ None => Ok(None),
+ }
+ }
+
+ fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::DeserializeSeed<'de>,
+ {
+ match self.value.take() {
+ Some((k, v)) => {
+ let span = v.span();
+ seed.deserialize(crate::de::ValueDeserializer::new(v))
+ .map_err(|mut e: Self::Error| {
+ if e.span().is_none() {
+ e.set_span(span);
+ }
+ e.add_key(k.as_str().to_owned());
+ e
+ })
+ }
+ None => {
+ panic!("no more values in next_value_seed, internal error in ValueDeserializer")
+ }
+ }
+ }
+}
+
+impl<'de> serde::de::EnumAccess<'de> for TableMapAccess {
+ type Error = Error;
+ type Variant = super::TableEnumDeserializer;
+
+ fn variant_seed<V>(mut self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error>
+ where
+ V: serde::de::DeserializeSeed<'de>,
+ {
+ let (key, value) = match self.iter.next() {
+ Some(pair) => pair,
+ None => {
+ return Err(Error::custom(
+ "expected table with exactly 1 entry, found empty table",
+ self.span,
+ ));
+ }
+ };
+
+ let val = seed
+ .deserialize(key.into_deserializer())
+ .map_err(|mut e: Self::Error| {
+ if e.span().is_none() {
+ e.set_span(value.key.span());
+ }
+ e
+ })?;
+
+ let variant = super::TableEnumDeserializer::new(value.value);
+
+ Ok((val, variant))
+ }
+}
diff --git a/src/de/table_enum.rs b/src/de/table_enum.rs
new file mode 100644
index 0000000..197ad6e
--- /dev/null
+++ b/src/de/table_enum.rs
@@ -0,0 +1,160 @@
+use crate::de::Error;
+
+/// Deserializes table values into enum variants.
+pub(crate) struct TableEnumDeserializer {
+ value: crate::Item,
+}
+
+impl TableEnumDeserializer {
+ pub(crate) fn new(value: crate::Item) -> Self {
+ TableEnumDeserializer { value }
+ }
+}
+
+impl<'de> serde::de::VariantAccess<'de> for TableEnumDeserializer {
+ type Error = Error;
+
+ fn unit_variant(self) -> Result<(), Self::Error> {
+ match self.value {
+ crate::Item::Table(values) => {
+ if values.is_empty() {
+ Ok(())
+ } else {
+ Err(Error::custom("expected empty table", values.span()))
+ }
+ }
+ crate::Item::Value(crate::Value::InlineTable(values)) => {
+ if values.is_empty() {
+ Ok(())
+ } else {
+ Err(Error::custom("expected empty table", values.span()))
+ }
+ }
+ e => Err(Error::custom(
+ format!("expected table, found {}", e.type_name()),
+ e.span(),
+ )),
+ }
+ }
+
+ fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value, Self::Error>
+ where
+ T: serde::de::DeserializeSeed<'de>,
+ {
+ seed.deserialize(super::ValueDeserializer::new(self.value))
+ }
+
+ fn tuple_variant<V>(self, len: usize, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ match self.value {
+ crate::Item::Table(values) => {
+ let values_span = values.span();
+ let tuple_values = values
+ .items
+ .into_iter()
+ .enumerate()
+ .map(
+ |(index, (_, value))| match value.key.get().parse::<usize>() {
+ Ok(key_index) if key_index == index => Ok(value.value),
+ Ok(_) | Err(_) => Err(Error::custom(
+ format!(
+ "expected table key `{}`, but was `{}`",
+ index,
+ value.key.get()
+ ),
+ value.key.span(),
+ )),
+ },
+ )
+ // Fold all values into a `Vec`, or return the first error.
+ .fold(Ok(Vec::with_capacity(len)), |result, value_result| {
+ result.and_then(move |mut tuple_values| match value_result {
+ Ok(value) => {
+ tuple_values.push(value);
+ Ok(tuple_values)
+ }
+ // `Result<de::Value, Self::Error>` to `Result<Vec<_>, Self::Error>`
+ Err(e) => Err(e),
+ })
+ })?;
+
+ if tuple_values.len() == len {
+ serde::de::Deserializer::deserialize_seq(
+ super::ArrayDeserializer::new(tuple_values, values_span),
+ visitor,
+ )
+ } else {
+ Err(Error::custom(
+ format!("expected tuple with length {}", len),
+ values_span,
+ ))
+ }
+ }
+ crate::Item::Value(crate::Value::InlineTable(values)) => {
+ let values_span = values.span();
+ let tuple_values = values
+ .items
+ .into_iter()
+ .enumerate()
+ .map(
+ |(index, (_, value))| match value.key.get().parse::<usize>() {
+ Ok(key_index) if key_index == index => Ok(value.value),
+ Ok(_) | Err(_) => Err(Error::custom(
+ format!(
+ "expected table key `{}`, but was `{}`",
+ index,
+ value.key.get()
+ ),
+ value.key.span(),
+ )),
+ },
+ )
+ // Fold all values into a `Vec`, or return the first error.
+ .fold(Ok(Vec::with_capacity(len)), |result, value_result| {
+ result.and_then(move |mut tuple_values| match value_result {
+ Ok(value) => {
+ tuple_values.push(value);
+ Ok(tuple_values)
+ }
+ // `Result<de::Value, Self::Error>` to `Result<Vec<_>, Self::Error>`
+ Err(e) => Err(e),
+ })
+ })?;
+
+ if tuple_values.len() == len {
+ serde::de::Deserializer::deserialize_seq(
+ super::ArrayDeserializer::new(tuple_values, values_span),
+ visitor,
+ )
+ } else {
+ Err(Error::custom(
+ format!("expected tuple with length {}", len),
+ values_span,
+ ))
+ }
+ }
+ e => Err(Error::custom(
+ format!("expected table, found {}", e.type_name()),
+ e.span(),
+ )),
+ }
+ }
+
+ fn struct_variant<V>(
+ self,
+ fields: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ serde::de::Deserializer::deserialize_struct(
+ super::ValueDeserializer::new(self.value).with_struct_key_validation(),
+ "", // TODO: this should be the variant name
+ fields,
+ visitor,
+ )
+ }
+}
diff --git a/src/de/value.rs b/src/de/value.rs
new file mode 100644
index 0000000..3984287
--- /dev/null
+++ b/src/de/value.rs
@@ -0,0 +1,252 @@
+use serde::de::IntoDeserializer as _;
+
+use crate::de::DatetimeDeserializer;
+use crate::de::Error;
+
+/// Deserialization implementation for TOML [values][crate::Value].
+///
+/// Can be creater either directly from TOML strings, using [`std::str::FromStr`],
+/// or from parsed [values][crate::Value] using [`serde::de::IntoDeserializer::into_deserializer`].
+///
+/// # Example
+///
+/// ```
+/// use serde::Deserialize;
+///
+/// #[derive(Deserialize)]
+/// struct Config {
+/// title: String,
+/// owner: Owner,
+/// }
+///
+/// #[derive(Deserialize)]
+/// struct Owner {
+/// name: String,
+/// }
+///
+/// let value = r#"{ title = 'TOML Example', owner = { name = 'Lisa' } }"#;
+/// let deserializer = value.parse::<toml_edit::de::ValueDeserializer>().unwrap();
+/// let config = Config::deserialize(deserializer).unwrap();
+/// assert_eq!(config.title, "TOML Example");
+/// assert_eq!(config.owner.name, "Lisa");
+/// ```
+pub struct ValueDeserializer {
+ input: crate::Item,
+ validate_struct_keys: bool,
+}
+
+impl ValueDeserializer {
+ pub(crate) fn new(input: crate::Item) -> Self {
+ Self {
+ input,
+ validate_struct_keys: false,
+ }
+ }
+
+ pub(crate) fn with_struct_key_validation(mut self) -> Self {
+ self.validate_struct_keys = true;
+ self
+ }
+}
+
+// Note: this is wrapped by `toml::de::ValueDeserializer` and any trait methods
+// implemented here need to be wrapped there
+impl<'de> serde::Deserializer<'de> for ValueDeserializer {
+ type Error = Error;
+
+ fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ let span = self.input.span();
+ match self.input {
+ crate::Item::None => visitor.visit_none(),
+ crate::Item::Value(crate::Value::String(v)) => visitor.visit_string(v.into_value()),
+ crate::Item::Value(crate::Value::Integer(v)) => visitor.visit_i64(v.into_value()),
+ crate::Item::Value(crate::Value::Float(v)) => visitor.visit_f64(v.into_value()),
+ crate::Item::Value(crate::Value::Boolean(v)) => visitor.visit_bool(v.into_value()),
+ crate::Item::Value(crate::Value::Datetime(v)) => {
+ visitor.visit_map(DatetimeDeserializer::new(v.into_value()))
+ }
+ crate::Item::Value(crate::Value::Array(v)) => {
+ v.into_deserializer().deserialize_any(visitor)
+ }
+ crate::Item::Value(crate::Value::InlineTable(v)) => {
+ v.into_deserializer().deserialize_any(visitor)
+ }
+ crate::Item::Table(v) => v.into_deserializer().deserialize_any(visitor),
+ crate::Item::ArrayOfTables(v) => v.into_deserializer().deserialize_any(visitor),
+ }
+ .map_err(|mut e: Self::Error| {
+ if e.span().is_none() {
+ e.set_span(span);
+ }
+ e
+ })
+ }
+
+ // `None` is interpreted as a missing field so be sure to implement `Some`
+ // as a present field.
+ fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ let span = self.input.span();
+ visitor.visit_some(self).map_err(|mut e: Self::Error| {
+ if e.span().is_none() {
+ e.set_span(span);
+ }
+ e
+ })
+ }
+
+ fn deserialize_newtype_struct<V>(
+ self,
+ _name: &'static str,
+ visitor: V,
+ ) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ let span = self.input.span();
+ visitor
+ .visit_newtype_struct(self)
+ .map_err(|mut e: Self::Error| {
+ if e.span().is_none() {
+ e.set_span(span);
+ }
+ e
+ })
+ }
+
+ fn deserialize_struct<V>(
+ self,
+ name: &'static str,
+ fields: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ if serde_spanned::__unstable::is_spanned(name, fields) {
+ if let Some(span) = self.input.span() {
+ return visitor.visit_map(super::SpannedDeserializer::new(self, span));
+ }
+ }
+
+ if name == toml_datetime::__unstable::NAME && fields == [toml_datetime::__unstable::FIELD] {
+ let span = self.input.span();
+ if let crate::Item::Value(crate::Value::Datetime(d)) = self.input {
+ return visitor
+ .visit_map(DatetimeDeserializer::new(d.into_value()))
+ .map_err(|mut e: Self::Error| {
+ if e.span().is_none() {
+ e.set_span(span);
+ }
+ e
+ });
+ }
+ }
+
+ if self.validate_struct_keys {
+ let span = self.input.span();
+ match &self.input {
+ crate::Item::Table(values) => super::validate_struct_keys(&values.items, fields),
+ crate::Item::Value(crate::Value::InlineTable(values)) => {
+ super::validate_struct_keys(&values.items, fields)
+ }
+ _ => Ok(()),
+ }
+ .map_err(|mut e: Self::Error| {
+ if e.span().is_none() {
+ e.set_span(span);
+ }
+ e
+ })?
+ }
+
+ self.deserialize_any(visitor)
+ }
+
+ // Called when the type to deserialize is an enum, as opposed to a field in the type.
+ fn deserialize_enum<V>(
+ self,
+ name: &'static str,
+ variants: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value, Error>
+ where
+ V: serde::de::Visitor<'de>,
+ {
+ let span = self.input.span();
+ match self.input {
+ crate::Item::Value(crate::Value::String(v)) => {
+ visitor.visit_enum(v.into_value().into_deserializer())
+ }
+ crate::Item::Value(crate::Value::InlineTable(v)) => {
+ if v.is_empty() {
+ Err(crate::de::Error::custom(
+ "wanted exactly 1 element, found 0 elements",
+ v.span(),
+ ))
+ } else if v.len() != 1 {
+ Err(crate::de::Error::custom(
+ "wanted exactly 1 element, more than 1 element",
+ v.span(),
+ ))
+ } else {
+ v.into_deserializer()
+ .deserialize_enum(name, variants, visitor)
+ }
+ }
+ crate::Item::Table(v) => v
+ .into_deserializer()
+ .deserialize_enum(name, variants, visitor),
+ e => Err(crate::de::Error::custom("wanted string or table", e.span())),
+ }
+ .map_err(|mut e: Self::Error| {
+ if e.span().is_none() {
+ e.set_span(span);
+ }
+ e
+ })
+ }
+
+ serde::forward_to_deserialize_any! {
+ bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq
+ bytes byte_buf map unit
+ ignored_any unit_struct tuple_struct tuple identifier
+ }
+}
+
+impl<'de> serde::de::IntoDeserializer<'de, crate::de::Error> for ValueDeserializer {
+ type Deserializer = Self;
+
+ fn into_deserializer(self) -> Self::Deserializer {
+ self
+ }
+}
+
+impl<'de> serde::de::IntoDeserializer<'de, crate::de::Error> for crate::Value {
+ type Deserializer = ValueDeserializer;
+
+ fn into_deserializer(self) -> Self::Deserializer {
+ ValueDeserializer::new(crate::Item::Value(self))
+ }
+}
+
+impl crate::Item {
+ pub(crate) fn into_deserializer(self) -> ValueDeserializer {
+ ValueDeserializer::new(self)
+ }
+}
+
+impl std::str::FromStr for ValueDeserializer {
+ type Err = Error;
+
+ /// Parses a value from a &str
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let v = crate::parser::parse_value(s).map_err(Error::from)?;
+ Ok(v.into_deserializer())
+ }
+}
diff --git a/src/document.rs b/src/document.rs
new file mode 100644
index 0000000..67dd293
--- /dev/null
+++ b/src/document.rs
@@ -0,0 +1,113 @@
+use std::str::FromStr;
+
+use crate::parser;
+use crate::table::Iter;
+use crate::{Item, RawString, Table};
+
+/// Type representing a TOML document
+#[derive(Debug, Clone)]
+pub struct Document {
+ pub(crate) root: Item,
+ // Trailing comments and whitespaces
+ pub(crate) trailing: RawString,
+ pub(crate) original: Option<String>,
+ pub(crate) span: Option<std::ops::Range<usize>>,
+}
+
+impl Document {
+ /// Creates an empty document
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ /// Returns a reference to the root item.
+ pub fn as_item(&self) -> &Item {
+ &self.root
+ }
+
+ /// Returns a mutable reference to the root item.
+ pub fn as_item_mut(&mut self) -> &mut Item {
+ &mut self.root
+ }
+
+ /// Returns a reference to the root table.
+ pub fn as_table(&self) -> &Table {
+ self.root.as_table().expect("root should always be a table")
+ }
+
+ /// Returns a mutable reference to the root table.
+ pub fn as_table_mut(&mut self) -> &mut Table {
+ self.root
+ .as_table_mut()
+ .expect("root should always be a table")
+ }
+
+ /// Returns an iterator over the root table.
+ pub fn iter(&self) -> Iter<'_> {
+ self.as_table().iter()
+ }
+
+ /// Set whitespace after last element
+ pub fn set_trailing(&mut self, trailing: impl Into<RawString>) {
+ self.trailing = trailing.into();
+ }
+
+ /// Whitespace after last element
+ pub fn trailing(&self) -> &RawString {
+ &self.trailing
+ }
+
+ /// # Panics
+ ///
+ /// If run on on a `Document` not generated by the parser
+ pub(crate) fn despan(&mut self) {
+ self.span = None;
+ self.root.despan(self.original.as_deref().unwrap());
+ self.trailing.despan(self.original.as_deref().unwrap());
+ }
+}
+
+impl Default for Document {
+ fn default() -> Self {
+ Self {
+ root: Item::Table(Table::with_pos(Some(0))),
+ trailing: Default::default(),
+ original: Default::default(),
+ span: Default::default(),
+ }
+ }
+}
+
+impl FromStr for Document {
+ type Err = crate::TomlError;
+
+ /// Parses a document from a &str
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut d = parser::parse_document(s)?;
+ d.despan();
+ Ok(d)
+ }
+}
+
+impl std::ops::Deref for Document {
+ type Target = Table;
+
+ fn deref(&self) -> &Self::Target {
+ self.as_table()
+ }
+}
+
+impl std::ops::DerefMut for Document {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.as_table_mut()
+ }
+}
+
+impl From<Table> for Document {
+ fn from(root: Table) -> Self {
+ Self {
+ root: Item::Table(root),
+ ..Default::default()
+ }
+ }
+}
diff --git a/src/encode.rs b/src/encode.rs
new file mode 100644
index 0000000..9940f28
--- /dev/null
+++ b/src/encode.rs
@@ -0,0 +1,569 @@
+use std::borrow::Cow;
+use std::fmt::{Display, Formatter, Result, Write};
+
+use toml_datetime::*;
+
+use crate::document::Document;
+use crate::inline_table::DEFAULT_INLINE_KEY_DECOR;
+use crate::key::Key;
+use crate::repr::{Formatted, Repr, ValueRepr};
+use crate::table::{DEFAULT_KEY_DECOR, DEFAULT_KEY_PATH_DECOR, DEFAULT_TABLE_DECOR};
+use crate::value::{
+ DEFAULT_LEADING_VALUE_DECOR, DEFAULT_TRAILING_VALUE_DECOR, DEFAULT_VALUE_DECOR,
+};
+use crate::{Array, InlineTable, Item, Table, Value};
+
+pub(crate) trait Encode {
+ fn encode(
+ &self,
+ buf: &mut dyn Write,
+ input: Option<&str>,
+ default_decor: (&str, &str),
+ ) -> Result;
+}
+
+impl Encode for Key {
+ fn encode(
+ &self,
+ buf: &mut dyn Write,
+ input: Option<&str>,
+ default_decor: (&str, &str),
+ ) -> Result {
+ let decor = self.decor();
+ decor.prefix_encode(buf, input, default_decor.0)?;
+
+ if let Some(input) = input {
+ let repr = self
+ .as_repr()
+ .map(Cow::Borrowed)
+ .unwrap_or_else(|| Cow::Owned(self.default_repr()));
+ repr.encode(buf, input)?;
+ } else {
+ let repr = self.display_repr();
+ write!(buf, "{}", repr)?;
+ };
+
+ decor.suffix_encode(buf, input, default_decor.1)?;
+ Ok(())
+ }
+}
+
+impl<'k> Encode for &'k [Key] {
+ fn encode(
+ &self,
+ buf: &mut dyn Write,
+ input: Option<&str>,
+ default_decor: (&str, &str),
+ ) -> Result {
+ for (i, key) in self.iter().enumerate() {
+ let first = i == 0;
+ let last = i + 1 == self.len();
+
+ let prefix = if first {
+ default_decor.0
+ } else {
+ DEFAULT_KEY_PATH_DECOR.0
+ };
+ let suffix = if last {
+ default_decor.1
+ } else {
+ DEFAULT_KEY_PATH_DECOR.1
+ };
+
+ if !first {
+ write!(buf, ".")?;
+ }
+ key.encode(buf, input, (prefix, suffix))?;
+ }
+ Ok(())
+ }
+}
+
+impl<'k> Encode for &'k [&'k Key] {
+ fn encode(
+ &self,
+ buf: &mut dyn Write,
+ input: Option<&str>,
+ default_decor: (&str, &str),
+ ) -> Result {
+ for (i, key) in self.iter().enumerate() {
+ let first = i == 0;
+ let last = i + 1 == self.len();
+
+ let prefix = if first {
+ default_decor.0
+ } else {
+ DEFAULT_KEY_PATH_DECOR.0
+ };
+ let suffix = if last {
+ default_decor.1
+ } else {
+ DEFAULT_KEY_PATH_DECOR.1
+ };
+
+ if !first {
+ write!(buf, ".")?;
+ }
+ key.encode(buf, input, (prefix, suffix))?;
+ }
+ Ok(())
+ }
+}
+
+impl<T> Encode for Formatted<T>
+where
+ T: ValueRepr,
+{
+ fn encode(
+ &self,
+ buf: &mut dyn Write,
+ input: Option<&str>,
+ default_decor: (&str, &str),
+ ) -> Result {
+ let decor = self.decor();
+ decor.prefix_encode(buf, input, default_decor.0)?;
+
+ if let Some(input) = input {
+ let repr = self
+ .as_repr()
+ .map(Cow::Borrowed)
+ .unwrap_or_else(|| Cow::Owned(self.default_repr()));
+ repr.encode(buf, input)?;
+ } else {
+ let repr = self.display_repr();
+ write!(buf, "{}", repr)?;
+ };
+
+ decor.suffix_encode(buf, input, default_decor.1)?;
+ Ok(())
+ }
+}
+
+impl Encode for Array {
+ fn encode(
+ &self,
+ buf: &mut dyn Write,
+ input: Option<&str>,
+ default_decor: (&str, &str),
+ ) -> Result {
+ let decor = self.decor();
+ decor.prefix_encode(buf, input, default_decor.0)?;
+ write!(buf, "[")?;
+
+ for (i, elem) in self.iter().enumerate() {
+ let inner_decor;
+ if i == 0 {
+ inner_decor = DEFAULT_LEADING_VALUE_DECOR;
+ } else {
+ inner_decor = DEFAULT_VALUE_DECOR;
+ write!(buf, ",")?;
+ }
+ elem.encode(buf, input, inner_decor)?;
+ }
+ if self.trailing_comma() && !self.is_empty() {
+ write!(buf, ",")?;
+ }
+
+ self.trailing().encode_with_default(buf, input, "")?;
+ write!(buf, "]")?;
+ decor.suffix_encode(buf, input, default_decor.1)?;
+
+ Ok(())
+ }
+}
+
+impl Encode for InlineTable {
+ fn encode(
+ &self,
+ buf: &mut dyn Write,
+ input: Option<&str>,
+ default_decor: (&str, &str),
+ ) -> Result {
+ let decor = self.decor();
+ decor.prefix_encode(buf, input, default_decor.0)?;
+ write!(buf, "{{")?;
+ self.preamble().encode_with_default(buf, input, "")?;
+
+ let children = self.get_values();
+ let len = children.len();
+ for (i, (key_path, value)) in children.into_iter().enumerate() {
+ if i != 0 {
+ write!(buf, ",")?;
+ }
+ let inner_decor = if i == len - 1 {
+ DEFAULT_TRAILING_VALUE_DECOR
+ } else {
+ DEFAULT_VALUE_DECOR
+ };
+ key_path
+ .as_slice()
+ .encode(buf, input, DEFAULT_INLINE_KEY_DECOR)?;
+ write!(buf, "=")?;
+ value.encode(buf, input, inner_decor)?;
+ }
+
+ write!(buf, "}}")?;
+ decor.suffix_encode(buf, input, default_decor.1)?;
+
+ Ok(())
+ }
+}
+
+impl Encode for Value {
+ fn encode(
+ &self,
+ buf: &mut dyn Write,
+ input: Option<&str>,
+ default_decor: (&str, &str),
+ ) -> Result {
+ match self {
+ Value::String(repr) => repr.encode(buf, input, default_decor),
+ Value::Integer(repr) => repr.encode(buf, input, default_decor),
+ Value::Float(repr) => repr.encode(buf, input, default_decor),
+ Value::Boolean(repr) => repr.encode(buf, input, default_decor),
+ Value::Datetime(repr) => repr.encode(buf, input, default_decor),
+ Value::Array(array) => array.encode(buf, input, default_decor),
+ Value::InlineTable(table) => table.encode(buf, input, default_decor),
+ }
+ }
+}
+
+impl Display for Document {
+ fn fmt(&self, f: &mut Formatter<'_>) -> Result {
+ let mut path = Vec::new();
+ let mut last_position = 0;
+ let mut tables = Vec::new();
+ visit_nested_tables(self.as_table(), &mut path, false, &mut |t, p, is_array| {
+ if let Some(pos) = t.position() {
+ last_position = pos;
+ }
+ tables.push((last_position, t, p.clone(), is_array));
+ Ok(())
+ })
+ .unwrap();
+
+ tables.sort_by_key(|&(id, _, _, _)| id);
+ let mut first_table = true;
+ for (_, table, path, is_array) in tables {
+ visit_table(
+ f,
+ self.original.as_deref(),
+ table,
+ &path,
+ is_array,
+ &mut first_table,
+ )?;
+ }
+ self.trailing()
+ .encode_with_default(f, self.original.as_deref(), "")
+ }
+}
+
+fn visit_nested_tables<'t, F>(
+ table: &'t Table,
+ path: &mut Vec<Key>,
+ is_array_of_tables: bool,
+ callback: &mut F,
+) -> Result
+where
+ F: FnMut(&'t Table, &Vec<Key>, bool) -> Result,
+{
+ if !table.is_dotted() {
+ callback(table, path, is_array_of_tables)?;
+ }
+
+ for kv in table.items.values() {
+ match kv.value {
+ Item::Table(ref t) => {
+ let mut key = kv.key.clone();
+ if t.is_dotted() {
+ // May have newlines and generally isn't written for standard tables
+ key.decor_mut().clear();
+ }
+ path.push(key);
+ visit_nested_tables(t, path, false, callback)?;
+ path.pop();
+ }
+ Item::ArrayOfTables(ref a) => {
+ for t in a.iter() {
+ let key = kv.key.clone();
+ path.push(key);
+ visit_nested_tables(t, path, true, callback)?;
+ path.pop();
+ }
+ }
+ _ => {}
+ }
+ }
+ Ok(())
+}
+
+fn visit_table(
+ buf: &mut dyn Write,
+ input: Option<&str>,
+ table: &Table,
+ path: &[Key],
+ is_array_of_tables: bool,
+ first_table: &mut bool,
+) -> Result {
+ let children = table.get_values();
+ // We are intentionally hiding implicit tables without any tables nested under them (ie
+ // `table.is_empty()` which is in contrast to `table.get_values().is_empty()`). We are
+ // trusting the user that an empty implicit table is not semantically meaningful
+ //
+ // This allows a user to delete all tables under this implicit table and the implicit table
+ // will disappear.
+ //
+ // However, this means that users need to take care in deciding what tables get marked as
+ // implicit.
+ let is_visible_std_table = !(table.implicit && children.is_empty());
+
+ if path.is_empty() {
+ // don't print header for the root node
+ if !children.is_empty() {
+ *first_table = false;
+ }
+ } else if is_array_of_tables {
+ let default_decor = if *first_table {
+ *first_table = false;
+ ("", DEFAULT_TABLE_DECOR.1)
+ } else {
+ DEFAULT_TABLE_DECOR
+ };
+ table.decor.prefix_encode(buf, input, default_decor.0)?;
+ write!(buf, "[[")?;
+ path.encode(buf, input, DEFAULT_KEY_PATH_DECOR)?;
+ write!(buf, "]]")?;
+ table.decor.suffix_encode(buf, input, default_decor.1)?;
+ writeln!(buf)?;
+ } else if is_visible_std_table {
+ let default_decor = if *first_table {
+ *first_table = false;
+ ("", DEFAULT_TABLE_DECOR.1)
+ } else {
+ DEFAULT_TABLE_DECOR
+ };
+ table.decor.prefix_encode(buf, input, default_decor.0)?;
+ write!(buf, "[")?;
+ path.encode(buf, input, DEFAULT_KEY_PATH_DECOR)?;
+ write!(buf, "]")?;
+ table.decor.suffix_encode(buf, input, default_decor.1)?;
+ writeln!(buf)?;
+ }
+ // print table body
+ for (key_path, value) in children {
+ key_path.as_slice().encode(buf, input, DEFAULT_KEY_DECOR)?;
+ write!(buf, "=")?;
+ value.encode(buf, input, DEFAULT_VALUE_DECOR)?;
+ writeln!(buf)?;
+ }
+ Ok(())
+}
+
+impl ValueRepr for String {
+ fn to_repr(&self) -> Repr {
+ to_string_repr(self, None, None)
+ }
+}
+
+pub(crate) fn to_string_repr(
+ value: &str,
+ style: Option<StringStyle>,
+ literal: Option<bool>,
+) -> Repr {
+ let (style, literal) = match (style, literal) {
+ (Some(style), Some(literal)) => (style, literal),
+ (_, Some(literal)) => (infer_style(value).0, literal),
+ (Some(style), _) => (style, infer_style(value).1),
+ (_, _) => infer_style(value),
+ };
+
+ let mut output = String::with_capacity(value.len() * 2);
+ if literal {
+ output.push_str(style.literal_start());
+ output.push_str(value);
+ output.push_str(style.literal_end());
+ } else {
+ output.push_str(style.standard_start());
+ for ch in value.chars() {
+ match ch {
+ '\u{8}' => output.push_str("\\b"),
+ '\u{9}' => output.push_str("\\t"),
+ '\u{a}' => match style {
+ StringStyle::NewlineTripple => output.push('\n'),
+ StringStyle::OnelineSingle => output.push_str("\\n"),
+ _ => unreachable!(),
+ },
+ '\u{c}' => output.push_str("\\f"),
+ '\u{d}' => output.push_str("\\r"),
+ '\u{22}' => output.push_str("\\\""),
+ '\u{5c}' => output.push_str("\\\\"),
+ c if c <= '\u{1f}' || c == '\u{7f}' => {
+ write!(output, "\\u{:04X}", ch as u32).unwrap();
+ }
+ ch => output.push(ch),
+ }
+ }
+ output.push_str(style.standard_end());
+ }
+
+ Repr::new_unchecked(output)
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub(crate) enum StringStyle {
+ NewlineTripple,
+ OnelineTripple,
+ OnelineSingle,
+}
+
+impl StringStyle {
+ fn literal_start(self) -> &'static str {
+ match self {
+ Self::NewlineTripple => "'''\n",
+ Self::OnelineTripple => "'''",
+ Self::OnelineSingle => "'",
+ }
+ }
+ fn literal_end(self) -> &'static str {
+ match self {
+ Self::NewlineTripple => "'''",
+ Self::OnelineTripple => "'''",
+ Self::OnelineSingle => "'",
+ }
+ }
+
+ fn standard_start(self) -> &'static str {
+ match self {
+ Self::NewlineTripple => "\"\"\"\n",
+ // note: OnelineTripple can happen if do_pretty wants to do
+ // '''it's one line'''
+ // but literal == false
+ Self::OnelineTripple | Self::OnelineSingle => "\"",
+ }
+ }
+
+ fn standard_end(self) -> &'static str {
+ match self {
+ Self::NewlineTripple => "\"\"\"",
+ // note: OnelineTripple can happen if do_pretty wants to do
+ // '''it's one line'''
+ // but literal == false
+ Self::OnelineTripple | Self::OnelineSingle => "\"",
+ }
+ }
+}
+
+fn infer_style(value: &str) -> (StringStyle, bool) {
+ // For doing pretty prints we store in a new String
+ // because there are too many cases where pretty cannot
+ // work. We need to determine:
+ // - if we are a "multi-line" pretty (if there are \n)
+ // - if ['''] appears if multi or ['] if single
+ // - if there are any invalid control characters
+ //
+ // Doing it any other way would require multiple passes
+ // to determine if a pretty string works or not.
+ let mut out = String::with_capacity(value.len() * 2);
+ let mut ty = StringStyle::OnelineSingle;
+ // found consecutive single quotes
+ let mut max_found_singles = 0;
+ let mut found_singles = 0;
+ let mut prefer_literal = false;
+ let mut can_be_pretty = true;
+
+ for ch in value.chars() {
+ if can_be_pretty {
+ if ch == '\'' {
+ found_singles += 1;
+ if found_singles >= 3 {
+ can_be_pretty = false;
+ }
+ } else {
+ if found_singles > max_found_singles {
+ max_found_singles = found_singles;
+ }
+ found_singles = 0
+ }
+ match ch {
+ '\t' => {}
+ '\\' => {
+ prefer_literal = true;
+ }
+ '\n' => ty = StringStyle::NewlineTripple,
+ // Escape codes are needed if any ascii control
+ // characters are present, including \b \f \r.
+ c if c <= '\u{1f}' || c == '\u{7f}' => can_be_pretty = false,
+ _ => {}
+ }
+ out.push(ch);
+ } else {
+ // the string cannot be represented as pretty,
+ // still check if it should be multiline
+ if ch == '\n' {
+ ty = StringStyle::NewlineTripple;
+ }
+ }
+ }
+ if found_singles > 0 && value.ends_with('\'') {
+ // We cannot escape the ending quote so we must use """
+ can_be_pretty = false;
+ }
+ if !prefer_literal {
+ can_be_pretty = false;
+ }
+ if !can_be_pretty {
+ debug_assert!(ty != StringStyle::OnelineTripple);
+ return (ty, false);
+ }
+ if found_singles > max_found_singles {
+ max_found_singles = found_singles;
+ }
+ debug_assert!(max_found_singles < 3);
+ if ty == StringStyle::OnelineSingle && max_found_singles >= 1 {
+ // no newlines, but must use ''' because it has ' in it
+ ty = StringStyle::OnelineTripple;
+ }
+ (ty, true)
+}
+
+impl ValueRepr for i64 {
+ fn to_repr(&self) -> Repr {
+ Repr::new_unchecked(self.to_string())
+ }
+}
+
+impl ValueRepr for f64 {
+ fn to_repr(&self) -> Repr {
+ to_f64_repr(*self)
+ }
+}
+
+fn to_f64_repr(f: f64) -> Repr {
+ let repr = match (f.is_sign_negative(), f.is_nan(), f == 0.0) {
+ (true, true, _) => "-nan".to_owned(),
+ (false, true, _) => "nan".to_owned(),
+ (true, false, true) => "-0.0".to_owned(),
+ (false, false, true) => "0.0".to_owned(),
+ (_, false, false) => {
+ if f % 1.0 == 0.0 {
+ format!("{}.0", f)
+ } else {
+ format!("{}", f)
+ }
+ }
+ };
+ Repr::new_unchecked(repr)
+}
+
+impl ValueRepr for bool {
+ fn to_repr(&self) -> Repr {
+ Repr::new_unchecked(self.to_string())
+ }
+}
+
+impl ValueRepr for Datetime {
+ fn to_repr(&self) -> Repr {
+ Repr::new_unchecked(self.to_string())
+ }
+}
diff --git a/src/index.rs b/src/index.rs
new file mode 100644
index 0000000..276db79
--- /dev/null
+++ b/src/index.rs
@@ -0,0 +1,156 @@
+use std::ops;
+
+use crate::document::Document;
+use crate::key::Key;
+use crate::table::TableKeyValue;
+use crate::{value, InlineTable, InternalString, Item, Table, Value};
+
+// copied from
+// https://github.com/serde-rs/json/blob/master/src/value/index.rs
+
+pub trait Index: crate::private::Sealed {
+ #[doc(hidden)]
+ fn index<'v>(&self, val: &'v Item) -> Option<&'v Item>;
+ #[doc(hidden)]
+ fn index_mut<'v>(&self, val: &'v mut Item) -> Option<&'v mut Item>;
+}
+
+impl Index for usize {
+ fn index<'v>(&self, v: &'v Item) -> Option<&'v Item> {
+ match *v {
+ Item::ArrayOfTables(ref aot) => aot.values.get(*self),
+ Item::Value(ref a) if a.is_array() => a.as_array().and_then(|a| a.values.get(*self)),
+ _ => None,
+ }
+ }
+ fn index_mut<'v>(&self, v: &'v mut Item) -> Option<&'v mut Item> {
+ match *v {
+ Item::ArrayOfTables(ref mut vec) => vec.values.get_mut(*self),
+ Item::Value(ref mut a) => a.as_array_mut().and_then(|a| a.values.get_mut(*self)),
+ _ => None,
+ }
+ }
+}
+
+impl Index for str {
+ fn index<'v>(&self, v: &'v Item) -> Option<&'v Item> {
+ match *v {
+ Item::Table(ref t) => t.get(self),
+ Item::Value(ref v) => v
+ .as_inline_table()
+ .and_then(|t| t.items.get(self))
+ .and_then(|kv| {
+ if !kv.value.is_none() {
+ Some(&kv.value)
+ } else {
+ None
+ }
+ }),
+ _ => None,
+ }
+ }
+ fn index_mut<'v>(&self, v: &'v mut Item) -> Option<&'v mut Item> {
+ if let Item::None = *v {
+ let mut t = InlineTable::default();
+ t.items.insert(
+ InternalString::from(self),
+ TableKeyValue::new(Key::new(self), Item::None),
+ );
+ *v = value(Value::InlineTable(t));
+ }
+ match *v {
+ Item::Table(ref mut t) => Some(t.entry(self).or_insert(Item::None)),
+ Item::Value(ref mut v) => v.as_inline_table_mut().map(|t| {
+ &mut t
+ .items
+ .entry(InternalString::from(self))
+ .or_insert_with(|| TableKeyValue::new(Key::new(self), Item::None))
+ .value
+ }),
+ _ => None,
+ }
+ }
+}
+
+impl Index for String {
+ fn index<'v>(&self, v: &'v Item) -> Option<&'v Item> {
+ self[..].index(v)
+ }
+ fn index_mut<'v>(&self, v: &'v mut Item) -> Option<&'v mut Item> {
+ self[..].index_mut(v)
+ }
+}
+
+impl<'a, T: ?Sized> Index for &'a T
+where
+ T: Index,
+{
+ fn index<'v>(&self, v: &'v Item) -> Option<&'v Item> {
+ (**self).index(v)
+ }
+ fn index_mut<'v>(&self, v: &'v mut Item) -> Option<&'v mut Item> {
+ (**self).index_mut(v)
+ }
+}
+
+impl<I> ops::Index<I> for Item
+where
+ I: Index,
+{
+ type Output = Item;
+
+ fn index(&self, index: I) -> &Item {
+ index.index(self).expect("index not found")
+ }
+}
+
+impl<I> ops::IndexMut<I> for Item
+where
+ I: Index,
+{
+ fn index_mut(&mut self, index: I) -> &mut Item {
+ index.index_mut(self).expect("index not found")
+ }
+}
+
+impl<'s> ops::Index<&'s str> for Table {
+ type Output = Item;
+
+ fn index(&self, key: &'s str) -> &Item {
+ self.get(key).expect("index not found")
+ }
+}
+
+impl<'s> ops::IndexMut<&'s str> for Table {
+ fn index_mut(&mut self, key: &'s str) -> &mut Item {
+ self.entry(key).or_insert(Item::None)
+ }
+}
+
+impl<'s> ops::Index<&'s str> for InlineTable {
+ type Output = Value;
+
+ fn index(&self, key: &'s str) -> &Value {
+ self.get(key).expect("index not found")
+ }
+}
+
+impl<'s> ops::IndexMut<&'s str> for InlineTable {
+ fn index_mut(&mut self, key: &'s str) -> &mut Value {
+ self.get_mut(key).expect("index not found")
+ }
+}
+
+impl<'s> ops::Index<&'s str> for Document {
+ type Output = Item;
+
+ fn index(&self, key: &'s str) -> &Item {
+ self.root.index(key)
+ }
+}
+
+impl<'s> ops::IndexMut<&'s str> for Document {
+ fn index_mut(&mut self, key: &'s str) -> &mut Item {
+ self.root.index_mut(key)
+ }
+}
diff --git a/src/inline_table.rs b/src/inline_table.rs
new file mode 100644
index 0000000..3dc6c0c
--- /dev/null
+++ b/src/inline_table.rs
@@ -0,0 +1,679 @@
+use std::iter::FromIterator;
+
+use crate::key::Key;
+use crate::repr::Decor;
+use crate::table::{Iter, IterMut, KeyValuePairs, TableKeyValue, TableLike};
+use crate::{InternalString, Item, KeyMut, RawString, Table, Value};
+
+/// Type representing a TOML inline table,
+/// payload of the `Value::InlineTable` variant
+#[derive(Debug, Default, Clone)]
+pub struct InlineTable {
+ // `preamble` represents whitespaces in an empty table
+ preamble: RawString,
+ // prefix before `{` and suffix after `}`
+ decor: Decor,
+ pub(crate) span: Option<std::ops::Range<usize>>,
+ // whether this is a proxy for dotted keys
+ dotted: bool,
+ pub(crate) items: KeyValuePairs,
+}
+
+/// Constructors
+///
+/// See also `FromIterator`
+impl InlineTable {
+ /// Creates an empty table.
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ pub(crate) fn with_pairs(items: KeyValuePairs) -> Self {
+ Self {
+ items,
+ ..Default::default()
+ }
+ }
+
+ /// Convert to a table
+ pub fn into_table(self) -> Table {
+ let mut t = Table::with_pairs(self.items);
+ t.fmt();
+ t
+ }
+}
+
+/// Formatting
+impl InlineTable {
+ /// Get key/values for values that are visually children of this table
+ ///
+ /// For example, this will return dotted keys
+ pub fn get_values(&self) -> Vec<(Vec<&Key>, &Value)> {
+ let mut values = Vec::new();
+ let root = Vec::new();
+ self.append_values(&root, &mut values);
+ values
+ }
+
+ pub(crate) fn append_values<'s, 'c>(
+ &'s self,
+ parent: &[&'s Key],
+ values: &'c mut Vec<(Vec<&'s Key>, &'s Value)>,
+ ) {
+ for value in self.items.values() {
+ let mut path = parent.to_vec();
+ path.push(&value.key);
+ match &value.value {
+ Item::Value(Value::InlineTable(table)) if table.is_dotted() => {
+ table.append_values(&path, values);
+ }
+ Item::Value(value) => {
+ values.push((path, value));
+ }
+ _ => {}
+ }
+ }
+ }
+
+ /// Auto formats the table.
+ pub fn fmt(&mut self) {
+ decorate_inline_table(self);
+ }
+
+ /// Sorts the key/value pairs by key.
+ pub fn sort_values(&mut self) {
+ // Assuming standard tables have their position set and this won't negatively impact them
+ self.items.sort_keys();
+ for kv in self.items.values_mut() {
+ match &mut kv.value {
+ Item::Value(Value::InlineTable(table)) if table.is_dotted() => {
+ table.sort_values();
+ }
+ _ => {}
+ }
+ }
+ }
+
+ /// Sort Key/Value Pairs of the table using the using the comparison function `compare`.
+ ///
+ /// The comparison function receives two key and value pairs to compare (you can sort by keys or
+ /// values or their combination as needed).
+ pub fn sort_values_by<F>(&mut self, mut compare: F)
+ where
+ F: FnMut(&Key, &Value, &Key, &Value) -> std::cmp::Ordering,
+ {
+ self.sort_values_by_internal(&mut compare);
+ }
+
+ fn sort_values_by_internal<F>(&mut self, compare: &mut F)
+ where
+ F: FnMut(&Key, &Value, &Key, &Value) -> std::cmp::Ordering,
+ {
+ let modified_cmp = |_: &InternalString,
+ val1: &TableKeyValue,
+ _: &InternalString,
+ val2: &TableKeyValue|
+ -> std::cmp::Ordering {
+ match (val1.value.as_value(), val2.value.as_value()) {
+ (Some(v1), Some(v2)) => compare(&val1.key, v1, &val2.key, v2),
+ (Some(_), None) => std::cmp::Ordering::Greater,
+ (None, Some(_)) => std::cmp::Ordering::Less,
+ (None, None) => std::cmp::Ordering::Equal,
+ }
+ };
+
+ self.items.sort_by(modified_cmp);
+ for kv in self.items.values_mut() {
+ match &mut kv.value {
+ Item::Value(Value::InlineTable(table)) if table.is_dotted() => {
+ table.sort_values_by_internal(compare);
+ }
+ _ => {}
+ }
+ }
+ }
+
+ /// Change this table's dotted status
+ pub fn set_dotted(&mut self, yes: bool) {
+ self.dotted = yes;
+ }
+
+ /// Check if this is a wrapper for dotted keys, rather than a standard table
+ pub fn is_dotted(&self) -> bool {
+ self.dotted
+ }
+
+ /// Returns the surrounding whitespace
+ pub fn decor_mut(&mut self) -> &mut Decor {
+ &mut self.decor
+ }
+
+ /// Returns the surrounding whitespace
+ pub fn decor(&self) -> &Decor {
+ &self.decor
+ }
+
+ /// Returns the decor associated with a given key of the table.
+ pub fn key_decor_mut(&mut self, key: &str) -> Option<&mut Decor> {
+ self.items.get_mut(key).map(|kv| &mut kv.key.decor)
+ }
+
+ /// Returns the decor associated with a given key of the table.
+ pub fn key_decor(&self, key: &str) -> Option<&Decor> {
+ self.items.get(key).map(|kv| &kv.key.decor)
+ }
+
+ /// Set whitespace after before element
+ pub fn set_preamble(&mut self, preamble: impl Into<RawString>) {
+ self.preamble = preamble.into();
+ }
+
+ /// Whitespace after before element
+ pub fn preamble(&self) -> &RawString {
+ &self.preamble
+ }
+
+ /// Returns the location within the original document
+ pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+ self.span.clone()
+ }
+
+ pub(crate) fn despan(&mut self, input: &str) {
+ self.span = None;
+ self.decor.despan(input);
+ self.preamble.despan(input);
+ for kv in self.items.values_mut() {
+ kv.key.despan(input);
+ kv.value.despan(input);
+ }
+ }
+}
+
+impl InlineTable {
+ /// Returns an iterator over key/value pairs.
+ pub fn iter(&self) -> InlineTableIter<'_> {
+ Box::new(
+ self.items
+ .iter()
+ .filter(|&(_, kv)| kv.value.is_value())
+ .map(|(k, kv)| (&k[..], kv.value.as_value().unwrap())),
+ )
+ }
+
+ /// Returns an iterator over key/value pairs.
+ pub fn iter_mut(&mut self) -> InlineTableIterMut<'_> {
+ Box::new(
+ self.items
+ .iter_mut()
+ .filter(|(_, kv)| kv.value.is_value())
+ .map(|(_, kv)| (kv.key.as_mut(), kv.value.as_value_mut().unwrap())),
+ )
+ }
+
+ /// Returns the number of key/value pairs.
+ pub fn len(&self) -> usize {
+ self.iter().count()
+ }
+
+ /// Returns true iff the table is empty.
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// Clears the table, removing all key-value pairs. Keeps the allocated memory for reuse.
+ pub fn clear(&mut self) {
+ self.items.clear()
+ }
+
+ /// Gets the given key's corresponding entry in the Table for in-place manipulation.
+ pub fn entry(&'_ mut self, key: impl Into<InternalString>) -> InlineEntry<'_> {
+ match self.items.entry(key.into()) {
+ indexmap::map::Entry::Occupied(mut entry) => {
+ // Ensure it is a `Value` to simplify `InlineOccupiedEntry`'s code.
+ let scratch = std::mem::take(&mut entry.get_mut().value);
+ let scratch = Item::Value(
+ scratch
+ .into_value()
+ // HACK: `Item::None` is a corner case of a corner case, let's just pick a
+ // "safe" value
+ .unwrap_or_else(|_| Value::InlineTable(Default::default())),
+ );
+ entry.get_mut().value = scratch;
+
+ InlineEntry::Occupied(InlineOccupiedEntry { entry })
+ }
+ indexmap::map::Entry::Vacant(entry) => {
+ InlineEntry::Vacant(InlineVacantEntry { entry, key: None })
+ }
+ }
+ }
+
+ /// Gets the given key's corresponding entry in the Table for in-place manipulation.
+ pub fn entry_format<'a>(&'a mut self, key: &Key) -> InlineEntry<'a> {
+ // Accept a `&Key` to be consistent with `entry`
+ match self.items.entry(key.get().into()) {
+ indexmap::map::Entry::Occupied(mut entry) => {
+ // Ensure it is a `Value` to simplify `InlineOccupiedEntry`'s code.
+ let scratch = std::mem::take(&mut entry.get_mut().value);
+ let scratch = Item::Value(
+ scratch
+ .into_value()
+ // HACK: `Item::None` is a corner case of a corner case, let's just pick a
+ // "safe" value
+ .unwrap_or_else(|_| Value::InlineTable(Default::default())),
+ );
+ entry.get_mut().value = scratch;
+
+ InlineEntry::Occupied(InlineOccupiedEntry { entry })
+ }
+ indexmap::map::Entry::Vacant(entry) => InlineEntry::Vacant(InlineVacantEntry {
+ entry,
+ key: Some(key.clone()),
+ }),
+ }
+ }
+ /// Return an optional reference to the value at the given the key.
+ pub fn get(&self, key: &str) -> Option<&Value> {
+ self.items.get(key).and_then(|kv| kv.value.as_value())
+ }
+
+ /// Return an optional mutable reference to the value at the given the key.
+ pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
+ self.items
+ .get_mut(key)
+ .and_then(|kv| kv.value.as_value_mut())
+ }
+
+ /// Return references to the key-value pair stored for key, if it is present, else None.
+ pub fn get_key_value<'a>(&'a self, key: &str) -> Option<(&'a Key, &'a Item)> {
+ self.items.get(key).and_then(|kv| {
+ if !kv.value.is_none() {
+ Some((&kv.key, &kv.value))
+ } else {
+ None
+ }
+ })
+ }
+
+ /// Return mutable references to the key-value pair stored for key, if it is present, else None.
+ pub fn get_key_value_mut<'a>(&'a mut self, key: &str) -> Option<(KeyMut<'a>, &'a mut Item)> {
+ self.items.get_mut(key).and_then(|kv| {
+ if !kv.value.is_none() {
+ Some((kv.key.as_mut(), &mut kv.value))
+ } else {
+ None
+ }
+ })
+ }
+
+ /// Returns true iff the table contains given key.
+ pub fn contains_key(&self, key: &str) -> bool {
+ if let Some(kv) = self.items.get(key) {
+ kv.value.is_value()
+ } else {
+ false
+ }
+ }
+
+ /// Inserts a key/value pair if the table does not contain the key.
+ /// Returns a mutable reference to the corresponding value.
+ pub fn get_or_insert<V: Into<Value>>(
+ &mut self,
+ key: impl Into<InternalString>,
+ value: V,
+ ) -> &mut Value {
+ let key = key.into();
+ self.items
+ .entry(key.clone())
+ .or_insert(TableKeyValue::new(Key::new(key), Item::Value(value.into())))
+ .value
+ .as_value_mut()
+ .expect("non-value type in inline table")
+ }
+
+ /// Inserts a key-value pair into the map.
+ pub fn insert(&mut self, key: impl Into<InternalString>, value: Value) -> Option<Value> {
+ let key = key.into();
+ let kv = TableKeyValue::new(Key::new(key.clone()), Item::Value(value));
+ self.items
+ .insert(key, kv)
+ .and_then(|kv| kv.value.into_value().ok())
+ }
+
+ /// Inserts a key-value pair into the map.
+ pub fn insert_formatted(&mut self, key: &Key, value: Value) -> Option<Value> {
+ let kv = TableKeyValue::new(key.to_owned(), Item::Value(value));
+ self.items
+ .insert(InternalString::from(key.get()), kv)
+ .filter(|kv| kv.value.is_value())
+ .map(|kv| kv.value.into_value().unwrap())
+ }
+
+ /// Removes an item given the key.
+ pub fn remove(&mut self, key: &str) -> Option<Value> {
+ self.items
+ .shift_remove(key)
+ .and_then(|kv| kv.value.into_value().ok())
+ }
+
+ /// Removes a key from the map, returning the stored key and value if the key was previously in the map.
+ pub fn remove_entry(&mut self, key: &str) -> Option<(Key, Value)> {
+ self.items.shift_remove(key).and_then(|kv| {
+ let key = kv.key;
+ kv.value.into_value().ok().map(|value| (key, value))
+ })
+ }
+
+ /// Retains only the elements specified by the `keep` predicate.
+ ///
+ /// In other words, remove all pairs `(key, value)` for which
+ /// `keep(&key, &mut value)` returns `false`.
+ ///
+ /// The elements are visited in iteration order.
+ pub fn retain<F>(&mut self, mut keep: F)
+ where
+ F: FnMut(&str, &mut Value) -> bool,
+ {
+ self.items.retain(|key, item| {
+ item.value
+ .as_value_mut()
+ .map(|value| keep(key, value))
+ .unwrap_or(false)
+ });
+ }
+}
+
+impl std::fmt::Display for InlineTable {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ crate::encode::Encode::encode(self, f, None, ("", ""))
+ }
+}
+
+impl<K: Into<Key>, V: Into<Value>> Extend<(K, V)> for InlineTable {
+ fn extend<T: IntoIterator<Item = (K, V)>>(&mut self, iter: T) {
+ for (key, value) in iter {
+ let key = key.into();
+ let value = Item::Value(value.into());
+ let value = TableKeyValue::new(key, value);
+ self.items
+ .insert(InternalString::from(value.key.get()), value);
+ }
+ }
+}
+
+impl<K: Into<Key>, V: Into<Value>> FromIterator<(K, V)> for InlineTable {
+ fn from_iter<I>(iter: I) -> Self
+ where
+ I: IntoIterator<Item = (K, V)>,
+ {
+ let mut table = InlineTable::new();
+ table.extend(iter);
+ table
+ }
+}
+
+impl IntoIterator for InlineTable {
+ type Item = (InternalString, Value);
+ type IntoIter = InlineTableIntoIter;
+
+ fn into_iter(self) -> Self::IntoIter {
+ Box::new(
+ self.items
+ .into_iter()
+ .filter(|(_, kv)| kv.value.is_value())
+ .map(|(k, kv)| (k, kv.value.into_value().unwrap())),
+ )
+ }
+}
+
+impl<'s> IntoIterator for &'s InlineTable {
+ type Item = (&'s str, &'s Value);
+ type IntoIter = InlineTableIter<'s>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter()
+ }
+}
+
+fn decorate_inline_table(table: &mut InlineTable) {
+ for (key_decor, value) in table
+ .items
+ .iter_mut()
+ .filter(|&(_, ref kv)| kv.value.is_value())
+ .map(|(_, kv)| (&mut kv.key.decor, kv.value.as_value_mut().unwrap()))
+ {
+ key_decor.clear();
+ value.decor_mut().clear();
+ }
+}
+
+/// An owned iterator type over key/value pairs of an inline table.
+pub type InlineTableIntoIter = Box<dyn Iterator<Item = (InternalString, Value)>>;
+/// An iterator type over key/value pairs of an inline table.
+pub type InlineTableIter<'a> = Box<dyn Iterator<Item = (&'a str, &'a Value)> + 'a>;
+/// A mutable iterator type over key/value pairs of an inline table.
+pub type InlineTableIterMut<'a> = Box<dyn Iterator<Item = (KeyMut<'a>, &'a mut Value)> + 'a>;
+
+impl TableLike for InlineTable {
+ fn iter(&self) -> Iter<'_> {
+ Box::new(self.items.iter().map(|(key, kv)| (&key[..], &kv.value)))
+ }
+ fn iter_mut(&mut self) -> IterMut<'_> {
+ Box::new(
+ self.items
+ .iter_mut()
+ .map(|(_, kv)| (kv.key.as_mut(), &mut kv.value)),
+ )
+ }
+ fn clear(&mut self) {
+ self.clear();
+ }
+ fn entry<'a>(&'a mut self, key: &str) -> crate::Entry<'a> {
+ // Accept a `&str` rather than an owned type to keep `InternalString`, well, internal
+ match self.items.entry(key.into()) {
+ indexmap::map::Entry::Occupied(entry) => {
+ crate::Entry::Occupied(crate::OccupiedEntry { entry })
+ }
+ indexmap::map::Entry::Vacant(entry) => {
+ crate::Entry::Vacant(crate::VacantEntry { entry, key: None })
+ }
+ }
+ }
+ fn entry_format<'a>(&'a mut self, key: &Key) -> crate::Entry<'a> {
+ // Accept a `&Key` to be consistent with `entry`
+ match self.items.entry(key.get().into()) {
+ indexmap::map::Entry::Occupied(entry) => {
+ crate::Entry::Occupied(crate::OccupiedEntry { entry })
+ }
+ indexmap::map::Entry::Vacant(entry) => crate::Entry::Vacant(crate::VacantEntry {
+ entry,
+ key: Some(key.to_owned()),
+ }),
+ }
+ }
+ fn get<'s>(&'s self, key: &str) -> Option<&'s Item> {
+ self.items.get(key).map(|kv| &kv.value)
+ }
+ fn get_mut<'s>(&'s mut self, key: &str) -> Option<&'s mut Item> {
+ self.items.get_mut(key).map(|kv| &mut kv.value)
+ }
+ fn get_key_value<'a>(&'a self, key: &str) -> Option<(&'a Key, &'a Item)> {
+ self.get_key_value(key)
+ }
+ fn get_key_value_mut<'a>(&'a mut self, key: &str) -> Option<(KeyMut<'a>, &'a mut Item)> {
+ self.get_key_value_mut(key)
+ }
+ fn contains_key(&self, key: &str) -> bool {
+ self.contains_key(key)
+ }
+ fn insert(&mut self, key: &str, value: Item) -> Option<Item> {
+ self.insert(key, value.into_value().unwrap())
+ .map(Item::Value)
+ }
+ fn remove(&mut self, key: &str) -> Option<Item> {
+ self.remove(key).map(Item::Value)
+ }
+
+ fn get_values(&self) -> Vec<(Vec<&Key>, &Value)> {
+ self.get_values()
+ }
+ fn fmt(&mut self) {
+ self.fmt()
+ }
+ fn sort_values(&mut self) {
+ self.sort_values()
+ }
+ fn set_dotted(&mut self, yes: bool) {
+ self.set_dotted(yes)
+ }
+ fn is_dotted(&self) -> bool {
+ self.is_dotted()
+ }
+
+ fn key_decor_mut(&mut self, key: &str) -> Option<&mut Decor> {
+ self.key_decor_mut(key)
+ }
+ fn key_decor(&self, key: &str) -> Option<&Decor> {
+ self.key_decor(key)
+ }
+}
+
+// `{ key1 = value1, ... }`
+pub(crate) const DEFAULT_INLINE_KEY_DECOR: (&str, &str) = (" ", " ");
+
+/// A view into a single location in a map, which may be vacant or occupied.
+pub enum InlineEntry<'a> {
+ /// An occupied Entry.
+ Occupied(InlineOccupiedEntry<'a>),
+ /// A vacant Entry.
+ Vacant(InlineVacantEntry<'a>),
+}
+
+impl<'a> InlineEntry<'a> {
+ /// Returns the entry key
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use toml_edit::Table;
+ ///
+ /// let mut map = Table::new();
+ ///
+ /// assert_eq!("hello", map.entry("hello").key());
+ /// ```
+ pub fn key(&self) -> &str {
+ match self {
+ InlineEntry::Occupied(e) => e.key(),
+ InlineEntry::Vacant(e) => e.key(),
+ }
+ }
+
+ /// Ensures a value is in the entry by inserting the default if empty, and returns
+ /// a mutable reference to the value in the entry.
+ pub fn or_insert(self, default: Value) -> &'a mut Value {
+ match self {
+ InlineEntry::Occupied(entry) => entry.into_mut(),
+ InlineEntry::Vacant(entry) => entry.insert(default),
+ }
+ }
+
+ /// Ensures a value is in the entry by inserting the result of the default function if empty,
+ /// and returns a mutable reference to the value in the entry.
+ pub fn or_insert_with<F: FnOnce() -> Value>(self, default: F) -> &'a mut Value {
+ match self {
+ InlineEntry::Occupied(entry) => entry.into_mut(),
+ InlineEntry::Vacant(entry) => entry.insert(default()),
+ }
+ }
+}
+
+/// A view into a single occupied location in a `IndexMap`.
+pub struct InlineOccupiedEntry<'a> {
+ entry: indexmap::map::OccupiedEntry<'a, InternalString, TableKeyValue>,
+}
+
+impl<'a> InlineOccupiedEntry<'a> {
+ /// Gets a reference to the entry key
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use toml_edit::Table;
+ ///
+ /// let mut map = Table::new();
+ ///
+ /// assert_eq!("foo", map.entry("foo").key());
+ /// ```
+ pub fn key(&self) -> &str {
+ self.entry.key().as_str()
+ }
+
+ /// Gets a mutable reference to the entry key
+ pub fn key_mut(&mut self) -> KeyMut<'_> {
+ self.entry.get_mut().key.as_mut()
+ }
+
+ /// Gets a reference to the value in the entry.
+ pub fn get(&self) -> &Value {
+ self.entry.get().value.as_value().unwrap()
+ }
+
+ /// Gets a mutable reference to the value in the entry.
+ pub fn get_mut(&mut self) -> &mut Value {
+ self.entry.get_mut().value.as_value_mut().unwrap()
+ }
+
+ /// Converts the OccupiedEntry into a mutable reference to the value in the entry
+ /// with a lifetime bound to the map itself
+ pub fn into_mut(self) -> &'a mut Value {
+ self.entry.into_mut().value.as_value_mut().unwrap()
+ }
+
+ /// Sets the value of the entry, and returns the entry's old value
+ pub fn insert(&mut self, value: Value) -> Value {
+ let mut value = Item::Value(value);
+ std::mem::swap(&mut value, &mut self.entry.get_mut().value);
+ value.into_value().unwrap()
+ }
+
+ /// Takes the value out of the entry, and returns it
+ pub fn remove(self) -> Value {
+ self.entry.shift_remove().value.into_value().unwrap()
+ }
+}
+
+/// A view into a single empty location in a `IndexMap`.
+pub struct InlineVacantEntry<'a> {
+ entry: indexmap::map::VacantEntry<'a, InternalString, TableKeyValue>,
+ key: Option<Key>,
+}
+
+impl<'a> InlineVacantEntry<'a> {
+ /// Gets a reference to the entry key
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use toml_edit::Table;
+ ///
+ /// let mut map = Table::new();
+ ///
+ /// assert_eq!("foo", map.entry("foo").key());
+ /// ```
+ pub fn key(&self) -> &str {
+ self.entry.key().as_str()
+ }
+
+ /// Sets the value of the entry with the VacantEntry's key,
+ /// and returns a mutable reference to it
+ pub fn insert(self, value: Value) -> &'a mut Value {
+ let entry = self.entry;
+ let key = self.key.unwrap_or_else(|| Key::new(entry.key().as_str()));
+ let value = Item::Value(value);
+ entry
+ .insert(TableKeyValue::new(key, value))
+ .value
+ .as_value_mut()
+ .unwrap()
+ }
+}
diff --git a/src/internal_string.rs b/src/internal_string.rs
new file mode 100644
index 0000000..d4347d2
--- /dev/null
+++ b/src/internal_string.rs
@@ -0,0 +1,183 @@
+use std::borrow::Borrow;
+use std::str::FromStr;
+
+/// Opaque string storage internal to `toml_edit`
+#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct InternalString(Inner);
+
+#[cfg(feature = "kstring")]
+type Inner = kstring::KString;
+#[cfg(not(feature = "kstring"))]
+type Inner = String;
+
+impl InternalString {
+ /// Create an empty string
+ pub fn new() -> Self {
+ InternalString(Inner::new())
+ }
+
+ /// Access the underlying string
+ #[inline]
+ pub fn as_str(&self) -> &str {
+ self.0.as_str()
+ }
+}
+
+impl std::fmt::Debug for InternalString {
+ #[inline]
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+ self.0.fmt(formatter)
+ }
+}
+
+impl std::ops::Deref for InternalString {
+ type Target = str;
+
+ #[inline]
+ fn deref(&self) -> &str {
+ self.as_str()
+ }
+}
+
+impl Borrow<str> for InternalString {
+ #[inline]
+ fn borrow(&self) -> &str {
+ self.as_str()
+ }
+}
+
+impl AsRef<str> for InternalString {
+ #[inline]
+ fn as_ref(&self) -> &str {
+ self.as_str()
+ }
+}
+
+impl From<&str> for InternalString {
+ #[inline]
+ fn from(s: &str) -> Self {
+ #[cfg(feature = "kstring")]
+ let inner = kstring::KString::from_ref(s);
+ #[cfg(not(feature = "kstring"))]
+ let inner = String::from(s);
+
+ InternalString(inner)
+ }
+}
+
+impl From<String> for InternalString {
+ #[inline]
+ fn from(s: String) -> Self {
+ #[allow(clippy::useless_conversion)] // handle any string type
+ InternalString(s.into())
+ }
+}
+
+impl From<&String> for InternalString {
+ #[inline]
+ fn from(s: &String) -> Self {
+ InternalString(s.into())
+ }
+}
+
+impl From<&InternalString> for InternalString {
+ #[inline]
+ fn from(s: &InternalString) -> Self {
+ s.clone()
+ }
+}
+
+impl From<Box<str>> for InternalString {
+ #[inline]
+ fn from(s: Box<str>) -> Self {
+ InternalString(s.into())
+ }
+}
+
+impl FromStr for InternalString {
+ type Err = core::convert::Infallible;
+ #[inline]
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(Self::from(s))
+ }
+}
+
+impl std::fmt::Display for InternalString {
+ #[inline]
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.as_str().fmt(f)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl serde::Serialize for InternalString {
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(self.as_str())
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> serde::Deserialize<'de> for InternalString {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ deserializer.deserialize_string(StringVisitor)
+ }
+}
+
+#[cfg(feature = "serde")]
+struct StringVisitor;
+
+#[cfg(feature = "serde")]
+impl<'de> serde::de::Visitor<'de> for StringVisitor {
+ type Value = InternalString;
+
+ fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ formatter.write_str("a string")
+ }
+
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ Ok(InternalString::from(v))
+ }
+
+ fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ Ok(InternalString::from(v))
+ }
+
+ fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ match std::str::from_utf8(v) {
+ Ok(s) => Ok(InternalString::from(s)),
+ Err(_) => Err(serde::de::Error::invalid_value(
+ serde::de::Unexpected::Bytes(v),
+ &self,
+ )),
+ }
+ }
+
+ fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ match String::from_utf8(v) {
+ Ok(s) => Ok(InternalString::from(s)),
+ Err(e) => Err(serde::de::Error::invalid_value(
+ serde::de::Unexpected::Bytes(&e.into_bytes()),
+ &self,
+ )),
+ }
+ }
+}
diff --git a/src/item.rs b/src/item.rs
new file mode 100644
index 0000000..2025fd9
--- /dev/null
+++ b/src/item.rs
@@ -0,0 +1,393 @@
+use std::str::FromStr;
+
+use toml_datetime::*;
+
+use crate::array_of_tables::ArrayOfTables;
+use crate::table::TableLike;
+use crate::{Array, InlineTable, Table, Value};
+
+/// Type representing either a value, a table, an array of tables, or none.
+#[derive(Debug)]
+pub enum Item {
+ /// Type representing none.
+ None,
+ /// Type representing value.
+ Value(Value),
+ /// Type representing table.
+ Table(Table),
+ /// Type representing array of tables.
+ ArrayOfTables(ArrayOfTables),
+}
+
+impl Item {
+ /// Sets `self` to the given item iff `self` is none and
+ /// returns a mutable reference to `self`.
+ pub fn or_insert(&mut self, item: Item) -> &mut Item {
+ if self.is_none() {
+ *self = item
+ }
+ self
+ }
+}
+
+// TODO: This should be generated by macro or derive
+/// Downcasting
+impl Item {
+ /// Text description of value type
+ pub fn type_name(&self) -> &'static str {
+ match self {
+ Item::None => "none",
+ Item::Value(v) => v.type_name(),
+ Item::Table(..) => "table",
+ Item::ArrayOfTables(..) => "array of tables",
+ }
+ }
+
+ /// Index into a TOML array or map. A string index can be used to access a
+ /// value in a map, and a usize index can be used to access an element of an
+ /// array.
+ ///
+ /// Returns `None` if:
+ /// - The type of `self` does not match the type of the
+ /// index, for example if the index is a string and `self` is an array or a
+ /// number.
+ /// - The given key does not exist in the map
+ /// or the given index is not within the bounds of the array.
+ pub fn get<I: crate::index::Index>(&self, index: I) -> Option<&Item> {
+ index.index(self)
+ }
+
+ /// Mutably index into a TOML array or map. A string index can be used to
+ /// access a value in a map, and a usize index can be used to access an
+ /// element of an array.
+ ///
+ /// Returns `None` if:
+ /// - The type of `self` does not match the type of the
+ /// index, for example if the index is a string and `self` is an array or a
+ /// number.
+ /// - The given key does not exist in the map
+ /// or the given index is not within the bounds of the array.
+ pub fn get_mut<I: crate::index::Index>(&mut self, index: I) -> Option<&mut Item> {
+ index.index_mut(self)
+ }
+
+ /// Casts `self` to value.
+ pub fn as_value(&self) -> Option<&Value> {
+ match *self {
+ Item::Value(ref v) => Some(v),
+ _ => None,
+ }
+ }
+ /// Casts `self` to table.
+ pub fn as_table(&self) -> Option<&Table> {
+ match *self {
+ Item::Table(ref t) => Some(t),
+ _ => None,
+ }
+ }
+ /// Casts `self` to array of tables.
+ pub fn as_array_of_tables(&self) -> Option<&ArrayOfTables> {
+ match *self {
+ Item::ArrayOfTables(ref a) => Some(a),
+ _ => None,
+ }
+ }
+ /// Casts `self` to mutable value.
+ pub fn as_value_mut(&mut self) -> Option<&mut Value> {
+ match *self {
+ Item::Value(ref mut v) => Some(v),
+ _ => None,
+ }
+ }
+ /// Casts `self` to mutable table.
+ pub fn as_table_mut(&mut self) -> Option<&mut Table> {
+ match *self {
+ Item::Table(ref mut t) => Some(t),
+ _ => None,
+ }
+ }
+ /// Casts `self` to mutable array of tables.
+ pub fn as_array_of_tables_mut(&mut self) -> Option<&mut ArrayOfTables> {
+ match *self {
+ Item::ArrayOfTables(ref mut a) => Some(a),
+ _ => None,
+ }
+ }
+ /// Casts `self` to value.
+ pub fn into_value(self) -> Result<Value, Self> {
+ match self {
+ Item::None => Err(self),
+ Item::Value(v) => Ok(v),
+ Item::Table(v) => {
+ let v = v.into_inline_table();
+ Ok(Value::InlineTable(v))
+ }
+ Item::ArrayOfTables(v) => {
+ let v = v.into_array();
+ Ok(Value::Array(v))
+ }
+ }
+ }
+ /// In-place convert to a value
+ pub fn make_value(&mut self) {
+ let other = std::mem::take(self);
+ let other = other.into_value().map(Item::Value).unwrap_or(Item::None);
+ *self = other;
+ }
+ /// Casts `self` to table.
+ pub fn into_table(self) -> Result<Table, Self> {
+ match self {
+ Item::Table(t) => Ok(t),
+ Item::Value(Value::InlineTable(t)) => Ok(t.into_table()),
+ _ => Err(self),
+ }
+ }
+ /// Casts `self` to array of tables.
+ pub fn into_array_of_tables(self) -> Result<ArrayOfTables, Self> {
+ match self {
+ Item::ArrayOfTables(a) => Ok(a),
+ Item::Value(Value::Array(a)) => {
+ if a.is_empty() {
+ Err(Item::Value(Value::Array(a)))
+ } else if a.iter().all(|v| v.is_inline_table()) {
+ let mut aot = ArrayOfTables::new();
+ aot.values = a.values;
+ for value in aot.values.iter_mut() {
+ value.make_item();
+ }
+ Ok(aot)
+ } else {
+ Err(Item::Value(Value::Array(a)))
+ }
+ }
+ _ => Err(self),
+ }
+ }
+ // Starting private because the name is unclear
+ pub(crate) fn make_item(&mut self) {
+ let other = std::mem::take(self);
+ let other = match other.into_table().map(crate::Item::Table) {
+ Ok(i) => i,
+ Err(i) => i,
+ };
+ let other = match other.into_array_of_tables().map(crate::Item::ArrayOfTables) {
+ Ok(i) => i,
+ Err(i) => i,
+ };
+ *self = other;
+ }
+ /// Returns true iff `self` is a value.
+ pub fn is_value(&self) -> bool {
+ self.as_value().is_some()
+ }
+ /// Returns true iff `self` is a table.
+ pub fn is_table(&self) -> bool {
+ self.as_table().is_some()
+ }
+ /// Returns true iff `self` is an array of tables.
+ pub fn is_array_of_tables(&self) -> bool {
+ self.as_array_of_tables().is_some()
+ }
+ /// Returns true iff `self` is `None`.
+ pub fn is_none(&self) -> bool {
+ matches!(*self, Item::None)
+ }
+
+ // Duplicate Value downcasting API
+
+ /// Casts `self` to integer.
+ pub fn as_integer(&self) -> Option<i64> {
+ self.as_value().and_then(Value::as_integer)
+ }
+
+ /// Returns true iff `self` is an integer.
+ pub fn is_integer(&self) -> bool {
+ self.as_integer().is_some()
+ }
+
+ /// Casts `self` to float.
+ pub fn as_float(&self) -> Option<f64> {
+ self.as_value().and_then(Value::as_float)
+ }
+
+ /// Returns true iff `self` is a float.
+ pub fn is_float(&self) -> bool {
+ self.as_float().is_some()
+ }
+
+ /// Casts `self` to boolean.
+ pub fn as_bool(&self) -> Option<bool> {
+ self.as_value().and_then(Value::as_bool)
+ }
+
+ /// Returns true iff `self` is a boolean.
+ pub fn is_bool(&self) -> bool {
+ self.as_bool().is_some()
+ }
+
+ /// Casts `self` to str.
+ pub fn as_str(&self) -> Option<&str> {
+ self.as_value().and_then(Value::as_str)
+ }
+
+ /// Returns true iff `self` is a string.
+ pub fn is_str(&self) -> bool {
+ self.as_str().is_some()
+ }
+
+ /// Casts `self` to date-time.
+ pub fn as_datetime(&self) -> Option<&Datetime> {
+ self.as_value().and_then(Value::as_datetime)
+ }
+
+ /// Returns true iff `self` is a date-time.
+ pub fn is_datetime(&self) -> bool {
+ self.as_datetime().is_some()
+ }
+
+ /// Casts `self` to array.
+ pub fn as_array(&self) -> Option<&Array> {
+ self.as_value().and_then(Value::as_array)
+ }
+
+ /// Casts `self` to mutable array.
+ pub fn as_array_mut(&mut self) -> Option<&mut Array> {
+ self.as_value_mut().and_then(Value::as_array_mut)
+ }
+
+ /// Returns true iff `self` is an array.
+ pub fn is_array(&self) -> bool {
+ self.as_array().is_some()
+ }
+
+ /// Casts `self` to inline table.
+ pub fn as_inline_table(&self) -> Option<&InlineTable> {
+ self.as_value().and_then(Value::as_inline_table)
+ }
+
+ /// Casts `self` to mutable inline table.
+ pub fn as_inline_table_mut(&mut self) -> Option<&mut InlineTable> {
+ self.as_value_mut().and_then(Value::as_inline_table_mut)
+ }
+
+ /// Returns true iff `self` is an inline table.
+ pub fn is_inline_table(&self) -> bool {
+ self.as_inline_table().is_some()
+ }
+
+ /// Casts `self` to either a table or an inline table.
+ pub fn as_table_like(&self) -> Option<&dyn TableLike> {
+ self.as_table()
+ .map(|t| t as &dyn TableLike)
+ .or_else(|| self.as_inline_table().map(|t| t as &dyn TableLike))
+ }
+
+ /// Casts `self` to either a table or an inline table.
+ pub fn as_table_like_mut(&mut self) -> Option<&mut dyn TableLike> {
+ match self {
+ Item::Table(t) => Some(t as &mut dyn TableLike),
+ Item::Value(Value::InlineTable(t)) => Some(t as &mut dyn TableLike),
+ _ => None,
+ }
+ }
+
+ /// Returns true iff `self` is either a table, or an inline table.
+ pub fn is_table_like(&self) -> bool {
+ self.as_table_like().is_some()
+ }
+
+ /// Returns the location within the original document
+ pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+ match self {
+ Item::None => None,
+ Item::Value(v) => v.span(),
+ Item::Table(v) => v.span(),
+ Item::ArrayOfTables(v) => v.span(),
+ }
+ }
+
+ pub(crate) fn despan(&mut self, input: &str) {
+ match self {
+ Item::None => {}
+ Item::Value(v) => v.despan(input),
+ Item::Table(v) => v.despan(input),
+ Item::ArrayOfTables(v) => v.despan(input),
+ }
+ }
+}
+
+impl Clone for Item {
+ #[inline(never)]
+ fn clone(&self) -> Self {
+ match self {
+ Item::None => Item::None,
+ Item::Value(v) => Item::Value(v.clone()),
+ Item::Table(v) => Item::Table(v.clone()),
+ Item::ArrayOfTables(v) => Item::ArrayOfTables(v.clone()),
+ }
+ }
+}
+
+impl Default for Item {
+ fn default() -> Self {
+ Item::None
+ }
+}
+
+impl FromStr for Item {
+ type Err = crate::TomlError;
+
+ /// Parses a value from a &str
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let value = s.parse::<Value>()?;
+ Ok(Item::Value(value))
+ }
+}
+
+impl std::fmt::Display for Item {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match &self {
+ Item::None => Ok(()),
+ Item::Value(v) => v.fmt(f),
+ Item::Table(v) => v.fmt(f),
+ Item::ArrayOfTables(v) => v.fmt(f),
+ }
+ }
+}
+
+/// Returns a formatted value.
+///
+/// Since formatting is part of a `Value`, the right hand side of the
+/// assignment needs to be decorated with a space before the value.
+/// The `value` function does just that.
+///
+/// # Examples
+/// ```rust
+/// # use snapbox::assert_eq;
+/// # use toml_edit::*;
+/// let mut table = Table::default();
+/// let mut array = Array::default();
+/// array.push("hello");
+/// array.push("\\, world"); // \ is only allowed in a literal string
+/// table["key1"] = value("value1");
+/// table["key2"] = value(42);
+/// table["key3"] = value(array);
+/// assert_eq(table.to_string(),
+/// r#"key1 = "value1"
+/// key2 = 42
+/// key3 = ["hello", '\, world']
+/// "#);
+/// ```
+pub fn value<V: Into<Value>>(v: V) -> Item {
+ Item::Value(v.into())
+}
+
+/// Returns an empty table.
+pub fn table() -> Item {
+ Item::Table(Table::new())
+}
+
+/// Returns an empty array of tables.
+pub fn array() -> Item {
+ Item::ArrayOfTables(ArrayOfTables::new())
+}
diff --git a/src/key.rs b/src/key.rs
new file mode 100644
index 0000000..c1ee165
--- /dev/null
+++ b/src/key.rs
@@ -0,0 +1,344 @@
+use std::borrow::Cow;
+use std::str::FromStr;
+
+use crate::encode::{to_string_repr, StringStyle};
+use crate::parser;
+use crate::parser::key::is_unquoted_char;
+use crate::repr::{Decor, Repr};
+use crate::InternalString;
+
+/// Key as part of a Key/Value Pair or a table header.
+///
+/// # Examples
+///
+/// ```notrust
+/// [dependencies."nom"]
+/// version = "5.0"
+/// 'literal key' = "nonsense"
+/// "basic string key" = 42
+/// ```
+///
+/// There are 3 types of keys:
+///
+/// 1. Bare keys (`version` and `dependencies`)
+///
+/// 2. Basic quoted keys (`"basic string key"` and `"nom"`)
+///
+/// 3. Literal quoted keys (`'literal key'`)
+///
+/// For details see [toml spec](https://github.com/toml-lang/toml/#keyvalue-pair).
+///
+/// To parse a key use `FromStr` trait implementation: `"string".parse::<Key>()`.
+#[derive(Debug)]
+pub struct Key {
+ key: InternalString,
+ pub(crate) repr: Option<Repr>,
+ pub(crate) decor: Decor,
+}
+
+impl Key {
+ /// Create a new table key
+ pub fn new(key: impl Into<InternalString>) -> Self {
+ Self {
+ key: key.into(),
+ repr: None,
+ decor: Default::default(),
+ }
+ }
+
+ /// Parse a TOML key expression
+ ///
+ /// Unlike `"".parse<Key>()`, this supports dotted keys.
+ pub fn parse(repr: &str) -> Result<Vec<Self>, crate::TomlError> {
+ Self::try_parse_path(repr)
+ }
+
+ pub(crate) fn with_repr_unchecked(mut self, repr: Repr) -> Self {
+ self.repr = Some(repr);
+ self
+ }
+
+ /// While creating the `Key`, add `Decor` to it
+ pub fn with_decor(mut self, decor: Decor) -> Self {
+ self.decor = decor;
+ self
+ }
+
+ /// Access a mutable proxy for the `Key`.
+ pub fn as_mut(&mut self) -> KeyMut<'_> {
+ KeyMut { key: self }
+ }
+
+ /// Returns the parsed key value.
+ pub fn get(&self) -> &str {
+ &self.key
+ }
+
+ pub(crate) fn get_internal(&self) -> &InternalString {
+ &self.key
+ }
+
+ /// Returns key raw representation, if available.
+ pub fn as_repr(&self) -> Option<&Repr> {
+ self.repr.as_ref()
+ }
+
+ /// Returns the default raw representation.
+ pub fn default_repr(&self) -> Repr {
+ to_key_repr(&self.key)
+ }
+
+ /// Returns a raw representation.
+ pub fn display_repr(&self) -> Cow<'_, str> {
+ self.as_repr()
+ .and_then(|r| r.as_raw().as_str())
+ .map(Cow::Borrowed)
+ .unwrap_or_else(|| {
+ Cow::Owned(self.default_repr().as_raw().as_str().unwrap().to_owned())
+ })
+ }
+
+ /// Returns the surrounding whitespace
+ pub fn decor_mut(&mut self) -> &mut Decor {
+ &mut self.decor
+ }
+
+ /// Returns the surrounding whitespace
+ pub fn decor(&self) -> &Decor {
+ &self.decor
+ }
+
+ /// Returns the location within the original document
+ #[cfg(feature = "serde")]
+ pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+ self.repr.as_ref().and_then(|r| r.span())
+ }
+
+ pub(crate) fn despan(&mut self, input: &str) {
+ self.decor.despan(input);
+ if let Some(repr) = &mut self.repr {
+ repr.despan(input)
+ }
+ }
+
+ /// Auto formats the key.
+ pub fn fmt(&mut self) {
+ self.repr = Some(to_key_repr(&self.key));
+ self.decor.clear();
+ }
+
+ fn try_parse_simple(s: &str) -> Result<Key, crate::TomlError> {
+ let mut key = parser::parse_key(s)?;
+ key.despan(s);
+ Ok(key)
+ }
+
+ fn try_parse_path(s: &str) -> Result<Vec<Key>, crate::TomlError> {
+ let mut keys = parser::parse_key_path(s)?;
+ for key in &mut keys {
+ key.despan(s);
+ }
+ Ok(keys)
+ }
+}
+
+impl Clone for Key {
+ #[inline(never)]
+ fn clone(&self) -> Self {
+ Self {
+ key: self.key.clone(),
+ repr: self.repr.clone(),
+ decor: self.decor.clone(),
+ }
+ }
+}
+
+impl std::ops::Deref for Key {
+ type Target = str;
+
+ fn deref(&self) -> &Self::Target {
+ self.get()
+ }
+}
+
+impl std::hash::Hash for Key {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.get().hash(state);
+ }
+}
+
+impl Ord for Key {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ self.get().cmp(other.get())
+ }
+}
+
+impl PartialOrd for Key {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Eq for Key {}
+
+impl PartialEq for Key {
+ #[inline]
+ fn eq(&self, other: &Key) -> bool {
+ PartialEq::eq(self.get(), other.get())
+ }
+}
+
+impl PartialEq<str> for Key {
+ #[inline]
+ fn eq(&self, other: &str) -> bool {
+ PartialEq::eq(self.get(), other)
+ }
+}
+
+impl<'s> PartialEq<&'s str> for Key {
+ #[inline]
+ fn eq(&self, other: &&str) -> bool {
+ PartialEq::eq(self.get(), *other)
+ }
+}
+
+impl PartialEq<String> for Key {
+ #[inline]
+ fn eq(&self, other: &String) -> bool {
+ PartialEq::eq(self.get(), other.as_str())
+ }
+}
+
+impl std::fmt::Display for Key {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ crate::encode::Encode::encode(self, f, None, ("", ""))
+ }
+}
+
+impl FromStr for Key {
+ type Err = crate::TomlError;
+
+ /// Tries to parse a key from a &str,
+ /// if fails, tries as basic quoted key (surrounds with "")
+ /// and then literal quoted key (surrounds with '')
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Key::try_parse_simple(s)
+ }
+}
+
+fn to_key_repr(key: &str) -> Repr {
+ if key.as_bytes().iter().copied().all(is_unquoted_char) && !key.is_empty() {
+ Repr::new_unchecked(key)
+ } else {
+ to_string_repr(key, Some(StringStyle::OnelineSingle), Some(false))
+ }
+}
+
+impl<'b> From<&'b str> for Key {
+ fn from(s: &'b str) -> Self {
+ Key::new(s)
+ }
+}
+
+impl<'b> From<&'b String> for Key {
+ fn from(s: &'b String) -> Self {
+ Key::new(s)
+ }
+}
+
+impl From<String> for Key {
+ fn from(s: String) -> Self {
+ Key::new(s)
+ }
+}
+
+impl From<InternalString> for Key {
+ fn from(s: InternalString) -> Self {
+ Key::new(s)
+ }
+}
+
+#[doc(hidden)]
+impl From<Key> for InternalString {
+ fn from(key: Key) -> InternalString {
+ key.key
+ }
+}
+
+/// A mutable reference to a `Key`
+#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
+pub struct KeyMut<'k> {
+ key: &'k mut Key,
+}
+
+impl<'k> KeyMut<'k> {
+ /// Returns the parsed key value.
+ pub fn get(&self) -> &str {
+ self.key.get()
+ }
+
+ /// Returns the raw representation, if available.
+ pub fn as_repr(&self) -> Option<&Repr> {
+ self.key.as_repr()
+ }
+
+ /// Returns the default raw representation.
+ pub fn default_repr(&self) -> Repr {
+ self.key.default_repr()
+ }
+
+ /// Returns a raw representation.
+ pub fn display_repr(&self) -> Cow<str> {
+ self.key.display_repr()
+ }
+
+ /// Returns the surrounding whitespace
+ pub fn decor_mut(&mut self) -> &mut Decor {
+ self.key.decor_mut()
+ }
+
+ /// Returns the surrounding whitespace
+ pub fn decor(&self) -> &Decor {
+ self.key.decor()
+ }
+
+ /// Auto formats the key.
+ pub fn fmt(&mut self) {
+ self.key.fmt()
+ }
+}
+
+impl<'k> std::ops::Deref for KeyMut<'k> {
+ type Target = str;
+
+ fn deref(&self) -> &Self::Target {
+ self.get()
+ }
+}
+
+impl<'s> PartialEq<str> for KeyMut<'s> {
+ #[inline]
+ fn eq(&self, other: &str) -> bool {
+ PartialEq::eq(self.get(), other)
+ }
+}
+
+impl<'s> PartialEq<&'s str> for KeyMut<'s> {
+ #[inline]
+ fn eq(&self, other: &&str) -> bool {
+ PartialEq::eq(self.get(), *other)
+ }
+}
+
+impl<'s> PartialEq<String> for KeyMut<'s> {
+ #[inline]
+ fn eq(&self, other: &String) -> bool {
+ PartialEq::eq(self.get(), other.as_str())
+ }
+}
+
+impl<'k> std::fmt::Display for KeyMut<'k> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ std::fmt::Display::fmt(&self.key, f)
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..80c0ddd
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,124 @@
+#![deny(missing_docs)]
+// https://github.com/Marwes/combine/issues/172
+#![recursion_limit = "256"]
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
+
+//! # `toml_edit`
+//!
+//! This crate allows you to parse and modify toml
+//! documents, while preserving comments, spaces *and
+//! relative order* or items.
+//!
+//! If you also need the ease of a more traditional API, see the [`toml`] crate.
+//!
+//! # Example
+//!
+//! ```rust
+//! use toml_edit::{Document, value};
+//!
+//! let toml = r#"
+//! "hello" = 'toml!' # comment
+//! ['a'.b]
+//! "#;
+//! let mut doc = toml.parse::<Document>().expect("invalid doc");
+//! assert_eq!(doc.to_string(), toml);
+//! // let's add a new key/value pair inside a.b: c = {d = "hello"}
+//! doc["a"]["b"]["c"]["d"] = value("hello");
+//! // autoformat inline table a.b.c: { d = "hello" }
+//! doc["a"]["b"]["c"].as_inline_table_mut().map(|t| t.fmt());
+//! let expected = r#"
+//! "hello" = 'toml!' # comment
+//! ['a'.b]
+//! c = { d = "hello" }
+//! "#;
+//! assert_eq!(doc.to_string(), expected);
+//! ```
+//!
+//! ## Controlling formatting
+//!
+//! By default, values are created with default formatting
+//! ```rust
+//! let mut doc = toml_edit::Document::new();
+//! doc["foo"] = toml_edit::value("bar");
+//! let expected = r#"foo = "bar"
+//! "#;
+//! assert_eq!(doc.to_string(), expected);
+//! ```
+//!
+//! You can choose a custom TOML representation by parsing the value.
+//! ```rust
+//! let mut doc = toml_edit::Document::new();
+//! doc["foo"] = "'bar'".parse::<toml_edit::Item>().unwrap();
+//! let expected = r#"foo = 'bar'
+//! "#;
+//! assert_eq!(doc.to_string(), expected);
+//! ```
+//!
+//! ## Limitations
+//!
+//! Things it does not preserve:
+//!
+//! * Scattered array of tables (tables are reordered by default, see [test]).
+//! * Order of dotted keys, see [issue](https://github.com/ordian/toml_edit/issues/163).
+//!
+//! [`toml`]: https://docs.rs/toml/latest/toml/
+//! [test]: https://github.com/ordian/toml_edit/blob/f09bd5d075fdb7d2ef8d9bb3270a34506c276753/tests/test_valid.rs#L84
+
+mod array;
+mod array_of_tables;
+mod document;
+mod encode;
+mod index;
+mod inline_table;
+mod internal_string;
+mod item;
+mod key;
+mod parser;
+mod raw_string;
+mod repr;
+mod table;
+mod value;
+
+#[cfg(feature = "serde")]
+pub mod de;
+#[cfg(feature = "serde")]
+pub mod ser;
+
+pub mod visit;
+pub mod visit_mut;
+
+pub use crate::array::{Array, ArrayIntoIter, ArrayIter, ArrayIterMut};
+pub use crate::array_of_tables::{
+ ArrayOfTables, ArrayOfTablesIntoIter, ArrayOfTablesIter, ArrayOfTablesIterMut,
+};
+pub use crate::document::Document;
+pub use crate::inline_table::{
+ InlineEntry, InlineOccupiedEntry, InlineTable, InlineTableIntoIter, InlineTableIter,
+ InlineTableIterMut, InlineVacantEntry,
+};
+pub use crate::internal_string::InternalString;
+pub use crate::item::{array, table, value, Item};
+pub use crate::key::{Key, KeyMut};
+pub use crate::parser::TomlError;
+pub use crate::raw_string::RawString;
+pub use crate::repr::{Decor, Formatted, Repr};
+pub use crate::table::{
+ Entry, IntoIter, Iter, IterMut, OccupiedEntry, Table, TableLike, VacantEntry,
+};
+pub use crate::value::Value;
+pub use toml_datetime::*;
+
+// Prevent users from some traits.
+pub(crate) mod private {
+ pub trait Sealed {}
+ impl Sealed for usize {}
+ impl Sealed for str {}
+ impl Sealed for String {}
+ impl Sealed for i64 {}
+ impl Sealed for f64 {}
+ impl Sealed for bool {}
+ impl Sealed for crate::Datetime {}
+ impl<'a, T: ?Sized> Sealed for &'a T where T: Sealed {}
+ impl Sealed for crate::Table {}
+ impl Sealed for crate::InlineTable {}
+}
diff --git a/src/parser/array.rs b/src/parser/array.rs
new file mode 100644
index 0000000..e3b1f3f
--- /dev/null
+++ b/src/parser/array.rs
@@ -0,0 +1,146 @@
+use winnow::combinator::cut_err;
+use winnow::combinator::delimited;
+use winnow::combinator::opt;
+use winnow::combinator::separated1;
+use winnow::trace::trace;
+
+use crate::parser::trivia::ws_comment_newline;
+use crate::parser::value::value;
+use crate::{Array, Item, RawString, Value};
+
+use crate::parser::prelude::*;
+
+// ;; Array
+
+// array = array-open array-values array-close
+pub(crate) fn array<'i>(check: RecursionCheck) -> impl Parser<Input<'i>, Array, ContextError> {
+ trace("array", move |input: &mut Input<'i>| {
+ delimited(
+ ARRAY_OPEN,
+ cut_err(array_values(check)),
+ cut_err(ARRAY_CLOSE)
+ .context(StrContext::Label("array"))
+ .context(StrContext::Expected(StrContextValue::CharLiteral(']'))),
+ )
+ .parse_next(input)
+ })
+}
+
+// note: we're omitting ws and newlines here, because
+// they should be part of the formatted values
+// array-open = %x5B ws-newline ; [
+pub(crate) const ARRAY_OPEN: u8 = b'[';
+// array-close = ws-newline %x5D ; ]
+const ARRAY_CLOSE: u8 = b']';
+// array-sep = ws %x2C ws ; , Comma
+const ARRAY_SEP: u8 = b',';
+
+// note: this rule is modified
+// array-values = [ ( array-value array-sep array-values ) /
+// array-value / ws-comment-newline ]
+pub(crate) fn array_values<'i>(
+ check: RecursionCheck,
+) -> impl Parser<Input<'i>, Array, ContextError> {
+ move |input: &mut Input<'i>| {
+ let check = check.recursing(input)?;
+ (
+ opt(
+ (separated1(array_value(check), ARRAY_SEP), opt(ARRAY_SEP)).map(
+ |(v, trailing): (Vec<Value>, Option<u8>)| {
+ (
+ Array::with_vec(v.into_iter().map(Item::Value).collect()),
+ trailing.is_some(),
+ )
+ },
+ ),
+ ),
+ ws_comment_newline.span(),
+ )
+ .try_map::<_, _, std::str::Utf8Error>(|(array, trailing)| {
+ let (mut array, comma) = array.unwrap_or_default();
+ array.set_trailing_comma(comma);
+ array.set_trailing(RawString::with_span(trailing));
+ Ok(array)
+ })
+ .parse_next(input)
+ }
+}
+
+pub(crate) fn array_value<'i>(
+ check: RecursionCheck,
+) -> impl Parser<Input<'i>, Value, ContextError> {
+ move |input: &mut Input<'i>| {
+ (
+ ws_comment_newline.span(),
+ value(check),
+ ws_comment_newline.span(),
+ )
+ .map(|(ws1, v, ws2)| v.decorated(RawString::with_span(ws1), RawString::with_span(ws2)))
+ .parse_next(input)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn arrays() {
+ let inputs = [
+ r#"[]"#,
+ r#"[ ]"#,
+ r#"[
+ 1, 2, 3
+]"#,
+ r#"[
+ 1,
+ 2, # this is ok
+]"#,
+ r#"[# comment
+# comment2
+
+
+ ]"#,
+ r#"[# comment
+# comment2
+ 1
+
+#sd
+,
+# comment3
+
+ ]"#,
+ r#"[1]"#,
+ r#"[1,]"#,
+ r#"[ "all", 'strings', """are the same""", '''type''']"#,
+ r#"[ 100, -2,]"#,
+ r#"[1, 2, 3]"#,
+ r#"[1.1, 2.1, 3.1]"#,
+ r#"["a", "b", "c"]"#,
+ r#"[ [ 1, 2 ], [3, 4, 5] ]"#,
+ r#"[ [ 1, 2 ], ["a", "b", "c"] ]"#,
+ r#"[ { x = 1, a = "2" }, {a = "a",b = "b", c = "c"} ]"#,
+ ];
+ for input in inputs {
+ dbg!(input);
+ let mut parsed = array(Default::default()).parse(new_input(input));
+ if let Ok(parsed) = &mut parsed {
+ parsed.despan(input);
+ }
+ assert_eq!(parsed.map(|a| a.to_string()), Ok(input.to_owned()));
+ }
+ }
+
+ #[test]
+ fn invalid_arrays() {
+ let invalid_inputs = [r#"["#, r#"[,]"#, r#"[,2]"#, r#"[1e165,,]"#];
+ for input in invalid_inputs {
+ dbg!(input);
+ let mut parsed = array(Default::default()).parse(new_input(input));
+ if let Ok(parsed) = &mut parsed {
+ parsed.despan(input);
+ }
+ assert!(parsed.is_err());
+ }
+ }
+}
diff --git a/src/parser/datetime.rs b/src/parser/datetime.rs
new file mode 100644
index 0000000..6e89b97
--- /dev/null
+++ b/src/parser/datetime.rs
@@ -0,0 +1,446 @@
+use std::ops::RangeInclusive;
+
+use crate::parser::errors::CustomError;
+use crate::parser::prelude::*;
+use crate::parser::trivia::from_utf8_unchecked;
+
+use toml_datetime::*;
+use winnow::combinator::alt;
+use winnow::combinator::cut_err;
+use winnow::combinator::opt;
+use winnow::combinator::preceded;
+use winnow::token::one_of;
+use winnow::token::take_while;
+use winnow::trace::trace;
+
+// ;; Date and Time (as defined in RFC 3339)
+
+// date-time = offset-date-time / local-date-time / local-date / local-time
+// offset-date-time = full-date time-delim full-time
+// local-date-time = full-date time-delim partial-time
+// local-date = full-date
+// local-time = partial-time
+// full-time = partial-time time-offset
+pub(crate) fn date_time(input: &mut Input<'_>) -> PResult<Datetime> {
+ trace(
+ "date-time",
+ alt((
+ (full_date, opt((time_delim, partial_time, opt(time_offset))))
+ .map(|(date, opt)| {
+ match opt {
+ // Offset Date-Time
+ Some((_, time, offset)) => Datetime {
+ date: Some(date),
+ time: Some(time),
+ offset,
+ },
+ // Local Date
+ None => Datetime {
+ date: Some(date),
+ time: None,
+ offset: None,
+ },
+ }
+ })
+ .context(StrContext::Label("date-time")),
+ partial_time
+ .map(|t| t.into())
+ .context(StrContext::Label("time")),
+ )),
+ )
+ .parse_next(input)
+}
+
+// full-date = date-fullyear "-" date-month "-" date-mday
+pub(crate) fn full_date(input: &mut Input<'_>) -> PResult<Date> {
+ trace(
+ "full-date",
+ (date_fullyear, b'-', cut_err((date_month, b'-', date_mday)))
+ .map(|(year, _, (month, _, day))| Date { year, month, day }),
+ )
+ .parse_next(input)
+}
+
+// partial-time = time-hour ":" time-minute ":" time-second [time-secfrac]
+pub(crate) fn partial_time(input: &mut Input<'_>) -> PResult<Time> {
+ trace(
+ "partial-time",
+ (
+ time_hour,
+ b':',
+ cut_err((time_minute, b':', time_second, opt(time_secfrac))),
+ )
+ .map(|(hour, _, (minute, _, second, nanosecond))| Time {
+ hour,
+ minute,
+ second,
+ nanosecond: nanosecond.unwrap_or_default(),
+ }),
+ )
+ .parse_next(input)
+}
+
+// time-offset = "Z" / time-numoffset
+// time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
+pub(crate) fn time_offset(input: &mut Input<'_>) -> PResult<Offset> {
+ trace(
+ "time-offset",
+ alt((
+ one_of((b'Z', b'z')).value(Offset::Z),
+ (
+ one_of((b'+', b'-')),
+ cut_err((time_hour, b':', time_minute)),
+ )
+ .map(|(sign, (hours, _, minutes))| {
+ let sign = match sign {
+ b'+' => 1,
+ b'-' => -1,
+ _ => unreachable!("Parser prevents this"),
+ };
+ sign * (hours as i16 * 60 + minutes as i16)
+ })
+ .verify(|minutes| ((-24 * 60)..=(24 * 60)).contains(minutes))
+ .map(|minutes| Offset::Custom { minutes }),
+ ))
+ .context(StrContext::Label("time offset")),
+ )
+ .parse_next(input)
+}
+
+// date-fullyear = 4DIGIT
+pub(crate) fn date_fullyear(input: &mut Input<'_>) -> PResult<u16> {
+ unsigned_digits::<4, 4>
+ .map(|s: &str| s.parse::<u16>().expect("4DIGIT should match u8"))
+ .parse_next(input)
+}
+
+// date-month = 2DIGIT ; 01-12
+pub(crate) fn date_month(input: &mut Input<'_>) -> PResult<u8> {
+ unsigned_digits::<2, 2>
+ .try_map(|s: &str| {
+ let d = s.parse::<u8>().expect("2DIGIT should match u8");
+ if (1..=12).contains(&d) {
+ Ok(d)
+ } else {
+ Err(CustomError::OutOfRange)
+ }
+ })
+ .parse_next(input)
+}
+
+// date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
+pub(crate) fn date_mday(input: &mut Input<'_>) -> PResult<u8> {
+ unsigned_digits::<2, 2>
+ .try_map(|s: &str| {
+ let d = s.parse::<u8>().expect("2DIGIT should match u8");
+ if (1..=31).contains(&d) {
+ Ok(d)
+ } else {
+ Err(CustomError::OutOfRange)
+ }
+ })
+ .parse_next(input)
+}
+
+// time-delim = "T" / %x20 ; T, t, or space
+pub(crate) fn time_delim(input: &mut Input<'_>) -> PResult<u8> {
+ one_of(TIME_DELIM).parse_next(input)
+}
+
+const TIME_DELIM: (u8, u8, u8) = (b'T', b't', b' ');
+
+// time-hour = 2DIGIT ; 00-23
+pub(crate) fn time_hour(input: &mut Input<'_>) -> PResult<u8> {
+ unsigned_digits::<2, 2>
+ .try_map(|s: &str| {
+ let d = s.parse::<u8>().expect("2DIGIT should match u8");
+ if (0..=23).contains(&d) {
+ Ok(d)
+ } else {
+ Err(CustomError::OutOfRange)
+ }
+ })
+ .parse_next(input)
+}
+
+// time-minute = 2DIGIT ; 00-59
+pub(crate) fn time_minute(input: &mut Input<'_>) -> PResult<u8> {
+ unsigned_digits::<2, 2>
+ .try_map(|s: &str| {
+ let d = s.parse::<u8>().expect("2DIGIT should match u8");
+ if (0..=59).contains(&d) {
+ Ok(d)
+ } else {
+ Err(CustomError::OutOfRange)
+ }
+ })
+ .parse_next(input)
+}
+
+// time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second rules
+pub(crate) fn time_second(input: &mut Input<'_>) -> PResult<u8> {
+ unsigned_digits::<2, 2>
+ .try_map(|s: &str| {
+ let d = s.parse::<u8>().expect("2DIGIT should match u8");
+ if (0..=60).contains(&d) {
+ Ok(d)
+ } else {
+ Err(CustomError::OutOfRange)
+ }
+ })
+ .parse_next(input)
+}
+
+// time-secfrac = "." 1*DIGIT
+pub(crate) fn time_secfrac(input: &mut Input<'_>) -> PResult<u32> {
+ static SCALE: [u32; 10] = [
+ 0,
+ 100_000_000,
+ 10_000_000,
+ 1_000_000,
+ 100_000,
+ 10_000,
+ 1_000,
+ 100,
+ 10,
+ 1,
+ ];
+ const INF: usize = usize::MAX;
+ preceded(b'.', unsigned_digits::<1, INF>)
+ .try_map(|mut repr: &str| -> Result<u32, CustomError> {
+ let max_digits = SCALE.len() - 1;
+ if max_digits < repr.len() {
+ // Millisecond precision is required. Further precision of fractional seconds is
+ // implementation-specific. If the value contains greater precision than the
+ // implementation can support, the additional precision must be truncated, not rounded.
+ repr = &repr[0..max_digits];
+ }
+
+ let v = repr.parse::<u32>().map_err(|_| CustomError::OutOfRange)?;
+ let num_digits = repr.len();
+
+ // scale the number accordingly.
+ let scale = SCALE.get(num_digits).ok_or(CustomError::OutOfRange)?;
+ let v = v.checked_mul(*scale).ok_or(CustomError::OutOfRange)?;
+ Ok(v)
+ })
+ .parse_next(input)
+}
+
+pub(crate) fn unsigned_digits<'i, const MIN: usize, const MAX: usize>(
+ input: &mut Input<'i>,
+) -> PResult<&'i str> {
+ take_while(MIN..=MAX, DIGIT)
+ .map(|b: &[u8]| unsafe { from_utf8_unchecked(b, "`is_ascii_digit` filters out on-ASCII") })
+ .parse_next(input)
+}
+
+// DIGIT = %x30-39 ; 0-9
+const DIGIT: RangeInclusive<u8> = b'0'..=b'9';
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn offset_date_time() {
+ let inputs = [
+ (
+ "1979-05-27T07:32:00Z",
+ Datetime {
+ date: Some(Date {
+ year: 1979,
+ month: 5,
+ day: 27,
+ }),
+ time: Some(Time {
+ hour: 7,
+ minute: 32,
+ second: 0,
+ nanosecond: 0,
+ }),
+ offset: Some(Offset::Z),
+ },
+ ),
+ (
+ "1979-05-27T00:32:00-07:00",
+ Datetime {
+ date: Some(Date {
+ year: 1979,
+ month: 5,
+ day: 27,
+ }),
+ time: Some(Time {
+ hour: 0,
+ minute: 32,
+ second: 0,
+ nanosecond: 0,
+ }),
+ offset: Some(Offset::Custom { minutes: -7 * 60 }),
+ },
+ ),
+ (
+ "1979-05-27T00:32:00-00:36",
+ Datetime {
+ date: Some(Date {
+ year: 1979,
+ month: 5,
+ day: 27,
+ }),
+ time: Some(Time {
+ hour: 0,
+ minute: 32,
+ second: 0,
+ nanosecond: 0,
+ }),
+ offset: Some(Offset::Custom { minutes: -36 }),
+ },
+ ),
+ (
+ "1979-05-27T00:32:00.999999",
+ Datetime {
+ date: Some(Date {
+ year: 1979,
+ month: 5,
+ day: 27,
+ }),
+ time: Some(Time {
+ hour: 0,
+ minute: 32,
+ second: 0,
+ nanosecond: 999999000,
+ }),
+ offset: None,
+ },
+ ),
+ ];
+ for (input, expected) in inputs {
+ dbg!(input);
+ let actual = date_time.parse(new_input(input)).unwrap();
+ assert_eq!(expected, actual);
+ }
+ }
+
+ #[test]
+ fn local_date_time() {
+ let inputs = [
+ (
+ "1979-05-27T07:32:00",
+ Datetime {
+ date: Some(Date {
+ year: 1979,
+ month: 5,
+ day: 27,
+ }),
+ time: Some(Time {
+ hour: 7,
+ minute: 32,
+ second: 0,
+ nanosecond: 0,
+ }),
+ offset: None,
+ },
+ ),
+ (
+ "1979-05-27T00:32:00.999999",
+ Datetime {
+ date: Some(Date {
+ year: 1979,
+ month: 5,
+ day: 27,
+ }),
+ time: Some(Time {
+ hour: 0,
+ minute: 32,
+ second: 0,
+ nanosecond: 999999000,
+ }),
+ offset: None,
+ },
+ ),
+ ];
+ for (input, expected) in inputs {
+ dbg!(input);
+ let actual = date_time.parse(new_input(input)).unwrap();
+ assert_eq!(expected, actual);
+ }
+ }
+
+ #[test]
+ fn local_date() {
+ let inputs = [
+ (
+ "1979-05-27",
+ Datetime {
+ date: Some(Date {
+ year: 1979,
+ month: 5,
+ day: 27,
+ }),
+ time: None,
+ offset: None,
+ },
+ ),
+ (
+ "2017-07-20",
+ Datetime {
+ date: Some(Date {
+ year: 2017,
+ month: 7,
+ day: 20,
+ }),
+ time: None,
+ offset: None,
+ },
+ ),
+ ];
+ for (input, expected) in inputs {
+ dbg!(input);
+ let actual = date_time.parse(new_input(input)).unwrap();
+ assert_eq!(expected, actual);
+ }
+ }
+
+ #[test]
+ fn local_time() {
+ let inputs = [
+ (
+ "07:32:00",
+ Datetime {
+ date: None,
+ time: Some(Time {
+ hour: 7,
+ minute: 32,
+ second: 0,
+ nanosecond: 0,
+ }),
+ offset: None,
+ },
+ ),
+ (
+ "00:32:00.999999",
+ Datetime {
+ date: None,
+ time: Some(Time {
+ hour: 0,
+ minute: 32,
+ second: 0,
+ nanosecond: 999999000,
+ }),
+ offset: None,
+ },
+ ),
+ ];
+ for (input, expected) in inputs {
+ dbg!(input);
+ let actual = date_time.parse(new_input(input)).unwrap();
+ assert_eq!(expected, actual);
+ }
+ }
+
+ #[test]
+ fn time_fraction_truncated() {
+ let input = "1987-07-05T17:45:00.123456789012345Z";
+ date_time.parse(new_input(input)).unwrap();
+ }
+}
diff --git a/src/parser/document.rs b/src/parser/document.rs
new file mode 100644
index 0000000..aa8fb11
--- /dev/null
+++ b/src/parser/document.rs
@@ -0,0 +1,141 @@
+use std::cell::RefCell;
+
+use winnow::combinator::cut_err;
+use winnow::combinator::eof;
+use winnow::combinator::opt;
+use winnow::combinator::peek;
+use winnow::combinator::repeat;
+use winnow::token::any;
+use winnow::token::one_of;
+use winnow::trace::trace;
+
+use crate::document::Document;
+use crate::key::Key;
+use crate::parser::inline_table::KEYVAL_SEP;
+use crate::parser::key::key;
+use crate::parser::prelude::*;
+use crate::parser::state::ParseState;
+use crate::parser::table::table;
+use crate::parser::trivia::{comment, line_ending, line_trailing, newline, ws};
+use crate::parser::value::value;
+use crate::table::TableKeyValue;
+use crate::Item;
+use crate::RawString;
+
+// ;; TOML
+
+// toml = expression *( newline expression )
+
+// expression = ( ( ws comment ) /
+// ( ws keyval ws [ comment ] ) /
+// ( ws table ws [ comment ] ) /
+// ws )
+pub(crate) fn document(input: &mut Input<'_>) -> PResult<Document> {
+ let state = RefCell::new(ParseState::default());
+ let state_ref = &state;
+
+ let _o = (
+ // Remove BOM if present
+ opt(b"\xEF\xBB\xBF"),
+ parse_ws(state_ref),
+ repeat(0.., (
+ dispatch! {peek(any);
+ crate::parser::trivia::COMMENT_START_SYMBOL => cut_err(parse_comment(state_ref)),
+ crate::parser::table::STD_TABLE_OPEN => cut_err(table(state_ref)),
+ crate::parser::trivia::LF |
+ crate::parser::trivia::CR => parse_newline(state_ref),
+ _ => cut_err(keyval(state_ref)),
+ },
+ parse_ws(state_ref),
+ ))
+ .map(|()| ()),
+ eof,
+ )
+ .parse_next(input)?;
+ state.into_inner().into_document().map_err(|err| {
+ winnow::error::ErrMode::from_external_error(input, winnow::error::ErrorKind::Verify, err)
+ })
+}
+
+pub(crate) fn parse_comment<'s, 'i>(
+ state: &'s RefCell<ParseState>,
+) -> impl Parser<Input<'i>, (), ContextError> + 's {
+ move |i: &mut Input<'i>| {
+ (comment, line_ending)
+ .span()
+ .map(|span| {
+ state.borrow_mut().on_comment(span);
+ })
+ .parse_next(i)
+ }
+}
+
+pub(crate) fn parse_ws<'s, 'i>(
+ state: &'s RefCell<ParseState>,
+) -> impl Parser<Input<'i>, (), ContextError> + 's {
+ move |i: &mut Input<'i>| {
+ ws.span()
+ .map(|span| state.borrow_mut().on_ws(span))
+ .parse_next(i)
+ }
+}
+
+pub(crate) fn parse_newline<'s, 'i>(
+ state: &'s RefCell<ParseState>,
+) -> impl Parser<Input<'i>, (), ContextError> + 's {
+ move |i: &mut Input<'i>| {
+ newline
+ .span()
+ .map(|span| state.borrow_mut().on_ws(span))
+ .parse_next(i)
+ }
+}
+
+pub(crate) fn keyval<'s, 'i>(
+ state: &'s RefCell<ParseState>,
+) -> impl Parser<Input<'i>, (), ContextError> + 's {
+ move |i: &mut Input<'i>| {
+ parse_keyval
+ .try_map(|(p, kv)| state.borrow_mut().on_keyval(p, kv))
+ .parse_next(i)
+ }
+}
+
+// keyval = key keyval-sep val
+pub(crate) fn parse_keyval(input: &mut Input<'_>) -> PResult<(Vec<Key>, TableKeyValue)> {
+ trace(
+ "keyval",
+ (
+ key,
+ cut_err((
+ one_of(KEYVAL_SEP)
+ .context(StrContext::Expected(StrContextValue::CharLiteral('.')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('='))),
+ (
+ ws.span(),
+ value(RecursionCheck::default()),
+ line_trailing
+ .context(StrContext::Expected(StrContextValue::CharLiteral('\n')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('#'))),
+ ),
+ )),
+ )
+ .try_map::<_, _, std::str::Utf8Error>(|(key, (_, v))| {
+ let mut path = key;
+ let key = path.pop().expect("grammar ensures at least 1");
+
+ let (pre, v, suf) = v;
+ let pre = RawString::with_span(pre);
+ let suf = RawString::with_span(suf);
+ let v = v.decorated(pre, suf);
+ Ok((
+ path,
+ TableKeyValue {
+ key,
+ value: Item::Value(v),
+ },
+ ))
+ }),
+ )
+ .parse_next(input)
+}
diff --git a/src/parser/errors.rs b/src/parser/errors.rs
new file mode 100644
index 0000000..859ed53
--- /dev/null
+++ b/src/parser/errors.rs
@@ -0,0 +1,316 @@
+use std::error::Error as StdError;
+use std::fmt::{Display, Formatter, Result};
+
+use crate::parser::prelude::*;
+use crate::Key;
+
+use winnow::error::ContextError;
+use winnow::error::ParseError;
+
+/// Type representing a TOML parse error
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct TomlError {
+ message: String,
+ original: Option<String>,
+ keys: Vec<String>,
+ span: Option<std::ops::Range<usize>>,
+}
+
+impl TomlError {
+ pub(crate) fn new(error: ParseError<Input<'_>, ContextError>, mut original: Input<'_>) -> Self {
+ use winnow::stream::Stream;
+
+ let offset = error.offset();
+ let span = if offset == original.len() {
+ offset..offset
+ } else {
+ offset..(offset + 1)
+ };
+
+ let message = error.inner().to_string();
+ let original = original.finish();
+
+ Self {
+ message,
+ original: Some(
+ String::from_utf8(original.to_owned()).expect("original document was utf8"),
+ ),
+ keys: Vec::new(),
+ span: Some(span),
+ }
+ }
+
+ #[cfg(feature = "serde")]
+ pub(crate) fn custom(message: String, span: Option<std::ops::Range<usize>>) -> Self {
+ Self {
+ message,
+ original: None,
+ keys: Vec::new(),
+ span,
+ }
+ }
+
+ #[cfg(feature = "serde")]
+ pub(crate) fn add_key(&mut self, key: String) {
+ self.keys.insert(0, key);
+ }
+
+ /// What went wrong
+ pub fn message(&self) -> &str {
+ &self.message
+ }
+
+ /// The start/end index into the original document where the error occurred
+ pub fn span(&self) -> Option<std::ops::Range<usize>> {
+ self.span.clone()
+ }
+
+ #[cfg(feature = "serde")]
+ pub(crate) fn set_span(&mut self, span: Option<std::ops::Range<usize>>) {
+ self.span = span;
+ }
+
+ #[cfg(feature = "serde")]
+ pub(crate) fn set_original(&mut self, original: Option<String>) {
+ self.original = original;
+ }
+}
+
+/// Displays a TOML parse error
+///
+/// # Example
+///
+/// TOML parse error at line 1, column 10
+/// |
+/// 1 | 00:32:00.a999999
+/// | ^
+/// Unexpected `a`
+/// Expected `digit`
+/// While parsing a Time
+/// While parsing a Date-Time
+impl Display for TomlError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> Result {
+ let mut context = false;
+ if let (Some(original), Some(span)) = (&self.original, self.span()) {
+ context = true;
+
+ let (line, column) = translate_position(original.as_bytes(), span.start);
+ let line_num = line + 1;
+ let col_num = column + 1;
+ let gutter = line_num.to_string().len();
+ let content = original.split('\n').nth(line).expect("valid line number");
+
+ writeln!(
+ f,
+ "TOML parse error at line {}, column {}",
+ line_num, col_num
+ )?;
+ // |
+ for _ in 0..=gutter {
+ write!(f, " ")?;
+ }
+ writeln!(f, "|")?;
+
+ // 1 | 00:32:00.a999999
+ write!(f, "{} | ", line_num)?;
+ writeln!(f, "{}", content)?;
+
+ // | ^
+ for _ in 0..=gutter {
+ write!(f, " ")?;
+ }
+ write!(f, "|")?;
+ for _ in 0..=column {
+ write!(f, " ")?;
+ }
+ // The span will be empty at eof, so we need to make sure we always print at least
+ // one `^`
+ write!(f, "^")?;
+ for _ in (span.start + 1)..(span.end.min(span.start + content.len())) {
+ write!(f, "^")?;
+ }
+ writeln!(f)?;
+ }
+ writeln!(f, "{}", self.message)?;
+ if !context && !self.keys.is_empty() {
+ writeln!(f, "in `{}`", self.keys.join("."))?;
+ }
+
+ Ok(())
+ }
+}
+
+impl StdError for TomlError {
+ fn description(&self) -> &'static str {
+ "TOML parse error"
+ }
+}
+
+fn translate_position(input: &[u8], index: usize) -> (usize, usize) {
+ if input.is_empty() {
+ return (0, index);
+ }
+
+ let safe_index = index.min(input.len() - 1);
+ let column_offset = index - safe_index;
+ let index = safe_index;
+
+ let nl = input[0..index]
+ .iter()
+ .rev()
+ .enumerate()
+ .find(|(_, b)| **b == b'\n')
+ .map(|(nl, _)| index - nl - 1);
+ let line_start = match nl {
+ Some(nl) => nl + 1,
+ None => 0,
+ };
+ let line = input[0..line_start].iter().filter(|b| **b == b'\n').count();
+ let line = line;
+
+ let column = std::str::from_utf8(&input[line_start..=index])
+ .map(|s| s.chars().count() - 1)
+ .unwrap_or_else(|_| index - line_start);
+ let column = column + column_offset;
+
+ (line, column)
+}
+
+#[cfg(test)]
+mod test_translate_position {
+ use super::*;
+
+ #[test]
+ fn empty() {
+ let input = b"";
+ let index = 0;
+ let position = translate_position(&input[..], index);
+ assert_eq!(position, (0, 0));
+ }
+
+ #[test]
+ fn start() {
+ let input = b"Hello";
+ let index = 0;
+ let position = translate_position(&input[..], index);
+ assert_eq!(position, (0, 0));
+ }
+
+ #[test]
+ fn end() {
+ let input = b"Hello";
+ let index = input.len() - 1;
+ let position = translate_position(&input[..], index);
+ assert_eq!(position, (0, input.len() - 1));
+ }
+
+ #[test]
+ fn after() {
+ let input = b"Hello";
+ let index = input.len();
+ let position = translate_position(&input[..], index);
+ assert_eq!(position, (0, input.len()));
+ }
+
+ #[test]
+ fn first_line() {
+ let input = b"Hello\nWorld\n";
+ let index = 2;
+ let position = translate_position(&input[..], index);
+ assert_eq!(position, (0, 2));
+ }
+
+ #[test]
+ fn end_of_line() {
+ let input = b"Hello\nWorld\n";
+ let index = 5;
+ let position = translate_position(&input[..], index);
+ assert_eq!(position, (0, 5));
+ }
+
+ #[test]
+ fn start_of_second_line() {
+ let input = b"Hello\nWorld\n";
+ let index = 6;
+ let position = translate_position(&input[..], index);
+ assert_eq!(position, (1, 0));
+ }
+
+ #[test]
+ fn second_line() {
+ let input = b"Hello\nWorld\n";
+ let index = 8;
+ let position = translate_position(&input[..], index);
+ assert_eq!(position, (1, 2));
+ }
+}
+
+#[derive(Debug, Clone)]
+pub(crate) enum CustomError {
+ DuplicateKey {
+ key: String,
+ table: Option<Vec<Key>>,
+ },
+ DottedKeyExtendWrongType {
+ key: Vec<Key>,
+ actual: &'static str,
+ },
+ OutOfRange,
+ #[cfg_attr(feature = "unbounded", allow(dead_code))]
+ RecursionLimitExceeded,
+}
+
+impl CustomError {
+ pub(crate) fn duplicate_key(path: &[Key], i: usize) -> Self {
+ assert!(i < path.len());
+ let key = &path[i];
+ let repr = key.display_repr();
+ Self::DuplicateKey {
+ key: repr.into(),
+ table: Some(path[..i].to_vec()),
+ }
+ }
+
+ pub(crate) fn extend_wrong_type(path: &[Key], i: usize, actual: &'static str) -> Self {
+ assert!(i < path.len());
+ Self::DottedKeyExtendWrongType {
+ key: path[..=i].to_vec(),
+ actual,
+ }
+ }
+}
+
+impl StdError for CustomError {
+ fn description(&self) -> &'static str {
+ "TOML parse error"
+ }
+}
+
+impl Display for CustomError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> Result {
+ match self {
+ CustomError::DuplicateKey { key, table } => {
+ if let Some(table) = table {
+ if table.is_empty() {
+ write!(f, "duplicate key `{}` in document root", key)
+ } else {
+ let path = table.iter().map(|k| k.get()).collect::<Vec<_>>().join(".");
+ write!(f, "duplicate key `{}` in table `{}`", key, path)
+ }
+ } else {
+ write!(f, "duplicate key `{}`", key)
+ }
+ }
+ CustomError::DottedKeyExtendWrongType { key, actual } => {
+ let path = key.iter().map(|k| k.get()).collect::<Vec<_>>().join(".");
+ write!(
+ f,
+ "dotted key `{}` attempted to extend non-table type ({})",
+ path, actual
+ )
+ }
+ CustomError::OutOfRange => write!(f, "value is out of range"),
+ CustomError::RecursionLimitExceeded => write!(f, "recursion limit exceded"),
+ }
+ }
+}
diff --git a/src/parser/inline_table.rs b/src/parser/inline_table.rs
new file mode 100644
index 0000000..994e003
--- /dev/null
+++ b/src/parser/inline_table.rs
@@ -0,0 +1,181 @@
+use winnow::combinator::cut_err;
+use winnow::combinator::delimited;
+use winnow::combinator::separated0;
+use winnow::token::one_of;
+use winnow::trace::trace;
+
+use crate::key::Key;
+use crate::parser::errors::CustomError;
+use crate::parser::key::key;
+use crate::parser::prelude::*;
+use crate::parser::trivia::ws;
+use crate::parser::value::value;
+use crate::table::TableKeyValue;
+use crate::{InlineTable, InternalString, Item, RawString, Value};
+
+use indexmap::map::Entry;
+
+// ;; Inline Table
+
+// inline-table = inline-table-open inline-table-keyvals inline-table-close
+pub(crate) fn inline_table<'i>(
+ check: RecursionCheck,
+) -> impl Parser<Input<'i>, InlineTable, ContextError> {
+ trace("inline-table", move |input: &mut Input<'i>| {
+ delimited(
+ INLINE_TABLE_OPEN,
+ cut_err(inline_table_keyvals(check).try_map(|(kv, p)| table_from_pairs(kv, p))),
+ cut_err(INLINE_TABLE_CLOSE)
+ .context(StrContext::Label("inline table"))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('}'))),
+ )
+ .parse_next(input)
+ })
+}
+
+fn table_from_pairs(
+ v: Vec<(Vec<Key>, TableKeyValue)>,
+ preamble: RawString,
+) -> Result<InlineTable, CustomError> {
+ let mut root = InlineTable::new();
+ root.set_preamble(preamble);
+ // Assuming almost all pairs will be directly in `root`
+ root.items.reserve(v.len());
+
+ for (path, kv) in v {
+ let table = descend_path(&mut root, &path)?;
+ let key: InternalString = kv.key.get_internal().into();
+ match table.items.entry(key) {
+ Entry::Vacant(o) => {
+ o.insert(kv);
+ }
+ Entry::Occupied(o) => {
+ return Err(CustomError::DuplicateKey {
+ key: o.key().as_str().into(),
+ table: None,
+ });
+ }
+ }
+ }
+ Ok(root)
+}
+
+fn descend_path<'a>(
+ mut table: &'a mut InlineTable,
+ path: &'a [Key],
+) -> Result<&'a mut InlineTable, CustomError> {
+ for (i, key) in path.iter().enumerate() {
+ let entry = table.entry_format(key).or_insert_with(|| {
+ let mut new_table = InlineTable::new();
+ new_table.set_dotted(true);
+
+ Value::InlineTable(new_table)
+ });
+ match *entry {
+ Value::InlineTable(ref mut sweet_child_of_mine) => {
+ table = sweet_child_of_mine;
+ }
+ ref v => {
+ return Err(CustomError::extend_wrong_type(path, i, v.type_name()));
+ }
+ }
+ }
+ Ok(table)
+}
+
+// inline-table-open = %x7B ws ; {
+pub(crate) const INLINE_TABLE_OPEN: u8 = b'{';
+// inline-table-close = ws %x7D ; }
+const INLINE_TABLE_CLOSE: u8 = b'}';
+// inline-table-sep = ws %x2C ws ; , Comma
+const INLINE_TABLE_SEP: u8 = b',';
+// keyval-sep = ws %x3D ws ; =
+pub(crate) const KEYVAL_SEP: u8 = b'=';
+
+// inline-table-keyvals = [ inline-table-keyvals-non-empty ]
+// inline-table-keyvals-non-empty =
+// ( key keyval-sep val inline-table-sep inline-table-keyvals-non-empty ) /
+// ( key keyval-sep val )
+
+fn inline_table_keyvals<'i>(
+ check: RecursionCheck,
+) -> impl Parser<Input<'i>, (Vec<(Vec<Key>, TableKeyValue)>, RawString), ContextError> {
+ move |input: &mut Input<'i>| {
+ let check = check.recursing(input)?;
+ (
+ separated0(keyval(check), INLINE_TABLE_SEP),
+ ws.span().map(RawString::with_span),
+ )
+ .parse_next(input)
+ }
+}
+
+fn keyval<'i>(
+ check: RecursionCheck,
+) -> impl Parser<Input<'i>, (Vec<Key>, TableKeyValue), ContextError> {
+ move |input: &mut Input<'i>| {
+ (
+ key,
+ cut_err((
+ one_of(KEYVAL_SEP)
+ .context(StrContext::Expected(StrContextValue::CharLiteral('.')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('='))),
+ (ws.span(), value(check), ws.span()),
+ )),
+ )
+ .map(|(key, (_, v))| {
+ let mut path = key;
+ let key = path.pop().expect("grammar ensures at least 1");
+
+ let (pre, v, suf) = v;
+ let pre = RawString::with_span(pre);
+ let suf = RawString::with_span(suf);
+ let v = v.decorated(pre, suf);
+ (
+ path,
+ TableKeyValue {
+ key,
+ value: Item::Value(v),
+ },
+ )
+ })
+ .parse_next(input)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn inline_tables() {
+ let inputs = [
+ r#"{}"#,
+ r#"{ }"#,
+ r#"{a = 1e165}"#,
+ r#"{ hello = "world", a = 1}"#,
+ r#"{ hello.world = "a" }"#,
+ ];
+ for input in inputs {
+ dbg!(input);
+ let mut parsed = inline_table(Default::default()).parse(new_input(input));
+ if let Ok(parsed) = &mut parsed {
+ parsed.despan(input);
+ }
+ assert_eq!(parsed.map(|a| a.to_string()), Ok(input.to_owned()));
+ }
+ }
+
+ #[test]
+ fn invalid_inline_tables() {
+ let invalid_inputs = [r#"{a = 1e165"#, r#"{ hello = "world", a = 2, hello = 1}"#];
+ for input in invalid_inputs {
+ dbg!(input);
+ let mut parsed = inline_table(Default::default()).parse(new_input(input));
+ if let Ok(parsed) = &mut parsed {
+ parsed.despan(input);
+ }
+ assert!(parsed.is_err());
+ }
+ }
+}
diff --git a/src/parser/key.rs b/src/parser/key.rs
new file mode 100644
index 0000000..12715da
--- /dev/null
+++ b/src/parser/key.rs
@@ -0,0 +1,112 @@
+use std::ops::RangeInclusive;
+
+use winnow::combinator::peek;
+use winnow::combinator::separated1;
+use winnow::token::any;
+use winnow::token::take_while;
+use winnow::trace::trace;
+
+use crate::key::Key;
+use crate::parser::errors::CustomError;
+use crate::parser::prelude::*;
+use crate::parser::strings::{basic_string, literal_string};
+use crate::parser::trivia::{from_utf8_unchecked, ws};
+use crate::repr::{Decor, Repr};
+use crate::InternalString;
+use crate::RawString;
+
+// key = simple-key / dotted-key
+// dotted-key = simple-key 1*( dot-sep simple-key )
+pub(crate) fn key(input: &mut Input<'_>) -> PResult<Vec<Key>> {
+ trace(
+ "dotted-key",
+ separated1(
+ (ws.span(), simple_key, ws.span()).map(|(pre, (raw, key), suffix)| {
+ Key::new(key)
+ .with_repr_unchecked(Repr::new_unchecked(raw))
+ .with_decor(Decor::new(
+ RawString::with_span(pre),
+ RawString::with_span(suffix),
+ ))
+ }),
+ DOT_SEP,
+ )
+ .context(StrContext::Label("key"))
+ .try_map(|k: Vec<_>| {
+ // Inserting the key will require recursion down the line
+ RecursionCheck::check_depth(k.len())?;
+ Ok::<_, CustomError>(k)
+ }),
+ )
+ .parse_next(input)
+}
+
+// simple-key = quoted-key / unquoted-key
+// quoted-key = basic-string / literal-string
+pub(crate) fn simple_key(input: &mut Input<'_>) -> PResult<(RawString, InternalString)> {
+ trace(
+ "simple-key",
+ dispatch! {peek(any);
+ crate::parser::strings::QUOTATION_MARK => basic_string
+ .map(|s: std::borrow::Cow<'_, str>| s.as_ref().into()),
+ crate::parser::strings::APOSTROPHE => literal_string.map(|s: &str| s.into()),
+ _ => unquoted_key.map(|s: &str| s.into()),
+ }
+ .with_span()
+ .map(|(k, span)| {
+ let raw = RawString::with_span(span);
+ (raw, k)
+ }),
+ )
+ .parse_next(input)
+}
+
+// unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
+fn unquoted_key<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ trace(
+ "unquoted-key",
+ take_while(1.., UNQUOTED_CHAR)
+ .map(|b| unsafe { from_utf8_unchecked(b, "`is_unquoted_char` filters out on-ASCII") }),
+ )
+ .parse_next(input)
+}
+
+pub(crate) fn is_unquoted_char(c: u8) -> bool {
+ use winnow::stream::ContainsToken;
+ UNQUOTED_CHAR.contains_token(c)
+}
+
+const UNQUOTED_CHAR: (
+ RangeInclusive<u8>,
+ RangeInclusive<u8>,
+ RangeInclusive<u8>,
+ u8,
+ u8,
+) = (b'A'..=b'Z', b'a'..=b'z', b'0'..=b'9', b'-', b'_');
+
+// dot-sep = ws %x2E ws ; . Period
+const DOT_SEP: u8 = b'.';
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn keys() {
+ let cases = [
+ ("a", "a"),
+ (r#""hello\n ""#, "hello\n "),
+ (r#"'hello\n '"#, "hello\\n "),
+ ];
+
+ for (input, expected) in cases {
+ dbg!(input);
+ let parsed = simple_key.parse(new_input(input));
+ assert_eq!(
+ parsed,
+ Ok((RawString::with_span(0..(input.len())), expected.into())),
+ "Parsing {input:?}"
+ );
+ }
+ }
+}
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
new file mode 100644
index 0000000..1b3cc4f
--- /dev/null
+++ b/src/parser/mod.rs
@@ -0,0 +1,265 @@
+#![allow(clippy::type_complexity)]
+
+pub(crate) mod array;
+pub(crate) mod datetime;
+pub(crate) mod document;
+pub(crate) mod errors;
+pub(crate) mod inline_table;
+pub(crate) mod key;
+pub(crate) mod numbers;
+pub(crate) mod state;
+pub(crate) mod strings;
+pub(crate) mod table;
+pub(crate) mod trivia;
+pub(crate) mod value;
+
+pub use errors::TomlError;
+
+pub(crate) fn parse_document(raw: &str) -> Result<crate::Document, TomlError> {
+ use prelude::*;
+
+ let b = new_input(raw);
+ let mut doc = document::document
+ .parse(b)
+ .map_err(|e| TomlError::new(e, b))?;
+ doc.span = Some(0..(raw.len()));
+ doc.original = Some(raw.to_owned());
+ Ok(doc)
+}
+
+pub(crate) fn parse_key(raw: &str) -> Result<crate::Key, TomlError> {
+ use prelude::*;
+
+ let b = new_input(raw);
+ let result = key::simple_key.parse(b);
+ match result {
+ Ok((raw, key)) => {
+ Ok(crate::Key::new(key).with_repr_unchecked(crate::Repr::new_unchecked(raw)))
+ }
+ Err(e) => Err(TomlError::new(e, b)),
+ }
+}
+
+pub(crate) fn parse_key_path(raw: &str) -> Result<Vec<crate::Key>, TomlError> {
+ use prelude::*;
+
+ let b = new_input(raw);
+ let result = key::key.parse(b);
+ match result {
+ Ok(mut keys) => {
+ for key in &mut keys {
+ key.despan(raw);
+ }
+ Ok(keys)
+ }
+ Err(e) => Err(TomlError::new(e, b)),
+ }
+}
+
+pub(crate) fn parse_value(raw: &str) -> Result<crate::Value, TomlError> {
+ use prelude::*;
+
+ let b = new_input(raw);
+ let parsed = value::value(RecursionCheck::default()).parse(b);
+ match parsed {
+ Ok(mut value) => {
+ // Only take the repr and not decor, as its probably not intended
+ value.decor_mut().clear();
+ value.despan(raw);
+ Ok(value)
+ }
+ Err(e) => Err(TomlError::new(e, b)),
+ }
+}
+
+pub(crate) mod prelude {
+ pub(crate) use winnow::combinator::dispatch;
+ pub(crate) use winnow::error::ContextError;
+ pub(crate) use winnow::error::FromExternalError;
+ pub(crate) use winnow::error::StrContext;
+ pub(crate) use winnow::error::StrContextValue;
+ pub(crate) use winnow::PResult;
+ pub(crate) use winnow::Parser;
+
+ pub(crate) type Input<'b> = winnow::Located<&'b winnow::BStr>;
+
+ pub(crate) fn new_input(s: &str) -> Input<'_> {
+ winnow::Located::new(winnow::BStr::new(s))
+ }
+
+ #[cfg(not(feature = "unbounded"))]
+ #[derive(Copy, Clone, Debug, Default)]
+ pub(crate) struct RecursionCheck {
+ current: usize,
+ }
+
+ #[cfg(not(feature = "unbounded"))]
+ impl RecursionCheck {
+ pub(crate) fn check_depth(depth: usize) -> Result<(), super::errors::CustomError> {
+ if depth < 128 {
+ Ok(())
+ } else {
+ Err(super::errors::CustomError::RecursionLimitExceeded)
+ }
+ }
+
+ pub(crate) fn recursing(
+ mut self,
+ input: &mut Input<'_>,
+ ) -> Result<Self, winnow::error::ErrMode<ContextError>> {
+ self.current += 1;
+ if self.current < 128 {
+ Ok(self)
+ } else {
+ Err(winnow::error::ErrMode::from_external_error(
+ input,
+ winnow::error::ErrorKind::Eof,
+ super::errors::CustomError::RecursionLimitExceeded,
+ ))
+ }
+ }
+ }
+
+ #[cfg(feature = "unbounded")]
+ #[derive(Copy, Clone, Debug, Default)]
+ pub(crate) struct RecursionCheck {}
+
+ #[cfg(feature = "unbounded")]
+ impl RecursionCheck {
+ pub(crate) fn check_depth(_depth: usize) -> Result<(), super::errors::CustomError> {
+ Ok(())
+ }
+
+ pub(crate) fn recursing(
+ self,
+ _input: &mut Input<'_>,
+ ) -> Result<Self, winnow::error::ErrMode<ContextError>> {
+ Ok(self)
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn documents() {
+ let documents = [
+ "",
+ r#"
+# This is a TOML document.
+
+title = "TOML Example"
+
+ [owner]
+ name = "Tom Preston-Werner"
+ dob = 1979-05-27T07:32:00-08:00 # First class dates
+
+ [database]
+ server = "192.168.1.1"
+ ports = [ 8001, 8001, 8002 ]
+ connection_max = 5000
+ enabled = true
+
+ [servers]
+
+ # Indentation (tabs and/or spaces) is allowed but not required
+[servers.alpha]
+ ip = "10.0.0.1"
+ dc = "eqdc10"
+
+ [servers.beta]
+ ip = "10.0.0.2"
+ dc = "eqdc10"
+
+ [clients]
+ data = [ ["gamma", "delta"], [1, 2] ]
+
+ # Line breaks are OK when inside arrays
+hosts = [
+ "alpha",
+ "omega"
+]
+
+ 'some.wierd .stuff' = """
+ like
+ that
+ # """ # this broke my sintax highlighting
+ " also. like " = '''
+that
+'''
+ double = 2e39 # this number looks familiar
+# trailing comment"#,
+ r#""#,
+ r#" "#,
+ r#" hello = 'darkness' # my old friend
+"#,
+ r#"[parent . child]
+key = "value"
+"#,
+ r#"hello.world = "a"
+"#,
+ r#"foo = 1979-05-27 # Comment
+"#,
+ ];
+ for input in documents {
+ dbg!(input);
+ let mut parsed = parse_document(input);
+ if let Ok(parsed) = &mut parsed {
+ parsed.despan();
+ }
+ let doc = match parsed {
+ Ok(doc) => doc,
+ Err(err) => {
+ panic!(
+ "Parse error: {:?}\nFailed to parse:\n```\n{}\n```",
+ err, input
+ )
+ }
+ };
+
+ snapbox::assert_eq(input, doc.to_string());
+ }
+ }
+
+ #[test]
+ fn documents_parse_only() {
+ let parse_only = ["\u{FEFF}
+[package]
+name = \"foo\"
+version = \"0.0.1\"
+authors = []
+"];
+ for input in parse_only {
+ dbg!(input);
+ let mut parsed = parse_document(input);
+ if let Ok(parsed) = &mut parsed {
+ parsed.despan();
+ }
+ match parsed {
+ Ok(_) => (),
+ Err(err) => {
+ panic!(
+ "Parse error: {:?}\nFailed to parse:\n```\n{}\n```",
+ err, input
+ )
+ }
+ }
+ }
+ }
+
+ #[test]
+ fn invalid_documents() {
+ let invalid_inputs = [r#" hello = 'darkness' # my old friend
+$"#];
+ for input in invalid_inputs {
+ dbg!(input);
+ let mut parsed = parse_document(input);
+ if let Ok(parsed) = &mut parsed {
+ parsed.despan();
+ }
+ assert!(parsed.is_err(), "Input: {:?}", input);
+ }
+ }
+}
diff --git a/src/parser/numbers.rs b/src/parser/numbers.rs
new file mode 100644
index 0000000..6e4757f
--- /dev/null
+++ b/src/parser/numbers.rs
@@ -0,0 +1,397 @@
+use std::ops::RangeInclusive;
+
+use winnow::combinator::alt;
+use winnow::combinator::cut_err;
+use winnow::combinator::opt;
+use winnow::combinator::peek;
+use winnow::combinator::preceded;
+use winnow::combinator::repeat;
+use winnow::combinator::rest;
+use winnow::token::one_of;
+use winnow::token::tag;
+use winnow::token::take;
+use winnow::trace::trace;
+
+use crate::parser::prelude::*;
+use crate::parser::trivia::from_utf8_unchecked;
+
+// ;; Boolean
+
+// boolean = true / false
+#[allow(dead_code)] // directly define in `fn value`
+pub(crate) fn boolean(input: &mut Input<'_>) -> PResult<bool> {
+ trace("boolean", alt((true_, false_))).parse_next(input)
+}
+
+pub(crate) fn true_(input: &mut Input<'_>) -> PResult<bool> {
+ (peek(TRUE[0]), cut_err(TRUE)).value(true).parse_next(input)
+}
+const TRUE: &[u8] = b"true";
+
+pub(crate) fn false_(input: &mut Input<'_>) -> PResult<bool> {
+ (peek(FALSE[0]), cut_err(FALSE))
+ .value(false)
+ .parse_next(input)
+}
+const FALSE: &[u8] = b"false";
+
+// ;; Integer
+
+// integer = dec-int / hex-int / oct-int / bin-int
+pub(crate) fn integer(input: &mut Input<'_>) -> PResult<i64> {
+ trace("integer",
+ dispatch! {peek(opt::<_, &[u8], _, _>(take(2usize)));
+ Some(b"0x") => cut_err(hex_int.try_map(|s| i64::from_str_radix(&s.replace('_', ""), 16))),
+ Some(b"0o") => cut_err(oct_int.try_map(|s| i64::from_str_radix(&s.replace('_', ""), 8))),
+ Some(b"0b") => cut_err(bin_int.try_map(|s| i64::from_str_radix(&s.replace('_', ""), 2))),
+ _ => dec_int.and_then(cut_err(rest
+ .try_map(|s: &str| s.replace('_', "").parse())))
+ })
+ .parse_next(input)
+}
+
+// dec-int = [ minus / plus ] unsigned-dec-int
+// unsigned-dec-int = DIGIT / digit1-9 1*( DIGIT / underscore DIGIT )
+pub(crate) fn dec_int<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ trace(
+ "dec-int",
+ (
+ opt(one_of((b'+', b'-'))),
+ alt((
+ (
+ one_of(DIGIT1_9),
+ repeat(
+ 0..,
+ alt((
+ digit.value(()),
+ (
+ one_of(b'_'),
+ cut_err(digit).context(StrContext::Expected(
+ StrContextValue::Description("digit"),
+ )),
+ )
+ .value(()),
+ )),
+ )
+ .map(|()| ()),
+ )
+ .value(()),
+ digit.value(()),
+ )),
+ )
+ .recognize()
+ .map(|b: &[u8]| unsafe {
+ from_utf8_unchecked(b, "`digit` and `_` filter out non-ASCII")
+ })
+ .context(StrContext::Label("integer")),
+ )
+ .parse_next(input)
+}
+const DIGIT1_9: RangeInclusive<u8> = b'1'..=b'9';
+
+// hex-prefix = %x30.78 ; 0x
+// hex-int = hex-prefix HEXDIG *( HEXDIG / underscore HEXDIG )
+pub(crate) fn hex_int<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ trace(
+ "hex-int",
+ preceded(
+ HEX_PREFIX,
+ cut_err((
+ hexdig,
+ repeat(
+ 0..,
+ alt((
+ hexdig.value(()),
+ (
+ one_of(b'_'),
+ cut_err(hexdig).context(StrContext::Expected(
+ StrContextValue::Description("digit"),
+ )),
+ )
+ .value(()),
+ )),
+ )
+ .map(|()| ()),
+ ))
+ .recognize(),
+ )
+ .map(|b| unsafe { from_utf8_unchecked(b, "`hexdig` and `_` filter out non-ASCII") })
+ .context(StrContext::Label("hexadecimal integer")),
+ )
+ .parse_next(input)
+}
+const HEX_PREFIX: &[u8] = b"0x";
+
+// oct-prefix = %x30.6F ; 0o
+// oct-int = oct-prefix digit0-7 *( digit0-7 / underscore digit0-7 )
+pub(crate) fn oct_int<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ trace(
+ "oct-int",
+ preceded(
+ OCT_PREFIX,
+ cut_err((
+ one_of(DIGIT0_7),
+ repeat(
+ 0..,
+ alt((
+ one_of(DIGIT0_7).value(()),
+ (
+ one_of(b'_'),
+ cut_err(one_of(DIGIT0_7)).context(StrContext::Expected(
+ StrContextValue::Description("digit"),
+ )),
+ )
+ .value(()),
+ )),
+ )
+ .map(|()| ()),
+ ))
+ .recognize(),
+ )
+ .map(|b| unsafe { from_utf8_unchecked(b, "`DIGIT0_7` and `_` filter out non-ASCII") })
+ .context(StrContext::Label("octal integer")),
+ )
+ .parse_next(input)
+}
+const OCT_PREFIX: &[u8] = b"0o";
+const DIGIT0_7: RangeInclusive<u8> = b'0'..=b'7';
+
+// bin-prefix = %x30.62 ; 0b
+// bin-int = bin-prefix digit0-1 *( digit0-1 / underscore digit0-1 )
+pub(crate) fn bin_int<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ trace(
+ "bin-int",
+ preceded(
+ BIN_PREFIX,
+ cut_err((
+ one_of(DIGIT0_1),
+ repeat(
+ 0..,
+ alt((
+ one_of(DIGIT0_1).value(()),
+ (
+ one_of(b'_'),
+ cut_err(one_of(DIGIT0_1)).context(StrContext::Expected(
+ StrContextValue::Description("digit"),
+ )),
+ )
+ .value(()),
+ )),
+ )
+ .map(|()| ()),
+ ))
+ .recognize(),
+ )
+ .map(|b| unsafe { from_utf8_unchecked(b, "`DIGIT0_1` and `_` filter out non-ASCII") })
+ .context(StrContext::Label("binary integer")),
+ )
+ .parse_next(input)
+}
+const BIN_PREFIX: &[u8] = b"0b";
+const DIGIT0_1: RangeInclusive<u8> = b'0'..=b'1';
+
+// ;; Float
+
+// float = float-int-part ( exp / frac [ exp ] )
+// float =/ special-float
+// float-int-part = dec-int
+pub(crate) fn float(input: &mut Input<'_>) -> PResult<f64> {
+ trace(
+ "float",
+ alt((
+ float_.and_then(cut_err(
+ rest.try_map(|s: &str| s.replace('_', "").parse())
+ .verify(|f: &f64| *f != f64::INFINITY),
+ )),
+ special_float,
+ ))
+ .context(StrContext::Label("floating-point number")),
+ )
+ .parse_next(input)
+}
+
+pub(crate) fn float_<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ (
+ dec_int,
+ alt((exp.void(), (frac.void(), opt(exp.void())).void())),
+ )
+ .recognize()
+ .map(|b: &[u8]| unsafe {
+ from_utf8_unchecked(
+ b,
+ "`dec_int`, `one_of`, `exp`, and `frac` filter out non-ASCII",
+ )
+ })
+ .parse_next(input)
+}
+
+// frac = decimal-point zero-prefixable-int
+// decimal-point = %x2E ; .
+pub(crate) fn frac<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ (
+ b'.',
+ cut_err(zero_prefixable_int)
+ .context(StrContext::Expected(StrContextValue::Description("digit"))),
+ )
+ .recognize()
+ .map(|b: &[u8]| unsafe {
+ from_utf8_unchecked(
+ b,
+ "`.` and `parse_zero_prefixable_int` filter out non-ASCII",
+ )
+ })
+ .parse_next(input)
+}
+
+// zero-prefixable-int = DIGIT *( DIGIT / underscore DIGIT )
+pub(crate) fn zero_prefixable_int<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ (
+ digit,
+ repeat(
+ 0..,
+ alt((
+ digit.value(()),
+ (
+ one_of(b'_'),
+ cut_err(digit)
+ .context(StrContext::Expected(StrContextValue::Description("digit"))),
+ )
+ .value(()),
+ )),
+ )
+ .map(|()| ()),
+ )
+ .recognize()
+ .map(|b: &[u8]| unsafe { from_utf8_unchecked(b, "`digit` and `_` filter out non-ASCII") })
+ .parse_next(input)
+}
+
+// exp = "e" float-exp-part
+// float-exp-part = [ minus / plus ] zero-prefixable-int
+pub(crate) fn exp<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ (
+ one_of((b'e', b'E')),
+ opt(one_of([b'+', b'-'])),
+ cut_err(zero_prefixable_int),
+ )
+ .recognize()
+ .map(|b: &[u8]| unsafe {
+ from_utf8_unchecked(
+ b,
+ "`one_of` and `parse_zero_prefixable_int` filter out non-ASCII",
+ )
+ })
+ .parse_next(input)
+}
+
+// special-float = [ minus / plus ] ( inf / nan )
+pub(crate) fn special_float(input: &mut Input<'_>) -> PResult<f64> {
+ (opt(one_of((b'+', b'-'))), alt((inf, nan)))
+ .map(|(s, f)| match s {
+ Some(b'+') | None => f,
+ Some(b'-') => -f,
+ _ => unreachable!("one_of should prevent this"),
+ })
+ .parse_next(input)
+}
+// inf = %x69.6e.66 ; inf
+pub(crate) fn inf(input: &mut Input<'_>) -> PResult<f64> {
+ tag(INF).value(f64::INFINITY).parse_next(input)
+}
+const INF: &[u8] = b"inf";
+// nan = %x6e.61.6e ; nan
+pub(crate) fn nan(input: &mut Input<'_>) -> PResult<f64> {
+ tag(NAN).value(f64::NAN).parse_next(input)
+}
+const NAN: &[u8] = b"nan";
+
+// DIGIT = %x30-39 ; 0-9
+pub(crate) fn digit(input: &mut Input<'_>) -> PResult<u8> {
+ one_of(DIGIT).parse_next(input)
+}
+const DIGIT: RangeInclusive<u8> = b'0'..=b'9';
+
+// HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
+pub(crate) fn hexdig(input: &mut Input<'_>) -> PResult<u8> {
+ one_of(HEXDIG).parse_next(input)
+}
+pub(crate) const HEXDIG: (RangeInclusive<u8>, RangeInclusive<u8>, RangeInclusive<u8>) =
+ (DIGIT, b'A'..=b'F', b'a'..=b'f');
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn integers() {
+ let cases = [
+ ("+99", 99),
+ ("42", 42),
+ ("0", 0),
+ ("-17", -17),
+ ("1_000", 1_000),
+ ("5_349_221", 5_349_221),
+ ("1_2_3_4_5", 1_2_3_4_5),
+ ("0xF", 15),
+ ("0o0_755", 493),
+ ("0b1_0_1", 5),
+ (&std::i64::MIN.to_string()[..], std::i64::MIN),
+ (&std::i64::MAX.to_string()[..], std::i64::MAX),
+ ];
+ for &(input, expected) in &cases {
+ dbg!(input);
+ let parsed = integer.parse(new_input(input));
+ assert_eq!(parsed, Ok(expected), "Parsing {input:?}");
+ }
+
+ let overflow = "1000000000000000000000000000000000";
+ let parsed = integer.parse(new_input(overflow));
+ assert!(parsed.is_err());
+ }
+
+ #[track_caller]
+ fn assert_float_eq(actual: f64, expected: f64) {
+ if expected.is_nan() {
+ assert!(actual.is_nan());
+ } else if expected.is_infinite() {
+ assert!(actual.is_infinite());
+ assert_eq!(expected.is_sign_positive(), actual.is_sign_positive());
+ } else {
+ dbg!(expected);
+ dbg!(actual);
+ assert!((expected - actual).abs() < std::f64::EPSILON);
+ }
+ }
+
+ #[test]
+ fn floats() {
+ let cases = [
+ ("+1.0", 1.0),
+ ("3.1419", 3.1419),
+ ("-0.01", -0.01),
+ ("5e+22", 5e+22),
+ ("1e6", 1e6),
+ ("-2E-2", -2E-2),
+ ("6.626e-34", 6.626e-34),
+ ("9_224_617.445_991_228_313", 9_224_617.445_991_227),
+ ("-1.7976931348623157e+308", std::f64::MIN),
+ ("1.7976931348623157e+308", std::f64::MAX),
+ ("nan", f64::NAN),
+ ("+nan", f64::NAN),
+ ("-nan", f64::NAN),
+ ("inf", f64::INFINITY),
+ ("+inf", f64::INFINITY),
+ ("-inf", f64::NEG_INFINITY),
+ // ("1e+400", std::f64::INFINITY),
+ ];
+ for &(input, expected) in &cases {
+ dbg!(input);
+ let parsed = float.parse(new_input(input)).unwrap();
+ assert_float_eq(parsed, expected);
+
+ let overflow = "9e99999";
+ let parsed = float.parse(new_input(overflow));
+ assert!(parsed.is_err(), "{:?}", parsed);
+ }
+ }
+}
diff --git a/src/parser/state.rs b/src/parser/state.rs
new file mode 100644
index 0000000..efa884d
--- /dev/null
+++ b/src/parser/state.rs
@@ -0,0 +1,323 @@
+use crate::key::Key;
+use crate::parser::errors::CustomError;
+use crate::repr::Decor;
+use crate::table::TableKeyValue;
+use crate::{ArrayOfTables, Document, InternalString, Item, RawString, Table};
+
+pub(crate) struct ParseState {
+ document: Document,
+ trailing: Option<std::ops::Range<usize>>,
+ current_table_position: usize,
+ current_table: Table,
+ current_is_array: bool,
+ current_table_path: Vec<Key>,
+}
+
+impl ParseState {
+ pub(crate) fn into_document(mut self) -> Result<Document, CustomError> {
+ self.finalize_table()?;
+ let trailing = self.trailing.map(RawString::with_span);
+ self.document.trailing = trailing.unwrap_or_default();
+ Ok(self.document)
+ }
+
+ pub(crate) fn on_ws(&mut self, span: std::ops::Range<usize>) {
+ if let Some(old) = self.trailing.take() {
+ self.trailing = Some(old.start..span.end);
+ } else {
+ self.trailing = Some(span);
+ }
+ }
+
+ pub(crate) fn on_comment(&mut self, span: std::ops::Range<usize>) {
+ if let Some(old) = self.trailing.take() {
+ self.trailing = Some(old.start..span.end);
+ } else {
+ self.trailing = Some(span);
+ }
+ }
+
+ pub(crate) fn on_keyval(
+ &mut self,
+ mut path: Vec<Key>,
+ mut kv: TableKeyValue,
+ ) -> Result<(), CustomError> {
+ {
+ let mut prefix = self.trailing.take();
+ let first_key = if path.is_empty() {
+ &mut kv.key
+ } else {
+ &mut path[0]
+ };
+ let prefix = match (
+ prefix.take(),
+ first_key.decor.prefix().and_then(|d| d.span()),
+ ) {
+ (Some(p), Some(k)) => Some(p.start..k.end),
+ (Some(p), None) | (None, Some(p)) => Some(p),
+ (None, None) => None,
+ };
+ first_key
+ .decor
+ .set_prefix(prefix.map(RawString::with_span).unwrap_or_default());
+ }
+
+ if let (Some(existing), Some(value)) = (self.current_table.span(), kv.value.span()) {
+ self.current_table.span = Some((existing.start)..(value.end));
+ }
+ let table = &mut self.current_table;
+ let table = Self::descend_path(table, &path, true)?;
+
+ // "Likewise, using dotted keys to redefine tables already defined in [table] form is not allowed"
+ let mixed_table_types = table.is_dotted() == path.is_empty();
+ if mixed_table_types {
+ return Err(CustomError::DuplicateKey {
+ key: kv.key.get().into(),
+ table: None,
+ });
+ }
+
+ let key: InternalString = kv.key.get_internal().into();
+ match table.items.entry(key) {
+ indexmap::map::Entry::Vacant(o) => {
+ o.insert(kv);
+ }
+ indexmap::map::Entry::Occupied(o) => {
+ // "Since tables cannot be defined more than once, redefining such tables using a [table] header is not allowed"
+ return Err(CustomError::DuplicateKey {
+ key: o.key().as_str().into(),
+ table: Some(self.current_table_path.clone()),
+ });
+ }
+ }
+
+ Ok(())
+ }
+
+ pub(crate) fn start_aray_table(
+ &mut self,
+ path: Vec<Key>,
+ decor: Decor,
+ span: std::ops::Range<usize>,
+ ) -> Result<(), CustomError> {
+ debug_assert!(!path.is_empty());
+ debug_assert!(self.current_table.is_empty());
+ debug_assert!(self.current_table_path.is_empty());
+
+ // Look up the table on start to ensure the duplicate_key error points to the right line
+ let root = self.document.as_table_mut();
+ let parent_table = Self::descend_path(root, &path[..path.len() - 1], false)?;
+ let key = &path[path.len() - 1];
+ let entry = parent_table
+ .entry_format(key)
+ .or_insert(Item::ArrayOfTables(ArrayOfTables::new()));
+ entry
+ .as_array_of_tables()
+ .ok_or_else(|| CustomError::duplicate_key(&path, path.len() - 1))?;
+
+ self.current_table_position += 1;
+ self.current_table.decor = decor;
+ self.current_table.set_implicit(false);
+ self.current_table.set_dotted(false);
+ self.current_table.set_position(self.current_table_position);
+ self.current_table.span = Some(span);
+ self.current_is_array = true;
+ self.current_table_path = path;
+
+ Ok(())
+ }
+
+ pub(crate) fn start_table(
+ &mut self,
+ path: Vec<Key>,
+ decor: Decor,
+ span: std::ops::Range<usize>,
+ ) -> Result<(), CustomError> {
+ debug_assert!(!path.is_empty());
+ debug_assert!(self.current_table.is_empty());
+ debug_assert!(self.current_table_path.is_empty());
+
+ // 1. Look up the table on start to ensure the duplicate_key error points to the right line
+ // 2. Ensure any child tables from an implicit table are preserved
+ let root = self.document.as_table_mut();
+ let parent_table = Self::descend_path(root, &path[..path.len() - 1], false)?;
+ let key = &path[path.len() - 1];
+ if let Some(entry) = parent_table.remove(key.get()) {
+ match entry {
+ Item::Table(t) if t.implicit && !t.is_dotted() => {
+ self.current_table = t;
+ }
+ // Since tables cannot be defined more than once, redefining such tables using a [table] header is not allowed. Likewise, using dotted keys to redefine tables already defined in [table] form is not allowed.
+ _ => return Err(CustomError::duplicate_key(&path, path.len() - 1)),
+ }
+ }
+
+ self.current_table_position += 1;
+ self.current_table.decor = decor;
+ self.current_table.set_implicit(false);
+ self.current_table.set_dotted(false);
+ self.current_table.set_position(self.current_table_position);
+ self.current_table.span = Some(span);
+ self.current_is_array = false;
+ self.current_table_path = path;
+
+ Ok(())
+ }
+
+ pub(crate) fn finalize_table(&mut self) -> Result<(), CustomError> {
+ let mut table = std::mem::take(&mut self.current_table);
+ let path = std::mem::take(&mut self.current_table_path);
+
+ let root = self.document.as_table_mut();
+ if path.is_empty() {
+ assert!(root.is_empty());
+ std::mem::swap(&mut table, root);
+ } else if self.current_is_array {
+ let parent_table = Self::descend_path(root, &path[..path.len() - 1], false)?;
+ let key = &path[path.len() - 1];
+
+ let entry = parent_table
+ .entry_format(key)
+ .or_insert(Item::ArrayOfTables(ArrayOfTables::new()));
+ let array = entry
+ .as_array_of_tables_mut()
+ .ok_or_else(|| CustomError::duplicate_key(&path, path.len() - 1))?;
+ array.push(table);
+ let span = if let (Some(first), Some(last)) = (
+ array.values.first().and_then(|t| t.span()),
+ array.values.last().and_then(|t| t.span()),
+ ) {
+ Some((first.start)..(last.end))
+ } else {
+ None
+ };
+ array.span = span;
+ } else {
+ let parent_table = Self::descend_path(root, &path[..path.len() - 1], false)?;
+ let key = &path[path.len() - 1];
+
+ let entry = parent_table.entry_format(key);
+ match entry {
+ crate::Entry::Occupied(entry) => {
+ match entry.into_mut() {
+ // if [a.b.c] header preceded [a.b]
+ Item::Table(ref mut t) if t.implicit => {
+ std::mem::swap(t, &mut table);
+ }
+ _ => return Err(CustomError::duplicate_key(&path, path.len() - 1)),
+ }
+ }
+ crate::Entry::Vacant(entry) => {
+ let item = Item::Table(table);
+ entry.insert(item);
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ pub(crate) fn descend_path<'t, 'k>(
+ mut table: &'t mut Table,
+ path: &'k [Key],
+ dotted: bool,
+ ) -> Result<&'t mut Table, CustomError> {
+ for (i, key) in path.iter().enumerate() {
+ let entry = table.entry_format(key).or_insert_with(|| {
+ let mut new_table = Table::new();
+ new_table.set_implicit(true);
+ new_table.set_dotted(dotted);
+
+ Item::Table(new_table)
+ });
+ match *entry {
+ Item::Value(ref v) => {
+ return Err(CustomError::extend_wrong_type(path, i, v.type_name()));
+ }
+ Item::ArrayOfTables(ref mut array) => {
+ debug_assert!(!array.is_empty());
+
+ let index = array.len() - 1;
+ let last_child = array.get_mut(index).unwrap();
+
+ table = last_child;
+ }
+ Item::Table(ref mut sweet_child_of_mine) => {
+ // Since tables cannot be defined more than once, redefining such tables using a
+ // [table] header is not allowed. Likewise, using dotted keys to redefine tables
+ // already defined in [table] form is not allowed.
+ if dotted && !sweet_child_of_mine.is_implicit() {
+ return Err(CustomError::DuplicateKey {
+ key: key.get().into(),
+ table: None,
+ });
+ }
+ table = sweet_child_of_mine;
+ }
+ _ => unreachable!(),
+ }
+ }
+ Ok(table)
+ }
+
+ pub(crate) fn on_std_header(
+ &mut self,
+ path: Vec<Key>,
+ trailing: std::ops::Range<usize>,
+ span: std::ops::Range<usize>,
+ ) -> Result<(), CustomError> {
+ debug_assert!(!path.is_empty());
+
+ self.finalize_table()?;
+ let leading = self
+ .trailing
+ .take()
+ .map(RawString::with_span)
+ .unwrap_or_default();
+ self.start_table(
+ path,
+ Decor::new(leading, RawString::with_span(trailing)),
+ span,
+ )?;
+
+ Ok(())
+ }
+
+ pub(crate) fn on_array_header(
+ &mut self,
+ path: Vec<Key>,
+ trailing: std::ops::Range<usize>,
+ span: std::ops::Range<usize>,
+ ) -> Result<(), CustomError> {
+ debug_assert!(!path.is_empty());
+
+ self.finalize_table()?;
+ let leading = self
+ .trailing
+ .take()
+ .map(RawString::with_span)
+ .unwrap_or_default();
+ self.start_aray_table(
+ path,
+ Decor::new(leading, RawString::with_span(trailing)),
+ span,
+ )?;
+
+ Ok(())
+ }
+}
+
+impl Default for ParseState {
+ fn default() -> Self {
+ let mut root = Table::new();
+ root.span = Some(0..0);
+ Self {
+ document: Document::new(),
+ trailing: None,
+ current_table_position: 0,
+ current_table: root,
+ current_is_array: false,
+ current_table_path: Vec::new(),
+ }
+ }
+}
diff --git a/src/parser/strings.rs b/src/parser/strings.rs
new file mode 100644
index 0000000..26f9cc2
--- /dev/null
+++ b/src/parser/strings.rs
@@ -0,0 +1,478 @@
+use std::borrow::Cow;
+use std::char;
+use std::ops::RangeInclusive;
+
+use winnow::combinator::alt;
+use winnow::combinator::cut_err;
+use winnow::combinator::delimited;
+use winnow::combinator::fail;
+use winnow::combinator::opt;
+use winnow::combinator::peek;
+use winnow::combinator::preceded;
+use winnow::combinator::repeat;
+use winnow::combinator::success;
+use winnow::combinator::terminated;
+use winnow::prelude::*;
+use winnow::stream::Stream;
+use winnow::token::any;
+use winnow::token::none_of;
+use winnow::token::one_of;
+use winnow::token::tag;
+use winnow::token::take_while;
+use winnow::trace::trace;
+
+use crate::parser::errors::CustomError;
+use crate::parser::numbers::HEXDIG;
+use crate::parser::prelude::*;
+use crate::parser::trivia::{from_utf8_unchecked, newline, ws, ws_newlines, NON_ASCII, WSCHAR};
+
+// ;; String
+
+// string = ml-basic-string / basic-string / ml-literal-string / literal-string
+pub(crate) fn string<'i>(input: &mut Input<'i>) -> PResult<Cow<'i, str>> {
+ trace(
+ "string",
+ alt((
+ ml_basic_string,
+ basic_string,
+ ml_literal_string,
+ literal_string.map(Cow::Borrowed),
+ )),
+ )
+ .parse_next(input)
+}
+
+// ;; Basic String
+
+// basic-string = quotation-mark *basic-char quotation-mark
+pub(crate) fn basic_string<'i>(input: &mut Input<'i>) -> PResult<Cow<'i, str>> {
+ trace("basic-string", |input: &mut Input<'i>| {
+ let _ = one_of(QUOTATION_MARK).parse_next(input)?;
+
+ let mut c = Cow::Borrowed("");
+ if let Some(ci) = opt(basic_chars).parse_next(input)? {
+ c = ci;
+ }
+ while let Some(ci) = opt(basic_chars).parse_next(input)? {
+ c.to_mut().push_str(&ci);
+ }
+
+ let _ = cut_err(one_of(QUOTATION_MARK))
+ .context(StrContext::Label("basic string"))
+ .parse_next(input)?;
+
+ Ok(c)
+ })
+ .parse_next(input)
+}
+
+// quotation-mark = %x22 ; "
+pub(crate) const QUOTATION_MARK: u8 = b'"';
+
+// basic-char = basic-unescaped / escaped
+fn basic_chars<'i>(input: &mut Input<'i>) -> PResult<Cow<'i, str>> {
+ alt((
+ // Deviate from the official grammar by batching the unescaped chars so we build a string a
+ // chunk at a time, rather than a `char` at a time.
+ take_while(1.., BASIC_UNESCAPED)
+ .try_map(std::str::from_utf8)
+ .map(Cow::Borrowed),
+ escaped.map(|c| Cow::Owned(String::from(c))),
+ ))
+ .parse_next(input)
+}
+
+// basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
+pub(crate) const BASIC_UNESCAPED: (
+ (u8, u8),
+ u8,
+ RangeInclusive<u8>,
+ RangeInclusive<u8>,
+ RangeInclusive<u8>,
+) = (WSCHAR, 0x21, 0x23..=0x5B, 0x5D..=0x7E, NON_ASCII);
+
+// escaped = escape escape-seq-char
+fn escaped(input: &mut Input<'_>) -> PResult<char> {
+ preceded(ESCAPE, escape_seq_char).parse_next(input)
+}
+
+// escape = %x5C ; \
+pub(crate) const ESCAPE: u8 = b'\\';
+
+// escape-seq-char = %x22 ; " quotation mark U+0022
+// escape-seq-char =/ %x5C ; \ reverse solidus U+005C
+// escape-seq-char =/ %x62 ; b backspace U+0008
+// escape-seq-char =/ %x66 ; f form feed U+000C
+// escape-seq-char =/ %x6E ; n line feed U+000A
+// escape-seq-char =/ %x72 ; r carriage return U+000D
+// escape-seq-char =/ %x74 ; t tab U+0009
+// escape-seq-char =/ %x75 4HEXDIG ; uXXXX U+XXXX
+// escape-seq-char =/ %x55 8HEXDIG ; UXXXXXXXX U+XXXXXXXX
+fn escape_seq_char(input: &mut Input<'_>) -> PResult<char> {
+ dispatch! {any;
+ b'b' => success('\u{8}'),
+ b'f' => success('\u{c}'),
+ b'n' => success('\n'),
+ b'r' => success('\r'),
+ b't' => success('\t'),
+ b'u' => cut_err(hexescape::<4>).context(StrContext::Label("unicode 4-digit hex code")),
+ b'U' => cut_err(hexescape::<8>).context(StrContext::Label("unicode 8-digit hex code")),
+ b'\\' => success('\\'),
+ b'"' => success('"'),
+ _ => {
+ cut_err(fail::<_, char, _>)
+ .context(StrContext::Label("escape sequence"))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('b')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('f')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('n')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('r')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('t')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('u')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('U')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('\\')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('"')))
+ }
+ }
+ .parse_next(input)
+}
+
+pub(crate) fn hexescape<const N: usize>(input: &mut Input<'_>) -> PResult<char> {
+ take_while(0..=N, HEXDIG)
+ .verify(|b: &[u8]| b.len() == N)
+ .map(|b: &[u8]| unsafe { from_utf8_unchecked(b, "`is_ascii_digit` filters out on-ASCII") })
+ .verify_map(|s| u32::from_str_radix(s, 16).ok())
+ .try_map(|h| char::from_u32(h).ok_or(CustomError::OutOfRange))
+ .parse_next(input)
+}
+
+// ;; Multiline Basic String
+
+// ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
+// ml-basic-string-delim
+fn ml_basic_string<'i>(input: &mut Input<'i>) -> PResult<Cow<'i, str>> {
+ trace(
+ "ml-basic-string",
+ delimited(
+ ML_BASIC_STRING_DELIM,
+ preceded(opt(newline), cut_err(ml_basic_body)),
+ cut_err(ML_BASIC_STRING_DELIM),
+ )
+ .context(StrContext::Label("multiline basic string")),
+ )
+ .parse_next(input)
+}
+
+// ml-basic-string-delim = 3quotation-mark
+pub(crate) const ML_BASIC_STRING_DELIM: &[u8] = b"\"\"\"";
+
+// ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ]
+fn ml_basic_body<'i>(input: &mut Input<'i>) -> PResult<Cow<'i, str>> {
+ let mut c = Cow::Borrowed("");
+ if let Some(ci) = opt(mlb_content).parse_next(input)? {
+ c = ci;
+ }
+ while let Some(ci) = opt(mlb_content).parse_next(input)? {
+ c.to_mut().push_str(&ci);
+ }
+
+ while let Some(qi) = opt(mlb_quotes(none_of(b'\"').value(()))).parse_next(input)? {
+ if let Some(ci) = opt(mlb_content).parse_next(input)? {
+ c.to_mut().push_str(qi);
+ c.to_mut().push_str(&ci);
+ while let Some(ci) = opt(mlb_content).parse_next(input)? {
+ c.to_mut().push_str(&ci);
+ }
+ } else {
+ break;
+ }
+ }
+
+ if let Some(qi) = opt(mlb_quotes(tag(ML_BASIC_STRING_DELIM).value(()))).parse_next(input)? {
+ c.to_mut().push_str(qi);
+ }
+
+ Ok(c)
+}
+
+// mlb-content = mlb-char / newline / mlb-escaped-nl
+// mlb-char = mlb-unescaped / escaped
+fn mlb_content<'i>(input: &mut Input<'i>) -> PResult<Cow<'i, str>> {
+ alt((
+ // Deviate from the official grammar by batching the unescaped chars so we build a string a
+ // chunk at a time, rather than a `char` at a time.
+ take_while(1.., MLB_UNESCAPED)
+ .try_map(std::str::from_utf8)
+ .map(Cow::Borrowed),
+ // Order changed fromg grammar so `escaped` can more easily `cut_err` on bad escape sequences
+ mlb_escaped_nl.map(|_| Cow::Borrowed("")),
+ escaped.map(|c| Cow::Owned(String::from(c))),
+ newline.map(|_| Cow::Borrowed("\n")),
+ ))
+ .parse_next(input)
+}
+
+// mlb-quotes = 1*2quotation-mark
+fn mlb_quotes<'i>(
+ mut term: impl winnow::Parser<Input<'i>, (), ContextError>,
+) -> impl Parser<Input<'i>, &'i str, ContextError> {
+ move |input: &mut Input<'i>| {
+ let start = input.checkpoint();
+ let res = terminated(b"\"\"", peek(term.by_ref()))
+ .map(|b| unsafe { from_utf8_unchecked(b, "`bytes` out non-ASCII") })
+ .parse_next(input);
+
+ match res {
+ Err(winnow::error::ErrMode::Backtrack(_)) => {
+ input.reset(start);
+ terminated(b"\"", peek(term.by_ref()))
+ .map(|b| unsafe { from_utf8_unchecked(b, "`bytes` out non-ASCII") })
+ .parse_next(input)
+ }
+ res => res,
+ }
+ }
+}
+
+// mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
+pub(crate) const MLB_UNESCAPED: (
+ (u8, u8),
+ u8,
+ RangeInclusive<u8>,
+ RangeInclusive<u8>,
+ RangeInclusive<u8>,
+) = (WSCHAR, 0x21, 0x23..=0x5B, 0x5D..=0x7E, NON_ASCII);
+
+// mlb-escaped-nl = escape ws newline *( wschar / newline
+// When the last non-whitespace character on a line is a \,
+// it will be trimmed along with all whitespace
+// (including newlines) up to the next non-whitespace
+// character or closing delimiter.
+fn mlb_escaped_nl(input: &mut Input<'_>) -> PResult<()> {
+ repeat(1.., (ESCAPE, ws, ws_newlines))
+ .map(|()| ())
+ .value(())
+ .parse_next(input)
+}
+
+// ;; Literal String
+
+// literal-string = apostrophe *literal-char apostrophe
+pub(crate) fn literal_string<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ trace(
+ "literal-string",
+ delimited(
+ APOSTROPHE,
+ cut_err(take_while(0.., LITERAL_CHAR)),
+ cut_err(APOSTROPHE),
+ )
+ .try_map(std::str::from_utf8)
+ .context(StrContext::Label("literal string")),
+ )
+ .parse_next(input)
+}
+
+// apostrophe = %x27 ; ' apostrophe
+pub(crate) const APOSTROPHE: u8 = b'\'';
+
+// literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
+pub(crate) const LITERAL_CHAR: (
+ u8,
+ RangeInclusive<u8>,
+ RangeInclusive<u8>,
+ RangeInclusive<u8>,
+) = (0x9, 0x20..=0x26, 0x28..=0x7E, NON_ASCII);
+
+// ;; Multiline Literal String
+
+// ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body
+// ml-literal-string-delim
+fn ml_literal_string<'i>(input: &mut Input<'i>) -> PResult<Cow<'i, str>> {
+ trace(
+ "ml-literal-string",
+ delimited(
+ (ML_LITERAL_STRING_DELIM, opt(newline)),
+ cut_err(ml_literal_body.map(|t| {
+ if t.contains("\r\n") {
+ Cow::Owned(t.replace("\r\n", "\n"))
+ } else {
+ Cow::Borrowed(t)
+ }
+ })),
+ cut_err(ML_LITERAL_STRING_DELIM),
+ )
+ .context(StrContext::Label("multiline literal string")),
+ )
+ .parse_next(input)
+}
+
+// ml-literal-string-delim = 3apostrophe
+pub(crate) const ML_LITERAL_STRING_DELIM: &[u8] = b"'''";
+
+// ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ]
+fn ml_literal_body<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ (
+ repeat(0.., mll_content).map(|()| ()),
+ repeat(
+ 0..,
+ (
+ mll_quotes(none_of(APOSTROPHE).value(())),
+ repeat(1.., mll_content).map(|()| ()),
+ ),
+ )
+ .map(|()| ()),
+ opt(mll_quotes(tag(ML_LITERAL_STRING_DELIM).value(()))),
+ )
+ .recognize()
+ .try_map(std::str::from_utf8)
+ .parse_next(input)
+}
+
+// mll-content = mll-char / newline
+fn mll_content(input: &mut Input<'_>) -> PResult<u8> {
+ alt((one_of(MLL_CHAR), newline)).parse_next(input)
+}
+
+// mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
+const MLL_CHAR: (
+ u8,
+ RangeInclusive<u8>,
+ RangeInclusive<u8>,
+ RangeInclusive<u8>,
+) = (0x9, 0x20..=0x26, 0x28..=0x7E, NON_ASCII);
+
+// mll-quotes = 1*2apostrophe
+fn mll_quotes<'i>(
+ mut term: impl winnow::Parser<Input<'i>, (), ContextError>,
+) -> impl Parser<Input<'i>, &'i str, ContextError> {
+ move |input: &mut Input<'i>| {
+ let start = input.checkpoint();
+ let res = terminated(b"''", peek(term.by_ref()))
+ .map(|b| unsafe { from_utf8_unchecked(b, "`bytes` out non-ASCII") })
+ .parse_next(input);
+
+ match res {
+ Err(winnow::error::ErrMode::Backtrack(_)) => {
+ input.reset(start);
+ terminated(b"'", peek(term.by_ref()))
+ .map(|b| unsafe { from_utf8_unchecked(b, "`bytes` out non-ASCII") })
+ .parse_next(input)
+ }
+ res => res,
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn basic_string() {
+ let input =
+ r#""I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF. \U0002070E""#;
+ let expected = "I\'m a string. \"You can quote me\". Name\tJosé\nLocation\tSF. \u{2070E}";
+ let parsed = string.parse(new_input(input));
+ assert_eq!(parsed.as_deref(), Ok(expected), "Parsing {input:?}");
+ }
+
+ #[test]
+ fn ml_basic_string() {
+ let cases = [
+ (
+ r#""""
+Roses are red
+Violets are blue""""#,
+ r#"Roses are red
+Violets are blue"#,
+ ),
+ (r#"""" \""" """"#, " \"\"\" "),
+ (r#"""" \\""""#, " \\"),
+ ];
+
+ for &(input, expected) in &cases {
+ let parsed = string.parse(new_input(input));
+ assert_eq!(parsed.as_deref(), Ok(expected), "Parsing {input:?}");
+ }
+
+ let invalid_cases = [r#"""" """#, r#"""" \""""#];
+
+ for input in &invalid_cases {
+ let parsed = string.parse(new_input(input));
+ assert!(parsed.is_err());
+ }
+ }
+
+ #[test]
+ fn ml_basic_string_escape_ws() {
+ let inputs = [
+ r#""""
+The quick brown \
+
+
+ fox jumps over \
+ the lazy dog.""""#,
+ r#""""\
+ The quick brown \
+ fox jumps over \
+ the lazy dog.\
+ """"#,
+ ];
+ for input in &inputs {
+ let expected = "The quick brown fox jumps over the lazy dog.";
+ let parsed = string.parse(new_input(input));
+ assert_eq!(parsed.as_deref(), Ok(expected), "Parsing {input:?}");
+ }
+ let empties = [
+ r#""""\
+ """"#,
+ r#""""
+\
+ \
+""""#,
+ ];
+ for input in &empties {
+ let expected = "";
+ let parsed = string.parse(new_input(input));
+ assert_eq!(parsed.as_deref(), Ok(expected), "Parsing {input:?}");
+ }
+ }
+
+ #[test]
+ fn literal_string() {
+ let inputs = [
+ r#"'C:\Users\nodejs\templates'"#,
+ r#"'\\ServerX\admin$\system32\'"#,
+ r#"'Tom "Dubs" Preston-Werner'"#,
+ r#"'<\i\c*\s*>'"#,
+ ];
+
+ for input in &inputs {
+ let expected = &input[1..input.len() - 1];
+ let parsed = string.parse(new_input(input));
+ assert_eq!(parsed.as_deref(), Ok(expected), "Parsing {input:?}");
+ }
+ }
+
+ #[test]
+ fn ml_literal_string() {
+ let inputs = [
+ r#"'''I [dw]on't need \d{2} apples'''"#,
+ r#"''''one_quote''''"#,
+ ];
+ for input in &inputs {
+ let expected = &input[3..input.len() - 3];
+ let parsed = string.parse(new_input(input));
+ assert_eq!(parsed.as_deref(), Ok(expected), "Parsing {input:?}");
+ }
+
+ let input = r#"'''
+The first newline is
+trimmed in raw strings.
+ All other whitespace
+ is preserved.
+'''"#;
+ let expected = &input[4..input.len() - 3];
+ let parsed = string.parse(new_input(input));
+ assert_eq!(parsed.as_deref(), Ok(expected), "Parsing {input:?}");
+ }
+}
diff --git a/src/parser/table.rs b/src/parser/table.rs
new file mode 100644
index 0000000..0ace0c7
--- /dev/null
+++ b/src/parser/table.rs
@@ -0,0 +1,89 @@
+use std::cell::RefCell;
+#[allow(unused_imports)]
+use std::ops::DerefMut;
+
+use winnow::combinator::cut_err;
+use winnow::combinator::delimited;
+use winnow::combinator::peek;
+use winnow::token::take;
+
+// https://github.com/rust-lang/rust/issues/41358
+use crate::parser::key::key;
+use crate::parser::prelude::*;
+use crate::parser::state::ParseState;
+use crate::parser::trivia::line_trailing;
+
+// std-table-open = %x5B ws ; [ Left square bracket
+pub(crate) const STD_TABLE_OPEN: u8 = b'[';
+// std-table-close = ws %x5D ; ] Right square bracket
+const STD_TABLE_CLOSE: u8 = b']';
+// array-table-open = %x5B.5B ws ; [[ Double left square bracket
+const ARRAY_TABLE_OPEN: &[u8] = b"[[";
+// array-table-close = ws %x5D.5D ; ]] Double right quare bracket
+const ARRAY_TABLE_CLOSE: &[u8] = b"]]";
+
+// ;; Standard Table
+
+// std-table = std-table-open key *( table-key-sep key) std-table-close
+pub(crate) fn std_table<'s, 'i>(
+ state: &'s RefCell<ParseState>,
+) -> impl Parser<Input<'i>, (), ContextError> + 's {
+ move |i: &mut Input<'i>| {
+ (
+ delimited(
+ STD_TABLE_OPEN,
+ cut_err(key),
+ cut_err(STD_TABLE_CLOSE)
+ .context(StrContext::Expected(StrContextValue::CharLiteral('.')))
+ .context(StrContext::Expected(StrContextValue::StringLiteral("]"))),
+ )
+ .with_span(),
+ cut_err(line_trailing)
+ .context(StrContext::Expected(StrContextValue::CharLiteral('\n')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('#'))),
+ )
+ .try_map(|((h, span), t)| state.borrow_mut().deref_mut().on_std_header(h, t, span))
+ .parse_next(i)
+ }
+}
+
+// ;; Array Table
+
+// array-table = array-table-open key *( table-key-sep key) array-table-close
+pub(crate) fn array_table<'s, 'i>(
+ state: &'s RefCell<ParseState>,
+) -> impl Parser<Input<'i>, (), ContextError> + 's {
+ move |i: &mut Input<'i>| {
+ (
+ delimited(
+ ARRAY_TABLE_OPEN,
+ cut_err(key),
+ cut_err(ARRAY_TABLE_CLOSE)
+ .context(StrContext::Expected(StrContextValue::CharLiteral('.')))
+ .context(StrContext::Expected(StrContextValue::StringLiteral("]]"))),
+ )
+ .with_span(),
+ cut_err(line_trailing)
+ .context(StrContext::Expected(StrContextValue::CharLiteral('\n')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('#'))),
+ )
+ .try_map(|((h, span), t)| state.borrow_mut().deref_mut().on_array_header(h, t, span))
+ .parse_next(i)
+ }
+}
+
+// ;; Table
+
+// table = std-table / array-table
+pub(crate) fn table<'s, 'i>(
+ state: &'s RefCell<ParseState>,
+) -> impl Parser<Input<'i>, (), ContextError> + 's {
+ move |i: &mut Input<'i>| {
+ dispatch!(peek::<_, &[u8],_,_>(take(2usize));
+ b"[[" => array_table(state),
+ _ => std_table(state),
+ )
+ .context(StrContext::Label("table header"))
+ .parse_next(i)
+ }
+}
diff --git a/src/parser/trivia.rs b/src/parser/trivia.rs
new file mode 100644
index 0000000..a359805
--- /dev/null
+++ b/src/parser/trivia.rs
@@ -0,0 +1,156 @@
+use std::ops::RangeInclusive;
+
+use winnow::combinator::alt;
+use winnow::combinator::eof;
+use winnow::combinator::opt;
+use winnow::combinator::repeat;
+use winnow::combinator::terminated;
+use winnow::prelude::*;
+use winnow::token::one_of;
+use winnow::token::take_while;
+
+use crate::parser::prelude::*;
+
+pub(crate) unsafe fn from_utf8_unchecked<'b>(
+ bytes: &'b [u8],
+ safety_justification: &'static str,
+) -> &'b str {
+ if cfg!(debug_assertions) {
+ // Catch problems more quickly when testing
+ std::str::from_utf8(bytes).expect(safety_justification)
+ } else {
+ std::str::from_utf8_unchecked(bytes)
+ }
+}
+
+// wschar = ( %x20 / ; Space
+// %x09 ) ; Horizontal tab
+pub(crate) const WSCHAR: (u8, u8) = (b' ', b'\t');
+
+// ws = *wschar
+pub(crate) fn ws<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ take_while(0.., WSCHAR)
+ .map(|b| unsafe { from_utf8_unchecked(b, "`is_wschar` filters out on-ASCII") })
+ .parse_next(input)
+}
+
+// non-ascii = %x80-D7FF / %xE000-10FFFF
+// - ASCII is 0xxxxxxx
+// - First byte for UTF-8 is 11xxxxxx
+// - Subsequent UTF-8 bytes are 10xxxxxx
+pub(crate) const NON_ASCII: RangeInclusive<u8> = 0x80..=0xff;
+
+// non-eol = %x09 / %x20-7E / non-ascii
+pub(crate) const NON_EOL: (u8, RangeInclusive<u8>, RangeInclusive<u8>) =
+ (0x09, 0x20..=0x7E, NON_ASCII);
+
+// comment-start-symbol = %x23 ; #
+pub(crate) const COMMENT_START_SYMBOL: u8 = b'#';
+
+// comment = comment-start-symbol *non-eol
+pub(crate) fn comment<'i>(input: &mut Input<'i>) -> PResult<&'i [u8]> {
+ (COMMENT_START_SYMBOL, take_while(0.., NON_EOL))
+ .recognize()
+ .parse_next(input)
+}
+
+// newline = ( %x0A / ; LF
+// %x0D.0A ) ; CRLF
+pub(crate) fn newline(input: &mut Input<'_>) -> PResult<u8> {
+ alt((
+ one_of(LF).value(b'\n'),
+ (one_of(CR), one_of(LF)).value(b'\n'),
+ ))
+ .parse_next(input)
+}
+pub(crate) const LF: u8 = b'\n';
+pub(crate) const CR: u8 = b'\r';
+
+// ws-newline = *( wschar / newline )
+pub(crate) fn ws_newline<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ repeat(
+ 0..,
+ alt((newline.value(&b"\n"[..]), take_while(1.., WSCHAR))),
+ )
+ .map(|()| ())
+ .recognize()
+ .map(|b| unsafe { from_utf8_unchecked(b, "`is_wschar` and `newline` filters out on-ASCII") })
+ .parse_next(input)
+}
+
+// ws-newlines = newline *( wschar / newline )
+pub(crate) fn ws_newlines<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ (newline, ws_newline)
+ .recognize()
+ .map(|b| unsafe {
+ from_utf8_unchecked(b, "`is_wschar` and `newline` filters out on-ASCII")
+ })
+ .parse_next(input)
+}
+
+// note: this rule is not present in the original grammar
+// ws-comment-newline = *( ws-newline-nonempty / comment )
+pub(crate) fn ws_comment_newline<'i>(input: &mut Input<'i>) -> PResult<&'i [u8]> {
+ repeat(
+ 0..,
+ alt((
+ repeat(
+ 1..,
+ alt((take_while(1.., WSCHAR), newline.value(&b"\n"[..]))),
+ )
+ .map(|()| ()),
+ comment.value(()),
+ )),
+ )
+ .map(|()| ())
+ .recognize()
+ .parse_next(input)
+}
+
+// note: this rule is not present in the original grammar
+// line-ending = newline / eof
+pub(crate) fn line_ending<'i>(input: &mut Input<'i>) -> PResult<&'i str> {
+ alt((newline.value("\n"), eof.value(""))).parse_next(input)
+}
+
+// note: this rule is not present in the original grammar
+// line-trailing = ws [comment] skip-line-ending
+pub(crate) fn line_trailing(input: &mut Input<'_>) -> PResult<std::ops::Range<usize>> {
+ terminated((ws, opt(comment)).span(), line_ending).parse_next(input)
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn trivia() {
+ let inputs = [
+ "",
+ r#" "#,
+ r#"
+"#,
+ r#"
+# comment
+
+# comment2
+
+
+"#,
+ r#"
+ "#,
+ r#"# comment
+# comment2
+
+
+ "#,
+ ];
+ for input in inputs {
+ dbg!(input);
+ let parsed = ws_comment_newline.parse(new_input(input));
+ assert!(parsed.is_ok(), "{:?}", parsed);
+ let parsed = parsed.unwrap();
+ assert_eq!(parsed, input.as_bytes());
+ }
+ }
+}
diff --git a/src/parser/value.rs b/src/parser/value.rs
new file mode 100644
index 0000000..14cd951
--- /dev/null
+++ b/src/parser/value.rs
@@ -0,0 +1,155 @@
+use winnow::combinator::alt;
+use winnow::combinator::fail;
+use winnow::combinator::peek;
+use winnow::token::any;
+
+use crate::parser::array::array;
+use crate::parser::datetime::date_time;
+use crate::parser::inline_table::inline_table;
+use crate::parser::numbers::{float, integer};
+use crate::parser::prelude::*;
+use crate::parser::strings::string;
+use crate::repr::{Formatted, Repr};
+use crate::value as v;
+use crate::RawString;
+use crate::Value;
+
+// val = string / boolean / array / inline-table / date-time / float / integer
+pub(crate) fn value<'i>(check: RecursionCheck) -> impl Parser<Input<'i>, v::Value, ContextError> {
+ move |input: &mut Input<'i>| {
+ dispatch!{peek(any);
+ crate::parser::strings::QUOTATION_MARK |
+ crate::parser::strings::APOSTROPHE => string.map(|s| {
+ v::Value::String(Formatted::new(
+ s.into_owned()
+ ))
+ }),
+ crate::parser::array::ARRAY_OPEN => array(check).map(v::Value::Array),
+ crate::parser::inline_table::INLINE_TABLE_OPEN => inline_table(check).map(v::Value::InlineTable),
+ // Date/number starts
+ b'+' | b'-' | b'0'..=b'9' => {
+ // Uncommon enough not to be worth optimizing at this time
+ alt((
+ date_time
+ .map(v::Value::from),
+ float
+ .map(v::Value::from),
+ integer
+ .map(v::Value::from),
+ ))
+ },
+ // Report as if they were numbers because its most likely a typo
+ b'_' => {
+ integer
+ .map(v::Value::from)
+ .context(StrContext::Expected(StrContextValue::Description("leading digit")))
+ },
+ // Report as if they were numbers because its most likely a typo
+ b'.' => {
+ float
+ .map(v::Value::from)
+ .context(StrContext::Expected(StrContextValue::Description("leading digit")))
+ },
+ b't' => {
+ crate::parser::numbers::true_.map(v::Value::from)
+ .context(StrContext::Label("string"))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('"')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('\'')))
+ },
+ b'f' => {
+ crate::parser::numbers::false_.map(v::Value::from)
+ .context(StrContext::Label("string"))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('"')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('\'')))
+ },
+ b'i' => {
+ crate::parser::numbers::inf.map(v::Value::from)
+ .context(StrContext::Label("string"))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('"')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('\'')))
+ },
+ b'n' => {
+ crate::parser::numbers::nan.map(v::Value::from)
+ .context(StrContext::Label("string"))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('"')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('\'')))
+ },
+ _ => {
+ fail
+ .context(StrContext::Label("string"))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('"')))
+ .context(StrContext::Expected(StrContextValue::CharLiteral('\'')))
+ },
+ }
+ .with_span()
+ .try_map(|(value, span)| apply_raw(value, span))
+ .parse_next(input)
+ }
+}
+
+fn apply_raw(mut val: Value, span: std::ops::Range<usize>) -> Result<Value, std::str::Utf8Error> {
+ match val {
+ Value::String(ref mut f) => {
+ let raw = RawString::with_span(span);
+ f.set_repr_unchecked(Repr::new_unchecked(raw));
+ }
+ Value::Integer(ref mut f) => {
+ let raw = RawString::with_span(span);
+ f.set_repr_unchecked(Repr::new_unchecked(raw));
+ }
+ Value::Float(ref mut f) => {
+ let raw = RawString::with_span(span);
+ f.set_repr_unchecked(Repr::new_unchecked(raw));
+ }
+ Value::Boolean(ref mut f) => {
+ let raw = RawString::with_span(span);
+ f.set_repr_unchecked(Repr::new_unchecked(raw));
+ }
+ Value::Datetime(ref mut f) => {
+ let raw = RawString::with_span(span);
+ f.set_repr_unchecked(Repr::new_unchecked(raw));
+ }
+ Value::Array(ref mut arr) => {
+ arr.span = Some(span);
+ }
+ Value::InlineTable(ref mut table) => {
+ table.span = Some(span);
+ }
+ };
+ val.decorate("", "");
+ Ok(val)
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn values() {
+ let inputs = [
+ "1979-05-27T00:32:00.999999",
+ "-239",
+ "1e200",
+ "9_224_617.445_991_228_313",
+ r#"'''I [dw]on't need \d{2} apples'''"#,
+ r#"'''
+The first newline is
+trimmed in raw strings.
+ All other whitespace
+ is preserved.
+'''"#,
+ r#""Jos\u00E9\n""#,
+ r#""\\\"\b/\f\n\r\t\u00E9\U000A0000""#,
+ r#"{ hello = "world", a = 1}"#,
+ r#"[ { x = 1, a = "2" }, {a = "a",b = "b", c = "c"} ]"#,
+ ];
+ for input in inputs {
+ dbg!(input);
+ let mut parsed = value(Default::default()).parse(new_input(input));
+ if let Ok(parsed) = &mut parsed {
+ parsed.despan(input);
+ }
+ assert_eq!(parsed.map(|a| a.to_string()), Ok(input.to_owned()));
+ }
+ }
+}
diff --git a/src/raw_string.rs b/src/raw_string.rs
new file mode 100644
index 0000000..c5961f1
--- /dev/null
+++ b/src/raw_string.rs
@@ -0,0 +1,182 @@
+use crate::InternalString;
+
+/// Opaque string storage for raw TOML; internal to `toml_edit`
+#[derive(PartialEq, Eq, Clone, Hash)]
+pub struct RawString(RawStringInner);
+
+#[derive(PartialEq, Eq, Clone, Hash)]
+enum RawStringInner {
+ Empty,
+ Explicit(InternalString),
+ Spanned(std::ops::Range<usize>),
+}
+
+impl RawString {
+ pub(crate) fn with_span(span: std::ops::Range<usize>) -> Self {
+ if span.start == span.end {
+ RawString(RawStringInner::Empty)
+ } else {
+ RawString(RawStringInner::Spanned(span))
+ }
+ }
+
+ /// Access the underlying string
+ pub fn as_str(&self) -> Option<&str> {
+ match &self.0 {
+ RawStringInner::Empty => Some(""),
+ RawStringInner::Explicit(s) => Some(s.as_str()),
+ RawStringInner::Spanned(_) => None,
+ }
+ }
+
+ pub(crate) fn to_str<'s>(&'s self, input: &'s str) -> &'s str {
+ match &self.0 {
+ RawStringInner::Empty => "",
+ RawStringInner::Explicit(s) => s.as_str(),
+ RawStringInner::Spanned(span) => input.get(span.clone()).unwrap_or_else(|| {
+ panic!("span {:?} should be in input:\n```\n{}\n```", span, input)
+ }),
+ }
+ }
+
+ pub(crate) fn to_str_with_default<'s>(
+ &'s self,
+ input: Option<&'s str>,
+ default: &'s str,
+ ) -> &'s str {
+ match &self.0 {
+ RawStringInner::Empty => "",
+ RawStringInner::Explicit(s) => s.as_str(),
+ RawStringInner::Spanned(span) => {
+ if let Some(input) = input {
+ input.get(span.clone()).unwrap_or_else(|| {
+ panic!("span {:?} should be in input:\n```\n{}\n```", span, input)
+ })
+ } else {
+ default
+ }
+ }
+ }
+ }
+
+ /// Access the underlying span
+ pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+ match &self.0 {
+ RawStringInner::Empty => None,
+ RawStringInner::Explicit(_) => None,
+ RawStringInner::Spanned(span) => Some(span.clone()),
+ }
+ }
+
+ pub(crate) fn despan(&mut self, input: &str) {
+ match &self.0 {
+ RawStringInner::Empty => {}
+ RawStringInner::Explicit(_) => {}
+ RawStringInner::Spanned(span) => {
+ *self = Self::from(input.get(span.clone()).unwrap_or_else(|| {
+ panic!("span {:?} should be in input:\n```\n{}\n```", span, input)
+ }))
+ }
+ }
+ }
+
+ pub(crate) fn encode(&self, buf: &mut dyn std::fmt::Write, input: &str) -> std::fmt::Result {
+ let raw = self.to_str(input);
+ for part in raw.split('\r') {
+ write!(buf, "{}", part)?;
+ }
+ Ok(())
+ }
+
+ pub(crate) fn encode_with_default(
+ &self,
+ buf: &mut dyn std::fmt::Write,
+ input: Option<&str>,
+ default: &str,
+ ) -> std::fmt::Result {
+ let raw = self.to_str_with_default(input, default);
+ for part in raw.split('\r') {
+ write!(buf, "{}", part)?;
+ }
+ Ok(())
+ }
+}
+
+impl Default for RawString {
+ fn default() -> Self {
+ Self(RawStringInner::Empty)
+ }
+}
+
+impl std::fmt::Debug for RawString {
+ #[inline]
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+ match &self.0 {
+ RawStringInner::Empty => write!(formatter, "empty"),
+ RawStringInner::Explicit(s) => write!(formatter, "{:?}", s),
+ RawStringInner::Spanned(s) => write!(formatter, "{:?}", s),
+ }
+ }
+}
+
+impl From<&str> for RawString {
+ #[inline]
+ fn from(s: &str) -> Self {
+ if s.is_empty() {
+ Self(RawStringInner::Empty)
+ } else {
+ InternalString::from(s).into()
+ }
+ }
+}
+
+impl From<String> for RawString {
+ #[inline]
+ fn from(s: String) -> Self {
+ if s.is_empty() {
+ Self(RawStringInner::Empty)
+ } else {
+ InternalString::from(s).into()
+ }
+ }
+}
+
+impl From<&String> for RawString {
+ #[inline]
+ fn from(s: &String) -> Self {
+ if s.is_empty() {
+ Self(RawStringInner::Empty)
+ } else {
+ InternalString::from(s).into()
+ }
+ }
+}
+
+impl From<InternalString> for RawString {
+ #[inline]
+ fn from(inner: InternalString) -> Self {
+ Self(RawStringInner::Explicit(inner))
+ }
+}
+
+impl From<&InternalString> for RawString {
+ #[inline]
+ fn from(s: &InternalString) -> Self {
+ if s.is_empty() {
+ Self(RawStringInner::Empty)
+ } else {
+ InternalString::from(s).into()
+ }
+ }
+}
+
+impl From<Box<str>> for RawString {
+ #[inline]
+ fn from(s: Box<str>) -> Self {
+ if s.is_empty() {
+ Self(RawStringInner::Empty)
+ } else {
+ InternalString::from(s).into()
+ }
+ }
+}
diff --git a/src/repr.rs b/src/repr.rs
new file mode 100644
index 0000000..d4ab6c2
--- /dev/null
+++ b/src/repr.rs
@@ -0,0 +1,253 @@
+use std::borrow::Cow;
+
+use crate::RawString;
+
+/// A value together with its `to_string` representation,
+/// including surrounding it whitespaces and comments.
+#[derive(Eq, PartialEq, Clone, Hash)]
+pub struct Formatted<T> {
+ value: T,
+ repr: Option<Repr>,
+ decor: Decor,
+}
+
+impl<T> Formatted<T>
+where
+ T: ValueRepr,
+{
+ /// Default-formatted value
+ pub fn new(value: T) -> Self {
+ Self {
+ value,
+ repr: None,
+ decor: Default::default(),
+ }
+ }
+
+ pub(crate) fn set_repr_unchecked(&mut self, repr: Repr) {
+ self.repr = Some(repr);
+ }
+
+ /// The wrapped value
+ pub fn value(&self) -> &T {
+ &self.value
+ }
+
+ /// The wrapped value
+ pub fn into_value(self) -> T {
+ self.value
+ }
+
+ /// Returns the raw representation, if available.
+ pub fn as_repr(&self) -> Option<&Repr> {
+ self.repr.as_ref()
+ }
+
+ /// Returns the default raw representation.
+ pub fn default_repr(&self) -> Repr {
+ self.value.to_repr()
+ }
+
+ /// Returns a raw representation.
+ pub fn display_repr(&self) -> Cow<str> {
+ self.as_repr()
+ .and_then(|r| r.as_raw().as_str())
+ .map(Cow::Borrowed)
+ .unwrap_or_else(|| {
+ Cow::Owned(self.default_repr().as_raw().as_str().unwrap().to_owned())
+ })
+ }
+
+ /// Returns the location within the original document
+ pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+ self.repr.as_ref().and_then(|r| r.span())
+ }
+
+ pub(crate) fn despan(&mut self, input: &str) {
+ self.decor.despan(input);
+ if let Some(repr) = &mut self.repr {
+ repr.despan(input);
+ }
+ }
+
+ /// Returns the surrounding whitespace
+ pub fn decor_mut(&mut self) -> &mut Decor {
+ &mut self.decor
+ }
+
+ /// Returns the surrounding whitespace
+ pub fn decor(&self) -> &Decor {
+ &self.decor
+ }
+
+ /// Auto formats the value.
+ pub fn fmt(&mut self) {
+ self.repr = Some(self.value.to_repr());
+ }
+}
+
+impl<T> std::fmt::Debug for Formatted<T>
+where
+ T: std::fmt::Debug,
+{
+ #[inline]
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+ let mut d = formatter.debug_struct("Formatted");
+ d.field("value", &self.value);
+ match &self.repr {
+ Some(r) => d.field("repr", r),
+ None => d.field("repr", &"default"),
+ };
+ d.field("decor", &self.decor);
+ d.finish()
+ }
+}
+
+impl<T> std::fmt::Display for Formatted<T>
+where
+ T: ValueRepr,
+{
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ crate::encode::Encode::encode(self, f, None, ("", ""))
+ }
+}
+
+pub trait ValueRepr: crate::private::Sealed {
+ /// The TOML representation of the value
+ fn to_repr(&self) -> Repr;
+}
+
+/// TOML-encoded value
+#[derive(Eq, PartialEq, Clone, Hash)]
+pub struct Repr {
+ raw_value: RawString,
+}
+
+impl Repr {
+ pub(crate) fn new_unchecked(raw: impl Into<RawString>) -> Self {
+ Repr {
+ raw_value: raw.into(),
+ }
+ }
+
+ /// Access the underlying value
+ pub fn as_raw(&self) -> &RawString {
+ &self.raw_value
+ }
+
+ /// Returns the location within the original document
+ pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+ self.raw_value.span()
+ }
+
+ pub(crate) fn despan(&mut self, input: &str) {
+ self.raw_value.despan(input)
+ }
+
+ pub(crate) fn encode(&self, buf: &mut dyn std::fmt::Write, input: &str) -> std::fmt::Result {
+ self.as_raw().encode(buf, input)
+ }
+}
+
+impl std::fmt::Debug for Repr {
+ #[inline]
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+ self.raw_value.fmt(formatter)
+ }
+}
+
+/// A prefix and suffix,
+///
+/// Including comments, whitespaces and newlines.
+#[derive(Eq, PartialEq, Clone, Default, Hash)]
+pub struct Decor {
+ prefix: Option<RawString>,
+ suffix: Option<RawString>,
+}
+
+impl Decor {
+ /// Creates a new decor from the given prefix and suffix.
+ pub fn new(prefix: impl Into<RawString>, suffix: impl Into<RawString>) -> Self {
+ Self {
+ prefix: Some(prefix.into()),
+ suffix: Some(suffix.into()),
+ }
+ }
+
+ /// Go back to default decor
+ pub fn clear(&mut self) {
+ self.prefix = None;
+ self.suffix = None;
+ }
+
+ /// Get the prefix.
+ pub fn prefix(&self) -> Option<&RawString> {
+ self.prefix.as_ref()
+ }
+
+ pub(crate) fn prefix_encode(
+ &self,
+ buf: &mut dyn std::fmt::Write,
+ input: Option<&str>,
+ default: &str,
+ ) -> std::fmt::Result {
+ if let Some(prefix) = self.prefix() {
+ prefix.encode_with_default(buf, input, default)
+ } else {
+ write!(buf, "{}", default)
+ }
+ }
+
+ /// Set the prefix.
+ pub fn set_prefix(&mut self, prefix: impl Into<RawString>) {
+ self.prefix = Some(prefix.into());
+ }
+
+ /// Get the suffix.
+ pub fn suffix(&self) -> Option<&RawString> {
+ self.suffix.as_ref()
+ }
+
+ pub(crate) fn suffix_encode(
+ &self,
+ buf: &mut dyn std::fmt::Write,
+ input: Option<&str>,
+ default: &str,
+ ) -> std::fmt::Result {
+ if let Some(suffix) = self.suffix() {
+ suffix.encode_with_default(buf, input, default)
+ } else {
+ write!(buf, "{}", default)
+ }
+ }
+
+ /// Set the suffix.
+ pub fn set_suffix(&mut self, suffix: impl Into<RawString>) {
+ self.suffix = Some(suffix.into());
+ }
+
+ pub(crate) fn despan(&mut self, input: &str) {
+ if let Some(prefix) = &mut self.prefix {
+ prefix.despan(input);
+ }
+ if let Some(suffix) = &mut self.suffix {
+ suffix.despan(input);
+ }
+ }
+}
+
+impl std::fmt::Debug for Decor {
+ #[inline]
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+ let mut d = formatter.debug_struct("Decor");
+ match &self.prefix {
+ Some(r) => d.field("prefix", r),
+ None => d.field("prefix", &"default"),
+ };
+ match &self.suffix {
+ Some(r) => d.field("suffix", r),
+ None => d.field("suffix", &"default"),
+ };
+ d.finish()
+ }
+}
diff --git a/src/ser/array.rs b/src/ser/array.rs
new file mode 100644
index 0000000..80eba8b
--- /dev/null
+++ b/src/ser/array.rs
@@ -0,0 +1,84 @@
+use super::Error;
+
+#[doc(hidden)]
+pub struct SerializeValueArray {
+ values: Vec<crate::Item>,
+}
+
+impl SerializeValueArray {
+ pub(crate) fn new() -> Self {
+ Self { values: Vec::new() }
+ }
+
+ pub(crate) fn with_capacity(len: usize) -> Self {
+ Self {
+ values: Vec::with_capacity(len),
+ }
+ }
+}
+
+impl serde::ser::SerializeSeq for SerializeValueArray {
+ type Ok = crate::Value;
+ type Error = Error;
+
+ fn serialize_element<T: ?Sized>(&mut self, value: &T) -> Result<(), Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ let value = value.serialize(super::ValueSerializer {})?;
+ self.values.push(crate::Item::Value(value));
+ Ok(())
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ Ok(crate::Value::Array(crate::Array::with_vec(self.values)))
+ }
+}
+
+impl serde::ser::SerializeTuple for SerializeValueArray {
+ type Ok = crate::Value;
+ type Error = Error;
+
+ fn serialize_element<T: ?Sized>(&mut self, value: &T) -> Result<(), Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ serde::ser::SerializeSeq::serialize_element(self, value)
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ serde::ser::SerializeSeq::end(self)
+ }
+}
+
+impl serde::ser::SerializeTupleVariant for SerializeValueArray {
+ type Ok = crate::Value;
+ type Error = Error;
+
+ fn serialize_field<T: ?Sized>(&mut self, value: &T) -> Result<(), Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ serde::ser::SerializeSeq::serialize_element(self, value)
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ serde::ser::SerializeSeq::end(self)
+ }
+}
+
+impl serde::ser::SerializeTupleStruct for SerializeValueArray {
+ type Ok = crate::Value;
+ type Error = Error;
+
+ fn serialize_field<T: ?Sized>(&mut self, value: &T) -> Result<(), Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ serde::ser::SerializeSeq::serialize_element(self, value)
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ serde::ser::SerializeSeq::end(self)
+ }
+}
diff --git a/src/ser/key.rs b/src/ser/key.rs
new file mode 100644
index 0000000..d5e381b
--- /dev/null
+++ b/src/ser/key.rs
@@ -0,0 +1,173 @@
+use crate::InternalString;
+
+use super::Error;
+
+pub(crate) struct KeySerializer;
+
+impl serde::ser::Serializer for KeySerializer {
+ type Ok = InternalString;
+ type Error = Error;
+ type SerializeSeq = serde::ser::Impossible<InternalString, Error>;
+ type SerializeTuple = serde::ser::Impossible<InternalString, Error>;
+ type SerializeTupleStruct = serde::ser::Impossible<InternalString, Error>;
+ type SerializeTupleVariant = serde::ser::Impossible<InternalString, Error>;
+ type SerializeMap = serde::ser::Impossible<InternalString, Error>;
+ type SerializeStruct = serde::ser::Impossible<InternalString, Error>;
+ type SerializeStructVariant = serde::ser::Impossible<InternalString, Error>;
+
+ fn serialize_bool(self, _v: bool) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_i8(self, _v: i8) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_i16(self, _v: i16) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_i32(self, _v: i32) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_i64(self, _v: i64) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_u8(self, _v: u8) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_u16(self, _v: u16) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_u32(self, _v: u32) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_u64(self, _v: u64) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_f32(self, _v: f32) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_f64(self, _v: f64) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_char(self, _v: char) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_str(self, value: &str) -> Result<InternalString, Self::Error> {
+ Ok(InternalString::from(value))
+ }
+
+ fn serialize_bytes(self, _value: &[u8]) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_none(self) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_some<T: ?Sized>(self, _value: &T) -> Result<InternalString, Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_unit(self) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_unit_struct(self, _name: &'static str) -> Result<InternalString, Self::Error> {
+ Err(Error::KeyNotString)
+ }
+
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ ) -> Result<InternalString, Self::Error> {
+ Ok(variant.into())
+ }
+
+ fn serialize_newtype_struct<T: ?Sized>(
+ self,
+ _name: &'static str,
+ value: &T,
+ ) -> Result<InternalString, Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_newtype_variant<T: ?Sized>(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _value: &T,
+ ) -> Result<InternalString, Self::Error>
+ where
+ T: serde::ser::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)
+ }
+}
diff --git a/src/ser/map.rs b/src/ser/map.rs
new file mode 100644
index 0000000..d743e3d
--- /dev/null
+++ b/src/ser/map.rs
@@ -0,0 +1,405 @@
+use super::{Error, KeySerializer};
+
+#[doc(hidden)]
+pub enum SerializeMap {
+ Datetime(SerializeDatetime),
+ Table(SerializeInlineTable),
+}
+
+impl SerializeMap {
+ pub(crate) fn table() -> Self {
+ Self::Table(SerializeInlineTable::new())
+ }
+
+ pub(crate) fn table_with_capacity(len: usize) -> Self {
+ Self::Table(SerializeInlineTable::with_capacity(len))
+ }
+
+ pub(crate) fn datetime() -> Self {
+ Self::Datetime(SerializeDatetime::new())
+ }
+}
+
+impl serde::ser::SerializeMap for SerializeMap {
+ type Ok = crate::Value;
+ type Error = Error;
+
+ fn serialize_key<T: ?Sized>(&mut self, input: &T) -> Result<(), Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ match self {
+ Self::Datetime(s) => s.serialize_key(input),
+ Self::Table(s) => s.serialize_key(input),
+ }
+ }
+
+ fn serialize_value<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ match self {
+ Self::Datetime(s) => s.serialize_value(value),
+ Self::Table(s) => s.serialize_value(value),
+ }
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ match self {
+ Self::Datetime(s) => s.end().map(|items| items.into()),
+ Self::Table(s) => s.end().map(|items| items.into()),
+ }
+ }
+}
+
+impl serde::ser::SerializeStruct for SerializeMap {
+ type Ok = crate::Value;
+ type Error = Error;
+
+ fn serialize_field<T: ?Sized>(
+ &mut self,
+ key: &'static str,
+ value: &T,
+ ) -> Result<(), Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ match self {
+ Self::Datetime(s) => s.serialize_field(key, value),
+ Self::Table(s) => s.serialize_field(key, value),
+ }
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ match self {
+ Self::Datetime(s) => s.end().map(|items| items.into()),
+ Self::Table(s) => s.end().map(|items| items.into()),
+ }
+ }
+}
+
+#[doc(hidden)]
+pub struct SerializeDatetime {
+ value: Option<crate::Datetime>,
+}
+
+impl SerializeDatetime {
+ pub(crate) fn new() -> Self {
+ Self { value: None }
+ }
+}
+
+impl serde::ser::SerializeMap for SerializeDatetime {
+ type Ok = crate::Datetime;
+ type Error = Error;
+
+ fn serialize_key<T: ?Sized>(&mut self, _input: &T) -> Result<(), Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ unreachable!("datetimes should only be serialized as structs, not maps")
+ }
+
+ fn serialize_value<T: ?Sized>(&mut self, _value: &T) -> Result<(), Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ unreachable!("datetimes should only be serialized as structs, not maps")
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ unreachable!("datetimes should only be serialized as structs, not maps")
+ }
+}
+
+impl serde::ser::SerializeStruct for SerializeDatetime {
+ type Ok = crate::Datetime;
+ type Error = Error;
+
+ fn serialize_field<T: ?Sized>(
+ &mut self,
+ key: &'static str,
+ value: &T,
+ ) -> Result<(), Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ if key == toml_datetime::__unstable::FIELD {
+ self.value = Some(value.serialize(DatetimeFieldSerializer::default())?);
+ }
+
+ Ok(())
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ self.value.ok_or(Error::UnsupportedNone)
+ }
+}
+
+#[doc(hidden)]
+pub struct SerializeInlineTable {
+ items: crate::table::KeyValuePairs,
+ key: Option<crate::InternalString>,
+}
+
+impl SerializeInlineTable {
+ pub(crate) fn new() -> Self {
+ Self {
+ items: Default::default(),
+ key: Default::default(),
+ }
+ }
+
+ pub(crate) fn with_capacity(len: usize) -> Self {
+ let mut s = Self::new();
+ s.items.reserve(len);
+ s
+ }
+}
+
+impl serde::ser::SerializeMap for SerializeInlineTable {
+ type Ok = crate::InlineTable;
+ type Error = Error;
+
+ fn serialize_key<T: ?Sized>(&mut self, input: &T) -> Result<(), Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ self.key = None;
+ self.key = Some(input.serialize(KeySerializer)?);
+ Ok(())
+ }
+
+ fn serialize_value<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ let res = value.serialize(super::ValueSerializer {});
+ match res {
+ Ok(item) => {
+ let key = self.key.take().unwrap();
+ let kv = crate::table::TableKeyValue::new(
+ crate::Key::new(&key),
+ crate::Item::Value(item),
+ );
+ self.items.insert(key, kv);
+ }
+ Err(e) => {
+ if e != Error::UnsupportedNone {
+ return Err(e);
+ }
+ }
+ }
+ Ok(())
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ Ok(crate::InlineTable::with_pairs(self.items))
+ }
+}
+
+impl serde::ser::SerializeStruct for SerializeInlineTable {
+ type Ok = crate::InlineTable;
+ type Error = Error;
+
+ fn serialize_field<T: ?Sized>(
+ &mut self,
+ key: &'static str,
+ value: &T,
+ ) -> Result<(), Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ let res = value.serialize(super::ValueSerializer {});
+ match res {
+ Ok(item) => {
+ let kv = crate::table::TableKeyValue::new(
+ crate::Key::new(key),
+ crate::Item::Value(item),
+ );
+ self.items.insert(crate::InternalString::from(key), kv);
+ }
+ Err(e) => {
+ if e != Error::UnsupportedNone {
+ return Err(e);
+ }
+ }
+ };
+ Ok(())
+ }
+
+ fn end(self) -> Result<Self::Ok, Self::Error> {
+ Ok(crate::InlineTable::with_pairs(self.items))
+ }
+}
+
+#[derive(Default)]
+struct DatetimeFieldSerializer {}
+
+impl serde::ser::Serializer for DatetimeFieldSerializer {
+ type Ok = toml_datetime::Datetime;
+ type Error = Error;
+ type SerializeSeq = serde::ser::Impossible<Self::Ok, Self::Error>;
+ type SerializeTuple = serde::ser::Impossible<Self::Ok, Self::Error>;
+ type SerializeTupleStruct = serde::ser::Impossible<Self::Ok, Self::Error>;
+ type SerializeTupleVariant = serde::ser::Impossible<Self::Ok, Self::Error>;
+ type SerializeMap = serde::ser::Impossible<Self::Ok, Self::Error>;
+ type SerializeStruct = serde::ser::Impossible<Self::Ok, Self::Error>;
+ type SerializeStructVariant = serde::ser::Impossible<Self::Ok, Self::Error>;
+
+ fn serialize_bool(self, _value: bool) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_i8(self, _value: i8) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_i16(self, _value: i16) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_i32(self, _value: i32) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_i64(self, _value: i64) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_u8(self, _value: u8) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_u16(self, _value: u16) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_u32(self, _value: u32) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_u64(self, _value: u64) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_f32(self, _value: f32) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_f64(self, _value: f64) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_char(self, _value: char) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> {
+ v.parse::<toml_datetime::Datetime>().map_err(Error::custom)
+ }
+
+ fn serialize_bytes(self, _value: &[u8]) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_some<T: ?Sized>(self, _value: &T) -> Result<Self::Ok, Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ ) -> Result<Self::Ok, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_newtype_struct<T: ?Sized>(
+ self,
+ _name: &'static str,
+ _value: &T,
+ ) -> Result<Self::Ok, Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_newtype_variant<T: ?Sized>(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _value: &T,
+ ) -> Result<Self::Ok, Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_tuple_struct(
+ self,
+ _name: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeTupleStruct, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_tuple_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeTupleVariant, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_struct(
+ self,
+ _name: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeStruct, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+
+ fn serialize_struct_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeStructVariant, Self::Error> {
+ Err(Error::DateInvalid)
+ }
+}
diff --git a/src/ser/mod.rs b/src/ser/mod.rs
new file mode 100644
index 0000000..2c31020
--- /dev/null
+++ b/src/ser/mod.rs
@@ -0,0 +1,165 @@
+//! Serializing Rust structures into TOML.
+//!
+//! This module contains all the Serde support for serializing Rust structures into TOML.
+
+mod array;
+mod key;
+mod map;
+mod pretty;
+mod value;
+
+pub(crate) use array::*;
+pub(crate) use key::*;
+pub(crate) use map::*;
+
+use crate::visit_mut::VisitMut;
+
+/// Errors that can occur when deserializing a type.
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum Error {
+ /// Type could not be serialized to TOML
+ UnsupportedType(Option<&'static str>),
+ /// Value was out of range for the given type
+ OutOfRange(Option<&'static str>),
+ /// `None` could not be serialized to TOML
+ UnsupportedNone,
+ /// Key was not convertable to `String` for serializing to TOML
+ KeyNotString,
+ /// A serialized date was invalid
+ DateInvalid,
+ /// Other serialization error
+ Custom(String),
+}
+
+impl Error {
+ pub(crate) fn custom<T>(msg: T) -> Self
+ where
+ T: std::fmt::Display,
+ {
+ Error::Custom(msg.to_string())
+ }
+}
+
+impl serde::ser::Error for Error {
+ fn custom<T>(msg: T) -> Self
+ where
+ T: std::fmt::Display,
+ {
+ Self::custom(msg)
+ }
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Self::UnsupportedType(Some(t)) => write!(formatter, "unsupported {t} type"),
+ Self::UnsupportedType(None) => write!(formatter, "unsupported rust type"),
+ Self::OutOfRange(Some(t)) => write!(formatter, "out-of-range value for {t} type"),
+ Self::OutOfRange(None) => write!(formatter, "out-of-range value"),
+ Self::UnsupportedNone => "unsupported None value".fmt(formatter),
+ Self::KeyNotString => "map key was not a string".fmt(formatter),
+ Self::DateInvalid => "a serialized date was invalid".fmt(formatter),
+ Self::Custom(s) => s.fmt(formatter),
+ }
+ }
+}
+
+impl From<crate::TomlError> for Error {
+ fn from(e: crate::TomlError) -> Error {
+ Self::custom(e)
+ }
+}
+
+impl From<Error> for crate::TomlError {
+ fn from(e: Error) -> crate::TomlError {
+ Self::custom(e.to_string(), None)
+ }
+}
+
+impl std::error::Error for Error {}
+
+/// Serialize the given data structure as a TOML byte vector.
+///
+/// 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_vec<T: ?Sized>(value: &T) -> Result<Vec<u8>, Error>
+where
+ T: serde::ser::Serialize,
+{
+ to_string(value).map(|e| e.into_bytes())
+}
+
+/// 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.
+///
+/// # Examples
+///
+/// ```
+/// use serde::Serialize;
+///
+/// #[derive(Serialize)]
+/// struct Config {
+/// database: Database,
+/// }
+///
+/// #[derive(Serialize)]
+/// struct Database {
+/// ip: String,
+/// port: Vec<u16>,
+/// connection_max: u32,
+/// enabled: bool,
+/// }
+///
+/// let config = Config {
+/// database: Database {
+/// ip: "192.168.1.1".to_string(),
+/// port: vec![8001, 8002, 8003],
+/// connection_max: 5000,
+/// enabled: false,
+/// },
+/// };
+///
+/// let toml = toml_edit::ser::to_string(&config).unwrap();
+/// println!("{}", toml)
+/// ```
+pub fn to_string<T: ?Sized>(value: &T) -> Result<String, Error>
+where
+ T: serde::ser::Serialize,
+{
+ to_document(value).map(|e| e.to_string())
+}
+
+/// Serialize the given data structure as a "pretty" String of TOML.
+///
+/// This is identical to `to_string` except the output string has a more
+/// "pretty" output. See `ValueSerializer::pretty` for more details.
+pub fn to_string_pretty<T: ?Sized>(value: &T) -> Result<String, Error>
+where
+ T: serde::ser::Serialize,
+{
+ let mut document = to_document(value)?;
+ pretty::Pretty.visit_document_mut(&mut document);
+ Ok(document.to_string())
+}
+
+/// Serialize the given data structure into a TOML document.
+///
+/// This would allow custom formatting to be applied, mixing with format preserving edits, etc.
+pub fn to_document<T: ?Sized>(value: &T) -> Result<crate::Document, Error>
+where
+ T: serde::ser::Serialize,
+{
+ let value = value.serialize(ValueSerializer::new())?;
+ let item = crate::Item::Value(value);
+ let root = item
+ .into_table()
+ .map_err(|_| Error::UnsupportedType(None))?;
+ Ok(root.into())
+}
+
+pub use value::ValueSerializer;
diff --git a/src/ser/pretty.rs b/src/ser/pretty.rs
new file mode 100644
index 0000000..2c22f68
--- /dev/null
+++ b/src/ser/pretty.rs
@@ -0,0 +1,45 @@
+pub(crate) struct Pretty;
+
+impl crate::visit_mut::VisitMut for Pretty {
+ fn visit_document_mut(&mut self, node: &mut crate::Document) {
+ crate::visit_mut::visit_document_mut(self, node);
+ }
+
+ fn visit_item_mut(&mut self, node: &mut crate::Item) {
+ node.make_item();
+
+ crate::visit_mut::visit_item_mut(self, node);
+ }
+
+ fn visit_table_mut(&mut self, node: &mut crate::Table) {
+ node.decor_mut().clear();
+
+ // Empty tables could be semantically meaningful, so make sure they are not implicit
+ if !node.is_empty() {
+ node.set_implicit(true);
+ }
+
+ crate::visit_mut::visit_table_mut(self, node);
+ }
+
+ fn visit_value_mut(&mut self, node: &mut crate::Value) {
+ node.decor_mut().clear();
+
+ crate::visit_mut::visit_value_mut(self, node);
+ }
+
+ fn visit_array_mut(&mut self, node: &mut crate::Array) {
+ crate::visit_mut::visit_array_mut(self, node);
+
+ if (0..=1).contains(&node.len()) {
+ node.set_trailing("");
+ node.set_trailing_comma(false);
+ } else {
+ for item in node.iter_mut() {
+ item.decor_mut().set_prefix("\n ");
+ }
+ node.set_trailing("\n");
+ node.set_trailing_comma(true);
+ }
+ }
+}
diff --git a/src/ser/value.rs b/src/ser/value.rs
new file mode 100644
index 0000000..d29390a
--- /dev/null
+++ b/src/ser/value.rs
@@ -0,0 +1,243 @@
+use super::Error;
+
+/// Serialization for TOML [values][crate::Value].
+///
+/// This structure implements serialization support for TOML to serialize an
+/// arbitrary type to TOML. Note that the TOML format does not support all
+/// datatypes in Rust, such as enums, tuples, and tuple structs. These types
+/// will generate an error when serialized.
+///
+/// Currently a serializer always writes its output to an in-memory `String`,
+/// which is passed in when creating the serializer itself.
+///
+/// # Examples
+///
+/// ```
+/// use serde::Serialize;
+///
+/// #[derive(Serialize)]
+/// struct Config {
+/// database: Database,
+/// }
+///
+/// #[derive(Serialize)]
+/// struct Database {
+/// ip: String,
+/// port: Vec<u16>,
+/// connection_max: u32,
+/// enabled: bool,
+/// }
+///
+/// let config = Config {
+/// database: Database {
+/// ip: "192.168.1.1".to_string(),
+/// port: vec![8001, 8002, 8003],
+/// connection_max: 5000,
+/// enabled: false,
+/// },
+/// };
+///
+/// let value = serde::Serialize::serialize(
+/// &config,
+/// toml_edit::ser::ValueSerializer::new()
+/// ).unwrap();
+/// println!("{}", value)
+/// ```
+#[derive(Default)]
+#[non_exhaustive]
+pub struct ValueSerializer {}
+
+impl ValueSerializer {
+ /// Creates a new serializer generate a TOML document.
+ pub fn new() -> Self {
+ Self {}
+ }
+}
+
+impl serde::ser::Serializer for ValueSerializer {
+ type Ok = crate::Value;
+ type Error = Error;
+ type SerializeSeq = super::SerializeValueArray;
+ type SerializeTuple = super::SerializeValueArray;
+ type SerializeTupleStruct = super::SerializeValueArray;
+ type SerializeTupleVariant = super::SerializeValueArray;
+ type SerializeMap = super::SerializeMap;
+ type SerializeStruct = super::SerializeMap;
+ type SerializeStructVariant = serde::ser::Impossible<Self::Ok, Self::Error>;
+
+ fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error> {
+ Ok(v.into())
+ }
+
+ fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error> {
+ self.serialize_i64(v as i64)
+ }
+
+ fn serialize_i16(self, v: i16) -> Result<Self::Ok, Self::Error> {
+ self.serialize_i64(v as i64)
+ }
+
+ fn serialize_i32(self, v: i32) -> Result<Self::Ok, Self::Error> {
+ self.serialize_i64(v as i64)
+ }
+
+ fn serialize_i64(self, v: i64) -> Result<Self::Ok, Self::Error> {
+ Ok(v.into())
+ }
+
+ fn serialize_u8(self, v: u8) -> Result<Self::Ok, Self::Error> {
+ self.serialize_i64(v as i64)
+ }
+
+ fn serialize_u16(self, v: u16) -> Result<Self::Ok, Self::Error> {
+ self.serialize_i64(v as i64)
+ }
+
+ fn serialize_u32(self, v: u32) -> Result<Self::Ok, Self::Error> {
+ self.serialize_i64(v as i64)
+ }
+
+ fn serialize_u64(self, v: u64) -> Result<Self::Ok, Self::Error> {
+ let v: i64 = v
+ .try_into()
+ .map_err(|_err| Error::OutOfRange(Some("u64")))?;
+ self.serialize_i64(v)
+ }
+
+ fn serialize_f32(self, v: f32) -> Result<Self::Ok, Self::Error> {
+ self.serialize_f64(v as f64)
+ }
+
+ fn serialize_f64(self, v: f64) -> Result<Self::Ok, Self::Error> {
+ Ok(v.into())
+ }
+
+ fn serialize_char(self, v: char) -> Result<Self::Ok, Self::Error> {
+ let mut buf = [0; 4];
+ self.serialize_str(v.encode_utf8(&mut buf))
+ }
+
+ fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> {
+ Ok(v.into())
+ }
+
+ fn serialize_bytes(self, value: &[u8]) -> Result<Self::Ok, Self::Error> {
+ use serde::ser::Serialize;
+ value.serialize(self)
+ }
+
+ fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
+ Err(Error::UnsupportedNone)
+ }
+
+ fn serialize_some<T: ?Sized>(self, value: &T) -> Result<Self::Ok, Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
+ Err(Error::UnsupportedType(Some("unit")))
+ }
+
+ fn serialize_unit_struct(self, name: &'static str) -> Result<Self::Ok, Self::Error> {
+ Err(Error::UnsupportedType(Some(name)))
+ }
+
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ ) -> Result<Self::Ok, Self::Error> {
+ self.serialize_str(variant)
+ }
+
+ fn serialize_newtype_struct<T: ?Sized>(
+ self,
+ _name: &'static str,
+ value: &T,
+ ) -> Result<Self::Ok, Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_newtype_variant<T: ?Sized>(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ value: &T,
+ ) -> Result<Self::Ok, Self::Error>
+ where
+ T: serde::ser::Serialize,
+ {
+ let value = value.serialize(self)?;
+ let mut table = crate::InlineTable::new();
+ table.insert(variant, value);
+ Ok(table.into())
+ }
+
+ fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
+ let serializer = match len {
+ Some(len) => super::SerializeValueArray::with_capacity(len),
+ None => super::SerializeValueArray::new(),
+ };
+ Ok(serializer)
+ }
+
+ 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> {
+ self.serialize_seq(Some(len))
+ }
+
+ fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
+ let serializer = match len {
+ Some(len) => super::SerializeMap::table_with_capacity(len),
+ None => super::SerializeMap::table(),
+ };
+ Ok(serializer)
+ }
+
+ fn serialize_struct(
+ self,
+ name: &'static str,
+ len: usize,
+ ) -> Result<Self::SerializeStruct, Self::Error> {
+ if name == toml_datetime::__unstable::NAME {
+ Ok(super::SerializeMap::datetime())
+ } else {
+ self.serialize_map(Some(len))
+ }
+ }
+
+ fn serialize_struct_variant(
+ self,
+ name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeStructVariant, Self::Error> {
+ Err(Error::UnsupportedType(Some(name)))
+ }
+}
diff --git a/src/table.rs b/src/table.rs
new file mode 100644
index 0000000..45d6d61
--- /dev/null
+++ b/src/table.rs
@@ -0,0 +1,757 @@
+use std::iter::FromIterator;
+
+use indexmap::map::IndexMap;
+
+use crate::key::Key;
+use crate::repr::Decor;
+use crate::value::DEFAULT_VALUE_DECOR;
+use crate::{InlineTable, InternalString, Item, KeyMut, Value};
+
+/// Type representing a TOML non-inline table
+#[derive(Clone, Debug, Default)]
+pub struct Table {
+ // Comments/spaces before and after the header
+ pub(crate) decor: Decor,
+ // Whether to hide an empty table
+ pub(crate) implicit: bool,
+ // Whether this is a proxy for dotted keys
+ pub(crate) dotted: bool,
+ // Used for putting tables back in their original order when serialising.
+ //
+ // `None` for user created tables (can be overridden with `set_position`)
+ doc_position: Option<usize>,
+ pub(crate) span: Option<std::ops::Range<usize>>,
+ pub(crate) items: KeyValuePairs,
+}
+
+/// Constructors
+///
+/// See also `FromIterator`
+impl Table {
+ /// Creates an empty table.
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ pub(crate) fn with_pos(doc_position: Option<usize>) -> Self {
+ Self {
+ doc_position,
+ ..Default::default()
+ }
+ }
+
+ pub(crate) fn with_pairs(items: KeyValuePairs) -> Self {
+ Self {
+ items,
+ ..Default::default()
+ }
+ }
+
+ /// Convert to an inline table
+ pub fn into_inline_table(mut self) -> InlineTable {
+ for (_, kv) in self.items.iter_mut() {
+ kv.value.make_value();
+ }
+ let mut t = InlineTable::with_pairs(self.items);
+ t.fmt();
+ t
+ }
+}
+
+/// Formatting
+impl Table {
+ /// Get key/values for values that are visually children of this table
+ ///
+ /// For example, this will return dotted keys
+ pub fn get_values(&self) -> Vec<(Vec<&Key>, &Value)> {
+ let mut values = Vec::new();
+ let root = Vec::new();
+ self.append_values(&root, &mut values);
+ values
+ }
+
+ fn append_values<'s, 'c>(
+ &'s self,
+ parent: &[&'s Key],
+ values: &'c mut Vec<(Vec<&'s Key>, &'s Value)>,
+ ) {
+ for value in self.items.values() {
+ let mut path = parent.to_vec();
+ path.push(&value.key);
+ match &value.value {
+ Item::Table(table) if table.is_dotted() => {
+ table.append_values(&path, values);
+ }
+ Item::Value(value) => {
+ if let Some(table) = value.as_inline_table() {
+ if table.is_dotted() {
+ table.append_values(&path, values);
+ } else {
+ values.push((path, value));
+ }
+ } else {
+ values.push((path, value));
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+
+ /// Auto formats the table.
+ pub fn fmt(&mut self) {
+ decorate_table(self);
+ }
+
+ /// Sorts Key/Value Pairs of the table.
+ ///
+ /// Doesn't affect subtables or subarrays.
+ pub fn sort_values(&mut self) {
+ // Assuming standard tables have their doc_position set and this won't negatively impact them
+ self.items.sort_keys();
+ for kv in self.items.values_mut() {
+ match &mut kv.value {
+ Item::Table(table) if table.is_dotted() => {
+ table.sort_values();
+ }
+ _ => {}
+ }
+ }
+ }
+
+ /// Sort Key/Value Pairs of the table using the using the comparison function `compare`.
+ ///
+ /// The comparison function receives two key and value pairs to compare (you can sort by keys or
+ /// values or their combination as needed).
+ pub fn sort_values_by<F>(&mut self, mut compare: F)
+ where
+ F: FnMut(&Key, &Item, &Key, &Item) -> std::cmp::Ordering,
+ {
+ self.sort_values_by_internal(&mut compare);
+ }
+
+ fn sort_values_by_internal<F>(&mut self, compare: &mut F)
+ where
+ F: FnMut(&Key, &Item, &Key, &Item) -> std::cmp::Ordering,
+ {
+ let modified_cmp = |_: &InternalString,
+ val1: &TableKeyValue,
+ _: &InternalString,
+ val2: &TableKeyValue|
+ -> std::cmp::Ordering {
+ compare(&val1.key, &val1.value, &val2.key, &val2.value)
+ };
+
+ self.items.sort_by(modified_cmp);
+
+ for kv in self.items.values_mut() {
+ match &mut kv.value {
+ Item::Table(table) if table.is_dotted() => {
+ table.sort_values_by_internal(compare);
+ }
+ _ => {}
+ }
+ }
+ }
+
+ /// If a table has no key/value pairs and implicit, it will not be displayed.
+ ///
+ /// # Examples
+ ///
+ /// ```notrust
+ /// [target."x86_64/windows.json".dependencies]
+ /// ```
+ ///
+ /// In the document above, tables `target` and `target."x86_64/windows.json"` are implicit.
+ ///
+ /// ```
+ /// use toml_edit::Document;
+ /// let mut doc = "[a]\n[a.b]\n".parse::<Document>().expect("invalid toml");
+ ///
+ /// doc["a"].as_table_mut().unwrap().set_implicit(true);
+ /// assert_eq!(doc.to_string(), "[a.b]\n");
+ /// ```
+ pub fn set_implicit(&mut self, implicit: bool) {
+ self.implicit = implicit;
+ }
+
+ /// If a table has no key/value pairs and implicit, it will not be displayed.
+ pub fn is_implicit(&self) -> bool {
+ self.implicit
+ }
+
+ /// Change this table's dotted status
+ pub fn set_dotted(&mut self, yes: bool) {
+ self.dotted = yes;
+ }
+
+ /// Check if this is a wrapper for dotted keys, rather than a standard table
+ pub fn is_dotted(&self) -> bool {
+ self.dotted
+ }
+
+ /// Sets the position of the `Table` within the `Document`.
+ pub fn set_position(&mut self, doc_position: usize) {
+ self.doc_position = Some(doc_position);
+ }
+
+ /// The position of the `Table` within the `Document`.
+ ///
+ /// Returns `None` if the `Table` was created manually (i.e. not via parsing)
+ /// in which case its position is set automatically. This can be overridden with
+ /// [`Table::set_position`].
+ pub fn position(&self) -> Option<usize> {
+ self.doc_position
+ }
+
+ /// Returns the surrounding whitespace
+ pub fn decor_mut(&mut self) -> &mut Decor {
+ &mut self.decor
+ }
+
+ /// Returns the decor associated with a given key of the table.
+ pub fn decor(&self) -> &Decor {
+ &self.decor
+ }
+
+ /// Returns the decor associated with a given key of the table.
+ pub fn key_decor_mut(&mut self, key: &str) -> Option<&mut Decor> {
+ self.items.get_mut(key).map(|kv| &mut kv.key.decor)
+ }
+
+ /// Returns the decor associated with a given key of the table.
+ pub fn key_decor(&self, key: &str) -> Option<&Decor> {
+ self.items.get(key).map(|kv| &kv.key.decor)
+ }
+
+ /// Returns the location within the original document
+ pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+ self.span.clone()
+ }
+
+ pub(crate) fn despan(&mut self, input: &str) {
+ self.span = None;
+ self.decor.despan(input);
+ for kv in self.items.values_mut() {
+ kv.key.despan(input);
+ kv.value.despan(input);
+ }
+ }
+}
+
+impl Table {
+ /// Returns an iterator over all key/value pairs, including empty.
+ pub fn iter(&self) -> Iter<'_> {
+ Box::new(
+ self.items
+ .iter()
+ .filter(|(_, kv)| !kv.value.is_none())
+ .map(|(key, kv)| (&key[..], &kv.value)),
+ )
+ }
+
+ /// Returns an mutable iterator over all key/value pairs, including empty.
+ pub fn iter_mut(&mut self) -> IterMut<'_> {
+ Box::new(
+ self.items
+ .iter_mut()
+ .filter(|(_, kv)| !kv.value.is_none())
+ .map(|(_, kv)| (kv.key.as_mut(), &mut kv.value)),
+ )
+ }
+
+ /// Returns the number of non-empty items in the table.
+ pub fn len(&self) -> usize {
+ self.items.iter().filter(|i| !(i.1).value.is_none()).count()
+ }
+
+ /// Returns true if the table is empty.
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// Clears the table, removing all key-value pairs. Keeps the allocated memory for reuse.
+ pub fn clear(&mut self) {
+ self.items.clear()
+ }
+
+ /// Gets the given key's corresponding entry in the Table for in-place manipulation.
+ pub fn entry<'a>(&'a mut self, key: &str) -> Entry<'a> {
+ // Accept a `&str` rather than an owned type to keep `InternalString`, well, internal
+ match self.items.entry(key.into()) {
+ indexmap::map::Entry::Occupied(entry) => Entry::Occupied(OccupiedEntry { entry }),
+ indexmap::map::Entry::Vacant(entry) => Entry::Vacant(VacantEntry { entry, key: None }),
+ }
+ }
+
+ /// Gets the given key's corresponding entry in the Table for in-place manipulation.
+ pub fn entry_format<'a>(&'a mut self, key: &Key) -> Entry<'a> {
+ // Accept a `&Key` to be consistent with `entry`
+ match self.items.entry(key.get().into()) {
+ indexmap::map::Entry::Occupied(entry) => Entry::Occupied(OccupiedEntry { entry }),
+ indexmap::map::Entry::Vacant(entry) => Entry::Vacant(VacantEntry {
+ entry,
+ key: Some(key.to_owned()),
+ }),
+ }
+ }
+
+ /// Returns an optional reference to an item given the key.
+ pub fn get<'a>(&'a self, key: &str) -> Option<&'a Item> {
+ self.items.get(key).and_then(|kv| {
+ if !kv.value.is_none() {
+ Some(&kv.value)
+ } else {
+ None
+ }
+ })
+ }
+
+ /// Returns an optional mutable reference to an item given the key.
+ pub fn get_mut<'a>(&'a mut self, key: &str) -> Option<&'a mut Item> {
+ self.items.get_mut(key).and_then(|kv| {
+ if !kv.value.is_none() {
+ Some(&mut kv.value)
+ } else {
+ None
+ }
+ })
+ }
+
+ /// Return references to the key-value pair stored for key, if it is present, else None.
+ pub fn get_key_value<'a>(&'a self, key: &str) -> Option<(&'a Key, &'a Item)> {
+ self.items.get(key).and_then(|kv| {
+ if !kv.value.is_none() {
+ Some((&kv.key, &kv.value))
+ } else {
+ None
+ }
+ })
+ }
+
+ /// Return mutable references to the key-value pair stored for key, if it is present, else None.
+ pub fn get_key_value_mut<'a>(&'a mut self, key: &str) -> Option<(KeyMut<'a>, &'a mut Item)> {
+ self.items.get_mut(key).and_then(|kv| {
+ if !kv.value.is_none() {
+ Some((kv.key.as_mut(), &mut kv.value))
+ } else {
+ None
+ }
+ })
+ }
+
+ /// Returns true if the table contains an item with the given key.
+ pub fn contains_key(&self, key: &str) -> bool {
+ if let Some(kv) = self.items.get(key) {
+ !kv.value.is_none()
+ } else {
+ false
+ }
+ }
+
+ /// Returns true if the table contains a table with the given key.
+ pub fn contains_table(&self, key: &str) -> bool {
+ if let Some(kv) = self.items.get(key) {
+ kv.value.is_table()
+ } else {
+ false
+ }
+ }
+
+ /// Returns true if the table contains a value with the given key.
+ pub fn contains_value(&self, key: &str) -> bool {
+ if let Some(kv) = self.items.get(key) {
+ kv.value.is_value()
+ } else {
+ false
+ }
+ }
+
+ /// Returns true if the table contains an array of tables with the given key.
+ pub fn contains_array_of_tables(&self, key: &str) -> bool {
+ if let Some(kv) = self.items.get(key) {
+ kv.value.is_array_of_tables()
+ } else {
+ false
+ }
+ }
+
+ /// Inserts a key-value pair into the map.
+ pub fn insert(&mut self, key: &str, item: Item) -> Option<Item> {
+ let kv = TableKeyValue::new(Key::new(key), item);
+ self.items.insert(key.into(), kv).map(|kv| kv.value)
+ }
+
+ /// Inserts a key-value pair into the map.
+ pub fn insert_formatted(&mut self, key: &Key, item: Item) -> Option<Item> {
+ let kv = TableKeyValue::new(key.to_owned(), item);
+ self.items.insert(key.get().into(), kv).map(|kv| kv.value)
+ }
+
+ /// Removes an item given the key.
+ pub fn remove(&mut self, key: &str) -> Option<Item> {
+ self.items.shift_remove(key).map(|kv| kv.value)
+ }
+
+ /// Removes a key from the map, returning the stored key and value if the key was previously in the map.
+ pub fn remove_entry(&mut self, key: &str) -> Option<(Key, Item)> {
+ self.items.shift_remove(key).map(|kv| (kv.key, kv.value))
+ }
+
+ /// Retains only the elements specified by the `keep` predicate.
+ ///
+ /// In other words, remove all pairs `(key, item)` for which
+ /// `keep(&key, &mut item)` returns `false`.
+ ///
+ /// The elements are visited in iteration order.
+ pub fn retain<F>(&mut self, mut keep: F)
+ where
+ F: FnMut(&str, &mut Item) -> bool,
+ {
+ self.items
+ .retain(|key, key_value| keep(key, &mut key_value.value));
+ }
+}
+
+impl std::fmt::Display for Table {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ use crate::encode::Encode;
+ let children = self.get_values();
+ // print table body
+ for (key_path, value) in children {
+ key_path.as_slice().encode(f, None, DEFAULT_KEY_DECOR)?;
+ write!(f, "=")?;
+ value.encode(f, None, DEFAULT_VALUE_DECOR)?;
+ writeln!(f)?;
+ }
+ Ok(())
+ }
+}
+
+impl<K: Into<Key>, V: Into<Value>> Extend<(K, V)> for Table {
+ fn extend<T: IntoIterator<Item = (K, V)>>(&mut self, iter: T) {
+ for (key, value) in iter {
+ let key = key.into();
+ let value = Item::Value(value.into());
+ let value = TableKeyValue::new(key, value);
+ self.items.insert(value.key.get().into(), value);
+ }
+ }
+}
+
+impl<K: Into<Key>, V: Into<Value>> FromIterator<(K, V)> for Table {
+ fn from_iter<I>(iter: I) -> Self
+ where
+ I: IntoIterator<Item = (K, V)>,
+ {
+ let mut table = Table::new();
+ table.extend(iter);
+ table
+ }
+}
+
+impl IntoIterator for Table {
+ type Item = (InternalString, Item);
+ type IntoIter = IntoIter;
+
+ fn into_iter(self) -> Self::IntoIter {
+ Box::new(self.items.into_iter().map(|(k, kv)| (k, kv.value)))
+ }
+}
+
+impl<'s> IntoIterator for &'s Table {
+ type Item = (&'s str, &'s Item);
+ type IntoIter = Iter<'s>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter()
+ }
+}
+
+pub(crate) type KeyValuePairs = IndexMap<InternalString, TableKeyValue>;
+
+fn decorate_table(table: &mut Table) {
+ for (key_decor, value) in table
+ .items
+ .iter_mut()
+ .filter(|&(_, ref kv)| kv.value.is_value())
+ .map(|(_, kv)| (&mut kv.key.decor, kv.value.as_value_mut().unwrap()))
+ {
+ key_decor.clear();
+ value.decor_mut().clear();
+ }
+}
+
+// `key1 = value1`
+pub(crate) const DEFAULT_KEY_DECOR: (&str, &str) = ("", " ");
+pub(crate) const DEFAULT_TABLE_DECOR: (&str, &str) = ("\n", "");
+pub(crate) const DEFAULT_KEY_PATH_DECOR: (&str, &str) = ("", "");
+
+#[derive(Debug, Clone)]
+pub(crate) struct TableKeyValue {
+ pub(crate) key: Key,
+ pub(crate) value: Item,
+}
+
+impl TableKeyValue {
+ pub(crate) fn new(key: Key, value: Item) -> Self {
+ TableKeyValue { key, value }
+ }
+}
+
+/// An owned iterator type over `Table`'s key/value pairs.
+pub type IntoIter = Box<dyn Iterator<Item = (InternalString, Item)>>;
+/// An iterator type over `Table`'s key/value pairs.
+pub type Iter<'a> = Box<dyn Iterator<Item = (&'a str, &'a Item)> + 'a>;
+/// A mutable iterator type over `Table`'s key/value pairs.
+pub type IterMut<'a> = Box<dyn Iterator<Item = (KeyMut<'a>, &'a mut Item)> + 'a>;
+
+/// This trait represents either a `Table`, or an `InlineTable`.
+pub trait TableLike: crate::private::Sealed {
+ /// Returns an iterator over key/value pairs.
+ fn iter(&self) -> Iter<'_>;
+ /// Returns an mutable iterator over all key/value pairs, including empty.
+ fn iter_mut(&mut self) -> IterMut<'_>;
+ /// Returns the number of nonempty items.
+ fn len(&self) -> usize {
+ self.iter().filter(|&(_, v)| !v.is_none()).count()
+ }
+ /// Returns true if the table is empty.
+ fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+ /// Clears the table, removing all key-value pairs. Keeps the allocated memory for reuse.
+ fn clear(&mut self);
+ /// Gets the given key's corresponding entry in the Table for in-place manipulation.
+ fn entry<'a>(&'a mut self, key: &str) -> Entry<'a>;
+ /// Gets the given key's corresponding entry in the Table for in-place manipulation.
+ fn entry_format<'a>(&'a mut self, key: &Key) -> Entry<'a>;
+ /// Returns an optional reference to an item given the key.
+ fn get<'s>(&'s self, key: &str) -> Option<&'s Item>;
+ /// Returns an optional mutable reference to an item given the key.
+ fn get_mut<'s>(&'s mut self, key: &str) -> Option<&'s mut Item>;
+ /// Return references to the key-value pair stored for key, if it is present, else None.
+ fn get_key_value<'a>(&'a self, key: &str) -> Option<(&'a Key, &'a Item)>;
+ /// Return mutable references to the key-value pair stored for key, if it is present, else None.
+ fn get_key_value_mut<'a>(&'a mut self, key: &str) -> Option<(KeyMut<'a>, &'a mut Item)>;
+ /// Returns true if the table contains an item with the given key.
+ fn contains_key(&self, key: &str) -> bool;
+ /// Inserts a key-value pair into the map.
+ fn insert(&mut self, key: &str, value: Item) -> Option<Item>;
+ /// Removes an item given the key.
+ fn remove(&mut self, key: &str) -> Option<Item>;
+
+ /// Get key/values for values that are visually children of this table
+ ///
+ /// For example, this will return dotted keys
+ fn get_values(&self) -> Vec<(Vec<&Key>, &Value)>;
+
+ /// Auto formats the table.
+ fn fmt(&mut self);
+ /// Sorts Key/Value Pairs of the table.
+ ///
+ /// Doesn't affect subtables or subarrays.
+ fn sort_values(&mut self);
+ /// Change this table's dotted status
+ fn set_dotted(&mut self, yes: bool);
+ /// Check if this is a wrapper for dotted keys, rather than a standard table
+ fn is_dotted(&self) -> bool;
+
+ /// Returns the decor associated with a given key of the table.
+ fn key_decor_mut(&mut self, key: &str) -> Option<&mut Decor>;
+ /// Returns the decor associated with a given key of the table.
+ fn key_decor(&self, key: &str) -> Option<&Decor>;
+}
+
+impl TableLike for Table {
+ fn iter(&self) -> Iter<'_> {
+ self.iter()
+ }
+ fn iter_mut(&mut self) -> IterMut<'_> {
+ self.iter_mut()
+ }
+ fn clear(&mut self) {
+ self.clear();
+ }
+ fn entry<'a>(&'a mut self, key: &str) -> Entry<'a> {
+ self.entry(key)
+ }
+ fn entry_format<'a>(&'a mut self, key: &Key) -> Entry<'a> {
+ self.entry_format(key)
+ }
+ fn get<'s>(&'s self, key: &str) -> Option<&'s Item> {
+ self.get(key)
+ }
+ fn get_mut<'s>(&'s mut self, key: &str) -> Option<&'s mut Item> {
+ self.get_mut(key)
+ }
+ fn get_key_value<'a>(&'a self, key: &str) -> Option<(&'a Key, &'a Item)> {
+ self.get_key_value(key)
+ }
+ fn get_key_value_mut<'a>(&'a mut self, key: &str) -> Option<(KeyMut<'a>, &'a mut Item)> {
+ self.get_key_value_mut(key)
+ }
+ fn contains_key(&self, key: &str) -> bool {
+ self.contains_key(key)
+ }
+ fn insert(&mut self, key: &str, value: Item) -> Option<Item> {
+ self.insert(key, value)
+ }
+ fn remove(&mut self, key: &str) -> Option<Item> {
+ self.remove(key)
+ }
+
+ fn get_values(&self) -> Vec<(Vec<&Key>, &Value)> {
+ self.get_values()
+ }
+ fn fmt(&mut self) {
+ self.fmt()
+ }
+ fn sort_values(&mut self) {
+ self.sort_values()
+ }
+ fn is_dotted(&self) -> bool {
+ self.is_dotted()
+ }
+ fn set_dotted(&mut self, yes: bool) {
+ self.set_dotted(yes)
+ }
+
+ fn key_decor_mut(&mut self, key: &str) -> Option<&mut Decor> {
+ self.key_decor_mut(key)
+ }
+ fn key_decor(&self, key: &str) -> Option<&Decor> {
+ self.key_decor(key)
+ }
+}
+
+/// A view into a single location in a map, which may be vacant or occupied.
+pub enum Entry<'a> {
+ /// An occupied Entry.
+ Occupied(OccupiedEntry<'a>),
+ /// A vacant Entry.
+ Vacant(VacantEntry<'a>),
+}
+
+impl<'a> Entry<'a> {
+ /// Returns the entry key
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use toml_edit::Table;
+ ///
+ /// let mut map = Table::new();
+ ///
+ /// assert_eq!("hello", map.entry("hello").key());
+ /// ```
+ pub fn key(&self) -> &str {
+ match self {
+ Entry::Occupied(e) => e.key(),
+ Entry::Vacant(e) => e.key(),
+ }
+ }
+
+ /// Ensures a value is in the entry by inserting the default if empty, and returns
+ /// a mutable reference to the value in the entry.
+ pub fn or_insert(self, default: Item) -> &'a mut Item {
+ match self {
+ Entry::Occupied(entry) => entry.into_mut(),
+ Entry::Vacant(entry) => entry.insert(default),
+ }
+ }
+
+ /// Ensures a value is in the entry by inserting the result of the default function if empty,
+ /// and returns a mutable reference to the value in the entry.
+ pub fn or_insert_with<F: FnOnce() -> Item>(self, default: F) -> &'a mut Item {
+ match self {
+ Entry::Occupied(entry) => entry.into_mut(),
+ Entry::Vacant(entry) => entry.insert(default()),
+ }
+ }
+}
+
+/// A view into a single occupied location in a `IndexMap`.
+pub struct OccupiedEntry<'a> {
+ pub(crate) entry: indexmap::map::OccupiedEntry<'a, InternalString, TableKeyValue>,
+}
+
+impl<'a> OccupiedEntry<'a> {
+ /// Gets a reference to the entry key
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use toml_edit::Table;
+ ///
+ /// let mut map = Table::new();
+ ///
+ /// assert_eq!("foo", map.entry("foo").key());
+ /// ```
+ pub fn key(&self) -> &str {
+ self.entry.key().as_str()
+ }
+
+ /// Gets a mutable reference to the entry key
+ pub fn key_mut(&mut self) -> KeyMut<'_> {
+ self.entry.get_mut().key.as_mut()
+ }
+
+ /// Gets a reference to the value in the entry.
+ pub fn get(&self) -> &Item {
+ &self.entry.get().value
+ }
+
+ /// Gets a mutable reference to the value in the entry.
+ pub fn get_mut(&mut self) -> &mut Item {
+ &mut self.entry.get_mut().value
+ }
+
+ /// Converts the OccupiedEntry into a mutable reference to the value in the entry
+ /// with a lifetime bound to the map itself
+ pub fn into_mut(self) -> &'a mut Item {
+ &mut self.entry.into_mut().value
+ }
+
+ /// Sets the value of the entry, and returns the entry's old value
+ pub fn insert(&mut self, mut value: Item) -> Item {
+ std::mem::swap(&mut value, &mut self.entry.get_mut().value);
+ value
+ }
+
+ /// Takes the value out of the entry, and returns it
+ pub fn remove(self) -> Item {
+ self.entry.shift_remove().value
+ }
+}
+
+/// A view into a single empty location in a `IndexMap`.
+pub struct VacantEntry<'a> {
+ pub(crate) entry: indexmap::map::VacantEntry<'a, InternalString, TableKeyValue>,
+ pub(crate) key: Option<Key>,
+}
+
+impl<'a> VacantEntry<'a> {
+ /// Gets a reference to the entry key
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use toml_edit::Table;
+ ///
+ /// let mut map = Table::new();
+ ///
+ /// assert_eq!("foo", map.entry("foo").key());
+ /// ```
+ pub fn key(&self) -> &str {
+ self.entry.key().as_str()
+ }
+
+ /// Sets the value of the entry with the VacantEntry's key,
+ /// and returns a mutable reference to it
+ pub fn insert(self, value: Item) -> &'a mut Item {
+ let entry = self.entry;
+ let key = self.key.unwrap_or_else(|| Key::new(entry.key().as_str()));
+ &mut entry.insert(TableKeyValue::new(key, value)).value
+ }
+}
diff --git a/src/value.rs b/src/value.rs
new file mode 100644
index 0000000..f10da9a
--- /dev/null
+++ b/src/value.rs
@@ -0,0 +1,372 @@
+use std::iter::FromIterator;
+use std::str::FromStr;
+
+use toml_datetime::*;
+
+use crate::key::Key;
+use crate::parser;
+use crate::repr::{Decor, Formatted};
+use crate::{Array, InlineTable, InternalString, RawString};
+
+/// Representation of a TOML Value (as part of a Key/Value Pair).
+#[derive(Debug, Clone)]
+pub enum Value {
+ /// A string value.
+ String(Formatted<String>),
+ /// A 64-bit integer value.
+ Integer(Formatted<i64>),
+ /// A 64-bit float value.
+ Float(Formatted<f64>),
+ /// A boolean value.
+ Boolean(Formatted<bool>),
+ /// An RFC 3339 formatted date-time with offset.
+ Datetime(Formatted<Datetime>),
+ /// An inline array of values.
+ Array(Array),
+ /// An inline table of key/value pairs.
+ InlineTable(InlineTable),
+}
+
+/// Downcasting
+impl Value {
+ /// Text description of value type
+ pub fn type_name(&self) -> &'static str {
+ match self {
+ Value::String(..) => "string",
+ Value::Integer(..) => "integer",
+ Value::Float(..) => "float",
+ Value::Boolean(..) => "boolean",
+ Value::Datetime(..) => "datetime",
+ Value::Array(..) => "array",
+ Value::InlineTable(..) => "inline table",
+ }
+ }
+
+ /// Casts `self` to str.
+ pub fn as_str(&self) -> Option<&str> {
+ match *self {
+ Value::String(ref value) => Some(value.value()),
+ _ => None,
+ }
+ }
+
+ /// Returns true iff `self` is a string.
+ pub fn is_str(&self) -> bool {
+ self.as_str().is_some()
+ }
+
+ /// Casts `self` to integer.
+ pub fn as_integer(&self) -> Option<i64> {
+ match *self {
+ Value::Integer(ref value) => Some(*value.value()),
+ _ => None,
+ }
+ }
+
+ /// Returns true iff `self` is an integer.
+ pub fn is_integer(&self) -> bool {
+ self.as_integer().is_some()
+ }
+
+ /// Casts `self` to float.
+ pub fn as_float(&self) -> Option<f64> {
+ match *self {
+ Value::Float(ref value) => Some(*value.value()),
+ _ => None,
+ }
+ }
+
+ /// Returns true iff `self` is a float.
+ pub fn is_float(&self) -> bool {
+ self.as_float().is_some()
+ }
+
+ /// Casts `self` to boolean.
+ pub fn as_bool(&self) -> Option<bool> {
+ match *self {
+ Value::Boolean(ref value) => Some(*value.value()),
+ _ => None,
+ }
+ }
+
+ /// Returns true iff `self` is a boolean.
+ pub fn is_bool(&self) -> bool {
+ self.as_bool().is_some()
+ }
+
+ /// Casts `self` to date-time.
+ pub fn as_datetime(&self) -> Option<&Datetime> {
+ match *self {
+ Value::Datetime(ref value) => Some(value.value()),
+ _ => None,
+ }
+ }
+
+ /// Returns true iff `self` is a date-time.
+ pub fn is_datetime(&self) -> bool {
+ self.as_datetime().is_some()
+ }
+
+ /// Casts `self` to array.
+ pub fn as_array(&self) -> Option<&Array> {
+ match *self {
+ Value::Array(ref value) => Some(value),
+ _ => None,
+ }
+ }
+
+ /// Casts `self` to mutable array.
+ pub fn as_array_mut(&mut self) -> Option<&mut Array> {
+ match *self {
+ Value::Array(ref mut value) => Some(value),
+ _ => None,
+ }
+ }
+
+ /// Returns true iff `self` is an array.
+ pub fn is_array(&self) -> bool {
+ self.as_array().is_some()
+ }
+
+ /// Casts `self` to inline table.
+ pub fn as_inline_table(&self) -> Option<&InlineTable> {
+ match *self {
+ Value::InlineTable(ref value) => Some(value),
+ _ => None,
+ }
+ }
+
+ /// Casts `self` to mutable inline table.
+ pub fn as_inline_table_mut(&mut self) -> Option<&mut InlineTable> {
+ match *self {
+ Value::InlineTable(ref mut value) => Some(value),
+ _ => None,
+ }
+ }
+
+ /// Returns true iff `self` is an inline table.
+ pub fn is_inline_table(&self) -> bool {
+ self.as_inline_table().is_some()
+ }
+}
+
+impl Value {
+ /// Get the decoration of the value.
+ /// # Example
+ /// ```rust
+ /// let v = toml_edit::Value::from(true);
+ /// assert_eq!(v.decor().suffix(), None);
+ ///```
+ pub fn decor_mut(&mut self) -> &mut Decor {
+ match self {
+ Value::String(f) => f.decor_mut(),
+ Value::Integer(f) => f.decor_mut(),
+ Value::Float(f) => f.decor_mut(),
+ Value::Boolean(f) => f.decor_mut(),
+ Value::Datetime(f) => f.decor_mut(),
+ Value::Array(a) => a.decor_mut(),
+ Value::InlineTable(t) => t.decor_mut(),
+ }
+ }
+
+ /// Get the decoration of the value.
+ /// # Example
+ /// ```rust
+ /// let v = toml_edit::Value::from(true);
+ /// assert_eq!(v.decor().suffix(), None);
+ ///```
+ pub fn decor(&self) -> &Decor {
+ match *self {
+ Value::String(ref f) => f.decor(),
+ Value::Integer(ref f) => f.decor(),
+ Value::Float(ref f) => f.decor(),
+ Value::Boolean(ref f) => f.decor(),
+ Value::Datetime(ref f) => f.decor(),
+ Value::Array(ref a) => a.decor(),
+ Value::InlineTable(ref t) => t.decor(),
+ }
+ }
+
+ /// Sets the prefix and the suffix for value.
+ /// # Example
+ /// ```rust
+ /// let mut v = toml_edit::Value::from(42);
+ /// assert_eq!(&v.to_string(), "42");
+ /// let d = v.decorated(" ", " ");
+ /// assert_eq!(&d.to_string(), " 42 ");
+ /// ```
+ pub fn decorated(mut self, prefix: impl Into<RawString>, suffix: impl Into<RawString>) -> Self {
+ self.decorate(prefix, suffix);
+ self
+ }
+
+ pub(crate) fn decorate(&mut self, prefix: impl Into<RawString>, suffix: impl Into<RawString>) {
+ let decor = self.decor_mut();
+ *decor = Decor::new(prefix, suffix);
+ }
+
+ /// Returns the location within the original document
+ pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+ match self {
+ Value::String(f) => f.span(),
+ Value::Integer(f) => f.span(),
+ Value::Float(f) => f.span(),
+ Value::Boolean(f) => f.span(),
+ Value::Datetime(f) => f.span(),
+ Value::Array(a) => a.span(),
+ Value::InlineTable(t) => t.span(),
+ }
+ }
+
+ pub(crate) fn despan(&mut self, input: &str) {
+ match self {
+ Value::String(f) => f.despan(input),
+ Value::Integer(f) => f.despan(input),
+ Value::Float(f) => f.despan(input),
+ Value::Boolean(f) => f.despan(input),
+ Value::Datetime(f) => f.despan(input),
+ Value::Array(a) => a.despan(input),
+ Value::InlineTable(t) => t.despan(input),
+ }
+ }
+}
+
+impl FromStr for Value {
+ type Err = crate::TomlError;
+
+ /// Parses a value from a &str
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ parser::parse_value(s)
+ }
+}
+
+impl<'b> From<&'b Value> for Value {
+ fn from(s: &'b Value) -> Self {
+ s.clone()
+ }
+}
+
+impl<'b> From<&'b str> for Value {
+ fn from(s: &'b str) -> Self {
+ s.to_owned().into()
+ }
+}
+
+impl<'b> From<&'b String> for Value {
+ fn from(s: &'b String) -> Self {
+ s.to_owned().into()
+ }
+}
+
+impl From<String> for Value {
+ fn from(s: String) -> Self {
+ Value::String(Formatted::new(s))
+ }
+}
+
+impl<'b> From<&'b InternalString> for Value {
+ fn from(s: &'b InternalString) -> Self {
+ s.as_str().into()
+ }
+}
+
+impl From<InternalString> for Value {
+ fn from(s: InternalString) -> Self {
+ s.as_str().into()
+ }
+}
+
+impl From<i64> for Value {
+ fn from(i: i64) -> Self {
+ Value::Integer(Formatted::new(i))
+ }
+}
+
+impl From<f64> for Value {
+ fn from(f: f64) -> Self {
+ Value::Float(Formatted::new(f))
+ }
+}
+
+impl From<bool> for Value {
+ fn from(b: bool) -> Self {
+ Value::Boolean(Formatted::new(b))
+ }
+}
+
+impl From<Datetime> for Value {
+ fn from(d: Datetime) -> Self {
+ Value::Datetime(Formatted::new(d))
+ }
+}
+
+impl From<Date> for Value {
+ fn from(d: Date) -> Self {
+ let d: Datetime = d.into();
+ d.into()
+ }
+}
+
+impl From<Time> for Value {
+ fn from(d: Time) -> Self {
+ let d: Datetime = d.into();
+ d.into()
+ }
+}
+
+impl From<Array> for Value {
+ fn from(array: Array) -> Self {
+ Value::Array(array)
+ }
+}
+
+impl From<InlineTable> for Value {
+ fn from(table: InlineTable) -> Self {
+ Value::InlineTable(table)
+ }
+}
+
+impl<V: Into<Value>> FromIterator<V> for Value {
+ fn from_iter<I>(iter: I) -> Self
+ where
+ I: IntoIterator<Item = V>,
+ {
+ let array: Array = iter.into_iter().collect();
+ Value::Array(array)
+ }
+}
+
+impl<K: Into<Key>, V: Into<Value>> FromIterator<(K, V)> for Value {
+ fn from_iter<I>(iter: I) -> Self
+ where
+ I: IntoIterator<Item = (K, V)>,
+ {
+ let table: InlineTable = iter.into_iter().collect();
+ Value::InlineTable(table)
+ }
+}
+
+impl std::fmt::Display for Value {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ crate::encode::Encode::encode(self, f, None, ("", ""))
+ }
+}
+
+// `key1 = value1`
+pub(crate) const DEFAULT_VALUE_DECOR: (&str, &str) = (" ", "");
+// `{ key = value }`
+pub(crate) const DEFAULT_TRAILING_VALUE_DECOR: (&str, &str) = (" ", " ");
+// `[value1, value2]`
+pub(crate) const DEFAULT_LEADING_VALUE_DECOR: (&str, &str) = ("", "");
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn from_iter_formatting() {
+ let features = vec!["node".to_owned(), "mouth".to_owned()];
+ let features: Value = features.iter().cloned().collect();
+ assert_eq!(features.to_string(), r#"["node", "mouth"]"#);
+ }
+}
diff --git a/src/visit.rs b/src/visit.rs
new file mode 100644
index 0000000..1bc640a
--- /dev/null
+++ b/src/visit.rs
@@ -0,0 +1,236 @@
+#![allow(missing_docs)]
+
+//! Document tree traversal to walk a shared borrow of a document tree.
+//!
+//! Each method of the [`Visit`] trait is a hook that can be overridden
+//! to customize the behavior when mutating the corresponding type of node.
+//! By default, every method recursively visits the substructure of the
+//! input by invoking the right visitor method of each of its fields.
+//!
+//! ```
+//! # use toml_edit::{Item, ArrayOfTables, Table, Value};
+//!
+//! pub trait Visit<'doc> {
+//! /* ... */
+//!
+//! fn visit_item(&mut self, i: &'doc Item) {
+//! visit_item(self, i);
+//! }
+//!
+//! /* ... */
+//! # fn visit_value(&mut self, i: &'doc Value);
+//! # fn visit_table(&mut self, i: &'doc Table);
+//! # fn visit_array_of_tables(&mut self, i: &'doc ArrayOfTables);
+//! }
+//!
+//! pub fn visit_item<'doc, V>(v: &mut V, node: &'doc Item)
+//! where
+//! V: Visit<'doc> + ?Sized,
+//! {
+//! match node {
+//! Item::None => {}
+//! Item::Value(value) => v.visit_value(value),
+//! Item::Table(table) => v.visit_table(table),
+//! Item::ArrayOfTables(array) => v.visit_array_of_tables(array),
+//! }
+//! }
+//! ```
+//!
+//! The API is modeled after [`syn::visit`](https://docs.rs/syn/1/syn/visit).
+//!
+//! # Examples
+//!
+//! This visitor stores every string in the document.
+//!
+//! ```
+//! # use toml_edit::*;
+//! use toml_edit::visit::*;
+//!
+//! #[derive(Default)]
+//! struct StringCollector<'doc> {
+//! strings: Vec<&'doc str>,
+//! }
+//!
+//! impl<'doc> Visit<'doc> for StringCollector<'doc> {
+//! fn visit_string(&mut self, node: &'doc Formatted<String>) {
+//! self.strings.push(node.value().as_str());
+//! }
+//! }
+//!
+//! let input = r#"
+//! laputa = "sky-castle"
+//! the-force = { value = "surrounds-you" }
+//! "#;
+//!
+//! let mut document: Document = input.parse().unwrap();
+//! let mut visitor = StringCollector::default();
+//! visitor.visit_document(&document);
+//!
+//! assert_eq!(visitor.strings, vec!["sky-castle", "surrounds-you"]);
+//! ```
+//!
+//! For a more complex example where the visitor has internal state, see `examples/visit.rs`
+//! [on GitHub](https://github.com/ordian/toml_edit/blob/master/examples/visit.rs).
+
+use crate::{
+ Array, ArrayOfTables, Datetime, Document, Formatted, InlineTable, Item, Table, TableLike, Value,
+};
+
+/// Document tree traversal to mutate an exclusive borrow of a document tree in-place.
+///
+/// See the [module documentation](self) for details.
+pub trait Visit<'doc> {
+ fn visit_document(&mut self, node: &'doc Document) {
+ visit_document(self, node);
+ }
+
+ fn visit_item(&mut self, node: &'doc Item) {
+ visit_item(self, node);
+ }
+
+ fn visit_table(&mut self, node: &'doc Table) {
+ visit_table(self, node);
+ }
+
+ fn visit_inline_table(&mut self, node: &'doc InlineTable) {
+ visit_inline_table(self, node)
+ }
+
+ fn visit_table_like(&mut self, node: &'doc dyn TableLike) {
+ visit_table_like(self, node);
+ }
+
+ fn visit_table_like_kv(&mut self, key: &'doc str, node: &'doc Item) {
+ visit_table_like_kv(self, key, node);
+ }
+
+ fn visit_array(&mut self, node: &'doc Array) {
+ visit_array(self, node);
+ }
+
+ fn visit_array_of_tables(&mut self, node: &'doc ArrayOfTables) {
+ visit_array_of_tables(self, node);
+ }
+
+ fn visit_value(&mut self, node: &'doc Value) {
+ visit_value(self, node);
+ }
+
+ fn visit_boolean(&mut self, node: &'doc Formatted<bool>) {
+ visit_boolean(self, node)
+ }
+
+ fn visit_datetime(&mut self, node: &'doc Formatted<Datetime>) {
+ visit_datetime(self, node);
+ }
+
+ fn visit_float(&mut self, node: &'doc Formatted<f64>) {
+ visit_float(self, node)
+ }
+
+ fn visit_integer(&mut self, node: &'doc Formatted<i64>) {
+ visit_integer(self, node)
+ }
+
+ fn visit_string(&mut self, node: &'doc Formatted<String>) {
+ visit_string(self, node)
+ }
+}
+
+pub fn visit_document<'doc, V>(v: &mut V, node: &'doc Document)
+where
+ V: Visit<'doc> + ?Sized,
+{
+ v.visit_table(node.as_table());
+}
+
+pub fn visit_item<'doc, V>(v: &mut V, node: &'doc Item)
+where
+ V: Visit<'doc> + ?Sized,
+{
+ match node {
+ Item::None => {}
+ Item::Value(value) => v.visit_value(value),
+ Item::Table(table) => v.visit_table(table),
+ Item::ArrayOfTables(array) => v.visit_array_of_tables(array),
+ }
+}
+
+pub fn visit_table<'doc, V>(v: &mut V, node: &'doc Table)
+where
+ V: Visit<'doc> + ?Sized,
+{
+ v.visit_table_like(node)
+}
+
+pub fn visit_inline_table<'doc, V>(v: &mut V, node: &'doc InlineTable)
+where
+ V: Visit<'doc> + ?Sized,
+{
+ v.visit_table_like(node)
+}
+
+pub fn visit_table_like<'doc, V>(v: &mut V, node: &'doc dyn TableLike)
+where
+ V: Visit<'doc> + ?Sized,
+{
+ for (key, item) in node.iter() {
+ v.visit_table_like_kv(key, item)
+ }
+}
+
+pub fn visit_table_like_kv<'doc, V>(v: &mut V, _key: &'doc str, node: &'doc Item)
+where
+ V: Visit<'doc> + ?Sized,
+{
+ v.visit_item(node)
+}
+
+pub fn visit_array<'doc, V>(v: &mut V, node: &'doc Array)
+where
+ V: Visit<'doc> + ?Sized,
+{
+ for value in node.iter() {
+ v.visit_value(value);
+ }
+}
+
+pub fn visit_array_of_tables<'doc, V>(v: &mut V, node: &'doc ArrayOfTables)
+where
+ V: Visit<'doc> + ?Sized,
+{
+ for table in node.iter() {
+ v.visit_table(table);
+ }
+}
+
+pub fn visit_value<'doc, V>(v: &mut V, node: &'doc Value)
+where
+ V: Visit<'doc> + ?Sized,
+{
+ match node {
+ Value::String(s) => v.visit_string(s),
+ Value::Integer(i) => v.visit_integer(i),
+ Value::Float(f) => v.visit_float(f),
+ Value::Boolean(b) => v.visit_boolean(b),
+ Value::Datetime(dt) => v.visit_datetime(dt),
+ Value::Array(array) => v.visit_array(array),
+ Value::InlineTable(table) => v.visit_inline_table(table),
+ }
+}
+
+macro_rules! empty_visit {
+ ($name: ident, $t: ty) => {
+ fn $name<'doc, V>(_v: &mut V, _node: &'doc $t)
+ where
+ V: Visit<'doc> + ?Sized,
+ {
+ }
+ };
+}
+
+empty_visit!(visit_boolean, Formatted<bool>);
+empty_visit!(visit_datetime, Formatted<Datetime>);
+empty_visit!(visit_float, Formatted<f64>);
+empty_visit!(visit_integer, Formatted<i64>);
+empty_visit!(visit_string, Formatted<String>);
diff --git a/src/visit_mut.rs b/src/visit_mut.rs
new file mode 100644
index 0000000..2c2af97
--- /dev/null
+++ b/src/visit_mut.rs
@@ -0,0 +1,252 @@
+#![allow(missing_docs)]
+
+//! Document tree traversal to mutate an exclusive borrow of a document tree in place.
+//!
+//!
+//! Each method of the [`VisitMut`] trait is a hook that can be overridden
+//! to customize the behavior when mutating the corresponding type of node.
+//! By default, every method recursively visits the substructure of the
+//! input by invoking the right visitor method of each of its fields.
+//!
+//! ```
+//! # use toml_edit::{Item, ArrayOfTables, Table, Value};
+//!
+//! pub trait VisitMut {
+//! /* ... */
+//!
+//! fn visit_item_mut(&mut self, i: &mut Item) {
+//! visit_item_mut(self, i);
+//! }
+//!
+//! /* ... */
+//! # fn visit_value_mut(&mut self, i: &mut Value);
+//! # fn visit_table_mut(&mut self, i: &mut Table);
+//! # fn visit_array_of_tables_mut(&mut self, i: &mut ArrayOfTables);
+//! }
+//!
+//! pub fn visit_item_mut<V>(v: &mut V, node: &mut Item)
+//! where
+//! V: VisitMut + ?Sized,
+//! {
+//! match node {
+//! Item::None => {}
+//! Item::Value(value) => v.visit_value_mut(value),
+//! Item::Table(table) => v.visit_table_mut(table),
+//! Item::ArrayOfTables(array) => v.visit_array_of_tables_mut(array),
+//! }
+//! }
+//! ```
+//!
+//! The API is modeled after [`syn::visit_mut`](https://docs.rs/syn/1/syn/visit_mut).
+//!
+//! # Examples
+//!
+//! This visitor replaces every floating point value with its decimal string representation, to
+//! 2 decimal points.
+//!
+//! ```
+//! # use toml_edit::*;
+//! use toml_edit::visit_mut::*;
+//!
+//! struct FloatToString;
+//!
+//! impl VisitMut for FloatToString {
+//! fn visit_value_mut(&mut self, node: &mut Value) {
+//! if let Value::Float(f) = node {
+//! // Convert the float to a string.
+//! let mut s = Formatted::new(format!("{:.2}", f.value()));
+//! // Copy over the formatting.
+//! std::mem::swap(s.decor_mut(), f.decor_mut());
+//! *node = Value::String(s);
+//! }
+//! // Most of the time, you will also need to call the default implementation to recurse
+//! // further down the document tree.
+//! visit_value_mut(self, node);
+//! }
+//! }
+//!
+//! let input = r#"
+//! banana = 3.26
+//! table = { apple = 4.5 }
+//! "#;
+//!
+//! let mut document: Document = input.parse().unwrap();
+//! let mut visitor = FloatToString;
+//! visitor.visit_document_mut(&mut document);
+//!
+//! let output = r#"
+//! banana = "3.26"
+//! table = { apple = "4.50" }
+//! "#;
+//!
+//! assert_eq!(format!("{}", document), output);
+//! ```
+//!
+//! For a more complex example where the visitor has internal state, see `examples/visit.rs`
+//! [on GitHub](https://github.com/ordian/toml_edit/blob/master/examples/visit.rs).
+
+use crate::{
+ Array, ArrayOfTables, Datetime, Document, Formatted, InlineTable, Item, KeyMut, Table,
+ TableLike, Value,
+};
+
+/// Document tree traversal to mutate an exclusive borrow of a document tree in-place.
+///
+/// See the [module documentation](self) for details.
+pub trait VisitMut {
+ fn visit_document_mut(&mut self, node: &mut Document) {
+ visit_document_mut(self, node);
+ }
+
+ fn visit_item_mut(&mut self, node: &mut Item) {
+ visit_item_mut(self, node);
+ }
+
+ fn visit_table_mut(&mut self, node: &mut Table) {
+ visit_table_mut(self, node);
+ }
+
+ fn visit_inline_table_mut(&mut self, node: &mut InlineTable) {
+ visit_inline_table_mut(self, node)
+ }
+
+ /// [`visit_table_mut`](Self::visit_table_mut) and
+ /// [`visit_inline_table_mut`](Self::visit_inline_table_mut) both recurse into this method.
+ fn visit_table_like_mut(&mut self, node: &mut dyn TableLike) {
+ visit_table_like_mut(self, node);
+ }
+
+ fn visit_table_like_kv_mut(&mut self, key: KeyMut<'_>, node: &mut Item) {
+ visit_table_like_kv_mut(self, key, node);
+ }
+
+ fn visit_array_mut(&mut self, node: &mut Array) {
+ visit_array_mut(self, node);
+ }
+
+ fn visit_array_of_tables_mut(&mut self, node: &mut ArrayOfTables) {
+ visit_array_of_tables_mut(self, node);
+ }
+
+ fn visit_value_mut(&mut self, node: &mut Value) {
+ visit_value_mut(self, node);
+ }
+
+ fn visit_boolean_mut(&mut self, node: &mut Formatted<bool>) {
+ visit_boolean_mut(self, node)
+ }
+
+ fn visit_datetime_mut(&mut self, node: &mut Formatted<Datetime>) {
+ visit_datetime_mut(self, node);
+ }
+
+ fn visit_float_mut(&mut self, node: &mut Formatted<f64>) {
+ visit_float_mut(self, node)
+ }
+
+ fn visit_integer_mut(&mut self, node: &mut Formatted<i64>) {
+ visit_integer_mut(self, node)
+ }
+
+ fn visit_string_mut(&mut self, node: &mut Formatted<String>) {
+ visit_string_mut(self, node)
+ }
+}
+
+pub fn visit_document_mut<V>(v: &mut V, node: &mut Document)
+where
+ V: VisitMut + ?Sized,
+{
+ v.visit_table_mut(node.as_table_mut());
+}
+
+pub fn visit_item_mut<V>(v: &mut V, node: &mut Item)
+where
+ V: VisitMut + ?Sized,
+{
+ match node {
+ Item::None => {}
+ Item::Value(value) => v.visit_value_mut(value),
+ Item::Table(table) => v.visit_table_mut(table),
+ Item::ArrayOfTables(array) => v.visit_array_of_tables_mut(array),
+ }
+}
+
+pub fn visit_table_mut<V>(v: &mut V, node: &mut Table)
+where
+ V: VisitMut + ?Sized,
+{
+ v.visit_table_like_mut(node);
+}
+
+pub fn visit_inline_table_mut<V>(v: &mut V, node: &mut InlineTable)
+where
+ V: VisitMut + ?Sized,
+{
+ v.visit_table_like_mut(node);
+}
+
+pub fn visit_table_like_mut<V>(v: &mut V, node: &mut dyn TableLike)
+where
+ V: VisitMut + ?Sized,
+{
+ for (key, item) in node.iter_mut() {
+ v.visit_table_like_kv_mut(key, item);
+ }
+}
+
+pub fn visit_table_like_kv_mut<V>(v: &mut V, _key: KeyMut<'_>, node: &mut Item)
+where
+ V: VisitMut + ?Sized,
+{
+ v.visit_item_mut(node)
+}
+
+pub fn visit_array_mut<V>(v: &mut V, node: &mut Array)
+where
+ V: VisitMut + ?Sized,
+{
+ for value in node.iter_mut() {
+ v.visit_value_mut(value);
+ }
+}
+
+pub fn visit_array_of_tables_mut<V>(v: &mut V, node: &mut ArrayOfTables)
+where
+ V: VisitMut + ?Sized,
+{
+ for table in node.iter_mut() {
+ v.visit_table_mut(table);
+ }
+}
+
+pub fn visit_value_mut<V>(v: &mut V, node: &mut Value)
+where
+ V: VisitMut + ?Sized,
+{
+ match node {
+ Value::String(s) => v.visit_string_mut(s),
+ Value::Integer(i) => v.visit_integer_mut(i),
+ Value::Float(f) => v.visit_float_mut(f),
+ Value::Boolean(b) => v.visit_boolean_mut(b),
+ Value::Datetime(dt) => v.visit_datetime_mut(dt),
+ Value::Array(array) => v.visit_array_mut(array),
+ Value::InlineTable(table) => v.visit_inline_table_mut(table),
+ }
+}
+
+macro_rules! empty_visit_mut {
+ ($name: ident, $t: ty) => {
+ fn $name<V>(_v: &mut V, _node: &mut $t)
+ where
+ V: VisitMut + ?Sized,
+ {
+ }
+ };
+}
+
+empty_visit_mut!(visit_boolean_mut, Formatted<bool>);
+empty_visit_mut!(visit_datetime_mut, Formatted<Datetime>);
+empty_visit_mut!(visit_float_mut, Formatted<f64>);
+empty_visit_mut!(visit_integer_mut, Formatted<i64>);
+empty_visit_mut!(visit_string_mut, Formatted<String>);
diff --git a/tests/decoder.rs b/tests/decoder.rs
new file mode 100644
index 0000000..7306d45
--- /dev/null
+++ b/tests/decoder.rs
@@ -0,0 +1,100 @@
+#[derive(Copy, Clone)]
+pub struct Decoder;
+
+impl toml_test_harness::Decoder for Decoder {
+ fn name(&self) -> &str {
+ "toml_edit"
+ }
+
+ fn decode(&self, data: &[u8]) -> Result<toml_test_harness::Decoded, toml_test_harness::Error> {
+ let data = std::str::from_utf8(data).map_err(toml_test_harness::Error::new)?;
+ let document = data
+ .parse::<toml_edit::Document>()
+ .map_err(toml_test_harness::Error::new)?;
+ document_to_decoded(&document)
+ }
+}
+
+fn document_to_decoded(
+ value: &toml_edit::Document,
+) -> Result<toml_test_harness::Decoded, toml_test_harness::Error> {
+ table_to_decoded(value)
+}
+
+fn item_to_decoded(
+ value: &toml_edit::Item,
+) -> Result<toml_test_harness::Decoded, toml_test_harness::Error> {
+ match value {
+ toml_edit::Item::None => unreachable!("No nones"),
+ toml_edit::Item::Value(v) => value_to_decoded(v),
+ toml_edit::Item::Table(v) => table_to_decoded(v),
+ toml_edit::Item::ArrayOfTables(v) => {
+ let v: Result<_, toml_test_harness::Error> = v.iter().map(table_to_decoded).collect();
+ Ok(toml_test_harness::Decoded::Array(v?))
+ }
+ }
+}
+
+fn value_to_decoded(
+ value: &toml_edit::Value,
+) -> Result<toml_test_harness::Decoded, toml_test_harness::Error> {
+ match value {
+ toml_edit::Value::Integer(v) => Ok(toml_test_harness::Decoded::Value(
+ toml_test_harness::DecodedValue::from(*v.value()),
+ )),
+ toml_edit::Value::String(v) => Ok(toml_test_harness::Decoded::Value(
+ toml_test_harness::DecodedValue::from(v.value()),
+ )),
+ toml_edit::Value::Float(v) => Ok(toml_test_harness::Decoded::Value(
+ toml_test_harness::DecodedValue::from(*v.value()),
+ )),
+ toml_edit::Value::Datetime(v) => {
+ let v = v.value();
+ let value = v.to_string();
+ let value = match (v.date.is_some(), v.time.is_some(), v.offset.is_some()) {
+ (true, true, true) => toml_test_harness::DecodedValue::Datetime(value),
+ (true, true, false) => toml_test_harness::DecodedValue::DatetimeLocal(value),
+ (true, false, false) => toml_test_harness::DecodedValue::DateLocal(value),
+ (false, true, false) => toml_test_harness::DecodedValue::TimeLocal(value),
+ _ => unreachable!("Unsupported case"),
+ };
+ Ok(toml_test_harness::Decoded::Value(value))
+ }
+ toml_edit::Value::Boolean(v) => Ok(toml_test_harness::Decoded::Value(
+ toml_test_harness::DecodedValue::from(*v.value()),
+ )),
+ toml_edit::Value::Array(v) => {
+ let v: Result<_, toml_test_harness::Error> = v.iter().map(value_to_decoded).collect();
+ Ok(toml_test_harness::Decoded::Array(v?))
+ }
+ toml_edit::Value::InlineTable(v) => inline_table_to_decoded(v),
+ }
+}
+
+fn table_to_decoded(
+ value: &toml_edit::Table,
+) -> Result<toml_test_harness::Decoded, toml_test_harness::Error> {
+ let table: Result<_, toml_test_harness::Error> = value
+ .iter()
+ .map(|(k, v)| {
+ let k = k.to_owned();
+ let v = item_to_decoded(v)?;
+ Ok((k, v))
+ })
+ .collect();
+ Ok(toml_test_harness::Decoded::Table(table?))
+}
+
+fn inline_table_to_decoded(
+ value: &toml_edit::InlineTable,
+) -> Result<toml_test_harness::Decoded, toml_test_harness::Error> {
+ let table: Result<_, toml_test_harness::Error> = value
+ .iter()
+ .map(|(k, v)| {
+ let k = k.to_owned();
+ let v = value_to_decoded(v)?;
+ Ok((k, v))
+ })
+ .collect();
+ Ok(toml_test_harness::Decoded::Table(table?))
+}
diff --git a/tests/decoder_compliance.rs b/tests/decoder_compliance.rs
new file mode 100644
index 0000000..0f0b350
--- /dev/null
+++ b/tests/decoder_compliance.rs
@@ -0,0 +1,17 @@
+mod decoder;
+
+fn main() {
+ let decoder = decoder::Decoder;
+ let mut harness = toml_test_harness::DecoderHarness::new(decoder);
+ harness
+ .ignore([
+ "valid/spec/float-0.toml", // Test issue; `Decoder` turns `6.626e-34` into `0.0`
+ // Unreleased
+ "valid/string/escape-esc.toml",
+ "valid/string/hex-escape.toml",
+ "valid/datetime/no-seconds.toml",
+ "valid/inline-table/newline.toml",
+ ])
+ .unwrap();
+ harness.test();
+}
diff --git a/tests/encoder.rs b/tests/encoder.rs
new file mode 100644
index 0000000..808a20c
--- /dev/null
+++ b/tests/encoder.rs
@@ -0,0 +1,111 @@
+#[derive(Copy, Clone)]
+pub struct Encoder;
+
+impl toml_test_harness::Encoder for Encoder {
+ fn name(&self) -> &str {
+ "toml_edit"
+ }
+
+ fn encode(&self, data: toml_test_harness::Decoded) -> Result<String, toml_test_harness::Error> {
+ let doc = decoded_to_document(&data)?;
+ Ok(doc.to_string())
+ }
+}
+
+fn decoded_to_document(
+ decoded: &toml_test_harness::Decoded,
+) -> Result<toml_edit::Document, toml_test_harness::Error> {
+ let item = root_from_decoded(decoded)?;
+ let mut doc = toml_edit::Document::new();
+ *doc = item;
+ Ok(doc)
+}
+
+fn root_from_decoded(
+ decoded: &toml_test_harness::Decoded,
+) -> Result<toml_edit::Table, toml_test_harness::Error> {
+ match decoded {
+ toml_test_harness::Decoded::Value(_) => {
+ Err(toml_test_harness::Error::new("Root cannot be a value"))
+ }
+ toml_test_harness::Decoded::Table(value) => value
+ .iter()
+ .map(|(k, v)| {
+ let k = k.as_str();
+ let v = from_decoded(v)?;
+ Ok((k, v))
+ })
+ .collect(),
+ toml_test_harness::Decoded::Array(_) => {
+ Err(toml_test_harness::Error::new("Root cannot be an array"))
+ }
+ }
+}
+
+fn from_decoded(
+ decoded: &toml_test_harness::Decoded,
+) -> Result<toml_edit::Value, toml_test_harness::Error> {
+ let value = match decoded {
+ toml_test_harness::Decoded::Value(value) => from_decoded_value(value)?,
+ toml_test_harness::Decoded::Table(value) => {
+ toml_edit::Value::InlineTable(from_table(value)?)
+ }
+ toml_test_harness::Decoded::Array(value) => toml_edit::Value::Array(from_array(value)?),
+ };
+ Ok(value)
+}
+
+fn from_decoded_value(
+ decoded: &toml_test_harness::DecodedValue,
+) -> Result<toml_edit::Value, toml_test_harness::Error> {
+ let value: toml_edit::Value = match decoded {
+ toml_test_harness::DecodedValue::String(value) => value.into(),
+ toml_test_harness::DecodedValue::Integer(value) => value
+ .parse::<i64>()
+ .map_err(toml_test_harness::Error::new)?
+ .into(),
+ toml_test_harness::DecodedValue::Float(value) => value
+ .parse::<f64>()
+ .map_err(toml_test_harness::Error::new)?
+ .into(),
+ toml_test_harness::DecodedValue::Bool(value) => value
+ .parse::<bool>()
+ .map_err(toml_test_harness::Error::new)?
+ .into(),
+ toml_test_harness::DecodedValue::Datetime(value) => value
+ .parse::<toml_edit::Datetime>()
+ .map_err(toml_test_harness::Error::new)?
+ .into(),
+ toml_test_harness::DecodedValue::DatetimeLocal(value) => value
+ .parse::<toml_edit::Datetime>()
+ .map_err(toml_test_harness::Error::new)?
+ .into(),
+ toml_test_harness::DecodedValue::DateLocal(value) => value
+ .parse::<toml_edit::Datetime>()
+ .map_err(toml_test_harness::Error::new)?
+ .into(),
+ toml_test_harness::DecodedValue::TimeLocal(value) => value
+ .parse::<toml_edit::Datetime>()
+ .map_err(toml_test_harness::Error::new)?
+ .into(),
+ };
+ Ok(value)
+}
+
+fn from_table(
+ decoded: &std::collections::HashMap<String, toml_test_harness::Decoded>,
+) -> Result<toml_edit::InlineTable, toml_test_harness::Error> {
+ decoded
+ .iter()
+ .map(|(k, v)| {
+ let v = from_decoded(v)?;
+ Ok((k, v))
+ })
+ .collect()
+}
+
+fn from_array(
+ decoded: &[toml_test_harness::Decoded],
+) -> Result<toml_edit::Array, toml_test_harness::Error> {
+ decoded.iter().map(from_decoded).collect()
+}
diff --git a/tests/encoder_compliance.rs b/tests/encoder_compliance.rs
new file mode 100644
index 0000000..ad65d75
--- /dev/null
+++ b/tests/encoder_compliance.rs
@@ -0,0 +1,14 @@
+mod decoder;
+mod encoder;
+
+fn main() {
+ let encoder = encoder::Encoder;
+ let decoder = decoder::Decoder;
+ let mut harness = toml_test_harness::EncoderHarness::new(encoder, decoder);
+ harness
+ .ignore([
+ "valid/spec/float-0.toml", // Test issue; `Decoder` turns `6.626e-34` into `0.0`
+ ])
+ .unwrap();
+ harness.test();
+}
diff --git a/tests/fixtures/invalid/array/double-comma-1.stderr b/tests/fixtures/invalid/array/double-comma-1.stderr
new file mode 100644
index 0000000..543e1b6
--- /dev/null
+++ b/tests/fixtures/invalid/array/double-comma-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 12
+ |
+1 | array = [1,,2]
+ | ^
+invalid array
+expected `]`
diff --git a/tests/fixtures/invalid/array/double-comma-2.stderr b/tests/fixtures/invalid/array/double-comma-2.stderr
new file mode 100644
index 0000000..694d7ec
--- /dev/null
+++ b/tests/fixtures/invalid/array/double-comma-2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 14
+ |
+1 | array = [1,2,,]
+ | ^
+invalid array
+expected `]`
diff --git a/tests/fixtures/invalid/array/extending-table.stderr b/tests/fixtures/invalid/array/extending-table.stderr
new file mode 100644
index 0000000..6f92ff6
--- /dev/null
+++ b/tests/fixtures/invalid/array/extending-table.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 5, column 1
+ |
+5 | [a.c]
+ | ^
+invalid table header
+dotted key `a` attempted to extend non-table type (array)
diff --git a/tests/fixtures/invalid/array/missing-separator.stderr b/tests/fixtures/invalid/array/missing-separator.stderr
new file mode 100644
index 0000000..8e21f51
--- /dev/null
+++ b/tests/fixtures/invalid/array/missing-separator.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 13
+ |
+1 | wrong = [ 1 2 3 ]
+ | ^
+invalid array
+expected `]`
diff --git a/tests/fixtures/invalid/array/no-close-2.stderr b/tests/fixtures/invalid/array/no-close-2.stderr
new file mode 100644
index 0000000..81ae5a9
--- /dev/null
+++ b/tests/fixtures/invalid/array/no-close-2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 11
+ |
+1 | x = [42 #
+ | ^
+invalid array
+expected `]`
diff --git a/tests/fixtures/invalid/array/no-close-table-2.stderr b/tests/fixtures/invalid/array/no-close-table-2.stderr
new file mode 100644
index 0000000..535943e
--- /dev/null
+++ b/tests/fixtures/invalid/array/no-close-table-2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 17
+ |
+1 | x = [{ key = 42 #
+ | ^
+invalid inline table
+expected `}`
diff --git a/tests/fixtures/invalid/array/no-close-table.stderr b/tests/fixtures/invalid/array/no-close-table.stderr
new file mode 100644
index 0000000..b4c3c32
--- /dev/null
+++ b/tests/fixtures/invalid/array/no-close-table.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 16
+ |
+1 | x = [{ key = 42
+ | ^
+invalid inline table
+expected `}`
diff --git a/tests/fixtures/invalid/array/no-close.stderr b/tests/fixtures/invalid/array/no-close.stderr
new file mode 100644
index 0000000..a4f0a88
--- /dev/null
+++ b/tests/fixtures/invalid/array/no-close.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 24
+ |
+1 | long_array = [ 1, 2, 3
+ | ^
+invalid array
+expected `]`
diff --git a/tests/fixtures/invalid/array/tables-1.stderr b/tests/fixtures/invalid/array/tables-1.stderr
new file mode 100644
index 0000000..771b4f6
--- /dev/null
+++ b/tests/fixtures/invalid/array/tables-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 4, column 1
+ |
+4 | [[fruit]] # Not allowed
+ | ^
+invalid table header
+duplicate key `fruit` in document root
diff --git a/tests/fixtures/invalid/array/tables-2.stderr b/tests/fixtures/invalid/array/tables-2.stderr
new file mode 100644
index 0000000..1f88e6e
--- /dev/null
+++ b/tests/fixtures/invalid/array/tables-2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 9, column 3
+ |
+9 | [fruit.variety]
+ | ^
+invalid table header
+duplicate key `variety` in table `fruit`
diff --git a/tests/fixtures/invalid/array/text-after-array-entries.stderr b/tests/fixtures/invalid/array/text-after-array-entries.stderr
new file mode 100644
index 0000000..23b5ac2
--- /dev/null
+++ b/tests/fixtures/invalid/array/text-after-array-entries.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 2, column 46
+ |
+2 | "Is there life after an array separator?", No
+ | ^
+invalid array
+expected `]`
diff --git a/tests/fixtures/invalid/array/text-before-array-separator.stderr b/tests/fixtures/invalid/array/text-before-array-separator.stderr
new file mode 100644
index 0000000..9d66799
--- /dev/null
+++ b/tests/fixtures/invalid/array/text-before-array-separator.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 2, column 46
+ |
+2 | "Is there life before an array separator?" No,
+ | ^
+invalid array
+expected `]`
diff --git a/tests/fixtures/invalid/array/text-in-array.stderr b/tests/fixtures/invalid/array/text-in-array.stderr
new file mode 100644
index 0000000..5802373
--- /dev/null
+++ b/tests/fixtures/invalid/array/text-in-array.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 3, column 3
+ |
+3 | I don't belong,
+ | ^
+invalid array
+expected `]`
diff --git a/tests/fixtures/invalid/bool/almost-false-with-extra.stderr b/tests/fixtures/invalid/bool/almost-false-with-extra.stderr
new file mode 100644
index 0000000..cd6c1cd
--- /dev/null
+++ b/tests/fixtures/invalid/bool/almost-false-with-extra.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 5
+ |
+1 | a = falsify
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/bool/almost-false.stderr b/tests/fixtures/invalid/bool/almost-false.stderr
new file mode 100644
index 0000000..550020b
--- /dev/null
+++ b/tests/fixtures/invalid/bool/almost-false.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 5
+ |
+1 | a = fals
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/bool/almost-true-with-extra.stderr b/tests/fixtures/invalid/bool/almost-true-with-extra.stderr
new file mode 100644
index 0000000..c75c553
--- /dev/null
+++ b/tests/fixtures/invalid/bool/almost-true-with-extra.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 5
+ |
+1 | a = truthy
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/bool/almost-true.stderr b/tests/fixtures/invalid/bool/almost-true.stderr
new file mode 100644
index 0000000..0c97e00
--- /dev/null
+++ b/tests/fixtures/invalid/bool/almost-true.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 5
+ |
+1 | a = tru
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/bool/just-f.stderr b/tests/fixtures/invalid/bool/just-f.stderr
new file mode 100644
index 0000000..ed2b9f0
--- /dev/null
+++ b/tests/fixtures/invalid/bool/just-f.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 5
+ |
+1 | a = f
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/bool/just-t.stderr b/tests/fixtures/invalid/bool/just-t.stderr
new file mode 100644
index 0000000..2c8b6a5
--- /dev/null
+++ b/tests/fixtures/invalid/bool/just-t.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 5
+ |
+1 | a = t
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/bool/mixed-case.stderr b/tests/fixtures/invalid/bool/mixed-case.stderr
new file mode 100644
index 0000000..b7c6192
--- /dev/null
+++ b/tests/fixtures/invalid/bool/mixed-case.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 9
+ |
+1 | valid = False
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/bool/starting-same-false.stderr b/tests/fixtures/invalid/bool/starting-same-false.stderr
new file mode 100644
index 0000000..b332089
--- /dev/null
+++ b/tests/fixtures/invalid/bool/starting-same-false.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 10
+ |
+1 | a = falsey
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/bool/starting-same-true.stderr b/tests/fixtures/invalid/bool/starting-same-true.stderr
new file mode 100644
index 0000000..6053103
--- /dev/null
+++ b/tests/fixtures/invalid/bool/starting-same-true.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 9
+ |
+1 | a = truer
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/bool/wrong-case-false.stderr b/tests/fixtures/invalid/bool/wrong-case-false.stderr
new file mode 100644
index 0000000..f67444c
--- /dev/null
+++ b/tests/fixtures/invalid/bool/wrong-case-false.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 5
+ |
+1 | b = FALSE
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/bool/wrong-case-true.stderr b/tests/fixtures/invalid/bool/wrong-case-true.stderr
new file mode 100644
index 0000000..82bb619
--- /dev/null
+++ b/tests/fixtures/invalid/bool/wrong-case-true.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 5
+ |
+1 | a = TRUE
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/control/bare-cr.stderr b/tests/fixtures/invalid/control/bare-cr.stderr
new file mode 100644
index 0000000..f0062f6
--- /dev/null
+++ b/tests/fixtures/invalid/control/bare-cr.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 2, column 1
+ |
+2 |
+ | ^
+
diff --git a/tests/fixtures/invalid/control/bare-formfeed.stderr b/tests/fixtures/invalid/control/bare-formfeed.stderr
new file mode 100644
index 0000000..313274a
--- /dev/null
+++ b/tests/fixtures/invalid/control/bare-formfeed.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 17
+ |
+1 | bare-formfeed =
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/control/bare-null.stderr b/tests/fixtures/invalid/control/bare-null.stderr
new file mode 100644
index 0000000..cd5e936
--- /dev/null
+++ b/tests/fixtures/invalid/control/bare-null.stderr
Binary files differ
diff --git a/tests/fixtures/invalid/control/bare-vertical-tab.stderr b/tests/fixtures/invalid/control/bare-vertical-tab.stderr
new file mode 100644
index 0000000..c8e01ba
--- /dev/null
+++ b/tests/fixtures/invalid/control/bare-vertical-tab.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 21
+ |
+1 | bare-vertical-tab =
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/control/comment-cr.stderr b/tests/fixtures/invalid/control/comment-cr.stderr
new file mode 100644
index 0000000..fb262e5
--- /dev/null
+++ b/tests/fixtures/invalid/control/comment-cr.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 45
+ |
+1 | comment-cr = "Carriage return in comment" #
+a=1
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/control/comment-del.stderr b/tests/fixtures/invalid/control/comment-del.stderr
new file mode 100644
index 0000000..3d25d68
--- /dev/null
+++ b/tests/fixtures/invalid/control/comment-del.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 24
+ |
+1 | comment-del = "0x7f" # 
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/control/comment-lf.stderr b/tests/fixtures/invalid/control/comment-lf.stderr
new file mode 100644
index 0000000..1613710
--- /dev/null
+++ b/tests/fixtures/invalid/control/comment-lf.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 25
+ |
+1 | comment-lf = "ctrl-P" # 
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/control/comment-null.stderr b/tests/fixtures/invalid/control/comment-null.stderr
new file mode 100644
index 0000000..4955b9d
--- /dev/null
+++ b/tests/fixtures/invalid/control/comment-null.stderr
Binary files differ
diff --git a/tests/fixtures/invalid/control/comment-us.stderr b/tests/fixtures/invalid/control/comment-us.stderr
new file mode 100644
index 0000000..b48d4f3
--- /dev/null
+++ b/tests/fixtures/invalid/control/comment-us.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 25
+ |
+1 | comment-us = "ctrl-_" # 
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/control/control.stderr b/tests/fixtures/invalid/control/control.stderr
new file mode 100644
index 0000000..486aacf
--- /dev/null
+++ b/tests/fixtures/invalid/control/control.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 9, column 22
+ |
+9 | string-null = "null\x00"
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/control/multi-del.stderr b/tests/fixtures/invalid/control/multi-del.stderr
new file mode 100644
index 0000000..62702da
--- /dev/null
+++ b/tests/fixtures/invalid/control/multi-del.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 20
+ |
+1 | multi-del = """null"""
+ | ^
+invalid multiline basic string
diff --git a/tests/fixtures/invalid/control/multi-lf.stderr b/tests/fixtures/invalid/control/multi-lf.stderr
new file mode 100644
index 0000000..7b7a138
--- /dev/null
+++ b/tests/fixtures/invalid/control/multi-lf.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 19
+ |
+1 | multi-lf = """null"""
+ | ^
+invalid multiline basic string
diff --git a/tests/fixtures/invalid/control/multi-null.stderr b/tests/fixtures/invalid/control/multi-null.stderr
new file mode 100644
index 0000000..2d3c335
--- /dev/null
+++ b/tests/fixtures/invalid/control/multi-null.stderr
Binary files differ
diff --git a/tests/fixtures/invalid/control/multi-us.stderr b/tests/fixtures/invalid/control/multi-us.stderr
new file mode 100644
index 0000000..cf8e732
--- /dev/null
+++ b/tests/fixtures/invalid/control/multi-us.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 19
+ |
+1 | multi-us = """null"""
+ | ^
+invalid multiline basic string
diff --git a/tests/fixtures/invalid/control/rawmulti-del.stderr b/tests/fixtures/invalid/control/rawmulti-del.stderr
new file mode 100644
index 0000000..3beeae0
--- /dev/null
+++ b/tests/fixtures/invalid/control/rawmulti-del.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 23
+ |
+1 | rawmulti-del = '''null'''
+ | ^
+invalid multiline literal string
diff --git a/tests/fixtures/invalid/control/rawmulti-lf.stderr b/tests/fixtures/invalid/control/rawmulti-lf.stderr
new file mode 100644
index 0000000..40782a2
--- /dev/null
+++ b/tests/fixtures/invalid/control/rawmulti-lf.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 22
+ |
+1 | rawmulti-lf = '''null'''
+ | ^
+invalid multiline literal string
diff --git a/tests/fixtures/invalid/control/rawmulti-null.stderr b/tests/fixtures/invalid/control/rawmulti-null.stderr
new file mode 100644
index 0000000..d583ce6
--- /dev/null
+++ b/tests/fixtures/invalid/control/rawmulti-null.stderr
Binary files differ
diff --git a/tests/fixtures/invalid/control/rawmulti-us.stderr b/tests/fixtures/invalid/control/rawmulti-us.stderr
new file mode 100644
index 0000000..d413d54
--- /dev/null
+++ b/tests/fixtures/invalid/control/rawmulti-us.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 22
+ |
+1 | rawmulti-us = '''null'''
+ | ^
+invalid multiline literal string
diff --git a/tests/fixtures/invalid/control/rawstring-del.stderr b/tests/fixtures/invalid/control/rawstring-del.stderr
new file mode 100644
index 0000000..640ba46
--- /dev/null
+++ b/tests/fixtures/invalid/control/rawstring-del.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 22
+ |
+1 | rawstring-del = 'null'
+ | ^
+invalid literal string
diff --git a/tests/fixtures/invalid/control/rawstring-lf.stderr b/tests/fixtures/invalid/control/rawstring-lf.stderr
new file mode 100644
index 0000000..e6499b6
--- /dev/null
+++ b/tests/fixtures/invalid/control/rawstring-lf.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 21
+ |
+1 | rawstring-lf = 'null'
+ | ^
+invalid literal string
diff --git a/tests/fixtures/invalid/control/rawstring-null.stderr b/tests/fixtures/invalid/control/rawstring-null.stderr
new file mode 100644
index 0000000..9227d09
--- /dev/null
+++ b/tests/fixtures/invalid/control/rawstring-null.stderr
Binary files differ
diff --git a/tests/fixtures/invalid/control/rawstring-us.stderr b/tests/fixtures/invalid/control/rawstring-us.stderr
new file mode 100644
index 0000000..492cdf7
--- /dev/null
+++ b/tests/fixtures/invalid/control/rawstring-us.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 21
+ |
+1 | rawstring-us = 'null'
+ | ^
+invalid literal string
diff --git a/tests/fixtures/invalid/control/string-bs.stderr b/tests/fixtures/invalid/control/string-bs.stderr
new file mode 100644
index 0000000..556ba1d
--- /dev/null
+++ b/tests/fixtures/invalid/control/string-bs.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 23
+ |
+1 | string-bs = "backspace"
+ | ^
+invalid basic string
diff --git a/tests/fixtures/invalid/control/string-del.stderr b/tests/fixtures/invalid/control/string-del.stderr
new file mode 100644
index 0000000..85d7af3
--- /dev/null
+++ b/tests/fixtures/invalid/control/string-del.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 19
+ |
+1 | string-del = "null"
+ | ^
+invalid basic string
diff --git a/tests/fixtures/invalid/control/string-lf.stderr b/tests/fixtures/invalid/control/string-lf.stderr
new file mode 100644
index 0000000..fbf0d1a
--- /dev/null
+++ b/tests/fixtures/invalid/control/string-lf.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 18
+ |
+1 | string-lf = "null"
+ | ^
+invalid basic string
diff --git a/tests/fixtures/invalid/control/string-null.stderr b/tests/fixtures/invalid/control/string-null.stderr
new file mode 100644
index 0000000..e9fc0be
--- /dev/null
+++ b/tests/fixtures/invalid/control/string-null.stderr
Binary files differ
diff --git a/tests/fixtures/invalid/control/string-us.stderr b/tests/fixtures/invalid/control/string-us.stderr
new file mode 100644
index 0000000..8278e57
--- /dev/null
+++ b/tests/fixtures/invalid/control/string-us.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 18
+ |
+1 | string-us = "null"
+ | ^
+invalid basic string
diff --git a/tests/fixtures/invalid/datetime/hour-over.stderr b/tests/fixtures/invalid/datetime/hour-over.stderr
new file mode 100644
index 0000000..0e6747d
--- /dev/null
+++ b/tests/fixtures/invalid/datetime/hour-over.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 2, column 15
+ |
+2 | d = 2006-01-01T24:00:00-00:00
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/datetime/mday-over.stderr b/tests/fixtures/invalid/datetime/mday-over.stderr
new file mode 100644
index 0000000..edb9769
--- /dev/null
+++ b/tests/fixtures/invalid/datetime/mday-over.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 3, column 13
+ |
+3 | d = 2006-01-32T00:00:00-00:00
+ | ^
+invalid date-time
+value is out of range
diff --git a/tests/fixtures/invalid/datetime/mday-under.stderr b/tests/fixtures/invalid/datetime/mday-under.stderr
new file mode 100644
index 0000000..6af2050
--- /dev/null
+++ b/tests/fixtures/invalid/datetime/mday-under.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 3, column 13
+ |
+3 | d = 2006-01-00T00:00:00-00:00
+ | ^
+invalid date-time
+value is out of range
diff --git a/tests/fixtures/invalid/datetime/minute-over.stderr b/tests/fixtures/invalid/datetime/minute-over.stderr
new file mode 100644
index 0000000..20f3b4b
--- /dev/null
+++ b/tests/fixtures/invalid/datetime/minute-over.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 2, column 19
+ |
+2 | d = 2006-01-01T00:60:00-00:00
+ | ^
+invalid date-time
+value is out of range
diff --git a/tests/fixtures/invalid/datetime/month-over.stderr b/tests/fixtures/invalid/datetime/month-over.stderr
new file mode 100644
index 0000000..3333475
--- /dev/null
+++ b/tests/fixtures/invalid/datetime/month-over.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 2, column 10
+ |
+2 | d = 2006-13-01T00:00:00-00:00
+ | ^
+invalid date-time
+value is out of range
diff --git a/tests/fixtures/invalid/datetime/month-under.stderr b/tests/fixtures/invalid/datetime/month-under.stderr
new file mode 100644
index 0000000..5d0cd06
--- /dev/null
+++ b/tests/fixtures/invalid/datetime/month-under.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 2, column 10
+ |
+2 | d = 2007-00-01T00:00:00-00:00
+ | ^
+invalid date-time
+value is out of range
diff --git a/tests/fixtures/invalid/datetime/no-leads-with-milli.stderr b/tests/fixtures/invalid/datetime/no-leads-with-milli.stderr
new file mode 100644
index 0000000..df9190d
--- /dev/null
+++ b/tests/fixtures/invalid/datetime/no-leads-with-milli.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 2, column 22
+ |
+2 | with-milli = 1987-07-5T17:45:00.12Z
+ | ^
+invalid date-time
diff --git a/tests/fixtures/invalid/datetime/no-leads.stderr b/tests/fixtures/invalid/datetime/no-leads.stderr
new file mode 100644
index 0000000..3e0ccf7
--- /dev/null
+++ b/tests/fixtures/invalid/datetime/no-leads.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 2, column 17
+ |
+2 | no-leads = 1987-7-05T17:45:00Z
+ | ^
+invalid date-time
diff --git a/tests/fixtures/invalid/datetime/no-secs.stderr b/tests/fixtures/invalid/datetime/no-secs.stderr
new file mode 100644
index 0000000..078d0a0
--- /dev/null
+++ b/tests/fixtures/invalid/datetime/no-secs.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 2, column 27
+ |
+2 | no-secs = 1987-07-05T17:45Z
+ | ^
+invalid date-time
diff --git a/tests/fixtures/invalid/datetime/no-t.stderr b/tests/fixtures/invalid/datetime/no-t.stderr
new file mode 100644
index 0000000..15e9554
--- /dev/null
+++ b/tests/fixtures/invalid/datetime/no-t.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 2, column 18
+ |
+2 | no-t = 1987-07-0517:45:00Z
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/datetime/second-over.stderr b/tests/fixtures/invalid/datetime/second-over.stderr
new file mode 100644
index 0000000..186a38d
--- /dev/null
+++ b/tests/fixtures/invalid/datetime/second-over.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 3, column 22
+ |
+3 | d = 2006-01-01T00:00:61-00:00
+ | ^
+invalid date-time
+value is out of range
diff --git a/tests/fixtures/invalid/datetime/time-no-leads-2.stderr b/tests/fixtures/invalid/datetime/time-no-leads-2.stderr
new file mode 100644
index 0000000..fd77fb2
--- /dev/null
+++ b/tests/fixtures/invalid/datetime/time-no-leads-2.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 2, column 11
+ |
+2 | d = 01:32:0
+ | ^
+invalid time
diff --git a/tests/fixtures/invalid/datetime/time-no-leads.stderr b/tests/fixtures/invalid/datetime/time-no-leads.stderr
new file mode 100644
index 0000000..7a98902
--- /dev/null
+++ b/tests/fixtures/invalid/datetime/time-no-leads.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 2, column 6
+ |
+2 | d = 1:32:00
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/datetime/trailing-t.stderr b/tests/fixtures/invalid/datetime/trailing-t.stderr
new file mode 100644
index 0000000..061ec26
--- /dev/null
+++ b/tests/fixtures/invalid/datetime/trailing-t.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 2, column 15
+ |
+2 | d = 2006-01-30T
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/encoding/bad-utf8-at-end.stderr b/tests/fixtures/invalid/encoding/bad-utf8-at-end.stderr
new file mode 100644
index 0000000..7a11cf9
--- /dev/null
+++ b/tests/fixtures/invalid/encoding/bad-utf8-at-end.stderr
@@ -0,0 +1 @@
+incomplete utf-8 byte sequence from index 241 \ No newline at end of file
diff --git a/tests/fixtures/invalid/encoding/bad-utf8-in-comment.stderr b/tests/fixtures/invalid/encoding/bad-utf8-in-comment.stderr
new file mode 100644
index 0000000..72d1465
--- /dev/null
+++ b/tests/fixtures/invalid/encoding/bad-utf8-in-comment.stderr
@@ -0,0 +1 @@
+invalid utf-8 sequence of 1 bytes from index 2 \ No newline at end of file
diff --git a/tests/fixtures/invalid/encoding/bad-utf8-in-multiline-literal.stderr b/tests/fixtures/invalid/encoding/bad-utf8-in-multiline-literal.stderr
new file mode 100644
index 0000000..0c70f28
--- /dev/null
+++ b/tests/fixtures/invalid/encoding/bad-utf8-in-multiline-literal.stderr
@@ -0,0 +1 @@
+invalid utf-8 sequence of 1 bytes from index 66 \ No newline at end of file
diff --git a/tests/fixtures/invalid/encoding/bad-utf8-in-multiline.stderr b/tests/fixtures/invalid/encoding/bad-utf8-in-multiline.stderr
new file mode 100644
index 0000000..0c70f28
--- /dev/null
+++ b/tests/fixtures/invalid/encoding/bad-utf8-in-multiline.stderr
@@ -0,0 +1 @@
+invalid utf-8 sequence of 1 bytes from index 66 \ No newline at end of file
diff --git a/tests/fixtures/invalid/encoding/bad-utf8-in-string-literal.stderr b/tests/fixtures/invalid/encoding/bad-utf8-in-string-literal.stderr
new file mode 100644
index 0000000..9e1a687
--- /dev/null
+++ b/tests/fixtures/invalid/encoding/bad-utf8-in-string-literal.stderr
@@ -0,0 +1 @@
+invalid utf-8 sequence of 1 bytes from index 64 \ No newline at end of file
diff --git a/tests/fixtures/invalid/encoding/bad-utf8-in-string.stderr b/tests/fixtures/invalid/encoding/bad-utf8-in-string.stderr
new file mode 100644
index 0000000..9e1a687
--- /dev/null
+++ b/tests/fixtures/invalid/encoding/bad-utf8-in-string.stderr
@@ -0,0 +1 @@
+invalid utf-8 sequence of 1 bytes from index 64 \ No newline at end of file
diff --git a/tests/fixtures/invalid/encoding/bom-not-at-start-1.stderr b/tests/fixtures/invalid/encoding/bom-not-at-start-1.stderr
new file mode 100644
index 0000000..8d9d90b
--- /dev/null
+++ b/tests/fixtures/invalid/encoding/bom-not-at-start-1.stderr
@@ -0,0 +1 @@
+invalid utf-8 sequence of 1 bytes from index 17 \ No newline at end of file
diff --git a/tests/fixtures/invalid/encoding/bom-not-at-start-2.stderr b/tests/fixtures/invalid/encoding/bom-not-at-start-2.stderr
new file mode 100644
index 0000000..8fce408
--- /dev/null
+++ b/tests/fixtures/invalid/encoding/bom-not-at-start-2.stderr
@@ -0,0 +1 @@
+invalid utf-8 sequence of 1 bytes from index 18 \ No newline at end of file
diff --git a/tests/fixtures/invalid/encoding/utf16-bom.stderr b/tests/fixtures/invalid/encoding/utf16-bom.stderr
new file mode 100644
index 0000000..b1b9eed
--- /dev/null
+++ b/tests/fixtures/invalid/encoding/utf16-bom.stderr
@@ -0,0 +1 @@
+invalid utf-8 sequence of 1 bytes from index 0 \ No newline at end of file
diff --git a/tests/fixtures/invalid/encoding/utf16.stderr b/tests/fixtures/invalid/encoding/utf16.stderr
new file mode 100644
index 0000000..6886165
--- /dev/null
+++ b/tests/fixtures/invalid/encoding/utf16.stderr
Binary files differ
diff --git a/tests/fixtures/invalid/float/double-point-1.stderr b/tests/fixtures/invalid/float/double-point-1.stderr
new file mode 100644
index 0000000..390520f
--- /dev/null
+++ b/tests/fixtures/invalid/float/double-point-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+ |
+1 | double-point-1 = 0..1
+ | ^
+invalid floating-point number
+expected digit
diff --git a/tests/fixtures/invalid/float/double-point-2.stderr b/tests/fixtures/invalid/float/double-point-2.stderr
new file mode 100644
index 0000000..a020a48
--- /dev/null
+++ b/tests/fixtures/invalid/float/double-point-2.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 21
+ |
+1 | double-point-2 = 0.1.2
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/float/exp-double-e-1.stderr b/tests/fixtures/invalid/float/exp-double-e-1.stderr
new file mode 100644
index 0000000..5411839
--- /dev/null
+++ b/tests/fixtures/invalid/float/exp-double-e-1.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 20
+ |
+1 | exp-double-e-1 = 1ee2
+ | ^
+invalid floating-point number
diff --git a/tests/fixtures/invalid/float/exp-double-e-2.stderr b/tests/fixtures/invalid/float/exp-double-e-2.stderr
new file mode 100644
index 0000000..1b99d37
--- /dev/null
+++ b/tests/fixtures/invalid/float/exp-double-e-2.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 21
+ |
+1 | exp-double-e-2 = 1e2e3
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/float/exp-double-us.stderr b/tests/fixtures/invalid/float/exp-double-us.stderr
new file mode 100644
index 0000000..6a66e43
--- /dev/null
+++ b/tests/fixtures/invalid/float/exp-double-us.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 19
+ |
+1 | exp-double-us = 1e__23
+ | ^
+invalid floating-point number
diff --git a/tests/fixtures/invalid/float/exp-leading-us.stderr b/tests/fixtures/invalid/float/exp-leading-us.stderr
new file mode 100644
index 0000000..7696686
--- /dev/null
+++ b/tests/fixtures/invalid/float/exp-leading-us.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 20
+ |
+1 | exp-leading-us = 1e_23
+ | ^
+invalid floating-point number
diff --git a/tests/fixtures/invalid/float/exp-point-1.stderr b/tests/fixtures/invalid/float/exp-point-1.stderr
new file mode 100644
index 0000000..87f0a0b
--- /dev/null
+++ b/tests/fixtures/invalid/float/exp-point-1.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 18
+ |
+1 | exp-point-1 = 1e2.3
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/float/exp-point-2.stderr b/tests/fixtures/invalid/float/exp-point-2.stderr
new file mode 100644
index 0000000..8089050
--- /dev/null
+++ b/tests/fixtures/invalid/float/exp-point-2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 17
+ |
+1 | exp-point-2 = 1.e2
+ | ^
+invalid floating-point number
+expected digit
diff --git a/tests/fixtures/invalid/float/exp-trailing-us.stderr b/tests/fixtures/invalid/float/exp-trailing-us.stderr
new file mode 100644
index 0000000..9a28184
--- /dev/null
+++ b/tests/fixtures/invalid/float/exp-trailing-us.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 21
+ |
+1 | exp-trailing-us = 1e_23_
+ | ^
+invalid floating-point number
diff --git a/tests/fixtures/invalid/float/float.stderr b/tests/fixtures/invalid/float/float.stderr
new file mode 100644
index 0000000..627650f
--- /dev/null
+++ b/tests/fixtures/invalid/float/float.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 17
+ |
+1 | leading-zero = 03.14
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/float/inf-incomplete-1.stderr b/tests/fixtures/invalid/float/inf-incomplete-1.stderr
new file mode 100644
index 0000000..99fe59b
--- /dev/null
+++ b/tests/fixtures/invalid/float/inf-incomplete-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+ |
+1 | inf-incomplete-1 = in
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/float/inf-incomplete-2.stderr b/tests/fixtures/invalid/float/inf-incomplete-2.stderr
new file mode 100644
index 0000000..38ef4cf
--- /dev/null
+++ b/tests/fixtures/invalid/float/inf-incomplete-2.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 21
+ |
+1 | inf-incomplete-2 = +in
+ | ^
+invalid integer
diff --git a/tests/fixtures/invalid/float/inf-incomplete-3.stderr b/tests/fixtures/invalid/float/inf-incomplete-3.stderr
new file mode 100644
index 0000000..af66478
--- /dev/null
+++ b/tests/fixtures/invalid/float/inf-incomplete-3.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 21
+ |
+1 | inf-incomplete-3 = -in
+ | ^
+invalid integer
diff --git a/tests/fixtures/invalid/float/inf_underscore.stderr b/tests/fixtures/invalid/float/inf_underscore.stderr
new file mode 100644
index 0000000..8254842
--- /dev/null
+++ b/tests/fixtures/invalid/float/inf_underscore.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 18
+ |
+1 | inf_underscore = in_f
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/float/leading-point-neg.stderr b/tests/fixtures/invalid/float/leading-point-neg.stderr
new file mode 100644
index 0000000..c46efe1
--- /dev/null
+++ b/tests/fixtures/invalid/float/leading-point-neg.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 22
+ |
+1 | leading-point-neg = -.12345
+ | ^
+invalid integer
diff --git a/tests/fixtures/invalid/float/leading-point-plus.stderr b/tests/fixtures/invalid/float/leading-point-plus.stderr
new file mode 100644
index 0000000..a643a7e
--- /dev/null
+++ b/tests/fixtures/invalid/float/leading-point-plus.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 23
+ |
+1 | leading-point-plus = +.12345
+ | ^
+invalid integer
diff --git a/tests/fixtures/invalid/float/leading-point.stderr b/tests/fixtures/invalid/float/leading-point.stderr
new file mode 100644
index 0000000..65e66b0
--- /dev/null
+++ b/tests/fixtures/invalid/float/leading-point.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 17
+ |
+1 | leading-point = .12345
+ | ^
+invalid floating-point number
+expected leading digit
diff --git a/tests/fixtures/invalid/float/leading-us.stderr b/tests/fixtures/invalid/float/leading-us.stderr
new file mode 100644
index 0000000..e6d8e52
--- /dev/null
+++ b/tests/fixtures/invalid/float/leading-us.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 14
+ |
+1 | leading-us = _1.2
+ | ^
+invalid integer
+expected leading digit
diff --git a/tests/fixtures/invalid/float/leading-zero-neg.stderr b/tests/fixtures/invalid/float/leading-zero-neg.stderr
new file mode 100644
index 0000000..a60ec4d
--- /dev/null
+++ b/tests/fixtures/invalid/float/leading-zero-neg.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 22
+ |
+1 | leading-zero-neg = -03.14
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/float/leading-zero-plus.stderr b/tests/fixtures/invalid/float/leading-zero-plus.stderr
new file mode 100644
index 0000000..f7c612b
--- /dev/null
+++ b/tests/fixtures/invalid/float/leading-zero-plus.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 23
+ |
+1 | leading-zero-plus = +03.14
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/float/leading-zero.stderr b/tests/fixtures/invalid/float/leading-zero.stderr
new file mode 100644
index 0000000..627650f
--- /dev/null
+++ b/tests/fixtures/invalid/float/leading-zero.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 17
+ |
+1 | leading-zero = 03.14
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/float/nan-incomplete-1.stderr b/tests/fixtures/invalid/float/nan-incomplete-1.stderr
new file mode 100644
index 0000000..36faeb0
--- /dev/null
+++ b/tests/fixtures/invalid/float/nan-incomplete-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+ |
+1 | nan-incomplete-1 = na
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/float/nan-incomplete-2.stderr b/tests/fixtures/invalid/float/nan-incomplete-2.stderr
new file mode 100644
index 0000000..38e0151
--- /dev/null
+++ b/tests/fixtures/invalid/float/nan-incomplete-2.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 21
+ |
+1 | nan-incomplete-2 = +na
+ | ^
+invalid integer
diff --git a/tests/fixtures/invalid/float/nan-incomplete-3.stderr b/tests/fixtures/invalid/float/nan-incomplete-3.stderr
new file mode 100644
index 0000000..e03ff4b
--- /dev/null
+++ b/tests/fixtures/invalid/float/nan-incomplete-3.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 21
+ |
+1 | nan-incomplete-3 = -na
+ | ^
+invalid integer
diff --git a/tests/fixtures/invalid/float/nan_underscore.stderr b/tests/fixtures/invalid/float/nan_underscore.stderr
new file mode 100644
index 0000000..f6dead3
--- /dev/null
+++ b/tests/fixtures/invalid/float/nan_underscore.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 18
+ |
+1 | nan_underscore = na_n
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/float/trailing-point-min.stderr b/tests/fixtures/invalid/float/trailing-point-min.stderr
new file mode 100644
index 0000000..41f4ad9
--- /dev/null
+++ b/tests/fixtures/invalid/float/trailing-point-min.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 25
+ |
+1 | trailing-point-min = -1.
+ | ^
+invalid floating-point number
+expected digit
diff --git a/tests/fixtures/invalid/float/trailing-point-plus.stderr b/tests/fixtures/invalid/float/trailing-point-plus.stderr
new file mode 100644
index 0000000..9927935
--- /dev/null
+++ b/tests/fixtures/invalid/float/trailing-point-plus.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 26
+ |
+1 | trailing-point-plus = +1.
+ | ^
+invalid floating-point number
+expected digit
diff --git a/tests/fixtures/invalid/float/trailing-point.stderr b/tests/fixtures/invalid/float/trailing-point.stderr
new file mode 100644
index 0000000..bd345d2
--- /dev/null
+++ b/tests/fixtures/invalid/float/trailing-point.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+ |
+1 | trailing-point = 1.
+ | ^
+invalid floating-point number
+expected digit
diff --git a/tests/fixtures/invalid/float/trailing-us-exp.stderr b/tests/fixtures/invalid/float/trailing-us-exp.stderr
new file mode 100644
index 0000000..811f951
--- /dev/null
+++ b/tests/fixtures/invalid/float/trailing-us-exp.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 2, column 21
+ |
+2 | trailing-us-exp = 1_e2
+ | ^
+invalid integer
+expected digit
diff --git a/tests/fixtures/invalid/float/trailing-us.stderr b/tests/fixtures/invalid/float/trailing-us.stderr
new file mode 100644
index 0000000..aa4f288
--- /dev/null
+++ b/tests/fixtures/invalid/float/trailing-us.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 19
+ |
+1 | trailing-us = 1.2_
+ | ^
+invalid floating-point number
+expected digit, digit
diff --git a/tests/fixtures/invalid/float/us-after-point.stderr b/tests/fixtures/invalid/float/us-after-point.stderr
new file mode 100644
index 0000000..d93821d
--- /dev/null
+++ b/tests/fixtures/invalid/float/us-after-point.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+ |
+1 | us-after-point = 1._2
+ | ^
+invalid floating-point number
+expected digit
diff --git a/tests/fixtures/invalid/float/us-before-point.stderr b/tests/fixtures/invalid/float/us-before-point.stderr
new file mode 100644
index 0000000..109d8f7
--- /dev/null
+++ b/tests/fixtures/invalid/float/us-before-point.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 21
+ |
+1 | us-before-point = 1_.2
+ | ^
+invalid integer
+expected digit
diff --git a/tests/fixtures/invalid/inline-table/add.stderr b/tests/fixtures/invalid/inline-table/add.stderr
new file mode 100644
index 0000000..4108142
--- /dev/null
+++ b/tests/fixtures/invalid/inline-table/add.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 3, column 1
+ |
+3 | [a.b]
+ | ^
+invalid table header
+dotted key `a` attempted to extend non-table type (inline table)
diff --git a/tests/fixtures/invalid/inline-table/double-comma.stderr b/tests/fixtures/invalid/inline-table/double-comma.stderr
new file mode 100644
index 0000000..ab4772b
--- /dev/null
+++ b/tests/fixtures/invalid/inline-table/double-comma.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 9
+ |
+1 | t = {x=3,,y=4}
+ | ^
+invalid inline table
+expected `}`
diff --git a/tests/fixtures/invalid/inline-table/duplicate-key.stderr b/tests/fixtures/invalid/inline-table/duplicate-key.stderr
new file mode 100644
index 0000000..acee967
--- /dev/null
+++ b/tests/fixtures/invalid/inline-table/duplicate-key.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 2, column 4
+ |
+2 | a={b=1, b=2}
+ | ^
+duplicate key `b`
diff --git a/tests/fixtures/invalid/inline-table/empty.stderr b/tests/fixtures/invalid/inline-table/empty.stderr
new file mode 100644
index 0000000..0fcfd34
--- /dev/null
+++ b/tests/fixtures/invalid/inline-table/empty.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 6
+ |
+1 | t = {,}
+ | ^
+invalid inline table
+expected `}`
diff --git a/tests/fixtures/invalid/inline-table/linebreak-1.stderr b/tests/fixtures/invalid/inline-table/linebreak-1.stderr
new file mode 100644
index 0000000..ed67c3a
--- /dev/null
+++ b/tests/fixtures/invalid/inline-table/linebreak-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 3, column 18
+ |
+3 | simple = { a = 1
+ | ^
+invalid inline table
+expected `}`
diff --git a/tests/fixtures/invalid/inline-table/linebreak-2.stderr b/tests/fixtures/invalid/inline-table/linebreak-2.stderr
new file mode 100644
index 0000000..a815758
--- /dev/null
+++ b/tests/fixtures/invalid/inline-table/linebreak-2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 9
+ |
+1 | t = {a=1,
+ | ^
+invalid inline table
+expected `}`
diff --git a/tests/fixtures/invalid/inline-table/linebreak-3.stderr b/tests/fixtures/invalid/inline-table/linebreak-3.stderr
new file mode 100644
index 0000000..4aff61b
--- /dev/null
+++ b/tests/fixtures/invalid/inline-table/linebreak-3.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 9
+ |
+1 | t = {a=1
+ | ^
+invalid inline table
+expected `}`
diff --git a/tests/fixtures/invalid/inline-table/linebreak-4.stderr b/tests/fixtures/invalid/inline-table/linebreak-4.stderr
new file mode 100644
index 0000000..658456b
--- /dev/null
+++ b/tests/fixtures/invalid/inline-table/linebreak-4.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 14
+ |
+1 | json_like = {
+ | ^
+invalid inline table
+expected `}`
diff --git a/tests/fixtures/invalid/inline-table/no-comma.stderr b/tests/fixtures/invalid/inline-table/no-comma.stderr
new file mode 100644
index 0000000..8955218
--- /dev/null
+++ b/tests/fixtures/invalid/inline-table/no-comma.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 12
+ |
+1 | t = {x = 3 y = 4}
+ | ^
+invalid inline table
+expected `}`
diff --git a/tests/fixtures/invalid/inline-table/overwrite.stderr b/tests/fixtures/invalid/inline-table/overwrite.stderr
new file mode 100644
index 0000000..5c48ee2
--- /dev/null
+++ b/tests/fixtures/invalid/inline-table/overwrite.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 3, column 1
+ |
+3 | a={}
+ | ^
+duplicate key `a` in document root
diff --git a/tests/fixtures/invalid/inline-table/trailing-comma.stderr b/tests/fixtures/invalid/inline-table/trailing-comma.stderr
new file mode 100644
index 0000000..b17fca5
--- /dev/null
+++ b/tests/fixtures/invalid/inline-table/trailing-comma.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 3, column 18
+ |
+3 | abc = { abc = 123, }
+ | ^
+invalid inline table
+expected `}`
diff --git a/tests/fixtures/invalid/integer/capital-bin.stderr b/tests/fixtures/invalid/integer/capital-bin.stderr
new file mode 100644
index 0000000..333ba92
--- /dev/null
+++ b/tests/fixtures/invalid/integer/capital-bin.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 16
+ |
+1 | capital-bin = 0B0
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/capital-hex.stderr b/tests/fixtures/invalid/integer/capital-hex.stderr
new file mode 100644
index 0000000..ac064f9
--- /dev/null
+++ b/tests/fixtures/invalid/integer/capital-hex.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 16
+ |
+1 | capital-hex = 0X1
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/capital-oct.stderr b/tests/fixtures/invalid/integer/capital-oct.stderr
new file mode 100644
index 0000000..774a8f2
--- /dev/null
+++ b/tests/fixtures/invalid/integer/capital-oct.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 16
+ |
+1 | capital-oct = 0O0
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/double-sign-nex.stderr b/tests/fixtures/invalid/integer/double-sign-nex.stderr
new file mode 100644
index 0000000..542ba06
--- /dev/null
+++ b/tests/fixtures/invalid/integer/double-sign-nex.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 20
+ |
+1 | double-sign-nex = --99
+ | ^
+invalid integer
diff --git a/tests/fixtures/invalid/integer/double-sign-plus.stderr b/tests/fixtures/invalid/integer/double-sign-plus.stderr
new file mode 100644
index 0000000..8f8bad0
--- /dev/null
+++ b/tests/fixtures/invalid/integer/double-sign-plus.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 21
+ |
+1 | double-sign-plus = ++99
+ | ^
+invalid integer
diff --git a/tests/fixtures/invalid/integer/double-us.stderr b/tests/fixtures/invalid/integer/double-us.stderr
new file mode 100644
index 0000000..e229868
--- /dev/null
+++ b/tests/fixtures/invalid/integer/double-us.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 15
+ |
+1 | double-us = 1__23
+ | ^
+invalid integer
+expected digit
diff --git a/tests/fixtures/invalid/integer/incomplete-bin.stderr b/tests/fixtures/invalid/integer/incomplete-bin.stderr
new file mode 100644
index 0000000..13d7b46
--- /dev/null
+++ b/tests/fixtures/invalid/integer/incomplete-bin.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 20
+ |
+1 | incomplete-bin = 0b
+ | ^
+invalid binary integer
diff --git a/tests/fixtures/invalid/integer/incomplete-hex.stderr b/tests/fixtures/invalid/integer/incomplete-hex.stderr
new file mode 100644
index 0000000..ea5073a
--- /dev/null
+++ b/tests/fixtures/invalid/integer/incomplete-hex.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 20
+ |
+1 | incomplete-hex = 0x
+ | ^
+invalid hexadecimal integer
diff --git a/tests/fixtures/invalid/integer/incomplete-oct.stderr b/tests/fixtures/invalid/integer/incomplete-oct.stderr
new file mode 100644
index 0000000..46d51c1
--- /dev/null
+++ b/tests/fixtures/invalid/integer/incomplete-oct.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 20
+ |
+1 | incomplete-oct = 0o
+ | ^
+invalid octal integer
diff --git a/tests/fixtures/invalid/integer/integer.stderr b/tests/fixtures/invalid/integer/integer.stderr
new file mode 100644
index 0000000..f058a2d
--- /dev/null
+++ b/tests/fixtures/invalid/integer/integer.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 19
+ |
+1 | leading-zero-1 = 01
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/invalid-bin.stderr b/tests/fixtures/invalid/integer/invalid-bin.stderr
new file mode 100644
index 0000000..62c00fb
--- /dev/null
+++ b/tests/fixtures/invalid/integer/invalid-bin.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 20
+ |
+1 | invalid-bin = 0b0012
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/invalid-hex.stderr b/tests/fixtures/invalid/integer/invalid-hex.stderr
new file mode 100644
index 0000000..29b112b
--- /dev/null
+++ b/tests/fixtures/invalid/integer/invalid-hex.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 20
+ |
+1 | invalid-hex = 0xaafz
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/invalid-oct.stderr b/tests/fixtures/invalid/integer/invalid-oct.stderr
new file mode 100644
index 0000000..f3e1ada
--- /dev/null
+++ b/tests/fixtures/invalid/integer/invalid-oct.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 19
+ |
+1 | invalid-oct = 0o778
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/leading-us-bin.stderr b/tests/fixtures/invalid/integer/leading-us-bin.stderr
new file mode 100644
index 0000000..c893e75
--- /dev/null
+++ b/tests/fixtures/invalid/integer/leading-us-bin.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 18
+ |
+1 | leading-us-bin = _0o1
+ | ^
+invalid integer
+expected leading digit
diff --git a/tests/fixtures/invalid/integer/leading-us-hex.stderr b/tests/fixtures/invalid/integer/leading-us-hex.stderr
new file mode 100644
index 0000000..12eb8e6
--- /dev/null
+++ b/tests/fixtures/invalid/integer/leading-us-hex.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 18
+ |
+1 | leading-us-hex = _0o1
+ | ^
+invalid integer
+expected leading digit
diff --git a/tests/fixtures/invalid/integer/leading-us-oct.stderr b/tests/fixtures/invalid/integer/leading-us-oct.stderr
new file mode 100644
index 0000000..c670551
--- /dev/null
+++ b/tests/fixtures/invalid/integer/leading-us-oct.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 18
+ |
+1 | leading-us-oct = _0o1
+ | ^
+invalid integer
+expected leading digit
diff --git a/tests/fixtures/invalid/integer/leading-us.stderr b/tests/fixtures/invalid/integer/leading-us.stderr
new file mode 100644
index 0000000..f53275c
--- /dev/null
+++ b/tests/fixtures/invalid/integer/leading-us.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 14
+ |
+1 | leading-us = _123
+ | ^
+invalid integer
+expected leading digit
diff --git a/tests/fixtures/invalid/integer/leading-zero-1.stderr b/tests/fixtures/invalid/integer/leading-zero-1.stderr
new file mode 100644
index 0000000..f058a2d
--- /dev/null
+++ b/tests/fixtures/invalid/integer/leading-zero-1.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 19
+ |
+1 | leading-zero-1 = 01
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/leading-zero-2.stderr b/tests/fixtures/invalid/integer/leading-zero-2.stderr
new file mode 100644
index 0000000..0823814
--- /dev/null
+++ b/tests/fixtures/invalid/integer/leading-zero-2.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 19
+ |
+1 | leading-zero-2 = 00
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/leading-zero-3.stderr b/tests/fixtures/invalid/integer/leading-zero-3.stderr
new file mode 100644
index 0000000..7b0e481
--- /dev/null
+++ b/tests/fixtures/invalid/integer/leading-zero-3.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 19
+ |
+1 | leading-zero-3 = 0_0
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/leading-zero-sign-1.stderr b/tests/fixtures/invalid/integer/leading-zero-sign-1.stderr
new file mode 100644
index 0000000..384c9d4
--- /dev/null
+++ b/tests/fixtures/invalid/integer/leading-zero-sign-1.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 25
+ |
+1 | leading-zero-sign-1 = -01
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/leading-zero-sign-2.stderr b/tests/fixtures/invalid/integer/leading-zero-sign-2.stderr
new file mode 100644
index 0000000..795c329
--- /dev/null
+++ b/tests/fixtures/invalid/integer/leading-zero-sign-2.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 25
+ |
+1 | leading-zero-sign-2 = +01
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/leading-zero-sign-3.stderr b/tests/fixtures/invalid/integer/leading-zero-sign-3.stderr
new file mode 100644
index 0000000..639aef5
--- /dev/null
+++ b/tests/fixtures/invalid/integer/leading-zero-sign-3.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 25
+ |
+1 | leading-zero-sign-3 = +0_1
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/negative-bin.stderr b/tests/fixtures/invalid/integer/negative-bin.stderr
new file mode 100644
index 0000000..7122d38
--- /dev/null
+++ b/tests/fixtures/invalid/integer/negative-bin.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 18
+ |
+1 | negative-bin = -0b11010110
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/negative-hex.stderr b/tests/fixtures/invalid/integer/negative-hex.stderr
new file mode 100644
index 0000000..0c7e584
--- /dev/null
+++ b/tests/fixtures/invalid/integer/negative-hex.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 18
+ |
+1 | negative-hex = -0xff
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/negative-oct.stderr b/tests/fixtures/invalid/integer/negative-oct.stderr
new file mode 100644
index 0000000..fcf3140
--- /dev/null
+++ b/tests/fixtures/invalid/integer/negative-oct.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 18
+ |
+1 | negative-oct = -0o99
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/positive-bin.stderr b/tests/fixtures/invalid/integer/positive-bin.stderr
new file mode 100644
index 0000000..bc5dc4e
--- /dev/null
+++ b/tests/fixtures/invalid/integer/positive-bin.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 18
+ |
+1 | positive-bin = +0b11010110
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/positive-hex.stderr b/tests/fixtures/invalid/integer/positive-hex.stderr
new file mode 100644
index 0000000..f571114
--- /dev/null
+++ b/tests/fixtures/invalid/integer/positive-hex.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 18
+ |
+1 | positive-hex = +0xff
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/positive-oct.stderr b/tests/fixtures/invalid/integer/positive-oct.stderr
new file mode 100644
index 0000000..cc09466
--- /dev/null
+++ b/tests/fixtures/invalid/integer/positive-oct.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 18
+ |
+1 | positive-oct = +0o99
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/text-after-integer.stderr b/tests/fixtures/invalid/integer/text-after-integer.stderr
new file mode 100644
index 0000000..0405cb4
--- /dev/null
+++ b/tests/fixtures/invalid/integer/text-after-integer.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 13
+ |
+1 | answer = 42 the ultimate answer?
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/integer/trailing-us-bin.stderr b/tests/fixtures/invalid/integer/trailing-us-bin.stderr
new file mode 100644
index 0000000..7042a6e
--- /dev/null
+++ b/tests/fixtures/invalid/integer/trailing-us-bin.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 23
+ |
+1 | trailing-us-bin = 0b1_
+ | ^
+invalid binary integer
+expected digit
diff --git a/tests/fixtures/invalid/integer/trailing-us-hex.stderr b/tests/fixtures/invalid/integer/trailing-us-hex.stderr
new file mode 100644
index 0000000..9526a94
--- /dev/null
+++ b/tests/fixtures/invalid/integer/trailing-us-hex.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 23
+ |
+1 | trailing-us-hex = 0x1_
+ | ^
+invalid hexadecimal integer
+expected digit
diff --git a/tests/fixtures/invalid/integer/trailing-us-oct.stderr b/tests/fixtures/invalid/integer/trailing-us-oct.stderr
new file mode 100644
index 0000000..bb4330d
--- /dev/null
+++ b/tests/fixtures/invalid/integer/trailing-us-oct.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 23
+ |
+1 | trailing-us-oct = 0o1_
+ | ^
+invalid octal integer
+expected digit
diff --git a/tests/fixtures/invalid/integer/trailing-us.stderr b/tests/fixtures/invalid/integer/trailing-us.stderr
new file mode 100644
index 0000000..f8c901e
--- /dev/null
+++ b/tests/fixtures/invalid/integer/trailing-us.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 19
+ |
+1 | trailing-us = 123_
+ | ^
+invalid integer
+expected digit
diff --git a/tests/fixtures/invalid/integer/us-after-bin.stderr b/tests/fixtures/invalid/integer/us-after-bin.stderr
new file mode 100644
index 0000000..1620bc6
--- /dev/null
+++ b/tests/fixtures/invalid/integer/us-after-bin.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 18
+ |
+1 | us-after-bin = 0b_1
+ | ^
+invalid binary integer
diff --git a/tests/fixtures/invalid/integer/us-after-hex.stderr b/tests/fixtures/invalid/integer/us-after-hex.stderr
new file mode 100644
index 0000000..e8283a0
--- /dev/null
+++ b/tests/fixtures/invalid/integer/us-after-hex.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 18
+ |
+1 | us-after-hex = 0x_1
+ | ^
+invalid hexadecimal integer
diff --git a/tests/fixtures/invalid/integer/us-after-oct.stderr b/tests/fixtures/invalid/integer/us-after-oct.stderr
new file mode 100644
index 0000000..e6884d0
--- /dev/null
+++ b/tests/fixtures/invalid/integer/us-after-oct.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 18
+ |
+1 | us-after-oct = 0o_1
+ | ^
+invalid octal integer
diff --git a/tests/fixtures/invalid/key/after-array.stderr b/tests/fixtures/invalid/key/after-array.stderr
new file mode 100644
index 0000000..861f82d
--- /dev/null
+++ b/tests/fixtures/invalid/key/after-array.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 14
+ |
+1 | [[agencies]] owner = "S Cjelli"
+ | ^
+invalid table header
+expected newline, `#`
diff --git a/tests/fixtures/invalid/key/after-table.stderr b/tests/fixtures/invalid/key/after-table.stderr
new file mode 100644
index 0000000..499a430
--- /dev/null
+++ b/tests/fixtures/invalid/key/after-table.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 9
+ |
+1 | [error] this = "should not be here"
+ | ^
+invalid table header
+expected newline, `#`
diff --git a/tests/fixtures/invalid/key/after-value.stderr b/tests/fixtures/invalid/key/after-value.stderr
new file mode 100644
index 0000000..9852ec8
--- /dev/null
+++ b/tests/fixtures/invalid/key/after-value.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 15
+ |
+1 | first = "Tom" last = "Preston-Werner" # INVALID
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/key/bare-invalid-character.stderr b/tests/fixtures/invalid/key/bare-invalid-character.stderr
new file mode 100644
index 0000000..0ba21fd
--- /dev/null
+++ b/tests/fixtures/invalid/key/bare-invalid-character.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 5
+ |
+1 | bare!key = 123
+ | ^
+expected `.`, `=`
diff --git a/tests/fixtures/invalid/key/dotted-redefine-table.stderr b/tests/fixtures/invalid/key/dotted-redefine-table.stderr
new file mode 100644
index 0000000..3a2b8d5
--- /dev/null
+++ b/tests/fixtures/invalid/key/dotted-redefine-table.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 4, column 1
+ |
+4 | a.b.c = 2
+ | ^
+dotted key `a.b` attempted to extend non-table type (integer)
diff --git a/tests/fixtures/invalid/key/duplicate-keys.stderr b/tests/fixtures/invalid/key/duplicate-keys.stderr
new file mode 100644
index 0000000..74149f8
--- /dev/null
+++ b/tests/fixtures/invalid/key/duplicate-keys.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 2, column 1
+ |
+2 | dupe = true
+ | ^
+duplicate key `dupe` in document root
diff --git a/tests/fixtures/invalid/key/duplicate.stderr b/tests/fixtures/invalid/key/duplicate.stderr
new file mode 100644
index 0000000..8c7d7b7
--- /dev/null
+++ b/tests/fixtures/invalid/key/duplicate.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 3, column 1
+ |
+3 | name = "Pradyun"
+ | ^
+duplicate key `name` in document root
diff --git a/tests/fixtures/invalid/key/empty.stderr b/tests/fixtures/invalid/key/empty.stderr
new file mode 100644
index 0000000..b859159
--- /dev/null
+++ b/tests/fixtures/invalid/key/empty.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 2
+ |
+1 | = 1
+ | ^
+invalid key
diff --git a/tests/fixtures/invalid/key/escape.stderr b/tests/fixtures/invalid/key/escape.stderr
new file mode 100644
index 0000000..2a4ee3f
--- /dev/null
+++ b/tests/fixtures/invalid/key/escape.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 1
+ |
+1 | \u00c0 = "latin capital letter A with grave"
+ | ^
+invalid key
diff --git a/tests/fixtures/invalid/key/hash.stderr b/tests/fixtures/invalid/key/hash.stderr
new file mode 100644
index 0000000..15d8048
--- /dev/null
+++ b/tests/fixtures/invalid/key/hash.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 2
+ |
+1 | a# = 1
+ | ^
+expected `.`, `=`
diff --git a/tests/fixtures/invalid/key/multiline.stderr b/tests/fixtures/invalid/key/multiline.stderr
new file mode 100644
index 0000000..b4241ee
--- /dev/null
+++ b/tests/fixtures/invalid/key/multiline.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 3
+ |
+1 | """long
+ | ^
+expected `.`, `=`
diff --git a/tests/fixtures/invalid/key/newline.stderr b/tests/fixtures/invalid/key/newline.stderr
new file mode 100644
index 0000000..141932e
--- /dev/null
+++ b/tests/fixtures/invalid/key/newline.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 8
+ |
+1 | barekey
+ | ^
+expected `.`, `=`
diff --git a/tests/fixtures/invalid/key/no-eol.stderr b/tests/fixtures/invalid/key/no-eol.stderr
new file mode 100644
index 0000000..fc3c01e
--- /dev/null
+++ b/tests/fixtures/invalid/key/no-eol.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 7
+ |
+1 | a = 1 b = 2
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/key/open-bracket.stderr b/tests/fixtures/invalid/key/open-bracket.stderr
new file mode 100644
index 0000000..eee8cba
--- /dev/null
+++ b/tests/fixtures/invalid/key/open-bracket.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 6
+ |
+1 | [abc = 1
+ | ^
+invalid table header
+expected `.`, `]`
diff --git a/tests/fixtures/invalid/key/partial-quoted.stderr b/tests/fixtures/invalid/key/partial-quoted.stderr
new file mode 100644
index 0000000..e0a7c32
--- /dev/null
+++ b/tests/fixtures/invalid/key/partial-quoted.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 8
+ |
+1 | partial"quoted" = 5
+ | ^
+expected `.`, `=`
diff --git a/tests/fixtures/invalid/key/quoted-unclosed-1.stderr b/tests/fixtures/invalid/key/quoted-unclosed-1.stderr
new file mode 100644
index 0000000..6f15232
--- /dev/null
+++ b/tests/fixtures/invalid/key/quoted-unclosed-1.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 9
+ |
+1 | "key = x
+ | ^
+invalid basic string
diff --git a/tests/fixtures/invalid/key/quoted-unclosed-2.stderr b/tests/fixtures/invalid/key/quoted-unclosed-2.stderr
new file mode 100644
index 0000000..f3fdc7f
--- /dev/null
+++ b/tests/fixtures/invalid/key/quoted-unclosed-2.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 5
+ |
+1 | "key
+ | ^
+invalid basic string
diff --git a/tests/fixtures/invalid/key/single-open-bracket.stderr b/tests/fixtures/invalid/key/single-open-bracket.stderr
new file mode 100644
index 0000000..022f1fa
--- /dev/null
+++ b/tests/fixtures/invalid/key/single-open-bracket.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 2
+ |
+1 | [
+ | ^
+invalid key
diff --git a/tests/fixtures/invalid/key/space.stderr b/tests/fixtures/invalid/key/space.stderr
new file mode 100644
index 0000000..cd3258b
--- /dev/null
+++ b/tests/fixtures/invalid/key/space.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 3
+ |
+1 | a b = 1
+ | ^
+expected `.`, `=`
diff --git a/tests/fixtures/invalid/key/special-character.stderr b/tests/fixtures/invalid/key/special-character.stderr
new file mode 100644
index 0000000..7ada2f2
--- /dev/null
+++ b/tests/fixtures/invalid/key/special-character.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 1
+ |
+1 | μ = "greek small letter mu"
+ | ^
+invalid key
diff --git a/tests/fixtures/invalid/key/start-bracket.stderr b/tests/fixtures/invalid/key/start-bracket.stderr
new file mode 100644
index 0000000..43f937b
--- /dev/null
+++ b/tests/fixtures/invalid/key/start-bracket.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 2, column 6
+ |
+2 | [xyz = 5
+ | ^
+invalid table header
+expected `.`, `]`
diff --git a/tests/fixtures/invalid/key/start-dot.stderr b/tests/fixtures/invalid/key/start-dot.stderr
new file mode 100644
index 0000000..c71f0cf
--- /dev/null
+++ b/tests/fixtures/invalid/key/start-dot.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 1
+ |
+1 | .key = 1
+ | ^
+invalid key
diff --git a/tests/fixtures/invalid/key/two-equals.stderr b/tests/fixtures/invalid/key/two-equals.stderr
new file mode 100644
index 0000000..2f92886
--- /dev/null
+++ b/tests/fixtures/invalid/key/two-equals.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 6
+ |
+1 | key= = 1
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/key/two-equals2.stderr b/tests/fixtures/invalid/key/two-equals2.stderr
new file mode 100644
index 0000000..f2adb1f
--- /dev/null
+++ b/tests/fixtures/invalid/key/two-equals2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 3
+ |
+1 | a==1
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/key/two-equals3.stderr b/tests/fixtures/invalid/key/two-equals3.stderr
new file mode 100644
index 0000000..dbee169
--- /dev/null
+++ b/tests/fixtures/invalid/key/two-equals3.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 3
+ |
+1 | a=b=1
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/key/without-value-1.stderr b/tests/fixtures/invalid/key/without-value-1.stderr
new file mode 100644
index 0000000..2ddb124
--- /dev/null
+++ b/tests/fixtures/invalid/key/without-value-1.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 4
+ |
+1 | key
+ | ^
+expected `.`, `=`
diff --git a/tests/fixtures/invalid/key/without-value-2.stderr b/tests/fixtures/invalid/key/without-value-2.stderr
new file mode 100644
index 0000000..a3f280f
--- /dev/null
+++ b/tests/fixtures/invalid/key/without-value-2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 7
+ |
+1 | key =
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/key/without-value-3.stderr b/tests/fixtures/invalid/key/without-value-3.stderr
new file mode 100644
index 0000000..a6ca6ff
--- /dev/null
+++ b/tests/fixtures/invalid/key/without-value-3.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 6
+ |
+1 | "key"
+ | ^
+expected `.`, `=`
diff --git a/tests/fixtures/invalid/key/without-value-4.stderr b/tests/fixtures/invalid/key/without-value-4.stderr
new file mode 100644
index 0000000..c14af0c
--- /dev/null
+++ b/tests/fixtures/invalid/key/without-value-4.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 9
+ |
+1 | "key" =
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/spec/inline-table-2-0.stderr b/tests/fixtures/invalid/spec/inline-table-2-0.stderr
new file mode 100644
index 0000000..7fed269
--- /dev/null
+++ b/tests/fixtures/invalid/spec/inline-table-2-0.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 3, column 1
+ |
+3 | type.edible = false # INVALID
+ | ^
+dotted key `type` attempted to extend non-table type (inline table)
diff --git a/tests/fixtures/invalid/spec/inline-table-3-0.stderr b/tests/fixtures/invalid/spec/inline-table-3-0.stderr
new file mode 100644
index 0000000..45ba696
--- /dev/null
+++ b/tests/fixtures/invalid/spec/inline-table-3-0.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 3, column 1
+ |
+3 | type = { edible = false } # INVALID
+ | ^
+duplicate key `type` in table `product`
diff --git a/tests/fixtures/invalid/spec/key-value-pair-1.stderr b/tests/fixtures/invalid/spec/key-value-pair-1.stderr
new file mode 100644
index 0000000..ba765a8
--- /dev/null
+++ b/tests/fixtures/invalid/spec/key-value-pair-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 7
+ |
+1 | key = # INVALID
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/spec/keys-2.stderr b/tests/fixtures/invalid/spec/keys-2.stderr
new file mode 100644
index 0000000..bdbcce4
--- /dev/null
+++ b/tests/fixtures/invalid/spec/keys-2.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 1
+ |
+1 | = "no key name" # INVALID
+ | ^
+invalid key
diff --git a/tests/fixtures/invalid/spec/string-4-0.stderr b/tests/fixtures/invalid/spec/string-4-0.stderr
new file mode 100644
index 0000000..e82601d
--- /dev/null
+++ b/tests/fixtures/invalid/spec/string-4-0.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 2, column 46
+ |
+2 | str5 = """Here are three quotation marks: """.""" # INVALID
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/spec/string-7-0.stderr b/tests/fixtures/invalid/spec/string-7-0.stderr
new file mode 100644
index 0000000..7a928e1
--- /dev/null
+++ b/tests/fixtures/invalid/spec/string-7-0.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 3, column 48
+ |
+3 | apos15 = '''Here are fifteen apostrophes: '''''''''''''''''' # INVALID
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/spec/table-9-0.stderr b/tests/fixtures/invalid/spec/table-9-0.stderr
new file mode 100644
index 0000000..a2e1d49
--- /dev/null
+++ b/tests/fixtures/invalid/spec/table-9-0.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 5, column 1
+ |
+5 | [fruit.apple] # INVALID
+ | ^
+invalid table header
+duplicate key `apple` in table `fruit`
diff --git a/tests/fixtures/invalid/spec/table-9-1.stderr b/tests/fixtures/invalid/spec/table-9-1.stderr
new file mode 100644
index 0000000..78dea21
--- /dev/null
+++ b/tests/fixtures/invalid/spec/table-9-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 6, column 1
+ |
+6 | [fruit.apple.taste] # INVALID
+ | ^
+invalid table header
+duplicate key `taste` in table `fruit.apple`
diff --git a/tests/fixtures/invalid/string/bad-byte-escape.stderr b/tests/fixtures/invalid/string/bad-byte-escape.stderr
new file mode 100644
index 0000000..6d88863
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-byte-escape.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 14
+ |
+1 | naughty = "\xAg"
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/bad-codepoint.stderr b/tests/fixtures/invalid/string/bad-codepoint.stderr
new file mode 100644
index 0000000..4061c79
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-codepoint.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 76
+ |
+1 | invalid-codepoint = "This string contains a non scalar unicode codepoint \uD801"
+ | ^
+invalid unicode 4-digit hex code
+value is out of range
diff --git a/tests/fixtures/invalid/string/bad-concat.stderr b/tests/fixtures/invalid/string/bad-concat.stderr
new file mode 100644
index 0000000..a7346a4
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-concat.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 21
+ |
+1 | no_concat = "first" "second"
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/string/bad-escape-1.stderr b/tests/fixtures/invalid/string/bad-escape-1.stderr
new file mode 100644
index 0000000..02cb483
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-escape-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 43
+ |
+1 | invalid-escape = "This string has a bad \a escape character."
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/bad-escape-2.stderr b/tests/fixtures/invalid/string/bad-escape-2.stderr
new file mode 100644
index 0000000..57f0ace
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-escape-2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 43
+ |
+1 | invalid-escape = "This string has a bad \ escape character."
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/bad-hex-esc-1.stderr b/tests/fixtures/invalid/string/bad-hex-esc-1.stderr
new file mode 100644
index 0000000..5d5577f
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-hex-esc-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+ |
+1 | bad-hex-esc-1 = "\x0g"
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/bad-hex-esc-2.stderr b/tests/fixtures/invalid/string/bad-hex-esc-2.stderr
new file mode 100644
index 0000000..5a40ad4
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-hex-esc-2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+ |
+1 | bad-hex-esc-2 = "\xG0"
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/bad-hex-esc-3.stderr b/tests/fixtures/invalid/string/bad-hex-esc-3.stderr
new file mode 100644
index 0000000..70e1183
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-hex-esc-3.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+ |
+1 | bad-hex-esc-3 = "\x"
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/bad-hex-esc-4.stderr b/tests/fixtures/invalid/string/bad-hex-esc-4.stderr
new file mode 100644
index 0000000..df028ee
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-hex-esc-4.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+ |
+1 | bad-hex-esc-4 = "\x 50"
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/bad-hex-esc-5.stderr b/tests/fixtures/invalid/string/bad-hex-esc-5.stderr
new file mode 100644
index 0000000..4b5cd33
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-hex-esc-5.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+ |
+1 | bad-hex-esc-5 = "\x 50"
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/bad-hex-esc.stderr b/tests/fixtures/invalid/string/bad-hex-esc.stderr
new file mode 100644
index 0000000..5d5577f
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-hex-esc.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+ |
+1 | bad-hex-esc-1 = "\x0g"
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/bad-multiline.stderr b/tests/fixtures/invalid/string/bad-multiline.stderr
new file mode 100644
index 0000000..f88e0dd
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-multiline.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 20
+ |
+1 | multi = "first line
+ | ^
+invalid basic string
diff --git a/tests/fixtures/invalid/string/bad-slash-escape.stderr b/tests/fixtures/invalid/string/bad-slash-escape.stderr
new file mode 100644
index 0000000..19bd111
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-slash-escape.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 43
+ |
+1 | invalid-escape = "This string has a bad \/ escape character."
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/bad-uni-esc-1.stderr b/tests/fixtures/invalid/string/bad-uni-esc-1.stderr
new file mode 100644
index 0000000..a9e439b
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-uni-esc-1.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 13
+ |
+1 | str = "val\ue"
+ | ^
+invalid unicode 4-digit hex code
diff --git a/tests/fixtures/invalid/string/bad-uni-esc-2.stderr b/tests/fixtures/invalid/string/bad-uni-esc-2.stderr
new file mode 100644
index 0000000..87c8681
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-uni-esc-2.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 13
+ |
+1 | str = "val\Ux"
+ | ^
+invalid unicode 8-digit hex code
diff --git a/tests/fixtures/invalid/string/bad-uni-esc-3.stderr b/tests/fixtures/invalid/string/bad-uni-esc-3.stderr
new file mode 100644
index 0000000..61f8ded
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-uni-esc-3.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 13
+ |
+1 | str = "val\U0000000"
+ | ^
+invalid unicode 8-digit hex code
diff --git a/tests/fixtures/invalid/string/bad-uni-esc-4.stderr b/tests/fixtures/invalid/string/bad-uni-esc-4.stderr
new file mode 100644
index 0000000..1a781d9
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-uni-esc-4.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 13
+ |
+1 | str = "val\U0000"
+ | ^
+invalid unicode 8-digit hex code
diff --git a/tests/fixtures/invalid/string/bad-uni-esc-5.stderr b/tests/fixtures/invalid/string/bad-uni-esc-5.stderr
new file mode 100644
index 0000000..88773ca
--- /dev/null
+++ b/tests/fixtures/invalid/string/bad-uni-esc-5.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 13
+ |
+1 | str = "val\Ugggggggg"
+ | ^
+invalid unicode 8-digit hex code
diff --git a/tests/fixtures/invalid/string/basic-byte-escapes.stderr b/tests/fixtures/invalid/string/basic-byte-escapes.stderr
new file mode 100644
index 0000000..64f8d86
--- /dev/null
+++ b/tests/fixtures/invalid/string/basic-byte-escapes.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 13
+ |
+1 | answer = "\x33"
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/basic-multiline-out-of-range-unicode-escape-1.stderr b/tests/fixtures/invalid/string/basic-multiline-out-of-range-unicode-escape-1.stderr
new file mode 100644
index 0000000..c4be6a6
--- /dev/null
+++ b/tests/fixtures/invalid/string/basic-multiline-out-of-range-unicode-escape-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 10
+ |
+1 | a = """\UFFFFFFFF"""
+ | ^
+invalid unicode 8-digit hex code
+value is out of range
diff --git a/tests/fixtures/invalid/string/basic-multiline-out-of-range-unicode-escape-2.stderr b/tests/fixtures/invalid/string/basic-multiline-out-of-range-unicode-escape-2.stderr
new file mode 100644
index 0000000..f034203
--- /dev/null
+++ b/tests/fixtures/invalid/string/basic-multiline-out-of-range-unicode-escape-2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 10
+ |
+1 | a = """\U00D80000"""
+ | ^
+invalid unicode 8-digit hex code
+value is out of range
diff --git a/tests/fixtures/invalid/string/basic-multiline-quotes.stderr b/tests/fixtures/invalid/string/basic-multiline-quotes.stderr
new file mode 100644
index 0000000..7a16008
--- /dev/null
+++ b/tests/fixtures/invalid/string/basic-multiline-quotes.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 46
+ |
+1 | str5 = """Here are three quotation marks: """."""
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/string/basic-multiline-unknown-escape.stderr b/tests/fixtures/invalid/string/basic-multiline-unknown-escape.stderr
new file mode 100644
index 0000000..d2462bf
--- /dev/null
+++ b/tests/fixtures/invalid/string/basic-multiline-unknown-escape.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 10
+ |
+1 | a = """\@"""
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/basic-out-of-range-unicode-escape-1.stderr b/tests/fixtures/invalid/string/basic-out-of-range-unicode-escape-1.stderr
new file mode 100644
index 0000000..806e6e7
--- /dev/null
+++ b/tests/fixtures/invalid/string/basic-out-of-range-unicode-escape-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 8
+ |
+1 | a = "\UFFFFFFFF"
+ | ^
+invalid unicode 8-digit hex code
+value is out of range
diff --git a/tests/fixtures/invalid/string/basic-out-of-range-unicode-escape-2.stderr b/tests/fixtures/invalid/string/basic-out-of-range-unicode-escape-2.stderr
new file mode 100644
index 0000000..69396c6
--- /dev/null
+++ b/tests/fixtures/invalid/string/basic-out-of-range-unicode-escape-2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 8
+ |
+1 | a = "\U00D80000"
+ | ^
+invalid unicode 8-digit hex code
+value is out of range
diff --git a/tests/fixtures/invalid/string/basic-unknown-escape.stderr b/tests/fixtures/invalid/string/basic-unknown-escape.stderr
new file mode 100644
index 0000000..a7fbb25
--- /dev/null
+++ b/tests/fixtures/invalid/string/basic-unknown-escape.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 8
+ |
+1 | a = "\@"
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/literal-multiline-quotes-1.stderr b/tests/fixtures/invalid/string/literal-multiline-quotes-1.stderr
new file mode 100644
index 0000000..ed65b33
--- /dev/null
+++ b/tests/fixtures/invalid/string/literal-multiline-quotes-1.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 28
+ |
+1 | a = '''6 apostrophes: ''''''
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/string/literal-multiline-quotes-2.stderr b/tests/fixtures/invalid/string/literal-multiline-quotes-2.stderr
new file mode 100644
index 0000000..3fbb44a
--- /dev/null
+++ b/tests/fixtures/invalid/string/literal-multiline-quotes-2.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 29
+ |
+1 | a = '''15 apostrophes: ''''''''''''''''''
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/string/missing-quotes.stderr b/tests/fixtures/invalid/string/missing-quotes.stderr
new file mode 100644
index 0000000..3558aab
--- /dev/null
+++ b/tests/fixtures/invalid/string/missing-quotes.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 8
+ |
+1 | name = value
+ | ^
+invalid string
+expected `"`, `'`
diff --git a/tests/fixtures/invalid/string/multiline-bad-escape-1.stderr b/tests/fixtures/invalid/string/multiline-bad-escape-1.stderr
new file mode 100644
index 0000000..1a3b24b
--- /dev/null
+++ b/tests/fixtures/invalid/string/multiline-bad-escape-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 11
+ |
+1 | k = """t\a"""
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/multiline-bad-escape-2.stderr b/tests/fixtures/invalid/string/multiline-bad-escape-2.stderr
new file mode 100644
index 0000000..db17685
--- /dev/null
+++ b/tests/fixtures/invalid/string/multiline-bad-escape-2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 2, column 11
+ |
+2 | k = """t\ t"""
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/multiline-bad-escape-3.stderr b/tests/fixtures/invalid/string/multiline-bad-escape-3.stderr
new file mode 100644
index 0000000..646aa4d
--- /dev/null
+++ b/tests/fixtures/invalid/string/multiline-bad-escape-3.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 2, column 11
+ |
+2 | k = """t\ """
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/multiline-escape-space.stderr b/tests/fixtures/invalid/string/multiline-escape-space.stderr
new file mode 100644
index 0000000..4b3c32b
--- /dev/null
+++ b/tests/fixtures/invalid/string/multiline-escape-space.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 2, column 9
+ |
+2 | foo \ \n
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/tests/fixtures/invalid/string/multiline-no-close-2.stderr b/tests/fixtures/invalid/string/multiline-no-close-2.stderr
new file mode 100644
index 0000000..be5420a
--- /dev/null
+++ b/tests/fixtures/invalid/string/multiline-no-close-2.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 7
+ |
+1 | x="""
+ | ^
+invalid multiline basic string
diff --git a/tests/fixtures/invalid/string/multiline-no-close.stderr b/tests/fixtures/invalid/string/multiline-no-close.stderr
new file mode 100644
index 0000000..99e967a
--- /dev/null
+++ b/tests/fixtures/invalid/string/multiline-no-close.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 2, column 20
+ |
+2 | this will fail
+ | ^
+invalid multiline basic string
diff --git a/tests/fixtures/invalid/string/multiline-quotes-1.stderr b/tests/fixtures/invalid/string/multiline-quotes-1.stderr
new file mode 100644
index 0000000..c3cf4c6
--- /dev/null
+++ b/tests/fixtures/invalid/string/multiline-quotes-1.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 23
+ |
+1 | a = """6 quotes: """"""
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/string/no-close.stderr b/tests/fixtures/invalid/string/no-close.stderr
new file mode 100644
index 0000000..f23223d
--- /dev/null
+++ b/tests/fixtures/invalid/string/no-close.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 42
+ |
+1 | no-ending-quote = "One time, at band camp
+ | ^
+invalid basic string
diff --git a/tests/fixtures/invalid/string/text-after-string.stderr b/tests/fixtures/invalid/string/text-after-string.stderr
new file mode 100644
index 0000000..f05e33b
--- /dev/null
+++ b/tests/fixtures/invalid/string/text-after-string.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 41
+ |
+1 | string = "Is there life after strings?" No.
+ | ^
+expected newline, `#`
diff --git a/tests/fixtures/invalid/string/wrong-close.stderr b/tests/fixtures/invalid/string/wrong-close.stderr
new file mode 100644
index 0000000..9a785e0
--- /dev/null
+++ b/tests/fixtures/invalid/string/wrong-close.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 39
+ |
+1 | bad-ending-quote = "double and single'
+ | ^
+invalid basic string
diff --git a/tests/fixtures/invalid/table/append-with-dotted-keys-1.stderr b/tests/fixtures/invalid/table/append-with-dotted-keys-1.stderr
new file mode 100644
index 0000000..54ee50e
--- /dev/null
+++ b/tests/fixtures/invalid/table/append-with-dotted-keys-1.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 17, column 3
+ |
+17 | b.c.t = "Using dotted keys to add to [a.b.c] after explicitly defining it above is not allowed"
+ | ^
+duplicate key `c`
diff --git a/tests/fixtures/invalid/table/append-with-dotted-keys-2.stderr b/tests/fixtures/invalid/table/append-with-dotted-keys-2.stderr
new file mode 100644
index 0000000..7c665f9
--- /dev/null
+++ b/tests/fixtures/invalid/table/append-with-dotted-keys-2.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 8, column 3
+ |
+8 | b.c.d.k.t = "Using dotted keys to add to [a.b.c.d] after explicitly defining it above is not allowed"
+ | ^
+duplicate key `d`
diff --git a/tests/fixtures/invalid/table/array-empty.stderr b/tests/fixtures/invalid/table/array-empty.stderr
new file mode 100644
index 0000000..ad2440a
--- /dev/null
+++ b/tests/fixtures/invalid/table/array-empty.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 3
+ |
+1 | [[]]
+ | ^
+invalid key
diff --git a/tests/fixtures/invalid/table/array-implicit.stderr b/tests/fixtures/invalid/table/array-implicit.stderr
new file mode 100644
index 0000000..ed1ab7f
--- /dev/null
+++ b/tests/fixtures/invalid/table/array-implicit.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 13, column 1
+ |
+13 | [[albums]]
+ | ^
+invalid table header
+duplicate key `albums` in document root
diff --git a/tests/fixtures/invalid/table/array-missing-bracket.stderr b/tests/fixtures/invalid/table/array-missing-bracket.stderr
new file mode 100644
index 0000000..ed370ef
--- /dev/null
+++ b/tests/fixtures/invalid/table/array-missing-bracket.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 9
+ |
+1 | [[albums]
+ | ^
+invalid table header
+expected `.`, `]]`
diff --git a/tests/fixtures/invalid/table/duplicate-key-dotted-table.stderr b/tests/fixtures/invalid/table/duplicate-key-dotted-table.stderr
new file mode 100644
index 0000000..3cbc0a3
--- /dev/null
+++ b/tests/fixtures/invalid/table/duplicate-key-dotted-table.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 4, column 1
+ |
+4 | [fruit.apple] # INVALID
+ | ^
+invalid table header
+duplicate key `apple` in table `fruit`
diff --git a/tests/fixtures/invalid/table/duplicate-key-dotted-table2.stderr b/tests/fixtures/invalid/table/duplicate-key-dotted-table2.stderr
new file mode 100644
index 0000000..c5ab1eb
--- /dev/null
+++ b/tests/fixtures/invalid/table/duplicate-key-dotted-table2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 4, column 1
+ |
+4 | [fruit.apple.taste] # INVALID
+ | ^
+invalid table header
+duplicate key `taste` in table `fruit.apple`
diff --git a/tests/fixtures/invalid/table/duplicate-key-table.stderr b/tests/fixtures/invalid/table/duplicate-key-table.stderr
new file mode 100644
index 0000000..bc76d9c
--- /dev/null
+++ b/tests/fixtures/invalid/table/duplicate-key-table.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 4, column 1
+ |
+4 | [fruit.type]
+ | ^
+invalid table header
+duplicate key `type` in table `fruit`
diff --git a/tests/fixtures/invalid/table/duplicate-table-array.stderr b/tests/fixtures/invalid/table/duplicate-table-array.stderr
new file mode 100644
index 0000000..536fdc3
--- /dev/null
+++ b/tests/fixtures/invalid/table/duplicate-table-array.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 2, column 1
+ |
+2 | [[tbl]]
+ | ^
+invalid table header
+duplicate key `tbl` in document root
diff --git a/tests/fixtures/invalid/table/duplicate-table-array2.stderr b/tests/fixtures/invalid/table/duplicate-table-array2.stderr
new file mode 100644
index 0000000..81c27fb
--- /dev/null
+++ b/tests/fixtures/invalid/table/duplicate-table-array2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 2, column 1
+ |
+2 | [tbl]
+ | ^
+invalid table header
+duplicate key `tbl` in document root
diff --git a/tests/fixtures/invalid/table/duplicate.stderr b/tests/fixtures/invalid/table/duplicate.stderr
new file mode 100644
index 0000000..81bf4ed
--- /dev/null
+++ b/tests/fixtures/invalid/table/duplicate.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 4, column 1
+ |
+4 | [a]
+ | ^
+invalid table header
+duplicate key `a` in document root
diff --git a/tests/fixtures/invalid/table/empty-implicit-table.stderr b/tests/fixtures/invalid/table/empty-implicit-table.stderr
new file mode 100644
index 0000000..a7f7444
--- /dev/null
+++ b/tests/fixtures/invalid/table/empty-implicit-table.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 9
+ |
+1 | [naughty..naughty]
+ | ^
+invalid table header
+expected `.`, `]`
diff --git a/tests/fixtures/invalid/table/empty.stderr b/tests/fixtures/invalid/table/empty.stderr
new file mode 100644
index 0000000..5dd0d56
--- /dev/null
+++ b/tests/fixtures/invalid/table/empty.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 2
+ |
+1 | []
+ | ^
+invalid key
diff --git a/tests/fixtures/invalid/table/equals-sign.stderr b/tests/fixtures/invalid/table/equals-sign.stderr
new file mode 100644
index 0000000..52451c9
--- /dev/null
+++ b/tests/fixtures/invalid/table/equals-sign.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 6
+ |
+1 | [name=bad]
+ | ^
+invalid table header
+expected `.`, `]`
diff --git a/tests/fixtures/invalid/table/llbrace.stderr b/tests/fixtures/invalid/table/llbrace.stderr
new file mode 100644
index 0000000..316590d
--- /dev/null
+++ b/tests/fixtures/invalid/table/llbrace.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 3
+ |
+1 | [ [table]]
+ | ^
+invalid key
diff --git a/tests/fixtures/invalid/table/nested-brackets-close.stderr b/tests/fixtures/invalid/table/nested-brackets-close.stderr
new file mode 100644
index 0000000..e74e178
--- /dev/null
+++ b/tests/fixtures/invalid/table/nested-brackets-close.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 4
+ |
+1 | [a]b]
+ | ^
+invalid table header
+expected newline, `#`
diff --git a/tests/fixtures/invalid/table/nested-brackets-open.stderr b/tests/fixtures/invalid/table/nested-brackets-open.stderr
new file mode 100644
index 0000000..094cf7c
--- /dev/null
+++ b/tests/fixtures/invalid/table/nested-brackets-open.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 3
+ |
+1 | [a[b]
+ | ^
+invalid table header
+expected `.`, `]`
diff --git a/tests/fixtures/invalid/table/quoted-no-close.stderr b/tests/fixtures/invalid/table/quoted-no-close.stderr
new file mode 100644
index 0000000..2b05e95
--- /dev/null
+++ b/tests/fixtures/invalid/table/quoted-no-close.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 21
+ |
+1 | ["where will it end]
+ | ^
+invalid basic string
diff --git a/tests/fixtures/invalid/table/redefine.stderr b/tests/fixtures/invalid/table/redefine.stderr
new file mode 100644
index 0000000..99c1f98
--- /dev/null
+++ b/tests/fixtures/invalid/table/redefine.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 5, column 1
+ |
+5 | [a.b]
+ | ^
+invalid table header
+duplicate key `b` in table `a`
diff --git a/tests/fixtures/invalid/table/rrbrace.stderr b/tests/fixtures/invalid/table/rrbrace.stderr
new file mode 100644
index 0000000..adcdf90
--- /dev/null
+++ b/tests/fixtures/invalid/table/rrbrace.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 8
+ |
+1 | [[table] ]
+ | ^
+invalid table header
+expected `.`, `]]`
diff --git a/tests/fixtures/invalid/table/text-after-table.stderr b/tests/fixtures/invalid/table/text-after-table.stderr
new file mode 100644
index 0000000..e7d7c4e
--- /dev/null
+++ b/tests/fixtures/invalid/table/text-after-table.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 9
+ |
+1 | [error] this shouldn't be here
+ | ^
+invalid table header
+expected newline, `#`
diff --git a/tests/fixtures/invalid/table/whitespace.stderr b/tests/fixtures/invalid/table/whitespace.stderr
new file mode 100644
index 0000000..aa149d2
--- /dev/null
+++ b/tests/fixtures/invalid/table/whitespace.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 10
+ |
+1 | [invalid key]
+ | ^
+invalid table header
+expected `.`, `]`
diff --git a/tests/fixtures/invalid/table/with-pound.stderr b/tests/fixtures/invalid/table/with-pound.stderr
new file mode 100644
index 0000000..f5531cc
--- /dev/null
+++ b/tests/fixtures/invalid/table/with-pound.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 5
+ |
+1 | [key#group]
+ | ^
+invalid table header
+expected `.`, `]`
diff --git a/tests/invalid.rs b/tests/invalid.rs
new file mode 100644
index 0000000..9fff235
--- /dev/null
+++ b/tests/invalid.rs
@@ -0,0 +1,26 @@
+use toml_edit::Document;
+
+fn main() {
+ let args = libtest_mimic::Arguments::from_args();
+ let tests = toml_test_data::invalid()
+ .map(|case| {
+ libtest_mimic::Trial::test(case.name.display().to_string(), || {
+ let expect_path =
+ std::path::Path::new("tests/fixtures").join(case.name.with_extension("stderr"));
+ let err = match run_case(case.fixture) {
+ Ok(()) => "".to_owned(),
+ Err(err) => err,
+ };
+ snapbox::assert_eq_path(expect_path, err);
+ Ok(())
+ })
+ })
+ .collect();
+ libtest_mimic::run(&args, tests).exit()
+}
+
+fn run_case(input: &[u8]) -> Result<(), String> {
+ let raw = std::str::from_utf8(input).map_err(|e| e.to_string())?;
+ let _ = raw.parse::<Document>().map_err(|e| e.to_string())?;
+ Ok(())
+}
diff --git a/tests/testsuite/convert.rs b/tests/testsuite/convert.rs
new file mode 100644
index 0000000..98f9397
--- /dev/null
+++ b/tests/testsuite/convert.rs
@@ -0,0 +1,79 @@
+use snapbox::assert_eq;
+
+use toml_edit::{Document, Item, Value};
+
+#[test]
+fn table_into_inline() {
+ let toml = r#"
+[table]
+string = "value"
+array = [1, 2, 3]
+inline = { "1" = 1, "2" = 2 }
+
+[table.child]
+other = "world"
+"#;
+ let mut doc = toml.parse::<Document>().unwrap();
+
+ doc.get_mut("table").unwrap().make_value();
+
+ let actual = doc.to_string();
+ // `table=` is because we didn't re-format the table key, only the value
+ let expected = r#"table= { string = "value", array = [1, 2, 3], inline = { "1" = 1, "2" = 2 }, child = { other = "world" } }
+"#;
+ assert_eq(expected, actual);
+}
+
+#[test]
+fn inline_table_to_table() {
+ let toml = r#"table = { string = "value", array = [1, 2, 3], inline = { "1" = 1, "2" = 2 }, child = { other = "world" } }
+"#;
+ let mut doc = toml.parse::<Document>().unwrap();
+
+ let t = doc.remove("table").unwrap();
+ let t = match t {
+ Item::Value(Value::InlineTable(t)) => t,
+ _ => unreachable!("Unexpected {:?}", t),
+ };
+ let t = t.into_table();
+ doc.insert("table", Item::Table(t));
+
+ let actual = doc.to_string();
+ let expected = r#"[table]
+string = "value"
+array = [1, 2, 3]
+inline = { "1" = 1, "2" = 2 }
+child = { other = "world" }
+"#;
+ assert_eq(expected, actual);
+}
+
+#[test]
+fn array_of_tables_to_array() {
+ let toml = r#"
+[[table]]
+string = "value"
+array = [1, 2, 3]
+inline = { "1" = 1, "2" = 2 }
+
+[table.child]
+other = "world"
+
+[[table]]
+string = "value"
+array = [1, 2, 3]
+inline = { "1" = 1, "2" = 2 }
+
+[table.child]
+other = "world"
+"#;
+ let mut doc = toml.parse::<Document>().unwrap();
+
+ doc.get_mut("table").unwrap().make_value();
+
+ let actual = doc.to_string();
+ // `table=` is because we didn't re-format the table key, only the value
+ let expected = r#"table= [{ string = "value", array = [1, 2, 3], inline = { "1" = 1, "2" = 2 }, child = { other = "world" } }, { string = "value", array = [1, 2, 3], inline = { "1" = 1, "2" = 2 }, child = { other = "world" } }]
+"#;
+ assert_eq(expected, actual);
+}
diff --git a/tests/testsuite/datetime.rs b/tests/testsuite/datetime.rs
new file mode 100644
index 0000000..541f8ea
--- /dev/null
+++ b/tests/testsuite/datetime.rs
@@ -0,0 +1,256 @@
+macro_rules! bad {
+ ($toml:expr, $msg:expr) => {
+ match $toml.parse::<toml_edit::Document>() {
+ Ok(s) => panic!("parsed to: {:#?}", s),
+ Err(e) => snapbox::assert_eq($msg, e.to_string()),
+ }
+ };
+}
+
+#[test]
+fn times() {
+ fn dogood(s: &str, serialized: &str) {
+ let to_parse = format!("foo = {}", s);
+ let document = to_parse.parse::<toml_edit::Document>().unwrap();
+ assert_eq!(
+ document["foo"].as_datetime().unwrap().to_string(),
+ serialized
+ );
+ }
+ fn good(s: &str) {
+ dogood(s, s);
+ dogood(&s.replace('T', " "), s);
+ dogood(&s.replace('T', "t"), s);
+ dogood(&s.replace('Z', "z"), s);
+ }
+
+ good("1997-09-09T09:09:09Z");
+ good("1997-09-09T09:09:09+09:09");
+ good("1997-09-09T09:09:09-09:09");
+ good("1997-09-09T09:09:09");
+ good("1997-09-09");
+ dogood("1997-09-09 ", "1997-09-09");
+ dogood("1997-09-09 # comment", "1997-09-09");
+ good("09:09:09");
+ good("1997-09-09T09:09:09.09Z");
+ good("1997-09-09T09:09:09.09+09:09");
+ good("1997-09-09T09:09:09.09-09:09");
+ good("1997-09-09T09:09:09.09");
+ good("09:09:09.09");
+}
+
+#[test]
+fn bad_times() {
+ bad!(
+ "foo = 199-09-09",
+ "\
+TOML parse error at line 1, column 10
+ |
+1 | foo = 199-09-09
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "foo = 199709-09",
+ "\
+TOML parse error at line 1, column 13
+ |
+1 | foo = 199709-09
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "foo = 1997-9-09",
+ "\
+TOML parse error at line 1, column 12
+ |
+1 | foo = 1997-9-09
+ | ^
+invalid date-time
+"
+ );
+ bad!(
+ "foo = 1997-09-9",
+ "\
+TOML parse error at line 1, column 15
+ |
+1 | foo = 1997-09-9
+ | ^
+invalid date-time
+"
+ );
+ bad!(
+ "foo = 1997-09-0909:09:09",
+ "\
+TOML parse error at line 1, column 17
+ |
+1 | foo = 1997-09-0909:09:09
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "foo = 1997-09-09T09:09:09.",
+ "\
+TOML parse error at line 1, column 26
+ |
+1 | foo = 1997-09-09T09:09:09.
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "foo = T",
+ r#"TOML parse error at line 1, column 7
+ |
+1 | foo = T
+ | ^
+invalid string
+expected `"`, `'`
+"#
+ );
+ bad!(
+ "foo = T.",
+ r#"TOML parse error at line 1, column 7
+ |
+1 | foo = T.
+ | ^
+invalid string
+expected `"`, `'`
+"#
+ );
+ bad!(
+ "foo = TZ",
+ r#"TOML parse error at line 1, column 7
+ |
+1 | foo = TZ
+ | ^
+invalid string
+expected `"`, `'`
+"#
+ );
+ bad!(
+ "foo = 1997-09-09T09:09:09.09+",
+ r#"TOML parse error at line 1, column 30
+ |
+1 | foo = 1997-09-09T09:09:09.09+
+ | ^
+invalid time offset
+"#
+ );
+ bad!(
+ "foo = 1997-09-09T09:09:09.09+09",
+ r#"TOML parse error at line 1, column 32
+ |
+1 | foo = 1997-09-09T09:09:09.09+09
+ | ^
+invalid time offset
+"#
+ );
+ bad!(
+ "foo = 1997-09-09T09:09:09.09+09:9",
+ r#"TOML parse error at line 1, column 33
+ |
+1 | foo = 1997-09-09T09:09:09.09+09:9
+ | ^
+invalid time offset
+"#
+ );
+ bad!(
+ "foo = 1997-09-09T09:09:09.09+0909",
+ r#"TOML parse error at line 1, column 32
+ |
+1 | foo = 1997-09-09T09:09:09.09+0909
+ | ^
+invalid time offset
+"#
+ );
+ bad!(
+ "foo = 1997-09-09T09:09:09.09-",
+ r#"TOML parse error at line 1, column 30
+ |
+1 | foo = 1997-09-09T09:09:09.09-
+ | ^
+invalid time offset
+"#
+ );
+ bad!(
+ "foo = 1997-09-09T09:09:09.09-09",
+ r#"TOML parse error at line 1, column 32
+ |
+1 | foo = 1997-09-09T09:09:09.09-09
+ | ^
+invalid time offset
+"#
+ );
+ bad!(
+ "foo = 1997-09-09T09:09:09.09-09:9",
+ r#"TOML parse error at line 1, column 33
+ |
+1 | foo = 1997-09-09T09:09:09.09-09:9
+ | ^
+invalid time offset
+"#
+ );
+ bad!(
+ "foo = 1997-09-09T09:09:09.09-0909",
+ r#"TOML parse error at line 1, column 32
+ |
+1 | foo = 1997-09-09T09:09:09.09-0909
+ | ^
+invalid time offset
+"#
+ );
+
+ bad!(
+ "foo = 1997-00-09T09:09:09.09Z",
+ r#"TOML parse error at line 1, column 12
+ |
+1 | foo = 1997-00-09T09:09:09.09Z
+ | ^
+invalid date-time
+value is out of range
+"#
+ );
+ bad!(
+ "foo = 1997-09-00T09:09:09.09Z",
+ r#"TOML parse error at line 1, column 15
+ |
+1 | foo = 1997-09-00T09:09:09.09Z
+ | ^
+invalid date-time
+value is out of range
+"#
+ );
+ bad!(
+ "foo = 1997-09-09T30:09:09.09Z",
+ r#"TOML parse error at line 1, column 17
+ |
+1 | foo = 1997-09-09T30:09:09.09Z
+ | ^
+expected newline, `#`
+"#
+ );
+ bad!(
+ "foo = 1997-09-09T12:69:09.09Z",
+ r#"TOML parse error at line 1, column 21
+ |
+1 | foo = 1997-09-09T12:69:09.09Z
+ | ^
+invalid date-time
+value is out of range
+"#
+ );
+ bad!(
+ "foo = 1997-09-09T12:09:69.09Z",
+ r#"TOML parse error at line 1, column 24
+ |
+1 | foo = 1997-09-09T12:09:69.09Z
+ | ^
+invalid date-time
+value is out of range
+"#
+ );
+}
diff --git a/tests/testsuite/edit.rs b/tests/testsuite/edit.rs
new file mode 100644
index 0000000..28f73c1
--- /dev/null
+++ b/tests/testsuite/edit.rs
@@ -0,0 +1,855 @@
+use std::fmt;
+use std::iter::FromIterator;
+
+use snapbox::assert_eq;
+use toml_edit::{array, table, value, Document, Item, Key, Table, Value};
+
+macro_rules! parse_key {
+ ($s:expr) => {{
+ let key = $s.parse::<Key>();
+ assert!(key.is_ok());
+ key.unwrap()
+ }};
+}
+
+macro_rules! as_table {
+ ($e:ident) => {{
+ assert!($e.is_table());
+ $e.as_table_mut().unwrap()
+ }};
+}
+
+// Copied from https://github.com/colin-kiegel/rust-pretty-assertions/issues/24
+/// Wrapper around string slice that makes debug output `{:?}` to print string same way as `{}`.
+/// Used in different `assert*!` macros in combination with `pretty_assertions` crate to make
+/// test failures to show nice diffs.
+#[derive(PartialEq, Eq)]
+struct PrettyString<'a>(pub &'a str);
+/// Make diff to display string as multi-line string
+impl<'a> fmt::Debug for PrettyString<'a> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(self.0)
+ }
+}
+
+struct Test {
+ doc: Document,
+}
+
+fn given(input: &str) -> Test {
+ let doc = input.parse::<Document>();
+ assert!(doc.is_ok());
+ Test { doc: doc.unwrap() }
+}
+
+impl Test {
+ fn running<F>(&mut self, func: F) -> &mut Self
+ where
+ F: Fn(&mut Table),
+ {
+ {
+ let root = self.doc.as_table_mut();
+ func(root);
+ }
+ self
+ }
+
+ #[track_caller]
+ fn produces_display(&self, expected: &str) -> &Self {
+ assert_eq(expected, self.doc.to_string());
+ self
+ }
+}
+
+// insertion
+
+#[test]
+fn test_insert_leaf_table() {
+ given(
+ r#"[servers]
+
+ [servers.alpha]
+ ip = "10.0.0.1"
+ dc = "eqdc10"
+
+ [other.table]"#,
+ )
+ .running(|root| {
+ root["servers"]["beta"] = table();
+ root["servers"]["beta"]["ip"] = value("10.0.0.2");
+ root["servers"]["beta"]["dc"] = value("eqdc10");
+ })
+ .produces_display(
+ r#"[servers]
+
+ [servers.alpha]
+ ip = "10.0.0.1"
+ dc = "eqdc10"
+
+[servers.beta]
+ip = "10.0.0.2"
+dc = "eqdc10"
+
+ [other.table]
+"#,
+ );
+}
+
+#[test]
+fn test_inserted_leaf_table_goes_after_last_sibling() {
+ given(
+ r#"
+ [package]
+ [dependencies]
+ [[example]]
+ [dependencies.opencl]
+ [dev-dependencies]"#,
+ )
+ .running(|root| {
+ root["dependencies"]["newthing"] = table();
+ })
+ .produces_display(
+ r#"
+ [package]
+ [dependencies]
+ [[example]]
+ [dependencies.opencl]
+
+[dependencies.newthing]
+ [dev-dependencies]
+"#,
+ );
+}
+
+#[test]
+fn test_inserting_tables_from_different_parsed_docs() {
+ given("[a]")
+ .running(|root| {
+ let other = "[b]".parse::<Document>().unwrap();
+ root["b"] = other["b"].clone();
+ })
+ .produces_display("[a]\n[b]\n");
+}
+#[test]
+fn test_insert_nonleaf_table() {
+ given(
+ r#"
+ [other.table]"#,
+ )
+ .running(|root| {
+ root["servers"] = table();
+ root["servers"]["alpha"] = table();
+ root["servers"]["alpha"]["ip"] = value("10.0.0.1");
+ root["servers"]["alpha"]["dc"] = value("eqdc10");
+ })
+ .produces_display(
+ r#"
+ [other.table]
+
+[servers]
+
+[servers.alpha]
+ip = "10.0.0.1"
+dc = "eqdc10"
+"#,
+ );
+}
+
+#[test]
+fn test_insert_array() {
+ given(
+ r#"
+ [package]
+ title = "withoutarray""#,
+ )
+ .running(|root| {
+ root["bin"] = array();
+ assert!(root["bin"].is_array_of_tables());
+ let array = root["bin"].as_array_of_tables_mut().unwrap();
+ {
+ let mut table = Table::new();
+ table["hello"] = value("world");
+ array.push(table);
+ }
+ array.push(Table::new());
+ })
+ .produces_display(
+ r#"
+ [package]
+ title = "withoutarray"
+
+[[bin]]
+hello = "world"
+
+[[bin]]
+"#,
+ );
+}
+
+#[test]
+fn test_insert_values() {
+ given(
+ r#"
+ [tbl.son]"#,
+ )
+ .running(|root| {
+ root["tbl"]["key1"] = value("value1");
+ root["tbl"]["key2"] = value(42);
+ root["tbl"]["key3"] = value(8.1415926);
+ })
+ .produces_display(
+ r#"[tbl]
+key1 = "value1"
+key2 = 42
+key3 = 8.1415926
+
+ [tbl.son]
+"#,
+ );
+}
+
+// removal
+
+#[test]
+fn test_remove_leaf_table() {
+ given(
+ r#"
+ [servers]
+
+ # Indentation (tabs and/or spaces) is allowed but not required
+[servers.alpha]
+ ip = "10.0.0.1"
+ dc = "eqdc10"
+
+ [servers.beta]
+ ip = "10.0.0.2"
+ dc = "eqdc10""#,
+ )
+ .running(|root| {
+ let servers = root.get_mut("servers").unwrap();
+ let servers = as_table!(servers);
+ assert!(servers.remove("alpha").is_some());
+ })
+ .produces_display(
+ r#"
+ [servers]
+
+ [servers.beta]
+ ip = "10.0.0.2"
+ dc = "eqdc10"
+"#,
+ );
+}
+
+#[test]
+fn test_remove_nonleaf_table() {
+ given(
+ r#"
+ title = "not relevant"
+
+ # comment 1
+ [a.b.c] # comment 1.1
+ key1 = 1 # comment 1.2
+ # comment 2
+ [b] # comment 2.1
+ key2 = 2 # comment 2.2
+
+ # comment 3
+ [a] # comment 3.1
+ key3 = 3 # comment 3.2
+ [[a.'array']]
+ b = 1
+
+ [[a.b.c.trololololololo]] # ohohohohoho
+ c = 2
+ key3 = 42
+
+ # comment on some other table
+ [some.other.table]
+
+
+
+
+ # comment 4
+ [a.b] # comment 4.1
+ key4 = 4 # comment 4.2
+ key41 = 41 # comment 4.3
+
+
+ "#,
+ )
+ .running(|root| {
+ assert!(root.remove("a").is_some());
+ })
+ .produces_display(
+ r#"
+ title = "not relevant"
+ # comment 2
+ [b] # comment 2.1
+ key2 = 2 # comment 2.2
+
+ # comment on some other table
+ [some.other.table]
+
+
+ "#,
+ );
+}
+
+#[test]
+fn test_remove_array_entry() {
+ given(
+ r#"
+ [package]
+ name = "hello"
+ version = "1.0.0"
+
+ [[bin]]
+ name = "world"
+ path = "src/bin/world/main.rs"
+
+ [dependencies]
+ nom = "4.0" # future is here
+
+ [[bin]]
+ name = "delete me please"
+ path = "src/bin/dmp/main.rs""#,
+ )
+ .running(|root| {
+ let dmp = root.get_mut("bin").unwrap();
+ assert!(dmp.is_array_of_tables());
+ let dmp = dmp.as_array_of_tables_mut().unwrap();
+ assert_eq!(dmp.len(), 2);
+ dmp.remove(1);
+ assert_eq!(dmp.len(), 1);
+ })
+ .produces_display(
+ r#"
+ [package]
+ name = "hello"
+ version = "1.0.0"
+
+ [[bin]]
+ name = "world"
+ path = "src/bin/world/main.rs"
+
+ [dependencies]
+ nom = "4.0" # future is here
+"#,
+ );
+}
+
+#[test]
+fn test_remove_array() {
+ given(
+ r#"
+ [package]
+ name = "hello"
+ version = "1.0.0"
+
+ [[bin]]
+ name = "world"
+ path = "src/bin/world/main.rs"
+
+ [dependencies]
+ nom = "4.0" # future is here
+
+ [[bin]]
+ name = "delete me please"
+ path = "src/bin/dmp/main.rs""#,
+ )
+ .running(|root| {
+ assert!(root.remove("bin").is_some());
+ })
+ .produces_display(
+ r#"
+ [package]
+ name = "hello"
+ version = "1.0.0"
+
+ [dependencies]
+ nom = "4.0" # future is here
+"#,
+ );
+}
+
+#[test]
+fn test_remove_value() {
+ given(
+ r#"
+ name = "hello"
+ # delete this
+ version = "1.0.0" # please
+ documentation = "https://docs.rs/hello""#,
+ )
+ .running(|root| {
+ let value = root.remove("version");
+ assert!(value.is_some());
+ let value = value.unwrap();
+ assert!(value.is_value());
+ let value = value.as_value().unwrap();
+ assert!(value.is_str());
+ let value = value.as_str().unwrap();
+ assert_eq(value, "1.0.0");
+ })
+ .produces_display(
+ r#"
+ name = "hello"
+ documentation = "https://docs.rs/hello"
+"#,
+ );
+}
+
+#[test]
+fn test_remove_last_value_from_implicit() {
+ given(
+ r#"
+ [a]
+ b = 1"#,
+ )
+ .running(|root| {
+ let a = root.get_mut("a").unwrap();
+ assert!(a.is_table());
+ let a = as_table!(a);
+ a.set_implicit(true);
+ let value = a.remove("b");
+ assert!(value.is_some());
+ let value = value.unwrap();
+ assert!(value.is_value());
+ let value = value.as_value().unwrap();
+ assert_eq!(value.as_integer(), Some(1));
+ })
+ .produces_display(r#""#);
+}
+
+// values
+
+#[test]
+fn test_sort_values() {
+ given(
+ r#"
+ [a.z]
+
+ [a]
+ # this comment is attached to b
+ b = 2 # as well as this
+ a = 1
+ c = 3
+
+ [a.y]"#,
+ )
+ .running(|root| {
+ let a = root.get_mut("a").unwrap();
+ let a = as_table!(a);
+ a.sort_values();
+ })
+ .produces_display(
+ r#"
+ [a.z]
+
+ [a]
+ a = 1
+ # this comment is attached to b
+ b = 2 # as well as this
+ c = 3
+
+ [a.y]
+"#,
+ );
+}
+
+#[test]
+fn test_sort_values_by() {
+ given(
+ r#"
+ [a.z]
+
+ [a]
+ # this comment is attached to b
+ b = 2 # as well as this
+ a = 1
+ "c" = 3
+
+ [a.y]"#,
+ )
+ .running(|root| {
+ let a = root.get_mut("a").unwrap();
+ let a = as_table!(a);
+ // Sort by the representation, not the value. So "\"c\"" sorts before "a" because '"' sorts
+ // before 'a'.
+ a.sort_values_by(|k1, _, k2, _| k1.display_repr().cmp(&k2.display_repr()));
+ })
+ .produces_display(
+ r#"
+ [a.z]
+
+ [a]
+ "c" = 3
+ a = 1
+ # this comment is attached to b
+ b = 2 # as well as this
+
+ [a.y]
+"#,
+ );
+}
+
+#[test]
+fn test_set_position() {
+ given(
+ r#"
+ [package]
+ [dependencies]
+ [dependencies.opencl]
+ [dev-dependencies]"#,
+ )
+ .running(|root| {
+ for (header, table) in root.iter_mut() {
+ if header == "dependencies" {
+ let tab = as_table!(table);
+ tab.set_position(0);
+ let (_, segmented) = tab.iter_mut().next().unwrap();
+ as_table!(segmented).set_position(5)
+ }
+ }
+ })
+ .produces_display(
+ r#" [dependencies]
+
+ [package]
+ [dev-dependencies]
+ [dependencies.opencl]
+"#,
+ );
+}
+
+#[test]
+fn test_multiple_zero_positions() {
+ given(
+ r#"
+ [package]
+ [dependencies]
+ [dependencies.opencl]
+ a=""
+ [dev-dependencies]"#,
+ )
+ .running(|root| {
+ for (_, table) in root.iter_mut() {
+ as_table!(table).set_position(0)
+ }
+ })
+ .produces_display(
+ r#"
+ [package]
+ [dependencies]
+ [dev-dependencies]
+ [dependencies.opencl]
+ a=""
+"#,
+ );
+}
+
+#[test]
+fn test_multiple_max_usize_positions() {
+ given(
+ r#"
+ [package]
+ [dependencies]
+ [dependencies.opencl]
+ a=""
+ [dev-dependencies]"#,
+ )
+ .running(|root| {
+ for (_, table) in root.iter_mut() {
+ as_table!(table).set_position(usize::MAX)
+ }
+ })
+ .produces_display(
+ r#" [dependencies.opencl]
+ a=""
+
+ [package]
+ [dependencies]
+ [dev-dependencies]
+"#,
+ );
+}
+
+macro_rules! as_array {
+ ($entry:ident) => {{
+ assert!($entry.is_value());
+ let a = $entry.as_value_mut().unwrap();
+ assert!(a.is_array());
+ a.as_array_mut().unwrap()
+ }};
+}
+
+#[test]
+fn test_insert_replace_into_array() {
+ given(
+ r#"
+ a = [1,2,3]
+ b = []"#,
+ )
+ .running(|root| {
+ {
+ let a = root.get_mut("a").unwrap();
+ let a = as_array!(a);
+ assert_eq!(a.len(), 3);
+ assert!(a.get(2).is_some());
+ a.push(4);
+ assert_eq!(a.len(), 4);
+ a.fmt();
+ }
+ let b = root.get_mut("b").unwrap();
+ let b = as_array!(b);
+ assert!(b.is_empty());
+ b.push("hello");
+ assert_eq!(b.len(), 1);
+
+ b.push_formatted(Value::from("world").decorated("\n", "\n"));
+ b.push_formatted(Value::from("test").decorated("", ""));
+
+ b.insert(1, "beep");
+ b.insert_formatted(2, Value::from("boop").decorated(" ", " "));
+
+ // This should preserve formatting.
+ assert_eq!(b.replace(2, "zoink").as_str(), Some("boop"));
+ // This should replace formatting.
+ assert_eq!(
+ b.replace_formatted(4, Value::from("yikes").decorated(" ", ""))
+ .as_str(),
+ Some("test")
+ );
+ dbg!(root);
+ })
+ .produces_display(
+ r#"
+ a = [1, 2, 3, 4]
+ b = ["hello", "beep", "zoink" ,
+"world"
+, "yikes"]
+"#,
+ );
+}
+
+#[test]
+fn test_remove_from_array() {
+ given(
+ r#"
+ a = [1, 2, 3, 4]
+ b = ["hello"]"#,
+ )
+ .running(|root| {
+ {
+ let a = root.get_mut("a").unwrap();
+ let a = as_array!(a);
+ assert_eq!(a.len(), 4);
+ assert!(a.remove(3).is_integer());
+ assert_eq!(a.len(), 3);
+ }
+ let b = root.get_mut("b").unwrap();
+ let b = as_array!(b);
+ assert_eq!(b.len(), 1);
+ assert!(b.remove(0).is_str());
+ assert!(b.is_empty());
+ })
+ .produces_display(
+ r#"
+ a = [1, 2, 3]
+ b = []
+"#,
+ );
+}
+
+#[test]
+fn test_format_array() {
+ given(
+ r#"
+ a = [
+ 1,
+ "2",
+ 3.0,
+ ]
+ "#,
+ )
+ .running(|root| {
+ for (_, v) in root.iter_mut() {
+ if let Item::Value(Value::Array(array)) = v {
+ array.fmt();
+ }
+ }
+ })
+ .produces_display(
+ r#"
+ a = [1, "2", 3.0]
+ "#,
+ );
+}
+
+macro_rules! as_inline_table {
+ ($entry:ident) => {{
+ assert!($entry.is_value());
+ let a = $entry.as_value_mut().unwrap();
+ assert!(a.is_inline_table());
+ a.as_inline_table_mut().unwrap()
+ }};
+}
+
+#[test]
+fn test_insert_into_inline_table() {
+ given(
+ r#"
+ a = {a=2, c = 3}
+ b = {}"#,
+ )
+ .running(|root| {
+ {
+ let a = root.get_mut("a").unwrap();
+ let a = as_inline_table!(a);
+ assert_eq!(a.len(), 2);
+ assert!(a.contains_key("a") && a.get("c").is_some() && a.get_mut("c").is_some());
+ a.get_or_insert("b", 42);
+ assert_eq!(a.len(), 3);
+ a.fmt();
+ }
+ let b = root.get_mut("b").unwrap();
+ let b = as_inline_table!(b);
+ assert!(b.is_empty());
+ b.get_or_insert("hello", "world");
+ assert_eq!(b.len(), 1);
+ b.fmt()
+ })
+ .produces_display(
+ r#"
+ a = { a = 2, c = 3, b = 42 }
+ b = { hello = "world" }
+"#,
+ );
+}
+
+#[test]
+fn test_remove_from_inline_table() {
+ given(
+ r#"
+ a = {a=2, c = 3, b = 42}
+ b = {'hello' = "world"}"#,
+ )
+ .running(|root| {
+ {
+ let a = root.get_mut("a").unwrap();
+ let a = as_inline_table!(a);
+ assert_eq!(a.len(), 3);
+ assert!(a.remove("c").is_some());
+ assert_eq!(a.len(), 2);
+ }
+ let b = root.get_mut("b").unwrap();
+ let b = as_inline_table!(b);
+ assert_eq!(b.len(), 1);
+ assert!(b.remove("hello").is_some());
+ assert!(b.is_empty());
+ })
+ .produces_display(
+ r#"
+ a = {a=2, b = 42}
+ b = {}
+"#,
+ );
+}
+
+#[test]
+fn test_as_table_like() {
+ given(
+ r#"
+ a = {a=2, c = 3, b = 42}
+ x = {}
+ [[bin]]
+ [b]
+ x = "y"
+ [empty]"#,
+ )
+ .running(|root| {
+ let a = root["a"].as_table_like();
+ assert!(a.is_some());
+ let a = a.unwrap();
+ assert_eq!(a.iter().count(), 3);
+ assert_eq!(a.len(), 3);
+ assert_eq!(a.get("a").and_then(Item::as_integer), Some(2));
+
+ let b = root["b"].as_table_like();
+ assert!(b.is_some());
+ let b = b.unwrap();
+ assert_eq!(b.iter().count(), 1);
+ assert_eq!(b.len(), 1);
+ assert_eq!(b.get("x").and_then(Item::as_str), Some("y"));
+
+ assert_eq!(root["x"].as_table_like().map(|t| t.iter().count()), Some(0));
+ assert_eq!(
+ root["empty"].as_table_like().map(|t| t.is_empty()),
+ Some(true)
+ );
+
+ assert!(root["bin"].as_table_like().is_none());
+ });
+}
+
+#[test]
+fn test_inline_table_append() {
+ let mut a = Value::from_iter(vec![
+ (parse_key!("a"), 1),
+ (parse_key!("b"), 2),
+ (parse_key!("c"), 3),
+ ]);
+ let a = a.as_inline_table_mut().unwrap();
+
+ let mut b = Value::from_iter(vec![
+ (parse_key!("c"), 4),
+ (parse_key!("d"), 5),
+ (parse_key!("e"), 6),
+ ]);
+ let b = b.as_inline_table_mut().unwrap();
+
+ a.extend(b.iter());
+ assert_eq!(a.len(), 5);
+ assert!(a.contains_key("e"));
+ assert_eq!(b.len(), 3);
+}
+
+#[test]
+fn test_insert_dotted_into_std_table() {
+ given("")
+ .running(|root| {
+ root["nixpkgs"] = table();
+
+ root["nixpkgs"]["src"] = table();
+ root["nixpkgs"]["src"]
+ .as_table_like_mut()
+ .unwrap()
+ .set_dotted(true);
+ root["nixpkgs"]["src"]["git"] = value("https://github.com/nixos/nixpkgs");
+ })
+ .produces_display(
+ r#"[nixpkgs]
+src.git = "https://github.com/nixos/nixpkgs"
+"#,
+ );
+}
+
+#[test]
+fn test_insert_dotted_into_implicit_table() {
+ given("")
+ .running(|root| {
+ root["nixpkgs"] = table();
+
+ root["nixpkgs"]["src"]["git"] = value("https://github.com/nixos/nixpkgs");
+ root["nixpkgs"]["src"]
+ .as_table_like_mut()
+ .unwrap()
+ .set_dotted(true);
+ })
+ .produces_display(
+ r#"[nixpkgs]
+src.git = "https://github.com/nixos/nixpkgs"
+"#,
+ );
+}
diff --git a/tests/testsuite/invalid.rs b/tests/testsuite/invalid.rs
new file mode 100644
index 0000000..cb13b4e
--- /dev/null
+++ b/tests/testsuite/invalid.rs
@@ -0,0 +1,211 @@
+#[test]
+fn incomplete_inline_table_issue_296() {
+ let err = "native = {".parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(
+ r#"TOML parse error at line 1, column 11
+ |
+1 | native = {
+ | ^
+invalid inline table
+expected `}`
+"#,
+ err.to_string(),
+ );
+}
+
+#[test]
+fn bare_value_disallowed_issue_293() {
+ let err = "value=zzz".parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(
+ r#"TOML parse error at line 1, column 7
+ |
+1 | value=zzz
+ | ^
+invalid string
+expected `"`, `'`
+"#,
+ err.to_string(),
+ );
+}
+
+#[test]
+fn bare_value_in_array_disallowed_issue_293() {
+ let err = "value=[zzz]".parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(
+ r#"TOML parse error at line 1, column 8
+ |
+1 | value=[zzz]
+ | ^
+invalid array
+expected `]`
+"#,
+ err.to_string(),
+ );
+}
+
+#[test]
+fn duplicate_table_after_dotted_key_issue_509() {
+ let err = "
+[dependencies.foo]
+version = \"0.16\"
+
+[dependencies]
+libc = \"0.2\"
+
+[dependencies]
+rand = \"0.3.14\"
+"
+ .parse::<toml_edit::Document>()
+ .unwrap_err();
+ snapbox::assert_eq(
+ r#"TOML parse error at line 8, column 1
+ |
+8 | [dependencies]
+ | ^
+invalid table header
+duplicate key `dependencies` in document root
+"#,
+ err.to_string(),
+ );
+}
+
+#[test]
+fn bad() {
+ let toml_input = "a = 01";
+ let expected_err = "\
+TOML parse error at line 1, column 6
+ |
+1 | a = 01
+ | ^
+expected newline, `#`
+";
+ let err = toml_input.parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(expected_err, err.to_string());
+
+ let toml_input = "a = 1__1";
+ let expected_err = "\
+TOML parse error at line 1, column 7
+ |
+1 | a = 1__1
+ | ^
+invalid integer
+expected digit
+";
+ let err = toml_input.parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(expected_err, err.to_string());
+
+ let toml_input = "a = 1_";
+ let expected_err = "\
+TOML parse error at line 1, column 7
+ |
+1 | a = 1_
+ | ^
+invalid integer
+expected digit
+";
+ let err = toml_input.parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(expected_err, err.to_string());
+
+ let toml_input = "''";
+ let expected_err = "\
+TOML parse error at line 1, column 3
+ |
+1 | ''
+ | ^
+expected `.`, `=`
+";
+ let err = toml_input.parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(expected_err, err.to_string());
+
+ let toml_input = "a = 9e99999";
+ let expected_err = "\
+TOML parse error at line 1, column 5
+ |
+1 | a = 9e99999
+ | ^
+invalid floating-point number
+";
+ let err = toml_input.parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(expected_err, err.to_string());
+
+ let toml_input = "a = \"\u{7f}\"";
+ let expected_err = "\
+TOML parse error at line 1, column 6
+ |
+1 | a = \"\u{7f}\"
+ | ^
+invalid basic string
+";
+ let err = toml_input.parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(expected_err, err.to_string());
+
+ let toml_input = "a = '\u{7f}'";
+ let expected_err = "\
+TOML parse error at line 1, column 6
+ |
+1 | a = '\u{7f}'
+ | ^
+invalid literal string
+";
+ let err = toml_input.parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(expected_err, err.to_string());
+
+ let toml_input = "a = -0x1";
+ let expected_err = "\
+TOML parse error at line 1, column 7
+ |
+1 | a = -0x1
+ | ^
+expected newline, `#`
+";
+ let err = toml_input.parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(expected_err, err.to_string());
+
+ let toml_input = "a = 0x-1";
+ let expected_err = "\
+TOML parse error at line 1, column 7
+ |
+1 | a = 0x-1
+ | ^
+invalid hexadecimal integer
+";
+ let err = toml_input.parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(expected_err, err.to_string());
+
+ // Dotted keys.
+ let toml_input = "a.b.c = 1
+ a.b = 2
+ ";
+ let expected_err = "\
+TOML parse error at line 2, column 10
+ |
+2 | a.b = 2
+ | ^
+duplicate key `b` in document root
+";
+ let err = toml_input.parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(expected_err, err.to_string());
+
+ let toml_input = "a = 1
+ a.b = 2";
+ let expected_err = "\
+TOML parse error at line 2, column 10
+ |
+2 | a.b = 2
+ | ^
+dotted key `a` attempted to extend non-table type (integer)
+";
+ let err = toml_input.parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(expected_err, err.to_string());
+
+ let toml_input = "a = {k1 = 1, k1.name = \"joe\"}";
+ let expected_err = "\
+TOML parse error at line 1, column 6
+ |
+1 | a = {k1 = 1, k1.name = \"joe\"}
+ | ^
+dotted key `k1` attempted to extend non-table type (integer)
+";
+ let err = toml_input.parse::<toml_edit::Document>().unwrap_err();
+ snapbox::assert_eq(expected_err, err.to_string());
+}
diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs
new file mode 100644
index 0000000..1476c5d
--- /dev/null
+++ b/tests/testsuite/main.rs
@@ -0,0 +1,8 @@
+#![recursion_limit = "256"]
+
+mod convert;
+mod datetime;
+mod edit;
+mod invalid;
+mod parse;
+mod stackoverflow;
diff --git a/tests/testsuite/parse.rs b/tests/testsuite/parse.rs
new file mode 100644
index 0000000..f1c3c27
--- /dev/null
+++ b/tests/testsuite/parse.rs
@@ -0,0 +1,1490 @@
+use snapbox::assert_eq;
+use toml_edit::{Document, Key, Value};
+
+macro_rules! parse {
+ ($s:expr, $ty:ty) => {{
+ let v = $s.parse::<$ty>();
+ assert!(v.is_ok(), "Failed with {}", v.unwrap_err());
+ v.unwrap()
+ }};
+}
+
+macro_rules! parse_value {
+ ($s:expr) => {
+ parse!($s, Value)
+ };
+}
+
+macro_rules! test_key {
+ ($s:expr, $expected:expr) => {{
+ let key = parse!($s, Key);
+ assert_eq!($expected, key.get(), "");
+ }};
+}
+
+#[test]
+fn test_key_from_str() {
+ test_key!("a", "a");
+ test_key!(r#"'hello key'"#, "hello key");
+ test_key!(
+ r#""Jos\u00E9\U000A0000\n\t\r\f\b\"""#,
+ "Jos\u{00E9}\u{A0000}\n\t\r\u{c}\u{8}\""
+ );
+ test_key!("\"\"", "");
+ test_key!("\"'hello key'bla\"", "'hello key'bla");
+ test_key!(
+ "'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\cargo-edit-test.YizxPxxElXn9'",
+ "C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\cargo-edit-test.YizxPxxElXn9"
+ );
+}
+
+#[test]
+fn test_value_from_str() {
+ assert!(parse_value!("1979-05-27T00:32:00.999999-07:00").is_datetime());
+ assert!(parse_value!("1979-05-27T00:32:00.999999Z").is_datetime());
+ assert!(parse_value!("1979-05-27T00:32:00.999999").is_datetime());
+ assert!(parse_value!("1979-05-27T00:32:00").is_datetime());
+ assert!(parse_value!("1979-05-27").is_datetime());
+ assert!(parse_value!("00:32:00").is_datetime());
+ assert!(parse_value!("-239").is_integer());
+ assert!(parse_value!("1e200").is_float());
+ assert!(parse_value!("9_224_617.445_991_228_313").is_float());
+ assert!(parse_value!(r#""basic string\nJos\u00E9\n""#).is_str());
+ assert!(parse_value!(
+ r#""""
+multiline basic string
+""""#
+ )
+ .is_str());
+ assert!(parse_value!(r#"'literal string\ \'"#).is_str());
+ assert!(parse_value!(
+ r#"'''multiline
+literal \ \
+string'''"#
+ )
+ .is_str());
+ assert!(parse_value!(r#"{ hello = "world", a = 1}"#).is_inline_table());
+ assert!(
+ parse_value!(r#"[ { x = 1, a = "2" }, {a = "a",b = "b", c = "c"} ]"#).is_array()
+ );
+ let wp = "C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\cargo-edit-test.YizxPxxElXn9";
+ let lwp = "'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\cargo-edit-test.YizxPxxElXn9'";
+ assert_eq!(Value::from(wp).as_str(), parse_value!(lwp).as_str());
+ assert!(parse_value!(r#""\\\"\b\f\n\r\t\u00E9\U000A0000""#).is_str());
+}
+
+#[test]
+fn test_key_unification() {
+ let toml = r#"
+[a]
+[a.'b'.c]
+[a."b".c.e]
+[a.b.c.d]
+"#;
+ let expected = r#"
+[a]
+[a.'b'.c]
+[a.'b'.c.e]
+[a.'b'.c.d]
+"#;
+ let doc = toml.parse::<Document>();
+ assert!(doc.is_ok());
+ let doc = doc.unwrap();
+
+ assert_eq(expected, doc.to_string());
+}
+
+macro_rules! bad {
+ ($toml:expr, $msg:expr) => {
+ match $toml.parse::<Document>() {
+ Ok(s) => panic!("parsed to: {:#?}", s),
+ Err(e) => snapbox::assert_eq($msg, e.to_string()),
+ }
+ };
+}
+
+#[test]
+fn crlf() {
+ "\
+ [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\
+ \"\"\"\
+ "
+ .parse::<Document>()
+ .unwrap();
+}
+
+#[test]
+fn fun_with_strings() {
+ let table = 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.
+'''
+"#
+ .parse::<Document>()
+ .unwrap();
+ assert_eq!(table["bar"].as_str(), Some("\0"));
+ assert_eq!(table["key1"].as_str(), Some("One\nTwo"));
+ assert_eq!(table["key2"].as_str(), Some("One\nTwo"));
+ assert_eq!(table["key3"].as_str(), Some("One\nTwo"));
+
+ let msg = "The quick brown fox jumps over the lazy dog.";
+ assert_eq!(table["key4"].as_str(), Some(msg));
+ assert_eq!(table["key5"].as_str(), Some(msg));
+ assert_eq!(table["key6"].as_str(), Some(msg));
+
+ assert_eq!(
+ table["winpath"].as_str(),
+ Some(r"C:\Users\nodejs\templates")
+ );
+ assert_eq!(
+ table["winpath2"].as_str(),
+ Some(r"\\ServerX\admin$\system32\")
+ );
+ assert_eq!(
+ table["quoted"].as_str(),
+ Some(r#"Tom "Dubs" Preston-Werner"#)
+ );
+ assert_eq!(table["regex"].as_str(), Some(r"<\i\c*\s*>"));
+ assert_eq!(
+ table["regex2"].as_str(),
+ Some(r"I [dw]on't need \d{2} apples")
+ );
+ assert_eq!(
+ table["lines"].as_str(),
+ Some(
+ "The first newline is\n\
+ trimmed in raw strings.\n\
+ All other whitespace\n\
+ is preserved.\n"
+ )
+ );
+}
+
+#[test]
+fn tables_in_arrays() {
+ let table = r#"
+[[foo]]
+#…
+[foo.bar]
+#…
+
+[[foo]] # ...
+#…
+[foo.bar]
+#...
+"#
+ .parse::<Document>()
+ .unwrap();
+ table["foo"][0]["bar"].as_table().unwrap();
+ table["foo"][1]["bar"].as_table().unwrap();
+}
+
+#[test]
+fn empty_table() {
+ let table = r#"
+[foo]"#
+ .parse::<Document>()
+ .unwrap();
+ table["foo"].as_table().unwrap();
+}
+
+#[test]
+fn mixed_table_issue_527() {
+ let input = r#"
+[package]
+metadata.msrv = "1.65.0"
+
+[package.metadata.release.pre-release-replacements]
+"#;
+ let document = input.parse::<Document>().unwrap();
+ let actual = document.to_string();
+ assert_eq(input, actual);
+}
+
+#[test]
+fn fruit() {
+ let table = 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"
+"#
+ .parse::<Document>()
+ .unwrap();
+ assert_eq!(table["fruit"][0]["name"].as_str(), Some("apple"));
+ assert_eq!(table["fruit"][0]["physical"]["color"].as_str(), Some("red"));
+ assert_eq!(
+ table["fruit"][0]["physical"]["shape"].as_str(),
+ Some("round")
+ );
+ assert_eq!(
+ table["fruit"][0]["variety"][0]["name"].as_str(),
+ Some("red delicious")
+ );
+ assert_eq!(
+ table["fruit"][0]["variety"][1]["name"].as_str(),
+ Some("granny smith")
+ );
+ assert_eq!(table["fruit"][1]["name"].as_str(), Some("banana"));
+ assert_eq!(
+ table["fruit"][1]["variety"][0]["name"].as_str(),
+ Some("plantain")
+ );
+}
+
+#[test]
+fn stray_cr() {
+ bad!(
+ "\r",
+ "\
+TOML parse error at line 1, column 1
+ |
+1 | \r
+ | ^
+
+"
+ );
+ bad!(
+ "a = [ \r ]",
+ "\
+TOML parse error at line 1, column 7
+ |
+1 | a = [ \r
+ ]
+ | ^
+invalid array
+expected `]`
+"
+ );
+ bad!(
+ "a = \"\"\"\r\"\"\"",
+ "\
+TOML parse error at line 1, column 8
+ |
+1 | a = \"\"\"\r
+\"\"\"
+ | ^
+invalid multiline basic string
+"
+ );
+ bad!(
+ "a = \"\"\"\\ \r \"\"\"",
+ "\
+TOML parse error at line 1, column 10
+ |
+1 | a = \"\"\"\\ \r
+ \"\"\"
+ | ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\\`, `\"`
+"
+ );
+ bad!(
+ "a = '''\r'''",
+ "\
+TOML parse error at line 1, column 8
+ |
+1 | a = '''\r
+'''
+ | ^
+invalid multiline literal string
+"
+ );
+ bad!(
+ "a = '\r'",
+ "\
+TOML parse error at line 1, column 6
+ |
+1 | a = '\r
+'
+ | ^
+invalid literal string
+"
+ );
+ bad!(
+ "a = \"\r\"",
+ "\
+TOML parse error at line 1, column 6
+ |
+1 | a = \"\r
+\"
+ | ^
+invalid basic string
+"
+ );
+}
+
+#[test]
+fn blank_literal_string() {
+ let table = "foo = ''".parse::<Document>().unwrap();
+ assert_eq!(table["foo"].as_str(), Some(""));
+}
+
+#[test]
+fn many_blank() {
+ let table = "foo = \"\"\"\n\n\n\"\"\"".parse::<Document>().unwrap();
+ assert_eq!(table["foo"].as_str(), Some("\n\n"));
+}
+
+#[test]
+fn literal_eats_crlf() {
+ let table = "
+ foo = \"\"\"\\\r\n\"\"\"
+ bar = \"\"\"\\\r\n \r\n \r\n a\"\"\"
+ "
+ .parse::<Document>()
+ .unwrap();
+ assert_eq!(table["foo"].as_str(), Some(""));
+ assert_eq!(table["bar"].as_str(), Some("a"));
+}
+
+#[test]
+fn string_no_newline() {
+ bad!(
+ "a = \"\n\"",
+ "\
+TOML parse error at line 1, column 6
+ |
+1 | a = \"
+ | ^
+invalid basic string
+"
+ );
+ bad!(
+ "a = '\n'",
+ "\
+TOML parse error at line 1, column 6
+ |
+1 | a = '
+ | ^
+invalid literal string
+"
+ );
+}
+
+#[test]
+fn bad_leading_zeros() {
+ bad!(
+ "a = 00",
+ "\
+TOML parse error at line 1, column 6
+ |
+1 | a = 00
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "a = -00",
+ "\
+TOML parse error at line 1, column 7
+ |
+1 | a = -00
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "a = +00",
+ "\
+TOML parse error at line 1, column 7
+ |
+1 | a = +00
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "a = 00.0",
+ "\
+TOML parse error at line 1, column 6
+ |
+1 | a = 00.0
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "a = -00.0",
+ "\
+TOML parse error at line 1, column 7
+ |
+1 | a = -00.0
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "a = +00.0",
+ "\
+TOML parse error at line 1, column 7
+ |
+1 | a = +00.0
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "a = 9223372036854775808",
+ "\
+TOML parse error at line 1, column 5
+ |
+1 | a = 9223372036854775808
+ | ^
+number too large to fit in target type
+"
+ );
+ bad!(
+ "a = -9223372036854775809",
+ "\
+TOML parse error at line 1, column 5
+ |
+1 | a = -9223372036854775809
+ | ^
+number too small to fit in target type
+"
+ );
+}
+
+#[test]
+fn bad_floats() {
+ bad!(
+ "a = 0.",
+ "\
+TOML parse error at line 1, column 7
+ |
+1 | a = 0.
+ | ^
+invalid floating-point number
+expected digit
+"
+ );
+ bad!(
+ "a = 0.e",
+ "\
+TOML parse error at line 1, column 7
+ |
+1 | a = 0.e
+ | ^
+invalid floating-point number
+expected digit
+"
+ );
+ bad!(
+ "a = 0.E",
+ "\
+TOML parse error at line 1, column 7
+ |
+1 | a = 0.E
+ | ^
+invalid floating-point number
+expected digit
+"
+ );
+ bad!(
+ "a = 0.0E",
+ "\
+TOML parse error at line 1, column 9
+ |
+1 | a = 0.0E
+ | ^
+invalid floating-point number
+"
+ );
+ bad!(
+ "a = 0.0e",
+ "\
+TOML parse error at line 1, column 9
+ |
+1 | a = 0.0e
+ | ^
+invalid floating-point number
+"
+ );
+ bad!(
+ "a = 0.0e-",
+ "\
+TOML parse error at line 1, column 10
+ |
+1 | a = 0.0e-
+ | ^
+invalid floating-point number
+"
+ );
+ bad!(
+ "a = 0.0e+",
+ "\
+TOML parse error at line 1, column 10
+ |
+1 | a = 0.0e+
+ | ^
+invalid floating-point number
+"
+ );
+}
+
+#[test]
+fn floats() {
+ macro_rules! t {
+ ($actual:expr, $expected:expr) => {{
+ let f = format!("foo = {}", $actual);
+ println!("{}", f);
+ let a = f.parse::<Document>().unwrap();
+ assert_eq!(a["foo"].as_float().unwrap(), $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 a = "
+ 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\"
+ "
+ .parse::<Document>()
+ .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",
+ "\
+TOML parse error at line 1, column 4
+ |
+1 | key
+ | ^
+expected `.`, `=`
+"
+ );
+ bad!(
+ "key=\n3",
+ "\
+TOML parse error at line 1, column 5
+ |
+1 | key=
+ | ^
+invalid string
+expected `\"`, `'`
+"
+ );
+ bad!(
+ "key|=3",
+ "\
+TOML parse error at line 1, column 4
+ |
+1 | key|=3
+ | ^
+expected `.`, `=`
+"
+ );
+ bad!(
+ "=3",
+ "\
+TOML parse error at line 1, column 1
+ |
+1 | =3
+ | ^
+invalid key
+"
+ );
+ bad!(
+ "\"\"|=3",
+ "\
+TOML parse error at line 1, column 3
+ |
+1 | \"\"|=3
+ | ^
+expected `.`, `=`
+"
+ );
+ bad!(
+ "\"\n\"|=3",
+ "\
+TOML parse error at line 1, column 2
+ |
+1 | \"
+ | ^
+invalid basic string
+"
+ );
+ bad!(
+ "\"\r\"|=3",
+ "\
+TOML parse error at line 1, column 2
+ |
+1 | \"\r\"|=3
+ | ^
+invalid basic string
+"
+ );
+ bad!(
+ "''''''=3",
+ "\
+TOML parse error at line 1, column 3
+ |
+1 | ''''''=3
+ | ^
+expected `.`, `=`
+"
+ );
+ bad!(
+ "\"\"\"\"\"\"=3",
+ "\
+TOML parse error at line 1, column 3
+ |
+1 | \"\"\"\"\"\"=3
+ | ^
+expected `.`, `=`
+"
+ );
+ bad!(
+ "'''key'''=3",
+ "\
+TOML parse error at line 1, column 3
+ |
+1 | '''key'''=3
+ | ^
+expected `.`, `=`
+"
+ );
+ bad!(
+ "\"\"\"key\"\"\"=3",
+ "\
+TOML parse error at line 1, column 3
+ |
+1 | \"\"\"key\"\"\"=3
+ | ^
+expected `.`, `=`
+"
+ );
+}
+
+#[test]
+fn bad_table_names() {
+ bad!(
+ "[]",
+ "\
+TOML parse error at line 1, column 2
+ |
+1 | []
+ | ^
+invalid key
+"
+ );
+ bad!(
+ "[.]",
+ "\
+TOML parse error at line 1, column 2
+ |
+1 | [.]
+ | ^
+invalid key
+"
+ );
+ bad!(
+ "[a.]",
+ "\
+TOML parse error at line 1, column 3
+ |
+1 | [a.]
+ | ^
+invalid table header
+expected `.`, `]`
+"
+ );
+ bad!(
+ "[!]",
+ "\
+TOML parse error at line 1, column 2
+ |
+1 | [!]
+ | ^
+invalid key
+"
+ );
+ bad!(
+ "[\"\n\"]",
+ "\
+TOML parse error at line 1, column 3
+ |
+1 | [\"
+ | ^
+invalid basic string
+"
+ );
+ bad!(
+ "[a.b]\n[a.\"b\"]",
+ "\
+TOML parse error at line 2, column 1
+ |
+2 | [a.\"b\"]
+ | ^
+invalid table header
+duplicate key `b` in table `a`
+"
+ );
+ bad!(
+ "[']",
+ "\
+TOML parse error at line 1, column 4
+ |
+1 | [']
+ | ^
+invalid literal string
+"
+ );
+ bad!(
+ "[''']",
+ "\
+TOML parse error at line 1, column 4
+ |
+1 | [''']
+ | ^
+invalid table header
+expected `.`, `]`
+"
+ );
+ bad!(
+ "['''''']",
+ "\
+TOML parse error at line 1, column 4
+ |
+1 | ['''''']
+ | ^
+invalid table header
+expected `.`, `]`
+"
+ );
+ bad!(
+ "['''foo''']",
+ "\
+TOML parse error at line 1, column 4
+ |
+1 | ['''foo''']
+ | ^
+invalid table header
+expected `.`, `]`
+"
+ );
+ bad!(
+ "[\"\"\"bar\"\"\"]",
+ "\
+TOML parse error at line 1, column 4
+ |
+1 | [\"\"\"bar\"\"\"]
+ | ^
+invalid table header
+expected `.`, `]`
+"
+ );
+ bad!(
+ "['\n']",
+ "\
+TOML parse error at line 1, column 3
+ |
+1 | ['
+ | ^
+invalid literal string
+"
+ );
+ bad!(
+ "['\r\n']",
+ "\
+TOML parse error at line 1, column 3
+ |
+1 | ['
+ | ^
+invalid literal string
+"
+ );
+}
+
+#[test]
+fn table_names() {
+ let a = "
+ [a.\"b\"]
+ [\"f f\"]
+ [\"f.f\"]
+ [\"\\\"\"]
+ ['a.a']
+ ['\"\"']
+ "
+ .parse::<Document>()
+ .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",
+ "\
+TOML parse error at line 1, column 2
+ |
+1 | 4
+ | ^
+expected `.`, `=`
+"
+ );
+}
+
+#[test]
+fn inline_tables() {
+ "a = {}".parse::<Document>().unwrap();
+ "a = {b=1}".parse::<Document>().unwrap();
+ "a = { b = 1 }".parse::<Document>().unwrap();
+ "a = {a=1,b=2}".parse::<Document>().unwrap();
+ "a = {a=1,b=2,c={}}".parse::<Document>().unwrap();
+
+ bad!(
+ "a = {a=1,}",
+ "\
+TOML parse error at line 1, column 9
+ |
+1 | a = {a=1,}
+ | ^
+invalid inline table
+expected `}`
+"
+ );
+ bad!(
+ "a = {,}",
+ "\
+TOML parse error at line 1, column 6
+ |
+1 | a = {,}
+ | ^
+invalid inline table
+expected `}`
+"
+ );
+ bad!(
+ "a = {a=1,a=1}",
+ "\
+TOML parse error at line 1, column 6
+ |
+1 | a = {a=1,a=1}
+ | ^
+duplicate key `a`
+"
+ );
+ bad!(
+ "a = {\n}",
+ "\
+TOML parse error at line 1, column 6
+ |
+1 | a = {
+ | ^
+invalid inline table
+expected `}`
+"
+ );
+ bad!(
+ "a = {",
+ "\
+TOML parse error at line 1, column 6
+ |
+1 | a = {
+ | ^
+invalid inline table
+expected `}`
+"
+ );
+
+ "a = {a=[\n]}".parse::<Document>().unwrap();
+ "a = {\"a\"=[\n]}".parse::<Document>().unwrap();
+ "a = [\n{},\n{},\n]".parse::<Document>().unwrap();
+}
+
+#[test]
+fn number_underscores() {
+ macro_rules! t {
+ ($actual:expr, $expected:expr) => {{
+ let f = format!("foo = {}", $actual);
+ let table = f.parse::<Document>().unwrap();
+ assert_eq!(table["foo"].as_integer().unwrap(), $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_",
+ "\
+TOML parse error at line 1, column 8
+ |
+1 | foo = 0_
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "foo = 0__0",
+ "\
+TOML parse error at line 1, column 8
+ |
+1 | foo = 0__0
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "foo = __0",
+ "\
+TOML parse error at line 1, column 7
+ |
+1 | foo = __0
+ | ^
+invalid integer
+expected leading digit
+"
+ );
+ bad!(
+ "foo = 1_0_",
+ "\
+TOML parse error at line 1, column 11
+ |
+1 | foo = 1_0_
+ | ^
+invalid integer
+expected digit
+"
+ );
+}
+
+#[test]
+fn bad_unicode_codepoint() {
+ bad!(
+ "foo = \"\\uD800\"",
+ "\
+TOML parse error at line 1, column 10
+ |
+1 | foo = \"\\uD800\"
+ | ^
+invalid unicode 4-digit hex code
+value is out of range
+"
+ );
+}
+
+#[test]
+fn bad_strings() {
+ bad!(
+ "foo = \"\\uxx\"",
+ "\
+TOML parse error at line 1, column 10
+ |
+1 | foo = \"\\uxx\"
+ | ^
+invalid unicode 4-digit hex code
+"
+ );
+ bad!(
+ "foo = \"\\u\"",
+ "\
+TOML parse error at line 1, column 10
+ |
+1 | foo = \"\\u\"
+ | ^
+invalid unicode 4-digit hex code
+"
+ );
+ bad!(
+ "foo = \"\\",
+ "\
+TOML parse error at line 1, column 8
+ |
+1 | foo = \"\\
+ | ^
+invalid basic string
+"
+ );
+ bad!(
+ "foo = '",
+ "\
+TOML parse error at line 1, column 8
+ |
+1 | foo = '
+ | ^
+invalid literal string
+"
+ );
+}
+
+#[test]
+fn empty_string() {
+ assert_eq!(
+ "foo = \"\"".parse::<Document>().unwrap()["foo"]
+ .as_str()
+ .unwrap(),
+ ""
+ );
+}
+
+#[test]
+fn booleans() {
+ let table = "foo = true".parse::<Document>().unwrap();
+ assert_eq!(table["foo"].as_bool(), Some(true));
+
+ let table = "foo = false".parse::<Document>().unwrap();
+ assert_eq!(table["foo"].as_bool(), Some(false));
+
+ bad!(
+ "foo = true2",
+ "\
+TOML parse error at line 1, column 11
+ |
+1 | foo = true2
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "foo = false2",
+ "\
+TOML parse error at line 1, column 12
+ |
+1 | foo = false2
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "foo = t1",
+ "\
+TOML parse error at line 1, column 7
+ |
+1 | foo = t1
+ | ^
+invalid string
+expected `\"`, `'`
+"
+ );
+ bad!(
+ "foo = f2",
+ "\
+TOML parse error at line 1, column 7
+ |
+1 | foo = f2
+ | ^
+invalid string
+expected `\"`, `'`
+"
+ );
+}
+
+#[test]
+fn bad_nesting() {
+ bad!(
+ "
+ a = [2]
+ [[a]]
+ b = 5
+ ",
+ "\
+TOML parse error at line 3, column 9
+ |
+3 | [[a]]
+ | ^
+invalid table header
+duplicate key `a` in document root
+"
+ );
+ bad!(
+ "
+ a = 1
+ [a.b]
+ ",
+ "\
+TOML parse error at line 3, column 9
+ |
+3 | [a.b]
+ | ^
+invalid table header
+dotted key `a` attempted to extend non-table type (integer)
+"
+ );
+ bad!(
+ "
+ a = []
+ [a.b]
+ ",
+ "\
+TOML parse error at line 3, column 9
+ |
+3 | [a.b]
+ | ^
+invalid table header
+dotted key `a` attempted to extend non-table type (array)
+"
+ );
+ bad!(
+ "
+ a = []
+ [[a.b]]
+ ",
+ "\
+TOML parse error at line 3, column 9
+ |
+3 | [[a.b]]
+ | ^
+invalid table header
+dotted key `a` attempted to extend non-table type (array)
+"
+ );
+ bad!(
+ "
+ [a]
+ b = { c = 2, d = {} }
+ [a.b]
+ c = 2
+ ",
+ "\
+TOML parse error at line 4, column 9
+ |
+4 | [a.b]
+ | ^
+invalid table header
+duplicate key `b` in table `a`
+"
+ );
+}
+
+#[test]
+fn bad_table_redefine() {
+ bad!(
+ "
+ [a]
+ foo=\"bar\"
+ [a.b]
+ foo=\"bar\"
+ [a]
+ ",
+ "\
+TOML parse error at line 6, column 9
+ |
+6 | [a]
+ | ^
+invalid table header
+duplicate key `a` in document root
+"
+ );
+ bad!(
+ "
+ [a]
+ foo=\"bar\"
+ b = { foo = \"bar\" }
+ [a]
+ ",
+ "\
+TOML parse error at line 5, column 9
+ |
+5 | [a]
+ | ^
+invalid table header
+duplicate key `a` in document root
+"
+ );
+ bad!(
+ "
+ [a]
+ b = {}
+ [a.b]
+ ",
+ "\
+TOML parse error at line 4, column 9
+ |
+4 | [a.b]
+ | ^
+invalid table header
+duplicate key `b` in table `a`
+"
+ );
+
+ bad!(
+ "
+ [a]
+ b = {}
+ [a]
+ ",
+ "\
+TOML parse error at line 4, column 9
+ |
+4 | [a]
+ | ^
+invalid table header
+duplicate key `a` in document root
+"
+ );
+}
+
+#[test]
+fn datetimes() {
+ macro_rules! t {
+ ($actual:expr) => {{
+ let f = format!("foo = {}", $actual);
+ let toml = f.parse::<Document>().expect(&format!("failed: {}", f));
+ assert_eq!(toml["foo"].as_datetime().unwrap().to_string(), $actual);
+ }};
+ }
+
+ t!("2016-09-09T09:09:09Z");
+ t!("2016-09-09T09:09:09.1Z");
+ t!("2016-09-09T09:09:09.2+10:00");
+ t!("2016-09-09T09:09:09.123456789-02:00");
+ bad!(
+ "foo = 2016-09-09T09:09:09.Z",
+ "\
+TOML parse error at line 1, column 26
+ |
+1 | foo = 2016-09-09T09:09:09.Z
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ "foo = 2016-9-09T09:09:09Z",
+ "\
+TOML parse error at line 1, column 12
+ |
+1 | foo = 2016-9-09T09:09:09Z
+ | ^
+invalid date-time
+"
+ );
+ bad!(
+ "foo = 2016-09-09T09:09:09+2:00",
+ "\
+TOML parse error at line 1, column 27
+ |
+1 | foo = 2016-09-09T09:09:09+2:00
+ | ^
+invalid time offset
+"
+ );
+ bad!(
+ "foo = 2016-09-09T09:09:09-2:00",
+ "\
+TOML parse error at line 1, column 27
+ |
+1 | foo = 2016-09-09T09:09:09-2:00
+ | ^
+invalid time offset
+"
+ );
+ bad!(
+ "foo = 2016-09-09T09:09:09Z-2:00",
+ "\
+TOML parse error at line 1, column 27
+ |
+1 | foo = 2016-09-09T09:09:09Z-2:00
+ | ^
+expected newline, `#`
+"
+ );
+}
+
+#[test]
+fn require_newline_after_value() {
+ bad!(
+ "0=0r=false",
+ "\
+TOML parse error at line 1, column 4
+ |
+1 | 0=0r=false
+ | ^
+expected newline, `#`
+"
+ );
+ bad!(
+ r#"
+0=""o=""m=""r=""00="0"q="""0"""e="""0"""
+"#,
+ r#"TOML parse error at line 2, column 5
+ |
+2 | 0=""o=""m=""r=""00="0"q="""0"""e="""0"""
+ | ^
+expected newline, `#`
+"#
+ );
+ bad!(
+ r#"
+[[0000l0]]
+0="0"[[0000l0]]
+0="0"[[0000l0]]
+0="0"l="0"
+"#,
+ r#"TOML parse error at line 3, column 6
+ |
+3 | 0="0"[[0000l0]]
+ | ^
+expected newline, `#`
+"#
+ );
+ bad!(
+ r#"
+0=[0]00=[0,0,0]t=["0","0","0"]s=[1000-00-00T00:00:00Z,2000-00-00T00:00:00Z]
+"#,
+ r#"TOML parse error at line 2, column 6
+ |
+2 | 0=[0]00=[0,0,0]t=["0","0","0"]s=[1000-00-00T00:00:00Z,2000-00-00T00:00:00Z]
+ | ^
+expected newline, `#`
+"#
+ );
+ bad!(
+ r#"
+0=0r0=0r=false
+"#,
+ r#"TOML parse error at line 2, column 4
+ |
+2 | 0=0r0=0r=false
+ | ^
+expected newline, `#`
+"#
+ );
+ bad!(
+ r#"
+0=0r0=0r=falsefal=false
+"#,
+ r#"TOML parse error at line 2, column 4
+ |
+2 | 0=0r0=0r=falsefal=false
+ | ^
+expected newline, `#`
+"#
+ );
+}
+
+#[test]
+fn dont_use_dotted_key_prefix_on_table_fuzz_57049() {
+ // This could generate
+ // ```toml
+ // [
+ // p.o]
+ // ```
+ let input = r#"
+p.a=4
+[p.o]
+"#;
+ let document = input.parse::<Document>().unwrap();
+ let actual = document.to_string();
+ assert_eq(input, actual);
+}
+
+#[test]
+fn despan_keys() {
+ let mut doc = r#"aaaaaa = 1"#.parse::<Document>().unwrap();
+ let key = "bbb".parse::<Key>().unwrap();
+ let table = doc.as_table_mut();
+ table.insert_formatted(
+ &key,
+ toml_edit::Item::Value(Value::Integer(toml_edit::Formatted::new(2))),
+ );
+
+ assert_eq!(doc.to_string(), "aaaaaa = 1\nbbb = 2\n");
+}
diff --git a/tests/testsuite/stackoverflow.rs b/tests/testsuite/stackoverflow.rs
new file mode 100644
index 0000000..de4c722
--- /dev/null
+++ b/tests/testsuite/stackoverflow.rs
@@ -0,0 +1,54 @@
+#[test]
+#[cfg(not(feature = "unbounded"))]
+fn array_recursion_limit() {
+ let depths = [(1, true), (20, true), (300, false)];
+ for (depth, is_ok) in depths {
+ let input = format!("x={}{}", &"[".repeat(depth), &"]".repeat(depth));
+ let document = input.parse::<toml_edit::Document>();
+ assert_eq!(document.is_ok(), is_ok, "depth: {}", depth);
+ }
+}
+
+#[test]
+#[cfg(not(feature = "unbounded"))]
+fn inline_table_recursion_limit() {
+ let depths = [(1, true), (20, true), (300, false)];
+ for (depth, is_ok) in depths {
+ let input = format!("x={}true{}", &"{ x = ".repeat(depth), &"}".repeat(depth));
+ let document = input.parse::<toml_edit::Document>();
+ assert_eq!(document.is_ok(), is_ok, "depth: {}", depth);
+ }
+}
+
+#[test]
+#[cfg(not(feature = "unbounded"))]
+fn table_key_recursion_limit() {
+ let depths = [(1, true), (20, true), (300, false)];
+ for (depth, is_ok) in depths {
+ let input = format!("[x{}]", &".x".repeat(depth));
+ let document = input.parse::<toml_edit::Document>();
+ assert_eq!(document.is_ok(), is_ok, "depth: {}", depth);
+ }
+}
+
+#[test]
+#[cfg(not(feature = "unbounded"))]
+fn dotted_key_recursion_limit() {
+ let depths = [(1, true), (20, true), (300, false)];
+ for (depth, is_ok) in depths {
+ let input = format!("x{} = true", &".x".repeat(depth));
+ let document = input.parse::<toml_edit::Document>();
+ assert_eq!(document.is_ok(), is_ok, "depth: {}", depth);
+ }
+}
+
+#[test]
+#[cfg(not(feature = "unbounded"))]
+fn inline_dotted_key_recursion_limit() {
+ let depths = [(1, true), (20, true), (300, false)];
+ for (depth, is_ok) in depths {
+ let input = format!("x = {{ x{} = true }}", &".x".repeat(depth));
+ let document = input.parse::<toml_edit::Document>();
+ assert_eq!(document.is_ok(), is_ok, "depth: {}", depth);
+ }
+}