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