use std::fmt;
use crate::{
Diagnostic, DiagnosticInfo, DiagnosticSeverity, ErrorCategory, Source, SourceSpan,
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 with_span(mut self, span: SourceSpan) -> Self {
let Some(position) = self
.diagnostics
.iter()
.position(|diagnostic| diagnostic.severity() == DiagnosticSeverity::Error)
else {
return self;
};
if let Err(refused) = self.diagnostics[position].add_span(span) {
self.diagnostics.extend(
refused
.into_diagnostics()
.into_iter()
.map(|diagnostic| diagnostic.with_severity(DiagnosticSeverity::Note)),
);
}
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 a_span_attaches_to_the_diagnostic_that_ended_the_operation() {
let source = crate::SourceId::new("/input").unwrap();
let error = Error::new(&crate::codes::READ_IO_READ, "read failed")
.with_diagnostic(
Diagnostic::of(&crate::codes::READ_IO_READ, "context")
.with_severity(DiagnosticSeverity::Note),
)
.with_span(SourceSpan::new(source.clone(), 4, 9).unwrap());
let spans = error.diagnostics()[0].spans();
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].source(), &source);
assert_eq!((spans[0].byte_start(), spans[0].byte_end()), (4, 9));
assert!(error.diagnostics()[1].spans().is_empty());
let mut crowded = Error::new(&crate::codes::READ_IO_READ, "read failed");
for _ in 0..crate::validation::MAX_DIAGNOSTIC_SPANS {
crowded = crowded.with_span(SourceSpan::new(source.clone(), 0, 1).unwrap());
}
let overflow = crowded.with_span(SourceSpan::new(source, 0, 1).unwrap());
assert_eq!(
overflow.diagnostics()[0].spans().len(),
crate::validation::MAX_DIAGNOSTIC_SPANS
);
assert_eq!(
overflow.diagnostics().last().unwrap().severity(),
DiagnosticSeverity::Note
);
}
#[test]
fn cause_and_shared_source_are_retained() {
let source = Source::from_memory("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
);
}
}