envx_core/
profile_manager.rs

1use crate::EnvVarManager;
2use crate::snapshot::Profile;
3use color_eyre::Result;
4use color_eyre::eyre::eyre;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fs;
8use std::path::PathBuf;
9
10#[derive(Debug, Serialize, Deserialize)]
11struct ProfileConfig {
12    pub active: Option<String>,
13    pub profiles: HashMap<String, Profile>,
14}
15
16pub struct ProfileManager {
17    config_path: PathBuf,
18    config: ProfileConfig,
19}
20
21impl ProfileManager {
22    /// Creates a new `ProfileManager` instance.
23    ///
24    /// # Errors
25    ///
26    /// This function will return an error if:
27    /// - The data/config directory cannot be found
28    /// - The config directory cannot be created
29    /// - The existing profiles.json file cannot be read or parsed
30    pub fn new() -> Result<Self> {
31        let config_dir = if cfg!(windows) {
32            dirs::data_dir()
33                .ok_or_else(|| eyre!("Could not find data directory"))?
34                .join("envx")
35        } else {
36            dirs::config_dir()
37                .ok_or_else(|| eyre!("Could not find config directory"))?
38                .join("envx")
39        };
40
41        fs::create_dir_all(&config_dir)?;
42        let config_path = config_dir.join("profiles.json");
43
44        let config = if config_path.exists() {
45            let content = fs::read_to_string(&config_path)?;
46            serde_json::from_str(&content)?
47        } else {
48            ProfileConfig {
49                active: None,
50                profiles: HashMap::new(),
51            }
52        };
53
54        Ok(Self { config_path, config })
55    }
56
57    /// Creates a new profile with the specified name and optional description.
58    ///
59    /// # Errors
60    ///
61    /// This function will return an error if:
62    /// - A profile with the given name already exists
63    /// - The configuration cannot be saved to disk
64    pub fn create(&mut self, name: String, description: Option<String>) -> Result<()> {
65        if self.config.profiles.contains_key(&name) {
66            return Err(eyre!("Profile '{}' already exists", name));
67        }
68
69        let profile = Profile::new(name.clone(), description);
70        self.config.profiles.insert(name, profile);
71        self.save()?;
72        Ok(())
73    }
74
75    /// Deletes the specified profile.
76    ///
77    /// If the deleted profile is currently active, the active profile will be set to None.
78    ///
79    /// # Errors
80    ///
81    /// This function will return an error if:
82    /// - The specified profile is not found
83    /// - The configuration cannot be saved to disk
84    pub fn delete(&mut self, name: &str) -> Result<()> {
85        if self.config.active.as_ref() == Some(&name.to_string()) {
86            self.config.active = None;
87        }
88
89        self.config
90            .profiles
91            .remove(name)
92            .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", name))?;
93
94        self.save()?;
95        Ok(())
96    }
97
98    #[must_use]
99    pub fn list(&self) -> Vec<&Profile> {
100        self.config.profiles.values().collect()
101    }
102
103    #[must_use]
104    pub fn get(&self, name: &str) -> Option<&Profile> {
105        self.config.profiles.get(name)
106    }
107
108    pub fn get_mut(&mut self, name: &str) -> Option<&mut Profile> {
109        self.config.profiles.get_mut(name)
110    }
111
112    #[must_use]
113    pub fn active(&self) -> Option<&Profile> {
114        self.config
115            .active
116            .as_ref()
117            .and_then(|name| self.config.profiles.get(name))
118    }
119
120    /// Switches to the specified profile, making it the active profile.
121    ///
122    /// # Errors
123    ///
124    /// This function will return an error if:
125    /// - The specified profile is not found
126    /// - The configuration cannot be saved to disk
127    pub fn switch(&mut self, name: &str) -> Result<()> {
128        if !self.config.profiles.contains_key(name) {
129            return Err(eyre!("Profile '{}' not found", name));
130        }
131
132        self.config.active = Some(name.to_string());
133        self.save()?;
134        Ok(())
135    }
136
137    /// Applies a profile's environment variables to the given `EnvVarManager`.
138    ///
139    /// If the profile has a parent profile, it will be applied first recursively,
140    /// then the current profile's variables will be applied, potentially overriding
141    /// parent values.
142    ///
143    /// # Errors
144    ///
145    /// This function will return an error if:
146    /// - The specified profile is not found
147    /// - A parent profile is not found during recursive application
148    /// - Setting environment variables in the manager fails
149    pub fn apply(&self, name: &str, manager: &mut EnvVarManager) -> Result<()> {
150        let profile = self
151            .get(name)
152            .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", name))?;
153
154        // Apply parent profile first if exists
155        if let Some(parent) = &profile.parent {
156            self.apply(parent, manager)?;
157        }
158
159        // Apply this profile's variables
160        for (var_name, var) in &profile.variables {
161            if var.enabled {
162                // Always set the variable, regardless of whether it exists
163                // This ensures profile switching actually updates values
164                manager.set(var_name, &var.value, true)?;
165            }
166        }
167
168        Ok(())
169    }
170
171    /// Exports a profile to JSON format.
172    ///
173    /// # Errors
174    ///
175    /// This function will return an error if:
176    /// - The specified profile is not found
177    /// - The profile cannot be serialized to JSON
178    pub fn export(&self, name: &str) -> Result<String> {
179        let profile = self.get(name).ok_or_else(|| eyre!("Profile '{}' not found", name))?;
180
181        Ok(serde_json::to_string_pretty(profile)?)
182    }
183
184    /// Imports a profile from JSON data.
185    ///
186    /// # Errors
187    ///
188    /// This function will return an error if:
189    /// - The profile already exists and `overwrite` is false
190    /// - The JSON data cannot be deserialized into a valid Profile
191    /// - The configuration cannot be saved to disk
192    pub fn import(&mut self, name: String, json: &str, overwrite: bool) -> Result<()> {
193        if !overwrite && self.config.profiles.contains_key(&name) {
194            return Err(eyre!("Profile '{}' already exists", name));
195        }
196
197        let mut profile: Profile = serde_json::from_str(json)?;
198        profile.name.clone_from(&name);
199
200        self.config.profiles.insert(name, profile);
201        self.save()?;
202        Ok(())
203    }
204
205    /// Saves the current profile configuration to disk.
206    ///
207    /// # Errors
208    ///
209    /// This function will return an error if:
210    /// - The configuration cannot be serialized to JSON
211    /// - The configuration file cannot be written to disk
212    pub fn save(&self) -> Result<()> {
213        let content = serde_json::to_string_pretty(&self.config)?;
214        fs::write(&self.config_path, content)?;
215        Ok(())
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use crate::ProfileVar;
222
223    use super::*;
224    use tempfile::TempDir;
225
226    fn create_test_profile_manager() -> (ProfileManager, TempDir) {
227        let temp_dir = TempDir::new().unwrap();
228        let config_path = temp_dir.path().join("profiles.json");
229
230        let config = ProfileConfig {
231            active: None,
232            profiles: HashMap::new(),
233        };
234
235        let manager = ProfileManager { config_path, config };
236
237        (manager, temp_dir)
238    }
239
240    fn create_test_profile(name: &str) -> Profile {
241        let mut profile = Profile::new(name.to_string(), Some(format!("{name} description")));
242        profile.add_var("TEST_VAR".to_string(), "test_value".to_string(), false);
243        profile
244    }
245
246    #[test]
247    fn test_profile_manager_new() {
248        // Use the test helper instead of calling ProfileManager::new() directly
249        let (manager, _temp) = create_test_profile_manager();
250
251        assert!(manager.config.profiles.is_empty());
252        assert!(manager.config.active.is_none());
253    }
254
255    #[test]
256    fn test_profile_manager_new_with_existing_config() {
257        let temp_dir = TempDir::new().unwrap();
258        let config_path = temp_dir.path().join("profiles.json");
259
260        // Create a config file
261        let mut profiles = HashMap::new();
262        profiles.insert("test".to_string(), create_test_profile("test"));
263
264        let config = ProfileConfig {
265            active: Some("test".to_string()),
266            profiles,
267        };
268
269        let content = serde_json::to_string_pretty(&config).unwrap();
270        fs::write(&config_path, content).unwrap();
271
272        // Now create manager with existing config
273        let manager = ProfileManager {
274            config_path: config_path.clone(),
275            config: if config_path.exists() {
276                let content = fs::read_to_string(&config_path).unwrap();
277                serde_json::from_str(&content).unwrap()
278            } else {
279                ProfileConfig {
280                    active: None,
281                    profiles: HashMap::new(),
282                }
283            },
284        };
285
286        assert_eq!(manager.config.profiles.len(), 1);
287        assert_eq!(manager.config.active, Some("test".to_string()));
288    }
289
290    #[test]
291    fn test_create_profile() {
292        let (mut manager, _temp) = create_test_profile_manager();
293
294        let result = manager.create("dev".to_string(), Some("Development profile".to_string()));
295        assert!(result.is_ok());
296
297        assert_eq!(manager.config.profiles.len(), 1);
298        assert!(manager.config.profiles.contains_key("dev"));
299
300        let profile = manager.get("dev").unwrap();
301        assert_eq!(profile.name, "dev");
302        assert_eq!(profile.description, Some("Development profile".to_string()));
303    }
304
305    #[test]
306    fn test_create_duplicate_profile() {
307        let (mut manager, _temp) = create_test_profile_manager();
308
309        manager.create("dev".to_string(), None).unwrap();
310        let result = manager.create("dev".to_string(), None);
311
312        assert!(result.is_err());
313        assert!(result.unwrap_err().to_string().contains("already exists"));
314    }
315
316    #[test]
317    fn test_delete_profile() {
318        let (mut manager, _temp) = create_test_profile_manager();
319
320        manager.create("dev".to_string(), None).unwrap();
321        assert_eq!(manager.config.profiles.len(), 1);
322
323        let result = manager.delete("dev");
324        assert!(result.is_ok());
325        assert_eq!(manager.config.profiles.len(), 0);
326    }
327
328    #[test]
329    fn test_delete_active_profile() {
330        let (mut manager, _temp) = create_test_profile_manager();
331
332        manager.create("dev".to_string(), None).unwrap();
333        manager.switch("dev").unwrap();
334        assert_eq!(manager.config.active, Some("dev".to_string()));
335
336        let result = manager.delete("dev");
337        assert!(result.is_ok());
338        assert!(manager.config.active.is_none());
339    }
340
341    #[test]
342    fn test_delete_nonexistent_profile() {
343        let (mut manager, _temp) = create_test_profile_manager();
344
345        let result = manager.delete("nonexistent");
346        assert!(result.is_err());
347        assert!(result.unwrap_err().to_string().contains("not found"));
348    }
349
350    #[test]
351    fn test_list_profiles() {
352        let (mut manager, _temp) = create_test_profile_manager();
353
354        manager.create("dev".to_string(), None).unwrap();
355        manager.create("prod".to_string(), None).unwrap();
356        manager.create("test".to_string(), None).unwrap();
357
358        let profiles = manager.list();
359        assert_eq!(profiles.len(), 3);
360
361        let names: Vec<&str> = profiles.iter().map(|p| p.name.as_str()).collect();
362        assert!(names.contains(&"dev"));
363        assert!(names.contains(&"prod"));
364        assert!(names.contains(&"test"));
365    }
366
367    #[test]
368    fn test_get_profile() {
369        let (mut manager, _temp) = create_test_profile_manager();
370
371        manager.create("dev".to_string(), Some("Dev env".to_string())).unwrap();
372
373        let profile = manager.get("dev");
374        assert!(profile.is_some());
375        assert_eq!(profile.unwrap().description, Some("Dev env".to_string()));
376
377        let profile = manager.get("nonexistent");
378        assert!(profile.is_none());
379    }
380
381    #[test]
382    fn test_get_mut_profile() {
383        let (mut manager, _temp) = create_test_profile_manager();
384
385        manager.create("dev".to_string(), None).unwrap();
386
387        let profile = manager.get_mut("dev").unwrap();
388        profile.add_var("NEW_VAR".to_string(), "new_value".to_string(), false);
389
390        let profile = manager.get("dev").unwrap();
391        assert!(profile.variables.contains_key("NEW_VAR"));
392    }
393
394    #[test]
395    fn test_switch_profile() {
396        let (mut manager, _temp) = create_test_profile_manager();
397
398        manager.create("dev".to_string(), None).unwrap();
399        manager.create("prod".to_string(), None).unwrap();
400
401        assert!(manager.config.active.is_none());
402
403        let result = manager.switch("dev");
404        assert!(result.is_ok());
405        assert_eq!(manager.config.active, Some("dev".to_string()));
406
407        let result = manager.switch("prod");
408        assert!(result.is_ok());
409        assert_eq!(manager.config.active, Some("prod".to_string()));
410    }
411
412    #[test]
413    fn test_switch_to_nonexistent_profile() {
414        let (mut manager, _temp) = create_test_profile_manager();
415
416        let result = manager.switch("nonexistent");
417        assert!(result.is_err());
418        assert!(result.unwrap_err().to_string().contains("not found"));
419    }
420
421    #[test]
422    fn test_active_profile() {
423        let (mut manager, _temp) = create_test_profile_manager();
424
425        assert!(manager.active().is_none());
426
427        manager.create("dev".to_string(), None).unwrap();
428        manager.switch("dev").unwrap();
429
430        let active = manager.active();
431        assert!(active.is_some());
432        assert_eq!(active.unwrap().name, "dev");
433    }
434
435    #[test]
436    fn test_apply_profile() {
437        let (mut manager, _temp) = create_test_profile_manager();
438        let mut env_manager = EnvVarManager::new();
439
440        // Create profile with variables
441        manager.create("dev".to_string(), None).unwrap();
442        let profile = manager.get_mut("dev").unwrap();
443        profile.add_var("NODE_ENV".to_string(), "development".to_string(), false);
444        profile.add_var("DEBUG".to_string(), "true".to_string(), false);
445
446        let result = manager.apply("dev", &mut env_manager);
447        assert!(result.is_ok());
448
449        // Verify variables were set
450        assert_eq!(env_manager.get("NODE_ENV").unwrap().value, "development");
451        assert_eq!(env_manager.get("DEBUG").unwrap().value, "true");
452    }
453
454    #[test]
455    fn test_apply_profile_with_disabled_var() {
456        let (mut manager, _temp) = create_test_profile_manager();
457        let mut env_manager = EnvVarManager::new();
458
459        manager.create("dev".to_string(), None).unwrap();
460        let profile = manager.get_mut("dev").unwrap();
461        profile.variables.insert(
462            "DISABLED_VAR".to_string(),
463            ProfileVar {
464                value: "should_not_be_set".to_string(),
465                enabled: false,
466                override_system: false,
467            },
468        );
469        profile.add_var("ENABLED_VAR".to_string(), "should_be_set".to_string(), false);
470
471        manager.apply("dev", &mut env_manager).unwrap();
472
473        assert!(env_manager.get("DISABLED_VAR").is_none());
474        assert_eq!(env_manager.get("ENABLED_VAR").unwrap().value, "should_be_set");
475    }
476
477    #[test]
478    fn test_apply_profile_with_parent() {
479        let (mut manager, _temp) = create_test_profile_manager();
480        let mut env_manager = EnvVarManager::new();
481
482        // Create parent profile
483        manager.create("base".to_string(), None).unwrap();
484        let profile = manager.get_mut("base").unwrap();
485        profile.add_var("BASE_VAR".to_string(), "base_value".to_string(), false);
486        profile.add_var("OVERRIDE_ME".to_string(), "base_override".to_string(), false);
487
488        // Create child profile
489        manager.create("dev".to_string(), None).unwrap();
490        let profile = manager.get_mut("dev").unwrap();
491        profile.parent = Some("base".to_string());
492        profile.add_var("DEV_VAR".to_string(), "dev_value".to_string(), false);
493        profile.add_var("OVERRIDE_ME".to_string(), "dev_override".to_string(), false);
494
495        manager.apply("dev", &mut env_manager).unwrap();
496
497        // Should have variables from both profiles
498        assert_eq!(env_manager.get("BASE_VAR").unwrap().value, "base_value");
499        assert_eq!(env_manager.get("DEV_VAR").unwrap().value, "dev_value");
500        // Child should override parent
501        assert_eq!(env_manager.get("OVERRIDE_ME").unwrap().value, "dev_override");
502    }
503
504    #[test]
505    fn test_apply_nonexistent_profile() {
506        let (manager, _temp) = create_test_profile_manager();
507        let mut env_manager = EnvVarManager::new();
508
509        let result = manager.apply("nonexistent", &mut env_manager);
510        assert!(result.is_err());
511        assert!(result.unwrap_err().to_string().contains("not found"));
512    }
513
514    #[test]
515    fn test_export_profile() {
516        let (mut manager, _temp) = create_test_profile_manager();
517
518        manager
519            .create("dev".to_string(), Some("Development".to_string()))
520            .unwrap();
521        let profile = manager.get_mut("dev").unwrap();
522        profile.add_var("TEST_VAR".to_string(), "test_value".to_string(), false);
523
524        let result = manager.export("dev");
525        assert!(result.is_ok());
526
527        let json = result.unwrap();
528        assert!(json.contains("\"name\": \"dev\""));
529        assert!(json.contains("\"description\": \"Development\""));
530        assert!(json.contains("TEST_VAR"));
531        assert!(json.contains("test_value"));
532    }
533
534    #[test]
535    fn test_export_nonexistent_profile() {
536        let (manager, _temp) = create_test_profile_manager();
537
538        let result = manager.export("nonexistent");
539        assert!(result.is_err());
540        assert!(result.unwrap_err().to_string().contains("not found"));
541    }
542
543    #[test]
544    fn test_import_profile() {
545        let (mut manager, _temp) = create_test_profile_manager();
546
547        let profile_json = r#"{
548            "name": "imported",
549            "description": "Imported profile",
550            "created_at": "2024-01-01T00:00:00Z",
551            "updated_at": "2024-01-01T00:00:00Z",
552            "variables": {
553                "IMPORT_VAR": {
554                    "value": "imported_value",
555                    "enabled": true,
556                    "override_system": false
557                }
558            },
559            "parent": null,
560            "metadata": {}
561        }"#;
562
563        let result = manager.import("new_name".to_string(), profile_json, false);
564        assert!(result.is_ok());
565
566        let profile = manager.get("new_name").unwrap();
567        assert_eq!(profile.name, "new_name"); // Should use provided name, not JSON name
568        assert_eq!(profile.description, Some("Imported profile".to_string()));
569        assert!(profile.variables.contains_key("IMPORT_VAR"));
570    }
571
572    #[test]
573    fn test_import_profile_overwrite() {
574        let (mut manager, _temp) = create_test_profile_manager();
575
576        manager.create("existing".to_string(), None).unwrap();
577
578        let profile_json = r#"{
579            "name": "imported",
580            "description": "New description",
581            "created_at": "2024-01-01T00:00:00Z",
582            "updated_at": "2024-01-01T00:00:00Z",
583            "variables": {},
584            "parent": null,
585            "metadata": {}
586        }"#;
587
588        // Should fail without overwrite
589        let result = manager.import("existing".to_string(), profile_json, false);
590        assert!(result.is_err());
591        assert!(result.unwrap_err().to_string().contains("already exists"));
592
593        // Should succeed with overwrite
594        let result = manager.import("existing".to_string(), profile_json, true);
595        assert!(result.is_ok());
596
597        let profile = manager.get("existing").unwrap();
598        assert_eq!(profile.description, Some("New description".to_string()));
599    }
600
601    #[test]
602    fn test_import_invalid_json() {
603        let (mut manager, _temp) = create_test_profile_manager();
604
605        let invalid_json = "{ invalid json }";
606
607        let result = manager.import("test".to_string(), invalid_json, false);
608        assert!(result.is_err());
609    }
610
611    #[test]
612    fn test_save_and_load() {
613        let temp_dir = TempDir::new().unwrap();
614        let config_path = temp_dir.path().join("profiles.json");
615
616        // Create and save
617        {
618            let mut manager = ProfileManager {
619                config_path: config_path.clone(),
620                config: ProfileConfig {
621                    active: None,
622                    profiles: HashMap::new(),
623                },
624            };
625
626            manager.create("dev".to_string(), None).unwrap();
627            manager.create("prod".to_string(), None).unwrap();
628            manager.switch("dev").unwrap();
629
630            let result = manager.save();
631            assert!(result.is_ok());
632        }
633
634        // Load and verify
635        {
636            assert!(config_path.exists());
637
638            let manager = ProfileManager {
639                config_path: config_path.clone(),
640                config: {
641                    let content = fs::read_to_string(&config_path).unwrap();
642                    serde_json::from_str(&content).unwrap()
643                },
644            };
645
646            assert_eq!(manager.config.profiles.len(), 2);
647            assert_eq!(manager.config.active, Some("dev".to_string()));
648            assert!(manager.get("dev").is_some());
649            assert!(manager.get("prod").is_some());
650        }
651    }
652
653    #[test]
654    fn test_profile_manager_thread_safety() {
655        // This test verifies that ProfileManager operations are safe
656        // Note: ProfileManager is not Send/Sync by default due to config mutability
657        // This test documents the current behavior
658        let (mut manager, _temp) = create_test_profile_manager();
659
660        manager.create("test".to_string(), None).unwrap();
661        let profile = manager.get("test");
662        assert!(profile.is_some());
663
664        // Mutable operations require exclusive access
665        let profile_mut = manager.get_mut("test");
666        assert!(profile_mut.is_some());
667    }
668}