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