1use ::id_effect::{Effect, Get, Here, effect};
9
10use crate::error::ConfigError;
11use crate::provider::{ConfigProviderKey, NeedsConfigProvider};
12
13pub trait WithConfigDefault<A, R>: Sized {
19 fn with_default(self, default: A) -> Effect<A, ConfigError, R>;
21}
22
23impl<A, R> WithConfigDefault<A, R> for Effect<A, ConfigError, R>
24where
25 A: Clone + 'static,
26 R: 'static,
27{
28 fn with_default(self, default: A) -> Effect<A, ConfigError, R> {
29 self.catch(move |e| match e {
30 ConfigError::Missing { .. } => ::id_effect::succeed(default.clone()),
31 other => ::id_effect::fail(other),
32 })
33 }
34}
35
36#[inline]
40pub fn nested_path(namespace: &str, leaf: &[&str]) -> Vec<String> {
41 std::iter::once(namespace.to_string())
42 .chain(leaf.iter().map(|s| (*s).to_string()))
43 .collect()
44}
45
46pub fn read_string<A, E, R>(path: &[&str]) -> Effect<A, E, R>
50where
51 A: From<String> + 'static,
52 E: From<ConfigError> + 'static,
53 R: NeedsConfigProvider + 'static,
54{
55 let path_owned: Vec<String> = path.iter().map(|s| s.to_string()).collect();
56 effect!(|r: &mut R| {
57 let provider = Get::<ConfigProviderKey, Here>::get(r);
58 let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
59 let path_str = refs.join(".");
60 match provider.0.load_raw(&refs) {
61 Err(e) => return Err(E::from(e)),
62 Ok(None) => return Err(E::from(ConfigError::Missing { path: path_str })),
63 Ok(Some(s)) => A::from(s),
64 }
65 })
66}
67
68pub fn read_string_opt<A, E, R>(path: &[&str]) -> Effect<A, E, R>
70where
71 A: From<Option<String>> + 'static,
72 E: From<ConfigError> + 'static,
73 R: NeedsConfigProvider + 'static,
74{
75 let path_owned: Vec<String> = path.iter().map(|s| s.to_string()).collect();
76 effect!(|r: &mut R| {
77 let provider = Get::<ConfigProviderKey, Here>::get(r);
78 let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
79 match provider.0.load_raw(&refs) {
80 Err(e) => return Err(E::from(e)),
81 Ok(raw) => A::from(raw),
82 }
83 })
84}
85
86pub fn read_number<A, E, R>(path: &[&str]) -> Effect<A, E, R>
88where
89 A: From<f64> + 'static,
90 E: From<ConfigError> + 'static,
91 R: NeedsConfigProvider + 'static,
92{
93 let path_owned: Vec<String> = path.iter().map(|s| s.to_string()).collect();
94 effect!(|r: &mut R| {
95 let provider = Get::<ConfigProviderKey, Here>::get(r);
96 let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
97 let path_str = refs.join(".");
98 let s = match provider.0.load_raw(&refs) {
99 Err(e) => return Err(E::from(e)),
100 Ok(None) => {
101 return Err(E::from(ConfigError::Missing {
102 path: path_str.clone(),
103 }));
104 }
105 Ok(Some(s)) => s,
106 };
107 let n = s.parse::<f64>().map_err(|e| {
108 E::from(ConfigError::Invalid {
109 path: path_str,
110 value: s,
111 reason: e.to_string(),
112 })
113 })?;
114 A::from(n)
115 })
116}
117
118pub fn read_i64<A, E, R>(path: &[&str]) -> Effect<A, E, R>
120where
121 A: From<i64> + 'static,
122 E: From<ConfigError> + 'static,
123 R: NeedsConfigProvider + 'static,
124{
125 let path_owned: Vec<String> = path.iter().map(|s| s.to_string()).collect();
126 effect!(|r: &mut R| {
127 let provider = Get::<ConfigProviderKey, Here>::get(r);
128 let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
129 let path_str = refs.join(".");
130 let s = match provider.0.load_raw(&refs) {
131 Err(e) => return Err(E::from(e)),
132 Ok(None) => {
133 return Err(E::from(ConfigError::Missing {
134 path: path_str.clone(),
135 }));
136 }
137 Ok(Some(s)) => s,
138 };
139 let n = s.parse::<i64>().map_err(|e| {
140 E::from(ConfigError::Invalid {
141 path: path_str,
142 value: s,
143 reason: e.to_string(),
144 })
145 })?;
146 A::from(n)
147 })
148}
149
150pub fn read_bool<A, E, R>(path: &[&str]) -> Effect<A, E, R>
152where
153 A: From<bool> + 'static,
154 E: From<ConfigError> + 'static,
155 R: NeedsConfigProvider + 'static,
156{
157 let path_owned: Vec<String> = path.iter().map(|s| s.to_string()).collect();
158 effect!(|r: &mut R| {
159 let provider = Get::<ConfigProviderKey, Here>::get(r);
160 let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
161 let path_str = refs.join(".");
162 let s = match provider.0.load_raw(&refs) {
163 Err(e) => return Err(E::from(e)),
164 Ok(None) => {
165 return Err(E::from(ConfigError::Missing {
166 path: path_str.clone(),
167 }));
168 }
169 Ok(Some(s)) => s,
170 };
171 let b = match s.to_ascii_lowercase().as_str() {
172 "true" | "1" | "yes" => true,
173 "false" | "0" | "no" => false,
174 _ => {
175 return Err(E::from(ConfigError::Invalid {
176 path: path_str,
177 value: s,
178 reason: "expected boolean string".into(),
179 }));
180 }
181 };
182 A::from(b)
183 })
184}
185
186pub fn read_string_list<A, E, R>(path: &[&str]) -> Effect<A, E, R>
188where
189 A: From<Vec<String>> + 'static,
190 E: From<ConfigError> + 'static,
191 R: NeedsConfigProvider + 'static,
192{
193 let path_owned: Vec<String> = path.iter().map(|s| s.to_string()).collect();
194 effect!(|r: &mut R| {
195 let provider = Get::<ConfigProviderKey, Here>::get(r);
196 let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
197 let path_str = refs.join(".");
198 let s = match provider.0.load_raw(&refs) {
199 Err(e) => return Err(E::from(e)),
200 Ok(None) => return Err(E::from(ConfigError::Missing { path: path_str })),
201 Ok(Some(s)) => s,
202 };
203 let delim = provider.0.seq_delim();
204 let list: Vec<String> = s
205 .split(delim)
206 .map(str::trim)
207 .filter(|x| !x.is_empty())
208 .map(str::to_string)
209 .collect();
210 A::from(list)
211 })
212}
213
214pub fn read_nested_string<A, E, R>(namespace: &str, leaf: &[&str]) -> Effect<A, E, R>
218where
219 A: From<String> + 'static,
220 E: From<ConfigError> + 'static,
221 R: NeedsConfigProvider + 'static,
222{
223 let path_owned = nested_path(namespace, leaf);
224 effect!(|r: &mut R| {
225 let provider = Get::<ConfigProviderKey, Here>::get(r);
226 let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
227 let path_str = refs.join(".");
228 match provider.0.load_raw(&refs) {
229 Err(e) => return Err(E::from(e)),
230 Ok(None) => return Err(E::from(ConfigError::Missing { path: path_str })),
231 Ok(Some(s)) => A::from(s),
232 }
233 })
234}
235
236pub fn read_nested_string_list<A, E, R>(namespace: &str, leaf: &[&str]) -> Effect<A, E, R>
238where
239 A: From<Vec<String>> + 'static,
240 E: From<ConfigError> + 'static,
241 R: NeedsConfigProvider + 'static,
242{
243 let path_owned = nested_path(namespace, leaf);
244 effect!(|r: &mut R| {
245 let provider = Get::<ConfigProviderKey, Here>::get(r);
246 let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
247 let path_str = refs.join(".");
248 let s = match provider.0.load_raw(&refs) {
249 Err(e) => return Err(E::from(e)),
250 Ok(None) => return Err(E::from(ConfigError::Missing { path: path_str })),
251 Ok(Some(s)) => s,
252 };
253 let delim = provider.0.seq_delim();
254 let list: Vec<String> = s
255 .split(delim)
256 .map(str::trim)
257 .filter(|x| !x.is_empty())
258 .map(str::to_string)
259 .collect();
260 A::from(list)
261 })
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use crate::ConfigEnv;
268 use crate::MapConfigProvider;
269 use crate::ProviderOptions;
270 use crate::config_env;
271 use ::id_effect::run_blocking;
272
273 fn env_map(pairs: &[(&str, &str)]) -> ConfigEnv {
274 config_env(MapConfigProvider::from_pairs(pairs.iter().copied()))
275 }
276
277 #[test]
278 fn nested_path_builds_segments() {
279 assert_eq!(
280 nested_path("SERVER", &["HOST", "PORT"]),
281 vec!["SERVER", "HOST", "PORT"]
282 );
283 }
284
285 #[test]
286 fn read_string_ok_and_missing() {
287 let v: String = run_blocking(
288 read_string::<String, ConfigError, _>(&["K"]),
289 env_map(&[("K", "hello")]),
290 )
291 .unwrap();
292 assert_eq!(v, "hello");
293
294 let err = run_blocking(
295 read_string::<String, ConfigError, _>(&["MISSING"]),
296 env_map(&[]),
297 )
298 .unwrap_err();
299 assert!(matches!(err, ConfigError::Missing { .. }));
300 }
301
302 #[test]
303 fn read_string_opt_some_and_none() {
304 let v: Option<String> = run_blocking(
305 read_string_opt::<Option<String>, ConfigError, _>(&["K"]),
306 env_map(&[("K", "x")]),
307 )
308 .unwrap();
309 assert_eq!(v, Some("x".into()));
310
311 let none: Option<String> = run_blocking(
312 read_string_opt::<Option<String>, ConfigError, _>(&["MISSING"]),
313 env_map(&[]),
314 )
315 .unwrap();
316 assert_eq!(none, None);
317 }
318
319 #[test]
320 fn read_number_ok_missing_invalid() {
321 let n: f64 = run_blocking(
322 read_number::<f64, ConfigError, _>(&["N"]),
323 env_map(&[("N", "3.5")]),
324 )
325 .unwrap();
326 assert!((n - 3.5).abs() < f64::EPSILON);
327
328 let err = run_blocking(
329 read_number::<f64, ConfigError, _>(&["MISSING"]),
330 env_map(&[]),
331 )
332 .unwrap_err();
333 assert!(matches!(err, ConfigError::Missing { .. }));
334
335 let err = run_blocking(
336 read_number::<f64, ConfigError, _>(&["N"]),
337 env_map(&[("N", "not-a-number")]),
338 )
339 .unwrap_err();
340 assert!(matches!(err, ConfigError::Invalid { .. }));
341 }
342
343 #[test]
344 fn read_i64_ok_missing_invalid() {
345 let n: i64 = run_blocking(
346 read_i64::<i64, ConfigError, _>(&["N"]),
347 env_map(&[("N", "-42")]),
348 )
349 .unwrap();
350 assert_eq!(n, -42);
351
352 let err =
353 run_blocking(read_i64::<i64, ConfigError, _>(&["MISSING"]), env_map(&[])).unwrap_err();
354 assert!(matches!(err, ConfigError::Missing { .. }));
355
356 let err = run_blocking(
357 read_i64::<i64, ConfigError, _>(&["N"]),
358 env_map(&[("N", "1.2")]),
359 )
360 .unwrap_err();
361 assert!(matches!(err, ConfigError::Invalid { .. }));
362 }
363
364 #[test]
365 fn read_bool_variants_and_invalid() {
366 for (raw, expected) in [
367 ("true", true),
368 ("TRUE", true),
369 ("1", true),
370 ("yes", true),
371 ("false", false),
372 ("0", false),
373 ("no", false),
374 ] {
375 let b: bool = run_blocking(
376 read_bool::<bool, ConfigError, _>(&["B"]),
377 env_map(&[("B", raw)]),
378 )
379 .unwrap();
380 assert_eq!(b, expected, "raw={raw}");
381 }
382
383 let err = run_blocking(
384 read_bool::<bool, ConfigError, _>(&["MISSING"]),
385 env_map(&[]),
386 )
387 .unwrap_err();
388 assert!(matches!(err, ConfigError::Missing { .. }));
389
390 let err = run_blocking(
391 read_bool::<bool, ConfigError, _>(&["B"]),
392 env_map(&[("B", "maybe")]),
393 )
394 .unwrap_err();
395 assert!(matches!(err, ConfigError::Invalid { .. }));
396 }
397
398 #[test]
399 fn read_string_list_splits_on_seq_delim() {
400 let mut m = std::collections::HashMap::new();
401 m.insert("TAGS".into(), "a, b ,c".into());
402 let p = MapConfigProvider::with_options(
403 m,
404 ProviderOptions {
405 path_delim: "_",
406 seq_delim: ",",
407 },
408 );
409 let env = config_env(p);
410 let tags: Vec<String> = run_blocking(
411 read_string_list::<Vec<String>, ConfigError, _>(&["TAGS"]),
412 env,
413 )
414 .unwrap();
415 assert_eq!(tags, vec!["a", "b", "c"]);
416 }
417
418 #[test]
419 fn read_nested_string_and_list() {
420 let v: String = run_blocking(
421 read_nested_string::<String, ConfigError, _>("SERVER", &["HOST"]),
422 env_map(&[("SERVER_HOST", "z")]),
423 )
424 .unwrap();
425 assert_eq!(v, "z");
426
427 let list: Vec<String> = run_blocking(
428 read_nested_string_list::<Vec<String>, ConfigError, _>("APP", &["IDS"]),
429 env_map(&[("APP_IDS", "1,2")]),
430 )
431 .unwrap();
432 assert_eq!(list, vec!["1", "2"]);
433 }
434
435 #[test]
436 fn read_with_empty_path_is_invalid() {
437 let err = run_blocking(read_string::<String, ConfigError, _>(&[]), env_map(&[])).unwrap_err();
438 assert!(matches!(err, ConfigError::Invalid { .. }));
439 }
440
441 #[test]
442 fn with_default_replaces_only_missing() {
443 let def: String = run_blocking(
444 read_string::<String, ConfigError, _>(&["K"]).with_default("default".into()),
445 env_map(&[]),
446 )
447 .unwrap();
448 assert_eq!(def, "default");
449
450 let v: String = run_blocking(
451 read_string::<String, ConfigError, _>(&["K"]).with_default("default".into()),
452 env_map(&[("K", "real")]),
453 )
454 .unwrap();
455 assert_eq!(v, "real");
456
457 let err = run_blocking(
458 read_number::<f64, ConfigError, _>(&["N"]).with_default(0.0),
459 env_map(&[("N", "bad")]),
460 )
461 .unwrap_err();
462 assert!(matches!(err, ConfigError::Invalid { .. }));
463 }
464}