Skip to main content

backbone_bucket/domain/entity/
content_hash.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5
6use super::StorageBackend;
7use super::AuditMetadata;
8
9use super::*;
10
11use thiserror::Error;
12
13/// Domain error for deduperror operations
14#[derive(Debug, Clone, Error)]
15pub enum DedupError {
16    #[error("{0}")]
17    Message(String),
18
19    #[error("Not found: {0}")]
20    NotFound(String),
21
22    #[error("Validation failed: {0}")]
23    ValidationFailed(String),
24
25    #[error("Conflict: {0}")]
26    Conflict(String),
27}
28
29impl From<String> for DedupError {
30    fn from(msg: String) -> Self { Self::Message(msg) }
31}
32
33impl From<&str> for DedupError {
34    fn from(msg: &str) -> Self { Self::Message(msg.to_string()) }
35}
36
37
38/// Strongly-typed ID for ContentHash
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
40#[serde(transparent)]
41pub struct ContentHashId(pub Uuid);
42
43impl ContentHashId {
44    pub fn new(id: Uuid) -> Self { Self(id) }
45    pub fn generate() -> Self { Self(Uuid::new_v4()) }
46    pub fn into_inner(self) -> Uuid { self.0 }
47}
48
49impl std::fmt::Display for ContentHashId {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(f, "{}", self.0)
52    }
53}
54
55impl std::str::FromStr for ContentHashId {
56    type Err = uuid::Error;
57    fn from_str(s: &str) -> Result<Self, Self::Err> {
58        Ok(Self(Uuid::parse_str(s)?))
59    }
60}
61
62impl From<Uuid> for ContentHashId {
63    fn from(id: Uuid) -> Self { Self(id) }
64}
65
66impl From<ContentHashId> for Uuid {
67    fn from(id: ContentHashId) -> Self { id.0 }
68}
69
70impl AsRef<Uuid> for ContentHashId {
71    fn as_ref(&self) -> &Uuid { &self.0 }
72}
73
74impl std::ops::Deref for ContentHashId {
75    type Target = Uuid;
76    fn deref(&self) -> &Self::Target { &self.0 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
80pub struct ContentHash {
81    pub id: Uuid,
82    pub hash: String,
83    pub size_bytes: i64,
84    pub storage_key: String,
85    pub storage_backend: StorageBackend,
86    pub reference_count: i32,
87    pub first_uploaded_at: DateTime<Utc>,
88    pub last_referenced_at: DateTime<Utc>,
89    pub fingerprint: Option<String>,
90    #[serde(default)]
91    #[sqlx(json)]
92    pub metadata: AuditMetadata,
93}
94
95impl ContentHash {
96    /// Create a builder for ContentHash
97    pub fn builder() -> ContentHashBuilder {
98        <ContentHashBuilder as Default>::default()
99    }
100
101    /// Create a new ContentHash with required fields
102    pub fn new(hash: String, size_bytes: i64, storage_key: String, storage_backend: StorageBackend, reference_count: i32, first_uploaded_at: DateTime<Utc>, last_referenced_at: DateTime<Utc>) -> Self {
103        Self {
104            id: Uuid::new_v4(),
105            hash,
106            size_bytes,
107            storage_key,
108            storage_backend,
109            reference_count,
110            first_uploaded_at,
111            last_referenced_at,
112            fingerprint: None,
113            metadata: AuditMetadata::default(),
114        }
115    }
116
117    /// Get the entity's unique identifier
118    pub fn id(&self) -> &Uuid {
119        &self.id
120    }
121
122    /// Get a strongly-typed ID for this entity
123    pub fn typed_id(&self) -> ContentHashId {
124        ContentHashId(self.id)
125    }
126
127    /// Get when this entity was created
128    pub fn created_at(&self) -> Option<&DateTime<Utc>> {
129        self.metadata.created_at.as_ref()
130    }
131
132    /// Get when this entity was last updated
133    pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
134        self.metadata.updated_at.as_ref()
135    }
136
137    /// Check if this entity is soft deleted
138    pub fn is_deleted(&self) -> bool {
139        self.metadata.deleted_at.is_some()
140    }
141
142    /// Check if this entity is active (not deleted)
143    pub fn is_active(&self) -> bool {
144        self.metadata.deleted_at.is_none()
145    }
146
147    /// Get when this entity was deleted
148    pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
149        self.metadata.deleted_at.as_ref()
150    }
151
152    /// Get who created this entity
153    pub fn created_by(&self) -> Option<&Uuid> {
154        self.metadata.created_by.as_ref()
155    }
156
157    /// Get who last updated this entity
158    pub fn updated_by(&self) -> Option<&Uuid> {
159        self.metadata.updated_by.as_ref()
160    }
161
162    /// Get who deleted this entity
163    pub fn deleted_by(&self) -> Option<&Uuid> {
164        self.metadata.deleted_by.as_ref()
165    }
166
167
168    // ==========================================================
169    // Fluent Setters (with_* for optional fields)
170    // ==========================================================
171
172    /// Set the fingerprint field (chainable)
173    pub fn with_fingerprint(mut self, value: String) -> Self {
174        self.fingerprint = Some(value);
175        self
176    }
177
178    // ==========================================================
179    // Partial Update
180    // ==========================================================
181
182    /// Apply partial updates from a map of field name to JSON value
183    pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
184        for (key, value) in fields {
185            match key.as_str() {
186                "hash" => {
187                    if let Ok(v) = serde_json::from_value(value) { self.hash = v; }
188                }
189                "size_bytes" => {
190                    if let Ok(v) = serde_json::from_value(value) { self.size_bytes = v; }
191                }
192                "storage_key" => {
193                    if let Ok(v) = serde_json::from_value(value) { self.storage_key = v; }
194                }
195                "storage_backend" => {
196                    if let Ok(v) = serde_json::from_value(value) { self.storage_backend = v; }
197                }
198                "reference_count" => {
199                    if let Ok(v) = serde_json::from_value(value) { self.reference_count = v; }
200                }
201                "first_uploaded_at" => {
202                    if let Ok(v) = serde_json::from_value(value) { self.first_uploaded_at = v; }
203                }
204                "last_referenced_at" => {
205                    if let Ok(v) = serde_json::from_value(value) { self.last_referenced_at = v; }
206                }
207                "fingerprint" => {
208                    if let Ok(v) = serde_json::from_value(value) { self.fingerprint = v; }
209                }
210                _ => {} // ignore unknown fields
211            }
212        }
213    }
214
215}
216
217impl super::Entity for ContentHash {
218    type Id = Uuid;
219
220    fn entity_id(&self) -> &Self::Id {
221        &self.id
222    }
223
224    fn entity_type() -> &'static str {
225        "ContentHash"
226    }
227}
228
229impl backbone_core::PersistentEntity for ContentHash {
230    fn entity_id(&self) -> String {
231        self.id.to_string()
232    }
233    fn set_entity_id(&mut self, id: String) {
234        if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
235            self.id = uuid;
236        }
237    }
238    fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
239        self.metadata.created_at
240    }
241    fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
242        self.metadata.created_at = Some(ts);
243    }
244    fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
245        self.metadata.updated_at
246    }
247    fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
248        self.metadata.updated_at = Some(ts);
249    }
250    fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
251        self.metadata.deleted_at
252    }
253    fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
254        self.metadata.deleted_at = ts;
255    }
256}
257
258impl backbone_orm::EntityRepoMeta for ContentHash {
259    fn column_types() -> std::collections::HashMap<String, String> {
260        let mut m = std::collections::HashMap::new();
261        m.insert("id".to_string(), "uuid".to_string());
262        m.insert("storage_backend".to_string(), "storage_backend".to_string());
263        m.insert("first_uploaded_at".to_string(), "timestamptz".to_string());
264        m.insert("last_referenced_at".to_string(), "timestamptz".to_string());
265        m
266    }
267    fn search_fields() -> &'static [&'static str] {
268        &["hash", "storage_key"]
269    }
270}
271
272/// Builder for ContentHash entity
273///
274/// Provides a fluent API for constructing ContentHash instances.
275/// System fields (id, metadata, timestamps) are auto-initialized.
276#[derive(Debug, Clone, Default)]
277pub struct ContentHashBuilder {
278    hash: Option<String>,
279    size_bytes: Option<i64>,
280    storage_key: Option<String>,
281    storage_backend: Option<StorageBackend>,
282    reference_count: Option<i32>,
283    first_uploaded_at: Option<DateTime<Utc>>,
284    last_referenced_at: Option<DateTime<Utc>>,
285    fingerprint: Option<String>,
286}
287
288impl ContentHashBuilder {
289    /// Set the hash field (required)
290    pub fn hash(mut self, value: String) -> Self {
291        self.hash = Some(value);
292        self
293    }
294
295    /// Set the size_bytes field (required)
296    pub fn size_bytes(mut self, value: i64) -> Self {
297        self.size_bytes = Some(value);
298        self
299    }
300
301    /// Set the storage_key field (required)
302    pub fn storage_key(mut self, value: String) -> Self {
303        self.storage_key = Some(value);
304        self
305    }
306
307    /// Set the storage_backend field (default: `StorageBackend::default()`)
308    pub fn storage_backend(mut self, value: StorageBackend) -> Self {
309        self.storage_backend = Some(value);
310        self
311    }
312
313    /// Set the reference_count field (default: `1`)
314    pub fn reference_count(mut self, value: i32) -> Self {
315        self.reference_count = Some(value);
316        self
317    }
318
319    /// Set the first_uploaded_at field (default: `Utc::now()`)
320    pub fn first_uploaded_at(mut self, value: DateTime<Utc>) -> Self {
321        self.first_uploaded_at = Some(value);
322        self
323    }
324
325    /// Set the last_referenced_at field (default: `Utc::now()`)
326    pub fn last_referenced_at(mut self, value: DateTime<Utc>) -> Self {
327        self.last_referenced_at = Some(value);
328        self
329    }
330
331    /// Set the fingerprint field (optional)
332    pub fn fingerprint(mut self, value: String) -> Self {
333        self.fingerprint = Some(value);
334        self
335    }
336
337    /// Build the ContentHash entity
338    ///
339    /// Returns Err if any required field without a default is missing.
340    pub fn build(self) -> Result<ContentHash, String> {
341        let hash = self.hash.ok_or_else(|| "hash is required".to_string())?;
342        let size_bytes = self.size_bytes.ok_or_else(|| "size_bytes is required".to_string())?;
343        let storage_key = self.storage_key.ok_or_else(|| "storage_key is required".to_string())?;
344
345        Ok(ContentHash {
346            id: Uuid::new_v4(),
347            hash,
348            size_bytes,
349            storage_key,
350            storage_backend: self.storage_backend.unwrap_or_default(),
351            reference_count: self.reference_count.unwrap_or(1),
352            first_uploaded_at: self.first_uploaded_at.unwrap_or(Utc::now()),
353            last_referenced_at: self.last_referenced_at.unwrap_or(Utc::now()),
354            fingerprint: self.fingerprint,
355            metadata: AuditMetadata::default(),
356        })
357    }
358}
359
360// This file's extension (hand-written; ADR-0031).
361#[path = "content_hash.ext.rs"]
362mod content_hash_ext;
363pub use content_hash_ext::*;