glrcfg 0.2.4

A Rust implementation of the GitLab Runner Advanced Configuration file format
Documentation
// Copyright 2024 bmc::labs GmbH. All rights reserved.

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

use serde::{Deserialize, Serialize};

/// A datetime type that serializes to and from ISO8601 strings using Zulu timezone, i.e. with no
/// offset and the letter `Z` instead of an offset. Based on [`chrono::DateTime<chrono::Utc>`].
/// Used as timestamp for the `token_obtained_at` and `token_expires_at` fields in
/// [`Runner`](crate::Runner).
///
/// # Example
///
/// ```rust
/// # use glrcfg::runner::DateTime;
/// let iso8601 = "2023-08-23T23:23:23Z";
/// assert_eq!(iso8601, DateTime::parse(iso8601).unwrap().to_iso8601());
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DateTime(chrono::DateTime<chrono::Utc>);

impl DateTime {
    /// Returns the current date and time in UTC.
    pub fn now() -> Self {
        Self(chrono::Utc::now())
    }

    /// Parses a datetime from an `Into<String>`, e.g. a `&str` or `String`.
    pub fn parse<S>(iso8601: S) -> Result<Self, chrono::ParseError>
    where
        S: Into<String>,
    {
        Ok(Self(
            chrono::DateTime::parse_from_rfc3339(&iso8601.into())?.with_timezone(&chrono::Utc),
        ))
    }

    /// Returns the datetime as a string slice.
    pub fn to_iso8601(&self) -> String {
        self.0.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
    }
}

impl fmt::Display for DateTime {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.to_iso8601())
    }
}

impl FromStr for DateTime {
    type Err = chrono::ParseError;

    fn from_str(iso8601: &str) -> Result<Self, Self::Err> {
        Self::parse(iso8601)
    }
}

impl Serialize for DateTime {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        self.to_iso8601().serialize(serializer)
    }
}

impl<'a> Deserialize<'a> for DateTime {
    fn deserialize<D>(deserializer: D) -> Result<DateTime, D::Error>
    where
        D: serde::Deserializer<'a>,
    {
        let date_time = String::deserialize(deserializer)?;
        DateTime::parse(date_time).map_err(serde::de::Error::custom)
    }
}

#[cfg(feature = "sqlx")]
impl<DB> sqlx::Type<DB> for DateTime
where
    DB: sqlx::Database,
    String: sqlx::Type<DB>,
{
    fn type_info() -> DB::TypeInfo {
        <String as sqlx::Type<DB>>::type_info()
    }

    fn compatible(ty: &DB::TypeInfo) -> bool {
        <String as sqlx::Type<DB>>::compatible(ty)
    }
}

#[cfg(feature = "sqlx")]
impl<'a, DB> sqlx::Encode<'a, DB> for DateTime
where
    DB: sqlx::Database,
    String: sqlx::Encode<'a, DB>,
{
    fn encode_by_ref(
        &self,
        buf: &mut <DB as sqlx::database::HasArguments<'a>>::ArgumentBuffer,
    ) -> sqlx::encode::IsNull {
        self.to_iso8601().encode_by_ref(buf)
    }
}

#[cfg(feature = "sqlx")]
impl<'a, DB> sqlx::Decode<'a, DB> for DateTime
where
    DB: sqlx::Database,
    String: sqlx::Decode<'a, DB>,
{
    fn decode(
        value: <DB as sqlx::database::HasValueRef<'a>>::ValueRef,
    ) -> Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
        let value = <String as sqlx::Decode<DB>>::decode(value)?;
        Ok(DateTime::parse(value)?)
    }
}