use figment::Error as FigmentError;
use std::{error::Error, fmt, sync::Arc};
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum OrthoError {
#[error("Failed to parse command-line arguments: {0}")]
CliParsing(#[from] Box<clap::Error>),
#[error("Configuration file error in '{path}': {source}")]
File {
path: std::path::PathBuf,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("cyclic extends detected: {cycle}")]
CyclicExtends { cycle: String },
#[error("Failed to gather configuration: {0}")]
Gathering(#[from] Box<FigmentError>),
#[error("Failed to merge CLI with configuration: {source}")]
Merge {
#[source]
source: Box<FigmentError>,
},
#[error("Validation failed for '{key}': {message}")]
Validation { key: String, message: String },
#[error("multiple configuration errors:\n{0}")]
Aggregate(Box<AggregatedErrors>),
}
#[derive(Debug, Default)]
pub struct AggregatedErrors(Vec<Arc<OrthoError>>);
impl AggregatedErrors {
#[must_use]
pub fn new(errors: Vec<Arc<OrthoError>>) -> Self {
Self(errors)
}
#[must_use = "iterators should be consumed to inspect errors"]
pub fn iter(&self) -> impl Iterator<Item = &OrthoError> {
self.0.iter().map(Arc::as_ref)
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
}
impl fmt::Display for AggregatedErrors {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, e) in self.0.iter().enumerate() {
if i > 0 {
writeln!(f)?;
}
write!(f, "{}: {e}", i + 1)?;
}
Ok(())
}
}
impl Error for AggregatedErrors {}
impl<'a> IntoIterator for &'a AggregatedErrors {
type Item = &'a OrthoError;
type IntoIter = std::iter::Map<
std::slice::Iter<'a, Arc<OrthoError>>,
fn(&'a Arc<OrthoError>) -> &'a OrthoError,
>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter().map(Arc::as_ref)
}
}
impl IntoIterator for AggregatedErrors {
type Item = Arc<OrthoError>;
type IntoIter = std::vec::IntoIter<Arc<OrthoError>>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl OrthoError {
#[must_use]
pub fn try_aggregate<I, E>(errors: I) -> Option<Self>
where
I: IntoIterator<Item = E>,
E: Into<Arc<OrthoError>>,
{
let mut arcs: Vec<Arc<OrthoError>> = errors.into_iter().map(Into::into).collect();
if arcs.is_empty() {
return None;
}
Some(if arcs.len() == 1 {
match Arc::try_unwrap(arcs.pop().unwrap()) {
Ok(err) => err,
Err(shared) => OrthoError::Aggregate(Box::new(AggregatedErrors::new(vec![shared]))),
}
} else {
OrthoError::Aggregate(Box::new(AggregatedErrors::new(arcs)))
})
}
#[must_use]
#[track_caller]
pub fn aggregate<I, E>(errors: I) -> Self
where
I: IntoIterator<Item = E>,
E: Into<Arc<OrthoError>>,
{
Self::try_aggregate(errors).expect("aggregate requires at least one error")
}
#[must_use]
pub fn merge(source: FigmentError) -> Self {
OrthoError::Merge {
source: Box::new(source),
}
}
#[must_use]
pub fn gathering(source: FigmentError) -> Self {
OrthoError::Gathering(Box::new(source))
}
#[must_use]
pub fn gathering_arc(source: FigmentError) -> Arc<Self> {
Arc::new(Self::gathering(source))
}
}
impl From<serde_json::Error> for OrthoError {
fn from(e: serde_json::Error) -> Self {
OrthoError::Gathering(Box::new(figment::Error::from(format!(
"JSON error: {} at line {}, column {}",
e,
e.line(),
e.column()
))))
}
}
impl From<clap::Error> for OrthoError {
fn from(e: clap::Error) -> Self {
OrthoError::CliParsing(e.into())
}
}
impl From<FigmentError> for OrthoError {
fn from(e: FigmentError) -> Self {
OrthoError::Gathering(e.into())
}
}
impl From<OrthoError> for FigmentError {
fn from(e: OrthoError) -> Self {
match e {
OrthoError::Merge { source: fe } | OrthoError::Gathering(fe) => *fe,
other => FigmentError::from(other.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::OrthoError;
use std::sync::Arc;
fn run_aggregate_tests<F>(name: &str, f: F)
where
F: Fn(Vec<Arc<OrthoError>>) -> OrthoError,
{
let err = Arc::new(OrthoError::Validation {
key: "k".into(),
message: "m".into(),
});
let res = f(vec![err]);
match res {
OrthoError::Validation { .. } => {}
other => panic!("{name}: expected Validation, got {other:?}"),
}
let shared = OrthoError::gathering_arc(figment::Error::from("boom"));
let res = f(vec![Arc::clone(&shared)]);
if let OrthoError::Aggregate(agg) = res {
assert_eq!(agg.len(), 1);
} else {
panic!("{name}: expected Aggregate");
}
let e1 = OrthoError::gathering_arc(figment::Error::from("one"));
let e2 = OrthoError::gathering_arc(figment::Error::from("two"));
let res = f(vec![e1, e2]);
if let OrthoError::Aggregate(agg) = res {
let agg = *agg;
let iter_items: Vec<_> = agg.iter().collect();
assert_eq!(iter_items.len(), 2);
let mut borrowed_items = Vec::new();
for e in &agg {
borrowed_items.push(e);
}
assert_eq!(borrowed_items.len(), 2);
let display = agg.to_string();
let owned_items: Vec<_> = agg.into_iter().collect();
assert_eq!(owned_items.len(), 2);
assert!(display.starts_with("1:"));
assert!(display.contains("\n2:"));
} else {
panic!("{name}: expected Aggregate");
}
}
#[test]
#[should_panic(expected = "aggregate requires at least one error")]
fn aggregate_panics_on_empty() {
let empty: Vec<Arc<OrthoError>> = vec![];
let _ = OrthoError::aggregate(empty);
}
#[test]
fn try_aggregate_none_on_empty() {
assert!(OrthoError::try_aggregate(Vec::<Arc<OrthoError>>::new()).is_none());
}
#[test]
fn both_aggregate_behaviours() {
run_aggregate_tests("try_aggregate", |v| OrthoError::try_aggregate(v).unwrap());
run_aggregate_tests("aggregate", OrthoError::aggregate);
}
}