use std::fmt::Write as _;
use crate::ErrorCategory;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ErrorCode(pub &'static str);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CategoryTag(pub ErrorCategory);
#[diagnostic::on_unimplemented(
message = "error types must implement `Coded` — define the type with `error!` to get it automatically",
note = "`error!` generates the entry, code, and category accessors"
)]
pub trait Coded {
fn code(&self) -> &'static str;
fn category(&self) -> ErrorCategory;
fn advice(&self) -> Option<&'static str> {
None
}
}
#[derive(Debug, Clone)]
pub struct ErrorRegistryEntry {
pub code: &'static str,
pub name: &'static str,
pub category: ErrorCategory,
pub display: &'static str,
pub advice: Option<&'static str>,
pub action: Option<&'static str>,
pub module: &'static str,
}
#[cfg(not(target_family = "wasm"))]
#[linkme::distributed_slice]
pub static ERROR_REGISTRY: [ErrorRegistryEntry];
#[cfg(target_family = "wasm")]
pub static ERROR_REGISTRY: [ErrorRegistryEntry; 0] = [];
#[cfg(target_family = "wasm")]
static STATIC_REGISTRY: std::sync::LazyLock<
parking_lot::RwLock<Vec<&'static [ErrorRegistryEntry]>>,
> = std::sync::LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
pub fn register_statics(entries: &'static [ErrorRegistryEntry]) {
#[cfg(target_family = "wasm")]
STATIC_REGISTRY.write().push(entries);
#[cfg(not(target_family = "wasm"))]
let _ = entries;
}
pub fn error_registry() -> impl Iterator<Item = &'static ErrorRegistryEntry> {
#[cfg(not(target_family = "wasm"))]
{
ERROR_REGISTRY.iter()
}
#[cfg(target_family = "wasm")]
{
ERROR_REGISTRY
.iter()
.chain(STATIC_REGISTRY.read().clone().into_iter().flatten())
}
}
#[must_use]
pub fn lookup_error(code: &str) -> Option<&'static ErrorRegistryEntry> {
error_registry().find(|e| e.code == code)
}
fn similar_codes(query: &str) -> Vec<&'static ErrorRegistryEntry> {
let mut matches: Vec<&'static ErrorRegistryEntry> = error_registry()
.filter(|entry| entry.code.starts_with(query))
.collect();
matches.sort_by(|a, b| a.code.cmp(b.code));
matches
}
#[must_use]
pub fn doctor(code: &str) -> Option<String> {
let Some(entry) = lookup_error(code) else {
let similar = similar_codes(code);
if similar.is_empty() {
return None;
}
let mut out = format!("no exact match for {code:?} — similar codes:");
for entry in similar {
let _ = write!(out, "\n{}: {} ({})", entry.code, entry.name, entry.category);
}
return Some(out);
};
let mut out = format!(
"code: {}\nname: {}\ncategory: {}\npolicy: {}\ndisplay: {}",
entry.code,
entry.name,
entry.category,
entry.category.policy().advice_line(),
entry.display,
);
if let Some(advice) = entry.advice {
let _ = write!(out, "\nadvice: {advice}");
}
if let Some(action) = entry.action {
let _ = write!(out, "\naction: {action}");
}
let _ = write!(out, "\nmodule: {}", entry.module);
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn similar_codes_matches_manual_filter_and_is_sorted() {
for query in ["E", "E1", "zz-no-such-prefix"] {
let expected: Vec<&'static str> = error_registry()
.filter(|entry| entry.code.starts_with(query))
.map(|entry| entry.code)
.collect();
let actual: Vec<&'static str> = similar_codes(query)
.iter()
.map(|entry| entry.code)
.collect();
assert!(actual.windows(2).all(|w| w[0] <= w[1]));
assert_eq!(actual, expected);
}
}
#[test]
fn doctor_unknown_code_with_no_prefix_match_is_none() {
assert!(similar_codes("\0not-a-code").is_empty());
assert!(doctor("\0not-a-code").is_none());
}
#[test]
fn doctor_prefix_miss_lists_similar_codes_sorted() {
let query = "E";
if lookup_error(query).is_some() {
return;
}
let similar = similar_codes(query);
match doctor(query) {
None => assert!(similar.is_empty()),
Some(report) => {
assert_eq!(
report.lines().next(),
Some("no exact match for \"E\" — similar codes:"),
"report:\n{report}"
);
let lines: Vec<&str> = report.lines().skip(1).collect();
assert_eq!(lines.len(), similar.len(), "report:\n{report}");
for (line, entry) in lines.iter().zip(&similar) {
assert_eq!(
*line,
format!("{}: {} ({})", entry.code, entry.name, entry.category)
);
}
}
}
}
}