Skip to main content

powerio_diag/
code.rs

1//! Diagnostic codes and the stage family their first segment names.
2
3use serde::{Deserialize, Serialize};
4
5/// A stable dotted diagnostic code, e.g. `EMIT.BMOPF.TRANSFORMER_UNSUPPORTED`.
6///
7/// The grammar is `NAMESPACE.SCOPE.SPECIFIC`: uppercase ASCII letters, digits
8/// and `_` inside a segment, `.` between segments, at least three segments. A
9/// large scope may use more (`EMIT.BMOPF.TRANSFORMER.TAP_COLLAPSED`), so a
10/// consumer reads the first segment and treats the rest as opaque identity.
11///
12/// The first segment is the namespace and names the stage the finding came
13/// from; [`DiagnosticStage`] decodes it. A code carried by a document powerio
14/// did not write may use a namespace outside the ten, which is data rather than
15/// a failure.
16#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18#[serde(transparent)]
19pub struct DiagnosticCode(pub String);
20
21impl DiagnosticCode {
22    pub fn new(code: impl Into<String>) -> Self {
23        Self(code.into())
24    }
25
26    /// The leading dotted segment (the namespace), e.g. `EMIT` for
27    /// `EMIT.PSSE.FIELD_DROPPED`.
28    pub fn namespace(&self) -> &str {
29        self.0.split('.').next().unwrap_or("")
30    }
31
32    /// The stage this code names, or `None` when the namespace is outside the
33    /// ten powerio emits.
34    pub fn stage(&self) -> Option<DiagnosticStage> {
35        DiagnosticStage::from_namespace(self.namespace())
36    }
37
38    pub fn as_str(&self) -> &str {
39        &self.0
40    }
41
42    /// Whether this code matches the grammar. Nothing refuses a code that does
43    /// not; the registry gates check it so a new code cannot be minted
44    /// malformed.
45    pub fn is_well_formed(&self) -> bool {
46        code_is_well_formed(&self.0)
47    }
48}
49
50/// Whether `code` matches `[A-Z][A-Z0-9_]*(\.[A-Z0-9_]+)+` with at least three
51/// segments.
52#[must_use]
53pub fn code_is_well_formed(code: &str) -> bool {
54    let mut segments = 0usize;
55    for (i, segment) in code.split('.').enumerate() {
56        segments += 1;
57        if segment.is_empty() {
58            return false;
59        }
60        if i == 0 && !segment.starts_with(|c: char| c.is_ascii_uppercase()) {
61            return false;
62        }
63        if !segment
64            .bytes()
65            .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_')
66        {
67            return false;
68        }
69    }
70    segments >= 3
71}
72
73impl std::fmt::Display for DiagnosticCode {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.write_str(&self.0)
76    }
77}
78
79impl From<&str> for DiagnosticCode {
80    fn from(s: &str) -> Self {
81        Self(s.to_owned())
82    }
83}
84
85impl From<String> for DiagnosticCode {
86    fn from(s: String) -> Self {
87        Self(s)
88    }
89}
90
91/// The stage a finding came from, decoded from the first segment of its code.
92///
93/// `PARSE` is source text or bytes that could not be decoded; `READ` is decoded
94/// input that could not be represented in the model, plus read side I/O.
95/// `CANONICALIZE` is normalization, `VALIDATE` is the document's own internal
96/// consistency, `LOWER` is a transformation between model families, `BUILD` is
97/// assembling a derived object (an index, a matrix, a solver table) from a
98/// network that already parsed. `EMIT` is serialization and write side I/O.
99/// `BIND` is the language boundary itself, `PARTNER` is a partner tool, and
100/// `REQUEST` is a call naming something powerio does not provide.
101///
102/// A failure detectable from an argument's representation alone is `BIND`; one
103/// that needs powerio's own vocabulary to detect is `REQUEST`.
104#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
106#[serde(rename_all = "snake_case")]
107#[non_exhaustive]
108pub enum DiagnosticStage {
109    Parse,
110    Read,
111    Canonicalize,
112    Validate,
113    Lower,
114    Build,
115    Emit,
116    Bind,
117    Partner,
118    Request,
119}
120
121impl DiagnosticStage {
122    /// Every stage, in pipeline order.
123    pub const ALL: [DiagnosticStage; 10] = [
124        DiagnosticStage::Parse,
125        DiagnosticStage::Read,
126        DiagnosticStage::Canonicalize,
127        DiagnosticStage::Validate,
128        DiagnosticStage::Lower,
129        DiagnosticStage::Build,
130        DiagnosticStage::Emit,
131        DiagnosticStage::Bind,
132        DiagnosticStage::Partner,
133        DiagnosticStage::Request,
134    ];
135
136    /// Every namespace powerio emits, for a consumer that wants the set without
137    /// hardcoding it. A code whose first segment is outside this set was
138    /// written by someone else.
139    pub const NAMESPACES: [&'static str; 10] = [
140        "PARSE",
141        "READ",
142        "CANONICALIZE",
143        "VALIDATE",
144        "LOWER",
145        "BUILD",
146        "EMIT",
147        "BIND",
148        "PARTNER",
149        "REQUEST",
150    ];
151
152    /// The namespace segment this stage owns, e.g. `EMIT`.
153    #[must_use]
154    pub fn namespace(self) -> &'static str {
155        match self {
156            DiagnosticStage::Parse => "PARSE",
157            DiagnosticStage::Read => "READ",
158            DiagnosticStage::Canonicalize => "CANONICALIZE",
159            DiagnosticStage::Validate => "VALIDATE",
160            DiagnosticStage::Lower => "LOWER",
161            DiagnosticStage::Build => "BUILD",
162            DiagnosticStage::Emit => "EMIT",
163            DiagnosticStage::Bind => "BIND",
164            DiagnosticStage::Partner => "PARTNER",
165            DiagnosticStage::Request => "REQUEST",
166        }
167    }
168
169    /// The stage a namespace segment names, or `None` for a namespace outside
170    /// the ten.
171    #[must_use]
172    pub fn from_namespace(namespace: &str) -> Option<Self> {
173        Self::ALL.into_iter().find(|s| s.namespace() == namespace)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn the_grammar_accepts_shipped_codes_and_refuses_malformed_ones() {
183        for code in [
184            "EMIT.BMOPF.TRANSFORMER_UNSUPPORTED",
185            "READ.DSS.INCLUDE_REFUSED",
186            "LOWER.MULTI_TO_BALANCED.UNKNOWN_BUS",
187            "EMIT.BMOPF.TRANSFORMER.TAP_COLLAPSED",
188            "VALIDATE.PACKAGE.OPERATING_IDENTITY",
189        ] {
190            assert!(code_is_well_formed(code), "{code}");
191        }
192        for code in [
193            "",
194            "EMIT",
195            "READ.PACKAGE",
196            "read.dss.include_refused",
197            "READ..INCLUDE_REFUSED",
198            "READ.DSS.INCLUDE REFUSED",
199            "READ.DSS.INCLUDE-REFUSED",
200            "1READ.DSS.INCLUDE_REFUSED",
201            "READ.DSS.INCLUDE_REFUSED.",
202        ] {
203            assert!(!code_is_well_formed(code), "{code}");
204        }
205    }
206
207    #[test]
208    fn every_namespace_decodes_to_its_stage_and_back() {
209        assert_eq!(
210            DiagnosticStage::ALL.len(),
211            DiagnosticStage::NAMESPACES.len()
212        );
213        for (stage, namespace) in DiagnosticStage::ALL
214            .into_iter()
215            .zip(DiagnosticStage::NAMESPACES)
216        {
217            assert_eq!(stage.namespace(), namespace);
218            assert_eq!(DiagnosticStage::from_namespace(namespace), Some(stage));
219        }
220        assert_eq!(DiagnosticStage::from_namespace("FIDELITY"), None);
221    }
222
223    #[test]
224    fn a_code_reports_the_stage_of_its_first_segment() {
225        let code = DiagnosticCode::new("EMIT.PSSE.FIELD_DROPPED");
226        assert_eq!(code.namespace(), "EMIT");
227        assert_eq!(code.stage(), Some(DiagnosticStage::Emit));
228        assert_eq!(DiagnosticCode::new("E.PSSE.DROPPED").stage(), None);
229    }
230
231    #[test]
232    fn a_stage_serializes_as_its_lowercase_token() {
233        let json = serde_json::to_string(&DiagnosticStage::Request).unwrap();
234        assert_eq!(json, "\"request\"");
235        assert_eq!(
236            serde_json::from_str::<DiagnosticStage>("\"build\"").unwrap(),
237            DiagnosticStage::Build
238        );
239    }
240}