Skip to main content

mockforge_collab/
core_bridge.rs

1//! Bridge between mockforge-collab and mockforge-core workspace types
2//!
3//! This module provides conversion and synchronization between:
4//! - `TeamWorkspace` (collaboration workspace with metadata)
5//! - `Workspace` (full mockforge-core workspace with mocks, folders, etc.)
6
7use crate::error::{CollabError, Result};
8use crate::models::TeamWorkspace;
9use mockforge_core::workspace::Workspace as CoreWorkspace;
10use mockforge_core::workspace_persistence::WorkspacePersistence;
11use serde_json::Value;
12use std::path::Path;
13use uuid::Uuid;
14
15/// Bridge service for integrating collaboration workspaces with core workspaces
16pub struct CoreBridge {
17    persistence: WorkspacePersistence,
18}
19
20impl CoreBridge {
21    /// Create a new core bridge
22    pub fn new<P: AsRef<Path>>(workspace_dir: P) -> Self {
23        Self {
24            persistence: WorkspacePersistence::new(workspace_dir),
25        }
26    }
27
28    /// Convert a `TeamWorkspace` to a Core Workspace
29    ///
30    /// Extracts the full workspace data from the `TeamWorkspace.config` field
31    /// and reconstructs a Core Workspace object.
32    ///
33    /// # Errors
34    ///
35    /// Returns an error if the workspace config cannot be deserialized.
36    pub fn team_to_core(&self, team_workspace: &TeamWorkspace) -> Result<CoreWorkspace> {
37        // The full workspace data is stored in the config field as JSON
38        let workspace_json = &team_workspace.config;
39
40        // Deserialize the workspace from JSON
41        let mut workspace: CoreWorkspace =
42            serde_json::from_value(workspace_json.clone()).map_err(|e| {
43                CollabError::Internal(format!("Failed to deserialize workspace from config: {e}"))
44            })?;
45
46        // Update the workspace ID to match the team workspace ID
47        // (convert UUID to String)
48        workspace.id = team_workspace.id.to_string();
49
50        // Update metadata
51        workspace.name.clone_from(&team_workspace.name);
52        workspace.description.clone_from(&team_workspace.description);
53        workspace.updated_at = team_workspace.updated_at;
54
55        // Initialize default mock environments if they don't exist (for backward compatibility)
56        workspace.initialize_default_mock_environments();
57
58        Ok(workspace)
59    }
60
61    /// Convert a Core Workspace to a `TeamWorkspace`
62    ///
63    /// Serializes the full workspace data into the `TeamWorkspace.config` field
64    /// and creates a `TeamWorkspace` with collaboration metadata.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if the workspace cannot be serialized or the ID is invalid.
69    pub fn core_to_team(
70        &self,
71        core_workspace: &CoreWorkspace,
72        owner_id: Uuid,
73    ) -> Result<TeamWorkspace> {
74        // Serialize the full workspace to JSON
75        let workspace_json = serde_json::to_value(core_workspace).map_err(|e| {
76            CollabError::Internal(format!("Failed to serialize workspace to JSON: {e}"))
77        })?;
78
79        // Create TeamWorkspace with the serialized workspace in config
80        let mut team_workspace = TeamWorkspace::new(core_workspace.name.clone(), owner_id);
81
82        // Parse the workspace ID - return error if invalid to prevent data corruption
83        team_workspace.id = Uuid::parse_str(&core_workspace.id).map_err(|e| {
84            CollabError::Internal(format!(
85                "Invalid workspace ID '{}': {}. Cannot convert to TeamWorkspace with corrupted ID.",
86                core_workspace.id, e
87            ))
88        })?;
89
90        team_workspace.description.clone_from(&core_workspace.description);
91        team_workspace.config = workspace_json;
92        team_workspace.created_at = core_workspace.created_at;
93        team_workspace.updated_at = core_workspace.updated_at;
94
95        Ok(team_workspace)
96    }
97
98    /// Get the full workspace state from a `TeamWorkspace`
99    ///
100    /// Returns the complete Core Workspace including all mocks, folders, and configuration.
101    ///
102    /// # Errors
103    ///
104    /// Returns an error if the workspace config cannot be deserialized.
105    pub fn get_workspace_state(&self, team_workspace: &TeamWorkspace) -> Result<CoreWorkspace> {
106        self.team_to_core(team_workspace)
107    }
108
109    /// Update the workspace state in a `TeamWorkspace`
110    ///
111    /// Serializes the Core Workspace and stores it in the `TeamWorkspace.config` field.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if the workspace cannot be serialized.
116    pub fn update_workspace_state(
117        &self,
118        team_workspace: &mut TeamWorkspace,
119        core_workspace: &CoreWorkspace,
120    ) -> Result<()> {
121        // Serialize the full workspace
122        let workspace_json = serde_json::to_value(core_workspace)
123            .map_err(|e| CollabError::Internal(format!("Failed to serialize workspace: {e}")))?;
124
125        // Update the config field
126        team_workspace.config = workspace_json;
127        team_workspace.updated_at = chrono::Utc::now();
128
129        Ok(())
130    }
131
132    /// Load workspace from disk using `WorkspacePersistence`
133    ///
134    /// This loads a workspace from the filesystem and converts it to a `TeamWorkspace`.
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if the workspace cannot be loaded from disk or converted.
139    pub async fn load_workspace_from_disk(
140        &self,
141        workspace_id: &str,
142        owner_id: Uuid,
143    ) -> Result<TeamWorkspace> {
144        // Load from disk
145        let core_workspace = self
146            .persistence
147            .load_workspace(workspace_id)
148            .await
149            .map_err(|e| CollabError::Internal(format!("Failed to load workspace: {e}")))?;
150
151        // Convert to TeamWorkspace
152        self.core_to_team(&core_workspace, owner_id)
153    }
154
155    /// Save workspace to disk using `WorkspacePersistence`
156    ///
157    /// This saves a `TeamWorkspace` to the filesystem as a Core Workspace.
158    ///
159    /// # Errors
160    ///
161    /// Returns an error if the workspace cannot be converted or saved to disk.
162    pub async fn save_workspace_to_disk(&self, team_workspace: &TeamWorkspace) -> Result<()> {
163        // Convert to Core Workspace
164        let core_workspace = self.team_to_core(team_workspace)?;
165
166        // Save to disk
167        self.persistence
168            .save_workspace(&core_workspace)
169            .await
170            .map_err(|e| CollabError::Internal(format!("Failed to save workspace: {e}")))?;
171
172        Ok(())
173    }
174
175    /// Export workspace for backup
176    ///
177    /// Uses `WorkspacePersistence` to create a backup-compatible export.
178    ///
179    /// # Errors
180    ///
181    /// Returns an error if the workspace cannot be converted or serialized.
182    #[allow(clippy::unused_async)]
183    pub async fn export_workspace_for_backup(
184        &self,
185        team_workspace: &TeamWorkspace,
186    ) -> Result<Value> {
187        // Convert to Core Workspace
188        let core_workspace = self.team_to_core(team_workspace)?;
189
190        // Serialize to JSON for backup
191        serde_json::to_value(&core_workspace)
192            .map_err(|e| CollabError::Internal(format!("Failed to serialize for backup: {e}")))
193    }
194
195    /// Import workspace from backup
196    ///
197    /// Restores a workspace from a backup JSON value.
198    ///
199    /// # Errors
200    ///
201    /// Returns an error if the backup data cannot be deserialized or converted.
202    #[allow(clippy::unused_async)]
203    pub async fn import_workspace_from_backup(
204        &self,
205        backup_data: &Value,
206        owner_id: Uuid,
207        new_name: Option<String>,
208    ) -> Result<TeamWorkspace> {
209        // Deserialize Core Workspace from backup
210        let mut core_workspace: CoreWorkspace = serde_json::from_value(backup_data.clone())
211            .map_err(|e| CollabError::Internal(format!("Failed to deserialize backup: {e}")))?;
212
213        // Update name if provided
214        if let Some(name) = new_name {
215            core_workspace.name = name;
216        }
217
218        // Generate new ID for restored workspace
219        core_workspace.id = Uuid::new_v4().to_string();
220        core_workspace.created_at = chrono::Utc::now();
221        core_workspace.updated_at = chrono::Utc::now();
222
223        // Convert to TeamWorkspace
224        self.core_to_team(&core_workspace, owner_id)
225    }
226
227    /// Get workspace state as JSON for sync
228    ///
229    /// Returns the full workspace state as a JSON value for real-time synchronization.
230    ///
231    /// # Errors
232    ///
233    /// Returns an error if the workspace cannot be converted or serialized.
234    pub fn get_workspace_state_json(&self, team_workspace: &TeamWorkspace) -> Result<Value> {
235        let core_workspace = self.team_to_core(team_workspace)?;
236        serde_json::to_value(&core_workspace)
237            .map_err(|e| CollabError::Internal(format!("Failed to serialize state: {e}")))
238    }
239
240    /// Update workspace state from JSON
241    ///
242    /// Updates the `TeamWorkspace` with state from a JSON value (from sync).
243    ///
244    /// # Errors
245    ///
246    /// Returns an error if the JSON cannot be deserialized.
247    pub fn update_workspace_state_from_json(
248        &self,
249        team_workspace: &mut TeamWorkspace,
250        state_json: &Value,
251    ) -> Result<()> {
252        // Deserialize Core Workspace from JSON
253        let mut core_workspace: CoreWorkspace = serde_json::from_value(state_json.clone())
254            .map_err(|e| CollabError::Internal(format!("Failed to deserialize state JSON: {e}")))?;
255
256        // Preserve TeamWorkspace metadata
257        core_workspace.id = team_workspace.id.to_string();
258        core_workspace.name.clone_from(&team_workspace.name);
259        core_workspace.description.clone_from(&team_workspace.description);
260
261        // Update the TeamWorkspace
262        self.update_workspace_state(team_workspace, &core_workspace)
263    }
264
265    /// Create a new empty workspace
266    ///
267    /// Creates a new Core Workspace and converts it to a `TeamWorkspace`.
268    ///
269    /// # Errors
270    ///
271    /// Returns an error if the workspace cannot be created.
272    pub fn create_empty_workspace(&self, name: String, owner_id: Uuid) -> Result<TeamWorkspace> {
273        let core_workspace = CoreWorkspace::new(name);
274        self.core_to_team(&core_workspace, owner_id)
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn test_team_to_core_conversion() {
284        let bridge = CoreBridge::new("/tmp/test");
285        let owner_id = Uuid::new_v4();
286
287        // Create a simple core workspace
288        let core_workspace = CoreWorkspace::new("Test Workspace".to_string());
289        let team_workspace = bridge.core_to_team(&core_workspace, owner_id).unwrap();
290
291        // Convert back
292        let restored = bridge.team_to_core(&team_workspace).unwrap();
293
294        assert_eq!(restored.name, core_workspace.name);
295        assert_eq!(restored.folders.len(), core_workspace.folders.len());
296        assert_eq!(restored.requests.len(), core_workspace.requests.len());
297    }
298
299    #[test]
300    fn test_state_json_roundtrip() {
301        let bridge = CoreBridge::new("/tmp/test");
302        let owner_id = Uuid::new_v4();
303
304        // Create workspace
305        let core_workspace = CoreWorkspace::new("Test".to_string());
306        let mut team_workspace = bridge.core_to_team(&core_workspace, owner_id).unwrap();
307
308        // Get state as JSON
309        let state_json = bridge.get_workspace_state_json(&team_workspace).unwrap();
310
311        // Update from JSON
312        bridge
313            .update_workspace_state_from_json(&mut team_workspace, &state_json)
314            .unwrap();
315
316        // Verify it still works
317        let restored = bridge.team_to_core(&team_workspace).unwrap();
318        assert_eq!(restored.name, "Test");
319    }
320
321    #[test]
322    fn test_invalid_uuid_returns_error() {
323        let bridge = CoreBridge::new("/tmp/test");
324        let owner_id = Uuid::new_v4();
325
326        // Create a workspace with an invalid UUID
327        let mut core_workspace = CoreWorkspace::new("Test Invalid UUID".to_string());
328        core_workspace.id = "not-a-valid-uuid".to_string();
329
330        // Attempting to convert should return an error, not silently create a new UUID
331        let result = bridge.core_to_team(&core_workspace, owner_id);
332        assert!(result.is_err(), "Expected error for invalid UUID, but conversion succeeded");
333
334        // Verify the error message mentions the invalid ID
335        if let Err(e) = result {
336            let error_msg = format!("{e}");
337            assert!(
338                error_msg.contains("not-a-valid-uuid"),
339                "Error message should contain the invalid UUID: {error_msg}",
340            );
341        }
342    }
343
344    #[test]
345    fn test_valid_uuid_conversion() {
346        let bridge = CoreBridge::new("/tmp/test");
347        let owner_id = Uuid::new_v4();
348        let workspace_uuid = Uuid::new_v4();
349
350        // Create a workspace with a valid UUID
351        let mut core_workspace = CoreWorkspace::new("Test Valid UUID".to_string());
352        core_workspace.id = workspace_uuid.to_string();
353
354        // Conversion should succeed
355        let team_workspace = bridge.core_to_team(&core_workspace, owner_id).unwrap();
356
357        // Verify the UUID was preserved correctly
358        assert_eq!(team_workspace.id, workspace_uuid);
359    }
360}