Repository: toml-rs/toml (language: rust)

Issue:
Implement `Serialize` and `Deserialize` for `Date` and `Time`
The [`v1.0.0` TOML spec][toml-spec] allows omission of the time or date portion of a [local date-time][toml-spec-local-date-time] to represent a [local date][toml-spec-local-date] or a [local time][toml-spec-local-time] respectively. However, there is currently no implementation for `Serialize` or `Deserialize` for [`Date`][toml-rs-date] or [`Time`][toml-rs-time] in the `toml` crate. While it is possible for consumers of `toml` to use [`Datetime`][toml-rs-datetime], it would be better to be able to use `Date`/`Time` directly to more clearly communicate intent, prevent mistakes in making runtime assertions, and avoid the need to manually unwrap the `date`/`time` `Option`s.

**NOTE:** I am not suggesting we interpret a local time as a duration as suggested in #696. [RFC 3339][rfc-3339-1] explicitly does not cover time intervals.

[rfc-3339-1]: https://datatracker.ietf.org/doc/html/rfc3339#section-1
[toml-spec]: https://toml.io/en/v1.0.0
[toml-spec-local-date]: https://toml.io/en/v1.0.0#local-date
[toml-spec-local-time]: https://toml.io/en/v1.0.0#local-time
[toml-spec-local-date-time]: https://toml.io/en/v1.0.0#local-date-time
[toml-rs-date]: https://docs.rs/toml/0.8.16/toml/value/struct.Date.html
[toml-rs-time]: https://docs.rs/toml/0.8.16/toml/value/struct.Time.html
[toml-rs-datetime]: https://docs.rs/toml/0.8.16/toml/value/struct.Datetime.html


Discussion / hints:
I am fine with someone implementing this.  I would expect the impl's to delegate to `Datetime` and then error if an unexpected field is set so we centralize all knowledge of the serde hacks we use to deserialize this time (and ensure full compatibility between them)

Fix patch:
diff --git a/crates/toml_datetime/src/datetime.rs b/crates/toml_datetime/src/datetime.rs
index e76b3b94..cea13b9f 100644
--- a/crates/toml_datetime/src/datetime.rs
+++ b/crates/toml_datetime/src/datetime.rs
@@ -184,6 +184,37 @@ pub enum Offset {
     },
 }
 
