#![allow(clippy::panic)]
use serde::Deserialize;
use std::collections::BTreeMap;
use std::sync::OnceLock;
const PROSE_TOML: &str = include_str!("../prose.toml");
#[derive(Debug, Deserialize)]
pub struct Prose {
rules: BTreeMap<String, RuleProse>,
}
#[derive(Debug, Deserialize)]
pub struct RuleProse {
pub severity_rationale: String,
#[serde(default)]
pub likely_cause: Option<String>,
#[serde(default)]
pub likely_cause_template: Option<String>,
#[serde(default)]
pub likely_cause_template_with_field: Option<String>,
#[serde(default)]
pub likely_cause_template_no_field: Option<String>,
pub hypotheses: Vec<String>,
pub unknowns: Vec<String>,
pub next_steps: Vec<String>,
pub escalation_note: String,
}
impl Prose {
pub fn rule(&self, name: &str) -> &RuleProse {
self.rules
.get(name)
.unwrap_or_else(|| panic!("missing prose entry for rule `{name}` in prose.toml"))
}
}
impl RuleProse {
pub fn likely_cause_static(&self) -> &str {
self.likely_cause.as_deref().unwrap_or_else(|| {
panic!(
"rule prose has no static `likely_cause`; \
use a `likely_cause_template*` accessor instead"
)
})
}
pub fn likely_cause_with_host(&self, host: &str) -> String {
self.likely_cause_template
.as_deref()
.unwrap_or_else(|| panic!("rule prose has no `likely_cause_template`"))
.replace("{host}", host)
}
pub fn likely_cause_with_peer(&self, peer: &str) -> String {
self.likely_cause_template
.as_deref()
.unwrap_or_else(|| panic!("rule prose has no `likely_cause_template`"))
.replace("{peer}", peer)
}
pub fn likely_cause_with_optional_field(&self, field: Option<&str>) -> String {
match field {
Some(f) => self
.likely_cause_template_with_field
.as_deref()
.unwrap_or_else(|| panic!("rule prose has no `likely_cause_template_with_field`"))
.replace("{field}", f),
None => self
.likely_cause_template_no_field
.as_deref()
.unwrap_or_else(|| panic!("rule prose has no `likely_cause_template_no_field`"))
.to_string(),
}
}
}
pub fn prose() -> &'static Prose {
static CACHE: OnceLock<Prose> = OnceLock::new();
CACHE.get_or_init(|| {
toml::from_str(PROSE_TOML).unwrap_or_else(|e| panic!("prose.toml is malformed: {e}"))
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic, clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn prose_parses() {
let _ = prose();
}
enum TemplateKind {
Static,
Host,
Peer,
OptionalField,
}
const TEMPLATE_BINDINGS: &[(&str, TemplateKind)] = &[
("dns_failure", TemplateKind::Host),
("tls_failure", TemplateKind::Peer),
("connection_timeout", TemplateKind::Static),
("webhook_signature", TemplateKind::Static),
("rate_limit", TemplateKind::Static),
("auth_missing", TemplateKind::Static),
("bad_payload", TemplateKind::OptionalField),
("unknown", TemplateKind::Static),
];
#[test]
fn every_rule_named_in_diagnose_has_prose() {
let p = prose();
for (rule, _) in TEMPLATE_BINDINGS {
let _ = p.rule(rule);
}
}
#[test]
fn every_rule_template_kind_matches_its_call_site() {
let p = prose();
for (rule, kind) in TEMPLATE_BINDINGS {
let entry = p.rule(rule);
let rendered = match kind {
TemplateKind::Static => entry.likely_cause_static().to_string(),
TemplateKind::Host => entry.likely_cause_with_host("example.test"),
TemplateKind::Peer => entry.likely_cause_with_peer("example.test"),
TemplateKind::OptionalField => {
let with = entry.likely_cause_with_optional_field(Some("field_name"));
let without = entry.likely_cause_with_optional_field(None);
assert!(!with.is_empty(), "{rule}: with-field template empty");
assert!(!without.is_empty(), "{rule}: no-field template empty");
with
}
};
assert!(!rendered.is_empty(), "{rule}: rendered likely_cause empty");
}
}
}