use serde::{Deserialize, Serialize};
use crate::judge::why::is_cause;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RungId {
ScopeReach,
KeyParse,
RegistryDeclared,
OriginAlive,
PublisherDeclared,
StorageCoverage,
StoredValue,
SampleFreshness,
AdminAnswered,
WireHeard,
}
impl RungId {
pub const ALL: [RungId; 10] = [
RungId::ScopeReach,
RungId::KeyParse,
RungId::RegistryDeclared,
RungId::OriginAlive,
RungId::PublisherDeclared,
RungId::StorageCoverage,
RungId::StoredValue,
RungId::SampleFreshness,
RungId::AdminAnswered,
RungId::WireHeard,
];
pub fn as_str(self) -> &'static str {
match self {
RungId::ScopeReach => "scope-reach",
RungId::KeyParse => "key-parse",
RungId::RegistryDeclared => "registry-declared",
RungId::OriginAlive => "origin-alive",
RungId::PublisherDeclared => "publisher-declared",
RungId::StorageCoverage => "storage-coverage",
RungId::StoredValue => "stored-value",
RungId::SampleFreshness => "sample-freshness",
RungId::AdminAnswered => "admin-answered",
RungId::WireHeard => "wire-heard",
}
}
pub fn question(self) -> &'static str {
match self {
RungId::ScopeReach => "does a `**` explorer scope reach this key?",
RungId::KeyParse => "does it parse as a v1 key under the base?",
RungId::RegistryDeclared => "does a loaded registry slice declare it?",
RungId::OriginAlive => "is the origin on the liveliness roster?",
RungId::PublisherDeclared => "did any session declare a matching publisher?",
RungId::StorageCoverage => "is a storage configured to capture it?",
RungId::StoredValue => "does a stored value answer a bounded GET?",
RungId::SampleFreshness => "is the last known sample within its declared ttl?",
RungId::AdminAnswered => "is the admin space answering at all?",
RungId::WireHeard => "did the key speak during a listen window?",
}
}
pub fn is_cause_when_unestablished(self) -> bool {
matches!(
self,
RungId::ScopeReach
| RungId::KeyParse
| RungId::RegistryDeclared
| RungId::OriginAlive
| RungId::SampleFreshness
)
}
pub fn parse(token: &str) -> Option<RungId> {
RungId::ALL.into_iter().find(|r| r.as_str() == token)
}
}
impl std::fmt::Display for RungId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
pub type RungAnswer = crate::report::Judgement;
#[derive(Debug, Clone, Serialize)]
pub struct Rung {
pub id: RungId,
pub question: &'static str,
#[serde(flatten)]
pub answer: RungAnswer,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum WhyVerdict {
Explained,
Healthy,
Impaired,
}
impl WhyVerdict {
pub fn to_judgement(self) -> crate::report::Judgement {
use crate::report::Judgement;
match self {
WhyVerdict::Explained => Judgement::Established,
WhyVerdict::Healthy => Judgement::NotEstablished {
reason: "no cause established, and everything checked looks healthy".into(),
},
WhyVerdict::Impaired => Judgement::Unobservable {
reason: "an input the ladder wanted could not be obtained — \"healthy\" \
cannot be claimed over questions it could not ask"
.into(),
},
}
}
}
impl From<crate::report::Judgement> for WhyVerdict {
fn from(j: crate::report::Judgement) -> WhyVerdict {
use crate::report::Judgement;
match j {
Judgement::Established => WhyVerdict::Explained,
Judgement::NotEstablished { .. } => WhyVerdict::Healthy,
Judgement::NotAsked | Judgement::Unobservable { .. } => WhyVerdict::Impaired,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct WhyReport {
pub key: String,
pub base: String,
pub rungs: Vec<Rung>,
pub verdict: WhyVerdict,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub impairments: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub listened_s: Option<f64>,
}
impl WhyReport {
pub fn causes(&self) -> Vec<RungId> {
self.rungs
.iter()
.filter(|r| is_cause(r.id, &r.answer))
.map(|r| r.id)
.collect()
}
}
#[cfg(test)]
mod rung_id_tests {
use super::*;
#[test]
fn rung_ids_are_stable() {
assert_eq!(
RungId::ALL.map(RungId::as_str),
[
"scope-reach",
"key-parse",
"registry-declared",
"origin-alive",
"publisher-declared",
"storage-coverage",
"stored-value",
"sample-freshness",
"admin-answered",
"wire-heard",
]
);
}
#[test]
fn every_rung_id_has_a_question_and_round_trips() {
for id in RungId::ALL {
assert!(id.question().ends_with('?'), "{id}: {}", id.question());
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, format!("\"{}\"", id.as_str()));
assert_eq!(serde_json::from_str::<RungId>(&json).unwrap(), id);
assert_eq!(RungId::parse(id.as_str()), Some(id));
}
assert_eq!(RungId::parse("wire-herd"), None);
}
#[test]
fn exactly_five_rungs_explain_a_silence() {
let causes: Vec<&str> = RungId::ALL
.into_iter()
.filter(|r| r.is_cause_when_unestablished())
.map(RungId::as_str)
.collect();
assert_eq!(
causes,
[
"scope-reach",
"key-parse",
"registry-declared",
"origin-alive",
"sample-freshness"
]
);
}
}