kabu_core_config/
config_loader.rs

1use async_trait::async_trait;
2use dotenvy::dotenv;
3use regex::{Captures, Regex};
4use serde::de::DeserializeOwned;
5use std::{env, fs};
6use thiserror::Error;
7
8#[derive(Debug, Error)]
9pub enum LoadConfigError {
10    #[error("IO error: {0}")]
11    IoError(#[from] std::io::Error),
12    #[error("TOML error: {0}")]
13    TomlError(#[from] toml::de::Error),
14    #[error("Error loading config: {0}")]
15    ConfigError(String),
16}
17
18#[async_trait]
19pub trait ConfigLoader {
20    type SectionType;
21
22    async fn load_from_file(file_name: String) -> Result<Self::SectionType, LoadConfigError>;
23}
24
25pub trait ConfigLoaderSync {
26    type SectionType;
27
28    fn load_from_file_sync(file_name: String) -> Result<Self::SectionType, LoadConfigError>;
29}
30
31pub async fn load_from_file<T: DeserializeOwned>(file_name: String) -> Result<T, LoadConfigError> {
32    dotenv().ok();
33    let contents = tokio::fs::read_to_string(file_name).await?;
34    let contents = expand_vars(&contents);
35    let config: T = toml::from_str(&contents)?;
36    Ok(config)
37}
38
39pub fn load_from_file_sync<T: DeserializeOwned>(file_name: String) -> Result<T, LoadConfigError> {
40    dotenv().ok();
41    let contents = fs::read_to_string(file_name)?;
42    let contents = expand_vars(&contents);
43    let config: T = toml::from_str(&contents)?;
44    Ok(config)
45}
46
47fn expand_vars(raw_config: &str) -> String {
48    // Expands ${VAR_NAME} with environment variables
49    let re = Regex::new(r"\$\{([a-zA-Z_][0-9a-zA-Z_]*)\}").unwrap();
50    re.replace_all(raw_config, |caps: &Captures| match env::var(&caps[1]) {
51        Ok(val) => val,
52        Err(_) => caps[0].to_string(),
53    })
54    .to_string()
55}