refactor: restructure domain crate by bounded context

This commit is contained in:
2026-05-31 04:44:48 +02:00
parent 2b62d1ec81
commit de93373b43
136 changed files with 2111 additions and 2096 deletions

View File

@@ -0,0 +1,17 @@
#[derive(Debug, thiserror::Error)]
pub enum DomainError {
#[error("Not found: {0}")]
NotFound(String),
#[error("Conflict: {0}")]
Conflict(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
#[error("Forbidden: {0}")]
Forbidden(String),
#[error("Validation error: {0}")]
Validation(String),
#[error("Quota exceeded: {0}")]
QuotaExceeded(String),
#[error("Internal error: {0}")]
Internal(String),
}

View File

@@ -0,0 +1,49 @@
use crate::common::value_objects::{DateTimeStamp, SystemId};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum DomainEvent {
AssetIngested {
asset_id: SystemId,
owner_user_id: SystemId,
timestamp: DateTimeStamp,
},
MetadataUpdated {
asset_id: SystemId,
updated_by: SystemId,
timestamp: DateTimeStamp,
},
AssetDeleted {
asset_id: SystemId,
deleted_by: SystemId,
timestamp: DateTimeStamp,
},
ShareCreated {
scope_id: SystemId,
shareable_id: SystemId,
created_by: SystemId,
timestamp: DateTimeStamp,
},
ShareRevoked {
scope_id: SystemId,
revoked_by: SystemId,
timestamp: DateTimeStamp,
},
SidecarSyncRequested {
asset_id: SystemId,
timestamp: DateTimeStamp,
},
JobEnqueued {
job_id: SystemId,
job_type: String,
timestamp: DateTimeStamp,
},
JobCompleted {
job_id: SystemId,
timestamp: DateTimeStamp,
},
JobFailed {
job_id: SystemId,
error: String,
timestamp: DateTimeStamp,
},
}

View File

@@ -0,0 +1,4 @@
pub mod errors;
pub mod events;
pub mod ports;
pub mod value_objects;

View File

@@ -0,0 +1,8 @@
use async_trait::async_trait;
use crate::common::errors::DomainError;
use crate::common::events::DomainEvent;
#[async_trait]
pub trait EventPublisher: Send + Sync {
async fn publish(&self, event: DomainEvent) -> Result<(), DomainError>;
}

View File

@@ -0,0 +1,29 @@
use crate::common::errors::DomainError;
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Checksum(String);
impl Checksum {
pub fn new(hex: impl Into<String>) -> Result<Self, DomainError> {
let hex = hex.into().to_lowercase();
if hex.len() != 64 {
return Err(DomainError::Validation(
format!("Checksum must be 64 hex characters, got {}", hex.len()),
));
}
if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(DomainError::Validation(
"Checksum contains non-hex characters".to_string(),
));
}
Ok(Self(hex))
}
pub fn as_str(&self) -> &str { &self.0 }
}
impl std::fmt::Display for Checksum {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

View File

@@ -0,0 +1,20 @@
use chrono::{DateTime, Utc};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
pub struct DateTimeStamp(DateTime<Utc>);
impl DateTimeStamp {
pub fn now() -> Self { Self(Utc::now()) }
pub fn from_datetime(dt: DateTime<Utc>) -> Self { Self(dt) }
pub fn as_datetime(&self) -> &DateTime<Utc> { &self.0 }
}
impl std::fmt::Display for DateTimeStamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.to_rfc3339())
}
}
impl From<DateTime<Utc>> for DateTimeStamp {
fn from(dt: DateTime<Utc>) -> Self { Self(dt) }
}

View File

@@ -0,0 +1,22 @@
use crate::common::errors::DomainError;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Email(String);
impl Email {
pub fn new(value: impl Into<String>) -> Result<Self, DomainError> {
let value = value.into().trim().to_lowercase();
if value.is_empty() || !value.contains('@') {
return Err(DomainError::Validation("Invalid email address".to_string()));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str { &self.0 }
}
impl std::fmt::Display for Email {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

View File

@@ -0,0 +1,43 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FilterOperator {
Equals,
NotEquals,
Contains,
Between,
GreaterThan,
LessThan,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FilterCondition {
pub field: String,
pub op: FilterOperator,
pub value: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FilterCriteria {
And(Vec<FilterCriteria>),
Or(Vec<FilterCriteria>),
Condition(FilterCondition),
}
impl FilterCriteria {
pub fn and(conditions: Vec<FilterCriteria>) -> Self {
Self::And(conditions)
}
pub fn or(conditions: Vec<FilterCriteria>) -> Self {
Self::Or(conditions)
}
pub fn condition(field: impl Into<String>, op: FilterOperator, value: serde_json::Value) -> Self {
Self::Condition(FilterCondition {
field: field.into(),
op,
value,
})
}
}

View File

@@ -0,0 +1,15 @@
mod checksum;
mod date_time_stamp;
mod email;
pub mod filter_criteria;
mod password;
mod structured_data;
mod system_id;
pub use checksum::Checksum;
pub use date_time_stamp::DateTimeStamp;
pub use email::Email;
pub use filter_criteria::{FilterCondition, FilterCriteria, FilterOperator};
pub use password::PasswordHash;
pub use structured_data::{MetadataValue, StructuredData};
pub use system_id::SystemId;

View File

@@ -0,0 +1,14 @@
// Manual Debug — redacts hash to prevent it appearing in logs
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct PasswordHash(String);
impl std::fmt::Debug for PasswordHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("PasswordHash").field(&"[redacted]").finish()
}
}
impl PasswordHash {
pub fn from_hash(hash: String) -> Self { Self(hash) }
pub fn as_str(&self) -> &str { &self.0 }
}

View File

@@ -0,0 +1,54 @@
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum MetadataValue {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Null,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct StructuredData(HashMap<String, MetadataValue>);
impl StructuredData {
pub fn new() -> Self { Self(HashMap::new()) }
pub fn insert(&mut self, key: impl Into<String>, value: MetadataValue) {
self.0.insert(key.into(), value);
}
pub fn get(&self, key: &str) -> Option<&MetadataValue> {
self.0.get(key)
}
pub fn get_string(&self, key: &str) -> Option<&str> {
match self.0.get(key) {
Some(MetadataValue::String(s)) => Some(s.as_str()),
_ => None,
}
}
pub fn keys(&self) -> impl Iterator<Item = &String> {
self.0.keys()
}
pub fn is_empty(&self) -> bool { self.0.is_empty() }
pub fn len(&self) -> usize { self.0.len() }
pub fn merge_from(&mut self, other: StructuredData) {
self.0.extend(other.0);
}
pub fn remove(&mut self, key: &str) -> Option<MetadataValue> {
self.0.remove(key)
}
pub fn inner(&self) -> &HashMap<String, MetadataValue> { &self.0 }
}
impl Default for StructuredData {
fn default() -> Self { Self::new() }
}

View File

@@ -0,0 +1,24 @@
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct SystemId(Uuid);
impl SystemId {
pub fn new() -> Self { Self(Uuid::new_v4()) }
pub fn from_uuid(id: Uuid) -> Self { Self(id) }
pub fn as_uuid(&self) -> &Uuid { &self.0 }
}
impl Default for SystemId {
fn default() -> Self { Self::new() }
}
impl std::fmt::Display for SystemId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<Uuid> for SystemId {
fn from(id: Uuid) -> Self { Self(id) }
}