use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Verdict {
Unavailable,
Insufficient,
Certified,
}
impl Verdict {
pub fn is_certified(self) -> bool {
matches!(self, Verdict::Certified)
}
pub fn label(self) -> &'static str {
match self {
Verdict::Unavailable => "unavailable",
Verdict::Insufficient => "insufficient",
Verdict::Certified => "certified",
}
}
pub fn meet(self, other: Verdict) -> Verdict {
self.min(other)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Claim {
pub id: &'static str,
pub statement: String,
}
impl Claim {
pub fn new(id: &'static str, statement: impl Into<String>) -> Self {
Self {
id,
statement: statement.into(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum EvidenceValue {
Scalar(f64),
Integer(i64),
Flag(bool),
Text(String),
Vector(Vec<f64>),
}
impl From<f64> for EvidenceValue {
fn from(v: f64) -> Self {
EvidenceValue::Scalar(v)
}
}
impl From<usize> for EvidenceValue {
fn from(v: usize) -> Self {
EvidenceValue::Integer(v as i64)
}
}
impl From<i64> for EvidenceValue {
fn from(v: i64) -> Self {
EvidenceValue::Integer(v)
}
}
impl From<bool> for EvidenceValue {
fn from(v: bool) -> Self {
EvidenceValue::Flag(v)
}
}
impl From<String> for EvidenceValue {
fn from(v: String) -> Self {
EvidenceValue::Text(v)
}
}
impl From<&str> for EvidenceValue {
fn from(v: &str) -> Self {
EvidenceValue::Text(v.to_string())
}
}
impl From<Vec<f64>> for EvidenceValue {
fn from(v: Vec<f64>) -> Self {
EvidenceValue::Vector(v)
}
}
pub type Evidence = BTreeMap<&'static str, EvidenceValue>;
pub trait Certificate {
fn claim(&self) -> Claim;
fn evidence(&self) -> Evidence;
fn verdict(&self) -> Verdict;
fn ledger_entry(&self) -> LedgerEntry {
LedgerEntry {
claim: self.claim(),
evidence: self.evidence(),
verdict: self.verdict(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LedgerEntry {
pub claim: Claim,
pub evidence: Evidence,
pub verdict: Verdict,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CertificateLedger {
entries: BTreeMap<&'static str, LedgerEntry>,
}
impl CertificateLedger {
pub fn new() -> Self {
Self::default()
}
pub fn record<C: Certificate>(&mut self, certificate: &C) {
self.record_entry(certificate.ledger_entry());
}
pub fn record_entry(&mut self, entry: LedgerEntry) {
match self.entries.get(entry.claim.id) {
Some(existing) if existing.verdict <= entry.verdict => {
}
_ => {
self.entries.insert(entry.claim.id, entry);
}
}
}
pub fn verdict_of(&self, claim_id: &str) -> Verdict {
self.entries
.get(claim_id)
.map(|e| e.verdict)
.unwrap_or(Verdict::Unavailable)
}
pub fn entries(&self) -> impl Iterator<Item = &LedgerEntry> {
self.entries.values()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn overall(&self) -> Verdict {
self.entries
.values()
.map(|e| e.verdict)
.fold(Verdict::Certified, Verdict::meet)
.min(if self.entries.is_empty() {
Verdict::Unavailable
} else {
Verdict::Certified
})
}
}
#[cfg(test)]
mod tests {
use super::*;
struct FakeCert {
id: &'static str,
verdict: Verdict,
}
impl Certificate for FakeCert {
fn claim(&self) -> Claim {
Claim::new(self.id, "fake claim")
}
fn evidence(&self) -> Evidence {
let mut e = Evidence::new();
e.insert("x", 1.0.into());
e
}
fn verdict(&self) -> Verdict {
self.verdict
}
}
#[test]
fn verdict_ladder_orders_weakest_to_strongest() {
assert!(Verdict::Unavailable < Verdict::Insufficient);
assert!(Verdict::Insufficient < Verdict::Certified);
assert!(!Verdict::Insufficient.is_certified());
assert!(!Verdict::Unavailable.is_certified());
assert!(Verdict::Certified.is_certified());
}
#[test]
fn meet_is_conservative() {
assert_eq!(
Verdict::Certified.meet(Verdict::Insufficient),
Verdict::Insufficient
);
assert_eq!(
Verdict::Insufficient.meet(Verdict::Unavailable),
Verdict::Unavailable
);
assert_eq!(
Verdict::Certified.meet(Verdict::Certified),
Verdict::Certified
);
}
#[test]
fn absent_claim_reads_as_unavailable_never_pass() {
let ledger = CertificateLedger::new();
assert_eq!(ledger.verdict_of("nonexistent"), Verdict::Unavailable);
assert!(!ledger.verdict_of("nonexistent").is_certified());
assert_eq!(ledger.overall(), Verdict::Unavailable);
}
#[test]
fn overall_is_weakest_member() {
let mut ledger = CertificateLedger::new();
ledger.record(&FakeCert {
id: "a",
verdict: Verdict::Certified,
});
ledger.record(&FakeCert {
id: "b",
verdict: Verdict::Insufficient,
});
assert_eq!(ledger.overall(), Verdict::Insufficient);
assert!(!ledger.overall().is_certified());
}
#[test]
fn duplicate_record_keeps_weaker_verdict() {
let mut ledger = CertificateLedger::new();
ledger.record(&FakeCert {
id: "a",
verdict: Verdict::Certified,
});
ledger.record(&FakeCert {
id: "a",
verdict: Verdict::Insufficient,
});
assert_eq!(ledger.verdict_of("a"), Verdict::Insufficient);
assert_eq!(ledger.len(), 1);
}
}