Skip to main content

expiration_date/
serde_impl.rs

1//! Hand-written serde impls for [`ExpirationDate`].
2//!
3//! The wire shape is a single-entry map: `{"days": <f64>}` or
4//! `{"datetime": "<RFC3339>"}`. That shape is a semver contract — changing
5//! the tag or payload layout is a breaking change to every persisted value.
6
7use crate::ExpirationDate;
8use chrono::{DateTime, Utc};
9use positive::Positive;
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11use std::fmt;
12
13impl Serialize for ExpirationDate {
14    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
15    where
16        S: Serializer,
17    {
18        use serde::ser::SerializeMap;
19        let mut state = serializer.serialize_map(Some(1))?;
20        match self {
21            Self::Days(days) => state.serialize_entry("days", &days.to_f64())?,
22            Self::DateTime(dt) => {
23                state.serialize_entry("datetime", &dt.format("%Y-%m-%dT%H:%M:%SZ").to_string())?
24            }
25        }
26        state.end()
27    }
28}
29
30impl<'de> Deserialize<'de> for ExpirationDate {
31    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
32    where
33        D: Deserializer<'de>,
34    {
35        use serde::de::{MapAccess, Visitor};
36        struct ExVisitor;
37        impl<'de> Visitor<'de> for ExVisitor {
38            type Value = ExpirationDate;
39            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
40                f.write_str("struct ExpirationDate")
41            }
42            fn visit_map<V>(self, mut map: V) -> Result<ExpirationDate, V::Error>
43            where
44                V: MapAccess<'de>,
45            {
46                let mut d = None;
47                let mut t = None;
48                while let Some(k) = map.next_key::<String>()? {
49                    match k.as_str() {
50                        "days" => {
51                            if d.is_some() {
52                                return Err(serde::de::Error::duplicate_field("days"));
53                            }
54                            d = Some(map.next_value::<f64>()?);
55                        }
56                        "datetime" => {
57                            if t.is_some() {
58                                return Err(serde::de::Error::duplicate_field("datetime"));
59                            }
60                            t = Some(map.next_value::<String>()?);
61                        }
62                        _ => {
63                            return Err(serde::de::Error::unknown_field(&k, &["days", "datetime"]));
64                        }
65                    }
66                }
67                match (d, t) {
68                    (Some(v), _) => Ok(ExpirationDate::Days(
69                        Positive::new(v).map_err(serde::de::Error::custom)?,
70                    )),
71                    (_, Some(v)) => Ok(ExpirationDate::DateTime(
72                        DateTime::parse_from_rfc3339(&v)
73                            .map_err(serde::de::Error::custom)?
74                            .with_timezone(&Utc),
75                    )),
76                    _ => Err(serde::de::Error::missing_field("days or datetime")),
77                }
78            }
79        }
80        deserializer.deserialize_struct("ExpirationDate", &["days", "datetime"], ExVisitor)
81    }
82}