integrationos-domain 8.0.0

Shared library for IntegrationOS
Documentation
pub mod prefix;

use crate::{id::prefix::IdPrefix, IntegrationOSError, InternalError};
use base64ct::{Base64UrlUnpadded, Encoding};
use chrono::{DateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use std::{
    fmt::{Display, Formatter},
    str::FromStr,
};
use uuid::Uuid;

#[derive(Debug, Copy, Clone, Ord, PartialOrd, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[serde(try_from = "String", into = "String")]
pub struct Id {
    prefix: IdPrefix,
    time: DateTime<Utc>,
    uuid: Uuid,
}

impl Id {
    pub fn new(prefix: IdPrefix, time: DateTime<Utc>) -> Self {
        Self {
            prefix,
            time,
            uuid: Uuid::new_v4(),
        }
    }

    pub fn now(prefix: IdPrefix) -> Self {
        Self {
            prefix,
            time: Utc::now(),
            uuid: Uuid::new_v4(),
        }
    }

    pub fn new_with_uuid(prefix: IdPrefix, time: DateTime<Utc>, uuid: Uuid) -> Self {
        Self { prefix, time, uuid }
    }
}

impl Display for Id {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let Some(ts) = self.time.timestamp_nanos_opt() else {
            return write!(f, "Invalid Id");
        };
        let timestamp = Base64UrlUnpadded::encode_string(ts.to_be_bytes().as_slice());
        let random = Base64UrlUnpadded::encode_string(self.uuid.as_bytes());
        write!(f, "{}::{timestamp}::{random}", self.prefix)
    }
}

impl From<Id> for String {
    fn from(value: Id) -> String {
        value.to_string()
    }
}

impl TryFrom<String> for Id {
    type Error = IntegrationOSError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Id::from_str(value.as_str())
    }
}

impl FromStr for Id {
    type Err = IntegrationOSError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut parts = s.splitn(3, "::");
        let prefix = parts
            .next()
            .ok_or(InternalError::invalid_argument(
                &format!("Invalid ID: {}", s),
                None,
            ))?
            .try_into()?;

        let timestamp = parts.next().ok_or(InternalError::invalid_argument(
            &format!("Invalid ID: {}", s),
            None,
        ))?;
        let mut timestamp_buf = [0u8; 8];
        Base64UrlUnpadded::decode(timestamp.as_bytes(), &mut timestamp_buf)
            .map_err(|e| InternalError::invalid_argument(&format!("Invalid ID: {}", e), None))?;
        let timestamp = i64::from_be_bytes(timestamp_buf);
        let time = Utc.timestamp_nanos(timestamp);

        let uuid = parts.next().ok_or(InternalError::invalid_argument(
            &format!("Invalid ID: {}", s),
            None,
        ))?;
        let mut uuid_buf = [0u8; 16];
        Base64UrlUnpadded::decode(uuid, &mut uuid_buf)
            .map_err(|e| InternalError::invalid_argument(&format!("Invalid ID: {}", e), None))?;

        let uuid = Uuid::from_bytes(uuid_buf);

        Ok(Self { prefix, time, uuid })
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use once_cell::sync::Lazy;

    const ID_STR: &str = "evt::AAAAAAAAAAA::AAAAAAAAAAAAAAAAAAAAAA";
    static PARSED_ID: Lazy<Id> = Lazy::new(|| {
        Id::new_with_uuid(
            IdPrefix::Event,
            Utc.timestamp_opt(0, 0).single().unwrap(),
            Uuid::nil(),
        )
    });

    #[test]
    fn test_id_from_str() {
        let id = Id::from_str(ID_STR).unwrap();
        assert_eq!(id.prefix, PARSED_ID.prefix);
        assert_eq!(id.time, PARSED_ID.time);
        assert_eq!(id.uuid, PARSED_ID.uuid);
    }

    #[test]
    fn test_id_from_str_invalid() {
        assert!(Id::from_str("foo::AAAAAAAAAAA::AAAAAAAAAAAAAAAAAAAAAA").is_err());
        assert!(Id::from_str("evt::AAAAAAAAAAA::AAAAAAAAAAAAAAAAAAAAAAS").is_err());
        assert!(Id::from_str("evt::AAAAAAAAAAAS::AAAAAAAAAAAAAAAAAAAAAA").is_err());
        assert!(Id::from_str("evt::AAAAAAAAAAA::AAAAAAAAAAAAAAAAAAAAAA::").is_err());
    }

    #[test]
    fn test_id_display() {
        assert_eq!(format!("{}", *PARSED_ID), ID_STR);
    }

    #[test]
    fn test_id_event_key() {
        let id_str = "evt_k::AAAAAAAAAAA::AAAAAAAAAAAAAAAAAAAAAA";
        let id = Id::new_with_uuid(
            IdPrefix::EventKey,
            Utc.timestamp_opt(0, 0).single().unwrap(),
            Uuid::nil(),
        );
        assert_eq!(id, Id::from_str(id_str).unwrap());
        assert_eq!(id.to_string(), id_str);
    }

    #[test]
    fn test_id_serde() {
        let id = Id::new_with_uuid(
            IdPrefix::Event,
            Utc.timestamp_opt(0, 0).single().unwrap(),
            Uuid::nil(),
        );
        let id_str = serde_json::to_string(&id).unwrap();
        assert_eq!(id_str, format!("\"{}\"", ID_STR));
        assert_eq!(id, serde_json::from_str(&id_str).unwrap());
    }
}