use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ParseMode {
Strict,
#[default]
BestEffort,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Warning {
pub code: String,
pub message: String,
pub offset: Option<usize>,
}
impl Warning {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
offset: None,
}
}
pub fn at(code: impl Into<String>, message: impl Into<String>, offset: usize) -> Self {
Self {
code: code.into(),
message: message.into(),
offset: Some(offset),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Diagnostics {
pub warnings: Vec<Warning>,
pub skipped_records: usize,
pub failed_streams: Vec<String>,
pub partial_fields: Vec<String>,
pub confidence: Option<f32>,
}
impl Diagnostics {
pub fn is_empty(&self) -> bool {
self.warnings.is_empty()
&& self.skipped_records == 0
&& self.failed_streams.is_empty()
&& self.partial_fields.is_empty()
}
pub fn warn(&mut self, w: Warning) {
self.warnings.push(w);
}
pub fn fail_stream(&mut self, name: impl Into<String>) {
self.failed_streams.push(name.into());
}
pub fn partial_field(&mut self, name: impl Into<String>) {
self.partial_fields.push(name.into());
}
pub fn extend(&mut self, other: Diagnostics) {
self.warnings.extend(other.warnings);
self.skipped_records += other.skipped_records;
self.failed_streams.extend(other.failed_streams);
self.partial_fields.extend(other.partial_fields);
if self.confidence.is_none() {
self.confidence = other.confidence;
}
}
}
impl std::fmt::Display for Diagnostics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_empty() {
return write!(f, "(no diagnostics)");
}
writeln!(f, "Diagnostics:")?;
if !self.warnings.is_empty() {
writeln!(f, " Warnings ({}):", self.warnings.len())?;
for w in &self.warnings {
match w.offset {
Some(o) => writeln!(f, " [{}] {} (@ 0x{:x})", w.code, w.message, o)?,
None => writeln!(f, " [{}] {}", w.code, w.message)?,
}
}
}
if self.skipped_records > 0 {
writeln!(f, " Skipped records: {}", self.skipped_records)?;
}
if !self.failed_streams.is_empty() {
writeln!(f, " Failed streams:")?;
for s in &self.failed_streams {
writeln!(f, " - {s}")?;
}
}
if !self.partial_fields.is_empty() {
writeln!(f, " Partial fields:")?;
for fld in &self.partial_fields {
writeln!(f, " - {fld}")?;
}
}
if let Some(c) = self.confidence {
writeln!(f, " Confidence: {c:.2}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Decoded<T> {
pub value: T,
pub diagnostics: Diagnostics,
pub complete: bool,
}
impl<T> Decoded<T> {
pub fn complete(value: T) -> Self {
Self {
value,
diagnostics: Diagnostics::default(),
complete: true,
}
}
pub fn partial(value: T, diagnostics: Diagnostics) -> Self {
Self {
value,
diagnostics,
complete: false,
}
}
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Decoded<U> {
Decoded {
value: f(self.value),
diagnostics: self.diagnostics,
complete: self.complete,
}
}
pub fn is_clean(&self) -> bool {
self.complete && self.diagnostics.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_mode_default_is_best_effort() {
assert_eq!(ParseMode::default(), ParseMode::BestEffort);
}
#[test]
fn warning_construction_with_and_without_offset() {
let w = Warning::new("test", "msg");
assert_eq!(w.offset, None);
let w2 = Warning::at("test", "msg", 0x100);
assert_eq!(w2.offset, Some(0x100));
}
#[test]
fn diagnostics_empty_by_default() {
let d = Diagnostics::default();
assert!(d.is_empty());
}
#[test]
fn diagnostics_not_empty_after_any_addition() {
let mut d = Diagnostics::default();
d.warn(Warning::new("x", "y"));
assert!(!d.is_empty());
let mut d = Diagnostics::default();
d.fail_stream("bad");
assert!(!d.is_empty());
let mut d = Diagnostics::default();
d.partial_field("f");
assert!(!d.is_empty());
}
#[test]
fn diagnostics_extend_merges() {
let mut a = Diagnostics::default();
a.warn(Warning::new("a", "1"));
let mut b = Diagnostics::default();
b.warn(Warning::new("b", "2"));
b.skipped_records = 3;
a.extend(b);
assert_eq!(a.warnings.len(), 2);
assert_eq!(a.skipped_records, 3);
}
#[test]
fn decoded_complete_has_empty_diagnostics() {
let d = Decoded::complete(42);
assert_eq!(d.value, 42);
assert!(d.complete);
assert!(d.diagnostics.is_empty());
assert!(d.is_clean());
}
#[test]
fn decoded_partial_not_clean() {
let mut diag = Diagnostics::default();
diag.warn(Warning::new("w", "oops"));
let d = Decoded::partial(42, diag);
assert!(!d.is_clean());
assert!(!d.complete);
}
#[test]
fn decoded_map_preserves_diagnostics() {
let mut diag = Diagnostics::default();
diag.warn(Warning::new("w", "oops"));
let d = Decoded::partial(42, diag).map(|n| n * 2);
assert_eq!(d.value, 84);
assert_eq!(d.diagnostics.warnings.len(), 1);
assert!(!d.complete);
}
#[test]
fn diagnostics_display_is_informative() {
let mut d = Diagnostics::default();
d.warn(Warning::at("schema-skip", "bad record", 0x123));
d.fail_stream("Formats/Latest");
d.partial_field("m_elemTable");
d.skipped_records = 2;
d.confidence = Some(0.75);
let s = format!("{d}");
assert!(s.contains("schema-skip"));
assert!(s.contains("0x123"));
assert!(s.contains("Formats/Latest"));
assert!(s.contains("m_elemTable"));
assert!(s.contains("Skipped records: 2"));
assert!(s.contains("Confidence: 0.75"));
}
#[test]
fn empty_diagnostics_display_is_explicit() {
let s = format!("{}", Diagnostics::default());
assert_eq!(s, "(no diagnostics)");
}
}