Skip to main content

backbone_bucket/domain/entity/
file_lock.rs

1use chrono::{DateTime, Utc, Duration};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5
6use super::LockStatus;
7use super::AuditMetadata;
8
9use super::*;
10
11use crate::domain::state_machine::{FileLockStateMachine, FileLockState, StateMachineError};
12
13use thiserror::Error;
14
15/// Domain error for lockerror operations
16#[derive(Debug, Clone, Error)]
17pub enum LockError {
18    #[error("{0}")]
19    Message(String),
20
21    #[error("Not found: {0}")]
22    NotFound(String),
23
24    #[error("Validation failed: {0}")]
25    ValidationFailed(String),
26
27    #[error("Conflict: {0}")]
28    Conflict(String),
29}
30
31impl From<String> for LockError {
32    fn from(msg: String) -> Self { Self::Message(msg) }
33}
34
35impl From<&str> for LockError {
36    fn from(msg: &str) -> Self { Self::Message(msg.to_string()) }
37}
38
39
40/// Strongly-typed ID for FileLock
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42#[serde(transparent)]
43pub struct FileLockId(pub Uuid);
44
45impl FileLockId {
46    pub fn new(id: Uuid) -> Self { Self(id) }
47    pub fn generate() -> Self { Self(Uuid::new_v4()) }
48    pub fn into_inner(self) -> Uuid { self.0 }
49}
50
51impl std::fmt::Display for FileLockId {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        write!(f, "{}", self.0)
54    }
55}
56
57impl std::str::FromStr for FileLockId {
58    type Err = uuid::Error;
59    fn from_str(s: &str) -> Result<Self, Self::Err> {
60        Ok(Self(Uuid::parse_str(s)?))
61    }
62}
63
64impl From<Uuid> for FileLockId {
65    fn from(id: Uuid) -> Self { Self(id) }
66}
67
68impl From<FileLockId> for Uuid {
69    fn from(id: FileLockId) -> Self { id.0 }
70}
71
72impl AsRef<Uuid> for FileLockId {
73    fn as_ref(&self) -> &Uuid { &self.0 }
74}
75
76impl std::ops::Deref for FileLockId {
77    type Target = Uuid;
78    fn deref(&self) -> &Self::Target { &self.0 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
82pub struct FileLock {
83    pub id: Uuid,
84    pub file_id: Uuid,
85    pub user_id: Uuid,
86    pub locked_at: DateTime<Utc>,
87    pub expires_at: DateTime<Utc>,
88    pub refreshed_at: Option<DateTime<Utc>>,
89    pub(crate) status: LockStatus,
90    #[serde(default)]
91    #[sqlx(json)]
92    pub metadata: AuditMetadata,
93}
94
95impl FileLock {
96    /// Create a builder for FileLock
97    pub fn builder() -> FileLockBuilder {
98        <FileLockBuilder as Default>::default()
99    }
100
101    /// Create a new FileLock with required fields
102    pub fn new(file_id: Uuid, user_id: Uuid, locked_at: DateTime<Utc>, expires_at: DateTime<Utc>, status: LockStatus) -> Self {
103        Self {
104            id: Uuid::new_v4(),
105            file_id,
106            user_id,
107            locked_at,
108            expires_at,
109            refreshed_at: None,
110            status,
111            metadata: AuditMetadata::default(),
112        }
113    }
114
115    /// Get the entity's unique identifier
116    pub fn id(&self) -> &Uuid {
117        &self.id
118    }
119
120    /// Get a strongly-typed ID for this entity
121    pub fn typed_id(&self) -> FileLockId {
122        FileLockId(self.id)
123    }
124
125    /// Get when this entity was created
126    pub fn created_at(&self) -> Option<&DateTime<Utc>> {
127        self.metadata.created_at.as_ref()
128    }
129
130    /// Get when this entity was last updated
131    pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
132        self.metadata.updated_at.as_ref()
133    }
134
135    /// Check if this entity is soft deleted
136    pub fn is_deleted(&self) -> bool {
137        self.metadata.deleted_at.is_some()
138    }
139
140    /// Check if this entity is active (not deleted)
141    pub fn is_active(&self) -> bool {
142        self.metadata.deleted_at.is_none()
143    }
144
145    /// Get when this entity was deleted
146    pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
147        self.metadata.deleted_at.as_ref()
148    }
149
150    /// Get who created this entity
151    pub fn created_by(&self) -> Option<&Uuid> {
152        self.metadata.created_by.as_ref()
153    }
154
155    /// Get who last updated this entity
156    pub fn updated_by(&self) -> Option<&Uuid> {
157        self.metadata.updated_by.as_ref()
158    }
159
160    /// Get who deleted this entity
161    pub fn deleted_by(&self) -> Option<&Uuid> {
162        self.metadata.deleted_by.as_ref()
163    }
164
165    /// Get the current status
166    pub fn status(&self) -> &LockStatus {
167        &self.status
168    }
169
170
171    // ==========================================================
172    // Fluent Setters (with_* for optional fields)
173    // ==========================================================
174
175    /// Set the refreshed_at field (chainable)
176    pub fn with_refreshed_at(mut self, value: DateTime<Utc>) -> Self {
177        self.refreshed_at = Some(value);
178        self
179    }
180
181    // ==========================================================
182    // State Machine
183    // ==========================================================
184
185    /// Transition to a new state via the status state machine.
186    ///
187    /// Returns `Err` if the transition is not permitted from the current state.
188    /// Use this method instead of assigning `self.status` directly.
189    pub fn transition_to(&mut self, new_state: FileLockState) -> Result<(), StateMachineError> {
190        let current = self.status.to_string().parse::<FileLockState>()?;
191        let mut sm = FileLockStateMachine::from_state(current);
192        sm.transition_to_state(new_state)?;
193        self.status = new_state.to_string().parse::<LockStatus>()
194            .map_err(|e| StateMachineError::InvalidState(e.to_string()))?;
195        Ok(())
196    }
197
198    // ==========================================================
199    // Partial Update
200    // ==========================================================
201
202    /// Apply partial updates from a map of field name to JSON value
203    pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
204        for (key, value) in fields {
205            match key.as_str() {
206                "file_id" => {
207                    if let Ok(v) = serde_json::from_value(value) { self.file_id = v; }
208                }
209                "user_id" => {
210                    if let Ok(v) = serde_json::from_value(value) { self.user_id = v; }
211                }
212                "locked_at" => {
213                    if let Ok(v) = serde_json::from_value(value) { self.locked_at = v; }
214                }
215                "expires_at" => {
216                    if let Ok(v) = serde_json::from_value(value) { self.expires_at = v; }
217                }
218                "refreshed_at" => {
219                    if let Ok(v) = serde_json::from_value(value) { self.refreshed_at = v; }
220                }
221                _ => {} // ignore unknown fields
222            }
223        }
224    }
225
226    // <<< CUSTOM METHODS START >>>
227
228    // ==========================================================
229    // DDD Entity Methods
230    // ==========================================================
231
232    /// Check if lock is still valid (active status and not yet expired)
233    pub fn is_valid(&self) -> bool {
234        self.status == LockStatus::Active && !self.is_expired()
235    }
236
237    /// Check if lock has expired (expires_at is at or before the current time)
238    pub fn is_expired(&self) -> bool {
239        self.expires_at <= Utc::now()
240    }
241
242    /// Check if lock belongs to the given user
243    pub fn is_owned_by(&self, user_id: Uuid) -> bool {
244        self.user_id == user_id
245    }
246
247    /// Check if lock can be refreshed (must be currently valid)
248    pub fn can_refresh(&self) -> bool {
249        self.is_valid()
250    }
251
252    /// Refresh lock expiry time by extending it by the given duration
253    pub fn refresh(&mut self, duration: Duration) -> Result<(), LockError> {
254        if !self.can_refresh() {
255            return Err(LockError::ValidationFailed(
256                "Cannot refresh lock: lock is not active or has expired".to_string(),
257            ));
258        }
259
260        let now = Utc::now();
261        self.expires_at = now + duration;
262        self.refreshed_at = Some(now);
263        self.metadata.touch();
264
265        Ok(())
266    }
267
268    /// Get remaining time until expiry. Returns zero duration if already expired.
269    pub fn time_remaining(&self) -> Duration {
270        let remaining = self.expires_at - Utc::now();
271        if remaining < Duration::zero() {
272            Duration::zero()
273        } else {
274            remaining
275        }
276    }
277
278    /// Check all business invariants
279    pub fn check_invariants(&self) -> Result<(), Vec<&'static str>> {
280        let mut errors = Vec::new();
281
282        // Invariant 1: expires_at must be after locked_at
283        if self.expires_at <= self.locked_at {
284            errors.push("expires_at must be after locked_at");
285        }
286
287        // Invariant 2: if status is Active, it must not be expired
288        if self.status == LockStatus::Active && self.is_expired() {
289            errors.push("active lock must not be expired (expires_at must be in the future)");
290        }
291
292        if errors.is_empty() { Ok(()) } else { Err(errors) }
293    }
294    // <<< CUSTOM METHODS END >>>
295}
296
297impl super::Entity for FileLock {
298    type Id = Uuid;
299
300    fn entity_id(&self) -> &Self::Id {
301        &self.id
302    }
303
304    fn entity_type() -> &'static str {
305        "FileLock"
306    }
307}
308
309impl backbone_core::PersistentEntity for FileLock {
310    fn entity_id(&self) -> String {
311        self.id.to_string()
312    }
313    fn set_entity_id(&mut self, id: String) {
314        if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
315            self.id = uuid;
316        }
317    }
318    fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
319        self.metadata.created_at
320    }
321    fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
322        self.metadata.created_at = Some(ts);
323    }
324    fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
325        self.metadata.updated_at
326    }
327    fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
328        self.metadata.updated_at = Some(ts);
329    }
330    fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
331        self.metadata.deleted_at
332    }
333    fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
334        self.metadata.deleted_at = ts;
335    }
336}
337
338impl backbone_orm::EntityRepoMeta for FileLock {
339    fn column_types() -> std::collections::HashMap<String, String> {
340        let mut m = std::collections::HashMap::new();
341        m.insert("id".to_string(), "uuid".to_string());
342        m.insert("file_id".to_string(), "uuid".to_string());
343        m.insert("user_id".to_string(), "uuid".to_string());
344        m.insert("status".to_string(), "lock_status".to_string());
345        m.insert("locked_at".to_string(), "timestamptz".to_string());
346        m.insert("expires_at".to_string(), "timestamptz".to_string());
347        m.insert("refreshed_at".to_string(), "timestamptz".to_string());
348        m
349    }
350    fn search_fields() -> &'static [&'static str] {
351        &[]
352    }
353}
354
355/// Builder for FileLock entity
356///
357/// Provides a fluent API for constructing FileLock instances.
358/// System fields (id, metadata, timestamps) are auto-initialized.
359#[derive(Debug, Clone, Default)]
360pub struct FileLockBuilder {
361    file_id: Option<Uuid>,
362    user_id: Option<Uuid>,
363    locked_at: Option<DateTime<Utc>>,
364    expires_at: Option<DateTime<Utc>>,
365    refreshed_at: Option<DateTime<Utc>>,
366    status: Option<LockStatus>,
367}
368
369impl FileLockBuilder {
370    /// Set the file_id field (required)
371    pub fn file_id(mut self, value: Uuid) -> Self {
372        self.file_id = Some(value);
373        self
374    }
375
376    /// Set the user_id field (required)
377    pub fn user_id(mut self, value: Uuid) -> Self {
378        self.user_id = Some(value);
379        self
380    }
381
382    /// Set the locked_at field (default: `Utc::now()`)
383    pub fn locked_at(mut self, value: DateTime<Utc>) -> Self {
384        self.locked_at = Some(value);
385        self
386    }
387
388    /// Set the expires_at field (required)
389    pub fn expires_at(mut self, value: DateTime<Utc>) -> Self {
390        self.expires_at = Some(value);
391        self
392    }
393
394    /// Set the refreshed_at field (optional)
395    pub fn refreshed_at(mut self, value: DateTime<Utc>) -> Self {
396        self.refreshed_at = Some(value);
397        self
398    }
399
400    /// Set the status field (default: `LockStatus::default()`)
401    pub fn status(mut self, value: LockStatus) -> Self {
402        self.status = Some(value);
403        self
404    }
405
406    /// Build the FileLock entity
407    ///
408    /// Returns Err if any required field without a default is missing.
409    pub fn build(self) -> Result<FileLock, String> {
410        let file_id = self.file_id.ok_or_else(|| "file_id is required".to_string())?;
411        let user_id = self.user_id.ok_or_else(|| "user_id is required".to_string())?;
412        let expires_at = self.expires_at.ok_or_else(|| "expires_at is required".to_string())?;
413
414        Ok(FileLock {
415            id: Uuid::new_v4(),
416            file_id,
417            user_id,
418            locked_at: self.locked_at.unwrap_or(Utc::now()),
419            expires_at,
420            refreshed_at: self.refreshed_at,
421            status: self.status.unwrap_or_default(),
422            metadata: AuditMetadata::default(),
423        })
424    }
425}