Skip to main content

openbim_step/
recovery.rs

1//! Opt-in malformed-record recovery policy and non-fatal diagnostics.
2//!
3//! Strict parsing is the default because an authoring tool that silently drops
4//! data corrupts the model it is editing. A consumer (viewer, importer,
5//! reporter) has the opposite need: real exporter output contains occasional
6//! damaged records, and refusing an entire file over one of them is not useful.
7//!
8//! Recovery is therefore explicit, bounded, and reported: the caller opts in,
9//! only data records are recoverable, and every skipped byte range comes back
10//! as a [`Diagnostic`] so a consumer can show what was lost instead of
11//! pretending the file was clean.
12
13use crate::{Exchange, Span};
14use std::fmt;
15
16/// What to do when a data record cannot be parsed.
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
18pub enum OnMalformed {
19    /// Fail the parse. The default: an unreadable record is a hard error.
20    #[default]
21    Abort,
22    /// Report the record as a diagnostic, resynchronize, and keep parsing.
23    Skip,
24}
25
26/// Parse behavior toggles.
27///
28/// Constructed with [`ParseOptions::default`] (strict) and adjusted through
29/// [`ParseOptions::on_malformed_record`], so later options cannot break
30/// existing call sites.
31#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
32#[non_exhaustive]
33pub struct ParseOptions {
34    /// Policy for unparsable data records.
35    pub on_malformed_record: OnMalformed,
36}
37
38impl ParseOptions {
39    /// Strict options: any malformed record aborts the parse.
40    #[must_use]
41    pub const fn strict() -> Self {
42        Self {
43            on_malformed_record: OnMalformed::Abort,
44        }
45    }
46
47    /// Options that skip and report malformed data records.
48    #[must_use]
49    pub const fn lenient() -> Self {
50        Self {
51            on_malformed_record: OnMalformed::Skip,
52        }
53    }
54
55    /// Sets the malformed-record policy.
56    #[must_use]
57    pub const fn on_malformed_record(mut self, policy: OnMalformed) -> Self {
58        self.on_malformed_record = policy;
59        self
60    }
61}
62
63/// Severity of a non-fatal parse diagnostic.
64///
65/// Only [`Severity::Warning`] exists today: a diagnostic is emitted exactly
66/// when input was accepted but not fully represented. Fatal problems are
67/// returned as [`StepError`](crate::StepError) instead of being reported here.
68#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
69#[non_exhaustive]
70pub enum Severity {
71    /// Input was recovered with loss.
72    #[default]
73    Warning,
74}
75
76/// A non-fatal problem found while parsing.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct Diagnostic {
79    severity: Severity,
80    span: Span,
81    detail: String,
82}
83
84impl Diagnostic {
85    pub(crate) fn skipped_record(span: Span, detail: impl Into<String>) -> Self {
86        Self {
87            severity: Severity::Warning,
88            span,
89            detail: detail.into(),
90        }
91    }
92
93    /// Severity of the diagnostic.
94    #[must_use]
95    pub const fn severity(&self) -> Severity {
96        self.severity
97    }
98
99    /// Byte range of the original input that the diagnostic covers.
100    ///
101    /// For a skipped record this is the whole discarded range, so a consumer
102    /// can quote the exact bytes that were dropped.
103    #[must_use]
104    pub const fn span(&self) -> Span {
105        self.span
106    }
107
108    /// Human-readable description without a location prefix.
109    #[must_use]
110    pub fn detail(&self) -> &str {
111        &self.detail
112    }
113}
114
115impl fmt::Display for Diagnostic {
116    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
117        write!(
118            formatter,
119            "STEP warning at bytes {}..{}: {}",
120            self.span.start, self.span.end, self.detail
121        )
122    }
123}
124
125/// A parsed exchange together with everything that was recovered with loss.
126#[derive(Debug, Clone, PartialEq)]
127pub struct ParseOutcome {
128    /// Records that were read successfully.
129    pub exchange: Exchange,
130    /// Non-fatal problems, in source order. Empty for a clean file.
131    pub diagnostics: Vec<Diagnostic>,
132}
133
134impl ParseOutcome {
135    /// Whether anything was dropped while reading.
136    #[must_use]
137    pub fn is_lossless(&self) -> bool {
138        self.diagnostics.is_empty()
139    }
140}