#![allow(
clippy::disallowed_types,
reason = "dev/verification tooling over JSON artifacts (the catalogue, results, wire \
exchanges) — not the application (#1694); the carriers here are cfg(test)-only, so \
#[expect] would be unfulfilled in the non-test build"
)]
use crate::model::binding::{OperationBinding, WireExpectation};
use crate::model::vocab_files::SelectorsVocab;
use crate::vocab::OutcomeKind;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Observation {
Kind(OutcomeKind),
Unmapped {
status: u16,
},
Transport(String),
}
#[must_use]
pub fn classify_status(
binding: &OperationBinding,
selectors: Option<&SelectorsVocab>,
status: u16,
expected: OutcomeKind,
) -> Observation {
if let Some(expectation) = binding.outcome(expected)
&& expectation_matches(expectation, status)
{
return Observation::Kind(expected);
}
for kind in OutcomeKind::ALL {
if let Some(expectation) = binding.outcome(*kind)
&& expectation_matches(expectation, status)
{
return Observation::Kind(*kind);
}
}
if let Some(universal) = selectors.and_then(|s| s.universal_outcomes.as_deref()) {
for (token, mapping) in universal {
if mapping.status == status
&& let Some(kind) = OutcomeKind::from_token(token)
{
return Observation::Kind(kind);
}
}
}
Observation::Unmapped { status }
}
fn expectation_matches(expectation: &WireExpectation, status: u16) -> bool {
if expectation.status.value() == status {
return true;
}
expectation
.alt_status
.as_deref()
.is_some_and(|alts| alts.iter().any(|alt| alt.value() == status))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StepJudgement {
Continue,
Failed {
expected: OutcomeKind,
observed: OutcomeKind,
},
Errored(String),
}
#[must_use]
pub fn judge(expected: OutcomeKind, observation: &Observation) -> StepJudgement {
match observation {
Observation::Kind(observed) if *observed == expected => StepJudgement::Continue,
Observation::Kind(observed) => StepJudgement::Failed {
expected,
observed: *observed,
},
Observation::Unmapped { status } => StepJudgement::Errored(format!(
"status {status} maps to no outcome of the operation's binding (inconclusive)"
)),
Observation::Transport(fault) => {
StepJudgement::Errored(format!("transport fault: {fault} (inconclusive)"))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn binding() -> OperationBinding {
serde_json::from_value(serde_json::json!({
"sm_operation": "I_EHR_SERVICE.create_ehr",
"its": "its-rest",
"request": { "method": "POST", "path": "/ehr" },
"outcomes": {
"created": { "status": 201 },
"already_exists": { "status": 409 }
}
}))
.unwrap()
}
fn selectors() -> SelectorsVocab {
serde_saphyr::from_str(
"body_selectors: [prefer_conditional, error_loose, result_set_body, negotiated, present, absent]\nheader_matchers: [\"present\", \"present?\", \"absent\", \"negotiated\", \"latest-version-uid\", \"pattern:<regex>\", \"<literal>\"]\nignore_sets:\n server_assigned: { per_binding: true, source: s }\n ctx_defaults: { paths: [context/start_time], source: s }\nuniversal_outcomes:\n unauthenticated: { status: 401, source: s }\n forbidden: { status: 403, source: s }\n",
)
.unwrap()
}
#[test]
fn law_c_classification() {
let b = binding();
let s = selectors();
assert_eq!(
classify_status(&b, Some(&s), 201, OutcomeKind::Created),
Observation::Kind(OutcomeKind::Created)
);
assert_eq!(
classify_status(&b, Some(&s), 409, OutcomeKind::Created),
Observation::Kind(OutcomeKind::AlreadyExists)
);
assert_eq!(
classify_status(&b, Some(&s), 401, OutcomeKind::Created),
Observation::Kind(OutcomeKind::Unauthenticated)
);
assert_eq!(
classify_status(&b, Some(&s), 500, OutcomeKind::Created),
Observation::Unmapped { status: 500 }
);
}
#[test]
fn law_b_and_c_judgement() {
assert_eq!(
judge(
OutcomeKind::Created,
&Observation::Kind(OutcomeKind::Created)
),
StepJudgement::Continue
);
assert!(matches!(
judge(
OutcomeKind::Created,
&Observation::Kind(OutcomeKind::AlreadyExists)
),
StepJudgement::Failed { .. }
));
assert!(matches!(
judge(OutcomeKind::Created, &Observation::Unmapped { status: 500 }),
StepJudgement::Errored(_)
));
assert!(matches!(
judge(
OutcomeKind::Created,
&Observation::Transport("timeout".into())
),
StepJudgement::Errored(_)
));
}
}