oparl-types 0.8.4

Type definitions for the OParl protocol
Documentation
// SPDX-FileCopyrightText: Politik im Blick developers
// SPDX-FileCopyrightText: Wolfgang Silbermayr <wolfgang@silbermayr.at>
//
// SPDX-License-Identifier: AGPL-3.0-or-later OR EUPL-1.2

use std::{fmt::Display, str::FromStr};

use time::OffsetDateTime;

use crate::Date;

#[derive(
    Debug,
    Clone,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    derive_more::AsRef,
    derive_more::From,
    derive_more::Into,
    serde_with::SerializeDisplay,
    serde_with::DeserializeFromStr,
)]
#[cfg_attr(feature = "sea-orm", derive(sea_orm::DeriveValueType))]
pub struct DateTime(OffsetDateTime);

impl DateTime {
    pub fn now() -> Self {
        Self(OffsetDateTime::now_utc())
    }

    pub fn date(self) -> Date {
        Date::from(self)
    }
}

impl FromStr for DateTime {
    type Err = time::error::Parse;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use time::format_description::well_known::Iso8601;

        Ok(Self(OffsetDateTime::parse(s, &Iso8601::DEFAULT)?))
    }
}

impl Display for DateTime {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        use time::format_description::well_known::{
            Iso8601,
            iso8601::{Config, EncodedConfig},
        };

        const CONFIG: EncodedConfig = Config::DEFAULT
            .set_time_precision(
                time::format_description::well_known::iso8601::TimePrecision::Second {
                    decimal_digits: None,
                },
            )
            .encode();

        write!(
            f,
            "{}",
            self.0.format(&Iso8601::<CONFIG>).expect("valid date")
        )
    }
}

#[cfg(feature = "sea-orm")]
mod sea_orm_impls {
    use sea_orm::{ActiveValue, IntoActiveValue};

    use super::DateTime;

    impl IntoActiveValue<DateTime> for DateTime {
        fn into_active_value(self) -> ActiveValue<DateTime> {
            ActiveValue::Set(self)
        }
    }
}

#[cfg(test)]
mod tests {
    use time::macros::datetime;

    use super::DateTime;

    #[test]
    fn from_str() {
        let parsed: DateTime = "2025-01-07T05:06:07+01:00"
            .parse()
            .expect("string must be parseable as DateTime");
        assert_eq!(parsed, DateTime(datetime!(2025-01-07 05:06:07 +01:00)));
    }

    #[test]
    fn to_string() {
        let dt = DateTime(datetime!(2025-01-07 05:06:07 +01:00));

        assert_eq!(dt.to_string(), "2025-01-07T05:06:07+01:00".to_string());
    }
}

#[cfg(test)]
mod serde_tests {
    use pretty_assertions::assert_eq;
    use serde_json::json;
    use time::macros::datetime;

    use super::DateTime;

    #[test]
    fn serialize() {
        assert_eq!(
            json!(DateTime(datetime!(2025-01-07 05:06:07 +01:00))),
            json!("2025-01-07T05:06:07+01:00")
        );
    }

    #[test]
    fn deserialize_good() {
        let deserialized: DateTime = serde_json::from_value(json!("2025-01-07T05:06:07+01:00"))
            .expect("json must be parseable as DateTime");

        assert_eq!(
            deserialized,
            DateTime(datetime!(2025-01-07 05:06:07 +01:00))
        );
    }

    #[test]
    fn deserialize_bad() {
        assert!(serde_json::from_value::<DateTime>(json!("xyzabcd")).is_err());
        assert!(serde_json::from_value::<DateTime>(json!(true)).is_err());
        assert!(serde_json::from_value::<DateTime>(json!(123)).is_err());
    }
}