Repository: toml-rs/toml
Pull request #972 (2025-06-27 17:45:09)

feat(toml)!: Make serde optional


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d245ba9a..f9fd5ab3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -49,8 +49,12 @@ jobs:
     - uses: taiki-e/install-action@cargo-hack
     - name: Build
       run: cargo test --workspace --no-run
-    - name: Test
-      run: cargo hack test --each-feature --workspace
+    - name: Test workspace
+      run: cargo hack test --each-feature --exclude-features debug --workspace
+    - name: Test toml
+      run: cargo hack test --feature-powerset --exclude-features debug -p toml
+    - name: Test benchmarks
+      run: cargo hack check --feature-powerset -p toml_benchmarks --all-targets
   msrv:
     name: "Check MSRV"
     runs-on: ubuntu-latest
diff --git a/.github/workflows/rust-next.yml b/.github/workflows/rust-next.yml
index b0a4f2bf..8ce6a049 100644
--- a/.github/workflows/rust-next.yml
+++ b/.github/workflows/rust-next.yml
@@ -39,8 +39,12 @@ jobs:
     - uses: taiki-e/install-action@cargo-hack
     - name: Build
       run: cargo test --workspace --no-run
-    - name: Test
-      run: cargo hack test --each-feature --workspace
+    - name: Test workspace
+      run: cargo hack test --each-feature --exclude-features debug --workspace
+    - name: Test toml
+      run: cargo hack test --feature-powerset --exclude-features debug -p toml
+    - name: Test benchmarks
+      run: cargo hack check --feature-powerset -p toml_benchmarks --all-targets
   latest:
     name: "Check latest dependencies"
     runs-on: ubuntu-latest
diff --git a/crates/toml/Cargo.toml b/crates/toml/Cargo.toml
index b8dfc147..40176d3d 100644
--- a/crates/toml/Cargo.toml
+++ b/crates/toml/Cargo.toml
@@ -28,8 +28,9 @@ pre-release-replacements = [
 ]
 
 [features]
-default = ["std", "parse", "display"]
+default = ["std", "serde", "parse", "display"]
 std = ["indexmap?/std"]
+serde = ["dep:serde", "toml_datetime/serde", "serde_spanned/serde"]
 parse = ["dep:toml_parse", "dep:winnow"]
 display = ["dep:toml_write"]
 unsafe = ["toml_parse?/unsafe"]
@@ -49,15 +50,15 @@ unbounded = []
 preserve_order = ["dep:indexmap", "std"]
 
 [dependencies]
-serde = { version = "1.0.145", default-features = false, features = ["alloc"] }
+serde = { version = "1.0.145", default-features = false, features = ["alloc"], optional = true }
 indexmap = { version = "2.0.0", default-features = false, optional = true }
 toml_parse = { version = "0.1.0", path = "../toml_parse", default-features = false, features = ["alloc"], optional = true }
 winnow = { version = "0.7.10", default-features = false, optional = true }
 anstream = { version = "0.6.15", optional = true }
 anstyle = { version = "1.0.8", optional = true }
-toml_datetime = { version = "0.6.11", path = "../toml_datetime", default-features = false, features = ["serde", "alloc"] }
+toml_datetime = { version = "0.6.11", path = "../toml_datetime", default-features = false, features = ["alloc"] }
 toml_write = { version = "0.1.0", path = "../toml_write", optional = true }
-serde_spanned = { version = "0.6.9", path = "../serde_spanned", default-features = false, features = ["serde", "alloc"] }
+serde_spanned = { version = "0.6.9", path = "../serde_spanned", default-features = false, features = ["alloc"] }
 foldhash = { version = "0.1.5", default-features = false, optional = true }
 
 [dev-dependencies]
@@ -84,15 +85,15 @@ harness = false
 
 [[example]]
 name = "decode"
-required-features = ["parse", "display"]
+required-features = ["parse", "display", "serde"]
 
 [[example]]
 name = "enum_external"
-required-features = ["parse", "display"]
+required-features = ["parse", "display", "serde"]
 
 [[example]]
 name = "toml2json"
-required-features = ["parse", "display"]
+required-features = ["parse", "display", "serde"]
 
 [lints]
 workspace = true
diff --git a/crates/toml/src/de/error.rs b/crates/toml/src/de/error.rs
index cba9a9d7..7cbf6459 100644
--- a/crates/toml/src/de/error.rs
+++ b/crates/toml/src/de/error.rs
@@ -80,6 +80,7 @@ impl Error {
     }
 }
 
