user_profile_dto.rs
207 lignes · 6663 octets
/// User Profile Data Transfer Object /// Feature 004: UI Post-Auth /// /// This DTO is used to transfer user profile information to the UI layer, /// including avatar generation (initials and color). use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use uuid::Uuid; /// User Profile DTO with avatar information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserProfileDTO { /// User ID pub id: Uuid, /// Username pub username: String, /// User email pub email: String, /// User roles (e.g., ["admin", "user"]) pub roles: Vec<String>, /// Account creation date pub created_at: DateTime<Utc>, /// Generated initials for avatar (e.g., "JD" for "John Doe") pub initials: String, /// Generated avatar color (hex code) pub avatar_color: String, } impl UserProfileDTO { /// Generate initials from username /// /// # Examples /// /// ``` /// use rustwarden_ui::dto::user_profile_dto::UserProfileDTO; /// /// assert_eq!(UserProfileDTO::generate_initials("john"), "J"); /// assert_eq!(UserProfileDTO::generate_initials("john_doe"), "JD"); /// assert_eq!(UserProfileDTO::generate_initials("john.doe.smith"), "JD"); /// assert_eq!(UserProfileDTO::generate_initials("a"), "A"); /// ``` pub fn generate_initials(username: &str) -> String { let parts: Vec<&str> = username .split(['_', '.', '-', ' ']) .filter(|s| !s.is_empty()) .collect(); match parts.len() { 0 => String::from("?"), 1 => parts[0] .chars() .next() .unwrap_or('?') .to_uppercase() .to_string(), _ => { let first = parts[0].chars().next().unwrap_or('?'); let second = parts[1].chars().next().unwrap_or('?'); format!("{}{}", first.to_uppercase(), second.to_uppercase()) } } } /// Generate avatar color based on username hash /// /// Returns a consistent color for the same username. /// Uses a predefined palette of pleasant colors. /// /// # Examples /// /// ``` /// use rustwarden_ui::dto::user_profile_dto::UserProfileDTO; /// /// let color1 = UserProfileDTO::generate_avatar_color("john"); /// let color2 = UserProfileDTO::generate_avatar_color("john"); /// assert_eq!(color1, color2); // Same username = same color /// /// let color3 = UserProfileDTO::generate_avatar_color("jane"); /// assert_ne!(color1, color3); // Different usernames = different colors (most likely) /// ``` pub fn generate_avatar_color(username: &str) -> String { // Palette of pleasant colors const COLORS: &[&str] = &[ "#0082c9", // RustWarden blue "#ce422b", // Rust red "#3b82f6", // Blue "#10b981", // Green "#f59e0b", // Amber "#8b5cf6", // Purple "#ec4899", // Pink "#06b6d4", // Cyan "#84cc16", // Lime "#f97316", // Orange ]; // Hash the username let mut hasher = DefaultHasher::new(); username.hash(&mut hasher); let hash = hasher.finish(); // Select color from palette let index = (hash % COLORS.len() as u64) as usize; COLORS[index].to_string() } /// Create a new UserProfileDTO from user model and roles /// /// # Arguments /// /// * `user` - The user model from database /// * `roles` - List of role names for this user pub fn new(user: rustwarden_core::models::user::Model, roles: Vec<String>) -> Self { let username = user.username.clone(); Self { id: user.id, username: user.username, email: user.email, roles, created_at: user.created_at, initials: Self::generate_initials(&username), avatar_color: Self::generate_avatar_color(&username), } } } /// Implement From trait for conversion from User model impl From<rustwarden_core::models::user::Model> for UserProfileDTO { fn from(user: rustwarden_core::models::user::Model) -> Self { Self::new(user, Vec::new()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_generate_initials_single_part() { assert_eq!(UserProfileDTO::generate_initials("john"), "J"); assert_eq!(UserProfileDTO::generate_initials("alice"), "A"); } #[test] fn test_generate_initials_two_parts() { assert_eq!(UserProfileDTO::generate_initials("john_doe"), "JD"); assert_eq!(UserProfileDTO::generate_initials("alice.smith"), "AS"); assert_eq!(UserProfileDTO::generate_initials("bob-jones"), "BJ"); } #[test] fn test_generate_initials_multiple_parts() { // Should only take first two parts assert_eq!(UserProfileDTO::generate_initials("john.doe.smith"), "JD"); assert_eq!(UserProfileDTO::generate_initials("a_b_c_d"), "AB"); } #[test] fn test_generate_initials_empty() { assert_eq!(UserProfileDTO::generate_initials(""), "?"); } #[test] fn test_generate_initials_single_char() { assert_eq!(UserProfileDTO::generate_initials("a"), "A"); assert_eq!(UserProfileDTO::generate_initials("z"), "Z"); } #[test] fn test_generate_avatar_color_consistency() { // Same username should always give same color let color1 = UserProfileDTO::generate_avatar_color("john"); let color2 = UserProfileDTO::generate_avatar_color("john"); assert_eq!(color1, color2); } #[test] fn test_generate_avatar_color_format() { // Color should be in hex format let color = UserProfileDTO::generate_avatar_color("test"); assert!(color.starts_with('#')); assert_eq!(color.len(), 7); // #RRGGBB } #[test] fn test_generate_avatar_color_different_users() { let color1 = UserProfileDTO::generate_avatar_color("alice"); let color2 = UserProfileDTO::generate_avatar_color("bob"); let color3 = UserProfileDTO::generate_avatar_color("charlie"); // All colors should be valid assert!(color1.starts_with('#')); assert!(color2.starts_with('#')); assert!(color3.starts_with('#')); // It's possible (but unlikely) that two different usernames // produce the same color, so we don't assert inequality } }
GitRust