backbone_bucket/domain/entity/
file_comment.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5
6use super::CommentStatus;
7use super::AuditMetadata;
8
9use super::*;
10
11use crate::domain::state_machine::{FileCommentStateMachine, FileCommentState, StateMachineError};
12
13use thiserror::Error;
14
15#[derive(Debug, Clone, Error)]
17pub enum CommentError {
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 CommentError {
32 fn from(msg: String) -> Self { Self::Message(msg) }
33}
34
35impl From<&str> for CommentError {
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 FileCommentId(pub Uuid);
44
45impl FileCommentId {
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 FileCommentId {
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 FileCommentId {
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 FileCommentId {
65 fn from(id: Uuid) -> Self { Self(id) }
66}
67
68impl From<FileCommentId> for Uuid {
69 fn from(id: FileCommentId) -> Self { id.0 }
70}
71
72impl AsRef<Uuid> for FileCommentId {
73 fn as_ref(&self) -> &Uuid { &self.0 }
74}
75
76impl std::ops::Deref for FileCommentId {
77 type Target = Uuid;
78 fn deref(&self) -> &Self::Target { &self.0 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
82pub struct FileComment {
83 pub id: Uuid,
84 pub file_id: Uuid,
85 pub user_id: Uuid,
86 pub parent_id: Option<Uuid>,
87 pub content: String,
88 pub annotation_region: Option<serde_json::Value>,
89 pub mentions: Vec<Uuid>,
90 pub resolved: bool,
91 pub resolved_by: Option<Uuid>,
92 pub resolved_at: Option<DateTime<Utc>>,
93 pub(crate) status: CommentStatus,
94 #[serde(default)]
95 #[sqlx(json)]
96 pub metadata: AuditMetadata,
97}
98
99impl FileComment {
100 pub fn builder() -> FileCommentBuilder {
102 <FileCommentBuilder as Default>::default()
103 }
104
105 pub fn new(file_id: Uuid, user_id: Uuid, content: String, mentions: Vec<Uuid>, resolved: bool, status: CommentStatus) -> Self {
107 Self {
108 id: Uuid::new_v4(),
109 file_id,
110 user_id,
111 parent_id: None,
112 content,
113 annotation_region: None,
114 mentions,
115 resolved,
116 resolved_by: None,
117 resolved_at: None,
118 status,
119 metadata: AuditMetadata::default(),
120 }
121 }
122
123 pub fn id(&self) -> &Uuid {
125 &self.id
126 }
127
128 pub fn typed_id(&self) -> FileCommentId {
130 FileCommentId(self.id)
131 }
132
133 pub fn created_at(&self) -> Option<&DateTime<Utc>> {
135 self.metadata.created_at.as_ref()
136 }
137
138 pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
140 self.metadata.updated_at.as_ref()
141 }
142
143 pub fn is_deleted(&self) -> bool {
145 self.metadata.deleted_at.is_some()
146 }
147
148 pub fn is_active(&self) -> bool {
150 self.metadata.deleted_at.is_none()
151 }
152
153 pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
155 self.metadata.deleted_at.as_ref()
156 }
157
158 pub fn created_by(&self) -> Option<&Uuid> {
160 self.metadata.created_by.as_ref()
161 }
162
163 pub fn updated_by(&self) -> Option<&Uuid> {
165 self.metadata.updated_by.as_ref()
166 }
167
168 pub fn deleted_by(&self) -> Option<&Uuid> {
170 self.metadata.deleted_by.as_ref()
171 }
172
173 pub fn status(&self) -> &CommentStatus {
175 &self.status
176 }
177
178
179 pub fn with_parent_id(mut self, value: Uuid) -> Self {
185 self.parent_id = Some(value);
186 self
187 }
188
189 pub fn with_annotation_region(mut self, value: serde_json::Value) -> Self {
191 self.annotation_region = Some(value);
192 self
193 }
194
195 pub fn with_resolved_by(mut self, value: Uuid) -> Self {
197 self.resolved_by = Some(value);
198 self
199 }
200
201 pub fn with_resolved_at(mut self, value: DateTime<Utc>) -> Self {
203 self.resolved_at = Some(value);
204 self
205 }
206
207 pub fn transition_to(&mut self, new_state: FileCommentState) -> Result<(), StateMachineError> {
216 let current = self.status.to_string().parse::<FileCommentState>()?;
217 let mut sm = FileCommentStateMachine::from_state(current);
218 sm.transition_to_state(new_state)?;
219 self.status = new_state.to_string().parse::<CommentStatus>()
220 .map_err(|e| StateMachineError::InvalidState(e.to_string()))?;
221 Ok(())
222 }
223
224 pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
230 for (key, value) in fields {
231 match key.as_str() {
232 "file_id" => {
233 if let Ok(v) = serde_json::from_value(value) { self.file_id = v; }
234 }
235 "user_id" => {
236 if let Ok(v) = serde_json::from_value(value) { self.user_id = v; }
237 }
238 "parent_id" => {
239 if let Ok(v) = serde_json::from_value(value) { self.parent_id = v; }
240 }
241 "content" => {
242 if let Ok(v) = serde_json::from_value(value) { self.content = v; }
243 }
244 "annotation_region" => {
245 if let Ok(v) = serde_json::from_value(value) { self.annotation_region = v; }
246 }
247 "mentions" => {
248 if let Ok(v) = serde_json::from_value(value) { self.mentions = v; }
249 }
250 "resolved" => {
251 if let Ok(v) = serde_json::from_value(value) { self.resolved = v; }
252 }
253 "resolved_by" => {
254 if let Ok(v) = serde_json::from_value(value) { self.resolved_by = v; }
255 }
256 "resolved_at" => {
257 if let Ok(v) = serde_json::from_value(value) { self.resolved_at = v; }
258 }
259 _ => {} }
261 }
262 }
263
264}
265
266impl super::Entity for FileComment {
267 type Id = Uuid;
268
269 fn entity_id(&self) -> &Self::Id {
270 &self.id
271 }
272
273 fn entity_type() -> &'static str {
274 "FileComment"
275 }
276}
277
278impl backbone_core::PersistentEntity for FileComment {
279 fn entity_id(&self) -> String {
280 self.id.to_string()
281 }
282 fn set_entity_id(&mut self, id: String) {
283 if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
284 self.id = uuid;
285 }
286 }
287 fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
288 self.metadata.created_at
289 }
290 fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
291 self.metadata.created_at = Some(ts);
292 }
293 fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
294 self.metadata.updated_at
295 }
296 fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
297 self.metadata.updated_at = Some(ts);
298 }
299 fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
300 self.metadata.deleted_at
301 }
302 fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
303 self.metadata.deleted_at = ts;
304 }
305}
306
307impl backbone_orm::EntityRepoMeta for FileComment {
308 fn column_types() -> std::collections::HashMap<String, String> {
309 let mut m = std::collections::HashMap::new();
310 m.insert("id".to_string(), "uuid".to_string());
311 m.insert("file_id".to_string(), "uuid".to_string());
312 m.insert("user_id".to_string(), "uuid".to_string());
313 m.insert("parent_id".to_string(), "uuid".to_string());
314 m.insert("status".to_string(), "comment_status".to_string());
315 m.insert("resolved_at".to_string(), "timestamptz".to_string());
316 m
317 }
318 fn search_fields() -> &'static [&'static str] {
319 &["content"]
320 }
321}
322
323#[derive(Debug, Clone, Default)]
328pub struct FileCommentBuilder {
329 file_id: Option<Uuid>,
330 user_id: Option<Uuid>,
331 parent_id: Option<Uuid>,
332 content: Option<String>,
333 annotation_region: Option<serde_json::Value>,
334 mentions: Option<Vec<Uuid>>,
335 resolved: Option<bool>,
336 resolved_by: Option<Uuid>,
337 resolved_at: Option<DateTime<Utc>>,
338 status: Option<CommentStatus>,
339}
340
341impl FileCommentBuilder {
342 pub fn file_id(mut self, value: Uuid) -> Self {
344 self.file_id = Some(value);
345 self
346 }
347
348 pub fn user_id(mut self, value: Uuid) -> Self {
350 self.user_id = Some(value);
351 self
352 }
353
354 pub fn parent_id(mut self, value: Uuid) -> Self {
356 self.parent_id = Some(value);
357 self
358 }
359
360 pub fn content(mut self, value: String) -> Self {
362 self.content = Some(value);
363 self
364 }
365
366 pub fn annotation_region(mut self, value: serde_json::Value) -> Self {
368 self.annotation_region = Some(value);
369 self
370 }
371
372 pub fn mentions(mut self, value: Vec<Uuid>) -> Self {
374 self.mentions = Some(value);
375 self
376 }
377
378 pub fn resolved(mut self, value: bool) -> Self {
380 self.resolved = Some(value);
381 self
382 }
383
384 pub fn resolved_by(mut self, value: Uuid) -> Self {
386 self.resolved_by = Some(value);
387 self
388 }
389
390 pub fn resolved_at(mut self, value: DateTime<Utc>) -> Self {
392 self.resolved_at = Some(value);
393 self
394 }
395
396 pub fn status(mut self, value: CommentStatus) -> Self {
398 self.status = Some(value);
399 self
400 }
401
402 pub fn build(self) -> Result<FileComment, String> {
406 let file_id = self.file_id.ok_or_else(|| "file_id is required".to_string())?;
407 let user_id = self.user_id.ok_or_else(|| "user_id is required".to_string())?;
408 let content = self.content.ok_or_else(|| "content is required".to_string())?;
409 let mentions = self.mentions.ok_or_else(|| "mentions is required".to_string())?;
410
411 Ok(FileComment {
412 id: Uuid::new_v4(),
413 file_id,
414 user_id,
415 parent_id: self.parent_id,
416 content,
417 annotation_region: self.annotation_region,
418 mentions,
419 resolved: self.resolved.unwrap_or(false),
420 resolved_by: self.resolved_by,
421 resolved_at: self.resolved_at,
422 status: self.status.unwrap_or_default(),
423 metadata: AuditMetadata::default(),
424 })
425 }
426}
427
428#[path = "file_comment.ext.rs"]
430mod file_comment_ext;
431pub use file_comment_ext::*;