#![allow(
clippy::disallowed_types,
reason = "the JSON carriers here are cfg(test)-only fixtures over the catalogue \
artifacts (dev/verification tooling), so #[expect] would be unfulfilled in \
the non-test build"
)]
use serde::Serialize;
use crate::ids::CapabilityName;
use crate::model::capability::CapabilityMatrix;
use crate::party::{OutcomeStatus, Results};
use crate::perf::{ClassVerdict, PerfClass};
use crate::verdict::{
Evidence, PerformanceVerdict, ProfileVerdict, SecBasicVerdict, VerdictReport, tier_members,
};
use crate::vocab::Tier;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Badge {
#[serde(rename = "schemaVersion")]
pub schema_version: u8,
pub label: String,
pub message: String,
pub color: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NamedBadge {
pub file: String,
pub badge: Badge,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CaseCounts {
pub passed: usize,
pub failed: usize,
pub errored: usize,
}
impl CaseCounts {
#[must_use]
pub fn of(results: &Results) -> Self {
let count = |status: OutcomeStatus| {
results
.outcomes
.iter()
.filter(|o| o.status == status)
.count()
};
Self {
passed: count(OutcomeStatus::Passed),
failed: count(OutcomeStatus::Failed),
errored: count(OutcomeStatus::Errored),
}
}
#[must_use]
pub const fn driven(&self) -> usize {
self.passed + self.failed + self.errored
}
#[must_use]
pub const fn red(&self) -> usize {
self.failed + self.errored
}
}
#[must_use]
pub fn badges(
report: &VerdictReport,
matrix: &CapabilityMatrix,
cases: CaseCounts,
) -> Vec<NamedBadge> {
let mut out: Vec<NamedBadge> = report
.profiles
.iter()
.map(|(tier, verdict)| profile_badge(*tier, *verdict, report, matrix))
.collect();
if let Some(security) = report.security {
out.push(security_badge(security, report, matrix));
}
out.push(performance_badge(&report.performance));
out.push(overall_badge(&report.profiles, cases));
out
}
fn profile_badge(
tier: Tier,
verdict: ProfileVerdict,
report: &VerdictReport,
matrix: &CapabilityMatrix,
) -> NamedBadge {
let members = tier_members(tier, matrix);
let token = token_of(verdict);
let noun = if tier == Tier::Options {
"optional capabilities"
} else {
"capabilities"
};
let amount = if members.is_empty() || verdict == ProfileVerdict::NotClaimed {
String::new()
} else {
format!(
" {}/{} {noun}",
passed_count(report, &members),
members.len()
)
};
NamedBadge {
file: format!("badge-{}.json", slug(tier)),
badge: Badge {
schema_version: 1,
label: format!("openEHR CNF {}", label(tier)),
message: format!("{}{amount}", shout(token)),
color: colour(token).to_owned(),
},
}
}
fn security_badge(
verdict: SecBasicVerdict,
report: &VerdictReport,
matrix: &CapabilityMatrix,
) -> NamedBadge {
let members = tier_members(Tier::SecBasic, matrix);
let token = match verdict {
SecBasicVerdict::Pass => "pass",
SecBasicVerdict::Fail => "fail",
};
NamedBadge {
file: "badge-sec-basic.json".to_owned(),
badge: Badge {
schema_version: 1,
label: "openEHR CNF SEC-BASIC".to_owned(),
message: format!(
"{} {}/{} capabilities",
shout(token),
passed_count(report, &members),
members.len()
),
color: colour(token).to_owned(),
},
}
}
fn performance_badge(performance: &[PerformanceVerdict]) -> NamedBadge {
let highest = |classes: Vec<PerfClass>| -> Option<PerfClass> {
PerfClass::ALL
.iter()
.rev()
.find(|c| classes.contains(c))
.copied()
};
let earned = highest(
performance
.iter()
.filter(|p| p.verdict == ClassVerdict::Earned)
.map(|p| p.class)
.collect(),
);
let measured = highest(performance.iter().map(|p| p.class).collect());
let (message, color) = match (earned, measured) {
(Some(class), _) => (format!("class {} earned", class.token()), "brightgreen"),
(None, Some(class)) => (format!("class {} not earned", class.token()), "red"),
(None, None) => ("not measured".to_owned(), "lightgrey"),
};
NamedBadge {
file: "badge-performance.json".to_owned(),
badge: Badge {
schema_version: 1,
label: "openEHR CNF performance".to_owned(),
message,
color: color.to_owned(),
},
}
}
fn overall_badge(profiles: &[(Tier, ProfileVerdict)], cases: CaseCounts) -> NamedBadge {
let passing = |tier: Tier| {
profiles
.iter()
.any(|(t, v)| *t == tier && *v == ProfileVerdict::Pass)
};
let tiers_pass = passing(Tier::Core) && passing(Tier::Standard);
let driven = cases.driven();
let passed = cases.passed;
let (message, color) = if tiers_pass && cases.red() == 0 {
(
format!("CORE+STANDARD PASS · {passed}/{driven} cases"),
"brightgreen",
)
} else if tiers_pass {
(
format!(
"CORE+STANDARD PASS · {passed}/{driven} cases ({} failing)",
cases.red()
),
"yellow",
)
} else {
(format!("NOT PASSING · {passed}/{driven} cases"), "red")
};
NamedBadge {
file: "badge.json".to_owned(),
badge: Badge {
schema_version: 1,
label: "openEHR conformance".to_owned(),
message,
color: color.to_owned(),
},
}
}
fn passed_count(report: &VerdictReport, members: &[CapabilityName]) -> usize {
members
.iter()
.filter(|name| {
report
.capabilities
.iter()
.any(|(n, e)| n == *name && *e == Evidence::Passed)
})
.count()
}
const fn token_of(verdict: ProfileVerdict) -> &'static str {
match verdict {
ProfileVerdict::Pass => "pass",
ProfileVerdict::Fail => "fail",
ProfileVerdict::NotClaimed => "not_claimed",
}
}
fn shout(token: &str) -> String {
token.to_uppercase().replace('_', " ")
}
fn colour(token: &str) -> &'static str {
match token {
"pass" => "brightgreen",
"fail" => "red",
_ => "lightgrey",
}
}
const fn slug(tier: Tier) -> &'static str {
match tier {
Tier::Core => "core",
Tier::Standard => "standard",
Tier::Options => "options",
Tier::SecBasic => "sec-basic",
Tier::EnterpriseD => "enterprise-d",
Tier::EnterpriseM => "enterprise-m",
Tier::EnterpriseX => "enterprise-x",
}
}
const fn label(tier: Tier) -> &'static str {
match tier {
Tier::Core => "CORE",
Tier::Standard => "STANDARD",
Tier::Options => "OPTIONS",
Tier::SecBasic => "SEC-BASIC",
Tier::EnterpriseD => "D",
Tier::EnterpriseM => "M",
Tier::EnterpriseX => "X",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::verdict::Coverage;
fn matrix() -> CapabilityMatrix {
serde_json::from_value(serde_json::json!({
"EhrOperations": { "family": "Platform", "tier": "CORE", "required": true },
"CompositionCrud": { "family": "Platform", "tier": "CORE", "required": true },
"AqlBasic": { "family": "Platform", "tier": "STANDARD", "required": true },
"SimplifiedFormats": { "family": "Platform", "tier": "OPTIONS", "required": false },
"AuthenticatedAccess": { "family": "Security", "tier": "SEC-BASIC", "required": true }
}))
.unwrap()
}
fn report(
capabilities: &[(&str, Evidence)],
profiles: &[(Tier, ProfileVerdict)],
security: Option<SecBasicVerdict>,
) -> VerdictReport {
VerdictReport {
review: Vec::new(),
capabilities: capabilities
.iter()
.map(|(n, e)| {
(
(*n).parse::<CapabilityName>()
.expect("test capability name"),
*e,
)
})
.collect(),
capability_tallies: Vec::new(),
profiles: profiles.to_vec(),
security,
performance: Vec::new(),
coverage: Coverage {
selected: 0,
driven: 0,
passed: 0,
failed: 0,
inconclusive: 0,
},
}
}
fn message(badges: &[NamedBadge], file: &str) -> String {
badges
.iter()
.find(|b| b.file == file)
.unwrap_or_else(|| panic!("no {file} badge"))
.badge
.message
.clone()
}
fn all_passed() -> Vec<(&'static str, Evidence)> {
vec![
("EhrOperations", Evidence::Passed),
("CompositionCrud", Evidence::Passed),
("AqlBasic", Evidence::Passed),
("SimplifiedFormats", Evidence::Passed),
("AuthenticatedAccess", Evidence::Passed),
]
}
#[test]
fn a_clean_run_counts_the_set_each_verdict_was_judged_on() {
let out = badges(
&report(
&all_passed(),
&[
(Tier::Core, ProfileVerdict::Pass),
(Tier::Standard, ProfileVerdict::Pass),
(Tier::Options, ProfileVerdict::Pass),
],
Some(SecBasicVerdict::Pass),
),
&matrix(),
CaseCounts {
passed: 40,
failed: 0,
errored: 0,
},
);
assert_eq!(message(&out, "badge-core.json"), "PASS 2/2 capabilities");
assert_eq!(
message(&out, "badge-standard.json"),
"PASS 3/3 capabilities"
);
assert_eq!(
message(&out, "badge-options.json"),
"PASS 1/1 optional capabilities"
);
assert_eq!(
message(&out, "badge-sec-basic.json"),
"PASS 1/1 capabilities"
);
assert_eq!(
message(&out, "badge.json"),
"CORE+STANDARD PASS · 40/40 cases"
);
}
#[test]
fn a_failing_tier_cannot_show_a_full_count() {
let mut caps = all_passed();
caps[1] = ("CompositionCrud", Evidence::Failed);
let out = badges(
&report(
&caps,
&[
(Tier::Core, ProfileVerdict::Fail),
(Tier::Standard, ProfileVerdict::Fail),
(Tier::Options, ProfileVerdict::Pass),
],
None,
),
&matrix(),
CaseCounts {
passed: 38,
failed: 2,
errored: 0,
},
);
assert_eq!(message(&out, "badge-core.json"), "FAIL 1/2 capabilities");
assert_eq!(
message(&out, "badge-standard.json"),
"FAIL 2/3 capabilities"
);
assert_eq!(
message(&out, "badge.json"),
"NOT PASSING · 38/40 cases",
"a failing tier is never CORE+STANDARD PASS"
);
}
#[test]
fn a_not_evidenced_capability_is_not_counted_as_satisfied() {
let mut caps = all_passed();
caps[0] = ("EhrOperations", Evidence::NotEvidenced);
let out = badges(
&report(&caps, &[(Tier::Core, ProfileVerdict::Fail)], None),
&matrix(),
CaseCounts::default(),
);
assert_eq!(message(&out, "badge-core.json"), "FAIL 1/2 capabilities");
}
#[test]
fn a_not_claimed_tier_shows_a_bare_token() {
let out = badges(
&report(
&all_passed(),
&[(Tier::Standard, ProfileVerdict::NotClaimed)],
None,
),
&matrix(),
CaseCounts::default(),
);
assert_eq!(message(&out, "badge-standard.json"), "NOT CLAIMED");
}
#[test]
fn a_red_row_under_passing_tiers_is_yellow_and_named() {
let out = badges(
&report(
&all_passed(),
&[
(Tier::Core, ProfileVerdict::Pass),
(Tier::Standard, ProfileVerdict::Pass),
],
None,
),
&matrix(),
CaseCounts {
passed: 39,
failed: 0,
errored: 1,
},
);
let overall = out
.iter()
.find(|b| b.file == "badge.json")
.expect("overall");
assert_eq!(
overall.badge.message,
"CORE+STANDARD PASS · 39/40 cases (1 failing)"
);
assert_eq!(overall.badge.color, "yellow");
}
#[test]
fn performance_names_its_class_or_says_unmeasured() {
let out = badges(
&report(&all_passed(), &[], None),
&matrix(),
CaseCounts::default(),
);
assert_eq!(message(&out, "badge-performance.json"), "not measured");
}
#[test]
fn a_measured_but_unearned_class_is_named_in_red() {
let mut r = report(&all_passed(), &[], None);
r.performance = vec![PerformanceVerdict {
case: crate::ids::CaseId::parse("PERF-hospital_sim-class_S").unwrap(),
class: PerfClass::S,
claimed: true,
verdict: ClassVerdict::NotEarned,
violations: vec!["error_rate 0.5 > max 0".to_owned()],
}];
let out = badges(&r, &matrix(), CaseCounts::default());
assert_eq!(
message(&out, "badge-performance.json"),
"class S not earned"
);
let badge = out
.iter()
.find(|b| b.file == "badge-performance.json")
.expect("performance badge");
assert_eq!(badge.badge.color, "red");
}
#[test]
fn the_performance_badge_names_the_highest_earned_class() {
let verdict = |class, verdict| PerformanceVerdict {
case: crate::ids::CaseId::parse("PERF-hospital_sim-class_x").unwrap(),
class,
claimed: true,
verdict,
violations: Vec::new(),
};
let mut r = report(&all_passed(), &[], None);
r.performance = vec![
verdict(PerfClass::Poc, ClassVerdict::Earned),
verdict(PerfClass::S, ClassVerdict::NotEarned),
];
let out = badges(&r, &matrix(), CaseCounts::default());
assert_eq!(message(&out, "badge-performance.json"), "class POC earned");
let badge = out
.iter()
.find(|b| b.file == "badge-performance.json")
.expect("performance badge");
assert_eq!(badge.badge.color, "brightgreen");
}
#[test]
fn a_failing_sec_basic_verdict_renders_as_fail() {
let out = badges(
&report(
&[("AuthenticatedAccess", Evidence::Failed)],
&[],
Some(SecBasicVerdict::Fail),
),
&matrix(),
CaseCounts::default(),
);
assert_eq!(
message(&out, "badge-sec-basic.json"),
"FAIL 0/1 capabilities"
);
}
#[test]
fn every_tier_carries_a_distinct_badge_slug_and_label() {
let slugs: std::collections::BTreeSet<&str> = Tier::ALL.iter().map(|t| slug(*t)).collect();
let labels: std::collections::BTreeSet<&str> =
Tier::ALL.iter().map(|t| label(*t)).collect();
assert_eq!(slugs.len(), Tier::ALL.len());
assert_eq!(labels.len(), Tier::ALL.len());
assert_eq!(slug(Tier::EnterpriseD), "enterprise-d");
assert_eq!(label(Tier::EnterpriseX), "X");
}
}