authnz-common 0.2.1

Authnz common library (types and utils).
Documentation
//! Identication types module.

#[cfg(any(feature = "app-server-types", feature = "authnz-server-types"))]
use impulse_server_kit::salvo;
#[cfg(any(feature = "app-server-types", feature = "authnz-server-types"))]
use salvo::oapi::ToSchema;
use serde::{Deserialize, Serialize};

use crate::{MResult, ServerError};

#[cfg_attr(any(feature = "app-server-types", feature = "authnz-server-types"), derive(ToSchema))]
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
/// Email.
pub struct Email(String);

impl Email {
  fn validate(email: &str) -> MResult<()> {
    let email_regex =
      regex::Regex::new(r"^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,10})$").map_err(|e| ServerError::from_private(e).with_500())?;
    if email_regex.is_match(email) && !email.contains("..") && !email.starts_with('.') {
      Ok(())
    } else {
      Err(ServerError::from_public("Invalid email!").with_400())
    }
  }

  /// Constructs new email from string.
  pub fn new(email: impl ToString) -> MResult<Self> {
    let email = email.to_string();
    Email::validate(&email)?;
    Ok(Self(email))
  }

  /// Returns email domain.
  pub fn domain(&self) -> MResult<&str> {
    self
      .0
      .split('@')
      .next_back()
      .ok_or(ServerError::from_private_str("Can't get email domain!").with_500())
  }
}

impl std::fmt::Display for Email {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    f.write_str(self.0.as_str())
  }
}

impl AsRef<str> for Email {
  fn as_ref(&self) -> &str {
    self.0.as_str()
  }
}

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

impl<'de> Deserialize<'de> for Email {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: serde::Deserializer<'de>,
  {
    deserializer.deserialize_str(EmailVisitor)
  }
}

struct EmailVisitor;

impl<'de> serde::de::Visitor<'de> for EmailVisitor {
  type Value = Email;

  fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
    formatter.write_str("a valid email address")
  }

  fn visit_str<E>(self, value: &str) -> Result<Email, E>
  where
    E: serde::de::Error,
  {
    if Email::validate(value).is_ok() {
      Ok(Email(value.to_string()))
    } else {
      Err(E::custom(format!("invalid email address: {value}")))
    }
  }
}

#[derive(Deserialize, Serialize, PartialEq, Eq, Hash, Clone, Debug)]
#[cfg_attr(any(feature = "app-server-types", feature = "authnz-server-types"), derive(ToSchema))]
#[serde(rename_all = "snake_case", tag = "type")]
#[allow(missing_docs)]
/// User identifier.
pub enum Id {
  Nickname { nickname: String },
  Email { email: Email },
}

impl std::fmt::Display for Id {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    f.write_str(self.as_str())
  }
}

impl Id {
  /// Nickname ID.
  pub fn nickname(nickname: impl ToString) -> Self {
    Self::Nickname {
      nickname: nickname.to_string(),
    }
  }

  /// Email ID.
  pub fn email(email: impl ToString) -> MResult<Self> {
    Ok(Self::Email { email: Email::new(email)? })
  }

  /// Returns the string representation of identifier.
  pub fn as_str(&self) -> &str {
    match &self {
      Self::Nickname { nickname } => nickname.as_str(),
      Self::Email { email } => email.0.as_str(),
    }
  }

  /// Is ID an email?
  pub fn is_email(&self) -> bool {
    matches!(self, Id::Email { .. })
  }
}

impl AsRef<str> for Id {
  fn as_ref(&self) -> &str {
    self.as_str()
  }
}