use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct DiagnosticCode(pub String);
impl DiagnosticCode {
pub fn new(code: impl Into<String>) -> Self {
Self(code.into())
}
pub fn namespace(&self) -> &str {
self.0.split('.').next().unwrap_or("")
}
pub fn stage(&self) -> Option<DiagnosticStage> {
DiagnosticStage::from_namespace(self.namespace())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_well_formed(&self) -> bool {
code_is_well_formed(&self.0)
}
}
#[must_use]
pub fn code_is_well_formed(code: &str) -> bool {
let mut segments = 0usize;
for (i, segment) in code.split('.').enumerate() {
segments += 1;
if segment.is_empty() {
return false;
}
if i == 0 && !segment.starts_with(|c: char| c.is_ascii_uppercase()) {
return false;
}
if !segment
.bytes()
.all(|b| b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_')
{
return false;
}
}
segments >= 3
}
impl std::fmt::Display for DiagnosticCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<&str> for DiagnosticCode {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl From<String> for DiagnosticCode {
fn from(s: String) -> Self {
Self(s)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum DiagnosticStage {
Parse,
Read,
Canonicalize,
Validate,
Lower,
Build,
Emit,
Bind,
Partner,
Request,
}
impl DiagnosticStage {
pub const ALL: [DiagnosticStage; 10] = [
DiagnosticStage::Parse,
DiagnosticStage::Read,
DiagnosticStage::Canonicalize,
DiagnosticStage::Validate,
DiagnosticStage::Lower,
DiagnosticStage::Build,
DiagnosticStage::Emit,
DiagnosticStage::Bind,
DiagnosticStage::Partner,
DiagnosticStage::Request,
];
pub const NAMESPACES: [&'static str; 10] = [
"PARSE",
"READ",
"CANONICALIZE",
"VALIDATE",
"LOWER",
"BUILD",
"EMIT",
"BIND",
"PARTNER",
"REQUEST",
];
#[must_use]
pub fn namespace(self) -> &'static str {
match self {
DiagnosticStage::Parse => "PARSE",
DiagnosticStage::Read => "READ",
DiagnosticStage::Canonicalize => "CANONICALIZE",
DiagnosticStage::Validate => "VALIDATE",
DiagnosticStage::Lower => "LOWER",
DiagnosticStage::Build => "BUILD",
DiagnosticStage::Emit => "EMIT",
DiagnosticStage::Bind => "BIND",
DiagnosticStage::Partner => "PARTNER",
DiagnosticStage::Request => "REQUEST",
}
}
#[must_use]
pub fn from_namespace(namespace: &str) -> Option<Self> {
Self::ALL.into_iter().find(|s| s.namespace() == namespace)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_grammar_accepts_shipped_codes_and_refuses_malformed_ones() {
for code in [
"EMIT.BMOPF.TRANSFORMER_UNSUPPORTED",
"READ.DSS.INCLUDE_REFUSED",
"LOWER.MULTI_TO_BALANCED.UNKNOWN_BUS",
"EMIT.BMOPF.TRANSFORMER.TAP_COLLAPSED",
"VALIDATE.PACKAGE.OPERATING_IDENTITY",
] {
assert!(code_is_well_formed(code), "{code}");
}
for code in [
"",
"EMIT",
"READ.PACKAGE",
"read.dss.include_refused",
"READ..INCLUDE_REFUSED",
"READ.DSS.INCLUDE REFUSED",
"READ.DSS.INCLUDE-REFUSED",
"1READ.DSS.INCLUDE_REFUSED",
"READ.DSS.INCLUDE_REFUSED.",
] {
assert!(!code_is_well_formed(code), "{code}");
}
}
#[test]
fn every_namespace_decodes_to_its_stage_and_back() {
assert_eq!(
DiagnosticStage::ALL.len(),
DiagnosticStage::NAMESPACES.len()
);
for (stage, namespace) in DiagnosticStage::ALL
.into_iter()
.zip(DiagnosticStage::NAMESPACES)
{
assert_eq!(stage.namespace(), namespace);
assert_eq!(DiagnosticStage::from_namespace(namespace), Some(stage));
}
assert_eq!(DiagnosticStage::from_namespace("FIDELITY"), None);
}
#[test]
fn a_code_reports_the_stage_of_its_first_segment() {
let code = DiagnosticCode::new("EMIT.PSSE.FIELD_DROPPED");
assert_eq!(code.namespace(), "EMIT");
assert_eq!(code.stage(), Some(DiagnosticStage::Emit));
assert_eq!(DiagnosticCode::new("E.PSSE.DROPPED").stage(), None);
}
#[test]
fn a_stage_serializes_as_its_lowercase_token() {
let json = serde_json::to_string(&DiagnosticStage::Request).unwrap();
assert_eq!(json, "\"request\"");
assert_eq!(
serde_json::from_str::<DiagnosticStage>("\"build\"").unwrap(),
DiagnosticStage::Build
);
}
}