ockam_api 0.93.0

Ockam's request-response API
use crate::colors::{color_error, color_ok, color_primary};
use crate::output::Output;
use crate::terminal::fmt;
use crate::ApiError;
use minicbor::{CborLen, Decode, Encode};
use ockam::identity::Identifier;
use ockam::Message;
use ockam_core::compat::fmt::Error as FmtError;
use ockam_core::{cbor_encode_preallocate, Decodable, Encodable, Encoded};
use serde::{Deserialize, Serialize};
use std::cmp::PartialEq;
use std::fmt::{Display, Formatter};
use strum::{Display, EnumString};
use time::OffsetDateTime;

#[derive(Encode, Decode, CborLen, Serialize, Deserialize, Debug, Clone, Message)]
#[cbor(map)]
pub struct LeaseToken {
    #[cbor(n(1))]
    pub id: String,

    #[cbor(n(2))]
    pub issued_for: Identifier,

    #[cbor(n(3))]
    pub created_at: i64,

    #[cbor(n(4))]
    pub expires_at: i64,

    #[cbor(n(5))]
    pub token: String,

    #[cbor(n(6))]
    pub status: TokenStatus,
}

impl Encodable for LeaseToken {
    fn encode(self) -> ockam_core::Result<Encoded> {
        cbor_encode_preallocate(self)
    }
}

impl Decodable for LeaseToken {
    fn decode(e: &[u8]) -> ockam_core::Result<Self> {
        Ok(minicbor::decode(e)?)
    }
}

#[derive(Encode, Decode, CborLen, Serialize, Deserialize, Debug, Clone, Message)]
#[cbor(transparent)]
pub struct LeaseTokenList(#[n(0)] pub Vec<LeaseToken>);

impl Encodable for LeaseTokenList {
    fn encode(self) -> ockam_core::Result<Encoded> {
        cbor_encode_preallocate(self)
    }
}

impl Decodable for LeaseTokenList {
    fn decode(e: &[u8]) -> ockam_core::Result<Self> {
        Ok(LeaseTokenList(minicbor::decode(e)?))
    }
}

#[cfg(test)]
impl Default for LeaseToken {
    fn default() -> Self {
        use std::str::FromStr;
        Self {
            id: "token_id".to_string(),
            issued_for: Identifier::from_str(
                "I0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
            )
            .unwrap(),
            created_at: OffsetDateTime::now_utc().unix_timestamp(),
            expires_at: OffsetDateTime::now_utc().unix_timestamp(),
            token: "token".to_string(),
            status: TokenStatus::Active,
        }
    }
}

impl LeaseToken {
    pub fn is_active(&self) -> bool {
        self.status == TokenStatus::Active
    }

    pub fn created_at(&self) -> ockam_core::Result<OffsetDateTime> {
        OffsetDateTime::from_unix_timestamp(self.created_at)
            .map_err(|e| ApiError::core(e.to_string()))
    }

    pub fn expires_at(&self) -> ockam_core::Result<OffsetDateTime> {
        OffsetDateTime::from_unix_timestamp(self.expires_at)
            .map_err(|e| ApiError::core(e.to_string()))
    }

    pub fn is_expired(&self) -> ockam_core::Result<bool> {
        Ok(self.expires_at()? < OffsetDateTime::now_utc())
    }
}

impl Display for LeaseToken {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "{}", color_primary(&self.id))?;

        writeln!(
            f,
            "{}With value {}",
            fmt::INDENTATION,
            color_primary(&self.token)
        )?;

        writeln!(
            f,
            "{}Issued for {}",
            fmt::INDENTATION,
            color_primary(&self.issued_for)
        )?;

        let created_at = self.created_at().map_err(|_| FmtError)?.to_string();
        writeln!(
            f,
            "{}Created at {}",
            fmt::INDENTATION,
            color_primary(created_at)
        )?;

        let status = if self.is_active() {
            color_ok(&self.status)
        } else {
            color_error(&self.status)
        };
        let expires_at = self.expires_at().map_err(|_| FmtError)?.to_string();
        let expiration_time = if self.is_expired().map_err(|_| FmtError)? {
            format!("Expired at {}", color_error(&expires_at))
        } else {
            format!("Expires at {}", color_primary(&expires_at))
        };
        writeln!(f, "{}{expiration_time} ({status})", fmt::INDENTATION)?;

        Ok(())
    }
}

impl Output for LeaseToken {
    fn item(&self) -> crate::Result<String> {
        Ok(self.padded_display())
    }
}

impl Ord for LeaseToken {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.expires_at.cmp(&other.expires_at)
    }
}

impl PartialOrd for LeaseToken {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl PartialEq for LeaseToken {
    fn eq(&self, other: &Self) -> bool {
        self.expires_at == other.expires_at
    }
}

impl Eq for LeaseToken {}

#[derive(
    Encode, Decode, CborLen, Serialize, Deserialize, PartialEq, Debug, Clone, EnumString, Display,
)]
#[cbor(index_only)]
pub enum TokenStatus {
    #[n(0)]
    #[strum(serialize = "active")]
    Active,

    #[n(1)]
    #[strum(serialize = "inactive")]
    Revoked,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn lease_token_display() {
        let token = LeaseToken {
            created_at: OffsetDateTime::now_utc().unix_timestamp(),
            expires_at: OffsetDateTime::now_utc().unix_timestamp(),
            ..Default::default()
        };
        assert!(token.expires_at().is_ok());
        assert!(token.is_expired().is_ok());
        assert!(token.item().is_ok());
    }

    #[test]
    fn token_lease_is_expired() {
        let mut token = LeaseToken {
            expires_at: OffsetDateTime::now_utc().unix_timestamp() - 100,
            ..Default::default()
        };
        assert!(token.is_expired().unwrap());

        token.expires_at = OffsetDateTime::now_utc().unix_timestamp() + 100;
        assert!(!token.is_expired().unwrap());
    }

    #[test]
    fn token_lease_ordering() {
        let token1 = LeaseToken {
            expires_at: OffsetDateTime::now_utc().unix_timestamp() + 100,
            ..Default::default()
        };
        let token2 = LeaseToken {
            expires_at: OffsetDateTime::now_utc().unix_timestamp() + 200,
            ..Default::default()
        };
        // token1 expires before token2
        assert!(token1 < token2);
    }
}