#![allow(dead_code)]
pub(crate) mod columns;
pub(crate) mod fmtnum;
pub(crate) mod kvn;
pub(crate) mod records;
pub(crate) mod tokens;
use crate::validate::FieldError;
pub(crate) trait FormatReader {
type Output;
fn read_str(&self, input: &str) -> Parsed<Self::Output>;
fn read_bytes(&self, input: &[u8]) -> Parsed<Self::Output>;
}
pub(crate) trait FormatWriter {
type Input;
fn write_string(&self, value: &Self::Input) -> String;
}
#[derive(Debug, Clone)]
pub struct Parsed<T> {
pub value: T,
pub diagnostics: Diagnostics,
}
impl<T> Parsed<T> {
pub fn new(value: T, diagnostics: Diagnostics) -> Self {
Self { value, diagnostics }
}
pub fn clean(value: T) -> Self {
Self::new(value, Diagnostics::new())
}
pub fn value(&self) -> &T {
&self.value
}
pub fn diagnostics(&self) -> &Diagnostics {
&self.diagnostics
}
pub fn into_parts(self) -> (T, Diagnostics) {
(self.value, self.diagnostics)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Diagnostics {
pub skips: Vec<Skip>,
pub warnings: Vec<Warning>,
}
impl Diagnostics {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.skips.is_empty() && self.warnings.is_empty()
}
pub fn push_skip(&mut self, skip: Skip) {
self.skips.push(skip);
}
pub fn push_warning(&mut self, warning: Warning) {
self.warnings.push(warning);
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Skip {
pub at: RecordRef,
pub reason: SkipReason,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SkipReason {
UnrepresentableSatellite,
UnsupportedRecordType(&'static str),
MalformedField(FieldError),
OutOfRangeEpoch,
Truncated,
UnsupportedUnit(String),
UnknownBlock(String),
InconsistentRecord(&'static str),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Warning {
pub at: RecordRef,
pub kind: WarningKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WarningKind {
Checksum,
Clamped,
Degraded,
Mismatch,
Overlap,
MissingMetadata,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct RecordRef {
pub line: Option<usize>,
pub record_index: Option<usize>,
pub satellite: Option<String>,
}
impl RecordRef {
pub fn at_line(line: usize) -> Self {
Self {
line: Some(line),
..Self::default()
}
}
pub fn at_record(record_index: usize) -> Self {
Self {
record_index: Some(record_index),
..Self::default()
}
}
pub fn with_satellite(mut self, satellite: impl Into<String>) -> Self {
self.satellite = Some(satellite.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parsed_round_trips_diagnostics() {
let mut diagnostics = Diagnostics::new();
assert!(diagnostics.is_empty());
let skip = Skip {
at: RecordRef::at_line(3).with_satellite("G05"),
reason: SkipReason::UnsupportedRecordType("DATA"),
};
diagnostics.push_skip(skip.clone());
assert!(!diagnostics.is_empty());
let warning = Warning {
at: RecordRef::at_record(0),
kind: WarningKind::Checksum,
};
diagnostics.push_warning(warning.clone());
let parsed = Parsed::new(42, diagnostics.clone());
assert_eq!(*parsed.value(), 42);
assert_eq!(parsed.diagnostics(), &diagnostics);
let (value, round_trip) = parsed.into_parts();
assert_eq!(value, 42);
assert_eq!(round_trip.skips, vec![skip]);
assert_eq!(round_trip.warnings, vec![warning]);
}
#[test]
fn clean_parsed_has_empty_diagnostics() {
let parsed = Parsed::clean("ok");
assert_eq!(parsed.value(), &"ok");
assert!(parsed.diagnostics().is_empty());
}
#[test]
fn malformed_field_wraps_field_error() {
let field_error = FieldError::FloatParse {
field: "epoch",
value: "bad".to_string(),
};
let reason = SkipReason::MalformedField(field_error.clone());
assert_eq!(reason, SkipReason::MalformedField(field_error));
}
}