Repository: not-fl3/nanoserde
Pull request #135 (2025-05-30 08:28:27)

Support std::time types
Adds support for std::time::Duration and std::time::SystemTime.

knickish: I think this needs some round-trip tests, and also for the other formats, otherwise looks good
novafacing: Ok! Added round trip and other tests, caught some errors in the process so good shout. I think it should be good to go :)
novafacing: Actually...I think I changed my mind about doing a string, one second. I'll push a change that makes the JSOn and RON serializers do an object.
novafacing: Ok, JSON and RON use a { "secs": 1000, "nanos": 100000 } structure now. Much more readable in serialized form.

Patch:
diff --git a/src/serde_bin.rs b/src/serde_bin.rs
index c1809bb..249b94a 100644
--- a/src/serde_bin.rs
+++ b/src/serde_bin.rs
@@ -1,5 +1,5 @@
-use core::convert::TryInto;
 use core::error::Error;
+use core::{convert::TryInto, time::Duration};
 
 use alloc::borrow::ToOwned;
 use alloc::boxed::Box;
@@ -63,6 +63,7 @@ pub enum DeBinErrReason {
         /// Actual Length
         actual_length: usize,
     },
+    Range(String),
 }
 
 /// The error message when failing to deserialize.
@@ -97,6 +98,7 @@ impl core::fmt::Debug for DeBinErr {
                 "Bin deserialize error at:{} wanted:{} bytes but max size is {}",
                 self.o, l, s
             ),
+            DeBinErrReason::Range(ref s) => write!(f, "Bin deserialize error at:{} {}", self.o, s),
         }
     }
 }
@@ -653,3 +655,46 @@ where
         Ok(Box::new(DeBin::de_bin(o, d)?))
     }
 }
+
+impl SerBin for Duration {
+    fn ser_bin(&self, s: &mut Vec<u8>) {
+        let secs = self.as_secs();
+        let nanos = self.subsec_nanos();
+        secs.ser_bin(s);
+        nanos.ser_bin(s);
+    }
+}
+
+impl DeBin for Duration {
+    fn de_bin(o: &mut usize, d: &[u8]) -> Result<Duration, DeBinErr> {
+        let secs: u64 = DeBin::de_bin(o, d)?;
+        let nanos: u32 = DeBin::de_bin(o, d)?;
+        if nanos > 1_000_000_000 {
+            return Err(DeBinErr {
+                o: *o,
+                msg: DeBinErrReason::Range(
+                    "Duration nanos must be at most 1,000,000,000".to_owned(),
+                ),
+            });
+        }
+        Ok(Duration::new(secs, nanos))
+    }
+}
+
+#[cfg(feature = "std")]
+impl SerBin for std::time::SystemTime {
+    fn ser_bin(&self, s: &mut Vec<u8>) {
+        let duration = self.duration_since(std::time::SystemTime::UNIX_EPOCH).ok();
+        duration.ser_bin(s);
+    }
+}
+
+#[cfg(feature = "std")]
+impl DeBin for std::time::SystemTime {
+    fn de_bin(o: &mut usize, d: &[u8]) -> Result<std::time::SystemTime, DeBinErr> {
+        match DeBin::de_bin(o, d)? {
+            Some(duration) => Ok(std::time::SystemTime::UNIX_EPOCH + duration),
+            None => Ok(std::time::SystemTime::UNIX_EPOCH),
+        }
+    }
+}
diff --git a/src/serde_json.rs b/src/serde_json.rs
index 5e58942..03ff695 100644
--- a/src/serde_json.rs
+++ b/src/serde_json.rs
@@ -1,5 +1,5 @@
-use core::error::Error;
 use core::str::Chars;
+use core::{error::Error, time::Duration};
 
 use alloc::boxed::Box;
 use alloc::collections::{BTreeMap, BTreeSet, LinkedList};
@@ -1305,3 +1305,77 @@ where
         Ok(Box::new(DeJson::de_json(s, i)?))
     }
 }
