settings_loader/loading_options.rs
1use std::path::PathBuf;
2
3use config::builder::DefaultState;
4use config::ConfigBuilder;
5
6use crate::environment::Environment;
7use crate::error::SettingsError;
8use crate::layer::LayerBuilder;
9
10pub const APP_ENVIRONMENT: &str = "APP_ENVIRONMENT";
11
12/// Defines the contract for specifying how configuration settings are loaded.
13///
14/// The `LoadingOptions` trait provides a flexible way to define where and how configuration
15/// settings should be retrieved. It enables applications to specify explicit configuration files,
16/// secrets files, search paths, and environment overrides, ensuring that configurations can be
17/// dynamically loaded from various sources.
18///
19/// The `SettingsLoader::load()` function serves as the primary driver of the settings loading
20/// process, but it relies on `LoadingOptions` to determine the details of where and how to fetch
21/// configuration values.
22///
23/// Implementing this trait allows users to:
24/// - Specify an explicit configuration file.
25/// - Define the location of a secrets file.
26/// - Configure search paths for implicit configuration loading.
27/// - Apply additional override mechanisms.
28/// - Resolve the active environment.
29///
30/// ## Usage Example
31///
32/// ```rust,no_run
33/// use std::path::PathBuf;
34/// use settings_loader::{Environment, LoadingOptions, SettingsError};
35///
36/// struct CliOptions {
37/// config: Option<PathBuf>,
38/// secrets: Option<PathBuf>,
39/// environment: Option<Environment>,
40/// }
41///
42/// impl LoadingOptions for CliOptions {
43/// type Error = SettingsError;
44///
45/// fn config_path(&self) -> Option<PathBuf> {
46/// self.config.clone()
47/// }
48///
49/// fn secrets_path(&self) -> Option<PathBuf> {
50/// self.secrets.clone()
51/// }
52///
53/// fn implicit_search_paths(&self) -> Vec<PathBuf> {
54/// vec![PathBuf::from("./config")]
55/// }
56/// }
57/// ```
58pub trait LoadingOptions: Sized {
59 /// The error type that can be returned from configuration operations.
60 type Error: std::error::Error + From<SettingsError> + Sync + Send + 'static;
61
62 /// Returns the path to an explicit configuration file, if provided.
63 ///
64 /// If a configuration file is specified via the command-line or other means,
65 /// this function should return its path. If `None` is returned, the system will
66 /// attempt to infer configuration from default locations.
67 fn config_path(&self) -> Option<PathBuf>;
68
69 /// Returns the path to a secrets file, if specified.
70 ///
71 /// This is useful for separating sensitive credentials (e.g., database passwords)
72 /// from the main configuration files.
73 fn secrets_path(&self) -> Option<PathBuf>;
74
75 /// Returns a list of directories to search for configuration files.
76 ///
77 /// If an explicit configuration file is not provided, this function determines
78 /// which directories will be scanned for inferred configuration files.
79 fn implicit_search_paths(&self) -> Vec<PathBuf>;
80
81 /// Allows customization of the configuration before finalization.
82 ///
83 /// This method provides an opportunity to apply additional runtime overrides,
84 /// such as modifying individual settings dynamically.
85 ///
86 /// The default implementation returns the configuration unchanged.
87 fn load_overrides(&self, config: ConfigBuilder<DefaultState>) -> Result<ConfigBuilder<DefaultState>, Self::Error> {
88 Ok(config)
89 }
90
91 /// Determines the active environment for configuration resolution.
92 ///
93 /// This function checks for an explicit environment override, falling back to
94 /// an environment variable lookup (`APP_ENVIRONMENT`). If no value is found,
95 /// it logs a warning and defaults to `None`.
96 fn environment(&self) -> Option<Environment> {
97 let env: Option<Environment> = self
98 .environment_override()
99 .map(Result::<_, std::env::VarError>::Ok)
100 .or_else(|| match std::env::var(Self::env_app_environment()) {
101 Ok(env_rep) => Some(Ok(env_rep.into())),
102 Err(std::env::VarError::NotPresent) => {
103 ::tracing::warn!(
104 "no environment variable override set at env var, {}",
105 Self::env_app_environment()
106 );
107
108 None
109 },
110 Err(err) => Some(Err(err)),
111 })
112 .transpose()
113 .expect("failed to pull application environment");
114
115 ::tracing::info!("loading settings for environment: {env:?}");
116 env
117 }
118
119 /// Provides an optional explicit override for the environment.
120 ///
121 /// This can be used to manually specify the environment without relying
122 /// on environment variables.
123 fn environment_override(&self) -> Option<Environment> {
124 None
125 }
126
127 /// Returns the environment variable key used to determine the application environment.
128 ///
129 /// Defaults to `"APP_ENVIRONMENT"`.
130 fn env_app_environment() -> &'static str {
131 APP_ENVIRONMENT
132 }
133
134 /// Returns the prefix for environment variables.
135 ///
136 /// Default prefix is `"APP"`. Override to customize environment variable naming convention.
137 ///
138 /// # Example
139 ///
140 /// ```rust,no_run
141 /// use std::path::PathBuf;
142 /// use settings_loader::LoadingOptions;
143 ///
144 /// struct TurtleOptions;
145 ///
146 /// impl LoadingOptions for TurtleOptions {
147 /// type Error = settings_loader::SettingsError;
148 ///
149 /// fn config_path(&self) -> Option<PathBuf> { None }
150 /// fn secrets_path(&self) -> Option<PathBuf> { None }
151 /// fn implicit_search_paths(&self) -> Vec<PathBuf> { Vec::new() }
152 ///
153 /// fn env_prefix() -> &'static str {
154 /// "TURTLE" // Override to use TURTLE__* convention
155 /// }
156 /// }
157 /// ```
158 fn env_prefix() -> &'static str {
159 "APP"
160 }
161
162 /// Returns the separator for nested environment variable keys.
163 ///
164 /// Default separator is `"__"` (double underscore). Override to customize how nested
165 /// configuration keys map to environment variable names.
166 ///
167 /// # Example
168 ///
169 /// With default separator `"__"`:
170 /// - `APP__DATABASE__HOST` maps to `database.host`
171 ///
172 /// With custom separator `"_"`:
173 /// - `APP_DATABASE_HOST` maps to `database.host`
174 ///
175 /// # Example
176 ///
177 /// ```rust,no_run
178 /// use std::path::PathBuf;
179 /// use settings_loader::LoadingOptions;
180 ///
181 /// struct CustomOptions;
182 ///
183 /// impl LoadingOptions for CustomOptions {
184 /// type Error = settings_loader::SettingsError;
185 ///
186 /// fn config_path(&self) -> Option<PathBuf> { None }
187 /// fn secrets_path(&self) -> Option<PathBuf> { None }
188 /// fn implicit_search_paths(&self) -> Vec<PathBuf> { Vec::new() }
189 ///
190 /// fn env_separator() -> &'static str {
191 /// "_" // Override to use single underscore
192 /// }
193 /// }
194 /// ```
195 fn env_separator() -> &'static str {
196 "__"
197 }
198
199 /// Build explicit configuration layers.
200 ///
201 /// Default implementation returns builder unchanged (backward compatible).
202 /// Override to define explicit layer composition.
203 fn build_layers(&self, builder: LayerBuilder) -> LayerBuilder {
204 builder
205 }
206}
207
208/// Trait for multi-scope configuration support with automatic path resolution.
209///
210/// This trait extends `LoadingOptions` to support loading configuration from multiple
211/// scopes (System, UserGlobal, ProjectLocal, Runtime) with automatic, platform-specific
212/// path resolution.
213///
214/// # Scopes
215///
216/// - `System` - Immutable system-wide defaults (platform-specific location)
217/// - `UserGlobal` - User preferences that apply everywhere (platform-specific location)
218/// - `ProjectLocal` - Project-specific overrides (current directory)
219/// - `Runtime` - Dynamic configuration from environment variables and CLI
220///
221/// # Constants
222///
223/// - `APP_NAME` - Required: The application name (e.g., "my-app")
224/// - `ORG_NAME` - Optional: Organization name for platform path resolution
225/// - `CONFIG_BASENAME` - Optional: Base name for config files (default: "settings")
226///
227/// # Platform Path Resolution
228///
229/// UserGlobal paths use platform conventions:
230/// - **Linux**: `~/.config/{APP_NAME}/settings.{ext}` (XDG Base Directory spec)
231/// - **macOS**: `~/Library/Application Support/{APP_NAME}/settings.{ext}`
232/// - **Windows**: `%APPDATA%/{APP_NAME}/settings.{ext}`
233///
234/// # Example
235///
236/// ```ignore
237/// use settings_loader::{LoadingOptions, MultiScopeConfig, ConfigScope};
238/// use std::path::{Path, PathBuf};
239///
240/// struct TurtleOptions;
241///
242/// impl LoadingOptions for TurtleOptions {
243/// type Error = settings_loader::SettingsError;
244/// fn config_path(&self) -> Option<PathBuf> { None }
245/// fn secrets_path(&self) -> Option<PathBuf> { None }
246/// fn implicit_search_paths(&self) -> Vec<PathBuf> { Vec::new() }
247/// }
248///
249/// impl MultiScopeConfig for TurtleOptions {
250/// const APP_NAME: &'static str = "spark-turtle";
251/// const ORG_NAME: &'static str = "spark-turtle";
252/// const CONFIG_BASENAME: &'static str = "settings";
253///
254/// fn find_config_in(dir: &Path) -> Option<PathBuf> {
255/// crate::scope::find_config_in(dir)
256/// }
257/// }
258/// ```
259pub trait MultiScopeConfig: LoadingOptions {
260 /// The application name used for path resolution (e.g., "my-app")
261 const APP_NAME: &'static str;
262
263 /// Optional organization name for platform-specific paths
264 const ORG_NAME: &'static str = "";
265
266 /// Base name for configuration files (default: "settings")
267 const CONFIG_BASENAME: &'static str = "settings";
268
269 /// Resolve a configuration path for the given scope.
270 ///
271 /// Returns the resolved path if the configuration file exists for that scope,
272 /// or `None` if the file doesn't exist or the scope is not file-based (Runtime).
273 ///
274 /// # Default Behavior
275 ///
276 /// The default implementation resolves paths based on scope:
277 /// - `System` - Platform-specific system path
278 /// - `UserGlobal` - Platform-specific user path using `directories` crate
279 /// - `ProjectLocal` - Current directory search
280 /// - `Runtime` - None (not file-based)
281 fn resolve_path(scope: crate::ConfigScope) -> Option<PathBuf> {
282 use crate::ConfigScope;
283
284 match scope {
285 ConfigScope::Preferences => Self::preferences_path(),
286 ConfigScope::UserGlobal => Self::user_global_path(),
287 ConfigScope::ProjectLocal => Self::project_local_path(),
288 ConfigScope::LocalData => Self::local_data_path(),
289 ConfigScope::PersistentData => Self::persistent_data_path(),
290 ConfigScope::Runtime => None,
291 }
292 }
293
294 /// Resolve the preferences configuration path.
295 ///
296 /// Platform-specific implementations using `directories` crate:
297 /// - **Linux**: `~/.config/APP_NAME/settings.{ext}` (or XDG_CONFIG_HOME/APP_NAME)
298 /// - **macOS**: `~/Library/Preferences/APP_NAME/settings.{ext}`
299 /// - **Windows**: `%APPDATA%/APP_NAME/settings.{ext}`
300 ///
301 /// # Requires Feature Flag
302 ///
303 /// This method requires the `multi-scope` feature (which enables the `directories` crate).
304 ///
305 /// # Returns
306 ///
307 /// `Some(PathBuf)` if configuration file exists in preferences path, `None` otherwise.
308 #[cfg(feature = "multi-scope")]
309 fn preferences_path() -> Option<PathBuf> {
310 use directories::BaseDirs;
311
312 let dirs = BaseDirs::new()?;
313 let pref_dir = dirs.preference_dir();
314 let app_dir = pref_dir.join(Self::APP_NAME);
315 Self::find_config_in(&app_dir)
316 }
317
318 #[cfg(not(feature = "multi-scope"))]
319 fn preferences_path() -> Option<PathBuf> {
320 None
321 }
322
323 /// Resolve the user global configuration path.
324 ///
325 /// Platform-specific implementations:
326 /// - **Linux**: `~/.config/{APP_NAME}/settings.{ext}` (XDG_CONFIG_HOME)
327 /// - **macOS**: `~/Library/Application Support/{APP_NAME}/settings.{ext}`
328 /// - **Windows**: `%APPDATA%/{APP_NAME}/settings.{ext}`
329 ///
330 /// # Requires Feature Flag
331 ///
332 /// This method requires the `multi-scope` feature (which enables the `directories` crate).
333 ///
334 /// # Returns
335 ///
336 /// `Some(PathBuf)` if configuration file exists in user path, `None` otherwise.
337 #[cfg(feature = "multi-scope")]
338 fn user_global_path() -> Option<PathBuf> {
339 use directories::ProjectDirs;
340
341 let proj = ProjectDirs::from(Self::ORG_NAME, Self::ORG_NAME, Self::APP_NAME)?;
342 let config_dir = proj.config_dir();
343 Self::find_config_in(config_dir)
344 }
345
346 #[cfg(not(feature = "multi-scope"))]
347 fn user_global_path() -> Option<PathBuf> {
348 // Without directories crate, we can't reliably resolve platform paths
349 None
350 }
351
352 /// Resolve the project-local configuration path.
353 ///
354 /// Searches for configuration files in the current directory.
355 /// Files are searched in order of format preference: TOML > YAML > JSON > etc.
356 ///
357 /// # Returns
358 ///
359 /// `Some(PathBuf)` if configuration file exists in current directory, `None` otherwise.
360 fn project_local_path() -> Option<PathBuf> {
361 let current_dir = std::env::current_dir().ok()?;
362 Self::find_config_in(¤t_dir)
363 }
364
365 /// Resolve the local data configuration path.
366 ///
367 /// Platform-specific implementations using `directories` crate:
368 /// - **Linux**: `~/.cache/APP_NAME/` (or XDG_CACHE_HOME/APP_NAME)
369 /// - **macOS**: `~/Library/Caches/ORG_NAME.APP_NAME/`
370 /// - **Windows**: `%LOCALAPPDATA%/ORG_NAME/APP_NAME/`
371 ///
372 /// Use for machine-local data that is not synced across machines.
373 ///
374 /// # Requires Feature Flag
375 ///
376 /// This method requires the `multi-scope` feature (which enables the `directories` crate).
377 ///
378 /// # Returns
379 ///
380 /// `Some(PathBuf)` if configuration file exists in local data path, `None` otherwise.
381 #[cfg(feature = "multi-scope")]
382 fn local_data_path() -> Option<PathBuf> {
383 use directories::BaseDirs;
384
385 let dirs = BaseDirs::new()?;
386 let data_dir = dirs.data_local_dir();
387 let app_dir = data_dir.join(Self::APP_NAME);
388 Self::find_config_in(&app_dir)
389 }
390
391 #[cfg(not(feature = "multi-scope"))]
392 fn local_data_path() -> Option<PathBuf> {
393 None
394 }
395
396 /// Resolve the persistent data configuration path.
397 ///
398 /// Platform-specific implementations using `directories` crate:
399 /// - **Linux**: `~/.local/share/APP_NAME/` (or XDG_DATA_HOME/APP_NAME)
400 /// - **macOS**: `~/Library/Application Support/ORG_NAME.APP_NAME/`
401 /// - **Windows**: `%APPDATA%/ORG_NAME/APP_NAME/`
402 ///
403 /// Use for persistent data that can be synced across machines.
404 ///
405 /// # Requires Feature Flag
406 ///
407 /// This method requires the `multi-scope` feature (which enables the `directories` crate).
408 ///
409 /// # Returns
410 ///
411 /// `Some(PathBuf)` if configuration file exists in persistent data path, `None` otherwise.
412 #[cfg(feature = "multi-scope")]
413 fn persistent_data_path() -> Option<PathBuf> {
414 use directories::BaseDirs;
415
416 let dirs = BaseDirs::new()?;
417 let data_dir = dirs.data_dir();
418 let app_dir = data_dir.join(Self::APP_NAME);
419 Self::find_config_in(&app_dir)
420 }
421
422 #[cfg(not(feature = "multi-scope"))]
423 fn persistent_data_path() -> Option<PathBuf> {
424 None
425 }
426
427 /// Search for a configuration file with multiple format extensions.
428 ///
429 /// This method must be implemented to provide the file discovery logic.
430 /// Most implementations should delegate to `crate::scope::find_config_in()`.
431 ///
432 /// # Arguments
433 ///
434 /// * `dir` - Directory to search for configuration files
435 ///
436 /// # Returns
437 ///
438 /// The first matching configuration file found, or `None` if no file matches.
439 fn find_config_in(dir: &std::path::Path) -> Option<PathBuf>;
440
441 /// Get the list of scopes to load in precedence order.
442 ///
443 /// Default order: Preferences → UserGlobal → ProjectLocal → LocalData → PersistentData
444 /// (Runtime is handled separately via environment variables).
445 /// Override to customize scope loading order.
446 fn default_scopes() -> Vec<crate::ConfigScope> {
447 use crate::ConfigScope;
448
449 vec![
450 ConfigScope::Preferences,
451 ConfigScope::UserGlobal,
452 ConfigScope::ProjectLocal,
453 ConfigScope::LocalData,
454 ConfigScope::PersistentData,
455 ]
456 }
457}
458
459pub type NoOptions = ();
460
461impl LoadingOptions for () {
462 type Error = SettingsError;
463
464 fn config_path(&self) -> Option<PathBuf> {
465 None
466 }
467
468 fn secrets_path(&self) -> Option<PathBuf> {
469 None
470 }
471
472 fn implicit_search_paths(&self) -> Vec<PathBuf> {
473 Vec::default()
474 }
475}
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use std::path::Path;
480 use tempfile::tempdir; // Import tempdir for use in tests
481
482 struct MockOptions {
483 config: Option<PathBuf>,
484 secrets: Option<PathBuf>,
485 env_override: Option<Environment>,
486 }
487
488 impl LoadingOptions for MockOptions {
489 type Error = SettingsError;
490
491 fn config_path(&self) -> Option<PathBuf> {
492 self.config.clone()
493 }
494
495 fn secrets_path(&self) -> Option<PathBuf> {
496 self.secrets.clone()
497 }
498
499 fn implicit_search_paths(&self) -> Vec<PathBuf> {
500 vec![PathBuf::from("/etc/app"), PathBuf::from("./config")]
501 }
502
503 fn environment_override(&self) -> Option<Environment> {
504 self.env_override.clone()
505 }
506 }
507
508 impl MultiScopeConfig for MockOptions {
509 const APP_NAME: &'static str = "test-app";
510 const ORG_NAME: &'static str = "test-org";
511
512 fn find_config_in(dir: &Path) -> Option<PathBuf> {
513 let path = dir.join("settings.toml");
514 if path.exists() {
515 Some(path)
516 } else {
517 None
518 }
519 }
520 }
521
522 #[test]
523 fn test_loading_options_default_impl() {
524 let opts = MockOptions {
525 config: Some(PathBuf::from("config.toml")),
526 secrets: Some(PathBuf::from("secrets.toml")),
527 env_override: Some(Environment::from("development")),
528 };
529
530 assert_eq!(opts.config_path(), Some(PathBuf::from("config.toml")));
531 assert_eq!(opts.secrets_path(), Some(PathBuf::from("secrets.toml")));
532 assert_eq!(opts.implicit_search_paths().len(), 2);
533 assert_eq!(opts.environment_override(), Some(Environment::from("development")));
534 assert_eq!(MockOptions::env_prefix(), "APP");
535 assert_eq!(MockOptions::env_separator(), "__");
536 }
537
538 #[test]
539 fn test_multi_scope_config_default_scopes() {
540 let scopes = MockOptions::default_scopes();
541 assert_eq!(scopes.len(), 5);
542 assert!(scopes.contains(&crate::ConfigScope::Preferences));
543 assert!(scopes.contains(&crate::ConfigScope::ProjectLocal));
544 }
545
546 #[test]
547 fn test_no_options_impl() {
548 let opts: NoOptions = ();
549 assert_eq!(opts.config_path(), None);
550 assert_eq!(opts.secrets_path(), None);
551 assert!(opts.implicit_search_paths().is_empty());
552 }
553
554 #[test]
555 fn test_multi_scope_resolve_path_runtime() {
556 assert_eq!(MockOptions::resolve_path(crate::ConfigScope::Runtime), None);
557 }
558
559 #[test]
560 #[cfg(not(feature = "multi-scope"))]
561 fn test_multi_scope_resolve_path_preferences_no_feature() {
562 assert_eq!(MockOptions::resolve_path(crate::ConfigScope::Preferences), None);
563 }
564
565 #[test]
566 #[cfg(not(feature = "multi-scope"))]
567 fn test_multi_scope_resolve_path_user_global_no_feature() {
568 assert_eq!(MockOptions::resolve_path(crate::ConfigScope::UserGlobal), None);
569 }
570
571 #[test]
572 #[cfg(not(feature = "multi-scope"))]
573 fn test_multi_scope_resolve_path_local_data_no_feature() {
574 assert_eq!(MockOptions::resolve_path(crate::ConfigScope::LocalData), None);
575 }
576
577 #[test]
578 #[cfg(not(feature = "multi-scope"))]
579 fn test_multi_scope_resolve_path_persistent_data_no_feature() {
580 assert_eq!(MockOptions::resolve_path(crate::ConfigScope::PersistentData), None);
581 }
582
583 #[test]
584 fn test_multi_scope_resolve_path_project_local() {
585 use std::fs;
586 use tempfile::tempdir;
587
588 // Create a temporary directory and a settings.toml file in it
589 let dir = tempdir().unwrap();
590 let current_dir_backup = std::env::current_dir().unwrap();
591
592 let file_path_in_temp = dir.path().join("settings.toml");
593 fs::write(&file_path_in_temp, "key = \"value\"").unwrap();
594
595 std::env::set_current_dir(dir.path()).unwrap();
596
597 // Use a consistent canonicalized path for the assertion
598 let expected_canonical_path = file_path_in_temp.canonicalize().unwrap();
599
600 let resolved_path = MockOptions::resolve_path(crate::ConfigScope::ProjectLocal);
601 assert!(resolved_path.is_some(), "Resolved path should not be None");
602 assert_eq!(resolved_path.unwrap().canonicalize().unwrap(), expected_canonical_path);
603
604 std::env::set_current_dir(current_dir_backup).unwrap(); // Restore original current directory
605 }
606
607 #[test]
608 fn test_multi_scope_resolve_path_project_local_no_file() {
609 let dir = tempdir().unwrap();
610 let current_dir_backup = std::env::current_dir().unwrap();
611 std::env::set_current_dir(&dir).unwrap();
612
613 let resolved_path = MockOptions::resolve_path(crate::ConfigScope::ProjectLocal);
614 assert_eq!(resolved_path, None);
615
616 std::env::set_current_dir(current_dir_backup).unwrap();
617 }
618
619 #[test]
620 fn test_environment_with_override() {
621 let opts = MockOptions {
622 config: None,
623 secrets: None,
624 env_override: Some(Environment::from("test_override")),
625 };
626 assert_eq!(opts.environment(), Some(Environment::from("test_override")));
627 }
628
629 #[test]
630 fn test_environment_with_env_var() {
631 // Ensure APP_ENVIRONMENT is not set initially for a clean test
632 std::env::remove_var("APP_ENVIRONMENT");
633 std::env::set_var("APP_ENVIRONMENT", "test_env_var");
634
635 let opts = MockOptions { config: None, secrets: None, env_override: None };
636 assert_eq!(opts.environment(), Some(Environment::from("test_env_var")));
637 std::env::remove_var("APP_ENVIRONMENT"); // Clean up
638 }
639
640 #[test]
641 fn test_environment_no_override_no_env_var() {
642 std::env::remove_var("APP_ENVIRONMENT"); // Ensure it's not set
643
644 let opts = MockOptions { config: None, secrets: None, env_override: None };
645 // It should warn and return None. We can't easily capture logs, so just check None.
646 assert_eq!(opts.environment(), None);
647 }
648
649 #[test]
650 fn test_environment_env_var_with_custom_name() {
651 std::env::remove_var("CUSTOM_APP_ENV"); // Clean up any previous run
652 std::env::set_var("CUSTOM_APP_ENV", "custom_env");
653
654 struct CustomEnvOptions;
655 impl LoadingOptions for CustomEnvOptions {
656 type Error = SettingsError;
657 fn config_path(&self) -> Option<PathBuf> {
658 None
659 }
660 fn secrets_path(&self) -> Option<PathBuf> {
661 None
662 }
663 fn implicit_search_paths(&self) -> Vec<PathBuf> {
664 Vec::new()
665 }
666 fn env_app_environment() -> &'static str {
667 "CUSTOM_APP_ENV"
668 }
669 }
670
671 let opts = CustomEnvOptions;
672 assert_eq!(opts.environment(), Some(Environment::from("custom_env")));
673 std::env::remove_var("CUSTOM_APP_ENV"); // Clean up
674 }
675}