#![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_json::Value;
use std::collections::BTreeMap;
use reqwest::StatusCode;
use crate::exec::resultset;
use crate::exec::state::{Captured, VarStore};
use crate::model::assertion::{Assertion, ColumnSpec, IgnoreSpec, RowsSpec};
use crate::refgrammar::{Segment, Template, ValueRef};
use crate::vocab::{CellComparison, IgnoreSetName, ResultSetMatch};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssertionFailure(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AssertionOutcome {
Mismatch(String),
Unjudgeable(String),
}
impl AssertionOutcome {
#[must_use]
pub fn reason(&self) -> &str {
match self {
Self::Mismatch(reason) | Self::Unjudgeable(reason) => reason,
}
}
}
impl From<AssertionFailure> for AssertionOutcome {
fn from(failure: AssertionFailure) -> Self {
Self::Mismatch(failure.0)
}
}
#[must_use]
pub fn resolve_path<'a>(root: &'a Value, path: &str) -> Option<&'a Value> {
let mut current = root;
for raw in path.split('/').filter(|s| !s.is_empty()) {
let (attr, index) = match raw.split_once('[') {
Some((attr, rest)) => {
let index: usize = rest.strip_suffix(']')?.parse().ok()?;
(attr, Some(index))
}
None => (raw, None),
};
if !attr.is_empty() {
current = current.get(attr)?;
}
if let Some(i) = index {
current = current.get(i)?;
}
}
Some(current)
}
pub fn render_template(template: &Template, vars: &VarStore) -> Result<String, String> {
let mut out = String::new();
for segment in template.segments() {
match segment {
Segment::Lit(s) => out.push_str(s),
Segment::Ref(ValueRef::Capture { name, .. }) => match vars.get(name) {
Some(Captured::Scalar(s)) => out.push_str(s),
Some(_) => return Err(format!("capture {name} is not scalar")),
None => return Err(format!("capture {name} is not bound")),
},
Segment::Ref(other) => {
return Err(format!("reference {other} must be resolved by the driver"));
}
}
}
Ok(out)
}
#[must_use]
pub fn strip_ignored(value: &Value, ignored_paths: &[String]) -> Value {
fn remove(value: &mut Value, segments: &[&str]) {
let Some((head, rest)) = segments.split_first() else {
return;
};
if *head == "**" {
remove(value, rest);
match value {
Value::Object(map) => {
for child in map.values_mut() {
remove(child, segments);
}
}
Value::Array(items) => {
for item in items {
remove(item, segments);
}
}
_ => {}
}
return;
}
match value {
Value::Object(map) => {
if rest.is_empty() {
map.remove(*head);
} else if let Some(next) = map.get_mut(*head) {
remove(next, rest);
}
}
Value::Array(items) => {
for item in items {
remove(item, segments);
}
}
_ => {}
}
}
let mut out = value.clone();
for path in ignored_paths {
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
remove(&mut out, &segments);
}
out
}
fn is_flat_map(value: &Value) -> bool {
match value {
Value::Object(map) => {
!map.is_empty()
&& map.keys().any(|k| k.contains('/'))
&& map.values().all(|v| !matches!(v, Value::Object(_)))
}
_ => false,
}
}
fn fold_flat_ctx(committed: &BTreeMap<String, Value>, root: &str) -> BTreeMap<String, Value> {
let id_namespace = committed.get("ctx/id_namespace").cloned();
let id_scheme = committed.get("ctx/id_scheme").cloned();
let mut out: BTreeMap<String, Value> = BTreeMap::new();
let mut folded_id_carriers: Vec<String> = Vec::new();
for (key, value) in committed {
if let Some(rest) = key.strip_prefix("ctx/participation_") {
let (field, index) = match rest.split_once(':') {
Some((f, i)) => (f, i),
None => (rest, "0"),
};
let target = format!("{root}/context/_participation:{index}|{field}");
if field == "id" {
folded_id_carriers.push(format!("{root}/context/_participation:{index}"));
}
out.insert(target, value.clone());
} else if let Some(rest) = key.strip_prefix("ctx/health_care_facility|") {
if rest == "id" {
folded_id_carriers.push(format!("{root}/context/_health_care_facility"));
}
out.insert(
format!("{root}/context/_health_care_facility|{rest}"),
value.clone(),
);
} else if key == "ctx/id_namespace" || key == "ctx/id_scheme" {
} else {
out.insert(key.clone(), value.clone());
}
}
for carrier in folded_id_carriers {
if let Some(ns) = &id_namespace {
out.insert(format!("{carrier}|id_namespace"), ns.clone());
}
if let Some(scheme) = &id_scheme {
out.insert(format!("{carrier}|id_scheme"), scheme.clone());
}
}
out
}
fn flat_key_ignored(key: &str, ignored_paths: &[String]) -> bool {
let effective: &str = match key {
"ctx/time" => "context/start_time",
"ctx/end_time" => "context/end_time",
"ctx/setting" => "context/setting",
k if k.starts_with("ctx/composer") => "composer",
k => k.split_once('/').map_or(k, |(_, rest)| rest),
};
let effective = effective.replace("/_uid", "/uid");
let effective = effective.strip_prefix('_').unwrap_or(&effective);
ignored_paths.iter().any(|p| {
effective == p.as_str()
|| effective.starts_with(&format!("{p}/"))
|| effective.starts_with(&format!("{p}|"))
})
}
fn dezero(key: &str) -> String {
let inner = key.replace(":0/", "/").replace(":0|", "|");
inner
.strip_suffix(":0")
.map_or(inner.clone(), ToOwned::to_owned)
}
fn flatten_structured(value: &Value, prefix: &str, out: &mut BTreeMap<String, Value>) {
match value {
Value::Object(map) => {
for (key, child) in map {
let next = if key.is_empty() {
prefix.to_owned()
} else if let Some(attr) = key.strip_prefix('|') {
format!("{prefix}|{attr}")
} else if prefix.is_empty() {
key.clone()
} else {
format!("{prefix}/{key}")
};
flatten_structured(child, &next, out);
}
}
Value::Array(items) => {
for (i, item) in items.iter().enumerate() {
flatten_structured(item, &format!("{prefix}:{i}"), out);
}
}
leaf => {
out.insert(prefix.to_owned(), leaf.clone());
}
}
}
fn has_simplified_leaf_keys(value: &Value) -> bool {
match value {
Value::Object(map) => map
.iter()
.any(|(k, v)| k.is_empty() || k.starts_with('|') || has_simplified_leaf_keys(v)),
Value::Array(items) => items.iter().any(has_simplified_leaf_keys),
_ => false,
}
}
fn simplified_as_flat(value: &Value) -> Option<BTreeMap<String, Value>> {
let Value::Object(map) = value else {
return None;
};
if is_flat_map(value) {
return Some(map.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
}
if !map.is_empty()
&& map.keys().all(|k| !k.contains('/'))
&& !map.contains_key("_type")
&& (map.contains_key("ctx") || has_simplified_leaf_keys(value))
{
let mut out = BTreeMap::new();
flatten_structured(value, "", &mut out);
return Some(out);
}
None
}
fn flat_equivalent(
actual: &BTreeMap<String, Value>,
committed: &BTreeMap<String, Value>,
ignored_paths: &[String],
) -> bool {
let root = actual
.keys()
.find(|k| !k.starts_with("ctx/"))
.and_then(|k| k.split(['/', ':']).next())
.unwrap_or_default()
.to_owned();
let normalized: BTreeMap<String, &Value> = actual.iter().map(|(k, v)| (dezero(k), v)).collect();
let folded = fold_flat_ctx(committed, &root);
folded
.iter()
.filter(|(k, _)| !flat_key_ignored(k, ignored_paths))
.all(|(k, want)| {
normalized
.get(&dezero(k))
.is_some_and(|got| resultset::cells_equal(got, want))
})
}
#[must_use]
pub fn equivalent(actual: &Value, expected: &Value, ignored_paths: &[String]) -> bool {
if let (Some(a), Some(e)) = (simplified_as_flat(actual), simplified_as_flat(expected)) {
return flat_equivalent(&a, &e, ignored_paths);
}
let a = strip_ignored(actual, ignored_paths);
let b = strip_ignored(expected, ignored_paths);
rm_cells_equal(&a, &b)
}
fn rm_cells_equal(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Object(x), Value::Object(y)) => {
let keys: std::collections::BTreeSet<&str> = x
.keys()
.chain(y.keys())
.map(String::as_str)
.filter(|k| *k != "_type")
.collect();
let type_tags_agree = match (x.get("_type"), y.get("_type")) {
(Some(ta), Some(tb)) => ta == tb,
_ => true,
};
type_tags_agree
&& keys.iter().all(|k| match (x.get(*k), y.get(*k)) {
(Some(va), Some(vb)) => rm_cells_equal(va, vb),
_ => false,
})
}
(Value::Array(x), Value::Array(y)) => {
x.len() == y.len() && x.iter().zip(y).all(|(va, vb)| rm_cells_equal(va, vb))
}
_ => resultset::cells_equal(a, b),
}
}
#[must_use]
pub fn resolve_ignore_sets(
specs: &[IgnoreSpec],
server_assigned: &[String],
ctx_defaults: &[String],
) -> Vec<String> {
let mut paths = Vec::new();
for spec in specs {
match spec {
IgnoreSpec::Named(IgnoreSetName::ServerAssigned) => {
paths.extend(server_assigned.iter().cloned());
}
IgnoreSpec::Named(IgnoreSetName::CtxDefaults) => {
paths.extend(ctx_defaults.iter().cloned());
}
IgnoreSpec::Path(p) => paths.push(p.clone()),
}
}
paths
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors the assertion's field set"
)]
pub fn eval_field(
body: &Value,
path: &str,
equals: Option<&Value>,
not_equals: Option<&Value>,
exists: Option<bool>,
absent: Option<bool>,
matches: Option<&str>,
absent_or_matches: Option<&str>,
) -> Result<(), AssertionFailure> {
let found = resolve_path(body, path);
if let Some(true) = exists {
return found
.map(|_| ())
.ok_or_else(|| AssertionFailure(format!("{path}: expected present, is absent")));
}
if let Some(true) = absent {
return match found {
None => Ok(()),
Some(v) => Err(AssertionFailure(format!(
"{path}: expected absent, found {v}"
))),
};
}
if let Some(pattern) = absent_or_matches {
return match found {
None => Ok(()),
Some(actual) => match_serialized(path, actual, pattern),
};
}
let Some(actual) = found else {
return Err(AssertionFailure(format!(
"{path}: path resolves to nothing"
)));
};
if let Some(want) = equals {
if resultset::cells_equal(actual, want) {
return Ok(());
}
return Err(AssertionFailure(format!(
"{path}: {actual} != expected {want}"
)));
}
if let Some(reject) = not_equals {
if resultset::cells_equal(actual, reject) {
return Err(AssertionFailure(format!(
"{path}: equals the client-supplied value {reject} (must be server-set)"
)));
}
return Ok(());
}
if let Some(pattern) = matches {
return match_serialized(path, actual, pattern);
}
Ok(())
}
fn match_serialized(path: &str, actual: &Value, pattern: &str) -> Result<(), AssertionFailure> {
let re = regex::Regex::new(pattern)
.map_err(|e| AssertionFailure(format!("{path}: pattern does not compile: {e}")))?;
let text = match actual {
Value::String(s) => s.clone(),
other => other.to_string(),
};
if re.is_match(&text) {
return Ok(());
}
Err(AssertionFailure(format!(
"{path}: {text:?} does not match {pattern:?}"
)))
}
pub fn eval_unique(
over: &crate::ids::CaptureName,
all_rows: &[VarStore],
) -> Result<(), AssertionFailure> {
let mut seen: Vec<&str> = Vec::new();
for (row, store) in all_rows.iter().enumerate() {
let Some(value) = store.scalar(over) else {
continue; };
if seen.contains(&value) {
return Err(AssertionFailure(format!(
"unique over ${{{over}}}: value {value:?} repeats at row {row}"
)));
}
seen.push(value);
}
Ok(())
}
pub fn eval_returns(
body: &Value,
equals: Option<&Value>,
matches: Option<&str>,
omits: Option<&str>,
) -> Result<(), AssertionFailure> {
if let Some(want) = equals {
if resultset::cells_equal(body, want) {
return Ok(());
}
return Err(AssertionFailure(format!(
"returns: {body} != expected {want}"
)));
}
let text = match body {
Value::String(s) => s.clone(),
other => other.to_string(),
};
if let Some(pattern) = matches {
let re = regex::Regex::new(pattern)
.map_err(|e| AssertionFailure(format!("returns pattern does not compile: {e}")))?;
if !re.is_match(&text) {
return Err(AssertionFailure(format!(
"returns: {text:?} does not match {pattern:?}"
)));
}
}
if let Some(pattern) = omits {
let re = regex::Regex::new(pattern).map_err(|e| {
AssertionFailure(format!("returns omits pattern does not compile: {e}"))
})?;
if re.is_match(&text) {
return Err(AssertionFailure(format!(
"returns: {text:?} matches {pattern:?} but must omit it"
)));
}
}
Ok(())
}
pub fn eval_returns_wire(
status: StatusCode,
body: &Value,
equals: Option<&Value>,
matches: Option<&str>,
omits: Option<&str>,
) -> Result<(), AssertionFailure> {
let Some(Value::Bool(want)) = equals else {
return eval_returns(body, equals, matches, omits);
};
let observed = status.is_success();
if observed == *want {
return Ok(());
}
Err(AssertionFailure(format!(
"returns: wire presence {observed} != expected {want} (status {})",
status.as_u16()
)))
}
pub fn eval_instance_of(body: &Value, rm_type: &str) -> Result<(), AssertionFailure> {
match body.get("_type").and_then(Value::as_str) {
Some(t) if t == rm_type => Ok(()),
Some(t) => Err(AssertionFailure(format!(
"instance_of: body is {t}, expected {rm_type}"
))),
None => Err(AssertionFailure(format!(
"instance_of: body carries no _type (expected {rm_type})"
))),
}
}
#[derive(Debug, Clone, Copy)]
pub struct ResolvedResultSet<'a> {
pub match_mode: ResultSetMatch,
pub rows: Option<&'a [Value]>,
pub count: Option<u64>,
pub columns: Option<&'a [ColumnSpec]>,
pub cells: CellComparison,
}
pub fn eval_result_set_against(
body: &Value,
expectation: ResolvedResultSet<'_>,
) -> Result<Vec<String>, AssertionFailure> {
let ResolvedResultSet {
match_mode,
rows,
count,
columns,
cells,
} = expectation;
let mut cmp = resultset::CellComparator::new(cells);
if let Some(cols) = columns {
let names: Vec<String> = cols.iter().map(|c| c.name.clone()).collect();
resultset::compare_columns(body, &names).map_err(|e| AssertionFailure(e.0))?;
}
let outcome = match (match_mode, rows, count) {
(ResultSetMatch::Count, _, Some(n)) => resultset::compare_count(body, n),
(ResultSetMatch::Ordered, Some(rows), _) => {
resultset::compare_ordered(body, rows, &mut cmp)
}
(ResultSetMatch::Set, Some(rows), _) => resultset::compare_bag(body, rows, &mut cmp),
(ResultSetMatch::Contains, Some(rows), _) => {
resultset::compare_contains(body, rows, &mut cmp)
}
_ => {
return Err(AssertionFailure(
"result_set: no comparable expectation resolved".into(),
));
}
};
outcome.map_err(|e| AssertionFailure(e.0))?;
Ok(recorded_divergences(&cmp))
}
fn recorded_divergences(cmp: &resultset::CellComparator) -> Vec<String> {
cmp.divergences()
.iter()
.map(|d| {
format!(
"result_set: {d} — ITS-REST docs/overview/Resources.md §Datetime format puts the \
served spelling at SHOULD strength, so the row still passes"
)
})
.collect()
}
const XSI_NAMESPACE: &str = "http://www.w3.org/2001/XMLSchema-instance";
#[derive(Debug, Clone, PartialEq, Eq)]
struct XmlRootElement {
local: String,
namespace: Option<String>,
xsi_type: Option<(String, Option<String>)>,
}
fn root_xsi_type(
reader: &mut quick_xml::NsReader<&[u8]>,
start: &quick_xml::events::BytesStart<'_>,
) -> Result<Option<(String, Option<String>)>, AssertionFailure> {
for attribute in start.attributes() {
let attribute = attribute
.map_err(|e| AssertionFailure(format!("xml_root: body is not well-formed XML: {e}")))?;
let (attribute_ns, attribute_local) =
reader.resolver_mut().resolve_attribute(attribute.key);
let is_xsi_type = attribute_local.as_ref() == b"type"
&& matches!(
attribute_ns,
quick_xml::name::ResolveResult::Bound(ns) if ns.as_ref() == XSI_NAMESPACE.as_bytes()
);
if !is_xsi_type {
continue;
}
let value = attribute
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.map_err(|e| AssertionFailure(format!("xml_root: xsi:type is not readable: {e}")))?;
let (type_ns, type_local) = reader
.resolver_mut()
.resolve_element(quick_xml::name::QName(value.as_bytes()));
let namespace = match type_ns {
quick_xml::name::ResolveResult::Bound(ns) => {
Some(String::from_utf8_lossy(ns.as_ref()).into_owned())
}
quick_xml::name::ResolveResult::Unbound => None,
quick_xml::name::ResolveResult::Unknown(prefix) => {
return Err(AssertionFailure(format!(
"xml_root: the xsi:type QName's prefix `{}` is not bound to any namespace",
String::from_utf8_lossy(&prefix)
)));
}
};
return Ok(Some((
String::from_utf8_lossy(type_local.as_ref()).into_owned(),
namespace,
)));
}
Ok(None)
}
fn xml_root_element(text: &str) -> Result<XmlRootElement, AssertionFailure> {
let mut reader = quick_xml::NsReader::from_str(text);
let mut root: Option<XmlRootElement> = None;
let mut depth: i64 = 0;
loop {
let (resolved, event) = reader
.read_resolved_event()
.map_err(|e| AssertionFailure(format!("xml_root: body is not well-formed XML: {e}")))?;
if matches!(event, quick_xml::events::Event::Start(_)) {
depth += 1;
} else if matches!(event, quick_xml::events::Event::End(_)) {
depth -= 1;
}
match event {
quick_xml::events::Event::Eof => break,
quick_xml::events::Event::Start(e) | quick_xml::events::Event::Empty(e)
if root.is_none() =>
{
let local = String::from_utf8_lossy(e.local_name().as_ref()).into_owned();
let namespace = match resolved {
quick_xml::name::ResolveResult::Bound(ns) => {
Some(String::from_utf8_lossy(ns.as_ref()).into_owned())
}
quick_xml::name::ResolveResult::Unbound => None,
quick_xml::name::ResolveResult::Unknown(prefix) => {
return Err(AssertionFailure(format!(
"xml_root: the root element's prefix `{}` is not bound to any namespace",
String::from_utf8_lossy(&prefix)
)));
}
};
let xsi_type = root_xsi_type(&mut reader, &e)?;
root = Some(XmlRootElement {
local,
namespace,
xsi_type,
});
}
_ => {}
}
}
if depth != 0 {
return Err(AssertionFailure(
"xml_root: body is not well-formed XML: the document ends with unclosed elements"
.to_owned(),
));
}
root.ok_or_else(|| AssertionFailure("xml_root: body carries no XML element at all".to_owned()))
}
pub fn eval_xml_root(
body: &Value,
name: &str,
namespace: Option<crate::vocab::XmlNamespace>,
xsi_type: Option<&str>,
) -> Result<(), AssertionFailure> {
let Value::String(text) = body else {
return Err(AssertionFailure(format!(
"xml_root: expected a canonical-XML document body, got {}",
match body {
Value::Null => "no body".to_owned(),
other => other.to_string().chars().take(80).collect::<String>(),
}
)));
};
let root = xml_root_element(text)?;
let local = root.local;
if local != name {
return Err(AssertionFailure(format!(
"xml_root: document root is `{local}`, expected the published document element `{name}`"
)));
}
if let Some(expected) = namespace {
match root.namespace.as_deref() {
Some(uri) if expected.accepts(uri) => {}
Some(uri) => {
return Err(AssertionFailure(format!(
"xml_root: root `{local}` is in namespace {uri:?}, expected {}",
expected.token()
)));
}
None => {
return Err(AssertionFailure(format!(
"xml_root: root `{local}` is in NO namespace, expected {} — every published \
ITS-XML schema declares elementFormDefault=\"qualified\" over its \
targetNamespace, so a conforming document's root is namespace-qualified",
expected.token()
)));
}
}
}
let Some(expected_type) = xsi_type else {
return Ok(());
};
let Some((type_local, type_uri)) = root.xsi_type else {
return Err(AssertionFailure(format!(
"xml_root: root `{local}` carries no xsi:type, expected `{expected_type}` — the \
published element's declared type is abstract, and an instance may not use an \
abstract type directly"
)));
};
if type_local != expected_type {
return Err(AssertionFailure(format!(
"xml_root: root `{local}` names concrete type `{type_local}`, expected \
`{expected_type}`"
)));
}
if let Some(expected) = namespace {
match type_uri.as_deref() {
Some(uri) if expected.accepts(uri) => {}
Some(uri) => {
return Err(AssertionFailure(format!(
"xml_root: xsi:type `{type_local}` is in namespace {uri:?}, expected {}",
expected.token()
)));
}
None => {
return Err(AssertionFailure(format!(
"xml_root: xsi:type `{type_local}` resolves to NO namespace, expected {} — \
the ITS-XML complexTypes are declared in each schema's targetNamespace",
expected.token()
)));
}
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Judgement {
PerStep,
Aggregate,
Informative,
}
#[must_use]
pub fn judgement_of(assertion: &Assertion) -> Judgement {
match assertion {
Assertion::Field { .. }
| Assertion::Equivalent { .. }
| Assertion::Returns { .. }
| Assertion::ResultSet { .. }
| Assertion::XmlRoot { .. }
| Assertion::InstanceOf { .. }
| Assertion::Signature { .. }
| Assertion::Version { .. } => Judgement::PerStep,
Assertion::Unique { .. } => Judgement::Aggregate,
Assertion::MessageExemplar { .. } | Assertion::State { .. } => Judgement::Informative,
}
}
#[derive(Debug, Clone, Copy)]
pub struct ExchangeFacts<'a> {
pub status: StatusCode,
pub body: &'a Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReplayJudgement {
FromExchange,
Unrecorded,
NotPerStep,
}
#[must_use]
pub fn replay_judgement(assertion: &Assertion) -> ReplayJudgement {
match assertion {
Assertion::Returns { .. } | Assertion::XmlRoot { .. } | Assertion::InstanceOf { .. } => {
ReplayJudgement::FromExchange
}
Assertion::Field {
equals, not_equals, ..
} => {
if equals
.iter()
.chain(not_equals)
.all(|value| value.literal().is_some())
{
ReplayJudgement::FromExchange
} else {
ReplayJudgement::Unrecorded
}
}
Assertion::ResultSet { rows, .. } => match rows {
Some(RowsSpec::From(_)) => ReplayJudgement::Unrecorded,
Some(RowsSpec::Inline(_)) | None => ReplayJudgement::FromExchange,
},
Assertion::Equivalent { .. } | Assertion::Signature { .. } | Assertion::Version { .. } => {
ReplayJudgement::Unrecorded
}
Assertion::Unique { .. } | Assertion::MessageExemplar { .. } | Assertion::State { .. } => {
ReplayJudgement::NotPerStep
}
}
}
pub fn eval_from_exchange(
assertion: &Assertion,
facts: ExchangeFacts<'_>,
) -> Result<Vec<String>, AssertionOutcome> {
let unrecorded = || {
AssertionOutcome::Unjudgeable(format!(
"{}: the recorded exchange carries no ground for this family",
assertion.family()
))
};
match replay_judgement(assertion) {
ReplayJudgement::NotPerStep => return Ok(Vec::new()),
ReplayJudgement::Unrecorded => return Err(unrecorded()),
ReplayJudgement::FromExchange => {}
}
let body = facts.body;
let judged: Result<(), AssertionFailure> = match assertion {
Assertion::Field {
path,
equals,
not_equals,
exists,
absent,
matches,
absent_or_matches,
} => eval_field(
body,
path,
equals
.as_ref()
.and_then(crate::model::value::TemplatedValue::literal)
.as_ref(),
not_equals
.as_ref()
.and_then(crate::model::value::TemplatedValue::literal)
.as_ref(),
*exists,
*absent,
matches.as_deref(),
absent_or_matches.as_deref(),
),
Assertion::Returns {
equals,
matches,
omits,
} => eval_returns_wire(
facts.status,
body,
equals.as_ref(),
matches.as_deref(),
omits.as_deref(),
),
Assertion::XmlRoot {
name,
namespace,
xsi_type,
} => eval_xml_root(body, name, *namespace, xsi_type.as_deref()),
Assertion::InstanceOf { rm_type, .. } => eval_instance_of(body, rm_type),
Assertion::ResultSet {
match_mode,
rows,
count,
columns,
cells,
} => {
let inline: Option<Vec<Value>> = match rows {
Some(RowsSpec::Inline(rows)) => {
Some(rows.iter().map(|r| Value::Array(r.clone())).collect())
}
Some(RowsSpec::From(_)) | None => None,
};
return eval_result_set_against(
body,
ResolvedResultSet {
match_mode: *match_mode,
rows: inline.as_deref(),
count: *count,
columns: columns.as_deref(),
cells: cells.unwrap_or_default(),
},
)
.map_err(AssertionOutcome::from);
}
Assertion::Equivalent { .. }
| Assertion::Signature { .. }
| Assertion::Version { .. }
| Assertion::Unique { .. }
| Assertion::MessageExemplar { .. }
| Assertion::State { .. } => return Err(unrecorded()),
};
judged.map(|()| Vec::new()).map_err(AssertionOutcome::from)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn every_assertion_variant_declares_when_it_is_judged() {
let cases: &[(Value, Judgement)] = &[
(
json!({ "assert": "field", "path": "uid/value", "exists": true }),
Judgement::PerStep,
),
(
json!({ "assert": "equivalent", "to": "committed" }),
Judgement::PerStep,
),
(
json!({ "assert": "returns", "equals": true }),
Judgement::PerStep,
),
(
json!({ "assert": "result_set", "match": "count", "count": 1 }),
Judgement::PerStep,
),
(
json!({ "assert": "xml_root", "name": "composition" }),
Judgement::PerStep,
),
(
json!({ "assert": "instance_of", "rm_type": "COMPOSITION" }),
Judgement::PerStep,
),
(
json!({ "assert": "signature", "of": "${v1}", "present": true }),
Judgement::PerStep,
),
(
json!({ "assert": "version", "count": 1 }),
Judgement::PerStep,
),
(
json!({ "assert": "unique", "over": "${new_ehr_id}", "aggregate": true }),
Judgement::Aggregate,
),
(
json!({ "assert": "message_exemplar", "text": "EHR not found" }),
Judgement::Informative,
),
(
json!({ "assert": "state", "text": "the EHR exists" }),
Judgement::Informative,
),
];
for (document, expected) in cases {
let assertion: Assertion = serde_json::from_value(document.clone())
.unwrap_or_else(|e| panic!("{document} does not parse: {e}"));
assert_eq!(
judgement_of(&assertion),
*expected,
"{document} changed judgement"
);
}
}
#[test]
fn every_assertion_variant_declares_what_a_recorded_exchange_decides() {
let cases: &[(Value, ReplayJudgement)] = &[
(
json!({ "assert": "field", "path": "uid/value", "exists": true }),
ReplayJudgement::FromExchange,
),
(
json!({ "assert": "field", "path": "ehr_id/value", "equals": "fixed" }),
ReplayJudgement::FromExchange,
),
(
json!({ "assert": "field", "path": "ehr_id/value", "equals": "${first_ehr_id}" }),
ReplayJudgement::Unrecorded,
),
(
json!({ "assert": "equivalent", "to": "committed" }),
ReplayJudgement::Unrecorded,
),
(
json!({ "assert": "returns", "equals": true }),
ReplayJudgement::FromExchange,
),
(
json!({ "assert": "result_set", "match": "count", "count": 1 }),
ReplayJudgement::FromExchange,
),
(
json!({ "assert": "result_set", "match": "ordered", "rows": [["a"]] }),
ReplayJudgement::FromExchange,
),
(
json!({ "assert": "result_set", "match": "ordered",
"rows": { "from": "${ds:cnf.set.bp-10#magnitude_ge_140_by_uid}" } }),
ReplayJudgement::Unrecorded,
),
(
json!({ "assert": "xml_root", "name": "composition" }),
ReplayJudgement::FromExchange,
),
(
json!({ "assert": "instance_of", "rm_type": "COMPOSITION" }),
ReplayJudgement::FromExchange,
),
(
json!({ "assert": "signature", "of": "${v1}", "present": true }),
ReplayJudgement::Unrecorded,
),
(
json!({ "assert": "version", "count": 1 }),
ReplayJudgement::Unrecorded,
),
(
json!({ "assert": "unique", "over": "${new_ehr_id}", "aggregate": true }),
ReplayJudgement::NotPerStep,
),
(
json!({ "assert": "message_exemplar", "text": "EHR not found" }),
ReplayJudgement::NotPerStep,
),
(
json!({ "assert": "state", "text": "the EHR exists" }),
ReplayJudgement::NotPerStep,
),
];
for (document, expected) in cases {
let assertion: Assertion = serde_json::from_value(document.clone())
.unwrap_or_else(|e| panic!("{document} does not parse: {e}"));
assert_eq!(
replay_judgement(&assertion),
*expected,
"{document} changed its replay classification"
);
}
}
#[test]
fn the_recorded_dispatch_judges_or_refuses_but_never_passes_silently() {
let body = json!({ "_type": "EHR", "ehr_id": { "value": "e-1" } });
let facts = ExchangeFacts {
status: StatusCode::OK,
body: &body,
};
let parse = |document: Value| -> Assertion {
serde_json::from_value(document).expect("the assertion parses")
};
assert_eq!(
eval_from_exchange(
&parse(json!({ "assert": "instance_of", "rm_type": "EHR" })),
facts
),
Ok(Vec::new())
);
let mismatch = eval_from_exchange(
&parse(json!({ "assert": "instance_of", "rm_type": "FOLDER" })),
facts,
)
.expect_err("a body of another type contradicts the assertion");
assert!(
matches!(mismatch, AssertionOutcome::Mismatch(_)),
"{mismatch:?}"
);
let unjudgeable = eval_from_exchange(
&parse(json!({ "assert": "equivalent", "to": "committed" })),
facts,
)
.expect_err("no committed payload is recorded");
assert!(
matches!(unjudgeable, AssertionOutcome::Unjudgeable(_)),
"{unjudgeable:?}"
);
assert_eq!(
eval_from_exchange(
&parse(json!({ "assert": "state", "text": "the EHR exists" })),
facts
),
Ok(Vec::new())
);
}
#[test]
fn a_boolean_returns_is_judged_by_wire_presence() {
let body = Value::Null;
assert!(eval_returns_wire(StatusCode::OK, &body, Some(&json!(true)), None, None).is_ok());
assert!(
eval_returns_wire(
StatusCode::NOT_FOUND,
&body,
Some(&json!(false)),
None,
None
)
.is_ok()
);
assert!(
eval_returns_wire(StatusCode::NOT_FOUND, &body, Some(&json!(true)), None, None)
.is_err()
);
assert!(
eval_returns_wire(StatusCode::OK, &json!("v1.2"), None, Some("^v1\\."), None).is_ok()
);
}
#[test]
fn path_resolution_addresses_objects_and_lists() {
let body = json!({
"context": { "setting": { "value": "other care" } },
"content": [ { "data": { "events": [ { "time": "t0" } ] } } ]
});
assert_eq!(
resolve_path(&body, "context/setting/value").unwrap(),
&json!("other care")
);
assert_eq!(
resolve_path(&body, "content[0]/data/events[0]/time").unwrap(),
&json!("t0")
);
assert!(resolve_path(&body, "content[1]").is_none());
}
#[test]
fn recursive_ignore_segment_strips_every_depth() {
let tree = json!({
"_type": "FOLDER",
"uid": { "_type": "OBJECT_VERSION_ID", "value": "r::s::1" },
"folders": [
{
"_type": "FOLDER",
"uid": { "_type": "HIER_OBJECT_ID", "value": "a" },
"name": { "value": "emergency" },
"folders": [
{ "_type": "FOLDER", "uid": { "value": "b" }, "name": { "value": "episode" } }
]
}
]
});
let shallow = strip_ignored(&tree, &["uid".to_owned()]);
assert!(shallow.get("uid").is_none());
assert!(shallow["folders"][0].get("uid").is_some());
let deep = strip_ignored(&tree, &["**/uid".to_owned()]);
assert!(deep.get("uid").is_none());
assert!(deep["folders"][0].get("uid").is_none());
assert!(deep["folders"][0]["folders"][0].get("uid").is_none());
assert_eq!(deep["folders"][0]["name"]["value"], json!("emergency"));
assert_eq!(
deep["folders"][0]["folders"][0]["name"]["value"],
json!("episode")
);
}
#[test]
fn field_predicates() {
let body =
json!({ "is_queryable": true, "audit": { "time_committed": "2026-07-21T10:00:00Z" } });
assert!(
eval_field(
&body,
"is_queryable",
Some(&json!(true)),
None,
None,
None,
None,
None
)
.is_ok()
);
assert!(
eval_field(
&body,
"is_queryable",
None,
None,
Some(true),
None,
None,
None
)
.is_ok()
);
assert!(eval_field(&body, "subject", None, None, None, Some(true), None, None).is_ok());
assert!(
eval_field(
&body,
"audit/time_committed",
None,
Some(&json!("1990-01-01T00:00:00Z")),
None,
None,
None,
None
)
.is_ok()
);
assert!(
eval_field(
&body,
"audit/time_committed",
None,
Some(&json!("2026-07-21T10:00:00Z")),
None,
None,
None,
None
)
.is_err()
);
}
#[test]
fn equivalence_strips_normative_ignore_sets_only() {
let committed = json!({ "name": { "value": "v1" }, "content": [{"x": 1}] });
let served = json!({
"uid": { "value": "generated::sut::1" },
"name": { "value": "v1" },
"content": [{"x": 1}]
});
assert!(equivalent(&served, &committed, &["uid".to_owned()]));
assert!(!equivalent(&served, &committed, &[])); }
#[test]
fn xml_root_judges_the_published_element_and_its_namespace() {
use crate::vocab::XmlNamespace;
let v1 = Value::String(
r#"<?xml version="1.0" encoding="UTF-8"?>
<composition xmlns="http://schemas.openehr.org/v1"><name/></composition>"#
.to_owned(),
);
assert!(eval_xml_root(&v1, "composition", Some(XmlNamespace::Published), None).is_ok());
assert!(eval_xml_root(&v1, "composition", Some(XmlNamespace::V1), None).is_ok());
assert!(eval_xml_root(&v1, "composition", Some(XmlNamespace::V2), None).is_err());
let prefixed = Value::String(
r#"<oe:composition xmlns:oe="http://schemas.openehr.org/v2"/>"#.to_owned(),
);
assert!(
eval_xml_root(
&prefixed,
"composition",
Some(XmlNamespace::Published),
None
)
.is_ok()
);
let unqualified = Value::String(r#"<composition archetype_node_id="x"/>"#.to_owned());
let failure = eval_xml_root(
&unqualified,
"composition",
Some(XmlNamespace::Published),
None,
)
.expect_err("an unqualified root must fail");
assert!(failure.0.contains("NO namespace"), "{failure:?}");
assert!(eval_xml_root(&unqualified, "composition", None, None).is_ok());
let wrong_name =
Value::String(r#"<folder xmlns="http://schemas.openehr.org/v1"/>"#.to_owned());
assert!(eval_xml_root(&wrong_name, "composition", None, None).is_err());
assert!(
eval_xml_root(
&json!({ "_type": "COMPOSITION" }),
"composition",
None,
None
)
.is_err()
);
assert!(eval_xml_root(&Value::Null, "composition", None, None).is_err());
let malformed = Value::String("<composition>".to_owned());
assert!(eval_xml_root(&malformed, "composition", None, None).is_err());
}
#[test]
fn xml_root_judges_the_concrete_type_of_an_abstract_root() {
use crate::vocab::XmlNamespace;
let original = Value::String(
r#"<version xmlns="http://schemas.openehr.org/v1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:type="ORIGINAL_VERSION"><uid/></version>"#
.to_owned(),
);
assert!(
eval_xml_root(
&original,
"version",
Some(XmlNamespace::Published),
Some("ORIGINAL_VERSION")
)
.is_ok()
);
let failure = eval_xml_root(
&original,
"version",
Some(XmlNamespace::Published),
Some("IMPORTED_VERSION"),
)
.expect_err("a different concrete type must fail");
assert!(failure.0.contains("ORIGINAL_VERSION"), "{failure:?}");
let prefixed = Value::String(
r#"<oe:version xmlns:oe="http://schemas.openehr.org/v1"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
i:type="oe:IMPORTED_VERSION"/>"#
.to_owned(),
);
assert!(
eval_xml_root(
&prefixed,
"version",
Some(XmlNamespace::Published),
Some("IMPORTED_VERSION")
)
.is_ok()
);
let bare = Value::String(r#"<version xmlns="http://schemas.openehr.org/v1"/>"#.to_owned());
let failure = eval_xml_root(
&bare,
"version",
Some(XmlNamespace::Published),
Some("ORIGINAL_VERSION"),
)
.expect_err("an abstract root must name its concrete type");
assert!(failure.0.contains("no xsi:type"), "{failure:?}");
assert!(eval_xml_root(&bare, "version", Some(XmlNamespace::Published), None).is_ok());
let foreign = Value::String(
r#"<version xmlns="http://schemas.openehr.org/v1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:x="http://example.org/other"
xsi:type="x:ORIGINAL_VERSION"/>"#
.to_owned(),
);
let failure = eval_xml_root(
&foreign,
"version",
Some(XmlNamespace::Published),
Some("ORIGINAL_VERSION"),
)
.expect_err("a foreign type namespace must fail");
assert!(failure.0.contains("example.org"), "{failure:?}");
let unbound = Value::String(
r#"<version xmlns="http://schemas.openehr.org/v1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:type="nope:ORIGINAL_VERSION"/>"#
.to_owned(),
);
assert!(
eval_xml_root(
&unbound,
"version",
Some(XmlNamespace::Published),
Some("ORIGINAL_VERSION")
)
.is_err()
);
}
#[test]
fn unique_is_aggregate_across_rows() {
let name = crate::ids::CaptureName::parse("new_ehr_id").unwrap();
let mut a = VarStore::default();
a.set(name.clone(), Captured::Scalar("id-1".into()));
let mut b = VarStore::default();
b.set(name.clone(), Captured::Scalar("id-2".into()));
assert!(eval_unique(&name, &[a.clone(), b]).is_ok());
let mut c = VarStore::default();
c.set(name.clone(), Captured::Scalar("id-1".into()));
assert!(eval_unique(&name, &[a, c]).is_err());
}
#[test]
fn a_flat_body_is_recognized_by_its_single_level_of_path_keys() {
let flat = json!({
"ctx/language": "en",
"vital_signs/body_temperature:0/any_event:0/temperature|magnitude": 37.5,
"vital_signs/body_temperature:0/any_event:0/temperature|unit": "°C"
});
assert!(is_flat_map(&flat));
assert!(!is_flat_map(&json!({
"_type": "COMPOSITION",
"name": { "value": "Vital signs" }
})));
assert!(!is_flat_map(&json!({
"vital_signs/x|magnitude": 1,
"nested": { "a": 1 }
})));
assert!(!is_flat_map(&json!({})), "an empty object is not a body");
assert!(!is_flat_map(&json!([])), "an array is not a FLAT map");
}
#[test]
fn a_structured_body_flattens_onto_its_flat_key_form() {
let structured = json!({
"vital_signs": [ {
"body_temperature": [ {
"any_event": [ {
"temperature": [ { "|magnitude": 37.5, "|unit": "°C" } ],
"time": [ { "": "2026-07-21T10:00:00Z" } ]
} ]
} ]
} ]
});
let mut flat = BTreeMap::new();
flatten_structured(&structured, "", &mut flat);
let keys: Vec<&str> = flat.keys().map(String::as_str).collect();
assert_eq!(
keys,
vec![
"vital_signs:0/body_temperature:0/any_event:0/temperature:0|magnitude",
"vital_signs:0/body_temperature:0/any_event:0/temperature:0|unit",
"vital_signs:0/body_temperature:0/any_event:0/time:0",
]
);
assert_eq!(
flat.get("vital_signs:0/body_temperature:0/any_event:0/temperature:0|magnitude"),
Some(&json!(37.5))
);
}
#[test]
fn a_canonical_body_is_never_read_as_a_simplified_one() {
let canonical = json!({
"_type": "COMPOSITION",
"name": { "_type": "DV_TEXT", "value": "Vital signs" }
});
assert!(!has_simplified_leaf_keys(&canonical));
assert!(
simplified_as_flat(&canonical).is_none(),
"a canonical body has no FLAT reading"
);
let structured = json!({ "vital_signs": [ { "temperature": [ { "|magnitude": 1 } ] } ] });
assert!(has_simplified_leaf_keys(&structured));
let flat = simplified_as_flat(&structured).expect("a STRUCTURED body reads as FLAT");
assert_eq!(
flat.keys().map(String::as_str).collect::<Vec<_>>(),
vec!["vital_signs:0/temperature:0|magnitude"]
);
let already_flat = json!({ "ctx/language": "en", "vitals/temp|magnitude": 37.5 });
assert_eq!(
simplified_as_flat(&already_flat).map(|m| m.len()),
Some(2),
"a FLAT body is its own key map"
);
assert!(simplified_as_flat(&json!("text")).is_none());
}
#[test]
fn the_ctx_input_keys_fold_onto_the_paths_a_read_back_uses() {
let committed: BTreeMap<String, Value> = [
("ctx/participation_name:1".to_owned(), json!("Lara Markham")),
("ctx/participation_id:1".to_owned(), json!("198")),
("ctx/participation_function".to_owned(), json!("performer")),
("ctx/health_care_facility|id".to_owned(), json!("9091")),
("ctx/id_namespace".to_owned(), json!("HOSPITAL-NS")),
("ctx/id_scheme".to_owned(), json!("HOSPITAL-NS")),
("ctx/language".to_owned(), json!("en")),
("vitals/temperature|magnitude".to_owned(), json!(37.5)),
]
.into_iter()
.collect();
let folded = fold_flat_ctx(&committed, "vitals");
assert_eq!(
folded.get("vitals/context/_participation:1|name"),
Some(&json!("Lara Markham"))
);
assert_eq!(
folded.get("vitals/context/_participation:0|function"),
Some(&json!("performer")),
"an index-free participation key is the first participation"
);
assert_eq!(
folded.get("vitals/context/_health_care_facility|id"),
Some(&json!("9091"))
);
assert_eq!(
folded.get("vitals/context/_participation:1|id_namespace"),
Some(&json!("HOSPITAL-NS"))
);
assert_eq!(
folded.get("vitals/context/_health_care_facility|id_scheme"),
Some(&json!("HOSPITAL-NS"))
);
assert!(!folded.contains_key("ctx/id_namespace"));
assert!(!folded.contains_key("ctx/id_scheme"));
assert_eq!(folded.get("ctx/language"), Some(&json!("en")));
assert_eq!(
folded.get("vitals/temperature|magnitude"),
Some(&json!(37.5))
);
}
#[test]
fn a_flat_key_is_ignored_at_the_rm_path_it_names() {
let ignored = [
"context/start_time".to_owned(),
"context/setting".to_owned(),
"composer".to_owned(),
"uid".to_owned(),
];
assert!(flat_key_ignored("ctx/time", &ignored));
assert!(flat_key_ignored("ctx/setting", &ignored));
assert!(flat_key_ignored("ctx/composer_name", &ignored));
assert!(flat_key_ignored("ctx/composer_id", &ignored));
assert!(flat_key_ignored("vitals/context/start_time", &ignored));
assert!(flat_key_ignored("vitals/uid|value", &ignored));
assert!(flat_key_ignored("vitals/_uid", &ignored));
assert!(!flat_key_ignored("vitals/temperature|magnitude", &ignored));
assert!(!flat_key_ignored("ctx/end_time", &ignored));
assert!(flat_key_ignored(
"ctx/end_time",
&["context/end_time".to_owned()]
));
}
#[test]
fn the_first_element_index_is_elided_before_keys_are_compared() {
assert_eq!(
dezero("vitals:0/temperature:0|magnitude"),
"vitals/temperature|magnitude"
);
assert_eq!(dezero("vitals/events:0"), "vitals/events");
assert_eq!(
dezero("vitals:1/temperature:2|magnitude"),
"vitals:1/temperature:2|magnitude",
"only the FIRST element's index is elidable"
);
assert_eq!(
dezero("vitals/temperature|magnitude"),
"vitals/temperature|magnitude"
);
}
#[test]
fn a_flat_round_trip_loses_no_committed_datum_and_tolerates_surplus() {
let committed = json!({
"ctx/language": "en",
"ctx/time": "2026-07-21T10:00:00Z",
"vitals/temperature|magnitude": 37.5,
"vitals/temperature|unit": "°C"
});
let read_back = json!({
"vitals/temperature:0|magnitude": 37.5,
"vitals/temperature:0|unit": "°C",
"vitals/context/start_time": "2026-07-21T10:00:04Z",
"vitals/category|code_string": "433",
"vitals/_uid": "8849182c-82ad-4088-a07f-48ead4180515::sut::1",
"ctx/language": "en"
});
let ignored = ["context/start_time".to_owned(), "uid".to_owned()];
assert!(
equivalent(&read_back, &committed, &ignored),
"the read-back carries every committed datum"
);
let altered = json!({
"vitals/temperature:0|magnitude": 38.5,
"vitals/temperature:0|unit": "°C",
"ctx/language": "en"
});
assert!(!equivalent(&altered, &committed, &ignored));
let lossy = json!({
"vitals/temperature:0|magnitude": 37.5,
"ctx/language": "en"
});
assert!(!equivalent(&lossy, &committed, &ignored));
assert!(!equivalent(&read_back, &committed, &[]));
}
#[test]
fn a_type_self_tag_present_on_one_side_only_is_not_a_content_difference() {
let served = json!({
"_type": "COMPOSITION",
"name": { "_type": "DV_TEXT", "value": "Vital signs" }
});
let committed = json!({ "name": { "value": "Vital signs" } });
assert!(equivalent(&served, &committed, &[]));
let substituted = json!({
"_type": "COMPOSITION",
"name": { "_type": "DV_CODED_TEXT", "value": "Vital signs" }
});
let tagged_committed = json!({
"name": { "_type": "DV_TEXT", "value": "Vital signs" }
});
assert!(
!equivalent(&substituted, &tagged_committed, &[]),
"two different concrete types are not the same content"
);
assert!(!equivalent(
&json!({ "content": [1, 2] }),
&json!({ "content": [1] }),
&[]
));
}
#[test]
fn named_ignore_sets_expand_from_their_own_artifacts() {
use crate::model::assertion::IgnoreSpec;
let specs = vec![
IgnoreSpec::Named(IgnoreSetName::ServerAssigned),
IgnoreSpec::Named(IgnoreSetName::CtxDefaults),
IgnoreSpec::Path("content[0]/uid".to_owned()),
];
let resolved = resolve_ignore_sets(
&specs,
&["uid".to_owned(), "**/uid".to_owned()],
&["context/start_time".to_owned()],
);
assert_eq!(
resolved,
vec![
"uid".to_owned(),
"**/uid".to_owned(),
"context/start_time".to_owned(),
"content[0]/uid".to_owned(),
],
"the sets expand in the order the row declares them"
);
assert!(
resolve_ignore_sets(&[], &["uid".to_owned()], &[]).is_empty(),
"a row that ignores nothing strips nothing"
);
}
#[test]
fn returns_predicates_judge_the_whole_body() {
assert!(eval_returns(&json!(3), Some(&json!(3)), None, None).is_ok());
let failure = eval_returns(&json!(3), Some(&json!(4)), None, None).expect_err("3 is not 4");
assert!(failure.0.contains("!= expected"), "{failure:?}");
assert!(eval_returns(&json!("v1.2.3"), None, Some(r"^v\d+\.\d+"), None).is_ok());
assert!(eval_returns(&json!("draft"), None, Some(r"^v\d+"), None).is_err());
assert!(eval_returns(&json!("public data"), None, None, Some("secret")).is_ok());
let leaked = eval_returns(&json!("carries a secret"), None, None, Some("secret"))
.expect_err("a body that must omit the pattern carries it");
assert!(leaked.0.contains("must omit"), "{leaked:?}");
let broken = eval_returns(&json!("x"), None, Some("("), None)
.expect_err("an uncompilable pattern is a failure");
assert!(broken.0.contains("does not compile"), "{broken:?}");
let broken = eval_returns(&json!("x"), None, None, Some("("))
.expect_err("an uncompilable omits pattern is a failure");
assert!(broken.0.contains("does not compile"), "{broken:?}");
}
#[test]
fn field_predicates_report_the_predicate_they_violated() {
let body = json!({
"system_id": "sut.example.org",
"versions": [ { "uid": { "value": "a::b::1" } } ]
});
assert!(
eval_field(
&body,
"system_id",
None,
None,
None,
None,
Some(r"\.org$"),
None
)
.is_ok()
);
let failure = eval_field(
&body,
"system_id",
None,
None,
None,
None,
Some(r"^\d+$"),
None,
)
.expect_err("an identifier is not digits");
assert!(failure.0.contains("does not match"), "{failure:?}");
let failure = eval_field(
&body,
"missing/leaf",
Some(&json!(1)),
None,
None,
None,
None,
None,
)
.expect_err("an unresolvable path cannot be compared");
assert!(failure.0.contains("resolves to nothing"), "{failure:?}");
let failure = eval_field(&body, "system_id", None, None, None, Some(true), None, None)
.expect_err("a present attribute is not absent");
assert!(failure.0.contains("expected absent"), "{failure:?}");
let failure = eval_field(&body, "audit", None, None, Some(true), None, None, None)
.expect_err("an absent attribute is not present");
assert!(failure.0.contains("expected present"), "{failure:?}");
assert!(eval_field(&body, "system_id", None, None, None, None, Some("("), None).is_err());
assert!(
eval_field(
&body,
"versions[0]/uid/value",
None,
None,
None,
None,
None,
None
)
.is_ok()
);
}
#[test]
fn absent_or_matches_passes_on_absence_and_judges_on_presence() {
let body = json!({ "meta": { "_created": "2026-07-21T10:00:00Z" } });
assert!(
eval_field(
&body,
"meta/_generator",
None,
None,
None,
None,
None,
Some("^x")
)
.is_ok()
);
assert!(
eval_field(
&body,
"meta/_created",
None,
None,
None,
None,
None,
Some(r"^\d{4}-\d{2}-\d{2}T")
)
.is_ok()
);
let failure = eval_field(
&body,
"meta/_created",
None,
None,
None,
None,
None,
Some(r"^\d+$"),
)
.expect_err("an extended ISO 8601 date-time is not a run of digits");
assert!(failure.0.contains("does not match"), "{failure:?}");
}
#[test]
fn a_malformed_index_step_resolves_to_nothing() {
let body = json!({ "versions": [{ "uid": "a" }, { "uid": "b" }] });
assert_eq!(
resolve_path(&body, "versions[1]/uid"),
Some(&json!("b")),
"the well-formed index addresses its element"
);
for path in ["versions[x]/uid", "versions[0/uid", "versions[9]/uid"] {
assert_eq!(resolve_path(&body, path), None, "{path}");
}
assert_eq!(resolve_path(&body, "versions/[0]/uid"), Some(&json!("a")));
assert_eq!(resolve_path(&body, "//versions[0]//uid"), Some(&json!("a")));
}
#[test]
fn only_scalar_captures_render_and_everything_else_is_refused() {
use crate::refgrammar::Template;
let mut vars = VarStore::default();
vars.set(
crate::ids::CaptureName::parse("ehr_id").unwrap(),
Captured::Scalar("e-1".to_owned()),
);
vars.set(
crate::ids::CaptureName::parse("uids").unwrap(),
Captured::List(vec!["a".to_owned()]),
);
let bound = Template::parse("/ehr/${ehr_id}").unwrap();
assert_eq!(render_template(&bound, &vars).unwrap(), "/ehr/e-1");
let non_scalar = Template::parse("${uids}").unwrap();
assert_eq!(
render_template(&non_scalar, &vars),
Err("capture uids is not scalar".to_owned())
);
let unbound = Template::parse("${ghost}").unwrap();
assert_eq!(
render_template(&unbound, &vars),
Err("capture ghost is not bound".to_owned())
);
let driver_side = Template::parse("${row.ehr_id}").unwrap();
let failure = render_template(&driver_side, &vars)
.expect_err("a row reference is the driver's to resolve");
assert!(
failure.contains("must be resolved by the driver"),
"{failure}"
);
}
#[test]
fn an_ignore_path_descends_and_an_empty_one_strips_nothing() {
let body = json!({
"context": { "start_time": "t", "setting": "s" },
"versions": [
{ "uid": "a", "commit_audit": { "time_committed": "t1" } },
{ "uid": "b", "commit_audit": { "time_committed": "t2" } }
]
});
let nested = strip_ignored(&body, &["context/start_time".to_owned()]);
assert_eq!(nested["context"], json!({ "setting": "s" }));
assert_eq!(nested["versions"], body["versions"], "untouched elsewhere");
let through_list =
strip_ignored(&body, &["versions/commit_audit/time_committed".to_owned()]);
for version in through_list["versions"].as_array().unwrap() {
assert_eq!(version["commit_audit"], json!({}));
assert!(version["uid"].is_string(), "siblings survive");
}
assert_eq!(strip_ignored(&body, &[String::new()]), body);
assert_eq!(strip_ignored(&body, &["/".to_owned()]), body);
assert_eq!(
strip_ignored(&body, &["context/setting/deeper".to_owned()]),
body
);
}
#[test]
fn a_non_string_leaf_is_matched_as_its_json_text() {
let body = json!({ "magnitude": 140, "uid": { "value": "a::b::1" } });
assert!(
eval_field(
&body,
"magnitude",
None,
None,
None,
None,
Some(r"^\d+$"),
None
)
.is_ok()
);
let failure = eval_field(&body, "magnitude", None, None, None, None, Some("^x"), None)
.expect_err("140 does not start with x");
assert!(failure.0.contains("\"140\""), "{failure:?}");
assert!(eval_field(&body, "uid", None, None, None, None, Some("a::b::1"), None).is_ok());
assert!(
eval_field(
&body,
"magnitude",
None,
Some(&json!(1)),
None,
None,
None,
None
)
.is_ok()
);
let failure = eval_field(
&body,
"magnitude",
None,
Some(&json!(140)),
None,
None,
None,
None,
)
.expect_err("the value is the client-supplied one");
assert!(failure.0.contains("must be server-set"), "{failure:?}");
}
#[test]
fn unique_ignores_rows_that_bound_nothing() {
let name = crate::ids::CaptureName::parse("ehr_id").unwrap();
let mut bound = VarStore::default();
bound.set(name.clone(), Captured::Scalar("e-1".to_owned()));
let mut same = VarStore::default();
same.set(name.clone(), Captured::Scalar("e-1".to_owned()));
assert!(
eval_unique(&name, &[VarStore::default(), VarStore::default()]).is_ok(),
"two rows that bound nothing are not two duplicates"
);
assert!(eval_unique(&name, &[bound.clone(), VarStore::default()]).is_ok());
let failure = eval_unique(&name, &[bound, VarStore::default(), same])
.expect_err("the same id at two rows is a duplicate");
assert!(failure.0.contains("repeats at row 2"), "{failure:?}");
}
#[test]
fn returns_predicates_read_a_non_string_body_as_its_json() {
let count = json!(7);
assert!(eval_returns(&count, None, Some(r"^\d$"), None).is_ok());
let failure =
eval_returns(&count, None, Some("^x"), None).expect_err("7 does not start with x");
assert!(failure.0.contains("does not match"), "{failure:?}");
assert!(eval_returns(&count, None, None, Some("nowhere")).is_ok());
assert!(eval_returns(&count, None, None, Some("7")).is_err());
assert!(eval_returns(&count, None, Some("("), None).is_err());
}
#[test]
fn an_xsi_type_in_the_wrong_namespace_is_refused() {
use crate::vocab::XmlNamespace;
let unqualified_type = Value::String(
r#"<oe:version xmlns:oe="http://schemas.openehr.org/v1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:type="ORIGINAL_VERSION"/>"#
.to_owned(),
);
let failure = eval_xml_root(
&unqualified_type,
"version",
Some(XmlNamespace::Published),
Some("ORIGINAL_VERSION"),
)
.expect_err("the type QName resolves to no namespace");
assert!(
failure.0.contains("resolves to NO namespace"),
"{failure:?}"
);
assert!(
eval_xml_root(
&unqualified_type,
"version",
Some(XmlNamespace::Published),
None
)
.is_ok()
);
let foreign_type = Value::String(
r#"<version xmlns="http://schemas.openehr.org/v1"
xmlns:other="http://example.invalid/other"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:type="other:ORIGINAL_VERSION"/>"#
.to_owned(),
);
let failure = eval_xml_root(
&foreign_type,
"version",
Some(XmlNamespace::Published),
Some("ORIGINAL_VERSION"),
)
.expect_err("the type is another schema's");
assert!(failure.0.contains("is in namespace"), "{failure:?}");
let unbound_type_prefix = Value::String(
r#"<version xmlns="http://schemas.openehr.org/v1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:type="zz:ORIGINAL_VERSION"/>"#
.to_owned(),
);
let failure = eval_xml_root(&unbound_type_prefix, "version", None, Some("x"))
.expect_err("the xsi:type prefix is unbound");
assert!(
failure.0.contains("is not bound to any namespace"),
"{failure:?}"
);
let unbound_root_prefix = Value::String("<zz:version/>".to_owned());
let failure = eval_xml_root(&unbound_root_prefix, "version", None, None)
.expect_err("the root's prefix is unbound");
assert!(
failure.0.contains("is not bound to any namespace"),
"{failure:?}"
);
}
}