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