Repository: not-fl3/nanoserde
Pull request #130 (2025-03-24 19:03:01)

Remove whitespace from serialized RON
This PR removes all whitespace from serialised RON strings.

Given the below program:
```rs
use std::collections::{BTreeMap, BTreeSet};

use nanoserde::{SerJson, SerRon};

fn main() {
    let set: BTreeSet<i32> = (0..5).collect();
    let map: BTreeMap<i32, ()> = (0..5).map(|x| (x, ())).collect();

    println!("Serialize set to JSON: {}", set.serialize_json());
    println!("Serialize set to RON: {}", set.serialize_ron());
    println!("Serialize map to JSON: {}", map.serialize_json());
    println!("Serialize map to RON: {}", map.serialize_ron());
}

```
Previously, the output was:
```
Serialize set to JSON: [0,1,2,3,4]
Serialize set to RON: [    0,    1,    2,    3,    4]
Serialize map to JSON: {0:null,1:null,2:null,3:null,4:null}
Serialize map to RON: {
    0:(),
    1:(),
    2:(),
    3:(),
    4:(),
}
```
The serialised RON set is clearly incorrect, as it attempts to indent without creating newlines.
The serialised RON map isn't strictly incorrect, but is inconsistent with the JSON implementation.

After this PR, the output is:
```
Serialize set to JSON: [0,1,2,3,4]
Serialize set to RON: [0,1,2,3,4]
Serialize map to JSON: {0:null,1:null,2:null,3:null,4:null}
Serialize map to RON: {0:(),1:(),2:(),3:(),4:(),}
```

I've simply commented out the code in `SerRonState::indent`, to align with what has already previously happened with `SerJsonState::indent`, and to keep the changes here as minimal as possible. I've also added a new test case to ensure no serialised RON values contain any whitespace.

In theory, all of the following could potentially be considered further improvements, but would come at the cost of changing the crate's public API in some capacity:
* Removing `SerJsonState::indent` `SerRonState::indent` entirely
* Removing `SerJsonState::conl` and `SerRonState::conl` (and inlining their body), or renaming them now they no longer add newlines
* Removing the `d` parameter and `indent_level` parameter from `SerJson::ser_json` and `SerRon::ser_ron`
* Removing the `d` parameter from `SerJsonState::field`, `SerJsonState::st_post`, `SerRonState::field`, and `SerRonState::st_post`

If any of the above are desirable I'm happy to amend!

jack-cooper: Just FYI, I've force pushed a new commit as I saw a couple of test failures without the `std` feature enabled. That should all be resolved now!
knickish: Thanks, looks good! I'm curious if there's some way we could add a pretty printing serializer also without breaking backwards compatibility

Patch:
diff --git a/derive/src/serde_ron.rs b/derive/src/serde_ron.rs
index 44f5118..7f8e863 100644
--- a/derive/src/serde_ron.rs
+++ b/derive/src/serde_ron.rs
@@ -391,7 +391,7 @@ pub fn derive_ser_ron_enum(enum_: &Enum, crate_name: &str) -> TokenStream {
                     let name = format!("f{}", index);
                     l!(inner, "{}.ser_ron(d, s);", name);
                     if index != last {
-                        l!(inner, "s.out.push_str(\", \");")
+                        l!(inner, "s.out.push_str(\",\");")
                     }
                     names.push(name);
                 }
