1use std::collections::HashMap;
7use std::fmt;
8use std::future::ready;
9use std::sync::Arc;
10
11use ::figment::Figment;
12use ::figment::value::{Num, Value};
13
14use ::id_effect::{BoxFuture, Get, Here, IntoBind};
15
16use crate::error::ConfigError;
17
18::id_effect::service_key!(
21 pub struct ConfigProviderKey
23);
24
25#[derive(Clone)]
30pub struct ConfigProviderService(pub Arc<dyn ConfigProvider>);
31
32impl fmt::Debug for ConfigProviderService {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 f.debug_tuple("ConfigProviderService")
35 .field(&"<dyn ConfigProvider>")
36 .finish()
37 }
38}
39
40impl<'a, R> IntoBind<'a, R, ConfigProviderService, ConfigError> for ConfigProviderService
41where
42 R: Get<ConfigProviderKey, Here, Target = ConfigProviderService> + 'a,
43{
44 fn into_bind(self, r: &'a mut R) -> BoxFuture<'a, Result<ConfigProviderService, ConfigError>> {
45 Box::pin(ready(Ok(Get::<ConfigProviderKey, Here>::get(r).clone())))
46 }
47}
48
49pub trait NeedsConfigProvider:
51 Get<ConfigProviderKey, Here, Target = ConfigProviderService>
52{
53}
54impl<R: Get<ConfigProviderKey, Here, Target = ConfigProviderService>> NeedsConfigProvider for R {}
55
56#[derive(Clone, Debug)]
58pub struct ProviderOptions {
59 pub path_delim: &'static str,
61 pub seq_delim: &'static str,
63}
64
65impl Default for ProviderOptions {
66 fn default() -> Self {
67 Self {
68 path_delim: "_",
69 seq_delim: ",",
70 }
71 }
72}
73
74pub trait ConfigProvider: Send + Sync {
76 fn load_raw(&self, path: &[&str]) -> Result<Option<String>, ConfigError>;
78
79 fn seq_delim(&self) -> &'static str {
81 ","
82 }
83
84 fn within(self, prefix: impl Into<String>) -> ScopedConfigProvider<Self>
96 where
97 Self: Sized,
98 {
99 ScopedConfigProvider::new(self, prefix)
100 }
101
102 fn or_else<B: ConfigProvider + Clone>(self, fallback: B) -> OrElseConfigProvider<Self, B>
107 where
108 Self: Sized + Clone,
109 {
110 OrElseConfigProvider::new(self, fallback)
111 }
112}
113
114fn join_path(path: &[&str], delim: &str) -> Result<String, ConfigError> {
115 if path.is_empty() {
116 return Err(ConfigError::Invalid {
117 path: String::new(),
118 value: String::new(),
119 reason: "empty configuration path".into(),
120 });
121 }
122 Ok(path.join(delim))
123}
124
125#[derive(Clone, Debug)]
127pub struct EnvConfigProvider {
128 options: ProviderOptions,
129}
130
131impl EnvConfigProvider {
132 #[inline]
134 pub fn from_env() -> Self {
135 Self::new(ProviderOptions::default())
136 }
137
138 #[inline]
140 pub fn new(options: ProviderOptions) -> Self {
141 Self { options }
142 }
143}
144
145impl ConfigProvider for EnvConfigProvider {
146 fn load_raw(&self, path: &[&str]) -> Result<Option<String>, ConfigError> {
147 let key = join_path(path, self.options.path_delim)?;
148 match std::env::var(&key) {
149 Ok(s) => Ok(Some(s)),
150 Err(std::env::VarError::NotPresent) => Ok(None),
151 Err(std::env::VarError::NotUnicode(_)) => Err(ConfigError::InvalidUtf8 { var: key }),
152 }
153 }
154
155 fn seq_delim(&self) -> &'static str {
156 self.options.seq_delim
157 }
158}
159
160#[derive(Clone, Debug)]
162pub struct MapConfigProvider {
163 map: HashMap<String, String>,
164 options: ProviderOptions,
165}
166
167impl MapConfigProvider {
168 #[inline]
170 pub fn from_map(map: HashMap<String, String>) -> Self {
171 Self::with_options(map, ProviderOptions::default())
172 }
173
174 #[inline]
176 pub fn with_options(map: HashMap<String, String>, options: ProviderOptions) -> Self {
177 Self { map, options }
178 }
179
180 #[inline]
182 pub fn from_pairs<I, K, V>(pairs: I) -> Self
183 where
184 I: IntoIterator<Item = (K, V)>,
185 K: Into<String>,
186 V: Into<String>,
187 {
188 Self::from_pairs_with_options(pairs, ProviderOptions::default())
189 }
190
191 #[inline]
193 pub fn from_pairs_with_options<I, K, V>(pairs: I, options: ProviderOptions) -> Self
194 where
195 I: IntoIterator<Item = (K, V)>,
196 K: Into<String>,
197 V: Into<String>,
198 {
199 Self {
200 map: pairs
201 .into_iter()
202 .map(|(k, v)| (k.into(), v.into()))
203 .collect(),
204 options,
205 }
206 }
207}
208
209impl ConfigProvider for MapConfigProvider {
210 fn load_raw(&self, path: &[&str]) -> Result<Option<String>, ConfigError> {
211 let key = join_path(path, self.options.path_delim)?;
212 Ok(self.map.get(&key).cloned())
213 }
214
215 fn seq_delim(&self) -> &'static str {
216 self.options.seq_delim
217 }
218}
219
220#[derive(Clone, Debug)]
222pub struct FigmentConfigProvider {
223 figment: Arc<Figment>,
224}
225
226impl FigmentConfigProvider {
227 #[inline]
229 pub fn new(figment: Figment) -> Self {
230 Self {
231 figment: Arc::new(figment),
232 }
233 }
234
235 #[inline]
237 pub fn from_shared(figment: Arc<Figment>) -> Self {
238 Self { figment }
239 }
240
241 #[inline]
243 pub fn figment(&self) -> &Figment {
244 self.figment.as_ref()
245 }
246}
247
248fn num_to_string(n: Num) -> String {
249 match n {
250 Num::U8(v) => v.to_string(),
251 Num::U16(v) => v.to_string(),
252 Num::U32(v) => v.to_string(),
253 Num::U64(v) => v.to_string(),
254 Num::U128(v) => v.to_string(),
255 Num::USize(v) => v.to_string(),
256 Num::I8(v) => v.to_string(),
257 Num::I16(v) => v.to_string(),
258 Num::I32(v) => v.to_string(),
259 Num::I64(v) => v.to_string(),
260 Num::I128(v) => v.to_string(),
261 Num::ISize(v) => v.to_string(),
262 Num::F32(v) => v.to_string(),
263 Num::F64(v) => v.to_string(),
264 }
265}
266
267fn figment_value_as_raw_string(v: &Value) -> Result<String, ConfigError> {
268 match v {
269 Value::String(_, s) => Ok(s.clone()),
270 Value::Char(_, c) => Ok(c.to_string()),
271 Value::Bool(_, b) => Ok(b.to_string()),
272 Value::Num(_, n) => Ok(num_to_string(*n)),
273 _ => Err(ConfigError::Invalid {
274 path: String::new(),
275 value: format!("{v:?}"),
276 reason: "expected a scalar string, bool, or number".into(),
277 }),
278 }
279}
280
281impl ConfigProvider for FigmentConfigProvider {
282 fn load_raw(&self, path: &[&str]) -> Result<Option<String>, ConfigError> {
283 let key_path = join_path(path, ".")?;
284 let fig = self.figment.as_ref();
285 if !fig.contains(&key_path) {
286 return Ok(None);
287 }
288 let v = fig.find_value(&key_path).map_err(ConfigError::from)?;
289 figment_value_as_raw_string(&v).map(Some)
290 }
291}
292
293#[derive(Clone, Debug)]
295pub struct OrElseConfigProvider<A, B> {
296 primary: A,
297 fallback: B,
298}
299
300impl<A, B> OrElseConfigProvider<A, B> {
301 #[inline]
303 pub fn new(primary: A, fallback: B) -> Self {
304 Self { primary, fallback }
305 }
306}
307
308impl<A: ConfigProvider + Clone + 'static, B: ConfigProvider + Clone + 'static> ConfigProvider
309 for OrElseConfigProvider<A, B>
310{
311 fn load_raw(&self, path: &[&str]) -> Result<Option<String>, ConfigError> {
312 let path_refs_primary: Vec<&str> = path.to_vec();
313 match self.primary.load_raw(&path_refs_primary)? {
314 None => self.fallback.load_raw(path),
315 some => Ok(some),
316 }
317 }
318
319 fn seq_delim(&self) -> &'static str {
321 self.primary.seq_delim()
322 }
323}
324
325#[derive(Clone, Debug)]
340pub struct ScopedConfigProvider<P> {
341 inner: P,
342 prefix: Vec<String>,
343}
344
345impl<P> ScopedConfigProvider<P> {
346 pub fn new(inner: P, prefix: impl Into<String>) -> Self {
348 Self {
349 inner,
350 prefix: prefix
351 .into()
352 .split('.')
353 .filter(|s| !s.is_empty())
354 .map(String::from)
355 .collect(),
356 }
357 }
358
359 #[inline]
361 pub fn inner(&self) -> &P {
362 &self.inner
363 }
364
365 #[inline]
367 pub fn prefix_segments(&self) -> &[String] {
368 &self.prefix
369 }
370}
371
372impl<P: ConfigProvider> ConfigProvider for ScopedConfigProvider<P> {
373 fn load_raw(&self, path: &[&str]) -> Result<Option<String>, ConfigError> {
374 let mut full: Vec<String> = self.prefix.clone();
375 full.extend(path.iter().map(|s| (*s).to_string()));
376 let refs: Vec<&str> = full.iter().map(String::as_str).collect();
377 self.inner.load_raw(&refs)
378 }
379
380 fn seq_delim(&self) -> &'static str {
381 self.inner.seq_delim()
382 }
383}
384
385#[cfg(test)]
388mod tests {
389 use std::collections::HashMap;
390 use std::sync::Arc;
391
392 use super::*;
393
394 fn pairs(entries: &[(&str, &str)]) -> MapConfigProvider {
395 MapConfigProvider::from_pairs(entries.iter().copied())
396 }
397
398 #[test]
401 fn config_provider_service_debug_format() {
402 let p = pairs(&[("K", "v")]);
403 let svc = ConfigProviderService(Arc::new(p));
404 let s = format!("{svc:?}");
405 assert!(s.contains("ConfigProviderService"));
406 }
407
408 #[test]
411 fn default_seq_delim_is_comma() {
412 struct MinimalProvider;
413 impl ConfigProvider for MinimalProvider {
414 fn load_raw(&self, _path: &[&str]) -> Result<Option<String>, ConfigError> {
415 Ok(None)
416 }
417 }
419 assert_eq!(MinimalProvider.seq_delim(), ",");
420 }
421
422 #[test]
425 fn within_scopes_lookup() {
426 let p = pairs(&[("SERVER_HOST", "localhost")]);
427 let scoped = p.within("SERVER");
428 assert_eq!(
429 scoped.load_raw(&["HOST"]).unwrap(),
430 Some("localhost".to_string())
431 );
432 }
433
434 #[test]
437 fn or_else_trait_method_falls_back() {
438 let a = MapConfigProvider::from_map(HashMap::new());
439 let b = pairs(&[("K", "from-b")]);
440 let composed = a.or_else(b);
441 assert_eq!(
442 composed.load_raw(&["K"]).unwrap(),
443 Some("from-b".to_string())
444 );
445 }
446
447 #[test]
450 fn env_config_provider_from_env_has_comma_seq_delim() {
451 let p = EnvConfigProvider::from_env();
452 assert_eq!(p.seq_delim(), ",");
453 }
454
455 #[test]
458 fn env_config_provider_new_custom_options() {
459 let opts = ProviderOptions {
460 path_delim: ".",
461 seq_delim: ";",
462 };
463 let p = EnvConfigProvider::new(opts);
464 assert_eq!(p.seq_delim(), ";");
465 }
466
467 #[test]
470 fn provider_options_default_values() {
471 let opts = ProviderOptions::default();
472 assert_eq!(opts.path_delim, "_");
473 assert_eq!(opts.seq_delim, ",");
474 }
475
476 #[test]
479 fn figment_config_provider_figment_accessor() {
480 let fig = Figment::new();
481 let p = FigmentConfigProvider::new(fig);
482 let _ = p.figment();
484 }
485
486 #[test]
489 fn or_else_provider_primary_found_returns_primary() {
490 let a = pairs(&[("K", "from-a")]);
491 let b = pairs(&[("K", "from-b")]);
492 let composed = OrElseConfigProvider::new(a, b);
493 assert_eq!(
494 composed.load_raw(&["K"]).unwrap(),
495 Some("from-a".to_string())
496 );
497 }
498
499 #[test]
502 fn or_else_provider_seq_delim_uses_primary() {
503 let a = MapConfigProvider::with_options(
504 HashMap::new(),
505 ProviderOptions {
506 path_delim: "_",
507 seq_delim: ";",
508 },
509 );
510 let b = pairs(&[("K", "v")]);
511 let composed = OrElseConfigProvider::new(a, b);
512 assert_eq!(composed.seq_delim(), ";");
513 }
514
515 #[test]
518 fn scoped_provider_seq_delim_delegates_to_inner() {
519 let p = MapConfigProvider::with_options(
520 HashMap::new(),
521 ProviderOptions {
522 path_delim: "_",
523 seq_delim: "|",
524 },
525 );
526 let scoped = ScopedConfigProvider::new(p, "NS");
527 assert_eq!(scoped.seq_delim(), "|");
528 }
529
530 #[test]
533 fn map_provider_empty_path_returns_error() {
534 let p = MapConfigProvider::from_map(HashMap::new());
535 let err = p.load_raw(&[]).unwrap_err();
536 assert!(matches!(err, ConfigError::Invalid { .. }));
537 }
538
539 #[cfg(feature = "toml")]
542 mod figment_num_variants {
543 use super::*;
544 use ::figment::providers::Serialized;
545 use serde::Serialize;
546
547 fn provider_from<T: Serialize>(value: T) -> FigmentConfigProvider {
548 FigmentConfigProvider::new(Figment::from(Serialized::defaults(value)))
549 }
550
551 #[derive(Serialize)]
552 struct U8Val {
553 val: u8,
554 }
555 #[derive(Serialize)]
556 struct U16Val {
557 val: u16,
558 }
559 #[derive(Serialize)]
560 struct U32Val {
561 val: u32,
562 }
563 #[derive(Serialize)]
564 struct U64Val {
565 val: u64,
566 }
567 #[derive(Serialize)]
568 struct I8Val {
569 val: i8,
570 }
571 #[derive(Serialize)]
572 struct I16Val {
573 val: i16,
574 }
575 #[derive(Serialize)]
576 struct I32Val {
577 val: i32,
578 }
579 #[derive(Serialize)]
580 struct F32Val {
581 val: f32,
582 }
583 #[derive(Serialize)]
584 struct CharVal {
585 val: char,
586 }
587
588 #[test]
589 fn num_u8() {
590 let p = provider_from(U8Val { val: 200 });
591 assert_eq!(p.load_raw(&["val"]).unwrap(), Some("200".to_string()));
592 }
593
594 #[test]
595 fn num_u16() {
596 let p = provider_from(U16Val { val: 1000 });
597 assert_eq!(p.load_raw(&["val"]).unwrap(), Some("1000".to_string()));
598 }
599
600 #[test]
601 fn num_u32() {
602 let p = provider_from(U32Val { val: 70000 });
603 assert_eq!(p.load_raw(&["val"]).unwrap(), Some("70000".to_string()));
604 }
605
606 #[test]
607 fn num_u64() {
608 let p = provider_from(U64Val { val: 1_000_000 });
609 assert_eq!(p.load_raw(&["val"]).unwrap(), Some("1000000".to_string()));
610 }
611
612 #[test]
613 fn num_i8() {
614 let p = provider_from(I8Val { val: -5 });
615 assert_eq!(p.load_raw(&["val"]).unwrap(), Some("-5".to_string()));
616 }
617
618 #[test]
619 fn num_i16() {
620 let p = provider_from(I16Val { val: -300 });
621 assert_eq!(p.load_raw(&["val"]).unwrap(), Some("-300".to_string()));
622 }
623
624 #[test]
625 fn num_i32() {
626 let p = provider_from(I32Val { val: 100_000 });
627 assert_eq!(p.load_raw(&["val"]).unwrap(), Some("100000".to_string()));
628 }
629
630 #[test]
631 fn num_f32() {
632 let p = provider_from(F32Val { val: 1.5 });
633 let s = p.load_raw(&["val"]).unwrap().unwrap();
634 let v: f64 = s.parse().unwrap();
635 assert!((v - 1.5).abs() < 0.01);
636 }
637
638 #[test]
639 fn char_variant() {
640 let p = provider_from(CharVal { val: 'x' });
641 assert_eq!(p.load_raw(&["val"]).unwrap(), Some("x".to_string()));
642 }
643 }
644}