use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Credentials {
pub email: String,
pub device_id: String,
#[serde(skip_serializing, default)]
password: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
token: Option<String>,
}
impl Credentials {
pub fn new(
email: impl Into<String>,
password: impl Into<String>,
device_id: impl Into<String>,
) -> Self {
Self {
email: email.into(),
device_id: device_id.into(),
password: Some(password.into()),
token: None,
}
}
pub fn from_token(
email: impl Into<String>,
device_id: impl Into<String>,
token: impl Into<String>,
) -> Self {
Self {
email: email.into(),
device_id: device_id.into(),
password: None,
token: Some(token.into()),
}
}
pub fn generate_device_id() -> String {
uuid::Uuid::new_v4().to_string()
}
pub fn token(&self) -> Option<&str> {
self.token.as_deref()
}
pub fn is_token_based(&self) -> bool {
self.token.is_some() && self.password.is_none()
}
pub fn has_password(&self) -> bool {
self.password.is_some()
}
pub(crate) fn password(&self) -> Option<&str> {
self.password.as_deref()
}
pub(crate) fn set_token(&mut self, token: String) {
self.token = Some(token);
self.password = None; }
#[allow(dead_code)]
pub(crate) fn clear_token(&mut self) {
self.token = None;
}
pub fn with_password(&self) -> WithPassword<'_> {
WithPassword(self)
}
}
#[derive(Debug)]
pub struct WithPassword<'a>(&'a Credentials);
impl<'a> Serialize for WithPassword<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let creds = self.0;
let field_count = 2 + creds.password.is_some() as usize + creds.token.is_some() as usize;
let mut state = serializer.serialize_struct("Credentials", field_count)?;
state.serialize_field("email", &creds.email)?;
state.serialize_field("device_id", &creds.device_id)?;
if let Some(ref password) = creds.password {
state.serialize_field("password", password)?;
}
if let Some(ref token) = creds.token {
state.serialize_field("token", token)?;
}
state.end()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_credentials() {
let creds = Credentials::new("test@example.com", "password123", "device-123");
assert_eq!(creds.email, "test@example.com");
assert_eq!(creds.device_id, "device-123");
assert!(creds.has_password());
assert!(!creds.is_token_based());
assert!(creds.token().is_none());
}
#[test]
fn test_token_credentials() {
let creds = Credentials::from_token("test@example.com", "device-123", "jwt-token");
assert_eq!(creds.email, "test@example.com");
assert_eq!(creds.device_id, "device-123");
assert!(!creds.has_password());
assert!(creds.is_token_based());
assert_eq!(creds.token(), Some("jwt-token"));
}
#[test]
fn test_set_token_clears_password() {
let mut creds = Credentials::new("test@example.com", "password123", "device-123");
assert!(creds.has_password());
creds.set_token("new-token".to_string());
assert!(!creds.has_password());
assert_eq!(creds.token(), Some("new-token"));
assert!(creds.is_token_based());
}
#[test]
fn test_generate_device_id_is_unique() {
let id1 = Credentials::generate_device_id();
let id2 = Credentials::generate_device_id();
assert_ne!(id1, id2);
assert_eq!(id1.len(), 36); }
#[test]
fn test_serialization_without_password() {
let creds = Credentials::new("test@example.com", "password123", "device-123");
let json = serde_json::to_string(&creds).unwrap();
assert!(!json.contains("password123"));
assert!(!json.contains("\"password\""));
}
#[test]
fn test_serialization_with_password_explicit() {
let creds = Credentials::new("test@example.com", "password123", "device-123");
let json = serde_json::to_string(&creds.with_password()).unwrap();
assert!(json.contains("password123"));
assert!(json.contains("\"password\""));
}
#[test]
fn test_deserialization_with_password() {
let json = r#"{
"email": "test@example.com",
"device_id": "device-123",
"password": "secret123"
}"#;
let creds: Credentials = serde_json::from_str(json).unwrap();
assert_eq!(creds.email, "test@example.com");
assert!(creds.has_password());
assert_eq!(creds.password(), Some("secret123"));
}
#[test]
fn test_deserialization() {
let json = r#"{
"email": "test@example.com",
"device_id": "device-123",
"token": "jwt-token"
}"#;
let creds: Credentials = serde_json::from_str(json).unwrap();
assert_eq!(creds.email, "test@example.com");
assert_eq!(creds.device_id, "device-123");
assert_eq!(creds.token(), Some("jwt-token"));
assert!(!creds.has_password());
}
}