assay_core/errors/
diagnostic.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct Diagnostic {
5    pub code: String,
6    pub severity: String,
7    pub source: String,
8    pub message: String,
9    pub context: serde_json::Value,
10    pub fix_steps: Vec<String>,
11}
12
13impl Diagnostic {
14    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
15        Self {
16            code: code.into(),
17            severity: "error".into(), // Default to error
18            source: "unknown".into(),
19            message: message.into(),
20            context: serde_json::json!({}),
21            fix_steps: vec![],
22        }
23    }
24
25    pub fn with_severity(mut self, severity: impl Into<String>) -> Self {
26        self.severity = severity.into();
27        self
28    }
29
30    pub fn with_source(mut self, source: impl Into<String>) -> Self {
31        self.source = source.into();
32        self
33    }
34
35    pub fn with_context(mut self, context: serde_json::Value) -> Self {
36        self.context = context;
37        self
38    }
39
40    pub fn with_fix_step(mut self, step: impl Into<String>) -> Self {
41        self.fix_steps.push(step.into());
42        self
43    }
44
45    pub fn format_terminal(&self) -> String {
46        let icon = if self.severity == "warn" {
47            "⚠️ "
48        } else {
49            "❌"
50        };
51        let mut s = format!("{} [{}] {}\n", icon, self.code, self.message);
52        s.push_str(&format!("  source: {}\n", self.source));
53
54        // Simple pretty print for context if not empty object
55        if !self.context.is_null() && self.context.as_object().is_some_and(|o| !o.is_empty()) {
56            if let Ok(json) = serde_json::to_string_pretty(&self.context) {
57                // Indent context
58                for line in json.lines() {
59                    s.push_str(&format!("  {}\n", line));
60                }
61            }
62        }
63
64        if !self.fix_steps.is_empty() {
65            s.push_str("\nFix:\n");
66            for (i, step) in self.fix_steps.iter().enumerate() {
67                s.push_str(&format!("  {}. {}\n", i + 1, step));
68            }
69        }
70        s
71    }
72
73    pub fn format_plain(&self) -> String {
74        // Strip out any ansi codes if we added them (we didn't yet), just text
75        self.format_terminal()
76    }
77}
78
79impl std::fmt::Display for Diagnostic {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        write!(f, "{}", self.format_terminal())
82    }
83}
84
85impl std::error::Error for Diagnostic {}
86
87// Common error codes
88pub mod codes {
89    // Errors (Exit 2)
90    pub const E_CFG_PARSE: &str = "E_CFG_PARSE";
91    pub const E_CFG_SCHEMA: &str = "E_CFG_SCHEMA";
92    pub const E_PATH_NOT_FOUND: &str = "E_PATH_NOT_FOUND";
93    pub const E_TRACE_MISS: &str = "E_TRACE_MISS";
94    pub const E_TRACE_INVALID: &str = "E_TRACE_INVALID";
95    pub const E_BASE_MISMATCH: &str = "E_BASE_MISMATCH";
96    pub const E_REPLAY_STRICT_MISSING: &str = "E_REPLAY_STRICT_MISSING";
97    pub const E_EMB_DIMS: &str = "E_EMB_DIMS";
98
99    // Warnings (Exit 0)
100    pub const W_BASE_FINGERPRINT: &str = "W_BASE_FINGERPRINT";
101    pub const W_CACHE_CONFUSION: &str = "W_CACHE_CONFUSION";
102}