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