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