canic_host/diagnostics/
mod.rs1mod generated;
8
9use canic_contracts::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 pub fn current_entries(&self) -> impl ExactSizeIterator<Item = &DiagnosticEntry> {
125 self.current.iter()
126 }
127
128 pub fn retired_entries(&self) -> impl ExactSizeIterator<Item = &RetiredDiagnosticEntry> {
130 self.retired.iter()
131 }
132}
133
134static CATALOG: DiagnosticCatalog =
135 DiagnosticCatalog::new(generated::CURRENT_REASONS, generated::RETIRED_REASONS);
136
137#[must_use]
139pub const fn diagnostic_catalog() -> &'static DiagnosticCatalog {
140 &CATALOG
141}
142
143#[must_use]
145pub fn lookup_diagnostic(code: DiagnosticCode) -> DiagnosticLookup<'static> {
146 diagnostic_catalog().lookup(code)
147}
148
149#[must_use]
151pub fn render_diagnostic(code: DiagnosticCode) -> String {
152 match lookup_diagnostic(code) {
153 DiagnosticLookup::Current(entry) => {
154 let mut rendered = format!(
155 "{} {} (origin: {}): {}",
156 entry.code, entry.name, entry.origin, entry.summary
157 );
158 if let Some(guidance) = entry.guidance {
159 rendered.push(' ');
160 rendered.push_str(guidance);
161 }
162 rendered
163 }
164 DiagnosticLookup::Retired(entry) => {
165 format!("{} (retired diagnostic: {})", entry.code, entry.name)
166 }
167 DiagnosticLookup::Unknown(code) => format!("{code} (unknown diagnostic)"),
168 }
169}
170
171#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn generated_catalogue_is_sorted_unique_and_complete() {
180 let catalog = diagnostic_catalog();
181 let current_codes = catalog
182 .current_entries()
183 .map(|entry| entry.code.raw())
184 .collect::<Vec<_>>();
185 let retired_codes = catalog
186 .retired_entries()
187 .map(|entry| entry.code.raw())
188 .collect::<Vec<_>>();
189 let current_names = catalog
190 .current_entries()
191 .map(|entry| entry.name)
192 .collect::<Vec<_>>();
193 let retired_names = catalog
194 .retired_entries()
195 .map(|entry| entry.name)
196 .collect::<Vec<_>>();
197 let entry_count = current_codes.len() + retired_codes.len();
198 let unique_codes = current_codes
199 .iter()
200 .chain(&retired_codes)
201 .copied()
202 .collect::<std::collections::BTreeSet<_>>();
203 let unique_names = current_names
204 .iter()
205 .chain(&retired_names)
206 .copied()
207 .collect::<std::collections::BTreeSet<_>>();
208
209 assert_eq!(unique_codes.len(), entry_count);
210 assert_eq!(unique_names.len(), entry_count);
211 assert!(unique_codes.iter().all(|code| *code != 0));
212 assert!(current_codes.windows(2).all(|pair| pair[0] < pair[1]));
213 assert!(retired_codes.windows(2).all(|pair| pair[0] < pair[1]));
214 }
215
216 #[test]
217 fn lookup_distinguishes_current_retired_and_unknown_codes() {
218 static RETIRED: &[RetiredDiagnosticEntry] =
219 &[RetiredDiagnosticEntry::new(23, "FORMER_IDENTITY")];
220
221 let current = diagnostic_catalog().lookup(DiagnosticCode::from_raw(1));
222 assert!(
223 matches!(current, DiagnosticLookup::Current(entry) if entry.name == "ACCESS_UNAVAILABLE")
224 );
225
226 let fixture = DiagnosticCatalog::new(&[], RETIRED);
227 let retired = fixture.lookup(DiagnosticCode::from_raw(23));
228 assert!(
229 matches!(retired, DiagnosticLookup::Retired(entry) if entry.name == "FORMER_IDENTITY")
230 );
231
232 let unknown = fixture.lookup(DiagnosticCode::from_raw(65_000));
233 assert_eq!(
234 unknown,
235 DiagnosticLookup::Unknown(DiagnosticCode::from_raw(65_000))
236 );
237 }
238}