use std::fmt;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorKind {
Io,
Parse,
Missing,
Type,
Env,
Invalid,
Remote,
Decrypt,
Backend,
}
impl ErrorKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Io => "io",
Self::Parse => "parse",
Self::Missing => "missing",
Self::Type => "type",
Self::Env => "env",
Self::Invalid => "invalid",
Self::Remote => "remote",
Self::Decrypt => "decrypt",
Self::Backend => "backend",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Origin {
File(PathBuf),
Env(String),
Inline,
Remote(String),
Runtime(&'static str),
Unknown,
}
impl fmt::Display for Origin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::File(path) => write!(f, "in {}", path.display()),
Self::Env(name) => write!(f, "from {name}"),
Self::Inline => f.write_str("in an inline source"),
Self::Remote(store) => write!(f, "from {store}"),
Self::Runtime(layer) => write!(f, "set as {layer}"),
Self::Unknown => f.write_str("origin unknown"),
}
}
}
pub struct Error {
inner: Box<Inner>,
}
struct Inner {
kind: ErrorKind,
path: Vec<String>,
origin: Origin,
message: String,
}
impl Error {
pub(crate) fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
Self {
inner: Box::new(Inner {
kind,
path: Vec::new(),
origin: Origin::Unknown,
message: message.into(),
}),
}
}
pub fn invalid<E: fmt::Display>(error: E) -> Self {
Self::new(ErrorKind::Invalid, error.to_string())
}
pub fn ok_or_invalid<T, E: fmt::Display>(outcome: Result<T, E>) -> Result<(), Self> {
outcome.map(|_| ()).map_err(Self::invalid)
}
pub fn remote<E: fmt::Display>(error: E) -> Self {
Self::new(ErrorKind::Remote, error.to_string())
}
pub fn decrypt<E: fmt::Display>(error: E) -> Self {
Self::new(ErrorKind::Decrypt, error.to_string())
}
#[must_use]
pub fn unsupported(path: &std::path::Path) -> Self {
Self::new(
ErrorKind::Backend,
"the extension names no supported format; expected `.json`, `.toml`, \
`.yaml` or `.yml`",
)
.with_origin(Origin::File(path.to_owned()))
}
#[must_use]
pub fn kind(&self) -> ErrorKind {
self.inner.kind
}
#[must_use]
pub fn path(&self) -> String {
self.inner.path.join(".")
}
#[must_use]
pub fn origin(&self) -> &Origin {
&self.inner.origin
}
#[must_use]
pub fn message(&self) -> &str {
&self.inner.message
}
pub(crate) fn prepend_key(mut self, key: impl Into<String>) -> Self {
self.inner.path.insert(0, key.into());
self
}
pub(crate) fn with_origin(mut self, origin: Origin) -> Self {
if self.inner.origin == Origin::Unknown {
self.inner.origin = origin;
}
self
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.inner.path.is_empty() {
f.write_str(&self.inner.message)?;
} else {
write!(f, "{}: {}", self.path(), self.inner.message)?;
}
if self.inner.origin != Origin::Unknown {
write!(f, " ({})", self.inner.origin)?;
}
Ok(())
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Error")
.field("kind", &self.inner.kind)
.field("path", &self.path())
.field("origin", &self.inner.origin)
.field("message", &self.inner.message)
.finish()
}
}
impl std::error::Error for Error {}
impl serde::de::Error for Error {
fn custom<T: fmt::Display>(msg: T) -> Self {
Self::new(ErrorKind::Type, msg.to_string())
}
fn missing_field(field: &'static str) -> Self {
Self::new(ErrorKind::Missing, "missing value").prepend_key(field)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::de::Error as _;
#[test]
fn prepend_key_assembles_the_path_outermost_first() {
let error = Error::new(ErrorKind::Type, "boom")
.prepend_key("max")
.prepend_key("pool")
.prepend_key("db");
assert_eq!(error.path(), "db.pool.max");
}
#[test]
fn display_includes_the_path_and_the_origin() {
let error = Error::new(ErrorKind::Type, "invalid type")
.prepend_key("port")
.with_origin(Origin::Env("APP_DB_PORT".to_owned()));
assert_eq!(error.to_string(), "port: invalid type (from APP_DB_PORT)");
}
#[test]
fn display_omits_both_decorations_when_they_are_absent() {
let error = Error::new(ErrorKind::Parse, "unexpected end of input");
assert_eq!(error.to_string(), "unexpected end of input");
}
#[test]
fn the_first_origin_recorded_wins() {
let error = Error::new(ErrorKind::Type, "boom")
.with_origin(Origin::Inline)
.with_origin(Origin::Env("APP_X".to_owned()));
assert_eq!(error.origin(), &Origin::Inline);
}
#[test]
fn a_missing_field_carries_its_name_and_kind() {
let error = Error::missing_field("host");
assert_eq!(error.kind(), ErrorKind::Missing);
assert_eq!(error.path(), "host");
}
}