sidereon_core/format/
mod.rs1#![allow(dead_code)]
12
13pub(crate) mod columns;
15pub(crate) mod fmtnum;
17pub(crate) mod kvn;
19pub(crate) mod records;
21pub(crate) mod tokens;
23
24use crate::validate::FieldError;
25
26pub(crate) trait FormatReader {
28 type Output;
30
31 fn read_str(&self, input: &str) -> Parsed<Self::Output>;
33
34 fn read_bytes(&self, input: &[u8]) -> Parsed<Self::Output>;
36}
37
38pub(crate) trait FormatWriter {
40 type Input;
42
43 fn write_string(&self, value: &Self::Input) -> String;
45}
46
47#[derive(Debug, Clone)]
49pub struct Parsed<T> {
50 pub value: T,
52 pub diagnostics: Diagnostics,
54}
55
56impl<T> Parsed<T> {
57 pub fn new(value: T, diagnostics: Diagnostics) -> Self {
59 Self { value, diagnostics }
60 }
61
62 pub fn clean(value: T) -> Self {
64 Self::new(value, Diagnostics::new())
65 }
66
67 pub fn value(&self) -> &T {
69 &self.value
70 }
71
72 pub fn diagnostics(&self) -> &Diagnostics {
74 &self.diagnostics
75 }
76
77 pub fn into_parts(self) -> (T, Diagnostics) {
79 (self.value, self.diagnostics)
80 }
81}
82
83#[derive(Debug, Clone, Default, PartialEq)]
85pub struct Diagnostics {
86 pub skips: Vec<Skip>,
88 pub warnings: Vec<Warning>,
90}
91
92impl Diagnostics {
93 pub fn new() -> Self {
95 Self::default()
96 }
97
98 pub fn is_empty(&self) -> bool {
100 self.skips.is_empty() && self.warnings.is_empty()
101 }
102
103 pub fn push_skip(&mut self, skip: Skip) {
105 self.skips.push(skip);
106 }
107
108 pub fn push_warning(&mut self, warning: Warning) {
110 self.warnings.push(warning);
111 }
112}
113
114#[derive(Debug, Clone, PartialEq)]
116pub struct Skip {
117 pub at: RecordRef,
119 pub reason: SkipReason,
121}
122
123#[derive(Debug, Clone, PartialEq)]
125pub enum SkipReason {
126 UnrepresentableSatellite,
128 UnsupportedRecordType(&'static str),
130 MalformedField(FieldError),
132 OutOfRangeEpoch,
134 Truncated,
136 UnsupportedUnit(String),
138 UnknownBlock(String),
140 InconsistentRecord(&'static str),
142}
143
144#[derive(Debug, Clone, PartialEq)]
146pub struct Warning {
147 pub at: RecordRef,
149 pub kind: WarningKind,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum WarningKind {
156 Checksum,
158 Clamped,
160 Degraded,
162 Mismatch,
164 Overlap,
166 MissingMetadata,
168}
169
170#[derive(Debug, Clone, PartialEq, Default)]
172pub struct RecordRef {
173 pub line: Option<usize>,
175 pub record_index: Option<usize>,
177 pub satellite: Option<String>,
179}
180
181impl RecordRef {
182 pub fn at_line(line: usize) -> Self {
184 Self {
185 line: Some(line),
186 ..Self::default()
187 }
188 }
189
190 pub fn at_record(record_index: usize) -> Self {
192 Self {
193 record_index: Some(record_index),
194 ..Self::default()
195 }
196 }
197
198 pub fn with_satellite(mut self, satellite: impl Into<String>) -> Self {
200 self.satellite = Some(satellite.into());
201 self
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn parsed_round_trips_diagnostics() {
211 let mut diagnostics = Diagnostics::new();
212 assert!(diagnostics.is_empty());
213
214 let skip = Skip {
215 at: RecordRef::at_line(3).with_satellite("G05"),
216 reason: SkipReason::UnsupportedRecordType("DATA"),
217 };
218 diagnostics.push_skip(skip.clone());
219 assert!(!diagnostics.is_empty());
220
221 let warning = Warning {
222 at: RecordRef::at_record(0),
223 kind: WarningKind::Checksum,
224 };
225 diagnostics.push_warning(warning.clone());
226
227 let parsed = Parsed::new(42, diagnostics.clone());
228 assert_eq!(*parsed.value(), 42);
229 assert_eq!(parsed.diagnostics(), &diagnostics);
230
231 let (value, round_trip) = parsed.into_parts();
232 assert_eq!(value, 42);
233 assert_eq!(round_trip.skips, vec![skip]);
234 assert_eq!(round_trip.warnings, vec![warning]);
235 }
236
237 #[test]
238 fn clean_parsed_has_empty_diagnostics() {
239 let parsed = Parsed::clean("ok");
240 assert_eq!(parsed.value(), &"ok");
241 assert!(parsed.diagnostics().is_empty());
242 }
243
244 #[test]
245 fn malformed_field_wraps_field_error() {
246 let field_error = FieldError::FloatParse {
247 field: "epoch",
248 value: "bad".to_string(),
249 };
250 let reason = SkipReason::MalformedField(field_error.clone());
251 assert_eq!(reason, SkipReason::MalformedField(field_error));
252 }
253}