Repository: ron-rs/ron
Pull request #313 (2021-09-09 15:36:06)

[0.6] Support raw identifiers
Required for https://github.com/gfx-rs/wgpu/issues/1931
```rust
enum Foo {
  #[serde(rename = "2d")]
  D2,
}
```


@torkleyy are you around to look at this?


Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 1cf42d1a..9521f82a 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -21,6 +21,18 @@ jobs:
           toolchain: ${{ matrix.rust }}
           profile: minimal
           override: true
+      - uses: actions-rs/cargo@v1
+        name: Downgrade bitflags to MSRV
+        if: ${{ matrix.rust }} == "1.36.0"
+        with:
+          command: update
+          args: -p bitflags --precise 1.2.1
+      - uses: actions-rs/cargo@v1
+        name: Downgrade indexmap to MSRV
+        if: ${{ matrix.rust }} == "1.36.0"
+        with:
+          command: update
+          args: -p indexmap --precise 1.6.2
       - run: cargo test
       - run: cargo test --all-features
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da8f997a..1582aad1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [0.6.5] - 2021-09-09
+- support serde renames that start with a digit
+
 ## [0.6.3] - 2020-12-18
 - bump `base64` dependency to 0.13
 
diff --git a/Cargo.toml b/Cargo.toml
index d2d6587d..12672edb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "ron"
 # Memo: update version in src/lib.rs too (doc link)
