mockforge-collab 0.3.92

Cloud collaboration features for MockForge - team workspaces, real-time sync, and version control
Documentation
//! 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);
    }
}