#[derive(Debug)]
#[non_exhaustive]
pub enum ConfigError {
Figment(
figment::Error,
),
Missing {
path: String,
},
Invalid {
path: String,
value: String,
reason: String,
},
InvalidUtf8 {
var: String,
},
}
impl From<figment::Error> for ConfigError {
fn from(value: figment::Error) -> Self {
Self::Figment(value)
}
}
impl From<core::convert::Infallible> for ConfigError {
fn from(e: core::convert::Infallible) -> Self {
match e {}
}
}
#[derive(Clone)]
enum ConfigErrorDisp {
Figment(String),
Missing {
path: String,
},
Invalid {
path: String,
value: String,
reason: String,
},
InvalidUtf8 {
var: String,
},
}
impl From<&ConfigError> for ConfigErrorDisp {
fn from(e: &ConfigError) -> Self {
match e {
ConfigError::Figment(err) => Self::Figment(format!("{err}")),
ConfigError::Missing { path } => Self::Missing { path: path.clone() },
ConfigError::Invalid {
path,
value,
reason,
} => Self::Invalid {
path: path.clone(),
value: value.clone(),
reason: reason.clone(),
},
ConfigError::InvalidUtf8 { var } => Self::InvalidUtf8 { var: var.clone() },
}
}
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use id_effect::Matcher;
let d = ConfigErrorDisp::from(self);
let s = Matcher::<ConfigErrorDisp, String>::new()
.when(
Box::new(|c: &ConfigErrorDisp| matches!(c, ConfigErrorDisp::Figment(_))),
|c| match c {
ConfigErrorDisp::Figment(msg) => msg,
_ => unreachable!(),
},
)
.when(
Box::new(|c: &ConfigErrorDisp| matches!(c, ConfigErrorDisp::Missing { .. })),
|c| match c {
ConfigErrorDisp::Missing { path } => format!("missing configuration at path {path:?}"),
_ => unreachable!(),
},
)
.when(
Box::new(|c: &ConfigErrorDisp| matches!(c, ConfigErrorDisp::Invalid { .. })),
|c| match c {
ConfigErrorDisp::Invalid {
path,
value,
reason,
} => format!("invalid configuration at path {path:?} (value={value:?}): {reason}"),
_ => unreachable!(),
},
)
.when(
Box::new(|c: &ConfigErrorDisp| matches!(c, ConfigErrorDisp::InvalidUtf8 { .. })),
|c| match c {
ConfigErrorDisp::InvalidUtf8 { var } => {
format!("environment variable {var:?} is not valid UTF-8")
}
_ => unreachable!(),
},
)
.run_exhaustive(d);
f.write_str(&s)
}
}
impl std::error::Error for ConfigError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConfigError::Figment(e) => Some(e),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
#[test]
fn config_error_matcher_all_variants_covered() {
use id_effect::Matcher;
let fig_display =
ConfigError::from(figment::Figment::new().extract::<i32>().unwrap_err()).to_string();
let fig_for_match = ConfigError::from(figment::Figment::new().extract::<i32>().unwrap_err());
let via_matcher = Matcher::<ConfigError, String>::new()
.when(
Box::new(|c: &ConfigError| matches!(c, ConfigError::Figment(_))),
|c: ConfigError| match c {
ConfigError::Figment(e) => format!("{e}"),
_ => unreachable!(),
},
)
.when(
Box::new(|c: &ConfigError| matches!(c, ConfigError::Missing { .. })),
|c: ConfigError| match c {
ConfigError::Missing { path } => format!("missing configuration at path {path:?}"),
_ => unreachable!(),
},
)
.when(
Box::new(|c: &ConfigError| matches!(c, ConfigError::Invalid { .. })),
|c: ConfigError| match c {
ConfigError::Invalid {
path,
value,
reason,
} => format!("invalid configuration at path {path:?} (value={value:?}): {reason}"),
_ => unreachable!(),
},
)
.when(
Box::new(|c: &ConfigError| matches!(c, ConfigError::InvalidUtf8 { .. })),
|c: ConfigError| match c {
ConfigError::InvalidUtf8 { var } => {
format!("environment variable {var:?} is not valid UTF-8")
}
_ => unreachable!(),
},
)
.or_else(|c| format!("configuration error: {c:?}"))
.run_exhaustive(fig_for_match);
assert!(!via_matcher.is_empty());
assert_eq!(via_matcher, fig_display);
let missing = ConfigError::Missing {
path: "db.url".into(),
};
assert_eq!(
missing.to_string(),
"missing configuration at path \"db.url\""
);
let invalid = ConfigError::Invalid {
path: "port".into(),
value: "x".into(),
reason: "not a number".into(),
};
assert_eq!(
invalid.to_string(),
"invalid configuration at path \"port\" (value=\"x\"): not a number"
);
let utf8 = ConfigError::InvalidUtf8 {
var: "WEIRD".into(),
};
assert_eq!(
utf8.to_string(),
"environment variable \"WEIRD\" is not valid UTF-8"
);
}
#[test]
fn config_error_source_figment_variant_has_source() {
let fig_err = figment::Figment::new().extract::<i32>().unwrap_err();
let e = ConfigError::Figment(fig_err);
assert!(e.source().is_some());
}
#[test]
fn config_error_source_non_figment_variants_return_none() {
let missing = ConfigError::Missing { path: "x".into() };
assert!(missing.source().is_none());
let invalid = ConfigError::Invalid {
path: "x".into(),
value: "v".into(),
reason: "r".into(),
};
assert!(invalid.source().is_none());
let utf8 = ConfigError::InvalidUtf8 { var: "VAR".into() };
assert!(utf8.source().is_none());
}
#[test]
fn config_error_disp_from_all_variants() {
let fig = ConfigError::Figment(figment::Figment::new().extract::<i32>().unwrap_err());
let missing = ConfigError::Missing { path: "p".into() };
let invalid = ConfigError::Invalid {
path: "p".into(),
value: "v".into(),
reason: "r".into(),
};
let utf8 = ConfigError::InvalidUtf8 { var: "V".into() };
for err in [fig, missing, invalid, utf8] {
let _disp = ConfigErrorDisp::from(&err);
let rendered = err.to_string();
assert!(!rendered.is_empty());
}
}
}