#![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 std::collections::BTreeMap;
use crate::exec::assertions;
use crate::exec::state::VarStore;
use crate::model::binding::{HeaderExpectation, HeaderMatcher, WireExpectation};
use crate::party::SpecVersions;
#[derive(Debug, Default)]
pub struct RequestContext<'a> {
pub accept: Option<&'a str>,
pub last_version_uid: Option<&'a str>,
pub spec_versions: Option<&'a SpecVersions>,
}
#[must_use]
pub fn evaluate(
expectation: &WireExpectation,
response_headers: &BTreeMap<String, String>,
ctx: &RequestContext<'_>,
vars: &VarStore,
) -> Vec<String> {
let Some(declared) = expectation.headers.as_deref() else {
return Vec::new();
};
let mut failures = Vec::new();
for (name, header) in declared {
if !in_scope(header, ctx) {
continue;
}
let observed = header_value(response_headers, name);
if header.optional && observed.is_none_or(|v| v.trim().is_empty()) {
continue;
}
if let Some(failure) = judge(name, &header.matcher, observed, ctx, vars) {
failures.push(failure);
}
}
failures
}
fn in_scope(header: &HeaderExpectation, ctx: &RequestContext<'_>) -> bool {
match &header.applies {
None => true,
Some(applies) => ctx
.spec_versions
.is_some_and(|versions| applies.satisfied_by(versions)),
}
}
fn header_value<'a>(headers: &'a BTreeMap<String, String>, name: &str) -> Option<&'a str> {
headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_str())
}
fn judge(
name: &str,
matcher: &HeaderMatcher,
observed: Option<&str>,
ctx: &RequestContext<'_>,
vars: &VarStore,
) -> Option<String> {
match matcher {
HeaderMatcher::Present => match observed {
Some(v) if !v.trim().is_empty() => None,
_ => Some(format!("header {name}: expected present, got none")),
},
HeaderMatcher::Absent => observed.map(|v| {
format!("header {name}: expected absent, got {v:?} (the binding's cited spec sentence forbids it on this outcome)")
}),
HeaderMatcher::Negotiated => {
let Some(accept) = ctx.accept.map(media_token) else {
return None;
};
match observed {
Some(v) if media_token(v).eq_ignore_ascii_case(accept) => None,
Some(v) => Some(format!(
"header {name}: expected the negotiated media type {accept:?}, got {v:?}"
)),
None => Some(format!(
"header {name}: expected the negotiated media type {accept:?}, got none"
)),
}
}
HeaderMatcher::LatestVersionUid => match observed {
None => Some(format!(
"header {name}: expected the latest version uid, got none"
)),
Some(v) => {
let payload = strip_entity_tag(v);
if payload.is_empty() {
return Some(format!(
"header {name}: expected the latest version uid, got an empty entity tag"
));
}
match ctx.last_version_uid {
Some(latest) if payload.eq_ignore_ascii_case(latest) => None,
Some(latest) => Some(format!(
"header {name}: expected the latest version uid {latest:?}, got {payload:?}"
)),
None => None,
}
}
},
HeaderMatcher::Pattern(pattern) => {
let Some(v) = observed else {
return Some(format!(
"header {name}: expected a value matching {pattern:?}, got none"
));
};
let resolved = match resolve_placeholders(pattern, vars) {
Ok(resolved) => resolved,
Err(missing) => {
return Some(format!(
"header {name}: pattern {pattern:?} names <{missing}>, which is \
neither a captured/with-supplied case variable nor a structural \
token (<n>, <system_id>, <versioned_object_uid>, <template_hrid>) — \
refusing the vacuous wildcard (#1852)"
));
}
};
match regex::Regex::new(&format!("^(?:{resolved})$")) {
Ok(re) if re.is_match(v) => None,
Ok(_) => Some(format!(
"header {name}: value {v:?} does not match {pattern:?} (resolved: {resolved:?})"
)),
Err(e) => Some(format!(
"header {name}: resolved pattern {resolved:?} does not compile: {e}"
)),
}
}
HeaderMatcher::Literal(template) => {
let Some(v) = observed else {
return Some(format!("header {name}: expected a value, got none"));
};
match assertions::render_template(template, vars) {
Ok(want) if want == v => None,
Ok(want) => Some(format!("header {name}: expected {want:?}, got {v:?}")),
Err(e) => Some(format!(
"header {name}: literal template unresolvable ({e}) — declared value cannot be checked"
)),
}
}
}
}
fn media_token(value: &str) -> &str {
value.split(';').next().unwrap_or(value).trim()
}
fn strip_entity_tag(value: &str) -> &str {
let v = value.trim();
let v = v
.strip_prefix("W/")
.or_else(|| v.strip_prefix("w/"))
.unwrap_or(v);
v.trim_matches('"')
}
const VERSION_TREE_ID: &str = r"[1-9][0-9]*(?:\.[0-9]+\.[0-9]+)?";
const UID: &str = concat!(
r"(?:[0-9A-Fa-f]+(?:-[0-9A-Fa-f]+){4}",
r"|[0-9]+(?:\.[0-9]+)*",
r"|(?:[A-Za-z0-9]|[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9])",
r"(?:\.(?:[A-Za-z0-9]|[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]))*)",
);
const ARCHETYPE_HRID: &str = concat!(
r"(?:[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)*::)?",
r"[A-Za-z][A-Za-z0-9_]+-[A-Za-z][A-Za-z0-9_]+-[A-Za-z][A-Za-z0-9_]+",
r"\.[A-Za-z][A-Za-z0-9_-]+",
r"\.v[0-9]+\.[0-9]+\.[0-9]+(?:-(?:rc|alpha)(?:\.[0-9]+)?)?",
);
#[must_use]
pub fn structural_token(name: &str) -> Option<&'static str> {
match name {
"n" => Some(VERSION_TREE_ID),
"system_id" | "versioned_object_uid" => Some(UID),
"template_hrid" => Some(ARCHETYPE_HRID),
_ => None,
}
}
fn resolve_placeholders(pattern: &str, vars: &VarStore) -> Result<String, String> {
let mut out = String::new();
let mut rest = pattern;
while let Some(start) = rest.find('<') {
let (head, tail) = rest.split_at(start);
out.push_str(head);
if let Some(end) = tail.find('>') {
let name = tail.get(1..end).unwrap_or_default();
let captured = crate::ids::CaptureName::parse(name)
.ok()
.and_then(|n| vars.scalar(&n).map(str::to_owned));
match (structural_token(name), captured) {
(Some(grammar), _) => out.push_str(grammar),
(None, Some(value)) => out.push_str(®ex::escape(&value)),
(None, None) => return Err(name.to_owned()),
}
rest = tail.get(end + 1..).unwrap_or_default();
} else {
out.push_str(tail);
rest = "";
}
}
out.push_str(rest);
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::exec::state::Captured;
use crate::ids::CaptureName;
fn expectation(headers_yaml: &serde_json::Value) -> WireExpectation {
serde_json::from_value(serde_json::json!({
"status": 200,
"headers": headers_yaml
}))
.unwrap()
}
fn response(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.collect()
}
fn ctx<'a>() -> RequestContext<'a> {
RequestContext::default()
}
fn versions(its_rest: &str) -> SpecVersions {
SpecVersions {
its_rest: Some(its_rest.to_owned()),
..SpecVersions::default()
}
}
#[test]
fn present_and_absent_are_enforced() {
let e = expectation(&serde_json::json!({ "ETag": "present", "Location": "absent" }));
let ok = response(&[("etag", "W/\"x::sys::1\"")]);
assert!(evaluate(&e, &ok, &ctx(), &VarStore::default()).is_empty());
let bad = response(&[("location", "/somewhere")]);
let failures = evaluate(&e, &bad, &ctx(), &VarStore::default());
assert_eq!(failures.len(), 2, "{failures:?}");
assert!(failures[0].contains("ETag"));
assert!(failures[1].contains("Location"));
}
#[test]
fn present_optional_never_fails() {
let e = expectation(&serde_json::json!({ "Preference-Applied": "present?" }));
assert!(evaluate(&e, &response(&[]), &ctx(), &VarStore::default()).is_empty());
}
#[test]
fn negotiated_compares_the_media_token() {
let e = expectation(&serde_json::json!({ "Content-Type": "negotiated" }));
let ctx = RequestContext {
accept: Some("application/json"),
..RequestContext::default()
};
let ok = response(&[("content-type", "application/json; charset=utf-8")]);
assert!(evaluate(&e, &ok, &ctx, &VarStore::default()).is_empty());
let bad = response(&[("content-type", "application/openehr.wt+json")]);
assert_eq!(evaluate(&e, &bad, &ctx, &VarStore::default()).len(), 1);
}
#[test]
fn latest_version_uid_compares_case_insensitively() {
let e = expectation(&serde_json::json!({ "ETag": "latest-version-uid" }));
let ctx = RequestContext {
last_version_uid: Some("abc::sys::2"),
..RequestContext::default()
};
let ok = response(&[("etag", "W/\"ABC::SYS::2\"")]);
assert!(evaluate(&e, &ok, &ctx, &VarStore::default()).is_empty());
let stale = response(&[("etag", "W/\"abc::sys::1\"")]);
assert_eq!(evaluate(&e, &stale, &ctx, &VarStore::default()).len(), 1);
let missing = response(&[]);
assert_eq!(evaluate(&e, &missing, &ctx, &VarStore::default()).len(), 1);
}
#[test]
fn pattern_resolves_placeholders_from_vars() {
let e = expectation(&serde_json::json!({
"ETag": "pattern:W/\"<contribution_uid>::<system_id>::<n>\""
}));
let mut vars = VarStore::default();
vars.set(
CaptureName::parse("contribution_uid").unwrap(),
Captured::Scalar("abc-123".to_owned()),
);
let ok = response(&[("etag", "W/\"abc-123::any.system::2\"")]);
assert!(evaluate(&e, &ok, &ctx(), &vars).is_empty());
let bad = response(&[("etag", "W/\"other-uid::any.system::2\"")]);
assert_eq!(evaluate(&e, &bad, &ctx(), &vars).len(), 1);
let empty_system = response(&[("etag", "W/\"abc-123::::2\"")]);
assert_eq!(evaluate(&e, &empty_system, &ctx(), &vars).len(), 1);
let zero_led = response(&[("etag", "W/\"abc-123::any.system::02\"")]);
assert_eq!(evaluate(&e, &zero_led, &ctx(), &vars).len(), 1);
}
#[test]
fn object_id_token_is_the_released_uid_grammar() {
let e = expectation(&serde_json::json!({
"ETag": "pattern:W/\"<versioned_object_uid>::<system_id>::1\""
}));
for uid in [
"019fcd81-9968-7511-90d8-9f9750ea042a",
"1.2.840.113554.1.2.2",
"uk.nhs.ehr1",
"a",
] {
let etag = format!("W/\"{uid}::ferroehr.local::1\"");
let ok = response(&[("etag", etag.as_str())]);
assert!(
evaluate(&e, &ok, &ctx(), &VarStore::default()).is_empty(),
"{uid} is a released uid form"
);
}
for payload in ["::ferroehr.local::1", "-bad::ferroehr.local::1"] {
let etag = format!("W/\"{payload}\"");
let bad = response(&[("etag", etag.as_str())]);
assert_eq!(
evaluate(&e, &bad, &ctx(), &VarStore::default()).len(),
1,
"{payload} is not a released uid form"
);
}
}
#[test]
fn system_id_token_is_the_released_uid_grammar() {
let e = expectation(&serde_json::json!({ "ETag": "pattern:W/\"<system_id>\"" }));
for system_id in [
"ferroehr.local",
"openEHRSys.example.com",
"uk.nhs.ehr1",
"1.2.840.113554",
"8849182c-82ad-4088-a07f-48ead4180515",
] {
let etag = format!("W/\"{system_id}\"");
let ok = response(&[("etag", etag.as_str())]);
assert!(
evaluate(&e, &ok, &ctx(), &VarStore::default()).is_empty(),
"{system_id} is a released uid form"
);
}
for payload in ["a::b", "sys\\\"tem", "not a uid!", ""] {
let etag = format!("W/\"{payload}\"");
let bad = response(&[("etag", etag.as_str())]);
assert_eq!(
evaluate(&e, &bad, &ctx(), &VarStore::default()).len(),
1,
"{payload} is not a released uid form"
);
}
}
#[test]
fn structural_tokens_outrank_a_same_named_variable() {
let e = expectation(&serde_json::json!({
"ETag": "pattern:W/\"<versioned_object_uid>::<system_id>::<n>\""
}));
let mut vars = VarStore::default();
vars.set(
CaptureName::parse("versioned_object_uid").unwrap(),
Captured::Scalar("019fcd6c-d514-7703-9491-b2c8d8413408::ferroehr.local::1".to_owned()),
);
let ok = response(&[(
"etag",
"W/\"019fcd6c-d514-7703-9491-b2c8d8413408::ferroehr.local::1\"",
)]);
assert!(evaluate(&e, &ok, &ctx(), &vars).is_empty());
}
#[test]
fn template_hrid_token_is_the_archetype_hrid_grammar() {
let e = expectation(&serde_json::json!({ "ETag": "pattern:W/\"<template_hrid>\"" }));
for hrid in [
"openEHR-EHR-COMPOSITION.cnf_minimal.v1.0.0",
"org.openehr::openEHR-EHR-OBSERVATION.blood_pressure.v2.1.0",
"openEHR-EHR-COMPOSITION.cnf_minimal.v1.0.0-rc.3",
] {
let etag = format!("W/\"{hrid}\"");
let ok = response(&[("etag", etag.as_str())]);
assert!(
evaluate(&e, &ok, &ctx(), &VarStore::default()).is_empty(),
"{hrid} is a released HRID"
);
}
let prefix = response(&[("etag", "W/\"openEHR-EHR-COMPOSITION.cnf_minimal.v1\"")]);
assert_eq!(evaluate(&e, &prefix, &ctx(), &VarStore::default()).len(), 1);
}
#[test]
fn unresolvable_placeholder_is_a_loud_failure_not_a_wildcard() {
let e = expectation(&serde_json::json!({
"ETag": "pattern:W/\"<no_such_capture>::x\""
}));
let would_pass = response(&[("etag", "W/\"anything::x\"")]);
let failures = evaluate(&e, &would_pass, &ctx(), &VarStore::default());
assert_eq!(failures.len(), 1, "{failures:?}");
assert!(
failures[0].contains("no_such_capture") && failures[0].contains("#1852"),
"the failure names the unresolvable placeholder: {failures:?}"
);
}
#[test]
fn literal_renders_the_template() {
let e = expectation(&serde_json::json!({ "Preference-Applied": "return=minimal" }));
let ok = response(&[("preference-applied", "return=minimal")]);
assert!(evaluate(&e, &ok, &ctx(), &VarStore::default()).is_empty());
let bad = response(&[("preference-applied", "return=representation")]);
assert_eq!(evaluate(&e, &bad, &ctx(), &VarStore::default()).len(), 1);
}
#[test]
fn header_lookup_is_case_insensitive() {
let e = expectation(&serde_json::json!({ "Last-Modified": "present" }));
let ok = response(&[("LAST-MODIFIED", "Wed, 22 Jul 2009 19:15:56 GMT")]);
assert!(evaluate(&e, &ok, &ctx(), &VarStore::default()).is_empty());
}
#[test]
fn optional_presence_still_judges_the_form_when_present() {
let e = expectation(&serde_json::json!({
"ETag": { "match": "pattern:W/\"[^\"]+\"", "optional": true }
}));
assert!(evaluate(&e, &response(&[]), &ctx(), &VarStore::default()).is_empty());
let blank = response(&[("etag", " ")]);
assert!(evaluate(&e, &blank, &ctx(), &VarStore::default()).is_empty());
let weak = response(&[("etag", "W/\"rs-1\"")]);
assert!(evaluate(&e, &weak, &ctx(), &VarStore::default()).is_empty());
let bare = response(&[("etag", "\"rs-1\"")]);
assert_eq!(evaluate(&e, &bare, &ctx(), &VarStore::default()).len(), 1);
}
#[test]
fn a_dated_expectation_binds_only_the_declaring_releases() {
let e = expectation(&serde_json::json!({
"ETag": {
"match": "pattern:W/\"[^\"]+\"",
"applies": { "its_rest": ">=1.1.0" }
}
}));
let bare = response(&[("etag", "\"x::sys::1\"")]);
let v11 = versions("1.1.0");
let judged = RequestContext {
spec_versions: Some(&v11),
..RequestContext::default()
};
assert_eq!(evaluate(&e, &bare, &judged, &VarStore::default()).len(), 1);
let v103 = versions("1.0.3");
let dated_out = RequestContext {
spec_versions: Some(&v103),
..RequestContext::default()
};
assert!(evaluate(&e, &bare, &dated_out, &VarStore::default()).is_empty());
assert!(evaluate(&e, &bare, &ctx(), &VarStore::default()).is_empty());
}
#[test]
fn dating_and_optionality_compose() {
let e = expectation(&serde_json::json!({
"ETag": {
"match": "pattern:W/\"[^\"]+\"",
"optional": true,
"applies": { "its_rest": ">=1.1.0" }
}
}));
let v11 = versions("1.1.0");
let judged = RequestContext {
spec_versions: Some(&v11),
..RequestContext::default()
};
assert!(evaluate(&e, &response(&[]), &judged, &VarStore::default()).is_empty());
let bare = response(&[("etag", "\"rs-1\"")]);
assert_eq!(evaluate(&e, &bare, &judged, &VarStore::default()).len(), 1);
}
}