Repository: not-fl3/nanoserde
Pull request #125 (2024-12-11 16:23:51)

bump msrv and crate versions, remove std::error references
I think we have just about all the changes that are reasonable to wait for for v0.2. All the breaking changes are pretty small, and the majority of users will likely only need to adjust MSRV. This PR bumps the MSRV to 1.81, the crate versions to 0.2, and removes the import of `std::error::error`

Breaking changes:
- A non-exhasutive `ErrReason` added to Error structs to improve forward compatibility
    - Breaks anyone who was destructuring/manually creating the `De{format}Err` structs
    - Can be fixed by non-exhaustively matching on error reason
- `binary`/`json`/`ron`/`toml` formats split into feature flags, with all enabled by default
    - Breaks anyone who was using `default-features = false` before, but we didn't have any features so should be rare
- `no_std` feature removed, added `std` feature which gates the couple remaining items (mostly `HashMap/HashSet`)
    - Breaks anyone who used the old `no_std` feature 
    - Can be fixed by removing the `no_std` feature, and switching to `default-features = false`
- Bump MSRV to 1.81 (for `core::error::Error`)
    - Breaks anyone who is on a rust version <1.81
- Hardcode the `nanoserde` crate name (with override)
    - Breaks anyone who renamed the nanoserde crate and used the derive macros
    - Can be fixed using default name or the new `nserde(crate="renamed")` attribute

Other changes since last non-beta release:
- Support for comments in JSON
- Support for `nserde(default)` on non-`Option` types and 
- Bug fixes to float serialization, proxy attrs, along with `toml` and explicitly numbered enum parsing
- Removed `no_std` `hashbrown` dependency

knickish: ```
Check for toolchain '1.80.1-x86_64-unknown-linux-gnu' failed with:
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ error: rustc 1.80.1 is not supported by the following packages:                                         │
│   nanoserde@0.2.0 requires rustc 1.81.0                                                                 │
│   nanoserde-derive@0.2.0 requires rustc 1.81.0                                                          │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Check for toolchain '1.82.0-x86_64-unknown-linux-gnu' succeeded
Check for toolchain '1.81.0-x86_64-unknown-linux-gnu' succeeded
   Finished The MSRV is: 1.81.0
```

Confirmed to work with MSRV of 1.81
knickish: @not-fl3 Anything else you want done before `v0.2`?
knickish: Dropping this back to draft. Realizing that the `#[non_exhaustive]` on the error types makes them essentially uncontrollable for user-defined errors, so working on a PR to fix that before this one should be merged.

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index b9a076e..95f424b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,12 +1,12 @@
 [package]
 name = "nanoserde"
-version = "0.2.0-beta.2"
+version = "0.2.0"
 authors = ["makepad <info@makepad.nl>", "Fedor <not.fl3@gmail.com>"]
 license = "MIT OR Apache-2.0"
 description = """Serialization library with zero dependencies.
 Supports Binary, JSON, RON and TOML."""
 edition = "2021"
-rust-version = "1.71.1"
+rust-version = "1.81.0"
 repository = "https://github.com/not-fl3/nanoserde"
 
 [features]
@@ -20,4 +20,4 @@ toml = []
 std = []
 
 [dependencies]
-nanoserde-derive = { path = "derive", version = "=0.2.0-beta.2", optional = true }
+nanoserde-derive = { path = "derive", version = "=0.2.0", optional = true }
diff --git a/README.md b/README.md
index a9c621f..76cf52b 100644
--- a/README.md
+++ b/README.md
@@ -10,8 +10,8 @@ No more syn, proc_macro2 or quote in the build tree!
 
 ```
 > cargo tree
-nanoserde v0.1.0 (/../nanoserde)
-└── nanoserde-derive v0.1.0 (/../nanoserde/derive)
+nanoserde v0.2.0 (/../nanoserde)
+└── nanoserde-derive v0.2.0 (/../nanoserde/derive)
 ```
 
 ## Example:
