Repository: ron-rs/ron (rust)

Issue:
Consider adding top-level attributes
**Syntax:** `#![attribute_name]` (like in Rust)

After reading https://ozkriff.github.io/2017-12-01--devlog.html where @ozkriff said that his configs (https://raw.githubusercontent.com/ozkriff/zemeroth_assets/master/objects.ron) aren't looking very nice because of how RON handles newtypes, I thought about adding attributes which allow to configure RON for your needs. Not only would this allow for toggles like `#![unwrap_newtypes]`, but it could also be used to deal with new features. That way, we can just enforce that you need `#![feature_name]` to enable a RON feature, such that a parser without support for that feature can return a simple and understandable error.

Pull request:
Add extension support and first extension, unwrap_newtypes
## TODO

* [x] More tests, especially regarding error handling
* [x] discuss TODOs / design
* [x] ~~docs for extensions~~ (postponed)
* [x] add syntax to Sublime plugin

## Fixes

Fixes #69 

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 41a5891f..dbc51bb4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,7 @@ exclude = ["bors.toml", ".travis.yml"]
 name = "ron"
 
 [dependencies]
+bitflags = "1"
 serde = { version = "1", features = ["serde_derive"] }
 
 [dev-dependencies]
diff --git a/highlight/RON.sublime-syntax b/highlight/RON.sublime-syntax
index 7a21c086..c34e9716 100644
--- a/highlight/RON.sublime-syntax
+++ b/highlight/RON.sublime-syntax
@@ -6,6 +6,10 @@ file_extensions:
 scope: source.ron
 contexts:
   main:
+    # extensions
+    - match: '\#\!'
+      push:
+        - include: array
     - include: value
   array:
     - match: '\['
