newton_prover_core/config/
loader.rs1use glob::glob;
4use serde::de::DeserializeOwned;
5use std::path::PathBuf;
6
7pub trait ConfigLoader: DeserializeOwned + Sized {
33 const FILE_NAME: &'static str;
36
37 const ENV_PREFIX: &'static str;
40
41 fn load_config(path_override: Option<PathBuf>) -> Result<Self, eyre::Error> {
67 let _ = crate::config::dotenv::init();
69
70 let app_base_dir =
71 std::env::var("APP_BASE_DIR").unwrap_or_else(|_| format!("{}/../../", env!("CARGO_MANIFEST_DIR")));
72
73 let builder = config::Config::builder();
74 let config = match path_override {
75 Some(path) => builder.add_source(config::File::from(path).format(config::FileFormat::Toml)),
76 None => builder.add_source(
77 glob(format!("{}/**/{}.toml", app_base_dir, Self::FILE_NAME).as_str())
78 .unwrap()
79 .map(|path| config::File::from(path.unwrap()))
80 .collect::<Vec<_>>(),
81 ),
82 }
83 .add_source(
84 config::Environment::with_prefix(Self::ENV_PREFIX)
85 .separator("__")
86 .try_parsing(false)
87 .ignore_empty(true),
88 )
89 .build()?;
90
91 config
92 .try_deserialize::<Self>()
93 .map_err(|e| eyre::eyre!("Failed to deserialize configuration: {e}"))
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use serde::{Deserialize, Serialize};
101 use std::env;
102 use tempfile::NamedTempFile;
103 use tracing::info;
104
105 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
106 struct TestNested {
107 pub private_key: Option<String>,
108 }
109
110 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
111 struct TestConfig {
112 pub signer: TestNested,
113 pub bls: TestNested,
114 pub top_level: String,
115 }
116
117 impl ConfigLoader for TestConfig {
118 const FILE_NAME: &'static str = "test-config";
119 const ENV_PREFIX: &'static str = "TEST";
120 }
121
122 #[test]
123 fn test_nested_env_var_override_investigation() {
124 env::remove_var("TEST_SIGNER__PRIVATE_KEY");
126 env::remove_var("TEST_BLS__PRIVATE_KEY");
127 env::remove_var("TEST_TOP__LEVEL");
128 env::remove_var("SIGNER__PRIVATE_KEY");
129
130 let toml_content = r#"
131 top_level = "from_file"
132 [signer]
133 private_key = "file_signer_key"
134 [bls]
135 private_key = "file_bls_key"
136 "#;
137
138 let temp_file = NamedTempFile::new().unwrap();
139 std::fs::write(temp_file.path(), toml_content).unwrap();
140
141 env::set_var("SIGNER__PRIVATE_KEY", "env_signer_no_prefix");
143 let config1 = config::Config::builder()
144 .add_source(config::File::from(temp_file.path()).format(config::FileFormat::Toml))
145 .add_source(config::Environment::default().separator("__").try_parsing(true))
146 .build()
147 .unwrap()
148 .try_deserialize::<TestConfig>()
149 .unwrap();
150 info!(
151 "Test 1 (no prefix, SIGNER__PRIVATE_KEY): {:?}",
152 config1.signer.private_key
153 );
154
155 env::remove_var("SIGNER__PRIVATE_KEY");
156
157 env::set_var("TEST_SIGNER_PRIVATE_KEY", "env_signer_single");
159 let config2 = config::Config::builder()
160 .add_source(config::File::from(temp_file.path()).format(config::FileFormat::Toml))
161 .add_source(
162 config::Environment::with_prefix("TEST")
163 .separator("_")
164 .try_parsing(true),
165 )
166 .build()
167 .unwrap()
168 .try_deserialize::<TestConfig>()
169 .unwrap();
170 info!(
171 "Test 2 (prefix TEST, single _, TEST_SIGNER_PRIVATE_KEY): {:?}",
172 config2.signer.private_key
173 );
174
175 env::remove_var("TEST_SIGNER_PRIVATE_KEY");
176
177 env::set_var("TEST_SIGNER__PRIVATE_KEY", "env_signer_double");
179 let config3 = config::Config::builder()
180 .add_source(config::File::from(temp_file.path()).format(config::FileFormat::Toml))
181 .add_source(
182 config::Environment::with_prefix("TEST")
183 .separator("__")
184 .try_parsing(true),
185 )
186 .build()
187 .unwrap()
188 .try_deserialize::<TestConfig>()
189 .unwrap();
190 info!(
191 "Test 3 (prefix TEST, double __, TEST_SIGNER__PRIVATE_KEY): {:?}",
192 config3.signer.private_key
193 );
194
195 env::set_var("TEST_SIGNER__PRIVATE_KEY", "env_signer_double_v2");
199
200 let config_env_only = config::Config::builder()
202 .add_source(
203 config::Environment::with_prefix("TEST")
204 .separator("__")
205 .try_parsing(true),
206 )
207 .build()
208 .unwrap();
209
210 if let Ok(all_keys) = config_env_only.get::<toml::Value>("") {
212 info!("All keys from env-only config: {:?}", all_keys);
213 }
214
215 info!(
217 "Test 4a - Direct path 'signer.private_key': {:?}",
218 config_env_only.get::<String>("signer.private_key")
219 );
220 info!(
221 "Test 4b - Direct path 'signer__private_key': {:?}",
222 config_env_only.get::<String>("signer__private_key")
223 );
224
225 env::remove_var("TEST_SIGNER__PRIVATE_KEY");
226
227 env::set_var("TEST__SIGNER__PRIVATE_KEY", "env_signer_with_prefix_sep");
230 let config5 = config::Config::builder()
231 .add_source(config::File::from(temp_file.path()).format(config::FileFormat::Toml))
232 .add_source(
233 config::Environment::with_prefix("TEST")
234 .separator("__")
235 .try_parsing(true),
236 )
237 .build()
238 .unwrap()
239 .try_deserialize::<TestConfig>()
240 .unwrap();
241 info!(
242 "Test 5 (TEST__SIGNER__PRIVATE_KEY with prefix separator): {:?}",
243 config5.signer.private_key
244 );
245 env::remove_var("TEST__SIGNER__PRIVATE_KEY");
246
247 }
253
254 #[test]
255 fn test_nested_env_var_override_works() {
256 env::remove_var("TEST__SIGNER__PRIVATE_KEY");
260 env::remove_var("TEST__BLS__PRIVATE_KEY");
261 env::remove_var("TEST__TOP__LEVEL");
262
263 let toml_content = r#"
264 top_level = "from_file"
265 [signer]
266 private_key = "file_signer_key"
267 [bls]
268 private_key = "file_bls_key"
269 "#;
270
271 let temp_file = NamedTempFile::new().unwrap();
272 std::fs::write(temp_file.path(), toml_content).unwrap();
273
274 env::set_var("TEST__SIGNER__PRIVATE_KEY", "env_signer_key");
276 env::set_var("TEST__BLS__PRIVATE_KEY", "env_bls_key");
277 env::set_var("TEST__TOP__LEVEL", "env_top_level");
280
281 let config = TestConfig::load_config(Some(temp_file.path().to_path_buf())).unwrap();
282
283 assert_eq!(
285 config.signer.private_key,
286 Some("env_signer_key".to_string()),
287 "Nested env var override should work with TEST__SIGNER__PRIVATE_KEY"
288 );
289 assert_eq!(
290 config.bls.private_key,
291 Some("env_bls_key".to_string()),
292 "Nested env var override should work with TEST__BLS__PRIVATE_KEY"
293 );
294 env::remove_var("TEST__SIGNER__PRIVATE_KEY");
300 env::remove_var("TEST__BLS__PRIVATE_KEY");
301 env::remove_var("TEST__TOP__LEVEL");
302 }
303}