Repository: not-fl3/nanoserde
Pull request #127 (2025-01-18 21:27:08)

add reason enum to use instead of string value for error messages
Changes the error type so that new variants may be added in future without breaking changes


Patch:
diff --git a/src/serde_bin.rs b/src/serde_bin.rs
index 8a7e3b1..eca52cc 100644
--- a/src/serde_bin.rs
+++ b/src/serde_bin.rs
@@ -59,28 +59,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 +125,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 +177,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 +195,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 +224,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 +254,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 +265,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 +409,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..f1fe3f1 100644
--- a/src/serde_json.rs
+++ b/src/serde_json.rs
@@ -107,7 +107,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 +142,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 +220,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 +228,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 +236,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 +244,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 +252,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 +260,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 +268,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..6110baf 100644
--- a/src/serde_ron.rs
+++ b/src/serde_ron.rs
@@ -98,7 +98,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 +133,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 +211,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 +219,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 +227,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 +235,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 +243,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 +251,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 +259,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,
         }


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]