Skip to main content

powerio_dist/
diagnostics.rs

1//! Structured diagnostics for distribution conversions.
2//!
3//! This mirrors the `.pio.json` diagnostic shape without depending on
4//! `powerio-pkg`, which already depends on this crate.
5
6use serde::{Deserialize, Serialize};
7
8/// A stable dotted diagnostic code, e.g. `EMIT.BMOPF.TRANSFORMER_UNSUPPORTED`.
9#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
11#[serde(transparent)]
12pub struct DiagnosticCode(pub String);
13
14impl DiagnosticCode {
15    pub fn new(code: impl Into<String>) -> Self {
16        Self(code.into())
17    }
18
19    pub fn namespace(&self) -> &str {
20        self.0.split('.').next().unwrap_or("")
21    }
22
23    pub fn as_str(&self) -> &str {
24        &self.0
25    }
26}
27
28impl std::fmt::Display for DiagnosticCode {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        f.write_str(&self.0)
31    }
32}
33
34impl From<&str> for DiagnosticCode {
35    fn from(s: &str) -> Self {
36        Self(s.to_owned())
37    }
38}
39
40impl From<String> for DiagnosticCode {
41    fn from(s: String) -> Self {
42        Self(s)
43    }
44}
45
46/// Severity, ordered worst last.
47#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
48#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
49#[serde(rename_all = "snake_case")]
50pub enum DiagnosticSeverity {
51    Debug,
52    Info,
53    Warning,
54    Error,
55    Fatal,
56}
57
58/// The conversion stage that emitted a diagnostic.
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
60#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
61#[serde(rename_all = "snake_case")]
62#[non_exhaustive]
63pub enum DiagnosticStage {
64    Parse,
65    Read,
66    Canonicalize,
67    Validate,
68    Lower,
69    Emit,
70    Bind,
71    Partner,
72}
73
74/// A `Redirect`/`Compile`/`Buscoords` include the reader refused because it
75/// escapes the case directory. Severity `Error`: the parse continued, but
76/// the network is incomplete.
77pub const READ_DSS_INCLUDE_REFUSED: &str = "READ.DSS.INCLUDE_REFUSED";
78
79/// A BMOPF field the schema types as a number holds something else. Severity
80/// `Error`: the field reads as `NaN`, which serializes on as an unbounded
81/// limit, so the parse states a fact the source never gave.
82pub const READ_BMOPF_FIELD_NOT_A_NUMBER: &str = "READ.BMOPF.FIELD_NOT_A_NUMBER";
83
84/// One structured conversion finding.
85#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
86#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
87pub struct StructuredDiagnostic {
88    pub code: DiagnosticCode,
89    pub severity: DiagnosticSeverity,
90    pub stage: DiagnosticStage,
91    pub message: String,
92    /// JSON pointer or best effort element locator.
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub element_path: Option<String>,
95    /// Code specific structured payload.
96    #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
97    pub details: serde_json::Map<String, serde_json::Value>,
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub suggested_action: Option<String>,
100    /// Workflows for which this finding is safe to ignore.
101    #[serde(default, skip_serializing_if = "Vec::is_empty")]
102    pub safe_to_ignore: Vec<String>,
103}
104
105impl StructuredDiagnostic {
106    pub fn new(
107        code: impl Into<DiagnosticCode>,
108        severity: DiagnosticSeverity,
109        stage: DiagnosticStage,
110        message: impl Into<String>,
111    ) -> Self {
112        Self {
113            code: code.into(),
114            severity,
115            stage,
116            message: message.into(),
117            element_path: None,
118            details: serde_json::Map::new(),
119            suggested_action: None,
120            safe_to_ignore: Vec::new(),
121        }
122    }
123
124    #[must_use]
125    pub fn with_element_path(mut self, path: impl Into<String>) -> Self {
126        self.element_path = Some(path.into());
127        self
128    }
129
130    #[must_use]
131    pub fn with_details(mut self, details: serde_json::Map<String, serde_json::Value>) -> Self {
132        self.details = details;
133        self
134    }
135
136    #[must_use]
137    pub fn with_suggested_action(mut self, action: impl Into<String>) -> Self {
138        self.suggested_action = Some(action.into());
139        self
140    }
141}