1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum DiagnosticSeverity {
6 Warning,
8 Error,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[non_exhaustive]
15pub enum DiagnosticCode {
16 UnsupportedArrowType,
18 LossyConversionRequiresPolicy,
20 PolicyApplied,
22 IdentifierInvalid,
24 IdentifierTooLong,
26 DecimalOutOfRange,
28 IntegerOutOfRange,
30 TimestampOutOfRange,
32 TimezoneUnsupported,
37 SchemaMismatch,
39 BackendUnavailable,
41 ProfileDependentConversion,
43 ObservedDataRequired,
45 ValueConversionUnsupported,
47 ValueTypeMismatch,
49 NullInNonNullableColumn,
51 NonFiniteFloat,
53 ValueTooLong,
55 RowIndexOutOfBounds,
57 DirectEncodingInvalidPayload,
59 DirectEncodingUnsupportedMapping,
61 DirectEncodingUnsupportedBatch,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Hash)]
67pub struct FieldRef {
68 index: usize,
69 name: String,
70}
71
72impl FieldRef {
73 pub fn new(index: usize, name: impl Into<String>) -> Self {
75 Self {
76 index,
77 name: name.into(),
78 }
79 }
80
81 pub const fn index(&self) -> usize {
83 self.index
84 }
85
86 pub fn name(&self) -> &str {
88 &self.name
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct Diagnostic {
95 severity: DiagnosticSeverity,
96 code: DiagnosticCode,
97 message: String,
98 field: Option<FieldRef>,
99 row: Option<usize>,
100}
101
102impl Diagnostic {
103 pub fn new(
105 severity: DiagnosticSeverity,
106 code: DiagnosticCode,
107 message: impl Into<String>,
108 ) -> Self {
109 Self {
110 severity,
111 code,
112 message: message.into(),
113 field: None,
114 row: None,
115 }
116 }
117
118 pub fn warning(code: DiagnosticCode, message: impl Into<String>) -> Self {
120 Self::new(DiagnosticSeverity::Warning, code, message)
121 }
122
123 pub fn error(code: DiagnosticCode, message: impl Into<String>) -> Self {
125 Self::new(DiagnosticSeverity::Error, code, message)
126 }
127
128 #[must_use]
130 pub fn with_field(mut self, field: FieldRef) -> Self {
131 self.field = Some(field);
132 self
133 }
134
135 #[must_use]
137 pub const fn with_row(mut self, row: usize) -> Self {
138 self.row = Some(row);
139 self
140 }
141
142 pub const fn severity(&self) -> DiagnosticSeverity {
144 self.severity
145 }
146
147 pub const fn code(&self) -> DiagnosticCode {
149 self.code
150 }
151
152 pub fn message(&self) -> &str {
154 &self.message
155 }
156
157 pub fn field(&self) -> Option<&FieldRef> {
159 self.field.as_ref()
160 }
161
162 pub const fn row(&self) -> Option<usize> {
164 self.row
165 }
166
167 pub const fn is_error(&self) -> bool {
169 matches!(self.severity, DiagnosticSeverity::Error)
170 }
171}
172
173#[derive(Debug, Clone, Default, PartialEq, Eq)]
175pub struct DiagnosticSet {
176 diagnostics: Vec<Diagnostic>,
177}
178
179impl DiagnosticSet {
180 pub const fn new() -> Self {
182 Self {
183 diagnostics: Vec::new(),
184 }
185 }
186
187 pub fn push(&mut self, diagnostic: Diagnostic) {
189 self.diagnostics.push(diagnostic);
190 }
191
192 pub fn all(&self) -> &[Diagnostic] {
194 &self.diagnostics
195 }
196
197 pub fn is_empty(&self) -> bool {
199 self.diagnostics.is_empty()
200 }
201
202 pub fn has_errors(&self) -> bool {
204 self.diagnostics.iter().any(Diagnostic::is_error)
205 }
206
207 pub fn len(&self) -> usize {
209 self.diagnostics.len()
210 }
211}
212
213impl From<Vec<Diagnostic>> for DiagnosticSet {
214 fn from(diagnostics: Vec<Diagnostic>) -> Self {
215 Self { diagnostics }
216 }
217}
218
219impl IntoIterator for DiagnosticSet {
220 type Item = Diagnostic;
221 type IntoIter = std::vec::IntoIter<Diagnostic>;
222
223 fn into_iter(self) -> Self::IntoIter {
224 self.diagnostics.into_iter()
225 }
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct PlanOutcome<T> {
231 value: T,
232 diagnostics: DiagnosticSet,
233}
234
235impl<T> PlanOutcome<T> {
236 pub const fn new(value: T, diagnostics: DiagnosticSet) -> Self {
238 Self { value, diagnostics }
239 }
240
241 pub const fn value(&self) -> &T {
243 &self.value
244 }
245
246 pub const fn diagnostics(&self) -> &DiagnosticSet {
248 &self.diagnostics
249 }
250
251 pub fn into_parts(self) -> (T, DiagnosticSet) {
253 (self.value, self.diagnostics)
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::{
260 Diagnostic, DiagnosticCode, DiagnosticSet, DiagnosticSeverity, FieldRef, PlanOutcome,
261 };
262
263 #[test]
264 fn creates_field_diagnostic() {
265 let diagnostic = Diagnostic::warning(DiagnosticCode::PolicyApplied, "policy applied")
266 .with_field(FieldRef::new(2, "amount"));
267
268 assert_eq!(diagnostic.severity(), DiagnosticSeverity::Warning);
269 assert_eq!(diagnostic.code(), DiagnosticCode::PolicyApplied);
270 assert_eq!(diagnostic.message(), "policy applied");
271
272 let field = diagnostic.field().unwrap();
273 assert_eq!(field.index(), 2);
274 assert_eq!(field.name(), "amount");
275 assert_eq!(diagnostic.row(), None);
276 }
277
278 #[test]
279 fn creates_row_and_field_diagnostic() {
280 let diagnostic = Diagnostic::error(
281 DiagnosticCode::NullInNonNullableColumn,
282 "null value cannot be written",
283 )
284 .with_field(FieldRef::new(3, "name"))
285 .with_row(42);
286
287 assert_eq!(diagnostic.severity(), DiagnosticSeverity::Error);
288 assert_eq!(diagnostic.code(), DiagnosticCode::NullInNonNullableColumn);
289 assert_eq!(diagnostic.row(), Some(42));
290
291 let field = diagnostic.field().unwrap();
292 assert_eq!(field.index(), 3);
293 assert_eq!(field.name(), "name");
294 }
295
296 #[test]
297 fn detects_error_diagnostics() {
298 let mut diagnostics = DiagnosticSet::new();
299 diagnostics.push(Diagnostic::warning(
300 DiagnosticCode::PolicyApplied,
301 "policy applied",
302 ));
303
304 assert!(!diagnostics.has_errors());
305
306 diagnostics.push(Diagnostic::error(
307 DiagnosticCode::UnsupportedArrowType,
308 "unsupported",
309 ));
310
311 assert!(diagnostics.has_errors());
312 assert_eq!(diagnostics.len(), 2);
313 }
314
315 #[test]
316 fn empty_diagnostic_set_has_no_errors() {
317 let diagnostics = DiagnosticSet::new();
318
319 assert!(diagnostics.is_empty());
320 assert!(!diagnostics.has_errors());
321 assert_eq!(diagnostics.all(), &[]);
322 }
323
324 #[test]
325 fn converts_from_vec() {
326 let diagnostics = DiagnosticSet::from(vec![Diagnostic::error(
327 DiagnosticCode::IdentifierInvalid,
328 "invalid",
329 )]);
330
331 assert_eq!(diagnostics.len(), 1);
332 assert!(!diagnostics.is_empty());
333 }
334
335 #[test]
336 fn preserves_diagnostic_order_when_consumed() {
337 let diagnostics = DiagnosticSet::from(vec![
338 Diagnostic::warning(DiagnosticCode::PolicyApplied, "first"),
339 Diagnostic::error(DiagnosticCode::SchemaMismatch, "second"),
340 ]);
341
342 let messages = diagnostics
343 .into_iter()
344 .map(|diagnostic| diagnostic.message().to_owned())
345 .collect::<Vec<_>>();
346
347 assert_eq!(messages, ["first", "second"]);
348 }
349
350 #[test]
351 fn plan_outcome_exposes_value_and_diagnostics() {
352 let diagnostics = DiagnosticSet::from(vec![Diagnostic::warning(
353 DiagnosticCode::ProfileDependentConversion,
354 "policy needed",
355 )]);
356 let outcome = PlanOutcome::new("plan", diagnostics);
357
358 assert_eq!(outcome.value(), &"plan");
359 assert_eq!(outcome.diagnostics().len(), 1);
360
361 let (value, diagnostics) = outcome.into_parts();
362 assert_eq!(value, "plan");
363 assert_eq!(diagnostics.len(), 1);
364 }
365}