Skip to main content

molframe_core/diagnostic/
code.rs

1//! Stable diagnostic codes and the two axes that govern them.
2//!
3//! Codes are stable forever. Users grep for them and scripts branch on them, so
4//! a withdrawn code is retired rather than reused. The number's leading digit
5//! names the class of problem, which lets a caller filter for, say, every
6//! conversion loss without enumerating individual codes.
7//!
8//! Severity and strictness are separate axes on purpose. Severity says how bad a
9//! finding is; strictness says how the caller wants to react to it. Keeping them
10//! apart is what lets one implementation serve a curation pipeline that must
11//! reject anything irregular and an analysis script that has to cope with what
12//! the archive actually contains.
13
14use std::fmt;
15
16/// Whether a code denotes a failure or an observation.
17#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
18pub enum Kind {
19    /// An error: rendered `MOLFRAME-Ennnn`.
20    Error,
21    /// A warning: rendered `MOLFRAME-Wnnnn`.
22    Warning,
23}
24
25impl Kind {
26    const fn letter(self) -> char {
27        match self {
28            Self::Error => 'E',
29            Self::Warning => 'W',
30        }
31    }
32}
33
34/// How bad a finding is, independent of how the caller wants to react.
35///
36/// Ordered so that greater is worse, which makes "at least this bad" a
37/// comparison and makes sorting diagnostics worst-first a plain sort.
38#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
39pub enum Severity {
40    /// Worth recording, and nothing more.
41    Info,
42    /// Unusual, but probably what the depositor intended.
43    Loose,
44    /// Violates the specification; usually still interpretable.
45    Strict,
46    /// The result computed from this would be wrong.
47    Invalidating,
48    /// The data cannot be interpreted at all.
49    Breaking,
50}
51
52/// How much irregularity the caller is willing to accept.
53#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
54pub enum Strictness {
55    /// Anything violating the specification is an error.
56    Strict,
57    /// Anything that would make the result wrong is an error.
58    #[default]
59    Medium,
60    /// Only uninterpretable data is an error.
61    Loose,
62}
63
64impl Severity {
65    /// Returns true when a finding of this severity is an error under
66    /// `strictness`.
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use molframe_core::{Severity, Strictness};
72    ///
73    /// assert!(Severity::Strict.is_error(Strictness::Strict));
74    /// assert!(!Severity::Strict.is_error(Strictness::Medium));
75    /// assert!(Severity::Breaking.is_error(Strictness::Loose));
76    /// ```
77    #[must_use]
78    pub const fn is_error(self, strictness: Strictness) -> bool {
79        let floor = match strictness {
80            Strictness::Strict => Self::Strict,
81            Strictness::Medium => Self::Invalidating,
82            Strictness::Loose => Self::Breaking,
83        };
84        (self as u8) >= (floor as u8)
85    }
86
87    /// The word used when rendering a finding of this severity.
88    #[must_use]
89    pub const fn label(self) -> &'static str {
90        match self {
91            Self::Info => "note",
92            Self::Loose | Self::Strict => "warning",
93            Self::Invalidating | Self::Breaking => "error",
94        }
95    }
96}
97
98/// A stable diagnostic code, rendered as `MOLFRAME-E1103` or `MOLFRAME-W3011`.
99///
100/// # Examples
101///
102/// ```
103/// use molframe_core::Code;
104///
105/// assert_eq!(Code::E1103.to_string(), "MOLFRAME-E1103");
106/// assert_eq!(Code::E1103.cause(), "loop_ row count is not a multiple of the column count");
107/// assert!(!Code::E1103.remedy().is_empty());
108/// ```
109#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
110pub struct Code {
111    kind: Kind,
112    number: u16,
113}
114
115impl Code {
116    /// Creates a code from its kind and number.
117    ///
118    /// Prefer the associated constants; this exists so the registry can be
119    /// declared as data.
120    #[must_use]
121    pub const fn new(kind: Kind, number: u16) -> Self {
122        Self { kind, number }
123    }
124
125    /// Whether this code is an error or a warning.
126    #[must_use]
127    pub const fn kind(self) -> Kind {
128        self.kind
129    }
130
131    /// The numeric part of the code.
132    #[must_use]
133    pub const fn number(self) -> u16 {
134        self.number
135    }
136
137    /// The class this code belongs to, taken from its leading digit.
138    #[must_use]
139    pub const fn class(self) -> Class {
140        Class::of(self.number)
141    }
142
143    fn entry(self) -> Option<&'static Entry> {
144        let registry = super::registry::ENTRIES;
145        let found = registry
146            .binary_search_by(|entry| entry.code.cmp(&self))
147            .ok()?;
148        registry.get(found)
149    }
150
151    /// The registered severity, or [`Severity::Breaking`] for a code that is not
152    /// in the registry.
153    ///
154    /// An unregistered code can only arise from a programming mistake, so the
155    /// conservative reading is the right one.
156    #[must_use]
157    pub fn severity(self) -> Severity {
158        self.entry()
159            .map_or(Severity::Breaking, |entry| entry.severity)
160    }
161
162    /// What went wrong.
163    #[must_use]
164    pub fn cause(self) -> &'static str {
165        self.entry()
166            .map_or("unregistered diagnostic code", |entry| entry.cause)
167    }
168
169    /// What to do about it.
170    ///
171    /// Every registered code has one. A diagnostic a user cannot act on is an
172    /// incomplete diagnostic, so the remedy is part of the registry rather than
173    /// something each call site is trusted to remember.
174    #[must_use]
175    pub fn remedy(self) -> &'static str {
176        self.entry()
177            .map_or("report this as a molframe bug", |entry| entry.remedy)
178    }
179
180    /// Returns true when this code is present in the registry.
181    #[must_use]
182    pub fn is_registered(self) -> bool {
183        self.entry().is_some()
184    }
185
186    /// Every registered code, ordered by kind and then number.
187    pub fn registered() -> impl Iterator<Item = Self> {
188        super::registry::ENTRIES.iter().map(|entry| entry.code)
189    }
190}
191
192impl fmt::Display for Code {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        write!(f, "MOLFRAME-{}{:04}", self.kind.letter(), self.number)
195    }
196}
197
198impl fmt::Debug for Code {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        write!(f, "{self}")
201    }
202}
203
204/// The class of problem a code's leading digit names.
205#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
206#[non_exhaustive]
207pub enum Class {
208    /// Syntax and lexical structure.
209    Syntax,
210    /// Schema and dictionary conformance.
211    Schema,
212    /// Structural consistency of the model.
213    Consistency,
214    /// Conversion and data loss.
215    Conversion,
216    /// Geometry and numerics.
217    Geometry,
218    /// Policy and analysis contracts.
219    Policy,
220    /// Input, output and resources.
221    Resource,
222    /// An internal invariant was violated. These are bugs in molframe.
223    Internal,
224}
225
226impl Class {
227    const fn of(number: u16) -> Self {
228        match number / 1000 {
229            1 => Self::Syntax,
230            2 => Self::Schema,
231            3 => Self::Consistency,
232            4 => Self::Conversion,
233            5 => Self::Geometry,
234            6 => Self::Policy,
235            7 => Self::Resource,
236            _ => Self::Internal,
237        }
238    }
239}
240
241/// One row of the code registry.
242pub(super) struct Entry {
243    pub(super) code: Code,
244    pub(super) severity: Severity,
245    pub(super) cause: &'static str,
246    pub(super) remedy: &'static str,
247}
248
249#[cfg(test)]
250#[path = "code_tests.rs"]
251mod tests;