canic_host/diagnostics/
mod.rs1mod generated;
8
9use canic_core::diagnostics::DiagnosticCode;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub struct DiagnosticEntry {
19 pub code: DiagnosticCode,
20 pub name: &'static str,
21 pub origin: &'static str,
22 pub summary: &'static str,
23 pub guidance: Option<&'static str>,
24}
25
26impl DiagnosticEntry {
27 pub(super) const fn new(
28 code: u16,
29 name: &'static str,
30 origin: &'static str,
31 summary: &'static str,
32 guidance: Option<&'static str>,
33 ) -> Self {
34 Self {
35 code: DiagnosticCode::from_raw(code),
36 name,
37 origin,
38 summary,
39 guidance,
40 }
41 }
42}
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub struct RetiredDiagnosticEntry {
52 pub code: DiagnosticCode,
53 pub name: &'static str,
54}
55
56impl RetiredDiagnosticEntry {
57 #[cfg_attr(
58 not(test),
59 expect(
60 dead_code,
61 reason = "the initial unreleased register has no retired rows"
62 )
63 )]
64 pub(super) const fn new(code: u16, name: &'static str) -> Self {
65 Self {
66 code: DiagnosticCode::from_raw(code),
67 name,
68 }
69 }
70}
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum DiagnosticLookup<'a> {
80 Current(&'a DiagnosticEntry),
81 Retired(&'a RetiredDiagnosticEntry),
82 Unknown(DiagnosticCode),
83}
84
85#[derive(Debug)]
92pub struct DiagnosticCatalog {
93 current: &'static [DiagnosticEntry],
94 retired: &'static [RetiredDiagnosticEntry],
95}
96
97impl DiagnosticCatalog {
98 const fn new(
99 current: &'static [DiagnosticEntry],
100 retired: &'static [RetiredDiagnosticEntry],
101 ) -> Self {
102 Self { current, retired }
103 }
104
105 #[must_use]
107 pub fn lookup(&self, code: DiagnosticCode) -> DiagnosticLookup<'_> {
108 if let Ok(index) = self
109 .current
110 .binary_search_by_key(&code.raw(), |entry| entry.code.raw())
111 {
112 return DiagnosticLookup::Current(&self.current[index]);
113 }
114 if let Ok(index) = self
115 .retired
116 .binary_search_by_key(&code.raw(), |entry| entry.code.raw())
117 {
118 return DiagnosticLookup::Retired(&self.retired[index]);
119 }
120 DiagnosticLookup::Unknown(code)
121 }
122
123 #[must_use]
125 pub fn current_entries(&self) -> impl ExactSizeIterator<Item = &DiagnosticEntry> {
126 self.current.iter()
127 }
128
129 #[must_use]
131 pub fn retired_entries(&self) -> impl ExactSizeIterator<Item = &RetiredDiagnosticEntry> {
132 self.retired.iter()
133 }
134}
135
136static CATALOG: DiagnosticCatalog =
137 DiagnosticCatalog::new(generated::CURRENT_REASONS, generated::RETIRED_REASONS);
138
139#[must_use]
141pub const fn diagnostic_catalog() -> &'static DiagnosticCatalog {
142 &CATALOG
143}
144
145#[must_use]
147pub fn lookup_diagnostic(code: DiagnosticCode) -> DiagnosticLookup<'static> {
148 diagnostic_catalog().lookup(code)
149}
150
151#[must_use]
153pub fn render_diagnostic(code: DiagnosticCode) -> String {
154 match lookup_diagnostic(code) {
155 DiagnosticLookup::Current(entry) => {
156 let mut rendered = format!(
157 "{} {} (origin: {}): {}",
158 entry.code, entry.name, entry.origin, entry.summary
159 );
160 if let Some(guidance) = entry.guidance {
161 rendered.push(' ');
162 rendered.push_str(guidance);
163 }
164 rendered
165 }
166 DiagnosticLookup::Retired(entry) => {
167 format!("{} (retired diagnostic: {})", entry.code, entry.name)
168 }
169 DiagnosticLookup::Unknown(code) => format!("{code} (unknown diagnostic)"),
170 }
171}
172
173#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn generated_catalogue_is_sorted_unique_and_complete() {
182 let catalog = diagnostic_catalog();
183 let current_codes = catalog
184 .current_entries()
185 .map(|entry| entry.code.raw())
186 .collect::<Vec<_>>();
187 let retired_codes = catalog
188 .retired_entries()
189 .map(|entry| entry.code.raw())
190 .collect::<Vec<_>>();
191 let current_names = catalog
192 .current_entries()
193 .map(|entry| entry.name)
194 .collect::<Vec<_>>();
195 let retired_names = catalog
196 .retired_entries()
197 .map(|entry| entry.name)
198 .collect::<Vec<_>>();
199 let entry_count = current_codes.len() + retired_codes.len();
200 let unique_codes = current_codes
201 .iter()
202 .chain(&retired_codes)
203 .copied()
204 .collect::<std::collections::BTreeSet<_>>();
205 let unique_names = current_names
206 .iter()
207 .chain(&retired_names)
208 .copied()
209 .collect::<std::collections::BTreeSet<_>>();
210
211 assert_eq!(unique_codes.len(), entry_count);
212 assert_eq!(unique_names.len(), entry_count);
213 assert!(unique_codes.iter().all(|code| *code != 0));
214 assert!(current_codes.windows(2).all(|pair| pair[0] < pair[1]));
215 assert!(retired_codes.windows(2).all(|pair| pair[0] < pair[1]));
216 }
217
218 #[test]
219 fn lookup_distinguishes_current_retired_and_unknown_codes() {
220 static RETIRED: &[RetiredDiagnosticEntry] =
221 &[RetiredDiagnosticEntry::new(23, "FORMER_IDENTITY")];
222
223 let current = diagnostic_catalog().lookup(DiagnosticCode::from_raw(1));
224 assert!(
225 matches!(current, DiagnosticLookup::Current(entry) if entry.name == "ACCESS_UNAVAILABLE")
226 );
227
228 let fixture = DiagnosticCatalog::new(&[], RETIRED);
229 let retired = fixture.lookup(DiagnosticCode::from_raw(23));
230 assert!(
231 matches!(retired, DiagnosticLookup::Retired(entry) if entry.name == "FORMER_IDENTITY")
232 );
233
234 let unknown = fixture.lookup(DiagnosticCode::from_raw(65_000));
235 assert_eq!(
236 unknown,
237 DiagnosticLookup::Unknown(DiagnosticCode::from_raw(65_000))
238 );
239 }
240}