Repository: toml-rs/toml (Rust)

Issue:
Decor in `DocumentMut` is ignored
## Example

```rs
use toml_edit::*;

let mut document = DocumentMut::new();
document.decor_mut().set_prefix("# HEADER\n");
document.decor_mut().set_prefix("# FOOTER\n");
println!("{document}");

document.as_table_mut().insert("key", value("value"));
println!("{document}");
```

## Expected

```
# HEADER
# FOOTER
# HEADER
key = "value"
# FOOTER
```

## Actual

```
key = "value"
```


Discussion:
What this is hiding is that this isn't `DocumentMut::decor_mut` but `Table::decor_mut` that is being accessed due to a convenience `Deref`.

I don't think I'm against this.  I'm also curious about collapsing `trailing` into the suffix.  I'm not sure why that wasn't done before.
We did run into the same issue today (what a coincidence).
For now we only wanted to add a document wide header and probably going to resort to prepending it to the first item in the document, though adding it to the document directly would definitely be more ergonomic.



Patch:
diff --git a/crates/toml_edit/src/encode.rs b/crates/toml_edit/src/encode.rs
index a8ccaa2a..b6142a6c 100644
--- a/crates/toml_edit/src/encode.rs
+++ b/crates/toml_edit/src/encode.rs
@@ -6,7 +6,9 @@ use toml_datetime::Datetime;
 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::table::{
+    DEFAULT_KEY_DECOR, DEFAULT_KEY_PATH_DECOR, DEFAULT_ROOT_DECOR, DEFAULT_TABLE_DECOR,
+};
 use crate::value::{
     DEFAULT_LEADING_VALUE_DECOR, DEFAULT_TRAILING_VALUE_DECOR, DEFAULT_VALUE_DECOR,
 };
@@ -197,6 +199,9 @@ pub(crate) fn encode_value(
 
 impl Display for DocumentMut {
     fn fmt(&self, f: &mut Formatter<'_>) -> Result {
+        let decor = self.decor();
+        decor.prefix_encode(f, None, DEFAULT_ROOT_DECOR.0)?;
+
         let mut path = Vec::new();
         let mut last_position = 0;
         let mut tables = Vec::new();
@@ -214,6 +219,7 @@ impl Display for DocumentMut {
         for (_, table, path, is_array) in tables {
             visit_table(f, None, table, &path, is_array, &mut first_table)?;
         }
+        decor.suffix_encode(f, None, DEFAULT_ROOT_DECOR.1)?;
         self.trailing().encode_with_default(f, None, "")
     }
 }
diff --git a/crates/toml_edit/src/table.rs b/crates/toml_edit/src/table.rs
index 2b52aec7..2863946f 100644
--- a/crates/toml_edit/src/table.rs
+++ b/crates/toml_edit/src/table.rs
@@ -504,6 +504,7 @@ fn decorate_table(table: &mut Table) {
 }
 
 // `key1 = value1`
+pub(crate) const DEFAULT_ROOT_DECOR: (&str, &str) = ("", "");
 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) = ("", "");


Test patch:
diff --git a/crates/toml_edit/tests/testsuite/edit.rs b/crates/toml_edit/tests/testsuite/edit.rs
index 3b6a21a2..01d9ef02 100644
--- a/crates/toml_edit/tests/testsuite/edit.rs
+++ b/crates/toml_edit/tests/testsuite/edit.rs
@@ -41,6 +41,15 @@ impl Test {
         }
         self
     }
+    fn running_on_doc<F>(&mut self, func: F) -> &mut Self
+    where
+        F: Fn(&mut DocumentMut),
+    {
+        {
+            func(&mut self.doc);
+        }
+        self
+    }
 
     #[track_caller]
     fn produces_display(&self, expected: snapbox::data::Inline) -> &Self {
@@ -49,6 +58,97 @@ impl Test {
     }
 }
 
+#[test]
+fn test_add_root_decor() {
+    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_on_doc(|document| {
+        document.decor_mut().set_prefix("# Some Header\n\n");
+        document.decor_mut().set_suffix("# Some Footer");
+        document.set_trailing("\n\ntrailing...");
+    })
+    .produces_display(str![[r#"
+# Some Header
+
+[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"
+# Some Footer
+
+trailing...
+"#]]);
+}
+
+/// Tests that default decor is None for both suffix and prefix and that this means empty strings
+#[test]
+fn test_no_root_decor() {
+    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_on_doc(|document| {
+        assert!(document.decor().prefix().is_none());
+        assert!(document.decor().suffix().is_none());
+        document.set_trailing("\n\ntrailing...");
+    })
+    .produces_display(str![[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"
+
+
+trailing...
+"#]]);
+}
+
 // insertion
 
 #[test]