mockforge_collab/
models.rs

1//! Core data models for collaboration
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// User role in a workspace
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
9#[sqlx(type_name = "user_role", rename_all = "lowercase")]
10#[serde(rename_all = "lowercase")]
11pub enum UserRole {
12    /// Full access including workspace management
13    Admin,
14    /// Can create and edit mocks
15    Editor,
16    /// Read-only access
17    Viewer,
18}
19
20impl UserRole {
21    /// Check if this role can perform admin actions
22    #[must_use]
23    pub const fn is_admin(&self) -> bool {
24        matches!(self, Self::Admin)
25    }
26
27    /// Check if this role can edit
28    #[must_use]
29    pub const fn can_edit(&self) -> bool {
30        matches!(self, Self::Admin | Self::Editor)
31    }
32
33    /// Check if this role can view
34    #[must_use]
35    pub const fn can_view(&self) -> bool {
36        true // All roles can view
37    }
38}
39
40/// User account
41#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
42pub struct User {
43    /// Unique user ID
44    pub id: Uuid,
45    /// Username (unique)
46    pub username: String,
47    /// Email address (unique)
48    pub email: String,
49    /// Password hash (not serialized)
50    #[serde(skip_serializing)]
51    pub password_hash: String,
52    /// Display name
53    pub display_name: Option<String>,
54    /// Avatar URL
55    pub avatar_url: Option<String>,
56    /// Account created timestamp
57    pub created_at: DateTime<Utc>,
58    /// Last updated timestamp
59    pub updated_at: DateTime<Utc>,
60    /// Whether the account is active
61    pub is_active: bool,
62}
63
64impl User {
65    /// Create a new user (for insertion)
66    #[must_use]
67    pub fn new(username: String, email: String, password_hash: String) -> Self {
68        let now = Utc::now();
69        Self {
70            id: Uuid::new_v4(),
71            username,
72            email,
73            password_hash,
74            display_name: None,
75            avatar_url: None,
76            created_at: now,
77            updated_at: now,
78            is_active: true,
79        }
80    }
81}
82
83/// Team workspace for collaboration
84#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
85pub struct TeamWorkspace {
86    /// Unique workspace ID
87    pub id: Uuid,
88    /// Workspace name
89    pub name: String,
90    /// Description
91    pub description: Option<String>,
92    /// Owner user ID
93    pub owner_id: Uuid,
94    /// Workspace configuration (JSON)
95    pub config: serde_json::Value,
96    /// Current version number
97    pub version: i64,
98    /// Created timestamp
99    pub created_at: DateTime<Utc>,
100    /// Last updated timestamp
101    pub updated_at: DateTime<Utc>,
102    /// Whether the workspace is archived
103    pub is_archived: bool,
104}
105
106impl TeamWorkspace {
107    /// Create a new workspace
108    #[must_use]
109    pub fn new(name: String, owner_id: Uuid) -> Self {
110        let now = Utc::now();
111        Self {
112            id: Uuid::new_v4(),
113            name,
114            description: None,
115            owner_id,
116            config: serde_json::json!({}),
117            version: 1,
118            created_at: now,
119            updated_at: now,
120            is_archived: false,
121        }
122    }
123}
124
125/// Workspace membership
126#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
127pub struct WorkspaceMember {
128    /// Unique membership ID
129    pub id: Uuid,
130    /// Workspace ID
131    pub workspace_id: Uuid,
132    /// User ID
133    pub user_id: Uuid,
134    /// Role in this workspace
135    pub role: UserRole,
136    /// When the user joined
137    pub joined_at: DateTime<Utc>,
138    /// Last activity timestamp
139    pub last_activity: DateTime<Utc>,
140}
141
142impl WorkspaceMember {
143    /// Create a new workspace member
144    #[must_use]
145    pub fn new(workspace_id: Uuid, user_id: Uuid, role: UserRole) -> Self {
146        let now = Utc::now();
147        Self {
148            id: Uuid::new_v4(),
149            workspace_id,
150            user_id,
151            role,
152            joined_at: now,
153            last_activity: now,
154        }
155    }
156}
157
158/// Workspace invitation
159#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
160pub struct WorkspaceInvitation {
161    /// Unique invitation ID
162    pub id: Uuid,
163    /// Workspace ID
164    pub workspace_id: Uuid,
165    /// Email address to invite
166    pub email: String,
167    /// Role to assign
168    pub role: UserRole,
169    /// User who sent the invitation
170    pub invited_by: Uuid,
171    /// Invitation token
172    pub token: String,
173    /// Expiration timestamp
174    pub expires_at: DateTime<Utc>,
175    /// Created timestamp
176    pub created_at: DateTime<Utc>,
177    /// Whether the invitation was accepted
178    pub accepted: bool,
179}
180
181/// Active user session in a workspace
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct ActiveSession {
184    /// User ID
185    pub user_id: Uuid,
186    /// Workspace ID
187    pub workspace_id: Uuid,
188    /// Session ID
189    pub session_id: Uuid,
190    /// When the session started
191    pub connected_at: DateTime<Utc>,
192    /// Last activity timestamp
193    pub last_activity: DateTime<Utc>,
194    /// Current cursor position (for presence)
195    pub cursor: Option<CursorPosition>,
196}
197
198/// Cursor position for presence awareness
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct CursorPosition {
201    /// File or resource being edited
202    pub resource: String,
203    /// Line number (if applicable)
204    pub line: Option<u32>,
205    /// Column number (if applicable)
206    pub column: Option<u32>,
207}
208
209/// Workspace fork relationship
210#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
211pub struct WorkspaceFork {
212    /// Unique fork ID
213    pub id: Uuid,
214    /// Source workspace ID (the original)
215    pub source_workspace_id: Uuid,
216    /// Forked workspace ID (the copy)
217    pub forked_workspace_id: Uuid,
218    /// When the fork was created
219    pub forked_at: DateTime<Utc>,
220    /// User who created the fork
221    pub forked_by: Uuid,
222    /// Commit ID at which fork was created (fork point)
223    pub fork_point_commit_id: Option<Uuid>,
224}
225
226impl WorkspaceFork {
227    /// Create a new fork record
228    #[must_use]
229    pub fn new(
230        source_workspace_id: Uuid,
231        forked_workspace_id: Uuid,
232        forked_by: Uuid,
233        fork_point_commit_id: Option<Uuid>,
234    ) -> Self {
235        Self {
236            id: Uuid::new_v4(),
237            source_workspace_id,
238            forked_workspace_id,
239            forked_by,
240            fork_point_commit_id,
241            forked_at: Utc::now(),
242        }
243    }
244}
245
246/// Merge status
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
248#[sqlx(type_name = "merge_status", rename_all = "lowercase")]
249#[serde(rename_all = "lowercase")]
250pub enum MergeStatus {
251    /// Merge is pending
252    Pending,
253    /// Merge is in progress
254    InProgress,
255    /// Merge completed successfully
256    Completed,
257    /// Merge has conflicts that need resolution
258    Conflict,
259    /// Merge was cancelled
260    Cancelled,
261}
262
263/// Workspace merge operation
264#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
265pub struct WorkspaceMerge {
266    /// Unique merge ID
267    pub id: Uuid,
268    /// Source workspace ID (being merged FROM)
269    pub source_workspace_id: Uuid,
270    /// Target workspace ID (being merged INTO)
271    pub target_workspace_id: Uuid,
272    /// Common ancestor commit ID
273    pub base_commit_id: Uuid,
274    /// Latest commit from source workspace
275    pub source_commit_id: Uuid,
276    /// Latest commit from target workspace
277    pub target_commit_id: Uuid,
278    /// Resulting merge commit ID (None if not completed)
279    pub merge_commit_id: Option<Uuid>,
280    /// Merge status
281    pub status: MergeStatus,
282    /// Conflict data (JSON array of conflicts)
283    pub conflict_data: Option<serde_json::Value>,
284    /// User who performed the merge
285    pub merged_by: Option<Uuid>,
286    /// When the merge was completed
287    pub merged_at: Option<DateTime<Utc>>,
288    /// When the merge was created
289    pub created_at: DateTime<Utc>,
290}
291
292impl WorkspaceMerge {
293    /// Create a new merge operation
294    #[must_use]
295    pub fn new(
296        source_workspace_id: Uuid,
297        target_workspace_id: Uuid,
298        base_commit_id: Uuid,
299        source_commit_id: Uuid,
300        target_commit_id: Uuid,
301    ) -> Self {
302        Self {
303            id: Uuid::new_v4(),
304            source_workspace_id,
305            target_workspace_id,
306            base_commit_id,
307            source_commit_id,
308            target_commit_id,
309            merge_commit_id: None,
310            status: MergeStatus::Pending,
311            conflict_data: None,
312            merged_by: None,
313            merged_at: None,
314            created_at: Utc::now(),
315        }
316    }
317}
318
319/// Conflict in a merge
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct MergeConflict {
322    /// Path to the conflicting field
323    pub path: String,
324    /// Base value (common ancestor)
325    pub base_value: Option<serde_json::Value>,
326    /// Source value (from workspace being merged)
327    pub source_value: Option<serde_json::Value>,
328    /// Target value (from current workspace)
329    pub target_value: Option<serde_json::Value>,
330    /// Conflict type
331    pub conflict_type: ConflictType,
332}
333
334/// Type of conflict
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
336#[serde(rename_all = "lowercase")]
337pub enum ConflictType {
338    /// Both sides modified the same field
339    Modified,
340    /// Field was deleted in one side, modified in the other
341    DeletedModified,
342    /// Field was added in both sides with different values
343    BothAdded,
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn test_user_role_permissions() {
352        assert!(UserRole::Admin.is_admin());
353        assert!(UserRole::Admin.can_edit());
354        assert!(UserRole::Admin.can_view());
355
356        assert!(!UserRole::Editor.is_admin());
357        assert!(UserRole::Editor.can_edit());
358        assert!(UserRole::Editor.can_view());
359
360        assert!(!UserRole::Viewer.is_admin());
361        assert!(!UserRole::Viewer.can_edit());
362        assert!(UserRole::Viewer.can_view());
363    }
364
365    #[test]
366    fn test_user_creation() {
367        let user = User::new(
368            "testuser".to_string(),
369            "test@example.com".to_string(),
370            "hashed_password".to_string(),
371        );
372
373        assert_eq!(user.username, "testuser");
374        assert_eq!(user.email, "test@example.com");
375        assert!(user.is_active);
376    }
377
378    #[test]
379    fn test_workspace_creation() {
380        let owner_id = Uuid::new_v4();
381        let workspace = TeamWorkspace::new("Test Workspace".to_string(), owner_id);
382
383        assert_eq!(workspace.name, "Test Workspace");
384        assert_eq!(workspace.owner_id, owner_id);
385        assert_eq!(workspace.version, 1);
386        assert!(!workspace.is_archived);
387    }
388
389    #[test]
390    fn test_workspace_member_creation() {
391        let workspace_id = Uuid::new_v4();
392        let user_id = Uuid::new_v4();
393        let member = WorkspaceMember::new(workspace_id, user_id, UserRole::Editor);
394
395        assert_eq!(member.workspace_id, workspace_id);
396        assert_eq!(member.user_id, user_id);
397        assert_eq!(member.role, UserRole::Editor);
398    }
399}