diff --git a/src/serde_ron.rs b/src/serde_ron.rs
index f6cc080..994313e 100644
--- a/src/serde_ron.rs
+++ b/src/serde_ron.rs
@@ -13,10 +13,10 @@ pub struct SerRonState {
 }
 
 impl SerRonState {
-    pub fn indent(&mut self, d: usize) {
-        for _ in 0..d {
-            self.out.push_str("    ");
-        }
+    pub fn indent(&mut self, _d: usize) {
+        // for _ in 0..d {
+        //     self.out.push_str("    ");
+        // }
     }
 
     pub fn field(&mut self, d: usize, field: &str) {
@@ -26,11 +26,11 @@ impl SerRonState {
     }
 
     pub fn conl(&mut self) {
-        self.out.push_str(",\n")
+        self.out.push(',')
     }
 
     pub fn st_pre(&mut self) {
-        self.out.push_str("(\n");
+        self.out.push('(');
     }
 
     pub fn st_post(&mut self, d: usize) {
@@ -870,7 +870,7 @@ where
     T: SerRon,
 {
     fn ser_ron(&self, d: usize, s: &mut SerRonState) {
-        s.out.push_str("[\n");
+        s.out.push('[');
         for item in self {
             s.indent(d + 1);
             item.ser_ron(d + 1, s);
@@ -1021,7 +1021,7 @@ where
         for (index, item) in self.iter().enumerate() {
             item.ser_ron(d + 1, s);
             if index != last {
-                s.out.push_str(", ");
+                s.out.push(',');
             }
         }
         s.out.push(')');
@@ -1112,7 +1112,7 @@ where
     fn ser_ron(&self, d: usize, s: &mut SerRonState) {
         s.out.push('(');
         self.0.ser_ron(d, s);
-        s.out.push_str(", ");
+        s.out.push(',');
         self.1.ser_ron(d, s);
         s.out.push(')');
     }
@@ -1140,9 +1140,9 @@ where
     fn ser_ron(&self, d: usize, s: &mut SerRonState) {
         s.out.push('(');
         self.0.ser_ron(d, s);
-        s.out.push_str(", ");
+        s.out.push(',');
         self.1.ser_ron(d, s);
-        s.out.push_str(", ");
+        s.out.push(',');
         self.2.ser_ron(d, s);
         s.out.push(')');
     }
@@ -1176,11 +1176,11 @@ where
     fn ser_ron(&self, d: usize, s: &mut SerRonState) {
         s.out.push('(');
         self.0.ser_ron(d, s);
-        s.out.push_str(", ");
+        s.out.push(',');
         self.1.ser_ron(d, s);
-        s.out.push_str(", ");
+        s.out.push(',');
         self.2.ser_ron(d, s);
-        s.out.push_str(", ");
+        s.out.push(',');
         self.3.ser_ron(d, s);
         s.out.push(')');
     }
@@ -1213,7 +1213,7 @@ where
     V: SerRon,
 {
     fn ser_ron(&self, d: usize, s: &mut SerRonState) {
-        s.out.push_str("{\n");
+        s.out.push('{');
         for (k, v) in self {
             s.indent(d + 1);
             k.ser_ron(d + 1, s);
@@ -1253,7 +1253,7 @@ where
     V: SerRon,
 {
     fn ser_ron(&self, d: usize, s: &mut SerRonState) {
-        s.out.push_str("{\n");
+        s.out.push('{');
         for (k, v) in self {
             s.indent(d + 1);
             k.ser_ron(d + 1, s);


Test patch:
diff --git a/tests/ron.rs b/tests/ron.rs
index cfb95fa..395ba27 100644
--- a/tests/ron.rs
+++ b/tests/ron.rs
@@ -7,7 +7,7 @@ use std::{
 };
 
 #[cfg(feature = "std")]
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 
 #[test]
 fn ron_de() {
@@ -450,7 +450,7 @@ fn de_ser_enum() {
         foo3: Fud,
     }
 
-    let ron = "(\n    foo1:A,\n    foo2:B,\n    foo3:C,\n)";
+    let ron = "(foo1:A,foo2:B,foo3:C,)";
 
     let data = Bar {
         foo1: Fud::A,
@@ -481,7 +481,7 @@ fn ser_enum_complex() {
         foo3: Foo,
     }
 
-    let ron = "(\n    foo1:A,\n    foo2:B(1, \"asd\"),\n    foo3:C(\n        a:2,\n        b:\"qwe\",\n    ),\n)";
+    let ron = "(foo1:A,foo2:B(1,\"asd\"),foo3:C(a:2,b:\"qwe\",),)";
 
     let data = Bar {
         foo1: Foo::A,
@@ -662,3 +662,50 @@ fn ron_crate() {
     assert_eq!(test.c, None);
     assert_eq!(test.d.unwrap(), "hello");
 }
+
+#[test]
+fn no_whitespace_when_serialized() {
+    // A vec of every type which implements `SerRon`. Actual values were picked arbitrarily.
+    let mut rons: Vec<Box<dyn SerRon>> = vec![
+        Box::new(()),
+        Box::new((0, 1.0)),
+        Box::new((0, 1.0, [2])),
+        Box::new((0, 1.0, [2], false)),
+        Box::new((0..5).collect::<BTreeSet<i32>>()),
+        Box::new((0..5).map(|x| (x, true)).collect::<BTreeMap<i32, bool>>()),
+        Box::new(Box::new(12_usize)),
+        Box::new((0..5).collect::<LinkedList<i32>>()),
+        Box::new(Some(false)),
+        Box::new(None::<bool>),
+        Box::new(String::from("a_string!")),
+        Box::new(vec![false, true, false]),
+        Box::new([true, false, true]),
+        Box::new(true),
+        Box::new(-32.0_f32),
+        Box::new(64.0_f64),
+        Box::new(-8_i8),
+        Box::new(-16_i16),
+        Box::new(-32_i32),
+        Box::new(-64_i64),
+        Box::new(8_u8),
+        Box::new(16_u16),
+        Box::new(32_u32),
+        Box::new(64_u64),
+        Box::new(usize::MAX),
+    ];
+
+    #[cfg(feature = "std")]
+    {
+        rons.push(Box::new((0..5).collect::<HashSet<i32>>()));
+        rons.push(Box::new(
+            (0..5).map(|x| (x, true)).collect::<HashMap<i32, bool>>(),
+        ));
+    }
+
+    for ron in rons {
+        let serialized = ron.serialize_ron();
+        let no_whitespace = serialized.chars().all(|char| !char.is_whitespace());
+
+        assert!(no_whitespace);
+    }
+}