klick-domain 0.4.11

Calculate the carbon footprints for wastewater treatment plants
Documentation
use std::{fmt, str::FromStr, string::FromUtf8Error};

use thiserror::Error;
use time::OffsetDateTime;
use uuid::Uuid;

use crate::authentication::{EmailAddress, EmailAddressParseError};

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Nonce(Uuid);

impl Nonce {
    pub const STR_LEN: usize = 32;

    #[must_use]
    pub fn new() -> Self {
        Self(Uuid::new_v4())
    }
}

#[derive(Debug, Error)]
#[error("invalid nonce")]
pub struct NonceParseError;

impl FromStr for Nonce {
    type Err = NonceParseError;

    fn from_str(nonce_str: &str) -> Result<Self, Self::Err> {
        nonce_str
            .parse::<Uuid>()
            .map(Nonce)
            .map_err(|_| NonceParseError)
    }
}

impl fmt::Display for Nonce {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), std::fmt::Error> {
        write!(f, "{}", self.0.as_simple())
    }
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct EmailNonce {
    pub email: EmailAddress,
    pub nonce: Nonce,
}

pub type ActualTokenLen = usize;

#[derive(Debug, Error)]
pub enum EmailNonceDecodingError {
    #[error(transparent)]
    Bs58(#[from] bs58::decode::Error),
    #[error(transparent)]
    Utf8(#[from] FromUtf8Error),
    #[error("nonce is too short: {0} instead of {}", Nonce::STR_LEN)]
    TooShort(ActualTokenLen),
    #[error(transparent)]
    Parse(#[from] NonceParseError),
    #[error(transparent)]
    EmailAddress(#[from] EmailAddressParseError),
}

impl EmailNonce {
    #[must_use]
    pub fn encode_to_string(&self) -> String {
        let nonce = self.nonce.to_string();
        debug_assert_eq!(Nonce::STR_LEN, nonce.len());
        let mut concat = String::with_capacity(self.email.as_str().len() + nonce.len());
        concat += self.email.as_str();
        concat += &nonce;
        bs58::encode(concat).into_string()
    }

    pub fn decode_from_str(encoded: &str) -> Result<Self, EmailNonceDecodingError> {
        let decoded = bs58::decode(encoded).into_vec()?;
        let mut concat = String::from_utf8(decoded)?;
        if concat.len() < Nonce::STR_LEN {
            return Err(EmailNonceDecodingError::TooShort(concat.len()));
        }
        let email_len = concat.len() - Nonce::STR_LEN;
        let nonce_slice: &str = &concat[email_len..];
        let nonce = nonce_slice.parse::<Nonce>()?;
        concat.truncate(email_len);
        let email = concat.parse()?;
        Ok(Self { email, nonce })
    }
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct AccountToken {
    pub email_nonce: EmailNonce,
    pub expires_at: OffsetDateTime,
}

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

    #[test]
    fn encode_decode_email_nonce() {
        let example = EmailNonce {
            email: "test@example.com".parse().unwrap(),
            nonce: Nonce::new(),
        };
        let encoded = example.encode_to_string();
        let decoded = EmailNonce::decode_from_str(&encoded).unwrap();
        assert_eq!(example, decoded);
    }

    #[test]
    fn decode_empty_email_nonce() {
        assert!(EmailNonce::decode_from_str("").is_err());
    }

    #[test]
    fn should_generate_unique_instances() {
        let n1 = Nonce::new();
        let n2 = Nonce::new();
        assert_ne!(n1, n2);
    }

    #[test]
    fn should_convert_from_to_string() {
        let n1 = Nonce::new();
        let s1 = n1.to_string();
        assert_eq!(Nonce::STR_LEN, s1.len());
        let n2 = s1.parse::<Nonce>().unwrap();
        assert_eq!(n1, n2);
        assert_eq!(s1, n2.to_string());
    }
}