1use serde::{Deserialize, Serialize};
4
5#[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 pub fn namespace(&self) -> &str {
29 self.0.split('.').next().unwrap_or("")
30 }
31
32 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 pub fn is_well_formed(&self) -> bool {
46 code_is_well_formed(&self.0)
47 }
48}
49
50#[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#[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 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 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 #[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 #[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}