1use std::fmt;
2
3use crate::{
4 Diagnostic, DiagnosticInfo, DiagnosticSeverity, ErrorCategory, Source, SourceSpan,
5 render_diagnostic,
6};
7
8type BoxedCause = Box<dyn std::error::Error + Send + Sync + 'static>;
9
10pub struct Error {
15 diagnostics: Vec<Diagnostic>,
16 cause: Option<BoxedCause>,
17 retained_source: Option<Source>,
18}
19
20impl Error {
21 #[must_use]
30 pub fn new(info: &'static DiagnosticInfo, message: impl Into<String>) -> Self {
31 debug_assert!(
32 info.category.is_some(),
33 "{} ends an operation but declares no error category",
34 info.code
35 );
36 let mut diagnostics =
37 vec![Diagnostic::of(info, message).with_severity(DiagnosticSeverity::Error)];
38 if info.category.is_none() {
39 diagnostics.push(
40 Diagnostic::of(
41 &crate::codes::REQUEST_DIAGNOSTIC_MISSING_CATEGORY,
42 format!("{} declares no error category", info.code),
43 )
44 .with_severity(DiagnosticSeverity::Note),
45 );
46 }
47 Self {
48 diagnostics,
49 cause: None,
50 retained_source: None,
51 }
52 }
53
54 #[must_use]
56 pub fn with_diagnostic(mut self, diagnostic: Diagnostic) -> Self {
57 self.diagnostics.push(diagnostic);
58 self
59 }
60
61 #[must_use]
63 pub fn with_diagnostics(mut self, diagnostics: impl IntoIterator<Item = Diagnostic>) -> Self {
64 self.diagnostics.extend(diagnostics);
65 self
66 }
67
68 #[must_use]
70 pub fn with_cause(mut self, cause: impl std::error::Error + Send + Sync + 'static) -> Self {
71 self.cause = Some(Box::new(cause));
72 self
73 }
74
75 #[must_use]
77 pub fn with_source(mut self, source: Source) -> Self {
78 self.retained_source = Some(source);
79 self
80 }
81
82 #[must_use]
89 pub fn with_span(mut self, span: SourceSpan) -> Self {
90 let Some(position) = self
91 .diagnostics
92 .iter()
93 .position(|diagnostic| diagnostic.severity() == DiagnosticSeverity::Error)
94 else {
95 return self;
96 };
97 if let Err(refused) = self.diagnostics[position].add_span(span) {
98 self.diagnostics.extend(
99 refused
100 .into_diagnostics()
101 .into_iter()
102 .map(|diagnostic| diagnostic.with_severity(DiagnosticSeverity::Note)),
103 );
104 }
105 self
106 }
107
108 #[must_use]
109 pub fn diagnostics(&self) -> &[Diagnostic] {
110 &self.diagnostics
111 }
112
113 #[must_use]
116 pub fn info(&self) -> Option<&'static crate::DiagnosticInfo> {
117 self.diagnostics
118 .first()
119 .and_then(Diagnostic::registered_info)
120 }
121
122 #[must_use]
124 pub fn category(&self) -> ErrorCategory {
125 self.diagnostics
126 .iter()
127 .find(|diagnostic| diagnostic.severity() == DiagnosticSeverity::Error)
128 .and_then(Diagnostic::registered_info)
129 .and_then(|info| info.category)
130 .unwrap_or(ErrorCategory::Data)
131 }
132
133 #[must_use]
134 pub const fn retained_source(&self) -> Option<&Source> {
135 self.retained_source.as_ref()
136 }
137
138 #[must_use]
139 pub fn into_diagnostics(self) -> Vec<Diagnostic> {
140 self.diagnostics
141 }
142}
143
144impl fmt::Display for Error {
145 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
146 let diagnostic = self
147 .diagnostics
148 .iter()
149 .find(|diagnostic| diagnostic.severity() == DiagnosticSeverity::Error);
150 match diagnostic {
151 Some(diagnostic) => formatter.write_str(&render_diagnostic(diagnostic)),
152 None => formatter.write_str("PowerIO operation failed without a diagnostic"),
153 }
154 }
155}
156
157impl fmt::Debug for Error {
158 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
159 formatter
160 .debug_struct("Error")
161 .field("category", &self.category())
162 .field("diagnostics", &self.diagnostics)
163 .field("cause", &self.cause.as_ref().map(ToString::to_string))
164 .field("retained_source", &self.retained_source)
165 .finish()
166 }
167}
168
169impl std::error::Error for Error {
170 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
171 self.cause
172 .as_deref()
173 .map(|cause| cause as &(dyn std::error::Error + 'static))
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use std::error::Error as _;
180
181 use super::*;
182
183 #[test]
184 fn an_error_has_one_error_diagnostic_and_a_registered_category() {
185 let error = Error::new(
186 &crate::codes::VALIDATE_TIME_SERIES_SHAPE,
187 "two values for one point",
188 );
189 assert_eq!(error.category(), ErrorCategory::Data);
190 assert_eq!(error.diagnostics().len(), 1);
191 assert_eq!(error.diagnostics()[0].severity(), DiagnosticSeverity::Error);
192 assert!(
193 error
194 .to_string()
195 .starts_with("VALIDATE.TIME_SERIES.SHAPE: ")
196 );
197 }
198
199 #[test]
200 fn a_span_attaches_to_the_diagnostic_that_ended_the_operation() {
201 let source = crate::SourceId::new("/input").unwrap();
202 let error = Error::new(&crate::codes::READ_IO_READ, "read failed")
203 .with_diagnostic(
204 Diagnostic::of(&crate::codes::READ_IO_READ, "context")
205 .with_severity(DiagnosticSeverity::Note),
206 )
207 .with_span(SourceSpan::new(source.clone(), 4, 9).unwrap());
208 let spans = error.diagnostics()[0].spans();
209 assert_eq!(spans.len(), 1);
210 assert_eq!(spans[0].source(), &source);
211 assert_eq!((spans[0].byte_start(), spans[0].byte_end()), (4, 9));
212 assert!(error.diagnostics()[1].spans().is_empty());
213
214 let mut crowded = Error::new(&crate::codes::READ_IO_READ, "read failed");
217 for _ in 0..crate::validation::MAX_DIAGNOSTIC_SPANS {
218 crowded = crowded.with_span(SourceSpan::new(source.clone(), 0, 1).unwrap());
219 }
220 let overflow = crowded.with_span(SourceSpan::new(source, 0, 1).unwrap());
221 assert_eq!(
222 overflow.diagnostics()[0].spans().len(),
223 crate::validation::MAX_DIAGNOSTIC_SPANS
224 );
225 assert_eq!(
226 overflow.diagnostics().last().unwrap().severity(),
227 DiagnosticSeverity::Note
228 );
229 }
230
231 #[test]
232 fn cause_and_shared_source_are_retained() {
233 let source = Source::from_memory("input.bin", vec![0, 255]).unwrap();
234 let byte_pointer = source.primary_buffer().unwrap().bytes().as_ptr();
235 let error = Error::new(&crate::codes::READ_IO_READ, "read failed")
236 .with_cause(std::io::Error::other("cause"))
237 .with_source(source);
238 assert_eq!(error.source().unwrap().to_string(), "cause");
239 assert_eq!(
240 error
241 .retained_source()
242 .unwrap()
243 .primary_buffer()
244 .unwrap()
245 .bytes()
246 .as_ptr(),
247 byte_pointer
248 );
249 }
250}