diff --git a/derive/Cargo.toml b/derive/Cargo.toml
index da83098..e5bc311 100644
--- a/derive/Cargo.toml
+++ b/derive/Cargo.toml
@@ -1,9 +1,9 @@
 [package]
 name = "nanoserde-derive"
-version = "0.2.0-beta.2"
+version = "0.2.0"
 authors = ["Makepad <info@makepad.nl>", "Fedor <not.fl3@gmail.com>"]
 edition = "2021"
-rust-version = "1.78.0"
+rust-version = "1.81.0"
 description = "Fork of makepad-tinyserde derive without any external dependencies"
 license = "MIT"
 
diff --git a/src/serde_bin.rs b/src/serde_bin.rs
index 8a7e3b1..c1809bb 100644
--- a/src/serde_bin.rs
+++ b/src/serde_bin.rs
@@ -1,10 +1,5 @@
 use core::convert::TryInto;
-
-// remove this after 1.81 is live
-#[cfg(not(feature = "std"))]
 use core::error::Error;
-#[cfg(feature = "std")]
-use std::error::Error;
 
 use alloc::borrow::ToOwned;
 use alloc::boxed::Box;
@@ -59,28 +54,50 @@ pub trait DeBin: Sized {
     fn de_bin(offset: &mut usize, bytes: &[u8]) -> Result<Self, DeBinErr>;
 }
 
-/// The error message when failing to deserialize from raw bytes.
 #[derive(Clone)]
 #[non_exhaustive]
+pub enum DeBinErrReason {
+    Length {
+        /// Expected Length
+        expected_length: usize,
+        /// Actual Length
+        actual_length: usize,
+    },
+}
+
+/// The error message when failing to deserialize.
+#[derive(Clone)]
 pub struct DeBinErr {
+    /// Offset
     pub o: usize,
-    pub l: usize,
-    pub s: usize,
+    pub msg: DeBinErrReason,
 }
 
 impl DeBinErr {
-    pub fn new(o: usize, l: usize, s: usize) -> Self {
-        Self { o, l, s }
+    /// Helper for constructing [`DeBinErr`]
+    pub fn new(offset: usize, expected_length: usize, actual_length: usize) -> Self {
+        Self {
+            o: offset,
+            msg: DeBinErrReason::Length {
+                expected_length,
+                actual_length,
+            },
+        }
     }
 }
 
 impl core::fmt::Debug for DeBinErr {
     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
-        write!(
-            f,
-            "Bin deserialize error at:{} wanted:{} bytes but max size is {}",
-            self.o, self.l, self.s
-        )
+        match self.msg {
+            DeBinErrReason::Length {
+                expected_length: l,
+                actual_length: s,
+            } => write!(
+                f,
+                "Bin deserialize error at:{} wanted:{} bytes but max size is {}",
+                self.o, l, s
+            ),
+        }
     }
 }
 
