#![expect(
clippy::disallowed_types,
reason = "dev/verification tooling over JSON artifacts (the catalogue, results, wire \
exchanges), whose shapes belong to the artifacts and the SUT"
)]
use serde::de::Error as DeError;
use serde::{Deserialize, Deserializer};
use crate::ids::{
AmbiguityId, CapabilityName, CaptureName, CaseId, CorpusKey, InstanceName, OptionTag,
SmOperationRef,
};
use crate::model::assertion::Assertion;
use crate::model::value::TemplatedValue;
use crate::refgrammar::CaptureValueSource;
use crate::vocab::{
CaseKind, CaseStatus, Component, FormatName, Iteration, OutcomeKind, ServerState,
SpecComponent, Tier, XVersionedClass,
};
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Applies {
#[serde(default)]
pub rm: Option<VersionRange>,
#[serde(default)]
pub base: Option<VersionRange>,
#[serde(default)]
pub am: Option<VersionRange>,
#[serde(default)]
pub aql: Option<VersionRange>,
#[serde(default)]
pub its_rest: Option<VersionRange>,
#[serde(default)]
pub term: Option<VersionRange>,
}
impl Applies {
#[must_use]
pub fn entries(&self) -> Vec<(SpecComponent, &VersionRange)> {
[
(SpecComponent::Rm, &self.rm),
(SpecComponent::Base, &self.base),
(SpecComponent::Am, &self.am),
(SpecComponent::Aql, &self.aql),
(SpecComponent::ItsRest, &self.its_rest),
(SpecComponent::Term, &self.term),
]
.into_iter()
.filter_map(|(c, r)| r.as_ref().map(|r| (c, r)))
.collect()
}
#[must_use]
pub fn satisfied_by(&self, versions: &crate::party::SpecVersions) -> bool {
self.entries().into_iter().all(|(component, range)| {
versions
.get(component)
.and_then(|raw| semver::Version::parse(raw).ok())
.is_some_and(|version| range.req().matches(&version))
})
}
}
#[derive(Debug, Clone)]
pub struct VersionRange {
raw: String,
req: semver::VersionReq,
}
impl VersionRange {
#[must_use]
pub fn raw(&self) -> &str {
&self.raw
}
#[must_use]
pub fn req(&self) -> &semver::VersionReq {
&self.req
}
}
impl<'de> Deserialize<'de> for VersionRange {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = String::deserialize(deserializer)?;
let req = semver::VersionReq::parse(&raw)
.map_err(|e| D::Error::custom(format!("applies range {raw:?}: {e}")))?;
Ok(Self { raw, req })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EhrRequirement {
None,
Exists {
commits: CommitState,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CommitState {
None,
Any,
}
impl<'de> Deserialize<'de> for EhrRequirement {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Qualified {
commits: CommitState,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Word(String),
Qualified(Qualified),
}
match Raw::deserialize(deserializer)? {
Raw::Word(w) if w == "none" => Ok(Self::None),
Raw::Word(w) => Err(D::Error::custom(format!(
"requires.ehr must be `none` or {{ commits: none | any }}, got {w:?}"
))),
Raw::Qualified(q) => Ok(Self::Exists { commits: q.commits }),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DirectoryRequirement {
None,
Tree(CorpusKey),
}
impl<'de> Deserialize<'de> for DirectoryRequirement {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
if s == "none" {
return Ok(Self::None);
}
CorpusKey::parse(&s)
.map(Self::Tree)
.map_err(D::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PartyRequirement {
None,
Exists(CorpusKey),
}
impl<'de> Deserialize<'de> for PartyRequirement {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
if s == "none" {
return Ok(Self::None);
}
CorpusKey::parse(&s)
.map(Self::Exists)
.map_err(D::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PartyRelationshipRequirement {
None,
Exists {
source: CorpusKey,
target: CorpusKey,
relationship: CorpusKey,
},
}
impl<'de> Deserialize<'de> for PartyRelationshipRequirement {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Qualified {
source: CorpusKey,
target: CorpusKey,
relationship: CorpusKey,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Word(String),
Qualified(Qualified),
}
match Raw::deserialize(deserializer)? {
Raw::Word(w) if w == "none" => Ok(Self::None),
Raw::Word(w) => Err(D::Error::custom(format!(
"requires.party_relationship must be `none` or \
{{ source, target, relationship }} corpus keys, got {w:?}"
))),
Raw::Qualified(q) => Ok(Self::Exists {
source: q.source,
target: q.target,
relationship: q.relationship,
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImportRequirement {
None,
Received {
extract: CorpusKey,
container: XVersionedClass,
},
}
impl<'de> Deserialize<'de> for ImportRequirement {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Qualified {
extract: CorpusKey,
container: XVersionedClass,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Word(String),
Qualified(Qualified),
}
match Raw::deserialize(deserializer)? {
Raw::Word(w) if w == "none" => Ok(Self::None),
Raw::Word(w) => Err(D::Error::custom(format!(
"requires.import must be `none` or {{ extract, container }}, got {w:?}"
))),
Raw::Qualified(q) => Ok(Self::Received {
extract: q.extract,
container: q.container,
}),
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Requires {
#[serde(default)]
pub server: Option<ServerState>,
#[serde(default)]
pub templates: Vec<CorpusKey>,
#[serde(default)]
pub ehr: Option<EhrRequirement>,
#[serde(default)]
pub directory: Option<DirectoryRequirement>,
#[serde(default)]
pub party: Option<PartyRequirement>,
#[serde(default)]
pub party_relationship: Option<PartyRelationshipRequirement>,
#[serde(default)]
pub import: Option<ImportRequirement>,
#[serde(default)]
pub commit: Vec<CorpusKey>,
#[serde(default)]
pub terminology: Option<TerminologyRequirement>,
#[serde(default)]
pub spec_profile: Option<crate::ixit::SpecProfile>,
#[serde(default)]
pub administrative: Option<bool>,
#[serde(default)]
pub instances: Option<std::collections::BTreeMap<InstanceName, Requires>>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TerminologyRequirement {
#[serde(default)]
pub posture: Option<crate::ixit::TerminologyPosture>,
#[serde(default)]
pub served: Vec<String>,
#[serde(default)]
pub unreachable: Vec<String>,
#[serde(default)]
pub distinct_servers: Option<usize>,
}
impl Requires {
#[must_use]
pub fn minted_handles(&self) -> Vec<CaptureName> {
let mut handles = Vec::new();
if matches!(self.ehr, Some(EhrRequirement::Exists { .. }))
&& let Ok(handle) = CaptureName::parse("ehr_id")
{
handles.push(handle);
}
if self.directory.is_some()
&& let Ok(handle) = CaptureName::parse("directory_version_uid")
{
handles.push(handle);
}
if matches!(self.party, Some(PartyRequirement::Exists(_)))
&& let Ok(handle) = CaptureName::parse("party_id")
{
handles.push(handle);
}
if matches!(
self.party_relationship,
Some(PartyRelationshipRequirement::Exists { .. })
) && let Ok(handle) = CaptureName::parse("party_relationship_id")
{
handles.push(handle);
}
if matches!(self.import, Some(ImportRequirement::Received { .. })) {
for name in [
"imported_versioned_object_uid",
"imported_version_uid",
"imported_branch_version_uid",
] {
if let Ok(handle) = CaptureName::parse(name) {
handles.push(handle);
}
}
if !matches!(self.ehr, Some(EhrRequirement::Exists { .. }))
&& let Ok(handle) = CaptureName::parse("ehr_id")
{
handles.push(handle);
}
}
handles
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum MatrixCell {
Absent,
Provided,
Null,
Literal(serde_json::Value),
}
impl<'de> Deserialize<'de> for MatrixCell {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(deserializer)?;
Ok(match &value {
serde_json::Value::Null => Self::Null,
serde_json::Value::String(s) if s == "absent" => Self::Absent,
serde_json::Value::String(s) if s == "provided" => Self::Provided,
_ => Self::Literal(value),
})
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Matrix {
pub columns: Vec<String>,
#[serde(default)]
pub rows: Vec<Vec<MatrixCell>>,
#[serde(default)]
pub rows_from: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FixtureEntry {
pub data_set: CorpusKey,
pub expected: OutcomeKind,
#[serde(default)]
pub defect: Option<String>,
#[serde(default)]
pub spec_ref: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Parameters {
pub iteration: Iteration,
#[serde(default)]
pub matrix: Option<Matrix>,
#[serde(default)]
pub fixture_set: Option<Vec<FixtureEntry>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExpectSpec {
Kind(OutcomeKind),
FixtureExpected,
}
impl<'de> Deserialize<'de> for ExpectSpec {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
if s == "${fixture.expected}" {
return Ok(Self::FixtureExpected);
}
OutcomeKind::from_token(&s).map(Self::Kind).ok_or_else(|| {
D::Error::custom(format!(
"expect must be an outcome kind or ${{fixture.expected}}, got {s:?}"
))
})
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FlowStep {
pub step: u32,
pub call: String,
#[serde(default)]
pub on: Option<InstanceName>,
#[serde(default)]
pub variant: Option<String>,
#[serde(default)]
pub format: Option<FormatName>,
#[serde(default)]
pub scopes: Option<Vec<TemplatedValue>>,
#[serde(default, deserialize_with = "crate::model::de::optional_ordered_map")]
pub with: Option<Vec<(String, TemplatedValue)>>,
pub expect: ExpectSpec,
#[serde(default, deserialize_with = "crate::model::de::optional_ordered_map")]
pub capture: Option<Vec<(CaptureName, CaptureValueSource)>>,
#[serde(default, rename = "assert")]
pub assertions: Vec<Assertion>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConstraintContext {
pub template: CorpusKey,
pub path: String,
#[serde(default)]
pub constraint_columns: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DecisionTable {
pub columns: Vec<String>,
pub rows: Vec<Vec<serde_json::Value>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RowVerdict {
Accepted,
Rejected,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CaseCore {
pub id: CaseId,
pub kind: CaseKind,
#[serde(default)]
pub status: CaseStatus,
pub component: Component,
#[serde(default)]
pub sm_operation: Option<SmOperationRef>,
#[serde(default)]
pub rm_class: Option<String>,
pub test_purpose: String,
pub description: String,
pub spec_refs: Vec<String>,
#[serde(default)]
pub applies: Applies,
#[serde(default)]
pub guards: Vec<String>,
#[serde(default)]
pub capabilities: Vec<CapabilityName>,
#[serde(default)]
pub exercises: Vec<CapabilityName>,
#[serde(default)]
pub profiles: Vec<Tier>,
#[serde(default)]
pub option: Option<OptionTag>,
#[serde(default)]
pub formats: Vec<FormatName>,
#[serde(default)]
pub requires: Requires,
#[serde(default)]
pub parameters: Option<Parameters>,
#[serde(default)]
pub flow: Vec<FlowStep>,
#[serde(default)]
pub constraint_context: Option<ConstraintContext>,
#[serde(default)]
pub decision_table: Option<DecisionTable>,
#[serde(default)]
pub postconditions: Vec<Assertion>,
#[serde(default)]
pub verified_by: Vec<CaseId>,
#[serde(default)]
pub ambiguities: Vec<AmbiguityId>,
#[serde(default)]
pub data_sets: Vec<CorpusKey>,
}
impl FlowStep {
#[must_use]
pub fn captures(&self) -> &[(CaptureName, CaptureValueSource)] {
self.capture.as_deref().unwrap_or_default()
}
#[must_use]
pub fn with_entries(&self) -> &[(String, TemplatedValue)] {
self.with.as_deref().unwrap_or_default()
}
#[must_use]
pub fn declares_scopes(&self) -> bool {
self.scopes.is_some()
}
#[must_use]
pub fn scope_templates(&self) -> &[TemplatedValue] {
self.scopes.as_deref().unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn requires_parses_the_schedule_vocabulary() {
let r: Requires = serde_json::from_value(serde_json::json!({
"server": "any",
"templates": ["cnf.opt.minimal_event"],
"ehr": { "commits": "none" },
"commit": ["cnf.set.bp-10"]
}))
.unwrap();
assert_eq!(r.server, Some(ServerState::Any));
assert!(matches!(
r.ehr,
Some(EhrRequirement::Exists {
commits: CommitState::None
})
));
assert_eq!(r.minted_handles().len(), 1);
let r: Requires = serde_json::from_value(serde_json::json!({ "ehr": "none" })).unwrap();
assert!(matches!(r.ehr, Some(EhrRequirement::None)));
assert!(r.minted_handles().is_empty());
assert!(serde_json::from_value::<Requires>(serde_json::json!({ "ehr": "maybe" })).is_err());
assert!(serde_json::from_value::<Requires>(serde_json::json!({ "srv": "empty" })).is_err());
}
#[test]
fn a_provisioned_party_relationship_mints_its_container_handle() {
let r: Requires = serde_json::from_value(serde_json::json!({
"party_relationship": {
"source": "cnf.demographic.person.v1",
"target": "cnf.demographic.organisation.v1",
"relationship": "cnf.demographic.party_relationship.v1"
}
}))
.unwrap();
assert!(matches!(
r.party_relationship,
Some(PartyRelationshipRequirement::Exists { .. })
));
assert_eq!(
r.minted_handles()
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>(),
vec!["party_relationship_id".to_owned()]
);
let r: Requires =
serde_json::from_value(serde_json::json!({ "party_relationship": "none" })).unwrap();
assert!(matches!(
r.party_relationship,
Some(PartyRelationshipRequirement::None)
));
assert!(r.minted_handles().is_empty());
assert!(
serde_json::from_value::<Requires>(serde_json::json!({
"party_relationship": { "relationship": "cnf.demographic.party_relationship.v1" }
}))
.is_err()
);
assert!(
serde_json::from_value::<Requires>(
serde_json::json!({ "party_relationship": "cnf.demographic.party_relationship.v1" })
)
.is_err()
);
}
#[test]
fn a_received_extract_mints_the_identities_it_carries() {
let into_existing: Requires = serde_json::from_value(serde_json::json!({
"ehr": { "commits": "any" },
"import": {
"extract": "cnf.messaging.ehr_extract.v1",
"container": "X_VERSIONED_COMPOSITION"
}
}))
.unwrap();
assert_eq!(
into_existing
.minted_handles()
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>(),
vec![
"ehr_id".to_owned(),
"imported_versioned_object_uid".to_owned(),
"imported_version_uid".to_owned(),
"imported_branch_version_uid".to_owned(),
]
);
let clone: Requires = serde_json::from_value(serde_json::json!({
"import": {
"extract": "cnf.messaging.ehr_extract.v1",
"container": "X_VERSIONED_EHR_STATUS"
}
}))
.unwrap();
assert!(
clone
.minted_handles()
.iter()
.any(|h| h.to_string() == "ehr_id"),
"a whole-EHR clone mints the EHR it created"
);
let none: Requires =
serde_json::from_value(serde_json::json!({ "import": "none" })).unwrap();
assert_eq!(none.import, Some(ImportRequirement::None));
assert!(none.minted_handles().is_empty());
assert!(
serde_json::from_value::<Requires>(serde_json::json!({
"import": { "extract": "cnf.messaging.ehr_extract.v1" }
}))
.is_err()
);
assert!(
serde_json::from_value::<Requires>(serde_json::json!({
"import": {
"extract": "cnf.messaging.ehr_extract.v1",
"container": "X_VERSIONED_THING"
}
}))
.is_err()
);
assert!(
serde_json::from_value::<Requires>(
serde_json::json!({ "import": "cnf.messaging.ehr_extract.v1" })
)
.is_err()
);
}
#[test]
fn matrix_cells_distinguish_sentinels() {
let m: Matrix = serde_json::from_value(serde_json::json!({
"columns": ["ehr_status", "is_queryable"],
"rows": [["absent", "-"], ["provided", true], [null, 1.5]]
}))
.unwrap();
assert!(matches!(m.rows[0][0], MatrixCell::Absent));
assert!(matches!(m.rows[0][1], MatrixCell::Literal(_)));
assert!(matches!(m.rows[1][0], MatrixCell::Provided));
assert!(matches!(
m.rows[1][1],
MatrixCell::Literal(serde_json::Value::Bool(true))
));
assert!(matches!(m.rows[2][0], MatrixCell::Null));
}
#[test]
fn expect_spec_is_closed() {
assert!(matches!(
serde_json::from_value::<ExpectSpec>(serde_json::json!("created")).unwrap(),
ExpectSpec::Kind(OutcomeKind::Created)
));
assert!(matches!(
serde_json::from_value::<ExpectSpec>(serde_json::json!("${fixture.expected}")).unwrap(),
ExpectSpec::FixtureExpected
));
assert!(serde_json::from_value::<ExpectSpec>(serde_json::json!("http_201")).is_err());
}
#[test]
fn the_corpus_keyed_preconditions_read_none_as_the_absent_state() {
let requires: Requires = serde_json::from_value(serde_json::json!({
"directory": "none",
"party": "none"
}))
.unwrap();
assert_eq!(requires.directory, Some(DirectoryRequirement::None));
assert_eq!(requires.party, Some(PartyRequirement::None));
let provisioned: Requires = serde_json::from_value(serde_json::json!({
"directory": "cnf.folder.tree.v1",
"party": "cnf.demographic.person.v1"
}))
.unwrap();
assert_eq!(
provisioned.directory,
Some(DirectoryRequirement::Tree(
CorpusKey::parse("cnf.folder.tree.v1").unwrap()
))
);
assert_eq!(
provisioned.party,
Some(PartyRequirement::Exists(
CorpusKey::parse("cnf.demographic.person.v1").unwrap()
))
);
assert!(
serde_json::from_value::<Requires>(serde_json::json!({ "directory": "Not.A.Key" }))
.is_err()
);
assert!(
serde_json::from_value::<Requires>(serde_json::json!({ "party": "Not.A.Key" }))
.is_err()
);
}
}