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