1#![forbid(unsafe_code)]
56#![deny(missing_docs)]
57
58mod ambient;
59mod config_desc;
60mod error;
61mod load;
62mod provider;
63mod secret;
64
65pub use ambient::{current_config_provider, with_config_provider};
66pub use config_desc::{Config, all, all_vec, all3, nest, or_else as or_else_config, zip_with};
67pub use error::ConfigError;
68pub use load::{
69 WithConfigDefault, nested_path, read_bool, read_i64, read_nested_string, read_nested_string_list,
70 read_number, read_string, read_string_list, read_string_opt,
71};
72pub use provider::{
73 ConfigProvider, ConfigProviderKey, ConfigProviderService, EnvConfigProvider,
74 FigmentConfigProvider, MapConfigProvider, NeedsConfigProvider, OrElseConfigProvider,
75 ProviderOptions, ScopedConfigProvider,
76};
77pub use secret::Secret;
78
79use std::sync::Arc;
82
83pub type ConfigEnv = ::id_effect::Context<
88 ::id_effect::Cons<
89 ::id_effect::Service<ConfigProviderKey, ConfigProviderService>,
90 ::id_effect::Nil,
91 >,
92>;
93
94pub fn config_env<P: ConfigProvider + 'static>(provider: P) -> ConfigEnv {
110 use ::id_effect::{Cons, Context, Nil, Service};
111 Context::new(Cons(
112 Service::<ConfigProviderKey, _>::new(ConfigProviderService(Arc::new(provider))),
113 Nil,
114 ))
115}
116
117pub mod config {
122 pub use crate::config_desc::{Config, all, all_vec, all3, nest, or_else, zip_with};
123 pub use crate::secret::Secret;
124 pub use crate::{current_config_provider, with_config_provider};
125
126 use crate::ConfigError;
127 use crate::ConfigProvider;
128
129 #[inline]
131 pub fn string(p: &impl ConfigProvider, name: &str) -> Result<String, ConfigError> {
132 Config::string(name).load(p)
133 }
134
135 #[inline]
137 pub fn optional_string(
138 p: &impl ConfigProvider,
139 name: &str,
140 ) -> Result<Option<String>, ConfigError> {
141 Config::optional_string(name).load(p)
142 }
143
144 #[inline]
146 pub fn number(p: &impl ConfigProvider, name: &str) -> Result<f64, ConfigError> {
147 Config::number(name).load(p)
148 }
149
150 #[inline]
152 pub fn integer(p: &impl ConfigProvider, name: &str) -> Result<i64, ConfigError> {
153 Config::integer(name).load(p)
154 }
155
156 #[inline]
158 pub fn boolean(p: &impl ConfigProvider, name: &str) -> Result<bool, ConfigError> {
159 Config::boolean(name).load(p)
160 }
161
162 #[inline]
164 pub fn nested_string(
165 p: &impl ConfigProvider,
166 namespace: &str,
167 name: &str,
168 ) -> Result<String, ConfigError> {
169 Config::string(name).nested(namespace).load(p)
170 }
171
172 #[inline]
174 pub fn nested_optional_string(
175 p: &impl ConfigProvider,
176 namespace: &str,
177 name: &str,
178 ) -> Result<Option<String>, ConfigError> {
179 Config::optional_string(name).nested(namespace).load(p)
180 }
181
182 #[inline]
184 pub fn nested_number(
185 p: &impl ConfigProvider,
186 namespace: &str,
187 name: &str,
188 ) -> Result<f64, ConfigError> {
189 Config::number(name).nested(namespace).load(p)
190 }
191
192 #[inline]
194 pub fn nested_integer(
195 p: &impl ConfigProvider,
196 namespace: &str,
197 name: &str,
198 ) -> Result<i64, ConfigError> {
199 Config::integer(name).nested(namespace).load(p)
200 }
201
202 #[inline]
204 pub fn nested_boolean(
205 p: &impl ConfigProvider,
206 namespace: &str,
207 name: &str,
208 ) -> Result<bool, ConfigError> {
209 Config::boolean(name).nested(namespace).load(p)
210 }
211
212 #[inline]
214 pub fn string_list(p: &impl ConfigProvider, name: &str) -> Result<Vec<String>, ConfigError> {
215 Config::string_list(name).load(p)
216 }
217
218 #[inline]
220 pub fn nested_string_list(
221 p: &impl ConfigProvider,
222 namespace: &str,
223 name: &str,
224 ) -> Result<Vec<String>, ConfigError> {
225 Config::string_list(name).nested(namespace).load(p)
226 }
227
228 #[inline]
230 pub fn with_default<T>(r: Result<T, ConfigError>, default: T) -> Result<T, ConfigError> {
231 match r {
232 Ok(v) => Ok(v),
233 Err(ConfigError::Missing { .. }) => Ok(default),
234 Err(e) => Err(e),
235 }
236 }
237}
238
239use std::marker::PhantomData;
240
241use ::figment::Figment;
242use id_effect::{Layer, Never};
243use serde::de::DeserializeOwned;
244
245#[inline]
247pub fn extract<T: DeserializeOwned>(figment: &Figment) -> Result<T, ConfigError> {
248 figment.extract().map_err(ConfigError::from)
249}
250
251#[inline]
253pub fn try_extract<T: DeserializeOwned>(figment: &Figment) -> Result<T, ConfigError> {
254 extract(figment)
255}
256
257#[derive(Debug)]
259pub struct FigmentLayer<T> {
260 figment: Arc<Figment>,
261 _marker: PhantomData<fn() -> T>,
262}
263
264impl<T> FigmentLayer<T> {
265 #[inline]
267 pub fn new(figment: Figment) -> Self {
268 Self {
269 figment: Arc::new(figment),
270 _marker: PhantomData,
271 }
272 }
273
274 #[inline]
276 pub fn from_shared(figment: Arc<Figment>) -> Self {
277 Self {
278 figment,
279 _marker: PhantomData,
280 }
281 }
282
283 #[inline]
285 pub fn figment(&self) -> &Figment {
286 self.figment.as_ref()
287 }
288}
289
290impl<T: DeserializeOwned + Send + Sync + 'static> Layer for FigmentLayer<T> {
291 type Output = T;
292 type Error = ConfigError;
293
294 fn build(&self) -> Result<Self::Output, Self::Error> {
295 extract(self.figment.as_ref())
296 }
297}
298
299#[derive(Clone, Debug)]
301pub struct FigmentProviderLayer {
302 figment: Arc<Figment>,
303}
304
305impl FigmentProviderLayer {
306 #[inline]
308 pub fn new(figment: Figment) -> Self {
309 Self {
310 figment: Arc::new(figment),
311 }
312 }
313
314 #[inline]
316 pub fn from_shared(figment: Arc<Figment>) -> Self {
317 Self { figment }
318 }
319
320 #[inline]
322 pub fn figment(&self) -> &Figment {
323 self.figment.as_ref()
324 }
325}
326
327impl Layer for FigmentProviderLayer {
328 type Output = FigmentConfigProvider;
329 type Error = Never;
330
331 fn build(&self) -> Result<Self::Output, Self::Error> {
332 Ok(FigmentConfigProvider::from_shared(self.figment.clone()))
333 }
334}
335
336#[derive(Clone, Debug)]
338pub struct EnvProviderLayer {
339 options: ProviderOptions,
340}
341
342impl EnvProviderLayer {
343 #[inline]
345 pub fn from_env() -> Self {
346 Self::new(ProviderOptions::default())
347 }
348
349 #[inline]
351 pub fn new(options: ProviderOptions) -> Self {
352 Self { options }
353 }
354
355 #[inline]
357 pub fn options(&self) -> &ProviderOptions {
358 &self.options
359 }
360}
361
362impl Layer for EnvProviderLayer {
363 type Output = EnvConfigProvider;
364 type Error = Never;
365
366 fn build(&self) -> Result<Self::Output, Self::Error> {
367 Ok(EnvConfigProvider::new(self.options.clone()))
368 }
369}
370
371pub mod figment {
373 #[cfg(any(feature = "env", feature = "toml", feature = "json", feature = "yaml"))]
374 use ::figment::Figment;
375 #[cfg(feature = "env")]
376 use ::figment::providers::Env;
377 #[cfg(feature = "toml")]
378 use ::figment::providers::{Format, Toml};
379 #[cfg(any(feature = "toml", feature = "json", feature = "yaml"))]
380 use std::path::Path;
381
382 #[must_use]
384 #[cfg(feature = "env")]
385 pub fn from_env_raw() -> Figment {
386 Figment::from(Env::raw())
387 }
388
389 #[must_use]
391 #[cfg(feature = "env")]
392 pub fn from_env_prefixed(prefix: impl AsRef<str>) -> Figment {
393 Figment::from(Env::prefixed(prefix.as_ref()))
394 }
395
396 #[must_use]
398 #[cfg(feature = "toml")]
399 pub fn merge_toml(figment: Figment, path: impl AsRef<Path>) -> Figment {
400 figment.merge(Toml::file(path))
401 }
402
403 #[must_use]
405 #[cfg(feature = "toml")]
406 pub fn from_toml_file(path: impl AsRef<Path>) -> Figment {
407 Figment::new().merge(Toml::file(path))
408 }
409
410 #[must_use]
412 #[cfg(feature = "json")]
413 pub fn merge_json(figment: Figment, path: impl AsRef<Path>) -> Figment {
414 use ::figment::providers::{Format, Json};
415 figment.merge(Json::file(path))
416 }
417
418 #[must_use]
420 #[cfg(feature = "yaml")]
421 pub fn merge_yaml(figment: Figment, path: impl AsRef<Path>) -> Figment {
422 use ::figment::providers::{Format, Yaml};
423 figment.merge(Yaml::file(path))
424 }
425
426 #[must_use]
428 #[cfg(all(feature = "toml", feature = "env"))]
429 pub fn layered_toml_env(
430 default_toml: impl AsRef<Path>,
431 local_toml: impl AsRef<Path>,
432 env_prefix: impl AsRef<str>,
433 ) -> Figment {
434 Figment::new()
435 .merge(Toml::file(default_toml))
436 .merge(Toml::file(local_toml))
437 .merge(Env::prefixed(env_prefix.as_ref()))
438 }
439}
440
441#[cfg(feature = "dotenv")]
443#[inline]
444pub fn load_dotenv() -> Result<(), dotenvy::Error> {
445 dotenvy::dotenv().map(|_| ())
446}
447
448#[cfg(feature = "dotenv")]
450#[inline]
451pub fn load_dotenv_optional() {
452 let _ = dotenvy::dotenv();
453}
454
455#[cfg(all(test, feature = "toml"))]
456mod tests {
457 use super::*;
458 use serde::Deserialize;
459 use std::io::Write;
460 use temp_env::with_var;
461
462 #[derive(Debug, Deserialize, PartialEq)]
463 struct Cfg {
464 n: u32,
465 s: String,
466 }
467
468 #[test]
469 fn extract_from_toml_file() {
470 let dir = tempfile::tempdir().expect("tempdir");
471 let path = dir.path().join("app.toml");
472 let mut f = std::fs::File::create(&path).expect("create");
473 writeln!(f, "n = 7\ns = \"hi\"").expect("write");
474 drop(f);
475
476 let fig = figment::from_toml_file(&path);
477 let cfg: Cfg = extract(&fig).expect("extract");
478 assert_eq!(
479 cfg,
480 Cfg {
481 n: 7,
482 s: "hi".into()
483 }
484 );
485 }
486
487 #[test]
488 fn figment_layer_build() {
489 let dir = tempfile::tempdir().expect("tempdir");
490 let path = dir.path().join("x.toml");
491 std::fs::write(&path, "n = 1\ns = \"a\"").expect("write");
492
493 let layer = FigmentLayer::<Cfg>::new(figment::from_toml_file(&path));
494 let cfg = Layer::build(&layer).expect("build");
495 assert_eq!(cfg.n, 1);
496 assert_eq!(cfg.s, "a");
497 }
498
499 #[test]
500 fn env_provider_nested_and_default() {
501 let key = "EFFECT_CONFIG_TEST_SERVER_HOST";
502 with_var(key, Some("localhost"), || {
503 let p = EnvConfigProvider::from_env();
504 let host = config::nested_string(&p, "EFFECT_CONFIG_TEST_SERVER", "HOST").expect("host");
505 assert_eq!(host, "localhost");
506 let port = config::with_default(
507 config::nested_integer(&p, "EFFECT_CONFIG_TEST_SERVER", "PORT"),
508 9,
509 )
510 .expect("port default");
511 assert_eq!(port, 9);
512 });
513 }
514
515 #[test]
516 fn map_provider_seq_delim() {
517 let mut m = std::collections::HashMap::new();
518 m.insert("TAGS".into(), "a,b, c".into());
519 let opts = ProviderOptions {
520 path_delim: "_",
521 seq_delim: ",",
522 };
523 let p = MapConfigProvider::with_options(m, opts);
524 let tags = config::string_list(&p, "TAGS").expect("tags");
525 assert_eq!(tags, vec!["a", "b", "c"]);
526 }
527
528 #[test]
529 fn figment_provider_scalar_from_toml() {
530 let dir = tempfile::tempdir().expect("tempdir");
531 let path = dir.path().join("c.toml");
532 std::fs::write(
533 &path,
534 r#"
535[server]
536host = "0.0.0.0"
537port = 3000
538"#,
539 )
540 .expect("write");
541 let fig = figment::from_toml_file(&path);
542 let p = FigmentConfigProvider::new(fig);
543 assert_eq!(
544 config::nested_string(&p, "server", "host").unwrap(),
545 "0.0.0.0"
546 );
547 assert_eq!(config::nested_integer(&p, "server", "port").unwrap(), 3000);
548 }
549
550 #[test]
551 fn map_from_pairs() {
552 let p = MapConfigProvider::from_pairs([("A_B", "x")]);
553 assert_eq!(config::nested_string(&p, "A", "B").unwrap(), "x");
554 }
555
556 #[test]
557 fn or_else_fallback() {
558 let a = MapConfigProvider::from_map(std::collections::HashMap::new());
559 let b = MapConfigProvider::from_pairs([("K", "from-b")]);
560 let p = OrElseConfigProvider::new(a, b);
561 assert_eq!(config::string(&p, "K").unwrap(), "from-b");
562 }
563
564 #[test]
565 fn figment_provider_layer_builds_provider() {
566 let dir = tempfile::tempdir().expect("tempdir");
567 let path = dir.path().join("d.toml");
568 std::fs::write(&path, "k = \"v\"").expect("write");
569 let layer = FigmentProviderLayer::new(figment::from_toml_file(&path));
570 let prov = Layer::build(&layer).expect("infallible");
571 assert_eq!(config::string(&prov, "k").unwrap(), "v");
572 }
573
574 #[test]
575 fn env_provider_layer() {
576 let key = "EFFECT_CONFIG_TEST_LAYER_X";
577 with_var(key, Some("42"), || {
578 let layer = EnvProviderLayer::from_env();
579 let p = Layer::build(&layer).expect("infallible");
580 assert_eq!(config::integer(&p, key).unwrap(), 42);
581 });
582 }
583
584 #[test]
585 fn scoped_provider_prefix_segments_and_nested_lookup() {
586 let p = MapConfigProvider::from_pairs([("A_B_C_D", "nested")]);
587 let scoped = ScopedConfigProvider::new(p, "A.B");
588 assert_eq!(scoped.prefix_segments(), &["A", "B"]);
589 assert_eq!(config::nested_string(&scoped, "C", "D").unwrap(), "nested");
590 assert_eq!(config::string(scoped.inner(), "A_B_C_D").unwrap(), "nested");
591 }
592
593 #[test]
594 fn figment_bool_float_and_non_scalar_error() {
595 let dir = tempfile::tempdir().expect("tempdir");
596 let path = dir.path().join("mix.toml");
597 std::fs::write(
598 &path,
599 r#"
600flag = true
601pi = 2.5
602bad = [1, 2]
603"#,
604 )
605 .expect("write");
606 let fig = figment::from_toml_file(&path);
607 let p = FigmentConfigProvider::new(fig);
608 assert!(config::boolean(&p, "flag").unwrap());
609 assert!((config::number(&p, "pi").unwrap() - 2.5).abs() < f64::EPSILON);
610 assert!(config::string(&p, "bad").is_err());
611 }
612
613 #[test]
616 fn config_env_runs_effect() {
617 use id_effect::run_blocking;
618
619 let p = MapConfigProvider::from_pairs([("K", "v")]);
620 let env = config_env(p);
621 let result: String =
622 run_blocking(Config::string("K").run::<String, ConfigError, _>(), env).unwrap();
623 assert_eq!(result, "v");
624 }
625
626 #[test]
629 fn config_optional_string_present_and_absent() {
630 let p = MapConfigProvider::from_pairs([("PRESENT", "yes")]);
631 assert_eq!(
632 config::optional_string(&p, "PRESENT").unwrap(),
633 Some("yes".to_string())
634 );
635 assert_eq!(config::optional_string(&p, "ABSENT").unwrap(), None);
636 }
637
638 #[test]
641 fn config_number_scalar() {
642 let p = MapConfigProvider::from_pairs([("PI", "3.14")]);
643 let v = config::number(&p, "PI").unwrap();
644 #[allow(clippy::approx_constant)]
645 let expected = 3.14_f64;
646 assert!((v - expected).abs() < f64::EPSILON);
647 }
648
649 #[test]
652 fn config_integer_scalar() {
653 let p = MapConfigProvider::from_pairs([("N", "99")]);
654 assert_eq!(config::integer(&p, "N").unwrap(), 99);
655 }
656
657 #[test]
660 fn config_boolean_scalar() {
661 let p = MapConfigProvider::from_pairs([("F", "true")]);
662 assert!(config::boolean(&p, "F").unwrap());
663 }
664
665 #[test]
668 fn config_nested_optional_string() {
669 let p = MapConfigProvider::from_pairs([("NS_KEY", "val")]);
670 assert_eq!(
671 config::nested_optional_string(&p, "NS", "KEY").unwrap(),
672 Some("val".to_string())
673 );
674 assert_eq!(
675 config::nested_optional_string(&p, "NS", "MISSING").unwrap(),
676 None
677 );
678 }
679
680 #[test]
683 fn config_nested_number() {
684 let p = MapConfigProvider::from_pairs([("SRV_RATE", "1.5")]);
685 let v = config::nested_number(&p, "SRV", "RATE").unwrap();
686 assert!((v - 1.5).abs() < f64::EPSILON);
687 }
688
689 #[test]
690 fn config_nested_integer() {
691 let p = MapConfigProvider::from_pairs([("SRV_PORT", "5432")]);
692 assert_eq!(config::nested_integer(&p, "SRV", "PORT").unwrap(), 5432);
693 }
694
695 #[test]
696 fn config_nested_boolean() {
697 let p = MapConfigProvider::from_pairs([("SRV_TLS", "true")]);
698 assert!(config::nested_boolean(&p, "SRV", "TLS").unwrap());
699 }
700
701 #[test]
704 fn config_nested_string_list() {
705 let p = MapConfigProvider::from_pairs([("NS_TAGS", "a,b,c")]);
706 let tags = config::nested_string_list(&p, "NS", "TAGS").unwrap();
707 assert_eq!(tags, vec!["a", "b", "c"]);
708 }
709
710 #[test]
713 fn config_string_list_free_fn() {
714 let p = MapConfigProvider::from_pairs([("HOSTS", "h1,h2")]);
715 let hosts = config::string_list(&p, "HOSTS").unwrap();
716 assert_eq!(hosts, vec!["h1", "h2"]);
717 }
718
719 #[test]
722 fn config_with_default_missing_uses_default() {
723 let p = MapConfigProvider::from_pairs::<[(&str, &str); 0], _, _>([]);
724 let v = config::with_default(config::string(&p, "MISSING"), "fallback".to_string()).unwrap();
725 assert_eq!(v, "fallback");
726 }
727
728 #[test]
729 fn config_with_default_present_ignores_default() {
730 let p = MapConfigProvider::from_pairs([("K", "real")]);
731 let v = config::with_default(config::string(&p, "K"), "fallback".to_string()).unwrap();
732 assert_eq!(v, "real");
733 }
734
735 #[test]
736 fn config_with_default_invalid_propagates() {
737 let p = MapConfigProvider::from_pairs([("N", "bad")]);
738 let err = config::with_default(config::integer(&p, "N"), 0_i64).unwrap_err();
739 assert!(matches!(err, ConfigError::Invalid { .. }));
740 }
741
742 #[test]
745 fn figment_layer_from_shared_and_figment_accessor() {
746 let dir = tempfile::tempdir().expect("tempdir");
747 let path = dir.path().join("shared.toml");
748 std::fs::write(&path, "n = 3\ns = \"hi\"").expect("write");
749 let shared = Arc::new(figment::from_toml_file(&path));
750 let layer = FigmentLayer::<Cfg>::from_shared(Arc::clone(&shared));
751 let _ = layer.figment();
753 let cfg = Layer::build(&layer).expect("build");
755 assert_eq!(cfg.n, 3);
756 }
757
758 #[test]
761 fn figment_provider_layer_from_shared_and_figment_accessor() {
762 let dir = tempfile::tempdir().expect("tempdir");
763 let path = dir.path().join("ps.toml");
764 std::fs::write(&path, "k = \"shared\"").expect("write");
765 let shared = Arc::new(figment::from_toml_file(&path));
766 let layer = FigmentProviderLayer::from_shared(Arc::clone(&shared));
767 let _ = layer.figment();
768 let prov = Layer::build(&layer).expect("infallible");
769 assert_eq!(config::string(&prov, "k").unwrap(), "shared");
770 }
771
772 #[test]
775 fn env_provider_layer_new_and_options_accessor() {
776 let opts = ProviderOptions {
777 path_delim: ".",
778 seq_delim: ";",
779 };
780 let layer = EnvProviderLayer::new(opts.clone());
781 assert_eq!(layer.options().seq_delim, ";");
782 let p = Layer::build(&layer).expect("infallible");
783 assert_eq!(p.seq_delim(), ";");
784 }
785}