+
+impl SerJson for Duration {
+    fn ser_json(&self, _d: usize, s: &mut SerJsonState) {
+        s.out.push('{');
+        s.out.push_str(&format!("\"secs\":{}", self.as_secs()));
+        if self.subsec_nanos() > 0 {
+            s.out
+                .push_str(&format!(",\"nanos\":{}", self.subsec_nanos()));
+        }
+        s.out.push('}');
+    }
+}
+
+impl DeJson for Duration {
+    fn de_json(s: &mut DeJsonState, i: &mut Chars) -> Result<Duration, DeJsonErr> {
+        let mut sec = None;
+        let mut nanos = None;
+        s.curly_open(i)?;
+        while s.tok != DeJsonTok::CurlyClose {
+            let k: String = DeJson::de_json(s, i)?;
+            match k.as_str() {
+                "secs" => {
+                    if sec.is_some() {
+                        return Err(s.err_parse("Duration can only have one secs field"));
+                    }
+                    s.colon(i)?;
+                    sec = Some(DeJson::de_json(s, i)?);
+                }
+                "nanos" => {
+                    if nanos.is_some() {
+                        return Err(s.err_parse("Duration can only have one nanos field"));
+                    }
+                    s.colon(i)?;
+                    nanos = Some(DeJson::de_json(s, i)?);
+                }
+                _ => {
+                    return Err(s.err_parse(&format!(
+                        "Duration can only have secs or nanos fields, got {k}"
+                    )));
+                }
+            }
+            s.eat_comma_curly(i)?;
+        }
+        s.curly_close(i)?;
+
+        let secs = sec.ok_or_else(|| s.err_parse("Duration must have secs field"))?;
+        let nanos = nanos.unwrap_or(0);
+
+        if nanos > 1_000_000_000 {
+            Err(s.err_range("Duration nanos must be at most 1,000,000,000"))
+        } else {
+            Ok(Duration::new(secs, nanos))
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl SerJson for std::time::SystemTime {
+    fn ser_json(&self, d: usize, s: &mut SerJsonState) {
+        self.duration_since(std::time::SystemTime::UNIX_EPOCH)
+            .ok()
+            .ser_json(d, s);
+    }
+}
+
+#[cfg(feature = "std")]
+impl DeJson for std::time::SystemTime {
+    fn de_json(s: &mut DeJsonState, i: &mut Chars) -> Result<std::time::SystemTime, DeJsonErr> {
+        match DeJson::de_json(s, i)? {
+            Some(dur) => Ok(std::time::SystemTime::UNIX_EPOCH + dur),
+            None => Ok(std::time::SystemTime::UNIX_EPOCH),
+        }
+    }
+}
diff --git a/src/serde_ron.rs b/src/serde_ron.rs
index 994313e..d04df66 100644
--- a/src/serde_ron.rs
+++ b/src/serde_ron.rs
@@ -1,5 +1,5 @@
-use core::error::Error;
 use core::str::Chars;
+use core::{error::Error, time::Duration};
 
 use alloc::boxed::Box;
 use alloc::collections::{BTreeMap, BTreeSet, LinkedList};
@@ -1303,3 +1303,77 @@ where
         Ok(Box::new(DeRon::de_ron(s, i)?))
     }
 }
+
+impl SerRon for Duration {
+    fn ser_ron(&self, _d: usize, s: &mut SerRonState) {
+        s.out.push('{');
+        s.out.push_str(&format!("\"secs\":{}", self.as_secs()));
+        if self.subsec_nanos() > 0 {
+            s.out
+                .push_str(&format!(",\"nanos\":{}", self.subsec_nanos()));
+        }
+        s.out.push('}');
+    }
+}
+
+impl DeRon for Duration {
+    fn de_ron(s: &mut DeRonState, i: &mut Chars) -> Result<Duration, DeRonErr> {
+        let mut sec = None;
+        let mut nanos = None;
+        s.curly_open(i)?;
+        while s.tok != DeRonTok::CurlyClose {
+            let k: String = DeRon::de_ron(s, i)?;
+            match k.as_str() {
+                "secs" => {
+                    if sec.is_some() {
+                        return Err(s.err_parse("Duration can only have one secs field"));
+                    }
+                    s.colon(i)?;
+                    sec = Some(DeRon::de_ron(s, i)?);
+                }
+                "nanos" => {
+                    if nanos.is_some() {
+                        return Err(s.err_parse("Duration can only have one nanos field"));
+                    }
+                    s.colon(i)?;
+                    nanos = Some(DeRon::de_ron(s, i)?);
+                }
+                _ => {
+                    return Err(s.err_parse(&format!(
+                        "Duration can only have secs or nanos fields, got {k}"
+                    )));
+                }
+            }
+            s.eat_comma_curly(i)?;
+        }
+        s.curly_close(i)?;
+
+        let secs = sec.ok_or_else(|| s.err_parse("Duration must have secs field"))?;
+        let nanos = nanos.unwrap_or(0);
+
+        if nanos > 1_000_000_000 {
+            Err(s.err_range("Duration nanos must be at most 1,000,000,000"))
+        } else {
+            Ok(Duration::new(secs, nanos))
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl SerRon for std::time::SystemTime {
+    fn ser_ron(&self, d: usize, s: &mut SerRonState) {
+        self.duration_since(std::time::SystemTime::UNIX_EPOCH)
+            .ok()
+            .ser_ron(d, s);
+    }
+}
+
+#[cfg(feature = "std")]
+impl DeRon for std::time::SystemTime {
+    fn de_ron(s: &mut DeRonState, i: &mut Chars) -> Result<std::time::SystemTime, DeRonErr> {
+        match DeRon::de_ron(s, i)? {
+            Some(dur) => Ok(std::time::SystemTime::UNIX_EPOCH + dur),
+            None => Ok(std::time::SystemTime::UNIX_EPOCH),
+        }
+    }
+}


Test patch:
diff --git a/tests/bin.rs b/tests/bin.rs
index fce03c4..b7f0a14 100644
--- a/tests/bin.rs
+++ b/tests/bin.rs
@@ -324,3 +324,85 @@ fn binary_crate() {
 
     assert!(test == test_deserialized);
 }
+
+#[cfg(feature = "std")]
+#[test]
+fn std_time() {
+    use std::time::{Duration, SystemTime, UNIX_EPOCH};
+
+    // Deserialize a known value
+    let known_bytes = [
+        42, 0, 0, 0, 0, 0, 0, 0, // seconds
+        123, 0, 0, 0, // nanoseconds
+    ];
+    let deserialized: Duration = DeBin::deserialize_bin(&known_bytes).unwrap();
+    assert_eq!(deserialized, Duration::new(42, 123));
+
+    // Duration round trip
+    let durations = [
+        Duration::new(0, 0),
+        Duration::new(42, 123_456_789),
+        Duration::new(u64::MAX, 999_999_999),
+    ];
+    for dur in durations {
+        let bytes = SerBin::serialize_bin(&dur);
+        let deserialized: Duration = DeBin::deserialize_bin(&bytes).unwrap();
+        assert_eq!(dur, deserialized);
+    }
+
+    // Not enough data
+    assert!(Duration::deserialize_bin(&[1, 2, 3]).is_err()); // Truncated
+
+    // Nanos too large
+    let invalid_nanos = {
+        let mut b = Vec::new();
+        42u64.ser_bin(&mut b);
+        1_000_000_001u32.ser_bin(&mut b);
+        b
+    };
+    assert!(Duration::deserialize_bin(&invalid_nanos).is_err()); // Invalid nanos
+
+    // SystemTime round trip
+    let times = [
+        UNIX_EPOCH,
+        UNIX_EPOCH + Duration::new(42, 0),
+        UNIX_EPOCH + Duration::new(1_640_995_200, 500_000_000),
+    ];
+    for time in times {
+        let bytes = SerBin::serialize_bin(&time);
+        let deserialized: SystemTime = DeBin::deserialize_bin(&bytes).unwrap();
+        assert_eq!(time, deserialized);
+    }
+
+    // Empty and not enough data
+    assert!(SystemTime::deserialize_bin(&[]).is_err());
+    assert!(SystemTime::deserialize_bin(&[1, 2, 3]).is_err());
+    let none_bytes = {
+        let mut b = Vec::new();
+        0u8.ser_bin(&mut b);
+        b
+    };
+    assert_eq!(
+        SystemTime::deserialize_bin(&none_bytes).unwrap(),
+        UNIX_EPOCH
+    );
+
+    // Combined round trip
+    #[derive(DeBin, SerBin, PartialEq, Debug)]
+    struct Test {
+        duration: Duration,
+        system_time: SystemTime,
+    }
+
+    let test = Test {
+        duration: Duration::new(42, 0),
+        system_time: UNIX_EPOCH + Duration::new(42, 0),
+    };
+    let bytes = SerBin::serialize_bin(&test);
+    let deserialized = DeBin::deserialize_bin(&bytes).unwrap();
+    assert_eq!(test, deserialized);
+
+    let bytes = SerBin::serialize_bin(&None::<SystemTime>);
+    let deserialized: SystemTime = DeBin::deserialize_bin(&bytes).unwrap();
+    assert_eq!(deserialized, UNIX_EPOCH);
+}
diff --git a/tests/json.rs b/tests/json.rs
index 5e669f3..a99c5fa 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -1200,3 +1200,67 @@ fn json_crate() {
     assert_eq!(test.d.unwrap(), "hello");
     assert_eq!(test.c, None);
 }
+
+#[cfg(feature = "std")]
+#[test]
+fn std_time() {
+    use std::time::{Duration, SystemTime};
+
+    // Deserialize known good
+    let duration_json = r#"{ "secs": 42, "nanos": 123456789 }"#;
+    let duration: Duration = DeJson::deserialize_json(duration_json).unwrap();
+    assert_eq!(duration, Duration::new(42, 123_456_789));
+
+    // Duration round trip
+    let durations = [
+        Duration::new(0, 0),
+        Duration::new(42, 123_456_789),
+        Duration::new(u64::MAX, 999_999_999),
+    ];
+    for dur in durations {
+        let serialized = SerJson::serialize_json(&dur);
+        let deserialized: Duration = DeJson::deserialize_json(&serialized).unwrap();
+        assert_eq!(dur, deserialized);
+    }
+
+    // Duration error cases
+    assert!(Duration::deserialize_json(r#""invalid""#).is_err());
+    assert!(Duration::deserialize_json(r#"{ "secs": 1000, "nanos": 1000000001 }"#).is_err()); // Nanos = 1B (invalid)
+    assert!(Duration::deserialize_json(r#""""#).is_err()); // Empty string
+
+    // SystemTime round trip
+    let times = [
+        SystemTime::UNIX_EPOCH,
+        SystemTime::UNIX_EPOCH + Duration::new(42, 0),
+        SystemTime::UNIX_EPOCH + Duration::new(1_640_995_200, 500_000_000),
+    ];
+    for time in times {
+        let serialized = SerJson::serialize_json(&time);
+        let deserialized: SystemTime = DeJson::deserialize_json(&serialized).unwrap();
+        assert_eq!(time, deserialized);
+    }
+
+    // SystemTime error cases
+    assert!(SystemTime::deserialize_json(r#""invalid""#).is_err());
+    assert!(SystemTime::deserialize_json(r#""""#).is_err()); // Empty string
+
+    // Combined struct test
+    #[derive(DeJson, SerJson, PartialEq, Debug)]
+    pub struct Test {
+        pub duration: Duration,
+        pub system_time: SystemTime,
+    }
+
+    let test = Test {
+        duration: Duration::new(1000, 999_999_999),
+        system_time: SystemTime::UNIX_EPOCH + Duration::new(1633072800, 500_000_000),
+    };
+    let serialized = SerJson::serialize_json(&test);
+    let deserialized = DeJson::deserialize_json(&serialized).unwrap();
+    assert_eq!(test, deserialized);
+
+    // Test nil for SystemTime
+    let none = r#"null"#;
+    let deserialized_none: SystemTime = DeJson::deserialize_json(none).unwrap();
+    assert_eq!(deserialized_none, SystemTime::UNIX_EPOCH);
+}
diff --git a/tests/ron.rs b/tests/ron.rs
index 395ba27..146e58a 100644
--- a/tests/ron.rs
+++ b/tests/ron.rs
@@ -709,3 +709,62 @@ fn no_whitespace_when_serialized() {
         assert!(no_whitespace);
     }
 }
+
+#[cfg(feature = "std")]
+#[test]
+fn std_time() {
+    use std::time::{Duration, SystemTime};
+
+    // Duration round trip
+    let durations = [
+        Duration::new(0, 0),
+        Duration::new(42, 123_456_789),
+        Duration::new(u64::MAX, 999_999_999),
+    ];
+    for dur in durations {
+        let serialized = SerRon::serialize_ron(&dur);
+        let deserialized: Duration = DeRon::deserialize_ron(&serialized).unwrap();
+        assert_eq!(dur, deserialized);
+    }
+
+    // Duration error cases
+    assert!(Duration::deserialize_ron(r#""invalid""#).is_err());
+    assert!(Duration::deserialize_ron(r#"(secs: 1000, nanos: 1000000001)"#).is_err()); // Nanos = 1B (invalid)
+    assert!(Duration::deserialize_ron(r#""""#).is_err()); // Empty string
+
+    // SystemTime round trip
+    let times = [
+        SystemTime::UNIX_EPOCH,
+        SystemTime::UNIX_EPOCH + Duration::new(42, 0),
+        SystemTime::UNIX_EPOCH + Duration::new(1_640_995_200, 500_000_000),
+    ];
+    for time in times {
+        let serialized = SerRon::serialize_ron(&time);
+        let deserialized: SystemTime = DeRon::deserialize_ron(&serialized).unwrap();
+        assert_eq!(time, deserialized);
+    }
+
+    // SystemTime error cases
+    assert!(SystemTime::deserialize_ron(r#""invalid""#).is_err());
+    assert!(SystemTime::deserialize_ron(r#""""#).is_err()); // Empty string
+
+    // Combined struct test
+    #[derive(DeRon, SerRon, PartialEq, Debug)]
+    pub struct Test {
+        pub duration: Duration,
+        pub system_time: SystemTime,
+    }
+
+    let test = Test {
+        duration: Duration::new(1000, 999_999_999),
+        system_time: SystemTime::UNIX_EPOCH + Duration::new(1633072800, 500_000_000),
+    };
+    let serialized = SerRon::serialize_ron(&test);
+    let deserialized = DeRon::deserialize_ron(&serialized).unwrap();
+    assert_eq!(test, deserialized);
+
+    // Deserialize none test for SystemTime
+    let none = r#"None"#;
+    let deserialized_none: SystemTime = DeRon::deserialize_ron(none).unwrap();
+    assert_eq!(deserialized_none, SystemTime::UNIX_EPOCH);
+}