1use super::env::Env;
2use super::{ConfigRegistry, Configurable};
3use crate::error::{AppError, Result};
4use anyhow::Context;
5use serde::de::DeserializeOwned;
6use serde_toml_merge::merge_tables;
7use std::fs;
8use std::path::Path;
9use std::str::FromStr;
10use toml::Table;
11
12#[derive(Default)]
14pub struct TomlConfigRegistry {
15 config: Table,
16}
17
18impl ConfigRegistry for TomlConfigRegistry {
19 fn get_config<T>(&self) -> Result<T>
20 where
21 T: DeserializeOwned + Configurable,
22 {
23 let prefix = T::config_prefix();
24 let table = self.get_by_prefix(prefix);
25 T::deserialize(table.to_owned()).map_err(|e| AppError::DeserializeErr(prefix, e))
26 }
27}
28
29impl TomlConfigRegistry {
30 pub fn new(config_path: &Path, env: Env) -> Result<Self> {
34 let config = Self::load_config(config_path, env)?;
35 Ok(Self { config })
36 }
37
38 pub fn get_by_prefix(&self, prefix: &str) -> Table {
40 match self.config.get(prefix) {
41 Some(toml::Value::Table(table)) => table.clone(),
42 _ => Table::new(),
43 }
44 }
45
46 fn load_config(config_path: &Path, env: Env) -> Result<Table> {
48 let config_file_content = fs::read_to_string(config_path);
49 let main_toml_str = match config_file_content {
50 Err(e) => {
51 log::warn!("Failed to read configuration file {:?}: {}", config_path, e);
52 return Ok(Table::new());
53 }
54 Ok(content) => super::env::interpolate(&content),
55 };
56
57 let main_table = toml::from_str::<Table>(main_toml_str.as_str())
58 .with_context(|| format!("Failed to parse the toml file at path {:?}", config_path))?;
59
60 let config_table: Table = match env.get_config_path(config_path) {
61 Ok(env_path) => {
62 let env_path = env_path.as_path();
63 if !env_path.exists() {
64 return Ok(main_table);
65 }
66 log::info!("The profile of the {:?} environment is active", env);
67
68 let env_toml_str = fs::read_to_string(env_path)
69 .with_context(|| format!("Failed to read configuration file {:?}", env_path))?;
70 let env_toml_str = super::env::interpolate(&env_toml_str);
71 let env_table =
72 toml::from_str::<Table>(env_toml_str.as_str()).with_context(|| {
73 format!("Failed to parse the toml file at path {:?}", env_path)
74 })?;
75 merge_tables(main_table, env_table)
76 .map_err(|e| AppError::TomlMergeError(e.to_string()))
77 .with_context(|| {
78 format!("Failed to merge files {:?} and {:?}", config_path, env_path)
79 })?
80 }
81 Err(_) => {
82 log::debug!("{:?} config not found", env);
83 main_table
84 }
85 };
86
87 Ok(config_table)
88 }
89}
90
91impl FromStr for TomlConfigRegistry {
92 type Err = AppError;
93
94 fn from_str(str: &str) -> std::result::Result<Self, Self::Err> {
95 let config = toml::from_str::<Table>(str)?;
96 Ok(Self { config })
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::Env;
103 use super::TomlConfigRegistry;
104 use crate::error::Result;
105 use std::fs;
106
107 #[test]
108 fn test_load_config() -> Result<()> {
109 let temp_dir = tempfile::tempdir()?;
110
111 let foo = temp_dir.path().join("foo.toml");
112 #[rustfmt::skip]
113 let _ = fs::write(&foo,r#"
114 [group]
115 key = "A"
116 "#,
117 );
118
119 let table = TomlConfigRegistry::new(&foo, Env::from_string("dev"))?;
120 let group = table.get_by_prefix("group");
121 assert_eq!(group.get("key").unwrap().as_str(), Some("A"));
122
123 let foo_dev = temp_dir.path().join("foo-dev.toml");
125 #[rustfmt::skip]
126 let _ = fs::write(foo_dev,r#"
127 [group]
128 key = "OOOOA"
129 "#,
130 );
131
132 let table = TomlConfigRegistry::new(&foo, Env::from_string("dev"))?;
133 let group = table.get_by_prefix("group");
134 assert_eq!(group.get("key").unwrap().as_str(), Some("OOOOA"));
135
136 Ok(())
137 }
138}