diff --git a/src/de/error.rs b/src/de/error.rs
index 7cf9f028..005b575e 100644
--- a/src/de/error.rs
+++ b/src/de/error.rs
@@ -23,6 +23,8 @@ pub enum ParseError {
     Eof,
     ExpectedArray,
     ExpectedArrayEnd,
+    ExpectedAttribute,
+    ExpectedAttributeEnd,
     ExpectedBoolean,
     ExpectedComma,
     ExpectedEnum,
@@ -44,6 +46,8 @@ pub enum ParseError {
 
     InvalidEscape,
 
+    NoSuchExtension(String),
+
     UnexpectedByte(char),
 
     Utf8Error(Utf8Error),
@@ -78,6 +82,8 @@ impl StdError for Error {
                 ParseError::Eof => "Unexpected end of file",
                 ParseError::ExpectedArray => "Expected array",
                 ParseError::ExpectedArrayEnd => "Expected end of array",
+                ParseError::ExpectedAttribute => "Expected an enable attribute",
+                ParseError::ExpectedAttributeEnd => "Expected closing `)` and `]` after the attribute",
                 ParseError::ExpectedBoolean => "Expected boolean",
                 ParseError::ExpectedComma => "Expected comma",
                 ParseError::ExpectedEnum => "Expected enum",
diff --git a/src/de/mod.rs b/src/de/mod.rs
index cd8bc725..4de9318d 100644
--- a/src/de/mod.rs
+++ b/src/de/mod.rs
@@ -9,7 +9,7 @@ use std::str;
 
 use serde::de::{self, Deserializer as Deserializer_, DeserializeSeed, Visitor};
 
-use parse::Bytes;
+use parse::{Bytes, Extensions};
 use self::id::IdDeserializer;
 
 mod error;
@@ -27,16 +27,14 @@ pub struct Deserializer<'de> {
 }
 
 impl<'de> Deserializer<'de> {
-    pub fn from_str(input: &'de str) -> Self {
-        Deserializer {
-            bytes: Bytes::new(input.as_bytes()),
-        }
+    pub fn from_str(input: &'de str) -> Result<Self> {
+        Deserializer::from_bytes(input.as_bytes())
     }
 
-    pub fn from_bytes(input: &'de [u8]) -> Self {
-        Deserializer {
-            bytes: Bytes::new(input),
-        }
+    pub fn from_bytes(input: &'de [u8]) -> Result<Self> {
+        Ok(Deserializer {
+            bytes: Bytes::new(input)?,
+        })
     }
 
     pub fn remainder(&self) -> Cow<str> {
@@ -45,23 +43,31 @@ impl<'de> Deserializer<'de> {
 }
 
 /// A convenience function for reading data from a reader
-/// and feeding into a deserializer
+/// and feeding into a deserializer.
 pub fn from_reader<R, T>(mut rdr: R) -> Result<T>
     where R: io::Read,
           T: de::DeserializeOwned
 {
     let mut bytes = Vec::new();
     rdr.read_to_end(&mut bytes)?;
-    let s = str::from_utf8(&bytes)?;
-    from_str(s)
+
+    from_bytes(&bytes)
 }
 
 /// A convenience function for building a deserializer
-/// and deserializing a value of type `T`.
+/// and deserializing a value of type `T` from a string.
 pub fn from_str<'a, T>(s: &'a str) -> Result<T>
     where T: de::Deserialize<'a>
 {
-    let mut deserializer = Deserializer::from_str(s);
+    from_bytes(s.as_bytes())
+}
+
+/// A convenience function for building a deserializer
+/// and deserializing a value of type `T` from bytes.
+pub fn from_bytes<'a, T>(s: &'a [u8]) -> Result<T>
+    where T: de::Deserialize<'a>
+{
+    let mut deserializer = Deserializer::from_bytes(s)?;
     let t = T::deserialize(&mut deserializer)?;
 
     deserializer.end()?;
@@ -274,6 +280,10 @@ impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
     ) -> Result<V::Value>
         where V: Visitor<'de>
     {
+        if self.bytes.exts.contains(Extensions::UNWRAP_NEWTYPES) {
+            return visitor.visit_newtype_struct(&mut *self);
+        }
+
         self.bytes.consume(name);
 
         self.bytes.skip_ws();
diff --git a/src/de/value.rs b/src/de/value.rs
index ca01f9b0..7d31adfe 100644
--- a/src/de/value.rs
+++ b/src/de/value.rs
@@ -10,7 +10,7 @@ use value::{Number, Value};
 impl Value {
     /// Creates a value from a string reference.
     pub fn from_str(s: &str) -> de::Result<Self> {
-        Self::deserialize(&mut super::Deserializer::from_str(s))
+        Value::deserialize(&mut super::Deserializer::from_str(s)?)
     }
 }
 
diff --git a/src/lib.rs b/src/lib.rs
index 070793e9..ab0ea8ef 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -57,6 +57,8 @@ Serializing / Deserializing is as simple as calling `to_string` / `from_str`.
 
 !*/
 
+#[macro_use]
+extern crate bitflags;
 #[macro_use]
 extern crate serde;
 
diff --git a/src/parse.rs b/src/parse.rs
index b5bb9bf8..03e1dcbe 100644
--- a/src/parse.rs
+++ b/src/parse.rs
@@ -12,22 +12,36 @@ const WHITE_SPACE: &[u8] = b"\n\t\r ";
 
 #[derive(Clone, Copy, Debug)]
 pub struct Bytes<'a> {
+    /// Bits set according to `Extension` enum.
+    pub exts: Extensions,
     bytes: &'a [u8],
     column: usize,
     line: usize,
 }
 
 impl<'a> Bytes<'a> {
-    pub fn new(bytes: &'a [u8]) -> Self {
+    pub fn new(bytes: &'a [u8]) -> Result<Self> {
         let mut b = Bytes {
             bytes,
             column: 1,
+            exts: Extensions::empty(),
             line: 1,
         };
 
         b.skip_ws();
+        // Loop over all extensions attributes
+        loop {
+            let attribute = b.extensions()?;
 
-        b
+            if attribute.is_empty() {
+                break;
+            }
+
+            b.exts |= attribute;
+            b.skip_ws();
+        }
+
+        Ok(b)
     }
 
     pub fn advance(&mut self, bytes: usize) -> Result<()> {
@@ -135,6 +149,21 @@ impl<'a> Bytes<'a> {
         }
     }
 
+    fn consume_all(&mut self, all: &[&str]) -> bool {
+        all
+            .iter()
+            .map(|elem| {
+                if self.consume(elem) {
+                    self.skip_ws();
+
+                    true
+                } else {
+                    false
+                }
+            })
+            .all(|b| b)
+    }
+
     pub fn eat_byte(&mut self) -> Result<u8> {
         let peek = self.peek_or_eof()?;
         let _ = self.advance_single();
@@ -150,6 +179,51 @@ impl<'a> Bytes<'a> {
         Error::Parser(kind, Position { line: self.line, col: self.column })
     }
 
+    /// Returns the extensions bit mask.
+    fn extensions(&mut self) -> Result<Extensions> {
+        if self.peek() != Some(b'#') {
+            return Ok(Extensions::empty())
+        }
+
+        if !self.consume_all(&["#", "!", "[", "enable", "("]) {
+            return self.err(ParseError::ExpectedAttribute);
+        }
+
+        self.skip_ws();
+        let mut extensions = Extensions::empty();
+
+        loop {
+            let ident = self.identifier()?;
+            let extension = Extensions::from_ident(ident)
+                .ok_or_else(|| {
+                    self.error(ParseError::NoSuchExtension(from_utf8(ident).unwrap().to_owned()))
+                })?;
+
+            extensions |= extension;
+
+            let comma = self.comma();
+
+            // If we have no comma but another item, return an error
+            if !comma && self.check_ident_char(0) {
+                return self.err(ParseError::ExpectedComma);
+            }
+
+            // If there's no comma, assume the list ended.
+            // If there is, it might be a trailing one, thus we only
+            // continue the loop if we get an ident char.
+            if !comma || !self.check_ident_char(0) {
+                break;
+            }
+        }
+
+        self.skip_ws();
+
+        match self.consume_all(&[")", "]"]) {
+            true => Ok(extensions),
+            false => Err(self.error(ParseError::ExpectedAttributeEnd)),
+        }
+    }
+
     pub fn float<T>(&mut self) -> Result<T>
         where T: FromStr
     {
@@ -163,7 +237,7 @@ impl<'a> Bytes<'a> {
         res
     }
 
-    pub fn identifier(&mut self) -> Result<&[u8]> {
+    pub fn identifier(&mut self) -> Result<&'a [u8]> {
         if IDENT_FIRST.contains(&self.peek_or_eof()?) {
             let bytes = self.next_bytes_contained_in(IDENT_CHAR);
 
@@ -274,7 +348,7 @@ impl<'a> Bytes<'a> {
         let num_bytes = self.next_bytes_contained_in(DIGITS);
 
         if num_bytes == 0 {
-            return self.err(ParseError::Eof);
+            return self.err(ParseError::ExpectedInteger);
         }
 
         let res = FromStr::from_str(unsafe { from_utf8_unchecked(&self.bytes[0..num_bytes]) })
@@ -382,6 +456,22 @@ impl<'a> Bytes<'a> {
     }
 }
 
+bitflags! {
+    pub struct Extensions: usize {
+        const UNWRAP_NEWTYPES = 0x1;
+    }
+}
+
+impl Extensions {
+    /// Creates an extension flag from an ident.
+    pub fn from_ident(ident: &[u8]) -> Option<Extensions> {
+        match ident {
+            b"unwrap_newtypes" => Some(Extensions::UNWRAP_NEWTYPES),
+            _ => None,
+        }
+    }
+}
+
 #[derive(Clone, Debug)]
 pub enum ParsedStr<'a> {
     Allocated(String),


Test patch:
diff --git a/src/de/tests.rs b/src/de/tests.rs
index 887d7e24..951e1059 100644
--- a/src/de/tests.rs
+++ b/src/de/tests.rs
@@ -169,3 +169,43 @@ fn forgot_apostrophes() {
         _ => false,
     });
 }
+
+#[test]
+fn expected_attribute() {
+    let de: Result<String> = from_str("#\"Hello\"");
+
+    assert_eq!(de, err(ParseError::ExpectedAttribute, 1, 2));
+}
+
+#[test]
+fn expected_attribute_end() {
+    let de: Result<String> = from_str("#![enable(unwrap_newtypes) \"Hello\"");
+
+    assert_eq!(de, err(ParseError::ExpectedAttributeEnd, 1, 28));
+}
+
+#[test]
+fn invalid_attribute() {
+    let de: Result<String> = from_str("#![enable(invalid)] \"Hello\"");
+
+    assert_eq!(de, err(ParseError::NoSuchExtension("invalid".to_string()), 1, 18));
+}
+
+#[test]
+fn multiple_attributes() {
+    #[derive(Debug, Deserialize, PartialEq)]
+    struct New(String);
+    let de: Result<New> = from_str("#![enable(unwrap_newtypes)] #![enable(unwrap_newtypes)] \"Hello\"");
+
+    assert_eq!(de, Ok(New("Hello".to_owned())));
+}
+
+#[test]
+fn uglified_attribute() {
+    let de: Result<()> = from_str("#   !\
+    // We definitely want to add a comment here
+    [\t\tenable( // best style ever
+            unwrap_newtypes  ) ] ()");
+
+    assert_eq!(de, Ok(()));
+}
diff --git a/tests/extensions.rs b/tests/extensions.rs
new file mode 100644
index 00000000..7702a930
--- /dev/null
+++ b/tests/extensions.rs
@@ -0,0 +1,56 @@
+extern crate ron;
+#[macro_use]
+extern crate serde;
+
+use std::collections::HashMap;
+
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
+struct UnitStruct;
+
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
+struct NewType(f32);
+
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
+struct TupleStruct(UnitStruct, i8);
+
+#[derive(Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+struct Key(u32);
+
+#[derive(Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+enum Enum {
+    Unit,
+    Bool(bool),
+    Chars(char, String),
+}
+
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
+struct Struct {
+    tuple: ((), NewType, TupleStruct),
+    vec: Vec<Option<UnitStruct>>,
+    map: HashMap<Key, Enum>,
+}
+
+const CONFIG_U_NT: &str = "
+#![enable(unwrap_newtypes)]
+
+(
+    tuple: ((), 0.5, ((), -5)),
+    vec: [
+        None,
+        Some(()),
+    ],
+    map: {
+        7: Bool(true),
+        9: Chars('x', \"\"),
+        6: Bool(false),
+        5: Unit,
+    },
+)
+";
+
+#[test]
+fn unwrap_newtypes() {
+    let d: Struct = ron::de::from_str(&CONFIG_U_NT).expect("Failed to deserialize");
+
+    println!("unwrap_newtypes: {:#?}", d);
+}
diff --git a/tests/roundtrip.rs b/tests/roundtrip.rs
index c88b9f48..c08840a4 100644
--- a/tests/roundtrip.rs
+++ b/tests/roundtrip.rs
@@ -40,7 +40,8 @@ fn roundtrip() {
             (Key(6), Enum::Bool(false)),
             (Key(7), Enum::Bool(true)),
             (Key(9), Enum::Chars('x', "".to_string())),
-        ].into_iter().collect()
+        ].into_iter()
+            .collect(),
     };
 
     let serial = ron::ser::to_string(&value).unwrap();
@@ -51,3 +52,26 @@ fn roundtrip() {
 
     assert_eq!(Ok(value), deserial);
 }
+
+#[test]
+fn roundtrip_pretty() {
+    let value = Struct {
+        tuple: ((), NewType(0.5), TupleStruct(UnitStruct, -5)),
+        vec: vec![None, Some(UnitStruct)],
+        map: vec![
+            (Key(5), Enum::Unit),
+            (Key(6), Enum::Bool(false)),
+            (Key(7), Enum::Bool(true)),
+            (Key(9), Enum::Chars('x', "".to_string())),
+        ].into_iter()
+            .collect(),
+    };
+
+    let serial = ron::ser::to_string_pretty(&value, Default::default()).unwrap();
+
+    println!("Serialized: {}", serial);
+
+    let deserial = ron::de::from_str(&serial);
+
+    assert_eq!(Ok(value), deserial);
+}