use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[expect(
clippy::exhaustive_enums,
reason = "verified/unverified is a closed set; callers should match exhaustively"
)]
pub enum Email {
Verified(String),
Unverified(String),
}
impl Email {
pub(crate) fn from_parts(email: String, verified: Option<bool>) -> Self {
if verified == Some(true) {
Self::Verified(email)
} else {
Self::Unverified(email)
}
}
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Verified(s) | Self::Unverified(s) => s,
}
}
#[must_use]
pub const fn is_verified(&self) -> bool {
matches!(self, Self::Verified(_))
}
}
impl AsRef<str> for Email {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for Email {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for Email {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for Email {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(Self::Unverified(s))
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unwrap_used,
reason = "tests do not need to meet production lint standards"
)]
use super::Email;
#[test]
fn oidc_claims_email_returns_email_newtype() {
let email = Email::from_parts("test@example.com".to_string(), None);
assert_eq!(email.as_str(), "test@example.com");
}
#[test]
fn oidc_claims_email_absent_returns_none() {
let email = Email::from_parts("u@example.com".to_string(), None);
assert!(!email.is_verified());
}
#[test]
fn oidc_claims_email_unverified_when_verified_false() {
let email = Email::from_parts("user@example.com".to_string(), Some(false));
assert!(!email.is_verified());
}
#[test]
fn oidc_claims_email_unverified_when_verified_absent() {
let email = Email::from_parts("user@example.com".to_string(), None);
assert!(!email.is_verified());
}
#[test]
fn email_display_formats_as_address() {
let email = Email::from_parts("user@example.com".to_string(), Some(true));
assert_eq!(format!("{email}"), "user@example.com");
}
#[test]
fn deserialized_email_defaults_as_unverified() {
let email: Email = serde_json::from_value(serde_json::json!("user@example.com")).unwrap();
assert_eq!(email, Email::Unverified("user@example.com".to_string()));
}
}