vine_core/config/
mod.rs

1mod property_resolver;
2
3use std::collections::HashMap;
4use std::path::Path;
5use std::sync::Arc;
6use config::{Config, Environment, File};
7use crate::context::context::Context;
8use crate::core::bean_def::BeanDef;
9use crate::core::Error;
10use crate::core::ty::Type;
11
12pub use property_resolver::PropertyResolver as PropertyResolver;
13
14pub fn get_config_context(config_files: Vec<String>) -> Result<Context, Error> {
15    let config_context = Context::new("config");
16
17    let ty = Type::of::<Config>();
18    ty.add_downcast::<Config>(|b| Ok(Arc::downcast::<Config>(b)?));
19    ty.add_downcast::<dyn PropertyResolver + Send + Sync>(|b| Ok(Arc::downcast::<Config>(b)?));
20
21    let mut config_builder = Config::builder();
22    for config_file in config_files {
23        if Path::new(config_file.as_str()).exists() {
24            log::debug!("Loading config file: {}", config_file);
25            config_builder = config_builder.add_source(File::with_name(&config_file))
26        }
27    }
28
29    // Use environment variables with APP prefix (e.g., APP_server_port=8080)
30    log::debug!("Loading environment variables with APP prefix");
31    config_builder = config_builder.add_source(
32        Environment::with_prefix("APP").prefix_separator("_").separator(".").try_parsing(true)
33    );
34
35    // Parse -- style command line arguments (e.g., --server.port=8080)
36    log::debug!("Parsing -- style command line arguments");
37    let d_args: HashMap<String, String> = std::env::args()
38        .filter_map(|arg| {
39            if let Some(stripped) = arg.strip_prefix("--") {
40                stripped.split_once('=').map(|(k, v)| (k.to_string(), v.to_string()))
41            } else {
42                None
43            }
44        })
45        .collect();
46
47    for (key, value) in d_args {
48        config_builder = config_builder.set_override(key, value).map_err(|e| {
49            Error::from(format!("configuration error: {:#?}", e))
50        })?;
51    }
52
53    let config = Arc::new(config_builder.build().map_err(|e| {
54        Error::from(format!("configuration error: {:#?}", e))
55    })?);
56    config_context.register(BeanDef::builder()
57        .name("config")
58        .ty(ty)
59        .get(Arc::new(move |_| { Ok(config.clone()) }))
60        .build())?;
61
62    Ok(config_context)
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use crate::config::PropertyResolver;
69
70    #[test]
71    fn should_allow_config_downcast_to_property_resolver() {
72        let context = get_config_context(vec![]).unwrap();
73        let resolver = context.get_bean::<dyn PropertyResolver + Send + Sync>("config");
74        assert!(resolver.is_ok());
75    }
76
77    #[test]
78    fn should_load_environment_variables() {
79        std::env::set_var("APP_test_key", "test_value");
80
81        let context = get_config_context(vec![]).unwrap();
82        let resolver = context.get_bean::<dyn PropertyResolver + Send + Sync>("config").unwrap();
83        assert_eq!(resolver.get_string("test_key"), Some("test_value".to_string()));
84
85        std::env::remove_var("APP_test_key");
86    }
87
88    #[test]
89    fn should_compute_template_values() {
90        std::env::set_var("APP_prop1", "value1");
91        std::env::set_var("APP_prop2", "value2");
92
93        let context = get_config_context(vec![]).unwrap();
94        let resolver = context.get_bean::<dyn PropertyResolver + Send + Sync>("config").unwrap();
95
96        let result = resolver.compute_template_value("${prop1}_${prop2}").unwrap();
97        assert_eq!(result, "value1_value2");
98
99        std::env::remove_var("APP_prop1");
100        std::env::remove_var("APP_prop2");
101    }
102
103    #[test]
104    fn should_compute_template_values_with_defaults() {
105        let context = get_config_context(vec![]).unwrap();
106        let resolver = context.get_bean::<dyn PropertyResolver + Send + Sync>("config").unwrap();
107
108        let result = resolver.compute_template_value("${missing_prop:default_value}").unwrap();
109        assert_eq!(result, "default_value");
110    }
111
112    #[test]
113    fn should_compute_typed_template_values() {
114        std::env::set_var("APP_server.port", "8080");
115        std::env::set_var("APP_enabled", "true");
116
117        let context = get_config_context(vec![]).unwrap();
118        let resolver = context.get_bean::<dyn PropertyResolver + Send + Sync>("config").unwrap();
119
120        assert_eq!(resolver.compute_template_value_as_u16("${server.port}").unwrap(), 8080);
121        assert_eq!(resolver.compute_template_value_as_bool("${enabled}").unwrap(), true);
122
123        std::env::remove_var("APP_server_port");
124        std::env::remove_var("APP_enabled");
125    }
126}