powerio-core 0.10.0

Shared source, diagnostic, module, collection, and output types for PowerIO.
Documentation
use std::fmt;

use crate::{
    Diagnostic, DiagnosticInfo, DiagnosticSeverity, ErrorCategory, Source, render_diagnostic,
};

type BoxedCause = Box<dyn std::error::Error + Send + Sync + 'static>;

/// Failure to produce an operation's requested output.
///
/// Every value contains a registered error diagnostic. An underlying I/O or
/// library error remains available through [`std::error::Error::source`].
pub struct Error {
    diagnostics: Vec<Diagnostic>,
    cause: Option<BoxedCause>,
    retained_source: Option<Source>,
}

impl Error {
    /// Construct a failure from a registered code carrying a category.
    ///
    /// A code that ends an operation must declare a category, because the
    /// category is what a binding and an exit status project the failure onto.
    /// A code that does not is a registry defect: the finding keeps its own
    /// code so its identity is not lost, and a `REQUEST.DIAGNOSTIC.MISSING_CATEGORY`
    /// note records the defect. A debug build asserts instead, so the defect
    /// surfaces in tests rather than in a released binding.
    #[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,
        }
    }

    /// Add a diagnostic emitted before or while the operation failed.
    #[must_use]
    pub fn with_diagnostic(mut self, diagnostic: Diagnostic) -> Self {
        self.diagnostics.push(diagnostic);
        self
    }

    /// Add diagnostics without changing their order.
    #[must_use]
    pub fn with_diagnostics(mut self, diagnostics: impl IntoIterator<Item = Diagnostic>) -> Self {
        self.diagnostics.extend(diagnostics);
        self
    }

    /// Retain the implementation error that caused this operation failure.
    #[must_use]
    pub fn with_cause(mut self, cause: impl std::error::Error + Send + Sync + 'static) -> Self {
        self.cause = Some(Box::new(cause));
        self
    }

    /// Retain the shared input owner needed to interpret diagnostic spans.
    #[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
    }

    /// The registered entry of the diagnostic that ended the operation, when
    /// the failure was built from one.
    #[must_use]
    pub fn info(&self) -> Option<&'static crate::DiagnosticInfo> {
        self.diagnostics
            .first()
            .and_then(Diagnostic::registered_info)
    }

    /// Coarse projection from the first registered error diagnostic.
    #[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
        );
    }
}