Skip to main content

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(&current_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}