rgen_utils/
app_config.rs

1use config::builder::DefaultState;
2use config::{Config, ConfigBuilder, Environment};
3use lazy_static::{__Deref, lazy_static};
4use serde::{Deserialize, Serialize};
5use std::path::Path;
6use std::sync::RwLock;
7
8use super::error::Result;
9use crate::types::LogLevel;
10
11// CONFIG static variable. It's actually an AppConfig
12// inside an RwLock.
13lazy_static! {
14    pub static ref BUILDER: RwLock<ConfigBuilder<DefaultState>> = RwLock::new(Config::builder());
15}
16
17#[derive(Debug, Serialize, Deserialize)]
18pub struct Database {
19    pub url: String,
20    pub variable: String,
21}
22
23#[derive(Debug, Serialize, Deserialize)]
24pub struct AppConfig {
25    pub debug: bool,
26    pub log_level: LogLevel,
27    pub database: Database,
28}
29
30impl AppConfig {
31    /// Initialize AppConfig.
32    pub fn init(default_config: Option<&str>) -> Result<()> {
33        let mut builder = Config::builder();
34
35        // Embed file into executable
36        // This macro will embed the configuration file into the
37        // executable. Check include_str! for more info.
38        if let Some(config_contents) = default_config {
39            //let contents = include_str!(config_file_path);
40            builder = builder.add_source(config::File::from_str(
41                config_contents,
42                config::FileFormat::Toml,
43            ));
44        }
45
46        // Merge settings with env variables
47        builder = builder.add_source(Environment::with_prefix("APP"));
48
49        // Save Config to RwLoc
50        {
51            let mut w = BUILDER.write()?;
52            *w = builder;
53        }
54
55        Ok(())
56    }
57
58    pub fn merge_args(args: clap::ArgMatches) -> Result<()> {
59        // Merge Clap arguments with existing configuration
60        // This allows CLI arguments to override environment variables and config files
61
62        if args.contains_id("debug") {
63            let value: &bool = args.get_one("debug").unwrap_or(&false);
64            AppConfig::set("debug", &value.to_string())?;
65        }
66
67        if args.contains_id("log_level") {
68            let value: &LogLevel = args.get_one("log_level").unwrap_or(&LogLevel::Info);
69            AppConfig::set("log_level", &value.to_string())?;
70        }
71
72        // Add support for more CLI arguments
73        if args.contains_id("config") {
74            if let Some(config_path) = args.get_one::<String>("config") {
75                AppConfig::merge_config(Some(Path::new(config_path)))?;
76            }
77        }
78
79        Ok(())
80    }
81
82    /// Initialize configuration with proper precedence order:
83    /// 1. Default configuration (embedded)
84    /// 2. Configuration file (if specified)
85    /// 3. Environment variables (APP_*)
86    /// 4. CLI arguments (highest precedence)
87    pub fn init_with_args(
88        default_config: Option<&str>, args: Option<clap::ArgMatches>,
89    ) -> Result<()> {
90        // Initialize with defaults and environment variables
91        AppConfig::init(default_config)?;
92
93        // Merge CLI arguments if provided
94        if let Some(arg_matches) = args {
95            AppConfig::merge_args(arg_matches)?;
96        }
97
98        Ok(())
99    }
100
101    pub fn merge_config(config_file: Option<&Path>) -> Result<()> {
102        // Merge settings with config file if there is one
103        if let Some(config_file_path) = config_file {
104            {
105                let mut w = BUILDER.write().unwrap();
106                *w = w.clone().add_source(config::File::with_name(
107                    config_file_path.to_str().unwrap_or(""),
108                ));
109            }
110        }
111        Ok(())
112    }
113
114    // Set CONFIG
115    pub fn set(key: &str, value: &str) -> Result<()> {
116        {
117            let mut w = BUILDER.write().unwrap();
118            *w = w.clone().set_override(key, value)?;
119        }
120
121        Ok(())
122    }
123
124    // Get a single value
125    pub fn get<'de, T>(key: &'de str) -> Result<T>
126    where
127        T: serde::Deserialize<'de>,
128    {
129        Ok(BUILDER.read()?.deref().clone().build()?.get::<T>(key)?)
130    }
131
132    // Get CONFIG
133    // This clones Config (from RwLock<Config>) into a new AppConfig object.
134    // This means you have to fetch this again if you changed the configuration.
135    pub fn fetch() -> Result<AppConfig> {
136        // Get a Read Lock from RwLock
137        let r = BUILDER.read()?;
138
139        // Clone the Config object
140        let config_clone = r.deref().clone().build()?;
141
142        // Coerce Config into AppConfig
143        let app_config: AppConfig = config_clone.try_into()?;
144        Ok(app_config)
145    }
146}
147
148// Coerce Config into AppConfig
149
150impl TryFrom<Config> for AppConfig {
151    type Error = crate::error::Error;
152
153    fn try_from(config: Config) -> Result<Self> {
154        Ok(AppConfig {
155            debug: config.get_bool("debug")?,
156            log_level: config.get::<LogLevel>("log_level")?,
157            database: config.get::<Database>("database")?,
158        })
159    }
160}