#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiagnosticSeverity {
Warning,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DiagnosticCode {
UnsupportedArrowType,
LossyConversionRequiresPolicy,
PolicyApplied,
IdentifierInvalid,
IdentifierTooLong,
DecimalOutOfRange,
IntegerOutOfRange,
TimestampOutOfRange,
TimezoneUnsupported,
SchemaMismatch,
BackendUnavailable,
ProfileDependentConversion,
ObservedDataRequired,
ValueConversionUnsupported,
ValueTypeMismatch,
NullInNonNullableColumn,
NonFiniteFloat,
ValueTooLong,
RowIndexOutOfBounds,
DirectEncodingInvalidPayload,
DirectEncodingUnsupportedMapping,
DirectEncodingUnsupportedBatch,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FieldRef {
index: usize,
name: String,
}
impl FieldRef {
pub fn new(index: usize, name: impl Into<String>) -> Self {
Self {
index,
name: name.into(),
}
}
pub const fn index(&self) -> usize {
self.index
}
pub fn name(&self) -> &str {
&self.name
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
severity: DiagnosticSeverity,
code: DiagnosticCode,
message: String,
field: Option<FieldRef>,
row: Option<usize>,
}
impl Diagnostic {
pub fn new(
severity: DiagnosticSeverity,
code: DiagnosticCode,
message: impl Into<String>,
) -> Self {
Self {
severity,
code,
message: message.into(),
field: None,
row: None,
}
}
pub fn warning(code: DiagnosticCode, message: impl Into<String>) -> Self {
Self::new(DiagnosticSeverity::Warning, code, message)
}
pub fn error(code: DiagnosticCode, message: impl Into<String>) -> Self {
Self::new(DiagnosticSeverity::Error, code, message)
}
#[must_use]
pub fn with_field(mut self, field: FieldRef) -> Self {
self.field = Some(field);
self
}
#[must_use]
pub const fn with_row(mut self, row: usize) -> Self {
self.row = Some(row);
self
}
pub const fn severity(&self) -> DiagnosticSeverity {
self.severity
}
pub const fn code(&self) -> DiagnosticCode {
self.code
}
pub fn message(&self) -> &str {
&self.message
}
pub fn field(&self) -> Option<&FieldRef> {
self.field.as_ref()
}
pub const fn row(&self) -> Option<usize> {
self.row
}
pub const fn is_error(&self) -> bool {
matches!(self.severity, DiagnosticSeverity::Error)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DiagnosticSet {
diagnostics: Vec<Diagnostic>,
}
impl DiagnosticSet {
pub const fn new() -> Self {
Self {
diagnostics: Vec::new(),
}
}
pub fn push(&mut self, diagnostic: Diagnostic) {
self.diagnostics.push(diagnostic);
}
pub fn all(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn is_empty(&self) -> bool {
self.diagnostics.is_empty()
}
pub fn has_errors(&self) -> bool {
self.diagnostics.iter().any(Diagnostic::is_error)
}
pub fn len(&self) -> usize {
self.diagnostics.len()
}
}
impl From<Vec<Diagnostic>> for DiagnosticSet {
fn from(diagnostics: Vec<Diagnostic>) -> Self {
Self { diagnostics }
}
}
impl IntoIterator for DiagnosticSet {
type Item = Diagnostic;
type IntoIter = std::vec::IntoIter<Diagnostic>;
fn into_iter(self) -> Self::IntoIter {
self.diagnostics.into_iter()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlanOutcome<T> {
value: T,
diagnostics: DiagnosticSet,
}
impl<T> PlanOutcome<T> {
pub const fn new(value: T, diagnostics: DiagnosticSet) -> Self {
Self { value, diagnostics }
}
pub const fn value(&self) -> &T {
&self.value
}
pub const fn diagnostics(&self) -> &DiagnosticSet {
&self.diagnostics
}
pub fn into_parts(self) -> (T, DiagnosticSet) {
(self.value, self.diagnostics)
}
}
#[cfg(test)]
mod tests {
use super::{
Diagnostic, DiagnosticCode, DiagnosticSet, DiagnosticSeverity, FieldRef, PlanOutcome,
};
#[test]
fn creates_field_diagnostic() {
let diagnostic = Diagnostic::warning(DiagnosticCode::PolicyApplied, "policy applied")
.with_field(FieldRef::new(2, "amount"));
assert_eq!(diagnostic.severity(), DiagnosticSeverity::Warning);
assert_eq!(diagnostic.code(), DiagnosticCode::PolicyApplied);
assert_eq!(diagnostic.message(), "policy applied");
let field = diagnostic.field().unwrap();
assert_eq!(field.index(), 2);
assert_eq!(field.name(), "amount");
assert_eq!(diagnostic.row(), None);
}
#[test]
fn creates_row_and_field_diagnostic() {
let diagnostic = Diagnostic::error(
DiagnosticCode::NullInNonNullableColumn,
"null value cannot be written",
)
.with_field(FieldRef::new(3, "name"))
.with_row(42);
assert_eq!(diagnostic.severity(), DiagnosticSeverity::Error);
assert_eq!(diagnostic.code(), DiagnosticCode::NullInNonNullableColumn);
assert_eq!(diagnostic.row(), Some(42));
let field = diagnostic.field().unwrap();
assert_eq!(field.index(), 3);
assert_eq!(field.name(), "name");
}
#[test]
fn detects_error_diagnostics() {
let mut diagnostics = DiagnosticSet::new();
diagnostics.push(Diagnostic::warning(
DiagnosticCode::PolicyApplied,
"policy applied",
));
assert!(!diagnostics.has_errors());
diagnostics.push(Diagnostic::error(
DiagnosticCode::UnsupportedArrowType,
"unsupported",
));
assert!(diagnostics.has_errors());
assert_eq!(diagnostics.len(), 2);
}
#[test]
fn empty_diagnostic_set_has_no_errors() {
let diagnostics = DiagnosticSet::new();
assert!(diagnostics.is_empty());
assert!(!diagnostics.has_errors());
assert_eq!(diagnostics.all(), &[]);
}
#[test]
fn converts_from_vec() {
let diagnostics = DiagnosticSet::from(vec![Diagnostic::error(
DiagnosticCode::IdentifierInvalid,
"invalid",
)]);
assert_eq!(diagnostics.len(), 1);
assert!(!diagnostics.is_empty());
}
#[test]
fn preserves_diagnostic_order_when_consumed() {
let diagnostics = DiagnosticSet::from(vec![
Diagnostic::warning(DiagnosticCode::PolicyApplied, "first"),
Diagnostic::error(DiagnosticCode::SchemaMismatch, "second"),
]);
let messages = diagnostics
.into_iter()
.map(|diagnostic| diagnostic.message().to_owned())
.collect::<Vec<_>>();
assert_eq!(messages, ["first", "second"]);
}
#[test]
fn plan_outcome_exposes_value_and_diagnostics() {
let diagnostics = DiagnosticSet::from(vec![Diagnostic::warning(
DiagnosticCode::ProfileDependentConversion,
"policy needed",
)]);
let outcome = PlanOutcome::new("plan", diagnostics);
assert_eq!(outcome.value(), &"plan");
assert_eq!(outcome.diagnostics().len(), 1);
let (value, diagnostics) = outcome.into_parts();
assert_eq!(value, "plan");
assert_eq!(diagnostics.len(), 1);
}
}