1#![warn(missing_docs)]
7
8use figment::{
9 Figment,
10 providers::{Env, Format, Toml, Yaml},
11};
12use serde::de::DeserializeOwned;
13use std::fmt;
14
15#[derive(Debug)]
17pub enum ConfigError {
18 LoadFailed(String),
20
21 ParseFailed(String),
23
24 ValidationFailed(String),
26
27 MissingConfig(String),
29}
30
31impl fmt::Display for ConfigError {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 ConfigError::LoadFailed(msg) => write!(f, "Failed to load config: {}", msg),
35 ConfigError::ParseFailed(msg) => write!(f, "Failed to parse config: {}", msg),
36 ConfigError::ValidationFailed(msg) => write!(f, "Config validation failed: {}", msg),
37 ConfigError::MissingConfig(msg) => write!(f, "Missing required config: {}", msg),
38 }
39 }
40}
41
42impl std::error::Error for ConfigError {}
43
44pub type ConfigResult<T> = Result<T, ConfigError>;
46
47pub struct ConfigLoader {
52 figment: Figment,
53}
54
55impl Default for ConfigLoader {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl ConfigLoader {
62 pub fn new() -> Self {
64 Self { figment: Figment::new() }
65 }
66
67 pub fn with_toml(mut self, path: &str) -> Self {
72 self.figment = self.figment.merge(Toml::file(path));
73 self
74 }
75
76 pub fn with_yaml(mut self, path: &str) -> Self {
81 self.figment = self.figment.merge(Yaml::file(path));
82 self
83 }
84
85 pub fn with_env(mut self, prefix: &str) -> Self {
90 self.figment = self.figment.merge(Env::prefixed(prefix));
91 self
92 }
93
94 pub fn with_env_separator(mut self, prefix: &str, separator: &str) -> Self {
100 self.figment = self.figment.merge(Env::prefixed(prefix).split(separator));
101 self
102 }
103
104 pub fn with_defaults<T: serde::Serialize>(mut self, defaults: &T) -> Self {
109 self.figment = self.figment.merge(figment::providers::Serialized::defaults(defaults));
110 self
111 }
112
113 pub fn extract<T: DeserializeOwned>(self) -> ConfigResult<T> {
118 self.figment.extract().map_err(|e| {
119 let msg = e.to_string();
120 if msg.contains("missing field") { ConfigError::MissingConfig(msg) } else { ConfigError::ParseFailed(msg) }
121 })
122 }
123
124 pub fn extract_with_context<T: DeserializeOwned>(self, context: &str) -> ConfigResult<T> {
130 self.extract().map_err(|e| ConfigError::LoadFailed(format!("{}: {}", context, e)))
131 }
132}
133
134pub fn load_config<T: DeserializeOwned>(config_path: &str, env_prefix: &str) -> ConfigResult<T> {
145 ConfigLoader::new().with_toml(config_path).with_env(env_prefix).extract()
146}
147
148pub fn from_env<T: DeserializeOwned>(prefix: &str) -> ConfigResult<T> {
156 ConfigLoader::new().with_env(prefix).extract()
157}