Skip to main content

error_engine/
catalog.rs

1use crate::error::EngineError;
2use serde::Deserialize;
3use std::collections::HashMap;
4
5/// Reserved code returned whenever a diagnostic cannot be resolved normally
6/// (catalog failed to load, or the requested code has no entry).
7pub const UNKNOWN_CODE: &str = "UNK-000";
8
9#[derive(Debug, Deserialize)]
10pub struct CatalogEntry {
11    pub template: String,
12    pub hint: Option<String>,
13}
14
15#[derive(Debug, Deserialize)]
16struct RawCatalog {
17    #[serde(flatten)]
18    entries: HashMap<String, CatalogEntry>,
19}
20
21#[derive(Debug)]
22pub struct Catalog {
23    entries: HashMap<String, CatalogEntry>,
24    /// Set when `load_or_fallback` had to fall back because the catalog
25    /// itself could not be read/parsed. `None` means the catalog loaded fine.
26    load_error: Option<String>,
27}
28
29impl Catalog {
30    /// Strict loader: fails if the file is missing or invalid. Use this at
31    /// startup when you want the application to refuse to run without a
32    /// valid catalog (propagate with `?` via `anyhow` in `main`).
33    pub fn load(path: &str) -> Result<Self, EngineError> {
34        let raw = std::fs::read_to_string(path)
35            .map_err(|source| EngineError::CatalogRead { path: path.to_string(), source })?;
36        let raw: RawCatalog = toml::from_str(&raw)
37            .map_err(|source| EngineError::CatalogParse { path: path.to_string(), source })?;
38        Ok(Self { entries: raw.entries, load_error: None })
39    }
40
41    /// Infallible loader. If the catalog can't be read or parsed, returns a
42    /// `Catalog` with no entries, remembering the error so `render()` can
43    /// surface it as `UNK-000` instead of panicking. This is the loader
44    /// most applications should use.
45    pub fn load_or_fallback(path: &str) -> Self {
46        match Self::load(path) {
47            Ok(catalog) => catalog,
48            Err(err) => Self {
49                entries: HashMap::new(),
50                load_error: Some(err.summary()),
51            },
52        }
53    }
54
55    /// Never fails. Falls back to `UNK-000` in two cases:
56    /// - the catalog itself failed to load (`load_error` is set)
57    /// - the code has no entry in an otherwise valid catalog
58    pub fn render(&self, code: &str, ctx: &[(&str, String)]) -> String {
59        if let Some(load_error) = &self.load_error {
60            return format!("[{UNKNOWN_CODE}] catalog failed to load: {load_error}");
61        }
62
63        let Some(entry) = self.entries.get(code) else {
64            return format!("[{UNKNOWN_CODE}] unknown diagnostic code '{code}'");
65        };
66
67        let mut text = entry.template.clone();
68        for (key, val) in ctx {
69            text = text.replace(&format!("{{{key}}}"), val);
70        }
71        text
72    }
73
74    /// `None` in both `UNK-000` cases (catalog load failure or missing code).
75    pub fn hint(&self, code: &str) -> Option<&str> {
76        if self.load_error.is_some() {
77            return None;
78        }
79        self.entries.get(code)?.hint.as_deref()
80    }
81}