Skip to main content

rch_common/config/
profiles.rs

1//! Configuration profiles for RCH.
2//!
3//! Profiles provide preset configurations for different environments:
4//! - dev: Development mode with verbose logging
5//! - prod: Production mode with minimal logging
6//! - test: Testing mode with mock SSH enabled
7
8use std::env;
9use tracing::debug;
10
11/// Predefined configuration profiles.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Profile {
14    /// Development mode: verbose logging, relaxed settings.
15    Dev,
16    /// Production mode: minimal logging, strict settings.
17    Prod,
18    /// Testing mode: mock SSH enabled, test fixtures.
19    Test,
20    /// Custom profile loaded from file.
21    Custom,
22}
23
24impl Profile {
25    /// Get the profile from the RCH_PROFILE environment variable.
26    pub fn from_env() -> Option<Self> {
27        let value = env::var("RCH_PROFILE").ok()?;
28        Some(Self::from_string(&value))
29    }
30
31    /// Parse a profile from a string.
32    pub fn from_string(s: &str) -> Self {
33        match s.to_lowercase().as_str() {
34            "dev" | "development" => Profile::Dev,
35            "prod" | "production" => Profile::Prod,
36            "test" | "testing" => Profile::Test,
37            _ => Profile::Custom,
38        }
39    }
40
41    /// Get the profile name as a string.
42    pub fn as_str(&self) -> &'static str {
43        match self {
44            Profile::Dev => "dev",
45            Profile::Prod => "prod",
46            Profile::Test => "test",
47            Profile::Custom => "custom",
48        }
49    }
50
51    /// Get profile defaults as key-value pairs.
52    ///
53    /// Returns environment variable defaults for this profile.
54    /// Only returns variables that are NOT already set in the environment.
55    /// The caller is responsible for actually setting them if desired.
56    pub fn get_defaults(&self) -> Vec<(&'static str, &'static str)> {
57        let candidates = match self {
58            Profile::Dev => {
59                debug!("Getting dev profile defaults");
60                vec![("RCH_LOG_LEVEL", "debug"), ("RCH_LOG_FORMAT", "pretty")]
61            }
62            Profile::Prod => {
63                debug!("Getting prod profile defaults");
64                vec![
65                    ("RCH_LOG_LEVEL", "warn"),
66                    ("RCH_LOG_FORMAT", "json"),
67                    ("RCH_ENABLE_METRICS", "true"),
68                ]
69            }
70            Profile::Test => {
71                debug!("Getting test profile defaults");
72                vec![
73                    ("RCH_MOCK_SSH", "1"),
74                    ("RCH_LOG_LEVEL", "debug"),
75                    ("RCH_TEST_MODE", "1"),
76                ]
77            }
78            Profile::Custom => {
79                debug!("Custom profile - no automatic defaults");
80                vec![]
81            }
82        };
83
84        // Filter out already-set variables
85        candidates
86            .into_iter()
87            .filter(|(key, _)| env::var(key).is_err())
88            .collect()
89    }
90
91    /// Get profile-specific description.
92    pub fn description(&self) -> &'static str {
93        match self {
94            Profile::Dev => "Development mode with verbose logging and relaxed settings",
95            Profile::Prod => "Production mode with minimal logging and strict settings",
96            Profile::Test => "Testing mode with mock SSH and test fixtures enabled",
97            Profile::Custom => "Custom profile with user-defined settings",
98        }
99    }
100
101    /// Check if this profile enables mock SSH by default.
102    pub fn uses_mock_ssh(&self) -> bool {
103        matches!(self, Profile::Test)
104    }
105
106    /// Check if this profile enables verbose logging by default.
107    pub fn is_verbose(&self) -> bool {
108        matches!(self, Profile::Dev | Profile::Test)
109    }
110}
111
112impl std::fmt::Display for Profile {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        write!(f, "{}", self.as_str())
115    }
116}
117
118#[cfg(test)]
119#[allow(unsafe_code)]
120mod tests {
121    use super::*;
122
123    fn env_guard() -> std::sync::MutexGuard<'static, ()> {
124        super::super::env_test_lock()
125    }
126
127    /// Helper to safely set an env var in tests.
128    fn set_env(key: &str, value: &str) {
129        // SAFETY: Tests are serialized with env_guard().
130        unsafe { std::env::set_var(key, value) };
131    }
132
133    /// Helper to safely remove an env var in tests.
134    fn remove_env(key: &str) {
135        // SAFETY: Tests are serialized with env_guard().
136        unsafe { std::env::remove_var(key) };
137    }
138
139    #[test]
140    fn test_profile_from_string() {
141        assert_eq!(Profile::from_string("dev"), Profile::Dev);
142        assert_eq!(Profile::from_string("DEV"), Profile::Dev);
143        assert_eq!(Profile::from_string("development"), Profile::Dev);
144        assert_eq!(Profile::from_string("prod"), Profile::Prod);
145        assert_eq!(Profile::from_string("production"), Profile::Prod);
146        assert_eq!(Profile::from_string("test"), Profile::Test);
147        assert_eq!(Profile::from_string("testing"), Profile::Test);
148        assert_eq!(Profile::from_string("unknown"), Profile::Custom);
149    }
150
151    #[test]
152    fn test_profile_as_str() {
153        assert_eq!(Profile::Dev.as_str(), "dev");
154        assert_eq!(Profile::Prod.as_str(), "prod");
155        assert_eq!(Profile::Test.as_str(), "test");
156        assert_eq!(Profile::Custom.as_str(), "custom");
157    }
158
159    #[test]
160    fn test_profile_uses_mock_ssh() {
161        assert!(!Profile::Dev.uses_mock_ssh());
162        assert!(!Profile::Prod.uses_mock_ssh());
163        assert!(Profile::Test.uses_mock_ssh());
164        assert!(!Profile::Custom.uses_mock_ssh());
165    }
166
167    #[test]
168    fn test_profile_is_verbose() {
169        assert!(Profile::Dev.is_verbose());
170        assert!(!Profile::Prod.is_verbose());
171        assert!(Profile::Test.is_verbose());
172        assert!(!Profile::Custom.is_verbose());
173    }
174
175    #[test]
176    fn test_dev_profile_defaults() {
177        let _guard = env_guard();
178        // Clean up any vars that might affect the test
179        remove_env("RCH_LOG_LEVEL");
180        remove_env("RCH_LOG_FORMAT");
181
182        let defaults = Profile::Dev.get_defaults();
183
184        // If variables are unset, defaults should include them; otherwise they should be omitted.
185        if env::var("RCH_LOG_LEVEL").is_ok() {
186            assert!(!defaults.iter().any(|(k, _)| *k == "RCH_LOG_LEVEL"));
187        } else {
188            assert!(
189                defaults
190                    .iter()
191                    .any(|(k, v)| *k == "RCH_LOG_LEVEL" && *v == "debug")
192            );
193        }
194
195        if env::var("RCH_LOG_FORMAT").is_ok() {
196            assert!(!defaults.iter().any(|(k, _)| *k == "RCH_LOG_FORMAT"));
197        } else {
198            assert!(
199                defaults
200                    .iter()
201                    .any(|(k, v)| *k == "RCH_LOG_FORMAT" && *v == "pretty")
202            );
203        }
204    }
205
206    #[test]
207    fn test_test_profile_enables_mock() {
208        let _guard = env_guard();
209        // Clean up any vars that might affect the test
210        remove_env("RCH_MOCK_SSH");
211        remove_env("RCH_TEST_MODE");
212        remove_env("RCH_LOG_LEVEL");
213
214        let defaults = Profile::Test.get_defaults();
215
216        // Should include mock SSH and test mode
217        assert!(
218            defaults
219                .iter()
220                .any(|(k, v)| *k == "RCH_MOCK_SSH" && *v == "1")
221        );
222        assert!(
223            defaults
224                .iter()
225                .any(|(k, v)| *k == "RCH_TEST_MODE" && *v == "1")
226        );
227    }
228
229    #[test]
230    fn test_profile_doesnt_override() {
231        let _guard = env_guard();
232        // Set a value before getting defaults
233        set_env("RCH_LOG_LEVEL", "error");
234
235        let defaults = Profile::Dev.get_defaults();
236
237        // Should NOT include RCH_LOG_LEVEL since it's already set
238        assert!(!defaults.iter().any(|(k, _)| *k == "RCH_LOG_LEVEL"));
239        // But should include RCH_LOG_FORMAT which isn't set
240        assert!(
241            defaults
242                .iter()
243                .any(|(k, v)| *k == "RCH_LOG_FORMAT" && *v == "pretty")
244        );
245
246        // Clean up
247        remove_env("RCH_LOG_LEVEL");
248    }
249}