mockforge_core/workspace/
environment.rs

1//! Environment configuration and management
2//!
3//! This module provides functionality for managing environments, variable substitution,
4//! and environment-specific configurations.
5
6use crate::workspace::core::{EntityId, Environment, EnvironmentColor};
7use chrono::Utc;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Environment manager for handling multiple environments
12#[derive(Debug, Clone)]
13pub struct EnvironmentManager {
14    /// All environments indexed by ID
15    environments: HashMap<EntityId, Environment>,
16    /// Active environment ID
17    active_environment_id: Option<EntityId>,
18}
19
20/// Environment variable substitution result
21#[derive(Debug, Clone)]
22pub struct VariableSubstitution {
23    /// The substituted value
24    pub value: String,
25    /// Whether substitution was successful
26    pub success: bool,
27    /// Any errors that occurred during substitution
28    pub errors: Vec<String>,
29}
30
31/// Environment validation result
32#[derive(Debug, Clone)]
33pub struct EnvironmentValidationResult {
34    /// Whether the environment is valid
35    pub is_valid: bool,
36    /// Validation errors
37    pub errors: Vec<String>,
38    /// Validation warnings
39    pub warnings: Vec<String>,
40}
41
42/// Environment export format
43#[derive(Debug, Clone)]
44pub enum EnvironmentExportFormat {
45    /// JSON format
46    Json,
47    /// YAML format
48    Yaml,
49    /// Environment variables file format (.env)
50    DotEnv,
51    /// Custom format with template
52    Custom(String),
53}
54
55impl EnvironmentManager {
56    /// Create a new empty environment manager
57    pub fn new() -> Self {
58        Self {
59            environments: HashMap::new(),
60            active_environment_id: None,
61        }
62    }
63
64    /// Add an environment
65    pub fn add_environment(&mut self, environment: Environment) -> EntityId {
66        let id = environment.id.clone();
67        self.environments.insert(id.clone(), environment);
68
69        // Set as active if it's the first environment
70        if self.environments.len() == 1 {
71            self.active_environment_id = Some(id.clone());
72            if let Some(env) = self.environments.get_mut(&id) {
73                env.active = true;
74            }
75        }
76
77        id
78    }
79
80    /// Get an environment by ID
81    pub fn get_environment(&self, id: &EntityId) -> Option<&Environment> {
82        self.environments.get(id)
83    }
84
85    /// Get a mutable environment by ID
86    pub fn get_environment_mut(&mut self, id: &EntityId) -> Option<&mut Environment> {
87        self.environments.get_mut(id)
88    }
89
90    /// Remove an environment
91    pub fn remove_environment(&mut self, id: &EntityId) -> Result<Environment, String> {
92        if let Some(environment) = self.environments.remove(id) {
93            // Update active environment if necessary
94            if self.active_environment_id.as_ref() == Some(id) {
95                self.active_environment_id = self.environments.keys().next().cloned();
96                if let Some(active_id) = &self.active_environment_id {
97                    if let Some(env) = self.environments.get_mut(active_id) {
98                        env.active = true;
99                    }
100                }
101            }
102
103            Ok(environment)
104        } else {
105            Err(format!("Environment with ID {} not found", id))
106        }
107    }
108
109    /// Get all environments
110    pub fn get_all_environments(&self) -> Vec<&Environment> {
111        self.environments.values().collect()
112    }
113
114    /// Get the active environment
115    pub fn get_active_environment(&self) -> Option<&Environment> {
116        self.active_environment_id.as_ref().and_then(|id| self.environments.get(id))
117    }
118
119    /// Set the active environment
120    pub fn set_active_environment(&mut self, id: EntityId) -> Result<(), String> {
121        if self.environments.contains_key(&id) {
122            // Deactivate all environments
123            for environment in self.environments.values_mut() {
124                environment.active = false;
125            }
126
127            // Activate the selected environment
128            if let Some(env) = self.environments.get_mut(&id) {
129                env.active = true;
130                self.active_environment_id = Some(id);
131            }
132
133            Ok(())
134        } else {
135            Err(format!("Environment with ID {} not found", id))
136        }
137    }
138
139    /// Substitute variables in a template string
140    pub fn substitute_variables(&self, template: &str) -> VariableSubstitution {
141        let mut result = String::new();
142        let mut success = true;
143        let mut errors = Vec::new();
144
145        // Get active environment variables
146        let variables = if let Some(active_env) = self.get_active_environment() {
147            &active_env.variables
148        } else {
149            // No active environment, so return empty variables (will fail on any variable reference)
150            &std::collections::HashMap::new()
151        };
152
153        let mut chars = template.chars().peekable();
154        while let Some(ch) = chars.next() {
155            if ch == '{' && chars.peek() == Some(&'{') {
156                // Found {{
157                chars.next(); // consume second {
158                if let Some(var_name) = self.parse_variable_name(&mut chars) {
159                    if let Some(value) = variables.get(&var_name) {
160                        result.push_str(value);
161                    } else {
162                        success = false;
163                        errors.push(format!("Variable '{}' not found", var_name));
164                        result.push_str(&format!("{{{{{}}}}}", var_name));
165                    }
166                } else {
167                    result.push_str("{{");
168                }
169            } else {
170                result.push(ch);
171            }
172        }
173
174        VariableSubstitution {
175            value: result,
176            success,
177            errors,
178        }
179    }
180
181    /// Parse variable name from template
182    fn parse_variable_name(
183        &self,
184        chars: &mut std::iter::Peekable<std::str::Chars>,
185    ) -> Option<String> {
186        let mut var_name = String::new();
187
188        while let Some(ch) = chars.peek() {
189            if *ch == '}' {
190                if let Some(next_ch) = chars.clone().nth(1) {
191                    if next_ch == '}' {
192                        // Found }} - end of variable
193                        chars.next(); // consume first }
194                        chars.next(); // consume second }
195                        break;
196                    }
197                }
198            } else if ch.is_alphanumeric() || *ch == '_' || *ch == '-' || *ch == '.' {
199                var_name.push(*ch);
200                chars.next();
201            } else {
202                return None; // Invalid character in variable name
203            }
204        }
205
206        if var_name.is_empty() {
207            None
208        } else {
209            Some(var_name)
210        }
211    }
212
213    /// Validate an environment
214    pub fn validate_environment(&self, environment: &Environment) -> EnvironmentValidationResult {
215        let mut errors = Vec::new();
216        let mut warnings = Vec::new();
217
218        // Check for empty name
219        if environment.name.trim().is_empty() {
220            errors.push("Environment name cannot be empty".to_string());
221        }
222
223        // Check for duplicate variable names
224        let mut seen_variables = std::collections::HashSet::new();
225        for (key, value) in &environment.variables {
226            if !seen_variables.insert(key.clone()) {
227                errors.push(format!("Duplicate variable name: {}", key));
228            }
229
230            // Check for empty keys
231            if key.trim().is_empty() {
232                errors.push("Variable key cannot be empty".to_string());
233            }
234
235            // Check for empty values (warning)
236            if value.trim().is_empty() {
237                warnings.push(format!("Variable '{}' has empty value", key));
238            }
239        }
240
241        // Color values are u8, so always valid (0-255)
242
243        EnvironmentValidationResult {
244            is_valid: errors.is_empty(),
245            errors,
246            warnings,
247        }
248    }
249
250    /// Export environment to specified format
251    pub fn export_environment(
252        &self,
253        environment_id: &EntityId,
254        format: EnvironmentExportFormat,
255    ) -> Result<String, String> {
256        let environment = self
257            .environments
258            .get(environment_id)
259            .ok_or_else(|| format!("Environment with ID {} not found", environment_id))?;
260
261        match format {
262            EnvironmentExportFormat::Json => serde_json::to_string_pretty(environment)
263                .map_err(|e| format!("Failed to serialize environment: {}", e)),
264            EnvironmentExportFormat::Yaml => serde_yaml::to_string(environment)
265                .map_err(|e| format!("Failed to serialize environment: {}", e)),
266            EnvironmentExportFormat::DotEnv => {
267                let mut result = String::new();
268                for (key, value) in &environment.variables {
269                    result.push_str(&format!("{}={}\n", key, value));
270                }
271                Ok(result)
272            }
273            EnvironmentExportFormat::Custom(template) => {
274                let mut result = template.clone();
275                for (key, value) in &environment.variables {
276                    let placeholder = format!("{{{{{}}}}}", key);
277                    result = result.replace(&placeholder, value);
278                }
279                Ok(result)
280            }
281        }
282    }
283
284    /// Import environment from JSON
285    pub fn import_environment(&mut self, json_data: &str) -> Result<EntityId, String> {
286        let environment: Environment = serde_json::from_str(json_data)
287            .map_err(|e| format!("Failed to deserialize environment: {}", e))?;
288
289        // Validate the imported environment
290        let validation = self.validate_environment(&environment);
291        if !validation.is_valid {
292            return Err(format!("Environment validation failed: {:?}", validation.errors));
293        }
294
295        Ok(self.add_environment(environment))
296    }
297
298    /// Get environment statistics
299    pub fn get_stats(&self) -> EnvironmentStats {
300        let total_variables =
301            self.environments.values().map(|env| env.variables.len()).sum::<usize>();
302
303        let active_count = self.environments.values().filter(|env| env.active).count();
304
305        EnvironmentStats {
306            total_environments: self.environments.len(),
307            total_variables,
308            active_environments: active_count,
309        }
310    }
311
312    /// Find environments by name
313    pub fn find_environments_by_name(&self, name_query: &str) -> Vec<&Environment> {
314        let query_lower = name_query.to_lowercase();
315        self.environments
316            .values()
317            .filter(|env| env.name.to_lowercase().contains(&query_lower))
318            .collect()
319    }
320
321    /// Get all variables across all environments
322    pub fn get_all_variables(&self) -> HashMap<String, String> {
323        let mut all_vars = HashMap::new();
324
325        for environment in self.environments.values() {
326            for (key, value) in &environment.variables {
327                all_vars.insert(key.clone(), value.clone());
328            }
329        }
330
331        all_vars
332    }
333
334    /// Clone environment
335    pub fn clone_environment(
336        &mut self,
337        source_id: &EntityId,
338        new_name: String,
339    ) -> Result<EntityId, String> {
340        let source_env = self
341            .environments
342            .get(source_id)
343            .ok_or_else(|| format!("Environment with ID {} not found", source_id))?;
344
345        let mut new_env = source_env.clone();
346        new_env.id = uuid::Uuid::new_v4().to_string();
347        new_env.name = new_name;
348        new_env.active = false;
349        new_env.created_at = Utc::now();
350        new_env.updated_at = Utc::now();
351
352        Ok(self.add_environment(new_env))
353    }
354
355    /// Merge environments (combine variables)
356    pub fn merge_environments(
357        &mut self,
358        environment_ids: &[EntityId],
359        merged_name: String,
360    ) -> Result<EntityId, String> {
361        let mut merged_variables = HashMap::new();
362
363        for env_id in environment_ids {
364            let env = self
365                .environments
366                .get(env_id)
367                .ok_or_else(|| format!("Environment with ID {} not found", env_id))?;
368
369            for (key, value) in &env.variables {
370                merged_variables.insert(key.clone(), value.clone());
371            }
372        }
373
374        let mut merged_env = Environment::new(merged_name);
375        merged_env.variables = merged_variables;
376
377        Ok(self.add_environment(merged_env))
378    }
379}
380
381/// Environment statistics
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct EnvironmentStats {
384    /// Total number of environments
385    pub total_environments: usize,
386    /// Total number of variables across all environments
387    pub total_variables: usize,
388    /// Number of active environments
389    pub active_environments: usize,
390}
391
392impl Default for EnvironmentManager {
393    fn default() -> Self {
394        Self::new()
395    }
396}
397
398/// Environment variable validation utilities
399pub struct EnvironmentValidator;
400
401impl EnvironmentValidator {
402    /// Validate variable name format
403    pub fn validate_variable_name(name: &str) -> Result<(), String> {
404        if name.is_empty() {
405            return Err("Variable name cannot be empty".to_string());
406        }
407
408        if !name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
409            return Err(
410                "Variable name can only contain letters, numbers, underscores, and hyphens"
411                    .to_string(),
412            );
413        }
414
415        if name.starts_with('-') || name.ends_with('-') {
416            return Err("Variable name cannot start or end with hyphens".to_string());
417        }
418
419        Ok(())
420    }
421
422    /// Validate variable value (basic checks)
423    pub fn validate_variable_value(value: &str) -> Result<(), String> {
424        if value.contains('\0') {
425            return Err("Variable value cannot contain null characters".to_string());
426        }
427
428        Ok(())
429    }
430
431    /// Validate color values
432    pub fn validate_color(_color: &EnvironmentColor) -> Result<(), String> {
433        // Color values are u8, so always valid (0-255)
434        Ok(())
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    #[test]
443    fn test_variable_substitution() {
444        let mut manager = EnvironmentManager::new();
445        let mut env = Environment::new("test".to_string());
446        env.set_variable("API_URL".to_string(), "https://api.example.com".to_string());
447        env.set_variable("VERSION".to_string(), "1.0.0".to_string());
448        manager.add_environment(env);
449
450        let result = manager.substitute_variables("API: {{API_URL}}, Version: {{VERSION}}");
451        assert!(result.success);
452        assert_eq!(result.value, "API: https://api.example.com, Version: 1.0.0");
453    }
454
455    #[test]
456    fn test_missing_variable_substitution() {
457        let manager = EnvironmentManager::new();
458        let result = manager.substitute_variables("Missing: {{MISSING_VAR}}");
459
460        assert!(!result.success);
461        assert!(result.errors.contains(&"Variable 'MISSING_VAR' not found".to_string()));
462    }
463}