+impl Datetime {
+    #[cfg(feature = "serde")]
+    fn type_name(&self) -> &'static str {
+        match (
+            self.date.is_some(),
+            self.time.is_some(),
+            self.offset.is_some(),
+        ) {
+            (true, true, true) => "offset datetime",
+            (true, true, false) => "local datetime",
+            (true, false, false) => Date::type_name(),
+            (false, true, false) => Time::type_name(),
+            _ => unreachable!("unsupported datetime combination"),
+        }
+    }
+}
+
+impl Date {
+    #[cfg(feature = "serde")]
+    fn type_name() -> &'static str {
+        "local date"
+    }
+}
+
+impl Time {
+    #[cfg(feature = "serde")]
+    fn type_name() -> &'static str {
+        "local time"
+    }
+}
+
 impl From<Date> for Datetime {
     fn from(other: Date) -> Self {
         Datetime {
@@ -480,6 +511,26 @@ impl ser::Serialize for Datetime {
     }
 }
 
+#[cfg(feature = "serde")]
+impl ser::Serialize for Date {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: ser::Serializer,
+    {
+        Datetime::from(*self).serialize(serializer)
+    }
+}
+
+#[cfg(feature = "serde")]
+impl ser::Serialize for Time {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: ser::Serializer,
+    {
+        Datetime::from(*self).serialize(serializer)
+    }
+}
+
 #[cfg(feature = "serde")]
 impl<'de> de::Deserialize<'de> for Datetime {
     fn deserialize<D>(deserializer: D) -> Result<Datetime, D::Error>
@@ -513,6 +564,46 @@ impl<'de> de::Deserialize<'de> for Datetime {
     }
 }
 
+#[cfg(feature = "serde")]
+impl<'de> de::Deserialize<'de> for Date {
+    fn deserialize<D>(deserializer: D) -> Result<Date, D::Error>
+    where
+        D: de::Deserializer<'de>,
+    {
+        match Datetime::deserialize(deserializer)? {
+            Datetime {
+                date: Some(date),
+                time: None,
+                offset: None,
+            } => Ok(date),
+            datetime => Err(de::Error::invalid_type(
+                de::Unexpected::Other(datetime.type_name()),
+                &Self::type_name(),
+            )),
+        }
+    }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> de::Deserialize<'de> for Time {
+    fn deserialize<D>(deserializer: D) -> Result<Time, D::Error>
+    where
+        D: de::Deserializer<'de>,
+    {
+        match Datetime::deserialize(deserializer)? {
+            Datetime {
+                date: None,
+                time: Some(time),
+                offset: None,
+            } => Ok(time),
+            datetime => Err(de::Error::invalid_type(
+                de::Unexpected::Other(datetime.type_name()),
+                &Self::type_name(),
+            )),
+        }
+    }
+}
+
 #[cfg(feature = "serde")]
 struct DatetimeKey;
 


Test patch:
diff --git a/crates/toml/tests/testsuite/serde.rs b/crates/toml/tests/testsuite/serde.rs
index c3a10eb7..a2a63cd6 100644
--- a/crates/toml/tests/testsuite/serde.rs
+++ b/crates/toml/tests/testsuite/serde.rs
@@ -1270,6 +1270,114 @@ fn datetime_offset_issue_496() {
     assert_data_eq!(output, original.raw());
 }
 
+#[test]
+fn serialize_date() {
+    use toml::value::Date;
+
+    #[derive(Serialize)]
+    struct Document {
+        date: Date,
+    }
+
+    let input = Document {
+        date: Date {
+            year: 2024,
+            month: 1,
+            day: 1,
+        },
+    };
+    let raw = toml::to_string(&input).unwrap();
+    assert_data_eq!(
+        raw,
+        str![[r#"
+date = 2024-01-01
+
+"#]]
+        .raw()
+    );
+}
+
+#[test]
+fn serialize_time() {
+    use toml::value::Time;
+
+    #[derive(Serialize)]
+    struct Document {
+        date: Time,
+    }
+
+    let input = Document {
+        date: Time {
+            hour: 5,
+            minute: 0,
+            second: 0,
+            nanosecond: 0,
+        },
+    };
+    let raw = toml::to_string(&input).unwrap();
+    assert_data_eq!(
+        raw,
+        str![[r#"
+date = 05:00:00
+
+"#]]
+        .raw()
+    );
+}
+
+#[test]
+fn deserialize_date() {
+    use toml::value::Date;
+
+    #[derive(Debug, Deserialize)]
+    struct Document {
+        date: Date,
+    }
+
+    let document: Document = toml::from_str("date = 2024-01-01").unwrap();
+    assert_eq!(
+        document.date,
+        Date {
+            year: 2024,
+            month: 1,
+            day: 1
+        }
+    );
+
+    let err = toml::from_str::<Document>("date = 2024-01-01T05:00:00").unwrap_err();
+    assert_data_eq!(
+        err.message(),
+        str!["invalid type: local datetime, expected local date"]
+    );
+}
+
+#[test]
+fn deserialize_time() {
+    use toml::value::Time;
+
+    #[derive(Debug, Deserialize)]
+    struct Document {
+        time: Time,
+    }
+
+    let document: Document = toml::from_str("time = 05:00:00").unwrap();
+    assert_eq!(
+        document.time,
+        Time {
+            hour: 5,
+            minute: 0,
+            second: 0,
+            nanosecond: 0,
+        }
+    );
+
+    let err = toml::from_str::<Document>("time = 2024-01-01T05:00:00").unwrap_err();
+    assert_data_eq!(
+        err.message(),
+        str!["invalid type: local datetime, expected local time"]
+    );
+}
+
 #[test]
 fn serialize_array_with_none_value() {
     #[derive(Serialize)]