+#[cfg(feature = "serde")]
 impl serde::de::Error for Error {
     fn custom<T>(msg: T) -> Self
     where
@@ -166,6 +167,7 @@ impl core::fmt::Display for Error {
 #[cfg(feature = "std")]
 impl std::error::Error for Error {}
 #[cfg(not(feature = "std"))]
+#[cfg(feature = "serde")]
 impl serde::de::StdError for Error {}
 
 fn translate_position(input: &[u8], index: usize) -> (usize, usize) {
diff --git a/crates/toml/src/de/mod.rs b/crates/toml/src/de/mod.rs
index a63dbe8e..8beed6a5 100644
--- a/crates/toml/src/de/mod.rs
+++ b/crates/toml/src/de/mod.rs
@@ -5,14 +5,17 @@
 //! provided at the top of the crate.
 
 #[cfg(feature = "parse")]
+#[cfg(feature = "serde")]
 mod deserializer;
 mod error;
 #[cfg(feature = "parse")]
 mod parser;
 
 #[cfg(feature = "parse")]
+#[cfg(feature = "serde")]
 pub use deserializer::Deserializer;
 #[cfg(feature = "parse")]
+#[cfg(feature = "serde")]
 pub use deserializer::ValueDeserializer;
 #[cfg(feature = "parse")]
 pub use parser::DeArray;
@@ -65,6 +68,7 @@ use crate::alloc_prelude::*;
 /// assert_eq!(config.owner.name, "Lisa");
 /// ```
 #[cfg(feature = "parse")]
+#[cfg(feature = "serde")]
 pub fn from_str<'de, T>(s: &'de str) -> Result<T, Error>
 where
     T: serde::de::Deserialize<'de>,
@@ -79,6 +83,7 @@ where
 ///
 /// To deserializes TOML values, instead of documents, see [`ValueDeserializer`].
 #[cfg(feature = "parse")]
+#[cfg(feature = "serde")]
 pub fn from_slice<'de, T>(s: &'de [u8]) -> Result<T, Error>
 where
     T: serde::de::Deserialize<'de>,
diff --git a/crates/toml/src/lib.rs b/crates/toml/src/lib.rs
index 04a9954d..f6512da5 100644
--- a/crates/toml/src/lib.rs
+++ b/crates/toml/src/lib.rs
@@ -17,7 +17,8 @@
 //!
 //! A TOML document is represented with the [`Table`] type which maps `String` to the [`Value`] enum:
 //!
-//! ```rust
+#![cfg_attr(not(feature = "default"), doc = " ```ignore")]
+#![cfg_attr(feature = "default", doc = " ```")]
 //! # use toml::value::{Datetime, Array, Table};
 //! pub enum Value {
 //!     String(String),
@@ -34,8 +35,8 @@
 //!
 //! The easiest way to parse a TOML document is via the [`Table`] type:
 //!
-#![cfg_attr(not(feature = "parse"), doc = " ```ignore")]
-#![cfg_attr(feature = "parse", doc = " ```")]
+#![cfg_attr(not(feature = "default"), doc = " ```ignore")]
+#![cfg_attr(feature = "default", doc = " ```")]
 //! use toml::Table;
 //!
 //! let value = "foo = 'bar'".parse::<Table>().unwrap();
@@ -71,8 +72,8 @@
 //!
 //! An example of deserializing with TOML is:
 //!
-#![cfg_attr(not(feature = "parse"), doc = " ```ignore")]
-#![cfg_attr(feature = "parse", doc = " ```")]
+#![cfg_attr(not(feature = "default"), doc = " ```ignore")]
+#![cfg_attr(feature = "default", doc = " ```")]
 //! use serde::Deserialize;
 //!
 //! #[derive(Deserialize)]
@@ -104,8 +105,8 @@
 //!
 //! You can serialize types in a similar fashion:
 //!
-#![cfg_attr(not(feature = "display"), doc = " ```ignore")]
-#![cfg_attr(feature = "display", doc = " ```")]
+#![cfg_attr(not(feature = "default"), doc = " ```ignore")]
+#![cfg_attr(feature = "default", doc = " ```")]
 //! use serde::Serialize;
 //!
 //! #[derive(Serialize)]
@@ -163,26 +164,33 @@ pub(crate) mod alloc_prelude {
 }
 
 pub mod map;
+#[cfg(feature = "serde")]
 pub mod value;
 
 pub mod de;
+#[cfg(feature = "serde")]
 pub mod ser;
 
 #[doc(hidden)]
+#[cfg(feature = "serde")]
 pub mod macros;
 
+#[cfg(feature = "serde")]
 mod table;
 
-#[cfg(feature = "parse")]
 #[doc(inline)]
+#[cfg(feature = "parse")]
+#[cfg(feature = "serde")]
 pub use crate::de::{from_slice, from_str, Deserializer};
-#[cfg(feature = "display")]
 #[doc(inline)]
+#[cfg(feature = "display")]
+#[cfg(feature = "serde")]
 pub use crate::ser::{to_string, to_string_pretty, Serializer};
 #[doc(inline)]
+#[cfg(feature = "serde")]
 pub use crate::value::Value;
-
 pub use serde_spanned::Spanned;
+#[cfg(feature = "serde")]
 pub use table::Table;
 
 // Shortcuts for the module doc-comment
diff --git a/crates/toml_edit/Cargo.toml b/crates/toml_edit/Cargo.toml
index 592c45e0..102f8a85 100644
--- a/crates/toml_edit/Cargo.toml
+++ b/crates/toml_edit/Cargo.toml
@@ -61,7 +61,7 @@ snapbox = "0.6.0"
 proptest = "1.5.0"
 walkdir = "2.5.0"
 serde_spanned = { path = "../serde_spanned" }
-toml_types = { path = "../toml", package = "toml", default-features = false, features = ["preserve_order"] }
+toml_types = { path = "../toml", package = "toml", default-features = false, features = ["serde", "preserve_order"] }
 serde-untagged = "0.1.7"
 
 [[test]]
diff --git a/crates/toml_edit/src/inline_table.rs b/crates/toml_edit/src/inline_table.rs
index 866acd8f..e999fcd7 100644
--- a/crates/toml_edit/src/inline_table.rs
+++ b/crates/toml_edit/src/inline_table.rs
@@ -71,9 +71,6 @@ impl InlineTable {
                 Item::Value(value) => {
                     values.push((path, value));
                 }
-                Item::Table(table) => {
-                    table.append_all_values(&path, values);
-                }
                 _ => {}
             }
         }