@@ -103,20 +120,23 @@ macro_rules! impl_ser_de_bin_for {
 
         impl DeBin for $ty {
             fn de_bin(o: &mut usize, d: &[u8]) -> Result<$ty, DeBinErr> {
-                let l = core::mem::size_of::<$ty>();
-                if *o + l > d.len() {
+                let expected_length = core::mem::size_of::<$ty>();
+                if *o + expected_length > d.len() {
                     return Err(DeBinErr {
                         o: *o,
-                        l,
-                        s: d.len(),
+                        msg: DeBinErrReason::Length {
+                            expected_length,
+                            actual_length: d.len(),
+                        },
                     });
                 }
 
                 // We just checked that the correct amount of bytes are available,
                 // and there are no invalid bit patterns for these primitives. This
                 // unwrap should be impossible to hit.
-                let ret: $ty = <$ty>::from_le_bytes(d[*o..(*o + l)].try_into().unwrap());
-                *o += l;
+                let ret: $ty =
+                    <$ty>::from_le_bytes(d[*o..(*o + expected_length)].try_into().unwrap());
+                *o += expected_length;
                 Ok(ret)
             }
         }
@@ -152,8 +172,10 @@ impl DeBin for usize {
             None => {
                 return Err(DeBinErr {
                     o: *o,
-                    l,
-                    s: d.len(),
+                    msg: DeBinErrReason::Length {
+                        expected_length: l,
+                        actual_length: d.len(),
+                    },
                 });
             }
         };
@@ -168,8 +190,10 @@ impl DeBin for u8 {
         if *o + 1 > d.len() {
             return Err(DeBinErr {
                 o: *o,
-                l: 1,
-                s: d.len(),
+                msg: DeBinErrReason::Length {
+                    expected_length: 1,
+                    actual_length: d.len(),
+                },
             });
         }
         let m = d[*o];
@@ -195,8 +219,10 @@ impl DeBin for bool {
         if *o + 1 > d.len() {
             return Err(DeBinErr {
                 o: *o,
-                l: 1,
-                s: d.len(),
+                msg: DeBinErrReason::Length {
+                    expected_length: 1,
+                    actual_length: d.len(),
+                },
             });
         }
         let m = d[*o];
@@ -223,8 +249,10 @@ impl DeBin for String {
         if *o + len > d.len() {
             return Err(DeBinErr {
                 o: *o,
-                l: 1,
-                s: d.len(),
+                msg: DeBinErrReason::Length {
+                    expected_length: 1,
+                    actual_length: d.len(),
+                },
             });
         }
         let r = match core::str::from_utf8(&d[*o..(*o + len)]) {
@@ -232,8 +260,10 @@ impl DeBin for String {
             Err(_) => {
                 return Err(DeBinErr {
                     o: *o,
-                    l: len,
-                    s: d.len(),
+                    msg: DeBinErrReason::Length {
+                        expected_length: len,
+                        actual_length: d.len(),
+                    },
                 })
             }
         };
@@ -374,8 +404,10 @@ where
         if *o + 1 > d.len() {
             return Err(DeBinErr {
                 o: *o,
-                l: 1,
-                s: d.len(),
+                msg: DeBinErrReason::Length {
+                    expected_length: 1,
+                    actual_length: d.len(),
+                },
             });
         }
         let m = d[*o];
diff --git a/src/serde_json.rs b/src/serde_json.rs
index 0f6306d..5e58942 100644
--- a/src/serde_json.rs
+++ b/src/serde_json.rs
@@ -1,10 +1,5 @@
-use core::str::Chars;
-
-// remove this after 1.81 is msrv
-#[cfg(not(feature = "std"))]
 use core::error::Error;
-#[cfg(feature = "std")]
-use std::error::Error;
+use core::str::Chars;
 
 use alloc::boxed::Box;
 use alloc::collections::{BTreeMap, BTreeSet, LinkedList};
@@ -107,7 +102,7 @@ pub trait DeJson: Sized {
 }
 
 /// A JSON parsed token.
-#[derive(PartialEq, Debug, Default)]
+#[derive(PartialEq, Debug, Default, Clone)]
 #[non_exhaustive]
 pub enum DeJsonTok {
     Str,
@@ -142,23 +137,55 @@ pub struct DeJsonState {
     pub col: usize,
 }
 
-/// The error message when failing to deserialize a JSON string.
 #[derive(Clone)]
 #[non_exhaustive]
+pub enum DeJsonErrReason {
+    UnexpectedKey(String),
+    UnexpectedToken(DeJsonTok, String),
+    MissingKey(String),
+    NoSuchEnum(String),
+    OutOfRange(String),
+    WrongType(String),
+    CannotParse(String),
+}
+
+/// The error message when failing to deserialize a JSON string.
+#[derive(Clone)]
 pub struct DeJsonErr {
-    pub msg: String,
     pub line: usize,
     pub col: usize,
+    pub msg: DeJsonErrReason,
+}
+
+impl core::fmt::Debug for DeJsonErrReason {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::UnexpectedKey(name) => write!(f, "Unexpected key {}", name),
+            Self::MissingKey(name) => write!(f, "Key not found {}", name),
+            Self::NoSuchEnum(name) => write!(f, "Enum not defined {}", name),
+            Self::UnexpectedToken(token, name) => {
+                write!(f, "Unexpected token {:?} expected {} ", token, name)
+            }
+            Self::OutOfRange(value) => write!(f, "Value out of range {} ", value),
+            Self::WrongType(found) => write!(f, "Token wrong type {} ", found),
+            Self::CannotParse(unparseable) => write!(f, "Cannot parse {} ", unparseable),
+        }
+    }
 }
 
 impl core::fmt::Debug for DeJsonErr {
     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        let DeJsonErr {
+            line,
+            col: column,
+            msg: reason,
+        } = self;
         write!(
             f,
-            "Json Deserialize error: {}, line:{} col:{}",
-            self.msg,
-            self.line + 1,
-            self.col + 1
+            "Json Deserialize error: {:?}, line:{} col:{}",
+            reason,
+            line + 1,
+            column + 1
         )
     }
 }
@@ -188,7 +215,7 @@ impl DeJsonState {
 
     pub fn err_exp(&self, name: &str) -> DeJsonErr {
         DeJsonErr {
-            msg: format!("Unexpected key {}", name),
+            msg: DeJsonErrReason::UnexpectedKey(name.to_string()),
             line: self.line,
             col: self.col,
         }
@@ -196,7 +223,7 @@ impl DeJsonState {
 
     pub fn err_nf(&self, name: &str) -> DeJsonErr {
         DeJsonErr {
-            msg: format!("Key not found {}", name),
+            msg: DeJsonErrReason::MissingKey(name.to_string()),
             line: self.line,
             col: self.col,
         }
@@ -204,7 +231,7 @@ impl DeJsonState {
 
     pub fn err_enum(&self, name: &str) -> DeJsonErr {
         DeJsonErr {
-            msg: format!("Enum not defined {}", name),
+            msg: DeJsonErrReason::NoSuchEnum(name.to_string()),
             line: self.line,
             col: self.col,
         }
@@ -212,7 +239,7 @@ impl DeJsonState {
 
     pub fn err_token(&self, what: &str) -> DeJsonErr {
         DeJsonErr {
-            msg: format!("Unexpected token {:?} expected {} ", self.tok, what),
+            msg: DeJsonErrReason::UnexpectedToken(self.tok.clone(), what.to_string()),
             line: self.line,
             col: self.col,
         }
@@ -220,7 +247,7 @@ impl DeJsonState {
 
     pub fn err_range(&self, what: &str) -> DeJsonErr {
         DeJsonErr {
-            msg: format!("Value out of range {} ", what),
+            msg: DeJsonErrReason::OutOfRange(what.to_string()),
             line: self.line,
             col: self.col,
         }
@@ -228,7 +255,7 @@ impl DeJsonState {
 
     pub fn err_type(&self, what: &str) -> DeJsonErr {
         DeJsonErr {
-            msg: format!("Token wrong type {} ", what),
+            msg: DeJsonErrReason::WrongType(what.to_string()),
             line: self.line,
             col: self.col,
         }
@@ -236,7 +263,7 @@ impl DeJsonState {
 
     pub fn err_parse(&self, what: &str) -> DeJsonErr {
         DeJsonErr {
-            msg: format!("Cannot parse {} ", what),
+            msg: DeJsonErrReason::CannotParse(what.to_string()),
             line: self.line,
             col: self.col,
         }
diff --git a/src/serde_ron.rs b/src/serde_ron.rs
index ff5a2b9..f6cc080 100644
--- a/src/serde_ron.rs
+++ b/src/serde_ron.rs
@@ -1,10 +1,5 @@
-use core::str::Chars;
-
-// remove this after 1.81 is live
-#[cfg(not(feature = "std"))]
 use core::error::Error;
-#[cfg(feature = "std")]
-use std::error::Error;
+use core::str::Chars;
 
 use alloc::boxed::Box;
 use alloc::collections::{BTreeMap, BTreeSet, LinkedList};
@@ -98,7 +93,7 @@ pub trait DeRon: Sized {
 }
 
 /// A RON parsed token.
-#[derive(PartialEq, Debug, Default)]
+#[derive(PartialEq, Debug, Default, Clone)]
 pub enum DeRonTok {
     Ident,
     Str,
@@ -133,23 +128,55 @@ pub struct DeRonState {
     pub col: usize,
 }
 
-/// The error message when failing to deserialize a RON string.
 #[derive(Clone)]
 #[non_exhaustive]
+pub enum DeRonErrReason {
+    UnexpectedKey(String),
+    UnexpectedToken(DeRonTok, String),
+    MissingKey(String),
+    NoSuchEnum(String),
+    OutOfRange(String),
+    WrongType(String),
+    CannotParse(String),
+}
+
+/// The error message when failing to deserialize a Ron string.
+#[derive(Clone)]
 pub struct DeRonErr {
-    pub msg: String,
     pub line: usize,
     pub col: usize,
+    pub msg: DeRonErrReason,
+}
+
+impl core::fmt::Debug for DeRonErrReason {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::UnexpectedKey(name) => write!(f, "Unexpected key {}", name),
+            Self::MissingKey(name) => write!(f, "Key not found {}", name),
+            Self::NoSuchEnum(name) => write!(f, "Enum not defined {}", name),
+            Self::UnexpectedToken(token, name) => {
+                write!(f, "Unexpected token {:?} expected {} ", token, name)
+            }
+            Self::OutOfRange(value) => write!(f, "Value out of range {} ", value),
+            Self::WrongType(found) => write!(f, "Token wrong type {} ", found),
+            Self::CannotParse(unparseable) => write!(f, "Cannot parse {} ", unparseable),
+        }
+    }
 }
 
 impl core::fmt::Debug for DeRonErr {
     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        let DeRonErr {
+            line,
+            col: column,
+            msg: reason,
+        } = self;
         write!(
             f,
-            "Ron Deserialize error: {}, line:{} col:{}",
-            self.msg,
-            self.line + 1,
-            self.col + 1
+            "Ron Deserialize error: {:?}, line:{} col:{}",
+            reason,
+            line + 1,
+            column + 1
         )
     }
 }
@@ -179,7 +206,7 @@ impl DeRonState {
 
     pub fn err_exp(&self, name: &str) -> DeRonErr {
         DeRonErr {
-            msg: format!("Unexpected key {}", name),
+            msg: DeRonErrReason::UnexpectedKey(name.to_string()),
             line: self.line,
             col: self.col,
         }
@@ -187,7 +214,7 @@ impl DeRonState {
 
     pub fn err_nf(&self, name: &str) -> DeRonErr {
         DeRonErr {
-            msg: format!("Key not found {}", name),
+            msg: DeRonErrReason::MissingKey(name.to_string()),
             line: self.line,
             col: self.col,
         }
@@ -195,7 +222,7 @@ impl DeRonState {
 
     pub fn err_enum(&self, name: &str) -> DeRonErr {
         DeRonErr {
-            msg: format!("Enum not defined {}", name),
+            msg: DeRonErrReason::NoSuchEnum(name.to_string()),
             line: self.line,
             col: self.col,
         }
@@ -203,7 +230,7 @@ impl DeRonState {
 
     pub fn err_token(&self, what: &str) -> DeRonErr {
         DeRonErr {
-            msg: format!("Unexpected token {:?} expected {} ", self.tok, what),
+            msg: DeRonErrReason::UnexpectedToken(self.tok.clone(), what.to_string()),
             line: self.line,
             col: self.col,
         }
@@ -211,7 +238,7 @@ impl DeRonState {
 
     pub fn err_range(&self, what: &str) -> DeRonErr {
         DeRonErr {
-            msg: format!("Value out of range {} ", what),
+            msg: DeRonErrReason::OutOfRange(what.to_string()),
             line: self.line,
             col: self.col,
         }
@@ -219,7 +246,7 @@ impl DeRonState {
 
     pub fn err_type(&self, what: &str) -> DeRonErr {
         DeRonErr {
-            msg: format!("Token wrong type {} ", what),
+            msg: DeRonErrReason::WrongType(what.to_string()),
             line: self.line,
             col: self.col,
         }
@@ -227,7 +254,7 @@ impl DeRonState {
 
     pub fn err_parse(&self, what: &str) -> DeRonErr {
         DeRonErr {
-            msg: format!("Cannot parse {} ", what),
+            msg: DeRonErrReason::CannotParse(what.to_string()),
             line: self.line,
             col: self.col,
         }
diff --git a/src/toml.rs b/src/toml.rs
index 731ceed..b512c45 100644
--- a/src/toml.rs
+++ b/src/toml.rs
@@ -1,10 +1,5 @@
-use core::str::Chars;
-
-// remove this after 1.81 is live
-#[cfg(not(feature = "std"))]
 use core::error::Error;
-#[cfg(feature = "std")]
-use std::error::Error;
+use core::str::Chars;
 
 use alloc::format;
 use alloc::string::{String, ToString};


Test patch:
diff --git a/tests/json.rs b/tests/json.rs
index e767bbe..5e669f3 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -1,5 +1,5 @@
 #![cfg(feature = "json")]
-use nanoserde::{DeJson, SerJson};
+use nanoserde::{DeJson, DeJsonErrReason, SerJson};
 
 use std::{
     collections::{BTreeMap, BTreeSet, LinkedList},
@@ -1166,16 +1166,14 @@ fn test_deser_oversized_value() {
         <EnumConstant as DeJson>::deserialize_json(&max_json).unwrap(),
         EnumConstant { value: i32::MAX }
     );
-    assert_eq!(
+
+    assert!(matches!(
         <EnumConstant as DeJson>::deserialize_json(&wrap_json)
             .unwrap_err()
             .msg,
-        format!(
-            "Value out of range {}>{} ",
-            (i32::MAX as i64 + 1).to_string(),
-            i32::MAX.to_string()
-        )
-    );
+        DeJsonErrReason::OutOfRange(v)
+            if v == format!("{}>{}", (i32::MAX as i64 + 1), i32::MAX),
+    ));
 }
 
 #[test]
diff --git a/tests/ron.rs b/tests/ron.rs
index 7569617..cfb95fa 100644
--- a/tests/ron.rs
+++ b/tests/ron.rs
@@ -1,5 +1,5 @@
 #![cfg(feature = "ron")]
-use nanoserde::{DeRon, SerRon};
+use nanoserde::{DeRon, DeRonErrReason, SerRon};
 
 use std::{
     collections::{BTreeMap, BTreeSet, LinkedList},
@@ -627,16 +627,14 @@ fn test_deser_oversized_value() {
         <EnumConstant as DeRon>::deserialize_ron(&max_ron).unwrap(),
         EnumConstant { value: i32::MAX }
     );
-    assert_eq!(
+
+    assert!(matches!(
         <EnumConstant as DeRon>::deserialize_ron(&wrap_ron)
             .unwrap_err()
             .msg,
-        format!(
-            "Value out of range {}>{} ",
-            (i32::MAX as i64 + 1).to_string(),
-            i32::MAX.to_string()
-        )
-    );
+        DeRonErrReason::OutOfRange(v)
+            if v == format!("{}>{}", (i32::MAX as i64 + 1), i32::MAX),
+    ));
 }
 
 #[test]