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/// The reader stopped following `Redirect`/`Compile`/`Buscoords` includes
80/// because the case exceeded the include budget. Severity `Error`: the parse
81/// continued, but the network is incomplete.
82pub const READ_DSS_INCLUDE_BUDGET: &str = "READ.DSS.INCLUDE_BUDGET";
83
84/// A BMOPF field the schema types as a number holds something else. Severity
85/// `Error`: the field reads as `NaN`, which serializes on as an unbounded
86/// limit, so the parse states a fact the source never gave.
87pub const READ_BMOPF_FIELD_NOT_A_NUMBER: &str = "READ.BMOPF.FIELD_NOT_A_NUMBER";
88
89/// One structured conversion finding.
90#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
91#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
92pub struct StructuredDiagnostic {
93    pub code: DiagnosticCode,
94    pub severity: DiagnosticSeverity,
95    pub stage: DiagnosticStage,
96    pub message: String,
97    /// JSON pointer or best effort element locator.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub element_path: Option<String>,
100    /// Code specific structured payload.
101    #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
102    pub details: serde_json::Map<String, serde_json::Value>,
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub suggested_action: Option<String>,
105    /// Workflows for which this finding is safe to ignore.
106    #[serde(default, skip_serializing_if = "Vec::is_empty")]
107    pub safe_to_ignore: Vec<String>,
108}
109
110impl StructuredDiagnostic {
111    pub fn new(
112        code: impl Into<DiagnosticCode>,
113        severity: DiagnosticSeverity,
114        stage: DiagnosticStage,
115        message: impl Into<String>,
116    ) -> Self {
117        Self {
118            code: code.into(),
119            severity,
120            stage,
121            message: message.into(),
122            element_path: None,
123            details: serde_json::Map::new(),
124            suggested_action: None,
125            safe_to_ignore: Vec::new(),
126        }
127    }
128
129    #[must_use]
130    pub fn with_element_path(mut self, path: impl Into<String>) -> Self {
131        self.element_path = Some(path.into());
132        self
133    }
134
135    #[must_use]
136    pub fn with_details(mut self, details: serde_json::Map<String, serde_json::Value>) -> Self {
137        self.details = details;
138        self
139    }
140
141    #[must_use]
142    pub fn with_suggested_action(mut self, action: impl Into<String>) -> Self {
143        self.suggested_action = Some(action.into());
144        self
145    }
146}