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;