1use serde::de::DeserializeOwned;
14
15#[derive(thiserror::Error, Debug)]
17pub enum ConfigError {
18 #[error("module '{module}' not found")]
19 ModuleNotFound { module: String },
20 #[error("module '{module}' config must be an object")]
21 InvalidModuleStructure { module: String },
22 #[error("missing 'config' section in module '{module}'")]
23 MissingConfigSection { module: String },
24 #[error("invalid config for module '{module}': {source}")]
25 InvalidConfig {
26 module: String,
27 #[source]
28 source: serde_json::Error,
29 },
30}
31
32pub trait ConfigProvider: Send + Sync {
34 fn get_module_config(&self, module_name: &str) -> Option<&serde_json::Value>;
36}
37
38pub fn module_config_or_default<T: DeserializeOwned + Default>(
51 provider: &dyn ConfigProvider,
52 module_name: &str,
53) -> Result<T, ConfigError> {
54 let Some(module_raw) = provider.get_module_config(module_name) else {
56 return Ok(T::default());
57 };
58
59 let Some(obj) = module_raw.as_object() else {
61 return Ok(T::default());
62 };
63
64 let Some(config_section) = obj.get("config") else {
66 return Ok(T::default());
67 };
68
69 let config: T =
71 serde_json::from_value(config_section.clone()).map_err(|e| ConfigError::InvalidConfig {
72 module: module_name.to_owned(),
73 source: e,
74 })?;
75
76 Ok(config)
77}
78
79pub fn module_config_required<T: DeserializeOwned>(
92 provider: &dyn ConfigProvider,
93 module_name: &str,
94) -> Result<T, ConfigError> {
95 let module_raw =
96 provider
97 .get_module_config(module_name)
98 .ok_or_else(|| ConfigError::ModuleNotFound {
99 module: module_name.to_owned(),
100 })?;
101
102 let obj = module_raw
104 .as_object()
105 .ok_or_else(|| ConfigError::InvalidModuleStructure {
106 module: module_name.to_owned(),
107 })?;
108
109 let config_section = obj
110 .get("config")
111 .ok_or_else(|| ConfigError::MissingConfigSection {
112 module: module_name.to_owned(),
113 })?;
114
115 let config: T =
116 serde_json::from_value(config_section.clone()).map_err(|e| ConfigError::InvalidConfig {
117 module: module_name.to_owned(),
118 source: e,
119 })?;
120
121 Ok(config)
122}
123
124#[cfg(test)]
125#[cfg_attr(coverage_nightly, coverage(off))]
126mod tests {
127 use super::*;
128 use serde::Deserialize;
129 use serde_json::json;
130 use std::collections::HashMap;
131
132 #[derive(Debug, PartialEq, Deserialize, Default)]
133 struct TestConfig {
134 #[serde(default)]
135 api_key: String,
136 #[serde(default)]
137 timeout_ms: u64,
138 #[serde(default)]
139 enabled: bool,
140 }
141
142 struct MockConfigProvider {
143 modules: HashMap<String, serde_json::Value>,
144 }
145
146 impl MockConfigProvider {
147 fn new() -> Self {
148 let mut modules = HashMap::new();
149
150 modules.insert(
152 "test_module".to_owned(),
153 json!({
154 "database": {
155 "url": "postgres://localhost/test"
156 },
157 "config": {
158 "api_key": "secret123",
159 "timeout_ms": 5000,
160 "enabled": true
161 }
162 }),
163 );
164
165 modules.insert(
167 "no_config_module".to_owned(),
168 json!({
169 "database": {
170 "url": "postgres://localhost/test"
171 }
172 }),
173 );
174
175 modules.insert("invalid_module".to_owned(), json!("not an object"));
177
178 Self { modules }
179 }
180 }
181
182 impl ConfigProvider for MockConfigProvider {
183 fn get_module_config(&self, module_name: &str) -> Option<&serde_json::Value> {
184 self.modules.get(module_name)
185 }
186 }
187
188 #[test]
191 fn test_lenient_success() {
192 let provider = MockConfigProvider::new();
193 let result: Result<TestConfig, ConfigError> =
194 module_config_or_default(&provider, "test_module");
195
196 assert!(result.is_ok());
197 let config = result.unwrap();
198 assert_eq!(config.api_key, "secret123");
199 assert_eq!(config.timeout_ms, 5000);
200 assert!(config.enabled);
201 }
202
203 #[test]
204 fn test_lenient_module_not_found_returns_default() {
205 let provider = MockConfigProvider::new();
206 let result: Result<TestConfig, ConfigError> =
207 module_config_or_default(&provider, "nonexistent");
208
209 assert!(result.is_ok());
210 let config = result.unwrap();
211 assert_eq!(config, TestConfig::default());
212 }
213
214 #[test]
215 fn test_lenient_missing_config_section_returns_default() {
216 let provider = MockConfigProvider::new();
217 let result: Result<TestConfig, ConfigError> =
218 module_config_or_default(&provider, "no_config_module");
219
220 assert!(result.is_ok());
221 let config = result.unwrap();
222 assert_eq!(config, TestConfig::default());
223 }
224
225 #[test]
226 fn test_lenient_invalid_structure_returns_default() {
227 let provider = MockConfigProvider::new();
228 let result: Result<TestConfig, ConfigError> =
229 module_config_or_default(&provider, "invalid_module");
230
231 assert!(result.is_ok());
232 let config = result.unwrap();
233 assert_eq!(config, TestConfig::default());
234 }
235
236 #[test]
237 fn test_lenient_invalid_config_returns_error() {
238 let mut provider = MockConfigProvider::new();
239 provider.modules.insert(
241 "bad_config_module".to_owned(),
242 json!({
243 "config": {
244 "api_key": "secret123",
245 "timeout_ms": "not_a_number", "enabled": true
247 }
248 }),
249 );
250
251 let result: Result<TestConfig, ConfigError> =
252 module_config_or_default(&provider, "bad_config_module");
253
254 assert!(matches!(result, Err(ConfigError::InvalidConfig { .. })));
255 if let Err(ConfigError::InvalidConfig { module, .. }) = result {
256 assert_eq!(module, "bad_config_module");
257 }
258 }
259
260 #[test]
261 fn test_lenient_helper_with_multiple_scenarios() {
262 let provider = MockConfigProvider::new();
263
264 let result: Result<TestConfig, ConfigError> =
266 module_config_or_default(&provider, "nonexistent");
267 assert!(result.is_ok());
268 assert_eq!(result.unwrap(), TestConfig::default());
269
270 let result: Result<TestConfig, ConfigError> =
272 module_config_or_default(&provider, "test_module");
273 assert!(result.is_ok());
274 let config = result.unwrap();
275 assert_eq!(config.api_key, "secret123");
276 }
277
278 #[test]
281 fn test_strict_success() {
282 let provider = MockConfigProvider::new();
283 let result: Result<TestConfig, ConfigError> =
284 module_config_required(&provider, "test_module");
285
286 assert!(result.is_ok());
287 let config = result.unwrap();
288 assert_eq!(config.api_key, "secret123");
289 assert_eq!(config.timeout_ms, 5000);
290 assert!(config.enabled);
291 }
292
293 #[test]
294 fn test_strict_module_not_found() {
295 let provider = MockConfigProvider::new();
296 let result: Result<TestConfig, ConfigError> =
297 module_config_required(&provider, "nonexistent");
298
299 assert!(matches!(result, Err(ConfigError::ModuleNotFound { .. })));
300 if let Err(ConfigError::ModuleNotFound { module }) = result {
301 assert_eq!(module, "nonexistent");
302 }
303 }
304
305 #[test]
306 fn test_strict_missing_config_section() {
307 let provider = MockConfigProvider::new();
308 let result: Result<TestConfig, ConfigError> =
309 module_config_required(&provider, "no_config_module");
310
311 assert!(matches!(
312 result,
313 Err(ConfigError::MissingConfigSection { .. })
314 ));
315 if let Err(ConfigError::MissingConfigSection { module }) = result {
316 assert_eq!(module, "no_config_module");
317 }
318 }
319
320 #[test]
321 fn test_strict_invalid_structure() {
322 let provider = MockConfigProvider::new();
323 let result: Result<TestConfig, ConfigError> =
324 module_config_required(&provider, "invalid_module");
325
326 assert!(matches!(
327 result,
328 Err(ConfigError::InvalidModuleStructure { .. })
329 ));
330 if let Err(ConfigError::InvalidModuleStructure { module }) = result {
331 assert_eq!(module, "invalid_module");
332 }
333 }
334
335 #[test]
336 fn test_strict_invalid_config() {
337 let mut provider = MockConfigProvider::new();
338 provider.modules.insert(
340 "bad_config_module".to_owned(),
341 json!({
342 "config": {
343 "api_key": "secret123",
344 "timeout_ms": "not_a_number", "enabled": true
346 }
347 }),
348 );
349
350 let result: Result<TestConfig, ConfigError> =
351 module_config_required(&provider, "bad_config_module");
352
353 assert!(matches!(result, Err(ConfigError::InvalidConfig { .. })));
354 if let Err(ConfigError::InvalidConfig { module, .. }) = result {
355 assert_eq!(module, "bad_config_module");
356 }
357 }
358
359 #[test]
362 fn test_config_error_messages() {
363 let module_not_found = ConfigError::ModuleNotFound {
364 module: "test".to_owned(),
365 };
366 assert_eq!(module_not_found.to_string(), "module 'test' not found");
367
368 let invalid_structure = ConfigError::InvalidModuleStructure {
369 module: "test".to_owned(),
370 };
371 assert_eq!(
372 invalid_structure.to_string(),
373 "module 'test' config must be an object"
374 );
375
376 let missing_config = ConfigError::MissingConfigSection {
377 module: "test".to_owned(),
378 };
379 assert_eq!(
380 missing_config.to_string(),
381 "missing 'config' section in module 'test'"
382 );
383 }
384}