Repository: alexcrichton/toml-rs (rust)

Issue:
allow configuration when encoding to strings
The use case I am trying to meet is the ability to configure `toml` to use tripple quotes (`'''`) when useful (such as when there are any newlines.

Here is some example code:
```
extern crate rustc_serialize;
extern crate toml;

use toml::{encode_str};

#[derive(RustcEncodable)]
struct MyStruct { foo: isize, bar: String }

const LONG_TEXT: &'static str = r#"
this is some text
here is some more
yay
"#;

fn main() {
    let my_struct = MyStruct { foo: 4, bar: LONG_TEXT.to_string() };
    println!("{}", encode_str(&my_struct));
}
```

This currently prints:
```
bar = "\nthis is some text\nhere is some more\nyay\n"
foo = 4
```

I would like for it to be able to print:
```
bar = '''
this is some text
here is some more
yay
'''
foo = 4
```

Pull request description:
close #119: allow "pretty" configuration
This is an MVP of the "pretty" configuration impelmentation. There are some things I would still like to do (like avoid having to clone `Settings`) and there are still a bunch of tests that need to be added but this seems to work 🎉 🎉 

This is necessary for:
- vitiral/artifact#87
- vitiral/artifact#152

I would particularily like feedback from @alexcrichton on:
- Is it okay to have a `Settings` struct instead of a `Formatter` trait, as suggested by @dtolnay. @dtolnay what do you think about this? I see the trait as probably "higher speed" but I don't think that's as much a requirement for toml as it is for json.
- What should the name be? Is `toml::ser::Settings` good?

Improvements needed:
- The `Serializer::pretty` is [mimicking serde_json's][1] implementation, but we should make `pretty` accept a `Settings` struct
- We should add a `to_string_pretty(dst: &mut W, settings: Settings)` method

[1]: https://docs.serde.rs/serde_json/struct.Serializer.html#method.pretty

Patch:
diff --git a/src/lib.rs b/src/lib.rs
index ee4e961..c4a7e9d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -161,7 +161,7 @@ mod datetime;
 
 pub mod ser;
 #[doc(no_inline)]
-pub use ser::{to_string, to_vec, Serializer};
+pub use ser::{to_string, to_string_pretty, to_vec, Serializer};
 pub mod de;
 #[doc(no_inline)]
 pub use de::{from_slice, from_str, Deserializer};
diff --git a/src/ser.rs b/src/ser.rs
index 77db867..863bd75 100644
--- a/src/ser.rs
+++ b/src/ser.rs
@@ -93,6 +93,18 @@ pub fn to_string<T: ?Sized>(value: &T) -> Result<String, Error>
     Ok(dst)
 }
 
+/// 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 `Serializer::pretty` for more details.
+pub fn to_string_pretty<T: ?Sized>(value: &T) -> Result<String, Error>
+    where T: ser::Serialize,
+{
+    let mut dst = String::with_capacity(128);
+    value.serialize(&mut Serializer::pretty(&mut dst))?;
+    Ok(dst)
+}
+
 /// Errors that can occur when serializing a type.
 #[derive(Debug, PartialEq, Eq, Clone)]
 pub enum Error {
@@ -137,6 +149,31 @@ pub enum Error {
     __Nonexhaustive,
 }
 
+#[derive(Debug, Default, Clone)]
+#[doc(hidden)]
+/// Internal place for holding array setings
+struct ArraySettings {
+    indent: usize,
+    trailing_comma: bool,
+}
+
+impl ArraySettings {
+    fn pretty() -> ArraySettings {
+        ArraySettings {
+            indent: 4,
+            trailing_comma: true,
+        }
+    }
+}
+
+#[derive(Debug, Default, Clone)]
+#[doc(hidden)]
+/// Internal struct for holding serialization settings
+struct Settings {
+    array: Option<ArraySettings>,
+    pretty_string: bool,
+}
+
 /// Serialization implementation for TOML.
 ///
 /// This structure implements serialization support for TOML to serialize an
@@ -149,6 +186,7 @@ pub enum Error {
 pub struct Serializer<'a> {
     dst: &'a mut String,
     state: State<'a>,
+    settings: Settings,
 }
 
 #[derive(Debug, Clone)]
@@ -194,9 +232,130 @@ impl<'a> Serializer<'a> {
         Serializer {
             dst: dst,
             state: State::End,
+            settings: Settings::default(),
+        }
+    }
+
+    /// Instantiate a "pretty" formatter
+    ///
+    /// By default this will use:
+    ///
+    /// - pretty strings: strings with newlines will use the `'''` syntax. See
+    ///   `Serializer::pretty_string`
+    /// - pretty arrays: each item in arrays will be on a newline, have an indentation of 4 and
+    ///   have a trailing comma. See `Serializer::pretty_array`
+    pub fn pretty(dst: &'a mut String) -> Serializer<'a> {
+        Serializer {
+            dst: dst,
+            state: State::End,
+            settings: Settings {
+                array: Some(ArraySettings::pretty()),
+                pretty_string: true,
+            },
         }
     }
 
+    /// Enable or Disable pretty strings
+    ///
+    /// If enabled, strings with one or more newline character will use the `'''` syntax.
+    ///
+    /// # Examples
+    ///
+    /// Instead of:
+    ///
+    /// ```ignore
+    /// single = "no newlines"
+    /// text = "\nfoo\nbar\n"
+    /// ```
+    ///
+    /// You will have:
+    ///
+    /// ```ignore
+    /// single = "no newlines"
+    /// text = '''
+    /// foo
+    /// bar
+    /// '''
+    /// ```
+    pub fn pretty_string(&mut self, value: bool) -> &mut Self {
+        self.settings.pretty_string = value;
+        self
+    }
+
+    /// Enable or Disable pretty arrays
+    ///
+    /// If enabled, arrays will always have each item on their own line.
+    ///
+    /// Some specific features can be controlled via other builder methods:
+    ///
+    /// - `Serializer::pretty_array_indent`: set the indent to a value other
+    ///   than 4.
+    /// - `Serializer::pretty_array_trailing_comma`: enable/disable the trailing
+    ///   comma on the last item.
+    ///
+    /// # Examples
+    ///
+    /// Instead of:
+    ///
+    /// ```ignore
+    /// array = ["foo", "bar"]
+    /// ```
+    ///
+    /// You will have:
+    ///
+    /// ```ignore
+    /// array = [
+    ///     "foo",
+    ///     "bar",
+    /// ]
+    /// ```
+    pub fn pretty_array(&mut self, value: bool) -> &mut Self {
+        self.settings.array = if value {
+            Some(ArraySettings::pretty())
+        } else {
+            None
+        };
+        self
+    }
+
+    /// Set the indent for pretty arrays
+    ///
+    /// See `Serializer::pretty_array` for more details.
+    pub fn pretty_array_indent(&mut self, value: usize) -> &mut Self {
+        let use_default = if let &mut Some(ref mut a) = &mut self.settings.array {
+            a.indent = value;
+            false
+        } else {
+            true
+        };
+
+        if use_default {
+            let mut array = ArraySettings::pretty();
+            array.indent = value;
+            self.settings.array = Some(array);
+        }
+        self
+    }
+
+    /// Specify whether to use a trailing comma when serializing pretty arrays
+    ///
+    /// See `Serializer::pretty_array` for more details.
+    pub fn pretty_array_trailing_comma(&mut self, value: bool) -> &mut Self {
+        let use_default = if let &mut Some(ref mut a) = &mut self.settings.array {
+            a.trailing_comma = value;
+            false
+        } else {
+            true
+        };
+
+        if use_default {
+            let mut array = ArraySettings::pretty();
+            array.trailing_comma = value;
+            self.settings.array = Some(array);
+        }
+        self
+    }
+
     fn display<T: fmt::Display>(&mut self,
                                 t: T,
                                 type_: &'static str) -> Result<(), Error> {
@@ -241,10 +400,24 @@ impl<'a> Serializer<'a> {
     }
 
     fn emit_array(&mut self, first: &Cell<bool>) -> Result<(), Error> {
-        if first.get() {
-            self.dst.push_str("[");
-        } else {
-            self.dst.push_str(", ");
+        match self.settings.array {
+            Some(ref a) => {
+                if first.get() {
+                    self.dst.push_str("[\n")
+                } else {
+                    self.dst.push_str(",\n")
+                }
+                for _ in 0..a.indent {
+                    self.dst.push_str(" ");
+                }
+            },
+            None => {
+                if first.get() {
+                    self.dst.push_str("[")
+                } else {
+                    self.dst.push_str(", ")
+                }
+            },
         }
         Ok(())
     }
@@ -283,15 +456,36 @@ impl<'a> Serializer<'a> {
     }
 
     fn emit_str(&mut self, value: &str) -> Result<(), Error> {
-        drop(write!(self.dst, "\""));
+        let do_pretty = if self.settings.pretty_string {
+            value.contains('\n')
+        } else {
+            false
+        };
+        if do_pretty {
+            drop(write!(self.dst, "'''\n"));
+        } else {
+            drop(write!(self.dst, "\""));
+        }
         for ch in value.chars() {
             match ch {
                 '\u{8}' => drop(write!(self.dst, "\\b")),
                 '\u{9}' => drop(write!(self.dst, "\\t")),
-                '\u{a}' => drop(write!(self.dst, "\\n")),
+                '\u{a}' => {
+                    if do_pretty {
+                        drop(write!(self.dst, "\n"));
+                    } else {
+                        drop(write!(self.dst, "\\n"));
+                    }
+                },
                 '\u{c}' => drop(write!(self.dst, "\\f")),
                 '\u{d}' => drop(write!(self.dst, "\\r")),
-                '\u{22}' => drop(write!(self.dst, "\\\"")),
+                '\u{22}' => {
+                    if do_pretty {
+                        drop(write!(self.dst, "\""))
+                    } else {
+                        drop(write!(self.dst, "\\\""))
+                    }
+                },
                 '\u{5c}' => drop(write!(self.dst, "\\\\")),
                 c if c < '\u{1f}' => {
                     drop(write!(self.dst, "\\u{:04X}", ch as u32))
@@ -299,7 +493,11 @@ impl<'a> Serializer<'a> {
                 ch => drop(write!(self.dst, "{}", ch)),
             }
         }
-        drop(write!(self.dst, "\""));
+        if do_pretty {
+            drop(write!(self.dst, "'''"));
+        } else {
+            drop(write!(self.dst, "\""));
+        }
         Ok(())
     }
 
@@ -591,6 +789,7 @@ impl<'a, 'b> ser::SerializeSeq for SerializeSeq<'a, 'b> {
                 first: &self.first,
                 type_: &self.type_,
             },
+            settings: self.ser.settings.clone(),
         })?;
         self.first.set(false);
         Ok(())
@@ -599,7 +798,19 @@ impl<'a, 'b> ser::SerializeSeq for SerializeSeq<'a, 'b> {
     fn end(self) -> Result<(), Error> {
         match self.type_.get() {
             Some("table") => return Ok(()),
-            Some(_) => self.ser.dst.push_str("]"),
+            Some(_) => {
+                match self.ser.settings.array {
+                    Some(ref a) => {
+                        if a.trailing_comma {
+                            self.ser.dst.push_str(",");
+                        }
+                        self.ser.dst.push_str("\n]");
+                    },
+                    None => {
+                        self.ser.dst.push_str("]");
+                    }
+                }
+            }
             None => {
                 assert!(self.first.get());
                 self.ser.emit_key("array")?;
@@ -650,6 +861,7 @@ impl<'a, 'b> ser::SerializeMap for SerializeTable<'a, 'b> {
                         first: first,
                         table_emitted: table_emitted,
                     },
+                    settings: ser.settings.clone(),
                 });
                 match res {
                     Ok(()) => first.set(false),
@@ -705,6 +917,7 @@ impl<'a, 'b> ser::SerializeStruct for SerializeTable<'a, 'b> {
                         first: first,
                         table_emitted: table_emitted,
                     },
+                    settings: ser.settings.clone(),
                 });
                 match res {
                     Ok(()) => first.set(false),


Test patch:
diff --git a/tests/pretty.rs b/tests/pretty.rs
new file mode 100644
index 0000000..ae9a839
--- /dev/null
+++ b/tests/pretty.rs
@@ -0,0 +1,168 @@
+extern crate toml;
+extern crate serde;
+
+use serde::ser::Serialize;
+
+const NO_PRETTY: &'static str = "\
+[example]
+array = [\"item 1\", \"item 2\"]
+empty = []
+oneline = \"this has no newlines.\"
+text = \"\\nthis is the first line\\nthis is the second line\\n\"
+";
+
+#[test]
+fn no_pretty() {
+    let toml = NO_PRETTY;
+    let value: toml::Value = toml::from_str(toml).unwrap();
+    let mut result = String::with_capacity(128);
+    value.serialize(&mut toml::Serializer::new(&mut result)).unwrap();
+    println!("EXPECTED:\n{}", toml);
+    println!("\nRESULT:\n{}", result);
+    assert_eq!(toml, &result);
+}
+
+#[test]
+fn disable_pretty() {
+    let toml = NO_PRETTY;
+    let value: toml::Value = toml::from_str(toml).unwrap();
+    let mut result = String::with_capacity(128);
+    {
+        let mut serializer = toml::Serializer::pretty(&mut result);
+        serializer.pretty_string(false);
+        serializer.pretty_array(false);
+        value.serialize(&mut serializer).unwrap();
+    }
+    println!("EXPECTED:\n{}", toml);
+    println!("\nRESULT:\n{}", result);
+    assert_eq!(toml, &result);
+}
+
+const PRETTY_STD: &'static str = "\
+[example]
+array = [
+    \"item 1\",
+    \"item 2\",
+]
+empty = []
+oneline = \"this has no newlines.\"
+text = '''
+this is the first line
+this is the second line
+'''
+";
+
+#[test]
+fn pretty_std() {
+    let toml = PRETTY_STD;
+    let value: toml::Value = toml::from_str(toml).unwrap();
+    let mut result = String::with_capacity(128);
+    value.serialize(&mut toml::Serializer::pretty(&mut result)).unwrap();
+    println!("EXPECTED:\n{}", toml);
+    println!("\nRESULT:\n{}", result);
+    assert_eq!(toml, &result);
+}
+
+
+const PRETTY_INDENT_2: &'static str = "\
+[example]
+array = [
+  \"item 1\",
+  \"item 2\",
+]
+empty = []
+oneline = \"this has no newlines.\"
+text = '''
+this is the first line
+this is the second line
+'''
+";
+
+#[test]
+fn pretty_indent_2() {
+    let toml = PRETTY_INDENT_2;
+    let value: toml::Value = toml::from_str(toml).unwrap();
+    let mut result = String::with_capacity(128);
+    {
+        let mut serializer = toml::Serializer::pretty(&mut result);
+        serializer.pretty_array_indent(2);
+        value.serialize(&mut serializer).unwrap();
+    }
+    assert_eq!(toml, &result);
+}
+
+const PRETTY_INDENT_2_OTHER: &'static str = "\
+[example]
+array = [
+  \"item 1\",
+  \"item 2\",
+]
+empty = []
+oneline = \"this has no newlines.\"
+text = \"\\nthis is the first line\\nthis is the second line\\n\"
+";
+
+
+#[test]
+/// Test pretty indent when gotten the other way
+fn pretty_indent_2_other() {
+    let toml = PRETTY_INDENT_2_OTHER;
+    let value: toml::Value = toml::from_str(toml).unwrap();
+    let mut result = String::with_capacity(128);
+    {
+        let mut serializer = toml::Serializer::new(&mut result);
+        serializer.pretty_array_indent(2);
+        value.serialize(&mut serializer).unwrap();
+    }
+    assert_eq!(toml, &result);
+}
+
+
+const PRETTY_ARRAY_NO_COMMA: &'static str = "\
+[example]
+array = [
+    \"item 1\",
+    \"item 2\"
+]
+empty = []
+oneline = \"this has no newlines.\"
+text = \"\\nthis is the first line\\nthis is the second line\\n\"
+";
+#[test]
+/// Test pretty indent when gotten the other way
+fn pretty_indent_array_no_comma() {
+    let toml = PRETTY_ARRAY_NO_COMMA;
+    let value: toml::Value = toml::from_str(toml).unwrap();
+    let mut result = String::with_capacity(128);
+    {
+        let mut serializer = toml::Serializer::new(&mut result);
+        serializer.pretty_array_trailing_comma(false);
+        value.serialize(&mut serializer).unwrap();
+    }
+    assert_eq!(toml, &result);
+}
+
+
+const PRETTY_NO_STRING: &'static str = "\
+[example]
+array = [
+    \"item 1\",
+    \"item 2\",
+]
+empty = []
+oneline = \"this has no newlines.\"
+text = \"\\nthis is the first line\\nthis is the second line\\n\"
+";
+#[test]
+/// Test pretty indent when gotten the other way
+fn pretty_no_string() {
+    let toml = PRETTY_NO_STRING;
+    let value: toml::Value = toml::from_str(toml).unwrap();
+    let mut result = String::with_capacity(128);
+    {
+        let mut serializer = toml::Serializer::pretty(&mut result);
+        serializer.pretty_string(false);
+        value.serialize(&mut serializer).unwrap();
+    }
+    assert_eq!(toml, &result);
+}