use std::fmt;
use crate::{
Diagnostic, DiagnosticInfo, DiagnosticSeverity, ErrorCategory, Source, render_diagnostic,
};
type BoxedCause = Box<dyn std::error::Error + Send + Sync + 'static>;
pub struct Error {
diagnostics: Vec<Diagnostic>,
cause: Option<BoxedCause>,
retained_source: Option<Source>,
}
impl Error {
#[must_use]
pub fn new(info: &'static DiagnosticInfo, message: impl Into<String>) -> Self {
debug_assert!(
info.category.is_some(),
"{} ends an operation but declares no error category",
info.code
);
let mut diagnostics =
vec![Diagnostic::of(info, message).with_severity(DiagnosticSeverity::Error)];
if info.category.is_none() {
diagnostics.push(
Diagnostic::of(
&crate::codes::REQUEST_DIAGNOSTIC_MISSING_CATEGORY,
format!("{} declares no error category", info.code),
)
.with_severity(DiagnosticSeverity::Note),
);
}
Self {
diagnostics,
cause: None,
retained_source: None,
}
}
#[must_use]
pub fn with_diagnostic(mut self, diagnostic: Diagnostic) -> Self {
self.diagnostics.push(diagnostic);
self
}
#[must_use]
pub fn with_diagnostics(mut self, diagnostics: impl IntoIterator<Item = Diagnostic>) -> Self {
self.diagnostics.extend(diagnostics);
self
}
#[must_use]
pub fn with_cause(mut self, cause: impl std::error::Error + Send + Sync + 'static) -> Self {
self.cause = Some(Box::new(cause));
self
}
#[must_use]
pub fn with_source(mut self, source: Source) -> Self {
self.retained_source = Some(source);
self
}
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
#[must_use]
pub fn info(&self) -> Option<&'static crate::DiagnosticInfo> {
self.diagnostics
.first()
.and_then(Diagnostic::registered_info)
}
#[must_use]
pub fn category(&self) -> ErrorCategory {
self.diagnostics
.iter()
.find(|diagnostic| diagnostic.severity() == DiagnosticSeverity::Error)
.and_then(Diagnostic::registered_info)
.and_then(|info| info.category)
.unwrap_or(ErrorCategory::Data)
}
#[must_use]
pub const fn retained_source(&self) -> Option<&Source> {
self.retained_source.as_ref()
}
#[must_use]
pub fn into_diagnostics(self) -> Vec<Diagnostic> {
self.diagnostics
}
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let diagnostic = self
.diagnostics
.iter()
.find(|diagnostic| diagnostic.severity() == DiagnosticSeverity::Error);
match diagnostic {
Some(diagnostic) => formatter.write_str(&render_diagnostic(diagnostic)),
None => formatter.write_str("PowerIO operation failed without a diagnostic"),
}
}
}
impl fmt::Debug for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Error")
.field("category", &self.category())
.field("diagnostics", &self.diagnostics)
.field("cause", &self.cause.as_ref().map(ToString::to_string))
.field("retained_source", &self.retained_source)
.finish()
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.cause
.as_deref()
.map(|cause| cause as &(dyn std::error::Error + 'static))
}
}
#[cfg(test)]
mod tests {
use std::error::Error as _;
use super::*;
#[test]
fn an_error_has_one_error_diagnostic_and_a_registered_category() {
let error = Error::new(
&crate::codes::VALIDATE_TIME_SERIES_SHAPE,
"two values for one point",
);
assert_eq!(error.category(), ErrorCategory::Data);
assert_eq!(error.diagnostics().len(), 1);
assert_eq!(error.diagnostics()[0].severity(), DiagnosticSeverity::Error);
assert!(
error
.to_string()
.starts_with("VALIDATE.TIME_SERIES.SHAPE: ")
);
}
#[test]
fn cause_and_shared_source_are_retained() {
let source = Source::from_bytes("input.bin", vec![0, 255]).unwrap();
let byte_pointer = source.primary_buffer().unwrap().bytes().as_ptr();
let error = Error::new(&crate::codes::READ_IO_READ, "read failed")
.with_cause(std::io::Error::other("cause"))
.with_source(source);
assert_eq!(error.source().unwrap().to_string(), "cause");
assert_eq!(
error
.retained_source()
.unwrap()
.primary_buffer()
.unwrap()
.bytes()
.as_ptr(),
byte_pointer
);
}
}