-version = "0.6.4"
+version = "0.6.5"
 license = "MIT/Apache-2.0"
 keywords = ["parser", "serde", "serialization"]
 authors = [
diff --git a/src/lib.rs b/src/lib.rs
index 602d0376..e6af663a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -57,7 +57,7 @@ Serializing / Deserializing is as simple as calling `to_string` / `from_str`.
 
 !*/
 
-#![doc(html_root_url = "https://docs.rs/ron/0.6.3")]
+#![doc(html_root_url = "https://docs.rs/ron/0.6.5")]
 
 pub mod de;
 pub mod ser;
diff --git a/src/parse.rs b/src/parse.rs
index d592e449..6905291c 100644
--- a/src/parse.rs
+++ b/src/parse.rs
@@ -14,15 +14,16 @@ const INT_CHAR: u8 = 1 << 0; // [0-9A-Fa-f_]
 const FLOAT_CHAR: u8 = 1 << 1; // [0-9\.Ee+-]
 const IDENT_FIRST_CHAR: u8 = 1 << 2; // [A-Za-z_]
 const IDENT_OTHER_CHAR: u8 = 1 << 3; // [A-Za-z_0-9]
-const WHITESPACE_CHAR: u8 = 1 << 4; // [\n\t\r ]
+const IDENT_RAW_CHAR: u8 = 1 << 4; // [A-Za-z_0-9\.+-]
+const WHITESPACE_CHAR: u8 = 1 << 5; // [\n\t\r ]
 
 // We encode each char as belonging to some number of these categories.
-const DIGIT: u8 = INT_CHAR | FLOAT_CHAR | IDENT_OTHER_CHAR; // [0-9]
-const ABCDF: u8 = INT_CHAR | IDENT_FIRST_CHAR | IDENT_OTHER_CHAR; // [ABCDFabcdf]
-const UNDER: u8 = INT_CHAR | IDENT_FIRST_CHAR | IDENT_OTHER_CHAR; // [_]
-const E____: u8 = INT_CHAR | FLOAT_CHAR | IDENT_FIRST_CHAR | IDENT_OTHER_CHAR; // [Ee]
-const G2Z__: u8 = IDENT_FIRST_CHAR | IDENT_OTHER_CHAR; // [G-Zg-z]
-const PUNCT: u8 = FLOAT_CHAR; // [\.+-]
+const DIGIT: u8 = INT_CHAR | FLOAT_CHAR | IDENT_OTHER_CHAR | IDENT_RAW_CHAR; // [0-9]
+const ABCDF: u8 = INT_CHAR | IDENT_FIRST_CHAR | IDENT_OTHER_CHAR | IDENT_RAW_CHAR; // [ABCDFabcdf]
+const UNDER: u8 = INT_CHAR | IDENT_FIRST_CHAR | IDENT_OTHER_CHAR | IDENT_RAW_CHAR; // [_]
+const E____: u8 = INT_CHAR | FLOAT_CHAR | IDENT_FIRST_CHAR | IDENT_OTHER_CHAR | IDENT_RAW_CHAR; // [Ee]
+const G2Z__: u8 = IDENT_FIRST_CHAR | IDENT_OTHER_CHAR | IDENT_RAW_CHAR; // [G-Zg-z]
+const PUNCT: u8 = FLOAT_CHAR | IDENT_RAW_CHAR; // [\.+-]
 const WS___: u8 = WHITESPACE_CHAR; // [\t\n\r ]
 const _____: u8 = 0; // everything else
 
@@ -67,14 +68,18 @@ const fn is_float_char(c: u8) -> bool {
     ENCODINGS[c as usize] & FLOAT_CHAR != 0
 }
 
-const fn is_ident_first_char(c: u8) -> bool {
+pub const fn is_ident_first_char(c: u8) -> bool {
     ENCODINGS[c as usize] & IDENT_FIRST_CHAR != 0
 }
 
-const fn is_ident_other_char(c: u8) -> bool {
+pub const fn is_ident_other_char(c: u8) -> bool {
     ENCODINGS[c as usize] & IDENT_OTHER_CHAR != 0
 }
 
+const fn is_ident_raw_char(c: u8) -> bool {
+    ENCODINGS[c as usize] & IDENT_RAW_CHAR != 0
+}
+
 const fn is_whitespace_char(c: u8) -> bool {
     ENCODINGS[c as usize] & WHITESPACE_CHAR != 0
 }
@@ -209,7 +214,7 @@ impl<'a> Bytes<'a> {
             }
 
             Ok(num_acc)
-        };
+        }
 
         let res = if sign > 0 {
             calc_num(&*self, s, base, T::checked_add_ext)
@@ -528,34 +533,41 @@ impl<'a> Bytes<'a> {
     }
 
     pub fn identifier(&mut self) -> Result<&'a [u8]> {
-        let bytes = self.identifier_len()?;
-        let ident = &self.bytes[..bytes];
-        let _ = self.advance(bytes);
-
-        Ok(ident)
-    }
-
-    pub fn identifier_len(&self) -> Result<usize> {
         let next = self.peek_or_eof()?;
-        if is_ident_first_char(next) {
-            // If the next two bytes signify the start of a raw string literal,
-            // return an error.
-            if next == b'r' {
-                let second = self
-                    .bytes
-                    .get(1)
-                    .ok_or_else(|| self.error(ErrorCode::Eof))?;
-                if *second == b'"' || *second == b'#' {
-                    return self.err(ErrorCode::ExpectedIdentifier);
+        if !is_ident_first_char(next) {
+            return self.err(ErrorCode::ExpectedIdentifier);
+        }
+
+        // If the next two bytes signify the start of a raw string literal,
+        // return an error.
+        let length = if next == b'r' {
+            match self
+                .bytes
+                .get(1)
+                .ok_or_else(|| self.error(ErrorCode::Eof))?
+            {
+                b'"' => return self.err(ErrorCode::ExpectedIdentifier),
+                b'#' => {
+                    let after_next = self.bytes.get(2).cloned().unwrap_or_default();
+                    //Note: it's important to check this before advancing forward, so that
+                    // the value-type deserializer can fall back to parsing it differently.
+                    if !is_ident_raw_char(after_next) {
+                        return self.err(ErrorCode::ExpectedIdentifier);
+                    }
+                    // skip "r#"
+                    let _ = self.advance(2);
+                    self.next_bytes_contained_in(is_ident_raw_char)
                 }
+                _ => self.next_bytes_contained_in(is_ident_other_char),
             }
+        } else {
+            self.next_bytes_contained_in(is_ident_other_char)
+        };
 
-            let bytes = self.next_bytes_contained_in(is_ident_other_char);
+        let ident = &self.bytes[..length];
+        let _ = self.advance(length);
 
-            Ok(bytes)
-        } else {
-            self.err(ErrorCode::ExpectedIdentifier)
-        }
+        Ok(ident)
     }
 
     pub fn next_bytes_contained_in(&self, allowed: fn(u8) -> bool) -> usize {
diff --git a/src/ser/mod.rs b/src/ser/mod.rs
index ca1cea3d..761de269 100644
--- a/src/ser/mod.rs
+++ b/src/ser/mod.rs
@@ -1,9 +1,14 @@
 use serde::{ser, Deserialize, Serialize};
 use std::io;
 
-use crate::error::{Error, Result};
-use crate::extensions::Extensions;
+use crate::{
+    error::{Error, Result},
+    extensions::Extensions,
+    parse::{is_ident_first_char, is_ident_other_char},
+};
 
+#[cfg(test)]
+mod tests;
 mod value;
 
 /// Serializes `value` into `writer`
@@ -335,6 +340,15 @@ impl<W: io::Write> Serializer<W> {
         self.output.write_all(b"\"")?;
         Ok(())
     }
+
+    fn write_identifier(&mut self, name: &str) -> io::Result<()> {
+        let mut bytes = name.as_bytes().iter().cloned();
+        if !bytes.next().map_or(false, is_ident_first_char) || !bytes.all(is_ident_other_char) {
+            self.output.write_all(b"r#")?;
+        }
+        self.output.write_all(name.as_bytes())?;
+        Ok(())
+    }
 }
 
 impl<'a, W: io::Write> ser::Serializer for &'a mut Serializer<W> {
@@ -466,7 +480,7 @@ impl<'a, W: io::Write> ser::Serializer for &'a mut Serializer<W> {
 
     fn serialize_unit_struct(self, name: &'static str) -> Result<()> {
         if self.struct_names {
-            self.output.write_all(name.as_bytes())?;
+            self.write_identifier(name)?;
 
             Ok(())
         } else {
@@ -475,7 +489,7 @@ impl<'a, W: io::Write> ser::Serializer for &'a mut Serializer<W> {
     }
 
     fn serialize_unit_variant(self, _: &'static str, _: u32, variant: &'static str) -> Result<()> {
-        self.output.write_all(variant.as_bytes())?;
+        self.write_identifier(variant)?;
 
         Ok(())
     }
@@ -485,7 +499,7 @@ impl<'a, W: io::Write> ser::Serializer for &'a mut Serializer<W> {
         T: ?Sized + Serialize,
     {
         if self.struct_names {
-            self.output.write_all(name.as_bytes())?;
+            self.write_identifier(name)?;
         }
 
         self.output.write_all(b"(")?;
@@ -504,7 +518,7 @@ impl<'a, W: io::Write> ser::Serializer for &'a mut Serializer<W> {
     where
         T: ?Sized + Serialize,
     {
-        self.output.write_all(variant.as_bytes())?;
+        self.write_identifier(variant)?;
         self.output.write_all(b"(")?;
 
         value.serialize(&mut *self)?;
@@ -553,7 +567,7 @@ impl<'a, W: io::Write> ser::Serializer for &'a mut Serializer<W> {
         len: usize,
     ) -> Result<Self::SerializeTupleStruct> {
         if self.struct_names {
-            self.output.write_all(name.as_bytes())?;
+            self.write_identifier(name)?;
         }
 
         self.serialize_tuple(len)
@@ -566,7 +580,7 @@ impl<'a, W: io::Write> ser::Serializer for &'a mut Serializer<W> {
         variant: &'static str,
         len: usize,
     ) -> Result<Self::SerializeTupleVariant> {
-        self.output.write_all(variant.as_bytes())?;
+        self.write_identifier(variant)?;
         self.output.write_all(b"(")?;
 
         if self.separate_tuple_members() {
@@ -598,7 +612,7 @@ impl<'a, W: io::Write> ser::Serializer for &'a mut Serializer<W> {
 
     fn serialize_struct(self, name: &'static str, len: usize) -> Result<Self::SerializeStruct> {
         if self.struct_names {
-            self.output.write_all(name.as_bytes())?;
+            self.write_identifier(name)?;
         }
         self.output.write_all(b"(")?;
 
@@ -618,7 +632,7 @@ impl<'a, W: io::Write> ser::Serializer for &'a mut Serializer<W> {
         variant: &'static str,
         len: usize,
     ) -> Result<Self::SerializeStructVariant> {
-        self.output.write_all(variant.as_bytes())?;
+        self.write_identifier(variant)?;
         self.output.write_all(b"(")?;
 
         self.is_empty = Some(len == 0);
@@ -963,127 +977,3 @@ impl<'a, W: io::Write> ser::SerializeStructVariant for Compound<'a, W> {
         ser::SerializeStruct::end(self)
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[derive(Serialize)]
-    struct EmptyStruct1;
-
-    #[derive(Serialize)]
-    struct EmptyStruct2 {}
-
-    #[derive(Serialize)]
-    struct MyStruct {
-        x: f32,
-        y: f32,
-    }
-
-    #[derive(Serialize)]
-    enum MyEnum {
-        A,
-        B(bool),
-        C(bool, f32),
-        D { a: i32, b: i32 },
-    }
-
-    #[test]
-    fn test_empty_struct() {
-        assert_eq!(to_string(&EmptyStruct1).unwrap(), "()");
-        assert_eq!(to_string(&EmptyStruct2 {}).unwrap(), "()");
-    }
-
-    #[test]
-    fn test_struct() {
-        let my_struct = MyStruct { x: 4.0, y: 7.0 };
-
-        assert_eq!(to_string(&my_struct).unwrap(), "(x:4,y:7)");
-
-        #[derive(Serialize)]
-        struct NewType(i32);
-
-        assert_eq!(to_string(&NewType(42)).unwrap(), "(42)");
-
-        #[derive(Serialize)]
-        struct TupleStruct(f32, f32);
-
-        assert_eq!(to_string(&TupleStruct(2.0, 5.0)).unwrap(), "(2,5)");
-    }
-
-    #[test]
-    fn test_option() {
-        assert_eq!(to_string(&Some(1u8)).unwrap(), "Some(1)");
-        assert_eq!(to_string(&None::<u8>).unwrap(), "None");
-    }
-
-    #[test]
-    fn test_enum() {
-        assert_eq!(to_string(&MyEnum::A).unwrap(), "A");
-        assert_eq!(to_string(&MyEnum::B(true)).unwrap(), "B(true)");
-        assert_eq!(to_string(&MyEnum::C(true, 3.5)).unwrap(), "C(true,3.5)");
-        assert_eq!(to_string(&MyEnum::D { a: 2, b: 3 }).unwrap(), "D(a:2,b:3)");
-    }
-
-    #[test]
-    fn test_array() {
-        let empty: [i32; 0] = [];
-        assert_eq!(to_string(&empty).unwrap(), "()");
-        let empty_ref: &[i32] = &empty;
-        assert_eq!(to_string(&empty_ref).unwrap(), "[]");
-
-        assert_eq!(to_string(&[2, 3, 4i32]).unwrap(), "(2,3,4)");
-        assert_eq!(to_string(&(&[2, 3, 4i32] as &[i32])).unwrap(), "[2,3,4]");
-    }
-
-    #[test]
-    fn test_map() {
-        use std::collections::HashMap;
-
-        let mut map = HashMap::new();
-        map.insert((true, false), 4);
-        map.insert((false, false), 123);
-
-        let s = to_string(&map).unwrap();
-        s.starts_with("{");
-        s.contains("(true,false):4");
-        s.contains("(false,false):123");
-        s.ends_with("}");
-    }
-
-    #[test]
-    fn test_string() {
-        assert_eq!(to_string(&"Some string").unwrap(), "\"Some string\"");
-    }
-
-    #[test]
-    fn test_char() {
-        assert_eq!(to_string(&'c').unwrap(), "'c'");
-    }
-
-    #[test]
-    fn test_escape() {
-        assert_eq!(to_string(&r#""Quoted""#).unwrap(), r#""\"Quoted\"""#);
-    }
-
-    #[test]
-    fn test_byte_stream() {
-        use serde_bytes;
-
-        let small: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
-        assert_eq!(
-            to_string(&small).unwrap(),
-            "(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15)"
-        );
-
-        let large = vec![255u8; 64];
-        let large = serde_bytes::Bytes::new(&large);
-        assert_eq!(
-            to_string(&large).unwrap(),
-            concat!(
-                "\"/////////////////////////////////////////",
-                "////////////////////////////////////////////w==\""
-            )
-        );
-    }
-}


Test patch:
diff --git a/src/de/tests.rs b/src/de/tests.rs
index f74b87b6..0311b6bf 100644
--- a/src/de/tests.rs
+++ b/src/de/tests.rs
@@ -202,6 +202,22 @@ fn untagged() {
     assert_eq!(from_str::<Untagged>("8").unwrap(), Untagged::U8(8));
 }
 
+#[test]
+fn rename() {
+    #[derive(Deserialize, Debug, PartialEq)]
+    enum Foo {
+        #[serde(rename = "2d")]
+        D2,
+        #[serde(rename = "triangle-list")]
+        TriangleList,
+    }
+    assert_eq!(from_str::<Foo>("r#2d").unwrap(), Foo::D2);
+    assert_eq!(
+        from_str::<Foo>("r#triangle-list").unwrap(),
+        Foo::TriangleList
+    );
+}
+
 #[test]
 fn forgot_apostrophes() {
     let de: Result<(i32, String)> = from_str("(4, \"Hello)");
diff --git a/src/ser/tests.rs b/src/ser/tests.rs
new file mode 100644
index 00000000..2b56b3d4
--- /dev/null
+++ b/src/ser/tests.rs
@@ -0,0 +1,135 @@
+use super::to_string;
+use serde::Serialize;
+
+#[derive(Serialize)]
+struct EmptyStruct1;
+
+#[derive(Serialize)]
+struct EmptyStruct2 {}
+
+#[derive(Serialize)]
+struct MyStruct {
+    x: f32,
+    y: f32,
+}
+
+#[derive(Serialize)]
+enum MyEnum {
+    A,
+    B(bool),
+    C(bool, f32),
+    D { a: i32, b: i32 },
+}
+
+#[test]
+fn test_empty_struct() {
+    assert_eq!(to_string(&EmptyStruct1).unwrap(), "()");
+    assert_eq!(to_string(&EmptyStruct2 {}).unwrap(), "()");
+}
+
+#[test]
+fn test_struct() {
+    let my_struct = MyStruct { x: 4.0, y: 7.0 };
+
+    assert_eq!(to_string(&my_struct).unwrap(), "(x:4,y:7)");
+
+    #[derive(Serialize)]
+    struct NewType(i32);
+
+    assert_eq!(to_string(&NewType(42)).unwrap(), "(42)");
+
+    #[derive(Serialize)]
+    struct TupleStruct(f32, f32);
+
+    assert_eq!(to_string(&TupleStruct(2.0, 5.0)).unwrap(), "(2,5)");
+}
+
+#[test]
+fn test_option() {
+    assert_eq!(to_string(&Some(1u8)).unwrap(), "Some(1)");
+    assert_eq!(to_string(&None::<u8>).unwrap(), "None");
+}
+
+#[test]
+fn test_enum() {
+    assert_eq!(to_string(&MyEnum::A).unwrap(), "A");
+    assert_eq!(to_string(&MyEnum::B(true)).unwrap(), "B(true)");
+    assert_eq!(to_string(&MyEnum::C(true, 3.5)).unwrap(), "C(true,3.5)");
+    assert_eq!(to_string(&MyEnum::D { a: 2, b: 3 }).unwrap(), "D(a:2,b:3)");
+}
+
+#[test]
+fn test_array() {
+    let empty: [i32; 0] = [];
+    assert_eq!(to_string(&empty).unwrap(), "()");
+    let empty_ref: &[i32] = &empty;
+    assert_eq!(to_string(&empty_ref).unwrap(), "[]");
+
+    assert_eq!(to_string(&[2, 3, 4i32]).unwrap(), "(2,3,4)");
+    assert_eq!(to_string(&(&[2, 3, 4i32] as &[i32])).unwrap(), "[2,3,4]");
+}
+
+#[test]
+fn test_map() {
+    use std::collections::HashMap;
+
+    let mut map = HashMap::new();
+    map.insert((true, false), 4);
+    map.insert((false, false), 123);
+
+    let s = to_string(&map).unwrap();
+    s.starts_with("{");
+    s.contains("(true,false):4");
+    s.contains("(false,false):123");
+    s.ends_with("}");
+}
+
+#[test]
+fn test_string() {
+    assert_eq!(to_string(&"Some string").unwrap(), "\"Some string\"");
+}
+
+#[test]
+fn test_char() {
+    assert_eq!(to_string(&'c').unwrap(), "'c'");
+}
+
+#[test]
+fn test_escape() {
+    assert_eq!(to_string(&r#""Quoted""#).unwrap(), r#""\"Quoted\"""#);
+}
+
+#[test]
+fn test_byte_stream() {
+    use serde_bytes;
+
+    let small: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
+    assert_eq!(
+        to_string(&small).unwrap(),
+        "(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15)"
+    );
+
+    let large = vec![255u8; 64];
+    let large = serde_bytes::Bytes::new(&large);
+    assert_eq!(
+        to_string(&large).unwrap(),
+        concat!(
+            "\"/////////////////////////////////////////",
+            "////////////////////////////////////////////w==\""
+        )
+    );
+}
+
+#[test]
+fn rename() {
+    #[derive(Serialize, Debug, PartialEq)]
+    enum Foo {
+        #[serde(rename = "2d")]
+        D2,
+        #[serde(rename = "triangle-list")]
+        TriangleList,
+    }
+
+    assert_eq!(to_string(&Foo::D2).unwrap(), "r#2d");
+    assert_eq!(to_string(&Foo::TriangleList).unwrap(), "r#triangle-list");
+}