1use crate::error::EngineError;
2use serde::Deserialize;
3use std::collections::HashMap;
4
5pub 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 load_error: Option<String>,
27}
28
29impl Catalog {
30 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 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 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 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}