#![expect(
clippy::disallowed_types,
reason = "dev/verification tooling over JSON artifacts (the catalogue, results, wire \
exchanges) — not the application (#1694)"
)]
use serde::de::Error as DeError;
use serde::{Deserialize, Deserializer};
use crate::ids::CaseId;
use crate::model::value::TemplatedValue;
use crate::refgrammar::{RefError, Template, ValueRef};
use crate::vocab::{ChangeType, FormatName, IgnoreSetName, ResultSetMatch, XmlNamespace};
#[derive(Debug, Clone, PartialEq)]
pub enum EquivalentTarget {
Committed,
Ref(ValueRef),
}
impl<'de> Deserialize<'de> for EquivalentTarget {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
if s == "committed" {
return Ok(Self::Committed);
}
let template = Template::parse(&s).map_err(D::Error::custom)?;
let reference = template.as_single_ref().ok_or_else(|| {
D::Error::custom("equivalent target must be `committed` or a single ${…} reference")
})?;
match reference {
ValueRef::DataSet { .. }
| ValueRef::Capture {
optional: false, ..
} => Ok(Self::Ref(reference.clone())),
_ => Err(D::Error::custom(
"equivalent target reference must be ${ds:…} or ${<capture>}",
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IgnoreSpec {
Named(IgnoreSetName),
Path(String),
}
impl<'de> Deserialize<'de> for IgnoreSpec {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
match s.as_str() {
"server_assigned" => Ok(Self::Named(IgnoreSetName::ServerAssigned)),
"ctx_defaults" => Ok(Self::Named(IgnoreSetName::CtxDefaults)),
_ if s.contains('/') => Ok(Self::Path(s)),
_ => Err(D::Error::custom(format!(
"ignoring entry {s:?} is neither a named ignore-set (server_assigned | ctx_defaults) nor an explicit path"
))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct IgnoreList(pub Vec<IgnoreSpec>);
impl<'de> Deserialize<'de> for IgnoreList {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany {
One(IgnoreSpec),
Many(Vec<IgnoreSpec>),
}
Ok(match OneOrMany::deserialize(deserializer)? {
OneOrMany::One(one) => Self(vec![one]),
OneOrMany::Many(many) => Self(many),
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SingleRef(pub ValueRef);
impl<'de> Deserialize<'de> for SingleRef {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
let template = Template::parse(&s).map_err(D::Error::custom)?;
let reference = template
.as_single_ref()
.ok_or_else(|| D::Error::custom(format!("{s:?} must be a single ${{…}} reference")))?;
Ok(Self(reference.clone()))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum RowsSpec {
From(ValueRef),
Inline(Vec<Vec<serde_json::Value>>),
}
impl<'de> Deserialize<'de> for RowsSpec {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct FromSpec {
from: SingleRef,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
From(FromSpec),
Inline(Vec<Vec<serde_json::Value>>),
}
match Raw::deserialize(deserializer)? {
Raw::From(FromSpec { from }) => match from.0 {
reference @ ValueRef::DataSet { .. } => Ok(Self::From(reference)),
other => Err(D::Error::custom(format!(
"result_set rows.from must be a ${{ds:…}} reference, got {other}"
))),
},
Raw::Inline(rows) => Ok(Self::Inline(rows)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ColumnSpec {
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(tag = "assert", rename_all = "snake_case", deny_unknown_fields)]
pub enum Assertion {
InstanceOf {
rm_type: String,
#[serde(default)]
format: Option<FormatName>,
},
Field {
path: String,
#[serde(default)]
equals: Option<TemplatedValue>,
#[serde(default)]
not_equals: Option<TemplatedValue>,
#[serde(default)]
exists: Option<bool>,
#[serde(default)]
absent: Option<bool>,
#[serde(default)]
matches: Option<String>,
},
Equivalent {
to: EquivalentTarget,
#[serde(default)]
ignoring: IgnoreList,
},
Signature {
#[serde(default)]
of: Option<SingleRef>,
#[serde(default)]
for_each: Option<SingleRef>,
#[serde(default)]
present: Option<bool>,
#[serde(default)]
verifiable: Option<bool>,
#[serde(default)]
equals: Option<TemplatedValue>,
#[serde(default)]
distinct_from: Option<TemplatedValue>,
},
Version {
#[serde(default)]
of: Option<SingleRef>,
#[serde(default)]
for_each: Option<SingleRef>,
#[serde(default)]
change_type: Option<ChangeType>,
#[serde(default)]
lifecycle_state: Option<String>,
#[serde(default)]
count: Option<u64>,
#[serde(default)]
uid_pattern: Option<Template>,
},
ResultSet {
#[serde(rename = "match")]
match_mode: ResultSetMatch,
#[serde(default)]
rows: Option<RowsSpec>,
#[serde(default)]
count: Option<u64>,
#[serde(default)]
columns: Option<Vec<ColumnSpec>>,
},
Unique {
over: SingleRef,
aggregate: bool,
},
Returns {
#[serde(default)]
equals: Option<serde_json::Value>,
#[serde(default)]
matches: Option<String>,
#[serde(default)]
omits: Option<String>,
},
XmlRoot {
name: String,
#[serde(default)]
namespace: Option<XmlNamespace>,
#[serde(default)]
xsi_type: Option<String>,
},
MessageExemplar {
text: String,
},
State {
text: String,
#[serde(default)]
verified_by: Option<CaseId>,
},
}
impl Assertion {
pub fn check_invariants(&self) -> Result<(), String> {
match self {
Self::Field { .. } => self.check_field_invariants(),
Self::Version { .. } => self.check_version_invariants(),
other => other.check_other_invariants(),
}
}
fn check_field_invariants(&self) -> Result<(), String> {
match self {
Self::Field {
equals,
not_equals,
exists,
absent,
matches,
path,
} => {
let predicates = usize::from(equals.is_some())
+ usize::from(not_equals.is_some())
+ usize::from(exists.is_some())
+ usize::from(absent.is_some())
+ usize::from(matches.is_some());
if predicates != 1 {
return Err(format!(
"field assertion on {path:?} must carry exactly one of equals | exists | absent | matches"
));
}
if let Some(re) = matches {
regex::Regex::new(re).map_err(|e| format!("field matches regex: {e}"))?;
}
Ok(())
}
_ => Ok(()),
}
}
fn check_version_invariants(&self) -> Result<(), String> {
match self {
Self::Version {
of,
for_each,
change_type,
lifecycle_state,
count,
uid_pattern,
} => {
if of.is_some() && for_each.is_some() {
return Err(
"version assertion: `of` and `for_each` are mutually exclusive".to_owned(),
);
}
if change_type.is_none()
&& lifecycle_state.is_none()
&& count.is_none()
&& uid_pattern.is_none()
{
return Err("version assertion carries no fact (change_type | lifecycle_state | count | uid_pattern)".to_owned());
}
if count.is_none() && of.is_none() && for_each.is_none() {
return Err(
"version assertion needs `of`/`for_each` (only `count` may stand alone)"
.to_owned(),
);
}
Ok(())
}
_ => Ok(()),
}
}
fn check_other_invariants(&self) -> Result<(), String> {
match self {
Self::Signature {
of,
for_each,
present,
verifiable,
equals,
distinct_from,
} => {
if of.is_some() == for_each.is_some() {
return Err(
"signature assertion needs exactly one of `of` | `for_each`".to_owned()
);
}
if present.is_none()
&& verifiable.is_none()
&& equals.is_none()
&& distinct_from.is_none()
{
return Err(
"signature assertion carries no fact (present | verifiable | equals | distinct_from)"
.to_owned(),
);
}
}
Self::ResultSet {
match_mode,
rows,
count,
..
} => match match_mode {
ResultSetMatch::Count => {
if count.is_none() {
return Err("result_set match:count requires `count`".to_owned());
}
}
_ => {
if rows.is_none() {
return Err("result_set requires `rows` (except match:count)".to_owned());
}
}
},
Self::Unique { aggregate, over } => {
if !aggregate {
return Err(
"unique is defined only as an aggregate assertion (aggregate: true)"
.to_owned(),
);
}
if !matches!(
over.0,
ValueRef::Capture {
optional: false,
..
}
) {
return Err("unique `over` must be a ${<capture>} reference".to_owned());
}
}
Self::Returns {
equals,
matches,
omits,
} => {
match (equals.is_some(), matches.is_some(), omits.is_some()) {
(true, false, false) | (false, true, _) | (false, false, true) => {}
_ => {
return Err(
"returns must carry exactly one of equals | matches [+ omits] | omits"
.to_owned(),
);
}
}
if let Some(re) = matches {
regex::Regex::new(re).map_err(|e| format!("returns matches regex: {e}"))?;
}
if let Some(re) = omits {
regex::Regex::new(re).map_err(|e| format!("returns omits regex: {e}"))?;
}
}
Self::XmlRoot {
name,
xsi_type,
namespace: _,
} => check_xml_root_invariants(name, xsi_type.as_deref())?,
Self::InstanceOf { .. }
| Self::Equivalent { .. }
| Self::MessageExemplar { .. }
| Self::State { .. }
| Self::Field { .. }
| Self::Version { .. } => {}
}
Ok(())
}
#[must_use]
pub fn is_aggregate(&self) -> bool {
matches!(self, Self::Unique { .. })
}
}
fn check_xml_root_invariants(name: &str, xsi_type: Option<&str>) -> Result<(), String> {
if name.trim().is_empty() {
return Err("xml_root assertion needs the expected root element local name".to_owned());
}
if name.contains(':') {
return Err(format!(
"xml_root name {name:?} must be the LOCAL name — the prefix is a document's own choice and the namespace is asserted by `namespace:`"
));
}
let Some(rm_type) = xsi_type else {
return Ok(());
};
if rm_type.trim().is_empty() {
return Err("xml_root xsi_type must name the concrete type, or be omitted".to_owned());
}
if rm_type.contains(':') {
return Err(format!(
"xml_root xsi_type {rm_type:?} must be the LOCAL name — the QName's prefix is a document's own choice and its namespace rides on `namespace:`"
));
}
Ok(())
}
#[must_use]
pub fn assertion_refs(assertion: &Assertion) -> Vec<ValueRef> {
let mut out: Vec<ValueRef> = Vec::new();
match assertion {
Assertion::Field {
equals, not_equals, ..
} => {
for v in [equals, not_equals].into_iter().flatten() {
out.extend(v.refs().into_iter().cloned());
}
}
Assertion::Equivalent { to, .. } => {
if let EquivalentTarget::Ref(r) = to {
out.push(r.clone());
}
}
Assertion::Version {
of,
for_each,
uid_pattern,
..
} => {
if let Some(SingleRef(r)) = of {
out.push(r.clone());
}
if let Some(SingleRef(r)) = for_each {
out.push(r.clone());
}
if let Some(t) = uid_pattern {
out.extend(t.refs().cloned());
}
}
Assertion::ResultSet { rows, .. } => {
if let Some(RowsSpec::From(r)) = rows {
out.push(r.clone());
}
}
Assertion::Signature {
of,
for_each,
equals,
distinct_from,
..
} => {
if let Some(SingleRef(r)) = of {
out.push(r.clone());
}
if let Some(SingleRef(r)) = for_each {
out.push(r.clone());
}
if let Some(v) = equals {
out.extend(v.refs().into_iter().cloned());
}
if let Some(v) = distinct_from {
out.extend(v.refs().into_iter().cloned());
}
}
Assertion::Unique { over, .. } => out.push(over.0.clone()),
Assertion::InstanceOf { .. }
| Assertion::Returns { .. }
| Assertion::XmlRoot { .. }
| Assertion::MessageExemplar { .. }
| Assertion::State { .. } => {}
}
out
}
impl From<RefError> for String {
fn from(e: RefError) -> Self {
e.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(v: serde_json::Value) -> Assertion {
serde_json::from_value(v).unwrap()
}
#[test]
fn pilot_assertions_parse() {
let a = parse(serde_json::json!({
"assert": "unique", "over": "${new_ehr_id}", "aggregate": true
}));
assert!(a.check_invariants().is_ok());
assert!(a.is_aggregate());
let a = parse(serde_json::json!({
"assert": "version", "of": "${v2_uid}",
"uid_pattern": "${versioned_object_uid}::<system>::2"
}));
assert!(a.check_invariants().is_ok());
let a = parse(serde_json::json!({ "assert": "version", "count": 2 }));
assert!(a.check_invariants().is_ok());
let a = parse(serde_json::json!({
"assert": "equivalent", "to": "committed", "ignoring": "server_assigned"
}));
assert!(a.check_invariants().is_ok());
let a = parse(serde_json::json!({
"assert": "result_set", "match": "ordered",
"rows": { "from": "${ds:cnf.set.bp-10#magnitude_ge_140_by_uid}" },
"columns": [{ "name": "uid" }]
}));
assert!(a.check_invariants().is_ok());
}
#[test]
fn signature_distinct_from_is_a_fact() {
let a = parse(serde_json::json!({
"assert": "signature", "of": "${v2_uid}", "distinct_from": "${sig_first}"
}));
assert!(a.check_invariants().is_ok());
assert!(
assertion_refs(&a).iter().any(
|r| matches!(r, ValueRef::Capture { name, .. } if name.as_str() == "sig_first")
)
);
let a = parse(serde_json::json!({ "assert": "signature", "of": "${v}" }));
assert!(a.check_invariants().is_err());
}
#[test]
fn xml_root_takes_a_local_name_and_a_published_namespace() {
let a = parse(serde_json::json!({
"assert": "xml_root", "name": "composition", "namespace": "openehr-published"
}));
assert!(a.check_invariants().is_ok());
assert!(assertion_refs(&a).is_empty());
let a = parse(serde_json::json!({ "assert": "xml_root", "name": "composition" }));
assert!(a.check_invariants().is_ok());
let a = parse(serde_json::json!({ "assert": "xml_root", "name": "oe:composition" }));
assert!(a.check_invariants().is_err());
let a = parse(serde_json::json!({ "assert": "xml_root", "name": " " }));
assert!(a.check_invariants().is_err());
assert!(
serde_json::from_value::<Assertion>(serde_json::json!({
"assert": "xml_root", "name": "composition", "namespace": "http://example.org"
}))
.is_err()
);
}
#[test]
fn xml_root_takes_the_concrete_type_of_an_abstract_root() {
let a = parse(serde_json::json!({
"assert": "xml_root", "name": "version",
"namespace": "openehr-published", "xsi_type": "ORIGINAL_VERSION"
}));
assert!(a.check_invariants().is_ok());
assert!(assertion_refs(&a).is_empty());
let a = parse(serde_json::json!({
"assert": "xml_root", "name": "version", "xsi_type": "oe:ORIGINAL_VERSION"
}));
assert!(a.check_invariants().is_err());
let a = parse(serde_json::json!({
"assert": "xml_root", "name": "version", "xsi_type": " "
}));
assert!(a.check_invariants().is_err());
}
#[test]
fn invariants_bite() {
let a = parse(serde_json::json!({ "assert": "version", "of": "${v}" }));
assert!(a.check_invariants().is_err());
let a = parse(serde_json::json!({
"assert": "field", "path": "x", "exists": true, "absent": true
}));
assert!(a.check_invariants().is_err());
let a = parse(serde_json::json!({
"assert": "unique", "over": "${x}", "aggregate": false
}));
assert!(a.check_invariants().is_err());
assert!(
serde_json::from_value::<Assertion>(serde_json::json!({
"assert": "equivalent", "to": "${row.thing}"
}))
.is_err()
);
assert!(
serde_json::from_value::<Assertion>(serde_json::json!({
"assert": "totally_new", "x": 1
}))
.is_err()
); }
}