backbone_bucket/domain/entity/
file_lock.rs1use 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#[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#[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 pub fn builder() -> FileLockBuilder {
98 <FileLockBuilder as Default>::default()
99 }
100
101 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 pub fn id(&self) -> &Uuid {
117 &self.id
118 }
119
120 pub fn typed_id(&self) -> FileLockId {
122 FileLockId(self.id)
123 }
124
125 pub fn created_at(&self) -> Option<&DateTime<Utc>> {
127 self.metadata.created_at.as_ref()
128 }
129
130 pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
132 self.metadata.updated_at.as_ref()
133 }
134
135 pub fn is_deleted(&self) -> bool {
137 self.metadata.deleted_at.is_some()
138 }
139
140 pub fn is_active(&self) -> bool {
142 self.metadata.deleted_at.is_none()
143 }
144
145 pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
147 self.metadata.deleted_at.as_ref()
148 }
149
150 pub fn created_by(&self) -> Option<&Uuid> {
152 self.metadata.created_by.as_ref()
153 }
154
155 pub fn updated_by(&self) -> Option<&Uuid> {
157 self.metadata.updated_by.as_ref()
158 }
159
160 pub fn deleted_by(&self) -> Option<&Uuid> {
162 self.metadata.deleted_by.as_ref()
163 }
164
165 pub fn status(&self) -> &LockStatus {
167 &self.status
168 }
169
170
171 pub fn with_refreshed_at(mut self, value: DateTime<Utc>) -> Self {
177 self.refreshed_at = Some(value);
178 self
179 }
180
181 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 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 _ => {} }
223 }
224 }
225
226}
227
228impl super::Entity for FileLock {
229 type Id = Uuid;
230
231 fn entity_id(&self) -> &Self::Id {
232 &self.id
233 }
234
235 fn entity_type() -> &'static str {
236 "FileLock"
237 }
238}
239
240impl backbone_core::PersistentEntity for FileLock {
241 fn entity_id(&self) -> String {
242 self.id.to_string()
243 }
244 fn set_entity_id(&mut self, id: String) {
245 if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
246 self.id = uuid;
247 }
248 }
249 fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
250 self.metadata.created_at
251 }
252 fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
253 self.metadata.created_at = Some(ts);
254 }
255 fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
256 self.metadata.updated_at
257 }
258 fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
259 self.metadata.updated_at = Some(ts);
260 }
261 fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
262 self.metadata.deleted_at
263 }
264 fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
265 self.metadata.deleted_at = ts;
266 }
267}
268
269impl backbone_orm::EntityRepoMeta for FileLock {
270 fn column_types() -> std::collections::HashMap<String, String> {
271 let mut m = std::collections::HashMap::new();
272 m.insert("id".to_string(), "uuid".to_string());
273 m.insert("file_id".to_string(), "uuid".to_string());
274 m.insert("user_id".to_string(), "uuid".to_string());
275 m.insert("status".to_string(), "lock_status".to_string());
276 m.insert("locked_at".to_string(), "timestamptz".to_string());
277 m.insert("expires_at".to_string(), "timestamptz".to_string());
278 m.insert("refreshed_at".to_string(), "timestamptz".to_string());
279 m
280 }
281 fn search_fields() -> &'static [&'static str] {
282 &[]
283 }
284}
285
286#[derive(Debug, Clone, Default)]
291pub struct FileLockBuilder {
292 file_id: Option<Uuid>,
293 user_id: Option<Uuid>,
294 locked_at: Option<DateTime<Utc>>,
295 expires_at: Option<DateTime<Utc>>,
296 refreshed_at: Option<DateTime<Utc>>,
297 status: Option<LockStatus>,
298}
299
300impl FileLockBuilder {
301 pub fn file_id(mut self, value: Uuid) -> Self {
303 self.file_id = Some(value);
304 self
305 }
306
307 pub fn user_id(mut self, value: Uuid) -> Self {
309 self.user_id = Some(value);
310 self
311 }
312
313 pub fn locked_at(mut self, value: DateTime<Utc>) -> Self {
315 self.locked_at = Some(value);
316 self
317 }
318
319 pub fn expires_at(mut self, value: DateTime<Utc>) -> Self {
321 self.expires_at = Some(value);
322 self
323 }
324
325 pub fn refreshed_at(mut self, value: DateTime<Utc>) -> Self {
327 self.refreshed_at = Some(value);
328 self
329 }
330
331 pub fn status(mut self, value: LockStatus) -> Self {
333 self.status = Some(value);
334 self
335 }
336
337 pub fn build(self) -> Result<FileLock, String> {
341 let file_id = self.file_id.ok_or_else(|| "file_id is required".to_string())?;
342 let user_id = self.user_id.ok_or_else(|| "user_id is required".to_string())?;
343 let expires_at = self.expires_at.ok_or_else(|| "expires_at is required".to_string())?;
344
345 Ok(FileLock {
346 id: Uuid::new_v4(),
347 file_id,
348 user_id,
349 locked_at: self.locked_at.unwrap_or(Utc::now()),
350 expires_at,
351 refreshed_at: self.refreshed_at,
352 status: self.status.unwrap_or_default(),
353 metadata: AuditMetadata::default(),
354 })
355 }
356}
357
358#[path = "file_lock.ext.rs"]
360mod file_lock_ext;
361pub use file_lock_ext::*;