Skip to main content

oparl_types/
date_time.rs

1// SPDX-FileCopyrightText: Politik im Blick developers
2// SPDX-FileCopyrightText: Wolfgang Silbermayr <wolfgang@silbermayr.at>
3//
4// SPDX-License-Identifier: AGPL-3.0-or-later OR EUPL-1.2
5
6use std::{fmt::Display, str::FromStr};
7
8use time::OffsetDateTime;
9
10use crate::Date;
11
12#[derive(
13    Debug,
14    Clone,
15    PartialEq,
16    Eq,
17    PartialOrd,
18    Ord,
19    Hash,
20    derive_more::AsRef,
21    derive_more::From,
22    derive_more::Into,
23    serde_with::SerializeDisplay,
24    serde_with::DeserializeFromStr,
25)]
26#[cfg_attr(feature = "sea-orm", derive(sea_orm::DeriveValueType))]
27pub struct DateTime(OffsetDateTime);
28
29impl DateTime {
30    pub fn now() -> Self {
31        Self(OffsetDateTime::now_utc())
32    }
33
34    pub fn date(self) -> Date {
35        Date::from(self)
36    }
37}
38
39impl FromStr for DateTime {
40    type Err = time::error::Parse;
41
42    fn from_str(s: &str) -> Result<Self, Self::Err> {
43        use time::format_description::well_known::Iso8601;
44
45        Ok(Self(OffsetDateTime::parse(s, &Iso8601::DEFAULT)?))
46    }
47}
48
49impl Display for DateTime {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        use time::format_description::well_known::{
52            Iso8601,
53            iso8601::{Config, EncodedConfig},
54        };
55
56        const CONFIG: EncodedConfig = Config::DEFAULT
57            .set_time_precision(
58                time::format_description::well_known::iso8601::TimePrecision::Second {
59                    decimal_digits: None,
60                },
61            )
62            .encode();
63
64        write!(
65            f,
66            "{}",
67            self.0.format(&Iso8601::<CONFIG>).expect("valid date")
68        )
69    }
70}
71
72#[cfg(feature = "sea-orm")]
73mod sea_orm_impls {
74    use sea_orm::{ActiveValue, IntoActiveValue};
75
76    use super::DateTime;
77
78    impl IntoActiveValue<DateTime> for DateTime {
79        fn into_active_value(self) -> ActiveValue<DateTime> {
80            ActiveValue::Set(self)
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use time::macros::datetime;
88
89    use super::DateTime;
90
91    #[test]
92    fn from_str() {
93        let parsed: DateTime = "2025-01-07T05:06:07+01:00"
94            .parse()
95            .expect("string must be parseable as DateTime");
96        assert_eq!(parsed, DateTime(datetime!(2025-01-07 05:06:07 +01:00)));
97    }
98
99    #[test]
100    fn to_string() {
101        let dt = DateTime(datetime!(2025-01-07 05:06:07 +01:00));
102
103        assert_eq!(dt.to_string(), "2025-01-07T05:06:07+01:00".to_string());
104    }
105}
106
107#[cfg(test)]
108mod serde_tests {
109    use pretty_assertions::assert_eq;
110    use serde_json::json;
111    use time::macros::datetime;
112
113    use super::DateTime;
114
115    #[test]
116    fn serialize() {
117        assert_eq!(
118            json!(DateTime(datetime!(2025-01-07 05:06:07 +01:00))),
119            json!("2025-01-07T05:06:07+01:00")
120        );
121    }
122
123    #[test]
124    fn deserialize_good() {
125        let deserialized: DateTime = serde_json::from_value(json!("2025-01-07T05:06:07+01:00"))
126            .expect("json must be parseable as DateTime");
127
128        assert_eq!(
129            deserialized,
130            DateTime(datetime!(2025-01-07 05:06:07 +01:00))
131        );
132    }
133
134    #[test]
135    fn deserialize_bad() {
136        assert!(serde_json::from_value::<DateTime>(json!("xyzabcd")).is_err());
137        assert!(serde_json::from_value::<DateTime>(json!(true)).is_err());
138        assert!(serde_json::from_value::<DateTime>(json!(123)).is_err());
139    }
140}