1#[derive(Debug)]
5#[non_exhaustive]
6pub enum ConfigError {
7 Figment(
9 figment::Error,
11 ),
12 Missing {
14 path: String,
16 },
17 Invalid {
19 path: String,
21 value: String,
23 reason: String,
25 },
26 InvalidUtf8 {
28 var: String,
30 },
31}
32
33impl From<figment::Error> for ConfigError {
34 fn from(value: figment::Error) -> Self {
35 Self::Figment(value)
36 }
37}
38
39impl From<core::convert::Infallible> for ConfigError {
40 fn from(e: core::convert::Infallible) -> Self {
41 match e {}
42 }
43}
44
45#[derive(Clone)]
48enum ConfigErrorDisp {
49 Figment(String),
50 Missing {
51 path: String,
52 },
53 Invalid {
54 path: String,
55 value: String,
56 reason: String,
57 },
58 InvalidUtf8 {
59 var: String,
60 },
61}
62
63impl From<&ConfigError> for ConfigErrorDisp {
64 fn from(e: &ConfigError) -> Self {
65 match e {
66 ConfigError::Figment(err) => Self::Figment(format!("{err}")),
67 ConfigError::Missing { path } => Self::Missing { path: path.clone() },
68 ConfigError::Invalid {
69 path,
70 value,
71 reason,
72 } => Self::Invalid {
73 path: path.clone(),
74 value: value.clone(),
75 reason: reason.clone(),
76 },
77 ConfigError::InvalidUtf8 { var } => Self::InvalidUtf8 { var: var.clone() },
78 }
79 }
80}
81
82impl std::fmt::Display for ConfigError {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 use id_effect::Matcher;
85
86 let d = ConfigErrorDisp::from(self);
87 let s = Matcher::<ConfigErrorDisp, String>::new()
88 .when(
89 Box::new(|c: &ConfigErrorDisp| matches!(c, ConfigErrorDisp::Figment(_))),
90 |c| match c {
91 ConfigErrorDisp::Figment(msg) => msg,
92 _ => unreachable!(),
93 },
94 )
95 .when(
96 Box::new(|c: &ConfigErrorDisp| matches!(c, ConfigErrorDisp::Missing { .. })),
97 |c| match c {
98 ConfigErrorDisp::Missing { path } => format!("missing configuration at path {path:?}"),
99 _ => unreachable!(),
100 },
101 )
102 .when(
103 Box::new(|c: &ConfigErrorDisp| matches!(c, ConfigErrorDisp::Invalid { .. })),
104 |c| match c {
105 ConfigErrorDisp::Invalid {
106 path,
107 value,
108 reason,
109 } => format!("invalid configuration at path {path:?} (value={value:?}): {reason}"),
110 _ => unreachable!(),
111 },
112 )
113 .when(
114 Box::new(|c: &ConfigErrorDisp| matches!(c, ConfigErrorDisp::InvalidUtf8 { .. })),
115 |c| match c {
116 ConfigErrorDisp::InvalidUtf8 { var } => {
117 format!("environment variable {var:?} is not valid UTF-8")
118 }
119 _ => unreachable!(),
120 },
121 )
122 .run_exhaustive(d);
123 f.write_str(&s)
124 }
125}
126
127impl std::error::Error for ConfigError {
128 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
129 match self {
130 ConfigError::Figment(e) => Some(e),
131 _ => None,
132 }
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use std::error::Error;
140
141 #[test]
142 fn config_error_matcher_all_variants_covered() {
143 use id_effect::Matcher;
144
145 let fig_display =
146 ConfigError::from(figment::Figment::new().extract::<i32>().unwrap_err()).to_string();
147 let fig_for_match = ConfigError::from(figment::Figment::new().extract::<i32>().unwrap_err());
148 let via_matcher = Matcher::<ConfigError, String>::new()
149 .when(
150 Box::new(|c: &ConfigError| matches!(c, ConfigError::Figment(_))),
151 |c: ConfigError| match c {
152 ConfigError::Figment(e) => format!("{e}"),
153 _ => unreachable!(),
154 },
155 )
156 .when(
157 Box::new(|c: &ConfigError| matches!(c, ConfigError::Missing { .. })),
158 |c: ConfigError| match c {
159 ConfigError::Missing { path } => format!("missing configuration at path {path:?}"),
160 _ => unreachable!(),
161 },
162 )
163 .when(
164 Box::new(|c: &ConfigError| matches!(c, ConfigError::Invalid { .. })),
165 |c: ConfigError| match c {
166 ConfigError::Invalid {
167 path,
168 value,
169 reason,
170 } => format!("invalid configuration at path {path:?} (value={value:?}): {reason}"),
171 _ => unreachable!(),
172 },
173 )
174 .when(
175 Box::new(|c: &ConfigError| matches!(c, ConfigError::InvalidUtf8 { .. })),
176 |c: ConfigError| match c {
177 ConfigError::InvalidUtf8 { var } => {
178 format!("environment variable {var:?} is not valid UTF-8")
179 }
180 _ => unreachable!(),
181 },
182 )
183 .or_else(|c| format!("configuration error: {c:?}"))
184 .run_exhaustive(fig_for_match);
185 assert!(!via_matcher.is_empty());
186 assert_eq!(via_matcher, fig_display);
187
188 let missing = ConfigError::Missing {
189 path: "db.url".into(),
190 };
191 assert_eq!(
192 missing.to_string(),
193 "missing configuration at path \"db.url\""
194 );
195
196 let invalid = ConfigError::Invalid {
197 path: "port".into(),
198 value: "x".into(),
199 reason: "not a number".into(),
200 };
201 assert_eq!(
202 invalid.to_string(),
203 "invalid configuration at path \"port\" (value=\"x\"): not a number"
204 );
205
206 let utf8 = ConfigError::InvalidUtf8 {
207 var: "WEIRD".into(),
208 };
209 assert_eq!(
210 utf8.to_string(),
211 "environment variable \"WEIRD\" is not valid UTF-8"
212 );
213 }
214
215 #[test]
216 fn config_error_source_figment_variant_has_source() {
217 let fig_err = figment::Figment::new().extract::<i32>().unwrap_err();
218 let e = ConfigError::Figment(fig_err);
219 assert!(e.source().is_some());
220 }
221
222 #[test]
223 fn config_error_source_non_figment_variants_return_none() {
224 let missing = ConfigError::Missing { path: "x".into() };
225 assert!(missing.source().is_none());
226
227 let invalid = ConfigError::Invalid {
228 path: "x".into(),
229 value: "v".into(),
230 reason: "r".into(),
231 };
232 assert!(invalid.source().is_none());
233
234 let utf8 = ConfigError::InvalidUtf8 { var: "VAR".into() };
235 assert!(utf8.source().is_none());
236 }
237}