Skip to main content

powerio_core/
error.rs

1use std::fmt;
2
3use crate::{
4    Diagnostic, DiagnosticInfo, DiagnosticSeverity, ErrorCategory, Source, render_diagnostic,
5};
6
7type BoxedCause = Box<dyn std::error::Error + Send + Sync + 'static>;
8
9/// Failure to produce an operation's requested output.
10///
11/// Every value contains a registered error diagnostic. An underlying I/O or
12/// library error remains available through [`std::error::Error::source`].
13pub struct Error {
14    diagnostics: Vec<Diagnostic>,
15    cause: Option<BoxedCause>,
16    retained_source: Option<Source>,
17}
18
19impl Error {
20    /// Construct a failure from a registered code carrying a category.
21    ///
22    /// A code that ends an operation must declare a category, because the
23    /// category is what a binding and an exit status project the failure onto.
24    /// A code that does not is a registry defect: the finding keeps its own
25    /// code so its identity is not lost, and a `REQUEST.DIAGNOSTIC.MISSING_CATEGORY`
26    /// note records the defect. A debug build asserts instead, so the defect
27    /// surfaces in tests rather than in a released binding.
28    #[must_use]
29    pub fn new(info: &'static DiagnosticInfo, message: impl Into<String>) -> Self {
30        debug_assert!(
31            info.category.is_some(),
32            "{} ends an operation but declares no error category",
33            info.code
34        );
35        let mut diagnostics =
36            vec![Diagnostic::of(info, message).with_severity(DiagnosticSeverity::Error)];
37        if info.category.is_none() {
38            diagnostics.push(
39                Diagnostic::of(
40                    &crate::codes::REQUEST_DIAGNOSTIC_MISSING_CATEGORY,
41                    format!("{} declares no error category", info.code),
42                )
43                .with_severity(DiagnosticSeverity::Note),
44            );
45        }
46        Self {
47            diagnostics,
48            cause: None,
49            retained_source: None,
50        }
51    }
52
53    /// Add a diagnostic emitted before or while the operation failed.
54    #[must_use]
55    pub fn with_diagnostic(mut self, diagnostic: Diagnostic) -> Self {
56        self.diagnostics.push(diagnostic);
57        self
58    }
59
60    /// Add diagnostics without changing their order.
61    #[must_use]
62    pub fn with_diagnostics(mut self, diagnostics: impl IntoIterator<Item = Diagnostic>) -> Self {
63        self.diagnostics.extend(diagnostics);
64        self
65    }
66
67    /// Retain the implementation error that caused this operation failure.
68    #[must_use]
69    pub fn with_cause(mut self, cause: impl std::error::Error + Send + Sync + 'static) -> Self {
70        self.cause = Some(Box::new(cause));
71        self
72    }
73
74    /// Retain the shared input owner needed to interpret diagnostic spans.
75    #[must_use]
76    pub fn with_source(mut self, source: Source) -> Self {
77        self.retained_source = Some(source);
78        self
79    }
80
81    #[must_use]
82    pub fn diagnostics(&self) -> &[Diagnostic] {
83        &self.diagnostics
84    }
85
86    /// The registered entry of the diagnostic that ended the operation, when
87    /// the failure was built from one.
88    #[must_use]
89    pub fn info(&self) -> Option<&'static crate::DiagnosticInfo> {
90        self.diagnostics
91            .first()
92            .and_then(Diagnostic::registered_info)
93    }
94
95    /// Coarse projection from the first registered error diagnostic.
96    #[must_use]
97    pub fn category(&self) -> ErrorCategory {
98        self.diagnostics
99            .iter()
100            .find(|diagnostic| diagnostic.severity() == DiagnosticSeverity::Error)
101            .and_then(Diagnostic::registered_info)
102            .and_then(|info| info.category)
103            .unwrap_or(ErrorCategory::Data)
104    }
105
106    #[must_use]
107    pub const fn retained_source(&self) -> Option<&Source> {
108        self.retained_source.as_ref()
109    }
110
111    #[must_use]
112    pub fn into_diagnostics(self) -> Vec<Diagnostic> {
113        self.diagnostics
114    }
115}
116
117impl fmt::Display for Error {
118    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        let diagnostic = self
120            .diagnostics
121            .iter()
122            .find(|diagnostic| diagnostic.severity() == DiagnosticSeverity::Error);
123        match diagnostic {
124            Some(diagnostic) => formatter.write_str(&render_diagnostic(diagnostic)),
125            None => formatter.write_str("PowerIO operation failed without a diagnostic"),
126        }
127    }
128}
129
130impl fmt::Debug for Error {
131    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132        formatter
133            .debug_struct("Error")
134            .field("category", &self.category())
135            .field("diagnostics", &self.diagnostics)
136            .field("cause", &self.cause.as_ref().map(ToString::to_string))
137            .field("retained_source", &self.retained_source)
138            .finish()
139    }
140}
141
142impl std::error::Error for Error {
143    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
144        self.cause
145            .as_deref()
146            .map(|cause| cause as &(dyn std::error::Error + 'static))
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use std::error::Error as _;
153
154    use super::*;
155
156    #[test]
157    fn an_error_has_one_error_diagnostic_and_a_registered_category() {
158        let error = Error::new(
159            &crate::codes::VALIDATE_TIME_SERIES_SHAPE,
160            "two values for one point",
161        );
162        assert_eq!(error.category(), ErrorCategory::Data);
163        assert_eq!(error.diagnostics().len(), 1);
164        assert_eq!(error.diagnostics()[0].severity(), DiagnosticSeverity::Error);
165        assert!(
166            error
167                .to_string()
168                .starts_with("VALIDATE.TIME_SERIES.SHAPE: ")
169        );
170    }
171
172    #[test]
173    fn cause_and_shared_source_are_retained() {
174        let source = Source::from_bytes("input.bin", vec![0, 255]).unwrap();
175        let byte_pointer = source.primary_buffer().unwrap().bytes().as_ptr();
176        let error = Error::new(&crate::codes::READ_IO_READ, "read failed")
177            .with_cause(std::io::Error::other("cause"))
178            .with_source(source);
179        assert_eq!(error.source().unwrap().to_string(), "cause");
180        assert_eq!(
181            error
182                .retained_source()
183                .unwrap()
184                .primary_buffer()
185                .unwrap()
186                .bytes()
187                .as_ptr(),
188            byte_pointer
189        );
190    }
191}