#![allow(
clippy::disallowed_types,
reason = "dev/verification tooling over JSON artifacts (the catalogue, results, wire \
exchanges), whose shapes belong to the artifacts and the SUT; the carriers \
here are cfg(test)-only, so #[expect] would be unfulfilled in the non-test build"
)]
use serde::{Deserialize, Serialize};
use crate::ids::{AmbiguityId, CapabilityName};
use crate::vocab::{Family, Tier};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Realization {
#[default]
ReleasedWire,
Extension,
}
impl Realization {
pub const ALL: &[Realization] = &[Realization::ReleasedWire, Realization::Extension];
#[must_use]
pub fn token(self) -> &'static str {
match self {
Realization::ReleasedWire => "released-wire",
Realization::Extension => "extension",
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RegisterAdjudication {
pub register: AmbiguityId,
pub reason: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CapabilityEntry {
pub family: Family,
pub tier: Tier,
pub required: bool,
#[serde(default)]
pub realization: Realization,
#[serde(default)]
pub min_cases: usize,
#[serde(default)]
pub evidence_exception: Option<RegisterAdjudication>,
#[serde(default)]
pub workload_exclusion: Option<RegisterAdjudication>,
#[serde(default)]
pub source: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CapabilityMatrix {
entries: Vec<(CapabilityName, CapabilityEntry)>,
}
impl CapabilityMatrix {
#[must_use]
pub fn get(&self, name: &CapabilityName) -> Option<&CapabilityEntry> {
self.entries.iter().find(|(n, _)| n == name).map(|(_, e)| e)
}
#[must_use]
pub fn entries(&self) -> &[(CapabilityName, CapabilityEntry)] {
&self.entries
}
pub fn check_tier_scoping(&self) -> Result<(), Vec<String>> {
let bad: Vec<String> = self
.entries
.iter()
.filter(|(_, e)| e.tier.family() != e.family)
.map(|(n, e)| {
format!(
"{n}: tier {:?} is not scoped to family {:?}",
e.tier, e.family
)
})
.collect();
if bad.is_empty() { Ok(()) } else { Err(bad) }
}
pub fn check_realization_scoping(&self) -> Result<(), Vec<String>> {
let bad: Vec<String> = self
.entries
.iter()
.filter(|(_, e)| e.required && e.realization == Realization::Extension)
.map(|(n, _)| {
format!(
"{n}: realization `extension` may not be `required` — an openEHR profile \
tier may not rest on a surface no openEHR specification governs"
)
})
.collect();
if bad.is_empty() { Ok(()) } else { Err(bad) }
}
}
impl<'de> Deserialize<'de> for CapabilityMatrix {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let entries = crate::model::de::ordered_map(deserializer)?;
Ok(Self { entries })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tier_scoping_is_enforced() {
let m: CapabilityMatrix = serde_json::from_value(serde_json::json!({
"EhrOperations": { "family": "Platform", "tier": "CORE", "required": true },
"Signing": { "family": "Platform", "tier": "STANDARD", "required": false }
}))
.unwrap();
assert!(m.check_tier_scoping().is_ok());
let m: CapabilityMatrix = serde_json::from_value(serde_json::json!({
"AuditAccountability": { "family": "Platform", "tier": "SEC-BASIC", "required": true }
}))
.unwrap();
assert!(m.check_tier_scoping().is_err());
}
#[test]
fn row_carries_realization_floor_and_adjudications() {
let m: CapabilityMatrix = serde_json::from_value(serde_json::json!({
"EhrOperations": { "family": "Platform", "tier": "CORE", "required": true,
"min_cases": 23 },
"Tds": { "family": "Platform", "tier": "OPTIONS", "required": false,
"min_cases": 4, "realization": "extension",
"evidence_exception": { "register": "AMB-34", "reason": "no released wire" },
"workload_exclusion": { "register": "AMB-170", "reason": "not in the load mix" } }
}))
.unwrap();
let ehr = m
.get(&CapabilityName::parse("EhrOperations").unwrap())
.unwrap();
assert_eq!(ehr.realization, Realization::ReleasedWire);
assert_eq!(ehr.min_cases, 23);
assert!(ehr.evidence_exception.is_none());
let tds = m.get(&CapabilityName::parse("Tds").unwrap()).unwrap();
assert_eq!(tds.realization, Realization::Extension);
assert_eq!(
tds.evidence_exception.as_ref().unwrap().register.as_str(),
"AMB-34"
);
assert_eq!(
tds.workload_exclusion.as_ref().unwrap().register.as_str(),
"AMB-170"
);
assert!(m.check_realization_scoping().is_ok());
}
#[test]
fn required_extension_row_is_rejected() {
let m: CapabilityMatrix = serde_json::from_value(serde_json::json!({
"MessageApi": { "family": "Platform", "tier": "CORE", "required": true,
"min_cases": 1, "realization": "extension" }
}))
.unwrap();
assert!(m.check_realization_scoping().is_err());
}
}