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