refactoring hexagonal and clean architecture

This commit is contained in:
Diocrafts
2026-02-08 13:40:23 +01:00
parent 3e4fb67c19
commit a82faa5eaf
101 changed files with 7433 additions and 9721 deletions
+294 -32
View File
@@ -3,28 +3,79 @@ use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct AddressBook {
pub id: Uuid,
pub name: String,
pub owner_id: String,
pub description: Option<String>,
pub color: Option<String>,
pub is_public: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
id: Uuid,
name: String,
owner_id: String,
description: Option<String>,
color: Option<String>,
is_public: bool,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl AddressBook {
/// Creates a new AddressBook with generated id and timestamps
pub fn new(
name: String,
owner_id: String,
description: Option<String>,
color: Option<String>,
is_public: bool,
) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
name,
owner_id,
description,
color,
is_public,
created_at: now,
updated_at: now,
}
}
/// Reconstructs from persistence (no validation)
pub fn from_raw(
id: Uuid,
name: String,
owner_id: String,
description: Option<String>,
color: Option<String>,
is_public: bool,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
) -> Self {
Self { id, name, owner_id, description, color, is_public, created_at, updated_at }
}
// --- Getters ---
pub fn id(&self) -> &Uuid { &self.id }
pub fn name(&self) -> &str { &self.name }
pub fn owner_id(&self) -> &str { &self.owner_id }
pub fn description(&self) -> Option<&str> { self.description.as_deref() }
pub fn color(&self) -> Option<&str> { self.color.as_deref() }
pub fn is_public(&self) -> bool { self.is_public }
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at }
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at }
// --- Setters for mutable operations ---
pub fn set_name(&mut self, name: String) { self.name = name; self.updated_at = Utc::now(); }
pub fn set_description(&mut self, description: Option<String>) { self.description = description; self.updated_at = Utc::now(); }
pub fn set_color(&mut self, color: Option<String>) { self.color = color; self.updated_at = Utc::now(); }
pub fn set_is_public(&mut self, is_public: bool) { self.is_public = is_public; self.updated_at = Utc::now(); }
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) { self.updated_at = updated_at; }
}
impl Default for AddressBook {
fn default() -> Self {
Self {
id: Uuid::new_v4(),
name: "Default Address Book".to_string(),
owner_id: "default".to_string(),
description: None,
color: None,
is_public: false,
created_at: Utc::now(),
updated_at: Utc::now(),
}
Self::new(
"Default Address Book".to_string(),
"default".to_string(),
None,
None,
false,
)
}
}
@@ -55,6 +106,192 @@ pub struct Address {
#[derive(Debug, Clone)]
pub struct Contact {
id: Uuid,
address_book_id: Uuid,
uid: String,
full_name: Option<String>,
first_name: Option<String>,
last_name: Option<String>,
nickname: Option<String>,
email: Vec<Email>,
phone: Vec<Phone>,
address: Vec<Address>,
organization: Option<String>,
title: Option<String>,
notes: Option<String>,
photo_url: Option<String>,
birthday: Option<NaiveDate>,
anniversary: Option<NaiveDate>,
vcard: String,
etag: String,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl Contact {
/// Creates a new Contact with generated id, uid, etag and timestamps
#[allow(clippy::too_many_arguments)]
pub fn new(
address_book_id: Uuid,
full_name: Option<String>,
first_name: Option<String>,
last_name: Option<String>,
nickname: Option<String>,
email: Vec<Email>,
phone: Vec<Phone>,
address: Vec<Address>,
organization: Option<String>,
title: Option<String>,
notes: Option<String>,
photo_url: Option<String>,
birthday: Option<NaiveDate>,
anniversary: Option<NaiveDate>,
vcard: String,
) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
address_book_id,
uid: format!("{}@oxicloud", Uuid::new_v4()),
full_name,
first_name,
last_name,
nickname,
email,
phone,
address,
organization,
title,
notes,
photo_url,
birthday,
anniversary,
vcard,
etag: Uuid::new_v4().to_string(),
created_at: now,
updated_at: now,
}
}
/// Reconstructs from persistence (no validation)
#[allow(clippy::too_many_arguments)]
pub fn from_raw(
id: Uuid,
address_book_id: Uuid,
uid: String,
full_name: Option<String>,
first_name: Option<String>,
last_name: Option<String>,
nickname: Option<String>,
email: Vec<Email>,
phone: Vec<Phone>,
address: Vec<Address>,
organization: Option<String>,
title: Option<String>,
notes: Option<String>,
photo_url: Option<String>,
birthday: Option<NaiveDate>,
anniversary: Option<NaiveDate>,
vcard: String,
etag: String,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
) -> Self {
Self {
id, address_book_id, uid, full_name, first_name, last_name, nickname,
email, phone, address, organization, title, notes, photo_url,
birthday, anniversary, vcard, etag, created_at, updated_at,
}
}
// --- Getters ---
pub fn id(&self) -> &Uuid { &self.id }
pub fn address_book_id(&self) -> &Uuid { &self.address_book_id }
pub fn uid(&self) -> &str { &self.uid }
pub fn full_name(&self) -> Option<&str> { self.full_name.as_deref() }
pub fn first_name(&self) -> Option<&str> { self.first_name.as_deref() }
pub fn last_name(&self) -> Option<&str> { self.last_name.as_deref() }
pub fn nickname(&self) -> Option<&str> { self.nickname.as_deref() }
pub fn email(&self) -> &[Email] { &self.email }
pub fn phone(&self) -> &[Phone] { &self.phone }
pub fn address(&self) -> &[Address] { &self.address }
pub fn organization(&self) -> Option<&str> { self.organization.as_deref() }
pub fn title(&self) -> Option<&str> { self.title.as_deref() }
pub fn notes(&self) -> Option<&str> { self.notes.as_deref() }
pub fn photo_url(&self) -> Option<&str> { self.photo_url.as_deref() }
pub fn birthday(&self) -> Option<&NaiveDate> { self.birthday.as_ref() }
pub fn anniversary(&self) -> Option<&NaiveDate> { self.anniversary.as_ref() }
pub fn vcard(&self) -> &str { &self.vcard }
pub fn etag(&self) -> &str { &self.etag }
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at }
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at }
// --- Owned getters for persistence layer bind() calls ---
pub fn full_name_owned(&self) -> Option<String> { self.full_name.clone() }
pub fn first_name_owned(&self) -> Option<String> { self.first_name.clone() }
pub fn last_name_owned(&self) -> Option<String> { self.last_name.clone() }
pub fn nickname_owned(&self) -> Option<String> { self.nickname.clone() }
pub fn organization_owned(&self) -> Option<String> { self.organization.clone() }
pub fn title_owned(&self) -> Option<String> { self.title.clone() }
pub fn notes_owned(&self) -> Option<String> { self.notes.clone() }
pub fn photo_url_owned(&self) -> Option<String> { self.photo_url.clone() }
// --- Setters for mutable operations (contact_service.rs needs these) ---
pub fn set_full_name(&mut self, v: Option<String>) { self.full_name = v; }
pub fn set_first_name(&mut self, v: Option<String>) { self.first_name = v; }
pub fn set_last_name(&mut self, v: Option<String>) { self.last_name = v; }
pub fn set_nickname(&mut self, v: Option<String>) { self.nickname = v; }
pub fn set_organization(&mut self, v: Option<String>) { self.organization = v; }
pub fn set_title(&mut self, v: Option<String>) { self.title = v; }
pub fn set_notes(&mut self, v: Option<String>) { self.notes = v; }
pub fn set_photo_url(&mut self, v: Option<String>) { self.photo_url = v; }
pub fn set_birthday(&mut self, v: Option<NaiveDate>) { self.birthday = v; }
pub fn set_anniversary(&mut self, v: Option<NaiveDate>) { self.anniversary = v; }
pub fn set_vcard(&mut self, vcard: String) { self.vcard = vcard; }
pub fn set_etag(&mut self, etag: String) { self.etag = etag; }
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) { self.updated_at = updated_at; }
pub fn set_address_book_id(&mut self, id: Uuid) { self.address_book_id = id; }
pub fn set_uid(&mut self, uid: String) { self.uid = uid; }
// --- Collection mutators ---
pub fn push_email(&mut self, e: Email) { self.email.push(e); }
pub fn push_phone(&mut self, p: Phone) { self.phone.push(p); }
pub fn set_email(&mut self, email: Vec<Email>) { self.email = email; }
pub fn set_phone(&mut self, phone: Vec<Phone>) { self.phone = phone; }
pub fn set_address(&mut self, address: Vec<Address>) { self.address = address; }
pub fn email_is_empty(&self) -> bool { self.email.is_empty() }
pub fn phone_is_empty(&self) -> bool { self.phone.is_empty() }
// --- Consuming methods for ownership transfer ---
pub fn into_email(self) -> Vec<Email> { self.email }
pub fn into_parts(self) -> ContactParts {
ContactParts {
id: self.id,
address_book_id: self.address_book_id,
uid: self.uid,
full_name: self.full_name,
first_name: self.first_name,
last_name: self.last_name,
nickname: self.nickname,
email: self.email,
phone: self.phone,
address: self.address,
organization: self.organization,
title: self.title,
notes: self.notes,
photo_url: self.photo_url,
birthday: self.birthday,
anniversary: self.anniversary,
vcard: self.vcard,
etag: self.etag,
created_at: self.created_at,
updated_at: self.updated_at,
}
}
}
/// Holds all Contact fields by value, for when ownership transfer is needed
pub struct ContactParts {
pub id: Uuid,
pub address_book_id: Uuid,
pub uid: String,
@@ -79,6 +316,7 @@ pub struct Contact {
impl Default for Contact {
fn default() -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
address_book_id: Uuid::new_v4(),
@@ -98,29 +336,53 @@ impl Default for Contact {
anniversary: None,
vcard: "BEGIN:VCARD\nVERSION:3.0\nEND:VCARD".to_string(),
etag: Uuid::new_v4().to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
created_at: now,
updated_at: now,
}
}
}
#[derive(Debug, Clone)]
pub struct ContactGroup {
pub id: Uuid,
pub address_book_id: Uuid,
pub name: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
id: Uuid,
address_book_id: Uuid,
name: String,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl ContactGroup {
/// Creates a new ContactGroup with generated id and timestamps
pub fn new(address_book_id: Uuid, name: String) -> Self {
let now = Utc::now();
Self { id: Uuid::new_v4(), address_book_id, name, created_at: now, updated_at: now }
}
/// Reconstructs from persistence
pub fn from_raw(
id: Uuid,
address_book_id: Uuid,
name: String,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
) -> Self {
Self { id, address_book_id, name, created_at, updated_at }
}
// --- Getters ---
pub fn id(&self) -> &Uuid { &self.id }
pub fn address_book_id(&self) -> &Uuid { &self.address_book_id }
pub fn name(&self) -> &str { &self.name }
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at }
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at }
// --- Setters ---
pub fn set_name(&mut self, name: String) { self.name = name; self.updated_at = Utc::now(); }
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) { self.updated_at = updated_at; }
}
impl Default for ContactGroup {
fn default() -> Self {
Self {
id: Uuid::new_v4(),
address_book_id: Uuid::new_v4(),
name: "New Group".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
}
ContactGroup::new(Uuid::new_v4(), "New Group".to_string())
}
}
+47 -8
View File
@@ -3,14 +3,14 @@ use chrono::{DateTime, Utc, Duration};
#[derive(Debug, Clone)]
pub struct Session {
pub id: String,
pub user_id: String,
pub refresh_token: String,
pub expires_at: DateTime<Utc>,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub created_at: DateTime<Utc>,
pub revoked: bool,
id: String,
user_id: String,
refresh_token: String,
expires_at: DateTime<Utc>,
ip_address: Option<String>,
user_agent: Option<String>,
created_at: DateTime<Utc>,
revoked: bool,
}
impl Session {
@@ -21,6 +21,13 @@ impl Session {
user_agent: Option<String>,
expires_in_days: i64,
) -> Self {
if user_id.is_empty() {
panic!("Session user_id cannot be empty");
}
if refresh_token.is_empty() {
panic!("Session refresh_token cannot be empty");
}
let now = Utc::now();
Self {
id: Uuid::new_v4().to_string(),
@@ -33,6 +40,30 @@ impl Session {
revoked: false,
}
}
/// Reconstruct a Session from persisted data (e.g. database row).
/// Skips ID generation — uses the provided values directly.
pub fn from_raw(
id: String,
user_id: String,
refresh_token: String,
expires_at: DateTime<Utc>,
ip_address: Option<String>,
user_agent: Option<String>,
created_at: DateTime<Utc>,
revoked: bool,
) -> Self {
Self {
id,
user_id,
refresh_token,
expires_at,
ip_address,
user_agent,
created_at,
revoked,
}
}
// Getters
pub fn id(&self) -> &str {
@@ -50,6 +81,14 @@ impl Session {
pub fn expires_at(&self) -> DateTime<Utc> {
self.expires_at
}
pub fn ip_address(&self) -> Option<&str> {
self.ip_address.as_deref()
}
pub fn user_agent(&self) -> Option<&str> {
self.user_agent.as_deref()
}
pub fn created_at(&self) -> DateTime<Utc> {
self.created_at
+149 -35
View File
@@ -6,23 +6,23 @@ pub use super::entity_errors::ShareError;
#[derive(Debug, Clone, PartialEq)]
pub struct Share {
pub id: String,
pub item_id: String,
pub item_type: ShareItemType,
pub token: String,
pub password_hash: Option<String>,
pub expires_at: Option<u64>,
pub permissions: SharePermissions,
pub created_at: u64,
pub created_by: String,
pub access_count: u64,
id: String,
item_id: String,
item_type: ShareItemType,
token: String,
password_hash: Option<String>,
expires_at: Option<u64>,
permissions: SharePermissions,
created_at: u64,
created_by: String,
access_count: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SharePermissions {
pub read: bool,
pub write: bool,
pub reshare: bool,
read: bool,
write: bool,
reshare: bool,
}
#[derive(Debug, Clone, PartialEq)]
@@ -80,6 +80,74 @@ impl Share {
})
}
/// Reconstruct a Share from persisted data (e.g. filesystem/database).
/// Skips validation and ID generation — uses the provided values directly.
pub fn from_raw(
id: String,
item_id: String,
item_type: ShareItemType,
token: String,
password_hash: Option<String>,
expires_at: Option<u64>,
permissions: SharePermissions,
created_at: u64,
created_by: String,
access_count: u64,
) -> Self {
Self {
id,
item_id,
item_type,
token,
password_hash,
expires_at,
permissions,
created_at,
created_by,
access_count,
}
}
// ── Getters ──
pub fn id(&self) -> &str {
&self.id
}
pub fn item_id(&self) -> &str {
&self.item_id
}
pub fn item_type(&self) -> &ShareItemType {
&self.item_type
}
pub fn token(&self) -> &str {
&self.token
}
pub fn expires_at(&self) -> Option<u64> {
self.expires_at
}
pub fn permissions(&self) -> &SharePermissions {
&self.permissions
}
pub fn created_at(&self) -> u64 {
self.created_at
}
pub fn created_by(&self) -> &str {
&self.created_by
}
pub fn access_count(&self) -> u64 {
self.access_count
}
// ── Builder-style modifiers (immutable) ──
pub fn with_permissions(mut self, permissions: SharePermissions) -> Self {
self.permissions = permissions;
self
@@ -118,15 +186,17 @@ impl Share {
self
}
pub fn verify_password(&self, password: &str) -> bool {
match &self.password_hash {
Some(hash) => {
// In a real implementation, use a proper password hashing function like bcrypt
// For simplicity, we're just comparing strings here
hash == password
}
None => true,
}
/// Returns whether this share requires a password to access.
pub fn has_password(&self) -> bool {
self.password_hash.is_some()
}
/// Returns a reference to the password hash, if one is set.
///
/// Password verification should be performed externally via PasswordHasherPort
/// to keep cryptographic dependencies out of the domain layer.
pub fn password_hash(&self) -> Option<&str> {
self.password_hash.as_deref()
}
}
@@ -138,13 +208,25 @@ impl SharePermissions {
reshare,
}
}
pub fn read(&self) -> bool {
self.read
}
pub fn write(&self) -> bool {
self.write
}
pub fn reshare(&self) -> bool {
self.reshare
}
}
impl ToString for ShareItemType {
fn to_string(&self) -> String {
impl std::fmt::Display for ShareItemType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ShareItemType::File => "file".to_string(),
ShareItemType::Folder => "folder".to_string(),
ShareItemType::File => write!(f, "file"),
ShareItemType::Folder => write!(f, "folder"),
}
}
}
@@ -177,15 +259,15 @@ mod tests {
)
.unwrap();
assert_eq!(share.item_id, "test_file_id");
assert_eq!(share.item_type, ShareItemType::File);
assert_eq!(share.created_by, "user123");
assert_eq!(share.permissions.read, true);
assert_eq!(share.permissions.write, false);
assert_eq!(share.permissions.reshare, false);
assert!(share.password_hash.is_none());
assert!(share.expires_at.is_none());
assert_eq!(share.access_count, 0);
assert_eq!(share.item_id(), "test_file_id");
assert_eq!(*share.item_type(), ShareItemType::File);
assert_eq!(share.created_by(), "user123");
assert_eq!(share.permissions().read(), true);
assert_eq!(share.permissions().write(), false);
assert_eq!(share.permissions().reshare(), false);
assert!(!share.has_password());
assert!(share.expires_at().is_none());
assert_eq!(share.access_count(), 0);
}
#[test]
@@ -233,4 +315,36 @@ mod tests {
assert_eq!(ShareItemType::try_from("FILE").unwrap(), ShareItemType::File);
assert!(ShareItemType::try_from("invalid").is_err());
}
#[test]
fn test_has_password_with_hash() {
let share = Share::new(
"test_file_id".to_string(),
ShareItemType::File,
"user123".to_string(),
None,
Some("some_hash_value".to_string()),
None,
)
.unwrap();
assert!(share.has_password());
assert_eq!(share.password_hash(), Some("some_hash_value"));
}
#[test]
fn test_has_password_without_hash() {
let share = Share::new(
"test_file_id".to_string(),
ShareItemType::File,
"user123".to_string(),
None,
None, // No password
None,
)
.unwrap();
assert!(!share.has_password());
assert_eq!(share.password_hash(), None);
}
}
+66 -8
View File
@@ -9,14 +9,14 @@ pub enum TrashedItemType {
#[derive(Debug, Clone)]
pub struct TrashedItem {
pub id: Uuid,
pub original_id: Uuid,
pub user_id: Uuid,
pub item_type: TrashedItemType,
pub name: String,
pub original_path: String,
pub trashed_at: DateTime<Utc>,
pub deletion_date: DateTime<Utc>, // Fecha de eliminación permanente automática
id: Uuid,
original_id: Uuid,
user_id: Uuid,
item_type: TrashedItemType,
name: String,
original_path: String,
trashed_at: DateTime<Utc>,
deletion_date: DateTime<Utc>,
}
impl TrashedItem {
@@ -41,6 +41,64 @@ impl TrashedItem {
}
}
/// Reconstruct a TrashedItem from persisted data (e.g. JSON index).
/// Skips ID generation — uses the provided values directly.
pub fn from_raw(
id: Uuid,
original_id: Uuid,
user_id: Uuid,
item_type: TrashedItemType,
name: String,
original_path: String,
trashed_at: DateTime<Utc>,
deletion_date: DateTime<Utc>,
) -> Self {
Self {
id,
original_id,
user_id,
item_type,
name,
original_path,
trashed_at,
deletion_date,
}
}
// ── Getters ──
pub fn id(&self) -> Uuid {
self.id
}
pub fn original_id(&self) -> Uuid {
self.original_id
}
pub fn user_id(&self) -> Uuid {
self.user_id
}
pub fn item_type(&self) -> &TrashedItemType {
&self.item_type
}
pub fn name(&self) -> &str {
&self.name
}
pub fn original_path(&self) -> &str {
&self.original_path
}
pub fn trashed_at(&self) -> DateTime<Utc> {
self.trashed_at
}
pub fn deletion_date(&self) -> DateTime<Utc> {
self.deletion_date
}
pub fn days_until_deletion(&self) -> i64 {
let now = Utc::now();
(self.deletion_date - now).num_days().max(0)