refactor: restructure domain crate by bounded context
This commit is contained in:
251
crates/domain/src/catalog/entities.rs
Normal file
251
crates/domain/src/catalog/entities.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{Checksum, DateTimeStamp, StructuredData, SystemId};
|
||||
|
||||
// --- Asset ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum AssetType {
|
||||
Image,
|
||||
Video,
|
||||
LivePhoto,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SourceReference {
|
||||
pub volume_id: SystemId,
|
||||
pub relative_path: String,
|
||||
pub checksum: Checksum,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Asset {
|
||||
pub asset_id: SystemId,
|
||||
pub source_reference: SourceReference,
|
||||
pub asset_type: AssetType,
|
||||
pub mime_type: String,
|
||||
pub file_size: u64,
|
||||
pub is_processed: bool,
|
||||
pub owner_user_id: SystemId,
|
||||
pub created_at: DateTimeStamp,
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
pub fn new(
|
||||
source_reference: SourceReference,
|
||||
asset_type: AssetType,
|
||||
mime_type: impl Into<String>,
|
||||
file_size: u64,
|
||||
owner: SystemId,
|
||||
) -> Self {
|
||||
Self {
|
||||
asset_id: SystemId::new(),
|
||||
source_reference,
|
||||
asset_type,
|
||||
mime_type: mime_type.into(),
|
||||
file_size,
|
||||
is_processed: false,
|
||||
owner_user_id: owner,
|
||||
created_at: DateTimeStamp::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_processed(&mut self) {
|
||||
self.is_processed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- AssetMetadata ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||
pub enum MetadataSource {
|
||||
ExifExtracted,
|
||||
AiGenerated,
|
||||
UserEdited,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AssetMetadata {
|
||||
pub asset_id: SystemId,
|
||||
pub metadata_source: MetadataSource,
|
||||
pub data: StructuredData,
|
||||
pub updated_at: DateTimeStamp,
|
||||
}
|
||||
|
||||
impl AssetMetadata {
|
||||
pub fn new(asset_id: SystemId, source: MetadataSource, data: StructuredData) -> Self {
|
||||
Self {
|
||||
asset_id,
|
||||
metadata_source: source,
|
||||
data,
|
||||
updated_at: DateTimeStamp::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- AssetStack ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum StackType {
|
||||
LivePhoto,
|
||||
FormatPair,
|
||||
BurstSequence,
|
||||
ExposureBracket,
|
||||
ManualGroup,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum StackMemberRole {
|
||||
PrimaryDisplay,
|
||||
HighResSource,
|
||||
MotionClip,
|
||||
AlternateFrame,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AssetStackMember {
|
||||
pub asset_id: SystemId,
|
||||
pub role: StackMemberRole,
|
||||
pub sort_order: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AssetStack {
|
||||
pub stack_id: SystemId,
|
||||
pub stack_type: StackType,
|
||||
pub primary_asset_id: SystemId,
|
||||
pub owner_user_id: SystemId,
|
||||
pub members: Vec<AssetStackMember>,
|
||||
}
|
||||
|
||||
impl AssetStack {
|
||||
pub fn new(stack_type: StackType, primary_asset_id: SystemId, owner: SystemId) -> Self {
|
||||
let primary_member = AssetStackMember {
|
||||
asset_id: primary_asset_id,
|
||||
role: StackMemberRole::PrimaryDisplay,
|
||||
sort_order: 0,
|
||||
};
|
||||
Self {
|
||||
stack_id: SystemId::new(),
|
||||
stack_type,
|
||||
primary_asset_id,
|
||||
owner_user_id: owner,
|
||||
members: vec![primary_member],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_member(&mut self, asset_id: SystemId, role: StackMemberRole) -> Result<(), DomainError> {
|
||||
if self.members.iter().any(|m| m.asset_id == asset_id) {
|
||||
return Err(DomainError::Conflict(
|
||||
"Asset already exists in stack".to_string(),
|
||||
));
|
||||
}
|
||||
let next_order = self.members.iter().map(|m| m.sort_order).max().unwrap_or(0) + 1;
|
||||
self.members.push(AssetStackMember {
|
||||
asset_id,
|
||||
role,
|
||||
sort_order: next_order,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// --- DerivativeAsset ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DerivativeProfile {
|
||||
ThumbnailSquare,
|
||||
ThumbnailLarge,
|
||||
WebOptimized,
|
||||
VideoSd,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum GenerationStatus {
|
||||
Pending,
|
||||
Ready,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DerivativeAsset {
|
||||
pub derivative_id: SystemId,
|
||||
pub parent_asset_id: SystemId,
|
||||
pub profile_type: DerivativeProfile,
|
||||
pub storage_path: String,
|
||||
pub mime_type: String,
|
||||
pub file_size: u64,
|
||||
pub dimensions: (u32, u32),
|
||||
pub generation_status: GenerationStatus,
|
||||
}
|
||||
|
||||
impl DerivativeAsset {
|
||||
pub fn new_pending(parent: SystemId, profile: DerivativeProfile, path: impl Into<String>) -> Self {
|
||||
Self {
|
||||
derivative_id: SystemId::new(),
|
||||
parent_asset_id: parent,
|
||||
profile_type: profile,
|
||||
storage_path: path.into(),
|
||||
mime_type: String::new(),
|
||||
file_size: 0,
|
||||
dimensions: (0, 0),
|
||||
generation_status: GenerationStatus::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_ready(&mut self, mime_type: impl Into<String>, size: u64, dims: (u32, u32)) {
|
||||
self.mime_type = mime_type.into();
|
||||
self.file_size = size;
|
||||
self.dimensions = dims;
|
||||
self.generation_status = GenerationStatus::Ready;
|
||||
}
|
||||
|
||||
pub fn mark_failed(&mut self) {
|
||||
self.generation_status = GenerationStatus::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Duplicate ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DetectionMethod {
|
||||
ExactHash,
|
||||
PerceptualHash,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DuplicateStatus {
|
||||
Unresolved,
|
||||
Resolved,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DuplicateCandidate {
|
||||
pub asset_id: SystemId,
|
||||
pub similarity_score: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DuplicateGroup {
|
||||
pub group_id: SystemId,
|
||||
pub detection_method: DetectionMethod,
|
||||
pub status: DuplicateStatus,
|
||||
pub candidates: Vec<DuplicateCandidate>,
|
||||
}
|
||||
|
||||
impl DuplicateGroup {
|
||||
pub fn new_exact(asset_a: SystemId, asset_b: SystemId) -> Self {
|
||||
Self {
|
||||
group_id: SystemId::new(),
|
||||
detection_method: DetectionMethod::ExactHash,
|
||||
status: DuplicateStatus::Unresolved,
|
||||
candidates: vec![
|
||||
DuplicateCandidate { asset_id: asset_a, similarity_score: 1.0 },
|
||||
DuplicateCandidate { asset_id: asset_b, similarity_score: 1.0 },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve(&mut self) {
|
||||
self.status = DuplicateStatus::Resolved;
|
||||
}
|
||||
}
|
||||
7
crates/domain/src/catalog/mod.rs
Normal file
7
crates/domain/src/catalog/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod entities;
|
||||
pub mod ports;
|
||||
pub mod services;
|
||||
|
||||
pub use entities::*;
|
||||
pub use ports::*;
|
||||
pub use services::*;
|
||||
58
crates/domain/src/catalog/ports.rs
Normal file
58
crates/domain/src/catalog/ports.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{Checksum, SystemId};
|
||||
use super::entities::{
|
||||
Asset, AssetMetadata, AssetStack, DerivativeAsset, DerivativeProfile,
|
||||
DuplicateGroup, MetadataSource,
|
||||
};
|
||||
|
||||
// --- AssetRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait AssetRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Asset>, DomainError>;
|
||||
async fn find_by_checksum(&self, checksum: &Checksum) -> Result<Vec<Asset>, DomainError>;
|
||||
async fn find_by_owner(&self, owner_id: &SystemId, limit: u32, offset: u32) -> Result<Vec<Asset>, DomainError>;
|
||||
async fn save(&self, asset: &Asset) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- AssetMetadataRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait AssetMetadataRepository: Send + Sync {
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<AssetMetadata>, DomainError>;
|
||||
async fn find_by_asset_and_source(&self, asset_id: &SystemId, source: MetadataSource) -> Result<Option<AssetMetadata>, DomainError>;
|
||||
async fn save(&self, metadata: &AssetMetadata) -> Result<(), DomainError>;
|
||||
async fn delete_by_asset_and_source(&self, asset_id: &SystemId, source: MetadataSource) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- AssetStackRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait AssetStackRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<AssetStack>, DomainError>;
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<AssetStack>, DomainError>;
|
||||
async fn save(&self, stack: &AssetStack) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- DerivativeRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait DerivativeRepository: Send + Sync {
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<DerivativeAsset>, DomainError>;
|
||||
async fn find_by_asset_and_profile(&self, asset_id: &SystemId, profile: DerivativeProfile) -> Result<Option<DerivativeAsset>, DomainError>;
|
||||
async fn save(&self, derivative: &DerivativeAsset) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- DuplicateRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait DuplicateRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<DuplicateGroup>, DomainError>;
|
||||
async fn find_unresolved(&self) -> Result<Vec<DuplicateGroup>, DomainError>;
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<DuplicateGroup>, DomainError>;
|
||||
async fn save(&self, group: &DuplicateGroup) -> Result<(), DomainError>;
|
||||
}
|
||||
32
crates/domain/src/catalog/services.rs
Normal file
32
crates/domain/src/catalog/services.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use super::entities::{AssetMetadata, MetadataSource};
|
||||
use crate::common::value_objects::{MetadataValue, StructuredData};
|
||||
|
||||
/// Merge metadata layers by priority: ExifExtracted < AiGenerated < UserEdited.
|
||||
/// Later (higher-priority) layers overwrite earlier ones.
|
||||
pub fn resolve_metadata(layers: &[AssetMetadata]) -> StructuredData {
|
||||
let mut sorted = layers.to_vec();
|
||||
sorted.sort_by_key(|l| l.metadata_source);
|
||||
|
||||
let mut result = StructuredData::new();
|
||||
for layer in &sorted {
|
||||
result.merge_from(layer.data.clone());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Return the source and value for a field from the highest-priority layer that contains it.
|
||||
pub fn get_field_source<'a>(
|
||||
layers: &'a [AssetMetadata],
|
||||
field: &str,
|
||||
) -> Option<(MetadataSource, &'a MetadataValue)> {
|
||||
// Build index list sorted by metadata_source descending (highest priority first)
|
||||
let mut indices: Vec<usize> = (0..layers.len()).collect();
|
||||
indices.sort_by(|&a, &b| layers[b].metadata_source.cmp(&layers[a].metadata_source));
|
||||
|
||||
for i in indices {
|
||||
if let Some(val) = layers[i].data.get(field) {
|
||||
return Some((layers[i].metadata_source, val));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
Reference in New Issue
Block a user