diff --git a/crates/toml_edit/src/table.rs b/crates/toml_edit/src/table.rs
index 0ad02da3..43464d24 100644
--- a/crates/toml_edit/src/table.rs
+++ b/crates/toml_edit/src/table.rs
@@ -99,34 +99,6 @@ impl Table {
         }
     }
 
-    pub(crate) fn append_all_values<'s>(
-        &'s self,
-        parent: &[&'s Key],
-        values: &mut Vec<(Vec<&'s Key>, &'s Value)>,
-    ) {
-        for (key, value) in self.items.iter() {
-            let mut path = parent.to_vec();
-            path.push(key);
-            match value {
-                Item::Table(table) => {
-                    table.append_all_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);


Test patch:
diff --git a/crates/toml/tests/compliance/main.rs b/crates/toml/tests/compliance/main.rs
index 4019cb5e..70dfb1e4 100644
--- a/crates/toml/tests/compliance/main.rs
+++ b/crates/toml/tests/compliance/main.rs
@@ -1,6 +1,6 @@
 #![recursion_limit = "256"]
 #![allow(clippy::dbg_macro)]
-#![cfg(all(feature = "parse", feature = "display"))]
+#![cfg(all(feature = "parse", feature = "display", feature = "serde"))]
 
 macro_rules! parse_value {
     ($s:expr) => {{
diff --git a/crates/toml/tests/decoder.rs b/crates/toml/tests/decoder.rs
index 2f0a9439..458dd80f 100644
--- a/crates/toml/tests/decoder.rs
+++ b/crates/toml/tests/decoder.rs
@@ -1,4 +1,4 @@
-#![cfg(all(feature = "parse", feature = "display"))]
+#![cfg(all(feature = "parse", feature = "display", feature = "serde"))]
 #![allow(dead_code)]
 
 #[derive(Copy, Clone)]
diff --git a/crates/toml/tests/decoder_compliance.rs b/crates/toml/tests/decoder_compliance.rs
index 7ba6b97b..abc8afa4 100644
--- a/crates/toml/tests/decoder_compliance.rs
+++ b/crates/toml/tests/decoder_compliance.rs
@@ -1,6 +1,6 @@
 mod decoder;
 
-#[cfg(all(feature = "parse", feature = "display"))]
+#[cfg(all(feature = "parse", feature = "display", feature = "serde"))]
 fn main() {
     let invalid_ext = walkdir::WalkDir::new("tests/fixtures/invalid")
         .sort_by_file_name()
@@ -28,5 +28,5 @@ fn main() {
     harness.test();
 }
 
-#[cfg(not(all(feature = "parse", feature = "display")))]
+#[cfg(not(all(feature = "parse", feature = "display", feature = "serde")))]
 fn main() {}
diff --git a/crates/toml/tests/encoder.rs b/crates/toml/tests/encoder.rs
index 9f211f27..68dac243 100644
--- a/crates/toml/tests/encoder.rs
+++ b/crates/toml/tests/encoder.rs
@@ -1,4 +1,4 @@
-#![cfg(all(feature = "parse", feature = "display"))]
+#![cfg(all(feature = "parse", feature = "display", feature = "serde"))]
 #![allow(dead_code)]
 
 #[derive(Copy, Clone)]
diff --git a/crates/toml/tests/encoder_compliance.rs b/crates/toml/tests/encoder_compliance.rs
index c593e596..fdcfc18e 100644
--- a/crates/toml/tests/encoder_compliance.rs
+++ b/crates/toml/tests/encoder_compliance.rs
@@ -1,7 +1,7 @@
 mod decoder;
 mod encoder;
 
-#[cfg(all(feature = "parse", feature = "display"))]
+#[cfg(all(feature = "parse", feature = "display", feature = "serde"))]
 fn main() {
     let encoder = encoder::Encoder;
     let decoder = decoder::Decoder;
@@ -10,5 +10,5 @@ fn main() {
     harness.test();
 }
 
-#[cfg(not(all(feature = "parse", feature = "display")))]
+#[cfg(not(all(feature = "parse", feature = "display", feature = "serde")))]
 fn main() {}
diff --git a/crates/toml/tests/encoder_pretty_compliance.rs b/crates/toml/tests/encoder_pretty_compliance.rs
index c593e596..fdcfc18e 100644
--- a/crates/toml/tests/encoder_pretty_compliance.rs
+++ b/crates/toml/tests/encoder_pretty_compliance.rs
@@ -1,7 +1,7 @@
 mod decoder;
 mod encoder;
 
-#[cfg(all(feature = "parse", feature = "display"))]
+#[cfg(all(feature = "parse", feature = "display", feature = "serde"))]
 fn main() {
     let encoder = encoder::Encoder;
     let decoder = decoder::Decoder;
@@ -10,5 +10,5 @@ fn main() {
     harness.test();
 }
 
-#[cfg(not(all(feature = "parse", feature = "display")))]
+#[cfg(not(all(feature = "parse", feature = "display", feature = "serde")))]
 fn main() {}
diff --git a/crates/toml/tests/serde/main.rs b/crates/toml/tests/serde/main.rs
index 0a499cd6..9936e097 100644
--- a/crates/toml/tests/serde/main.rs
+++ b/crates/toml/tests/serde/main.rs
@@ -1,5 +1,5 @@
 #![recursion_limit = "256"]
-#![cfg(all(feature = "parse", feature = "display"))]
+#![cfg(all(feature = "parse", feature = "display", feature = "serde"))]
 
 macro_rules! t {
     ($e:expr) => {
diff --git a/crates/toml/tests/testsuite/main.rs b/crates/toml/tests/testsuite/main.rs
index c90afae2..e6d3379d 100644
--- a/crates/toml/tests/testsuite/main.rs
+++ b/crates/toml/tests/testsuite/main.rs
@@ -1,5 +1,5 @@
 #![recursion_limit = "256"]
-#![cfg(all(feature = "parse", feature = "display"))]
+#![cfg(all(feature = "parse", feature = "display", feature = "serde"))]
 
 macro_rules! map( ($($k:expr => $v:expr),*) => ({
     let mut _m = Map::new();
diff --git a/crates/toml_edit/tests/testsuite/edit.rs b/crates/toml_edit/tests/testsuite/edit.rs
index 869a1230..c970c577 100644
--- a/crates/toml_edit/tests/testsuite/edit.rs
+++ b/crates/toml_edit/tests/testsuite/edit.rs
@@ -1465,18 +1465,3 @@ fn assert_key_value_roundtrip(input: &str, expected: impl IntoData) {
     });
     assert_data_eq!(actual, expected.raw());
 }
-
-#[test]
-fn table_under_inline() {
-    let mut doc = DocumentMut::new();
-    doc["tool"]["typst-test"] = table();
-    doc["tool"]["typst-test"]["tests"] = value("tests");
-
-    assert_data_eq!(
-        doc.to_string(),
-        str![[r#"
-tool = { typst-test.tests = "tests" }
-
-"#]]
-    );
-}