Skip to main content

mcd_core/
errors.rs

1//! Error and diagnostic types shared across the core crate.
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6/// Result type used by the MCD core crate.
7pub type Result<T> = std::result::Result<T, McdError>;
8
9/// A stable validation diagnostic.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct Diagnostic {
12    /// Severity level.
13    pub level: DiagnosticLevel,
14    /// Stable machine-readable code.
15    pub code: String,
16    /// Human-readable message.
17    pub message: String,
18    /// Optional source path and location.
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub source: Option<String>,
21    /// Related source paths and locations.
22    #[serde(default, skip_serializing_if = "Vec::is_empty")]
23    pub related: Vec<String>,
24}
25
26impl Diagnostic {
27    /// Construct an error diagnostic.
28    #[must_use]
29    pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
30        Self {
31            level: DiagnosticLevel::Error,
32            code: code.into(),
33            message: message.into(),
34            source: None,
35            related: Vec::new(),
36        }
37    }
38
39    /// Attach a source reference.
40    #[must_use]
41    pub fn with_source(mut self, source: impl Into<String>) -> Self {
42        self.source = Some(source.into());
43        self
44    }
45
46    /// Attach a related source reference.
47    #[must_use]
48    pub fn with_related(mut self, related: impl Into<String>) -> Self {
49        self.related.push(related.into());
50        self
51    }
52}
53
54/// Diagnostic severity.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "lowercase")]
57pub enum DiagnosticLevel {
58    /// Error severity.
59    Error,
60    /// Warning severity.
61    Warning,
62    /// Informational severity.
63    Info,
64}
65
66/// Fatal errors returned by parser operations.
67#[derive(Debug, Error)]
68pub enum McdError {
69    /// I/O failure.
70    #[error("I/O error: {0}")]
71    Io(#[from] std::io::Error),
72    /// ZIP archive failure.
73    #[error("package archive error: {0}")]
74    Zip(#[from] zip::result::ZipError),
75    /// JSON failure.
76    #[error("JSON error: {0}")]
77    Json(#[from] serde_json::Error),
78    /// UTF-8 failure.
79    #[error("UTF-8 error: {0}")]
80    Utf8(#[from] std::string::FromUtf8Error),
81    /// Structured validation failure.
82    #[error("{diagnostic_code}: {diagnostic_message}")]
83    Diagnostic {
84        /// Stable diagnostic code.
85        diagnostic_code: String,
86        /// Human-readable diagnostic message.
87        diagnostic_message: String,
88        /// Full diagnostic.
89        diagnostic: Box<Diagnostic>,
90    },
91}
92
93impl McdError {
94    /// Convert a diagnostic into a fatal error.
95    #[must_use]
96    pub fn from_diagnostic(diagnostic: Diagnostic) -> Self {
97        Self::Diagnostic {
98            diagnostic_code: diagnostic.code.clone(),
99            diagnostic_message: diagnostic.message.clone(),
100            diagnostic: Box::new(diagnostic),
101        }
102    }
103
104    /// Borrow the structured diagnostic when available.
105    #[must_use]
106    pub fn diagnostic(&self) -> Option<&Diagnostic> {
107        match self {
108            Self::Diagnostic { diagnostic, .. } => Some(diagnostic.as_ref()),
109            Self::Io(_) | Self::Zip(_) | Self::Json(_) | Self::Utf8(_) => None,
110        }
111    }
112}