mockforge_core/
workspace.rs

1//! Workspace and folder organization for MockForge requests
2//!
3//! This module has been refactored into sub-modules for better organization:
4//! - core: Core workspace and folder structures
5//! - registry: Workspace registry and management
6//! - environment: Environment configuration and management
7//! - sync: Synchronization functionality
8//! - request: Mock request handling and processing
9
10// Re-export sub-modules for backward compatibility
11pub mod core;
12pub mod environment;
13pub mod mock_environment;
14pub mod registry;
15pub mod request;
16pub mod scenario_promotion;
17pub mod sync;
18
19// Re-export commonly used types
20pub use environment::*;
21pub use mock_environment::*;
22pub use registry::*;
23pub use request::*;
24pub use scenario_promotion::*;
25pub use sync::*;
26
27// Legacy imports for compatibility
28use crate::config::AuthConfig;
29use crate::encryption::AutoEncryptionConfig;
30use crate::fidelity::FidelityScore;
31use crate::reality::RealityLevel;
32use crate::routing::{HttpMethod, Route, RouteRegistry};
33use crate::{Error, Result};
34use chrono::{DateTime, Utc};
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37use uuid::Uuid;
38
39/// Unique identifier for workspace entities
40pub type EntityId = String;
41
42/// Workspace represents a top-level organizational unit
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Workspace {
45    /// Unique identifier
46    pub id: EntityId,
47    /// Human-readable name
48    pub name: String,
49    /// Optional description
50    pub description: Option<String>,
51    /// Creation timestamp
52    pub created_at: DateTime<Utc>,
53    /// Last modification timestamp
54    pub updated_at: DateTime<Utc>,
55    /// Associated tags for filtering and organization
56    pub tags: Vec<String>,
57    /// Configuration specific to this workspace
58    pub config: WorkspaceConfig,
59    /// Root folders in this workspace
60    pub folders: Vec<Folder>,
61    /// Root requests (not in any folder)
62    pub requests: Vec<MockRequest>,
63    /// Display order for UI sorting (lower numbers appear first)
64    #[serde(default)]
65    pub order: i32,
66}
67
68/// Configuration for request inheritance at folder level
69#[derive(Debug, Clone, Serialize, Deserialize, Default)]
70pub struct FolderInheritanceConfig {
71    /// Headers to be inherited by child requests (if not overridden)
72    #[serde(default)]
73    pub headers: HashMap<String, String>,
74    /// Authentication configuration for inheritance
75    pub auth: Option<AuthConfig>,
76}
77
78/// Folder represents a hierarchical grouping within a workspace
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct Folder {
81    /// Unique identifier
82    pub id: EntityId,
83    /// Human-readable name
84    pub name: String,
85    /// Optional description
86    pub description: Option<String>,
87    /// Parent folder ID (None if root folder)
88    pub parent_id: Option<EntityId>,
89    /// Creation timestamp
90    pub created_at: DateTime<Utc>,
91    /// Last modification timestamp
92    pub updated_at: DateTime<Utc>,
93    /// Associated tags
94    pub tags: Vec<String>,
95    /// Inheritance configuration for this folder
96    #[serde(default)]
97    pub inheritance: FolderInheritanceConfig,
98    /// Child folders
99    pub folders: Vec<Folder>,
100    /// Requests in this folder
101    pub requests: Vec<MockRequest>,
102}
103
104/// Mock request definition with metadata
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct MockRequest {
107    /// Unique identifier
108    pub id: EntityId,
109    /// Human-readable name
110    pub name: String,
111    /// Optional description
112    pub description: Option<String>,
113    /// HTTP method
114    pub method: HttpMethod,
115    /// Request path
116    pub path: String,
117    /// HTTP headers
118    pub headers: HashMap<String, String>,
119    /// Query parameters
120    pub query_params: HashMap<String, String>,
121    /// Request body template
122    pub body: Option<String>,
123    /// Expected response
124    pub response: MockResponse,
125    /// History of actual request executions
126    pub response_history: Vec<ResponseHistoryEntry>,
127    /// Creation timestamp
128    pub created_at: DateTime<Utc>,
129    /// Last modification timestamp
130    pub updated_at: DateTime<Utc>,
131    /// Associated tags
132    pub tags: Vec<String>,
133    /// Authentication configuration
134    pub auth: Option<AuthConfig>,
135    /// Priority for route matching (higher = more specific)
136    pub priority: i32,
137}
138
139/// Mock response definition
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct MockResponse {
142    /// HTTP status code
143    pub status_code: u16,
144    /// Response headers
145    pub headers: HashMap<String, String>,
146    /// Response body template
147    pub body: Option<String>,
148    /// Content type
149    pub content_type: Option<String>,
150    /// Response delay in milliseconds
151    pub delay_ms: Option<u64>,
152}
153
154/// Response history entry for tracking actual request executions
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct ResponseHistoryEntry {
157    /// Unique execution ID
158    pub id: String,
159    /// Execution timestamp
160    pub executed_at: DateTime<Utc>,
161    /// Actual request method used
162    pub request_method: HttpMethod,
163    /// Actual request path used
164    pub request_path: String,
165    /// Request headers sent
166    pub request_headers: HashMap<String, String>,
167    /// Request body sent
168    pub request_body: Option<String>,
169    /// Response status code received
170    pub response_status_code: u16,
171    /// Response headers received
172    pub response_headers: HashMap<String, String>,
173    /// Response body received
174    pub response_body: Option<String>,
175    /// Response time in milliseconds
176    pub response_time_ms: u64,
177    /// Response size in bytes
178    pub response_size_bytes: u64,
179    /// Error message if execution failed
180    pub error_message: Option<String>,
181}
182
183/// Represents a color for environment visualization
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct EnvironmentColor {
186    /// Hex color code (e.g., "#FF5733")
187    pub hex: String,
188    /// Optional color name for accessibility
189    pub name: Option<String>,
190}
191
192/// Environment for managing variable collections
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct Environment {
195    /// Unique identifier
196    pub id: EntityId,
197    /// Human-readable name
198    pub name: String,
199    /// Optional description
200    pub description: Option<String>,
201    /// Color for visual distinction in UI
202    pub color: Option<EnvironmentColor>,
203    /// Environment variables
204    pub variables: HashMap<String, String>,
205    /// Creation timestamp
206    pub created_at: DateTime<Utc>,
207    /// Last modification timestamp
208    pub updated_at: DateTime<Utc>,
209    /// Display order for UI sorting (lower numbers appear first)
210    #[serde(default)]
211    pub order: i32,
212    /// Whether this environment can be shared/synced
213    #[serde(default)]
214    pub sharable: bool,
215}
216
217/// Directory sync configuration for a workspace
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct SyncConfig {
220    /// Enable directory syncing for this workspace
221    pub enabled: bool,
222    /// Target directory for sync (relative or absolute path)
223    pub target_directory: Option<String>,
224    /// Directory structure to use (flat, nested, grouped)
225    pub directory_structure: SyncDirectoryStructure,
226    /// Auto-sync direction (one-way workspace→directory, bidirectional, or manual)
227    pub sync_direction: SyncDirection,
228    /// Whether to include metadata files
229    pub include_metadata: bool,
230    /// Filesystem monitoring enabled for real-time sync
231    pub realtime_monitoring: bool,
232    /// Custom filename pattern for exported files
233    pub filename_pattern: String,
234    /// Regular expression for excluding workspaces/requests
235    pub exclude_pattern: Option<String>,
236    /// Force overwrite existing files during sync
237    pub force_overwrite: bool,
238    /// Last sync timestamp
239    pub last_sync: Option<DateTime<Utc>>,
240}
241
242/// Directory structure options for sync
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub enum SyncDirectoryStructure {
245    /// All workspaces in flat structure: workspace-name.yaml
246    Flat,
247    /// Nested by workspace: workspaces/{name}/workspace.yaml + requests/
248    Nested,
249    /// Grouped by type: requests/, responses/, metadata/
250    Grouped,
251}
252
253/// Sync direction options
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub enum SyncDirection {
256    /// Manual sync only (one-off operations)
257    Manual,
258    /// One-way: workspace changes sync silently to directory
259    WorkspaceToDirectory,
260    /// Bidirectional: changes in either direction trigger sync
261    Bidirectional,
262}
263
264/// Workspace-specific configuration
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct WorkspaceConfig {
267    /// Base URL for all requests in this workspace
268    pub base_url: Option<String>,
269    /// Default headers for all requests
270    pub default_headers: HashMap<String, String>,
271    /// Authentication configuration
272    pub auth: Option<AuthConfig>,
273    /// Global environment (always available)
274    pub global_environment: Environment,
275    /// Sub-environments (switchable)
276    pub environments: Vec<Environment>,
277    /// Currently active environment ID (None means only global)
278    pub active_environment_id: Option<EntityId>,
279    /// Directory sync configuration
280    pub sync: SyncConfig,
281    /// Automatic encryption configuration
282    #[serde(default)]
283    pub auto_encryption: AutoEncryptionConfig,
284    /// Reality level for this workspace (1-5)
285    /// Controls the realism of mock behavior (chaos, latency, MockAI)
286    #[serde(default)]
287    pub reality_level: Option<RealityLevel>,
288    /// Current fidelity score for this workspace
289    /// Measures how close the mock is to the real upstream
290    #[serde(default, skip_serializing_if = "Option::is_none")]
291    pub fidelity_score: Option<FidelityScore>,
292}
293
294/// Workspace registry for managing multiple workspaces
295#[derive(Debug, Clone)]
296pub struct WorkspaceRegistry {
297    workspaces: HashMap<EntityId, Workspace>,
298    active_workspace: Option<EntityId>,
299}
300
301impl Workspace {
302    /// Create a new workspace
303    pub fn new(name: String) -> Self {
304        let now = Utc::now();
305        Self {
306            id: Uuid::new_v4().to_string(),
307            name,
308            description: None,
309            created_at: now,
310            updated_at: now,
311            tags: Vec::new(),
312            config: WorkspaceConfig::default(),
313            folders: Vec::new(),
314            requests: Vec::new(),
315            order: 0, // Default order will be updated when added to registry
316        }
317    }
318
319    /// Add a folder to this workspace
320    pub fn add_folder(&mut self, name: String) -> Result<EntityId> {
321        let folder = Folder::new(name);
322        let id = folder.id.clone();
323        self.folders.push(folder);
324        self.updated_at = Utc::now();
325        Ok(id)
326    }
327
328    /// Create a new environment
329    pub fn create_environment(
330        &mut self,
331        name: String,
332        description: Option<String>,
333    ) -> Result<EntityId> {
334        // Check if environment name already exists
335        if self.config.environments.iter().any(|env| env.name == name) {
336            return Err(Error::generic(format!("Environment with name '{}' already exists", name)));
337        }
338
339        let mut environment = Environment::new(name);
340        environment.description = description;
341
342        // Set order to the end of the list
343        environment.order = self.config.environments.len() as i32;
344
345        let id = environment.id.clone();
346
347        self.config.environments.push(environment);
348        self.updated_at = Utc::now();
349        Ok(id)
350    }
351
352    /// Get all environments (global + sub-environments)
353    pub fn get_environments(&self) -> Vec<&Environment> {
354        let mut all_envs = vec![&self.config.global_environment];
355        all_envs.extend(self.config.environments.iter());
356        all_envs
357    }
358
359    /// Get environment by ID
360    pub fn get_environment(&self, id: &str) -> Option<&Environment> {
361        if self.config.global_environment.id == id {
362            Some(&self.config.global_environment)
363        } else {
364            self.config.environments.iter().find(|env| env.id == id)
365        }
366    }
367
368    /// Get environment by ID (mutable)
369    pub fn get_environment_mut(&mut self, id: &str) -> Option<&mut Environment> {
370        if self.config.global_environment.id == id {
371            Some(&mut self.config.global_environment)
372        } else {
373            self.config.environments.iter_mut().find(|env| env.id == id)
374        }
375    }
376
377    /// Set active environment
378    pub fn set_active_environment(&mut self, environment_id: Option<String>) -> Result<()> {
379        if let Some(ref id) = environment_id {
380            if self.get_environment(id).is_none() {
381                return Err(Error::generic(format!("Environment with ID '{}' not found", id)));
382            }
383        }
384        self.config.active_environment_id = environment_id;
385        self.updated_at = Utc::now();
386        Ok(())
387    }
388
389    /// Get active environment (returns global if no sub-environment is active)
390    pub fn get_active_environment(&self) -> &Environment {
391        if let Some(ref active_id) = self.config.active_environment_id {
392            self.get_environment(active_id).unwrap_or(&self.config.global_environment)
393        } else {
394            &self.config.global_environment
395        }
396    }
397
398    /// Get active environment ID
399    pub fn get_active_environment_id(&self) -> Option<&str> {
400        self.config.active_environment_id.as_deref()
401    }
402
403    /// Get variable value from current active environment
404    pub fn get_variable(&self, key: &str) -> Option<&String> {
405        // First check active environment, then global environment
406        let active_env = self.get_active_environment();
407        active_env.get_variable(key).or_else(|| {
408            if active_env.id != self.config.global_environment.id {
409                self.config.global_environment.get_variable(key)
410            } else {
411                None
412            }
413        })
414    }
415
416    /// Get all variables from current active environment context
417    pub fn get_all_variables(&self) -> HashMap<String, String> {
418        let mut variables = HashMap::new();
419
420        // Start with global environment variables
421        variables.extend(self.config.global_environment.variables.clone());
422
423        // Override with active environment variables if different from global
424        let active_env = self.get_active_environment();
425        if active_env.id != self.config.global_environment.id {
426            variables.extend(active_env.variables.clone());
427        }
428
429        variables
430    }
431
432    /// Delete an environment
433    pub fn delete_environment(&mut self, id: &str) -> Result<()> {
434        if id == self.config.global_environment.id {
435            return Err(Error::generic("Cannot delete global environment".to_string()));
436        }
437
438        let position = self.config.environments.iter().position(|env| env.id == id);
439        if let Some(pos) = position {
440            self.config.environments.remove(pos);
441
442            // Clear active environment if it was deleted
443            if self.config.active_environment_id.as_deref() == Some(id) {
444                self.config.active_environment_id = None;
445            }
446
447            self.updated_at = Utc::now();
448            Ok(())
449        } else {
450            Err(Error::generic(format!("Environment with ID '{}' not found", id)))
451        }
452    }
453
454    /// Update the order of environments
455    pub fn update_environments_order(&mut self, environment_ids: Vec<String>) -> Result<()> {
456        // Validate that all provided IDs exist
457        for env_id in &environment_ids {
458            if !self.config.environments.iter().any(|env| env.id == *env_id) {
459                return Err(Error::generic(format!("Environment with ID '{}' not found", env_id)));
460            }
461        }
462
463        // Update order for each environment
464        for (index, env_id) in environment_ids.iter().enumerate() {
465            if let Some(env) = self.config.environments.iter_mut().find(|env| env.id == *env_id) {
466                env.order = index as i32;
467                env.updated_at = Utc::now();
468            }
469        }
470
471        self.updated_at = Utc::now();
472        Ok(())
473    }
474
475    /// Get environments sorted by order
476    pub fn get_environments_ordered(&self) -> Vec<&Environment> {
477        let mut all_envs = vec![&self.config.global_environment];
478        all_envs.extend(self.config.environments.iter());
479        all_envs.sort_by_key(|env| env.order);
480        all_envs
481    }
482
483    /// Configure directory sync for this workspace
484    pub fn configure_sync(&mut self, config: SyncConfig) -> Result<()> {
485        self.config.sync = config;
486        self.updated_at = Utc::now();
487        Ok(())
488    }
489
490    /// Enable directory sync with default settings
491    pub fn enable_sync(&mut self, target_directory: String) -> Result<()> {
492        self.config.sync.enabled = true;
493        self.config.sync.target_directory = Some(target_directory);
494        self.config.sync.realtime_monitoring = true; // Enable realtime monitoring by default
495        self.updated_at = Utc::now();
496        Ok(())
497    }
498
499    /// Disable directory sync
500    pub fn disable_sync(&mut self) -> Result<()> {
501        self.config.sync.enabled = false;
502        self.updated_at = Utc::now();
503        Ok(())
504    }
505
506    /// Get sync configuration
507    pub fn get_sync_config(&self) -> &SyncConfig {
508        &self.config.sync
509    }
510
511    /// Check if sync is enabled
512    pub fn is_sync_enabled(&self) -> bool {
513        self.config.sync.enabled
514    }
515
516    /// Get the target sync directory
517    pub fn get_sync_directory(&self) -> Option<&str> {
518        self.config.sync.target_directory.as_deref()
519    }
520
521    /// Set sync directory
522    pub fn set_sync_directory(&mut self, directory: Option<String>) -> Result<()> {
523        self.config.sync.target_directory = directory;
524        self.updated_at = Utc::now();
525        Ok(())
526    }
527
528    /// Set sync direction
529    pub fn set_sync_direction(&mut self, direction: SyncDirection) -> Result<()> {
530        self.config.sync.sync_direction = direction;
531        self.updated_at = Utc::now();
532        Ok(())
533    }
534
535    /// Get sync direction
536    pub fn get_sync_direction(&self) -> &SyncDirection {
537        &self.config.sync.sync_direction
538    }
539
540    /// Enable/disable real-time monitoring
541    pub fn set_realtime_monitoring(&mut self, enabled: bool) -> Result<()> {
542        self.config.sync.realtime_monitoring = enabled;
543        self.updated_at = Utc::now();
544        Ok(())
545    }
546
547    /// Check if real-time monitoring is enabled
548    pub fn is_realtime_monitoring_enabled(&self) -> bool {
549        self.config.sync.realtime_monitoring && self.config.sync.enabled
550    }
551
552    /// Create filtered copy for directory sync (removes sensitive environments and non-sharable environments)
553    pub fn to_filtered_for_sync(&self) -> Workspace {
554        let mut filtered = self.clone();
555
556        // Remove sensitive environment variables before sync
557        // This implementation filters out common sensitive keys
558        filtered.config.global_environment.variables =
559            self.filter_sensitive_variables(&self.config.global_environment.variables);
560
561        // Filter out non-sharable environments
562        filtered.config.environments = filtered
563            .config
564            .environments
565            .into_iter()
566            .filter(|env| env.sharable)
567            .map(|mut env| {
568                env.variables = self.filter_sensitive_variables(&env.variables);
569                env
570            })
571            .collect();
572
573        filtered
574    }
575
576    /// Filter out sensitive environment variables
577    fn filter_sensitive_variables(
578        &self,
579        variables: &HashMap<String, String>,
580    ) -> HashMap<String, String> {
581        let sensitive_keys = [
582            // Common sensitive keys that should not be synced
583            "password",
584            "secret",
585            "key",
586            "token",
587            "credential",
588            "api_key",
589            "apikey",
590            "api_secret",
591            "db_password",
592            "database_password",
593            "aws_secret_key",
594            "aws_session_token",
595            "private_key",
596            "authorization",
597            "auth_token",
598            "access_token",
599            "refresh_token",
600            "cookie",
601            "session",
602            "csrf",
603            "jwt",
604            "bearer",
605        ];
606
607        variables
608            .iter()
609            .filter(|(key, _)| {
610                let key_lower = key.to_lowercase();
611                !sensitive_keys.iter().any(|sensitive| key_lower.contains(sensitive))
612            })
613            .map(|(k, v)| (k.clone(), v.clone()))
614            .collect()
615    }
616
617    /// Check if this workspace should be included in directory sync
618    pub fn should_sync(&self) -> bool {
619        self.config.sync.enabled && self.config.sync.target_directory.is_some()
620    }
621
622    /// Get the filename for this workspace in directory sync
623    pub fn get_sync_filename(&self) -> String {
624        // Apply filename pattern, default to {name}
625        let pattern = &self.config.sync.filename_pattern;
626
627        // Simple pattern replacement - {name} → workspace name, {id} → workspace id
628        let filename = pattern
629            .replace("{name}", &sanitize_filename(&self.name))
630            .replace("{id}", &self.id);
631
632        if filename.ends_with(".yaml") || filename.ends_with(".yml") {
633            filename
634        } else {
635            format!("{}.yaml", filename)
636        }
637    }
638}
639
640/// Helper function to sanitize filenames for cross-platform compatibility
641fn sanitize_filename(name: &str) -> String {
642    name.chars()
643        .map(|c| match c {
644            '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
645            c if c.is_control() => '_',
646            c if c.is_whitespace() => '-',
647            c => c,
648        })
649        .collect::<String>()
650        .to_lowercase()
651}
652
653impl Folder {
654    /// Create a new folder
655    pub fn new(name: String) -> Self {
656        let now = Utc::now();
657        Self {
658            id: Uuid::new_v4().to_string(),
659            name,
660            description: None,
661            parent_id: None,
662            created_at: now,
663            updated_at: now,
664            tags: Vec::new(),
665            inheritance: FolderInheritanceConfig::default(),
666            folders: Vec::new(),
667            requests: Vec::new(),
668        }
669    }
670
671    /// Add a subfolder
672    pub fn add_folder(&mut self, name: String) -> Result<EntityId> {
673        let mut folder = Folder::new(name);
674        folder.parent_id = Some(self.id.clone());
675        let id = folder.id.clone();
676        self.folders.push(folder);
677        self.updated_at = Utc::now();
678        Ok(id)
679    }
680
681    /// Add a request to this folder
682    pub fn add_request(&mut self, request: MockRequest) -> Result<EntityId> {
683        let id = request.id.clone();
684        self.requests.push(request);
685        self.updated_at = Utc::now();
686        Ok(id)
687    }
688
689    /// Find a folder by ID recursively
690    pub fn find_folder(&self, id: &str) -> Option<&Folder> {
691        for folder in &self.folders {
692            if folder.id == id {
693                return Some(folder);
694            }
695            if let Some(found) = folder.find_folder(id) {
696                return Some(found);
697            }
698        }
699        None
700    }
701
702    /// Find a folder by ID recursively (mutable)
703    pub fn find_folder_mut(&mut self, id: &str) -> Option<&mut Folder> {
704        for folder in &mut self.folders {
705            if folder.id == id {
706                return Some(folder);
707            }
708            if let Some(found) = folder.find_folder_mut(id) {
709                return Some(found);
710            }
711        }
712        None
713    }
714
715    /// Find a request by ID recursively
716    pub fn find_request(&self, id: &str) -> Option<&MockRequest> {
717        // Check this folder's requests
718        for request in &self.requests {
719            if request.id == id {
720                return Some(request);
721            }
722        }
723
724        // Check subfolders
725        for folder in &self.folders {
726            if let Some(found) = folder.find_request(id) {
727                return Some(found);
728            }
729        }
730        None
731    }
732
733    /// Get all routes from this folder and subfolders
734    pub fn get_routes(&self, workspace_id: &str) -> Vec<Route> {
735        let mut routes = Vec::new();
736
737        // Add this folder's requests
738        for request in &self.requests {
739            routes.push(
740                Route::new(request.method.clone(), request.path.clone())
741                    .with_priority(request.priority)
742                    .with_metadata("request_id".to_string(), serde_json::json!(request.id))
743                    .with_metadata("folder_id".to_string(), serde_json::json!(self.id))
744                    .with_metadata("workspace_id".to_string(), serde_json::json!(workspace_id)),
745            );
746        }
747
748        // Add routes from subfolders
749        for folder in &self.folders {
750            routes.extend(folder.get_routes(workspace_id));
751        }
752
753        routes
754    }
755}
756
757impl Workspace {
758    /// Find a folder by ID recursively
759    pub fn find_folder(&self, id: &str) -> Option<&Folder> {
760        for folder in &self.folders {
761            if folder.id == id {
762                return Some(folder);
763            }
764            if let Some(found) = folder.find_folder(id) {
765                return Some(found);
766            }
767        }
768        None
769    }
770
771    /// Find a folder by ID recursively (mutable)
772    pub fn find_folder_mut(&mut self, id: &str) -> Option<&mut Folder> {
773        for folder in &mut self.folders {
774            if folder.id == id {
775                return Some(folder);
776            }
777            if let Some(found) = folder.find_folder_mut(id) {
778                return Some(found);
779            }
780        }
781        None
782    }
783
784    /// Add a request to this workspace
785    pub fn add_request(&mut self, request: MockRequest) -> Result<EntityId> {
786        let id = request.id.clone();
787        self.requests.push(request);
788        self.updated_at = Utc::now();
789        Ok(id)
790    }
791
792    /// Get all routes from this workspace
793    pub fn get_routes(&self) -> Vec<Route> {
794        let mut routes = Vec::new();
795
796        // Add workspace-level requests
797        for request in &self.requests {
798            routes.push(
799                Route::new(request.method.clone(), request.path.clone())
800                    .with_priority(request.priority)
801                    .with_metadata("request_id".to_string(), serde_json::json!(request.id))
802                    .with_metadata("workspace_id".to_string(), serde_json::json!(self.id)),
803            );
804        }
805
806        // Add routes from folders
807        for folder in &self.folders {
808            routes.extend(folder.get_routes(&self.id));
809        }
810
811        routes
812    }
813
814    /// Get effective authentication for a request at the given path
815    pub fn get_effective_auth<'a>(&'a self, folder_path: &[&'a Folder]) -> Option<&'a AuthConfig> {
816        // Check folder inheritance (higher priority)
817        for folder in folder_path.iter().rev() {
818            if let Some(auth) = &folder.inheritance.auth {
819                return Some(auth);
820            }
821        }
822
823        // Fall back to workspace auth
824        self.config.auth.as_ref()
825    }
826
827    /// Get merged headers for a request at the given path
828    pub fn get_effective_headers(&self, folder_path: &[&Folder]) -> HashMap<String, String> {
829        let mut effective_headers = HashMap::new();
830
831        // Start with workspace headers (lowest priority)
832        for (key, value) in &self.config.default_headers {
833            effective_headers.insert(key.clone(), value.clone());
834        }
835
836        // Add folder headers (higher priority) in order from parent to child
837        for folder in folder_path {
838            for (key, value) in &folder.inheritance.headers {
839                effective_headers.insert(key.clone(), value.clone());
840            }
841        }
842
843        effective_headers
844    }
845}
846
847impl Folder {
848    /// Get the inheritance path from this folder to root
849    pub fn get_inheritance_path<'a>(&'a self, workspace: &'a Workspace) -> Vec<&'a Folder> {
850        let mut path = Vec::new();
851        let mut current = Some(self);
852
853        while let Some(folder) = current {
854            path.push(folder);
855            current =
856                folder.parent_id.as_ref().and_then(|parent_id| workspace.find_folder(parent_id));
857        }
858
859        path.reverse(); // Root first
860        path
861    }
862}
863
864impl MockRequest {
865    /// Apply inheritance to this request, returning headers and auth from the hierarchy
866    pub fn apply_inheritance(
867        &mut self,
868        effective_headers: HashMap<String, String>,
869        effective_auth: Option<&AuthConfig>,
870    ) {
871        // Merge headers - request headers override inherited ones
872        for (key, value) in effective_headers {
873            self.headers.entry(key).or_insert(value);
874        }
875
876        // For authentication - store it as a tag or custom field for use by the handler
877        // This will be used by the request processing middleware
878        if let Some(auth) = effective_auth {
879            self.auth = Some(auth.clone());
880        }
881    }
882
883    /// Create inherited request with merged headers and auth
884    pub fn create_inherited_request(
885        mut self,
886        workspace: &Workspace,
887        folder_path: &[&Folder],
888    ) -> Self {
889        let effective_headers = workspace.get_effective_headers(folder_path);
890        let effective_auth = workspace.get_effective_auth(folder_path);
891
892        self.apply_inheritance(effective_headers, effective_auth);
893        self
894    }
895
896    /// Create a new mock request
897    pub fn new(method: HttpMethod, path: String, name: String) -> Self {
898        let now = Utc::now();
899        Self {
900            id: Uuid::new_v4().to_string(),
901            name,
902            description: None,
903            method,
904            path,
905            headers: HashMap::new(),
906            query_params: HashMap::new(),
907            body: None,
908            response: MockResponse::default(),
909            response_history: Vec::new(),
910            created_at: now,
911            updated_at: now,
912            tags: Vec::new(),
913            auth: None,
914            priority: 0,
915        }
916    }
917
918    /// Set the response for this request
919    pub fn with_response(mut self, response: MockResponse) -> Self {
920        self.response = response;
921        self
922    }
923
924    /// Add a header
925    pub fn with_header(mut self, key: String, value: String) -> Self {
926        self.headers.insert(key, value);
927        self
928    }
929
930    /// Add a query parameter
931    pub fn with_query_param(mut self, key: String, value: String) -> Self {
932        self.query_params.insert(key, value);
933        self
934    }
935
936    /// Set request body
937    pub fn with_body(mut self, body: String) -> Self {
938        self.body = Some(body);
939        self
940    }
941
942    /// Add a tag
943    pub fn with_tag(mut self, tag: String) -> Self {
944        self.tags.push(tag);
945        self
946    }
947
948    /// Add a response history entry
949    pub fn add_response_history(&mut self, entry: ResponseHistoryEntry) {
950        self.response_history.push(entry);
951        // Keep only last 100 history entries to prevent unbounded growth
952        if self.response_history.len() > 100 {
953            self.response_history.remove(0);
954        }
955        // Sort by execution time (newest first)
956        self.response_history.sort_by(|a, b| b.executed_at.cmp(&a.executed_at));
957    }
958
959    /// Get response history (sorted by execution time, newest first)
960    pub fn get_response_history(&self) -> &[ResponseHistoryEntry] {
961        &self.response_history
962    }
963}
964
965impl Default for MockResponse {
966    fn default() -> Self {
967        Self {
968            status_code: 200,
969            headers: HashMap::new(),
970            body: Some("{}".to_string()),
971            content_type: Some("application/json".to_string()),
972            delay_ms: None,
973        }
974    }
975}
976
977impl Environment {
978    /// Create a new environment
979    pub fn new(name: String) -> Self {
980        let now = Utc::now();
981        Self {
982            id: Uuid::new_v4().to_string(),
983            name,
984            description: None,
985            color: None,
986            variables: HashMap::new(),
987            created_at: now,
988            updated_at: now,
989            order: 0,        // Default order will be updated when added to workspace
990            sharable: false, // Default to not sharable
991        }
992    }
993
994    /// Create a new global environment
995    pub fn new_global() -> Self {
996        let mut env = Self::new("Global".to_string());
997        env.description =
998            Some("Global environment variables available in all contexts".to_string());
999        env
1000    }
1001
1002    /// Add or update a variable
1003    pub fn set_variable(&mut self, key: String, value: String) {
1004        self.variables.insert(key, value);
1005        self.updated_at = Utc::now();
1006    }
1007
1008    /// Remove a variable
1009    pub fn remove_variable(&mut self, key: &str) -> bool {
1010        let removed = self.variables.remove(key).is_some();
1011        if removed {
1012            self.updated_at = Utc::now();
1013        }
1014        removed
1015    }
1016
1017    /// Get a variable value
1018    pub fn get_variable(&self, key: &str) -> Option<&String> {
1019        self.variables.get(key)
1020    }
1021
1022    /// Set the environment color
1023    pub fn set_color(&mut self, color: EnvironmentColor) {
1024        self.color = Some(color);
1025        self.updated_at = Utc::now();
1026    }
1027}
1028
1029impl Default for SyncConfig {
1030    fn default() -> Self {
1031        Self {
1032            enabled: false,
1033            target_directory: None,
1034            directory_structure: SyncDirectoryStructure::Nested,
1035            sync_direction: SyncDirection::Manual,
1036            include_metadata: true,
1037            realtime_monitoring: false,
1038            filename_pattern: "{name}".to_string(),
1039            exclude_pattern: None,
1040            force_overwrite: false,
1041            last_sync: None,
1042        }
1043    }
1044}
1045
1046impl Default for WorkspaceConfig {
1047    fn default() -> Self {
1048        Self {
1049            base_url: None,
1050            default_headers: HashMap::new(),
1051            auth: None,
1052            global_environment: Environment::new_global(),
1053            environments: Vec::new(),
1054            active_environment_id: None,
1055            sync: SyncConfig::default(),
1056            auto_encryption: AutoEncryptionConfig::default(),
1057            reality_level: None,
1058            fidelity_score: None,
1059        }
1060    }
1061}
1062
1063impl WorkspaceRegistry {
1064    /// Create a new workspace registry
1065    pub fn new() -> Self {
1066        Self {
1067            workspaces: HashMap::new(),
1068            active_workspace: None,
1069        }
1070    }
1071
1072    /// Add a workspace
1073    pub fn add_workspace(&mut self, mut workspace: Workspace) -> Result<EntityId> {
1074        let id = workspace.id.clone();
1075
1076        // Set order to the end of the list if not already set
1077        if workspace.order == 0 && !self.workspaces.is_empty() {
1078            workspace.order = self.workspaces.len() as i32;
1079        }
1080
1081        self.workspaces.insert(id.clone(), workspace);
1082        Ok(id)
1083    }
1084
1085    /// Get a workspace by ID
1086    pub fn get_workspace(&self, id: &str) -> Option<&Workspace> {
1087        self.workspaces.get(id)
1088    }
1089
1090    /// Get a workspace by ID (mutable)
1091    pub fn get_workspace_mut(&mut self, id: &str) -> Option<&mut Workspace> {
1092        self.workspaces.get_mut(id)
1093    }
1094
1095    /// Remove a workspace
1096    pub fn remove_workspace(&mut self, id: &str) -> Result<()> {
1097        if self.workspaces.remove(id).is_some() {
1098            // Clear active workspace if it was removed
1099            if self.active_workspace.as_deref() == Some(id) {
1100                self.active_workspace = None;
1101            }
1102            Ok(())
1103        } else {
1104            Err(Error::generic(format!("Workspace with ID '{}' not found", id)))
1105        }
1106    }
1107
1108    /// Set the active workspace
1109    pub fn set_active_workspace(&mut self, id: Option<String>) -> Result<()> {
1110        if let Some(ref workspace_id) = id {
1111            if !self.workspaces.contains_key(workspace_id) {
1112                return Err(Error::generic(format!(
1113                    "Workspace with ID '{}' not found",
1114                    workspace_id
1115                )));
1116            }
1117        }
1118        self.active_workspace = id;
1119        Ok(())
1120    }
1121
1122    /// Get the active workspace
1123    pub fn get_active_workspace(&self) -> Option<&Workspace> {
1124        self.active_workspace.as_ref().and_then(|id| self.workspaces.get(id))
1125    }
1126
1127    /// Get the active workspace ID
1128    pub fn get_active_workspace_id(&self) -> Option<&str> {
1129        self.active_workspace.as_deref()
1130    }
1131
1132    /// Get all workspaces
1133    pub fn get_workspaces(&self) -> Vec<&Workspace> {
1134        self.workspaces.values().collect()
1135    }
1136
1137    /// Get all workspaces sorted by order
1138    pub fn get_workspaces_ordered(&self) -> Vec<&Workspace> {
1139        let mut workspaces: Vec<&Workspace> = self.workspaces.values().collect();
1140        workspaces.sort_by_key(|w| w.order);
1141        workspaces
1142    }
1143
1144    /// Update the order of workspaces
1145    pub fn update_workspaces_order(&mut self, workspace_ids: Vec<String>) -> Result<()> {
1146        // Validate that all provided IDs exist
1147        for workspace_id in &workspace_ids {
1148            if !self.workspaces.contains_key(workspace_id) {
1149                return Err(Error::generic(format!(
1150                    "Workspace with ID '{}' not found",
1151                    workspace_id
1152                )));
1153            }
1154        }
1155
1156        // Update order for each workspace
1157        for (index, workspace_id) in workspace_ids.iter().enumerate() {
1158            if let Some(workspace) = self.workspaces.get_mut(workspace_id) {
1159                workspace.order = index as i32;
1160                workspace.updated_at = Utc::now();
1161            }
1162        }
1163
1164        Ok(())
1165    }
1166
1167    /// Get all routes from all workspaces
1168    pub fn get_all_routes(&self) -> Vec<Route> {
1169        let mut all_routes = Vec::new();
1170        for workspace in self.workspaces.values() {
1171            all_routes.extend(workspace.get_routes());
1172        }
1173        all_routes
1174    }
1175
1176    /// Create a route registry from all workspaces
1177    pub fn create_route_registry(&self) -> Result<RouteRegistry> {
1178        let mut registry = RouteRegistry::new();
1179        let routes = self.get_all_routes();
1180
1181        for route in routes {
1182            registry.add_http_route(route)?;
1183        }
1184
1185        Ok(registry)
1186    }
1187}
1188
1189impl Default for WorkspaceRegistry {
1190    fn default() -> Self {
1191        Self::new()
1192    }
1193}
1194
1195#[cfg(test)]
1196mod tests {
1197    use super::*;
1198
1199    use crate::ApiKeyConfig;
1200
1201    #[test]
1202    fn test_workspace_creation() {
1203        let workspace = Workspace::new("Test Workspace".to_string());
1204        assert_eq!(workspace.name, "Test Workspace");
1205        assert!(!workspace.id.is_empty());
1206        assert!(workspace.folders.is_empty());
1207        assert!(workspace.requests.is_empty());
1208    }
1209
1210    #[test]
1211    fn test_folder_creation() {
1212        let folder = Folder::new("Test Folder".to_string());
1213        assert_eq!(folder.name, "Test Folder");
1214        assert!(!folder.id.is_empty());
1215        assert!(folder.folders.is_empty());
1216        assert!(folder.requests.is_empty());
1217    }
1218
1219    #[test]
1220    fn test_request_creation() {
1221        let request =
1222            MockRequest::new(HttpMethod::GET, "/test".to_string(), "Test Request".to_string());
1223        assert_eq!(request.name, "Test Request");
1224        assert_eq!(request.method, HttpMethod::GET);
1225        assert_eq!(request.path, "/test");
1226        assert_eq!(request.response.status_code, 200);
1227    }
1228
1229    #[test]
1230    fn test_workspace_hierarchy() {
1231        let mut workspace = Workspace::new("Test Workspace".to_string());
1232
1233        // Add folder
1234        let folder_id = workspace.add_folder("Test Folder".to_string()).unwrap();
1235        assert_eq!(workspace.folders.len(), 1);
1236
1237        // Add request to workspace
1238        let request =
1239            MockRequest::new(HttpMethod::GET, "/test".to_string(), "Test Request".to_string());
1240        workspace.add_request(request).unwrap();
1241        assert_eq!(workspace.requests.len(), 1);
1242
1243        // Add request to folder
1244        let folder = workspace.find_folder_mut(&folder_id).unwrap();
1245        let folder_request = MockRequest::new(
1246            HttpMethod::POST,
1247            "/folder-test".to_string(),
1248            "Folder Request".to_string(),
1249        );
1250        folder.add_request(folder_request).unwrap();
1251        assert_eq!(folder.requests.len(), 1);
1252    }
1253
1254    #[test]
1255    fn test_workspace_registry() {
1256        let mut registry = WorkspaceRegistry::new();
1257
1258        let workspace = Workspace::new("Test Workspace".to_string());
1259        let workspace_id = registry.add_workspace(workspace).unwrap();
1260
1261        // Set as active
1262        registry.set_active_workspace(Some(workspace_id.clone())).unwrap();
1263        assert!(registry.get_active_workspace().is_some());
1264
1265        // Get workspace
1266        let retrieved = registry.get_workspace(&workspace_id).unwrap();
1267        assert_eq!(retrieved.name, "Test Workspace");
1268
1269        // Remove workspace
1270        registry.remove_workspace(&workspace_id).unwrap();
1271        assert!(registry.get_workspace(&workspace_id).is_none());
1272    }
1273
1274    #[test]
1275    fn test_inheritance_header_priority() {
1276        let mut workspace = Workspace::new("Test Workspace".to_string());
1277        workspace
1278            .config
1279            .default_headers
1280            .insert("X-Common".to_string(), "workspace-value".to_string());
1281        workspace
1282            .config
1283            .default_headers
1284            .insert("X-Workspace-Only".to_string(), "workspace-only-value".to_string());
1285
1286        // Add folder with inheritance
1287        let mut folder = Folder::new("Test Folder".to_string());
1288        folder
1289            .inheritance
1290            .headers
1291            .insert("X-Common".to_string(), "folder-value".to_string());
1292        folder
1293            .inheritance
1294            .headers
1295            .insert("X-Folder-Only".to_string(), "folder-only-value".to_string());
1296
1297        // Test single folder
1298        let folder_path = vec![&folder];
1299        let effective_headers = workspace.get_effective_headers(&folder_path);
1300
1301        assert_eq!(effective_headers.get("X-Common").unwrap(), "folder-value"); // Folder overrides workspace
1302        assert_eq!(effective_headers.get("X-Workspace-Only").unwrap(), "workspace-only-value"); // Workspace value preserved
1303        assert_eq!(effective_headers.get("X-Folder-Only").unwrap(), "folder-only-value");
1304        // Folder value added
1305    }
1306
1307    #[test]
1308    fn test_inheritance_request_headers_override() {
1309        let mut workspace = Workspace::new("Test Workspace".to_string());
1310        workspace
1311            .config
1312            .default_headers
1313            .insert("Authorization".to_string(), "Bearer workspace-token".to_string());
1314
1315        let folder_path = vec![];
1316        let effective_headers = workspace.get_effective_headers(&folder_path);
1317        let mut request = MockRequest::new(
1318            crate::routing::HttpMethod::GET,
1319            "/test".to_string(),
1320            "Test Request".to_string(),
1321        );
1322
1323        // Request headers should override inherited ones
1324        request
1325            .headers
1326            .insert("Authorization".to_string(), "Bearer request-token".to_string());
1327
1328        // Apply inheritance - request headers should take priority
1329        request.apply_inheritance(effective_headers, None);
1330
1331        assert_eq!(request.headers.get("Authorization").unwrap(), "Bearer request-token");
1332    }
1333
1334    #[test]
1335    fn test_inheritance_nested_folders() {
1336        let mut workspace = Workspace::new("Test Workspace".to_string());
1337        workspace
1338            .config
1339            .default_headers
1340            .insert("X-Level".to_string(), "workspace".to_string());
1341
1342        // Parent folder
1343        let mut parent_folder = Folder::new("Parent Folder".to_string());
1344        parent_folder
1345            .inheritance
1346            .headers
1347            .insert("X-Level".to_string(), "parent".to_string());
1348        parent_folder
1349            .inheritance
1350            .headers
1351            .insert("X-Parent-Only".to_string(), "parent-value".to_string());
1352
1353        // Child folder
1354        let mut child_folder = Folder::new("Child Folder".to_string());
1355        child_folder
1356            .inheritance
1357            .headers
1358            .insert("X-Level".to_string(), "child".to_string());
1359        child_folder
1360            .inheritance
1361            .headers
1362            .insert("X-Child-Only".to_string(), "child-value".to_string());
1363
1364        // Parent-to-child hierarchy
1365        let folder_path = vec![&parent_folder, &child_folder];
1366        let effective_headers = workspace.get_effective_headers(&folder_path);
1367
1368        // Child should override parent which overrides workspace
1369        assert_eq!(effective_headers.get("X-Level").unwrap(), "child");
1370        assert_eq!(effective_headers.get("X-Parent-Only").unwrap(), "parent-value");
1371        assert_eq!(effective_headers.get("X-Child-Only").unwrap(), "child-value");
1372    }
1373
1374    #[test]
1375    fn test_inheritance_auth_from_folder() {
1376        // Create workspace without auth
1377        let workspace = Workspace::new("Test Workspace".to_string());
1378
1379        // Create folder with auth
1380        let mut folder = Folder::new("Test Folder".to_string());
1381        let auth = AuthConfig {
1382            require_auth: true,
1383            api_key: Some(ApiKeyConfig {
1384                header_name: "X-API-Key".to_string(),
1385                query_name: Some("api_key".to_string()),
1386                keys: vec!["folder-key".to_string()],
1387            }),
1388            ..Default::default()
1389        };
1390        folder.inheritance.auth = Some(auth);
1391
1392        let folder_path = vec![&folder];
1393        let effective_auth = workspace.get_effective_auth(&folder_path);
1394
1395        assert!(effective_auth.is_some());
1396        let auth_config = effective_auth.unwrap();
1397        assert!(auth_config.require_auth);
1398        let api_key_config = auth_config.api_key.as_ref().unwrap();
1399        assert_eq!(api_key_config.keys, vec!["folder-key".to_string()]);
1400    }
1401}