#![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, Serialize};
use crate::ids::{CaptureName, SmOperationRef};
use crate::model::case::Applies;
use crate::refgrammar::Template;
use crate::vocab::{FormatName, HttpMethod, ItsName, OutcomeKind};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WireFrom {
Header {
name: String,
last_segment: bool,
},
Body {
path: String,
},
Capture(CaptureName),
}
impl WireFrom {
pub fn parse(raw: &str) -> Result<Self, String> {
if let Some(rest) = raw.strip_prefix("header ") {
let (name, last_segment) = match rest.strip_suffix(" last-segment") {
Some(name) => (name, true),
None => (rest, false),
};
let name = name.trim();
if name.is_empty() || name.contains(char::is_whitespace) {
return Err(format!(
"capture source {raw:?}: header name must be one token"
));
}
return Ok(Self::Header {
name: name.to_owned(),
last_segment,
});
}
if let Some(rest) = raw.strip_prefix("body ") {
let path = rest
.trim()
.strip_prefix('"')
.and_then(|p| p.strip_suffix('"'))
.ok_or_else(|| format!("capture source {raw:?}: body path must be quoted"))?;
if path.is_empty() {
return Err(format!("capture source {raw:?}: empty body path"));
}
return Ok(Self::Body {
path: path.to_owned(),
});
}
if let Some(rest) = raw.strip_prefix("capture ") {
return CaptureName::parse(rest.trim())
.map(Self::Capture)
.map_err(|e| format!("capture source {raw:?}: {e}"));
}
Err(format!(
"capture source {raw:?} is outside the closed grammar (header … | body \"…\" | capture …)"
))
}
}
impl<'de> Deserialize<'de> for WireFrom {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::parse(&s).map_err(D::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum StripRule {
#[serde(rename = "weak-quotes")]
WeakQuotes,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum TransformRule {
#[serde(rename = "root-uid")]
RootUid,
#[serde(rename = "creating-system-id")]
CreatingSystemId,
#[serde(rename = "uppercase")]
Uppercase,
}
impl TransformRule {
#[must_use]
pub fn apply(self, value: &str) -> Option<String> {
let mut segments = value.split("::");
match self {
Self::RootUid => segments.next().map(ToOwned::to_owned),
Self::CreatingSystemId => {
let _object_id = segments.next()?;
segments
.next()
.filter(|s| !s.is_empty())
.map(ToOwned::to_owned)
}
Self::Uppercase => Some(value.to_ascii_uppercase()),
}
}
pub const ALL: &[TransformRule] = &[
TransformRule::RootUid,
TransformRule::CreatingSystemId,
TransformRule::Uppercase,
];
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WireCapture {
pub from: WireFrom,
#[serde(default)]
pub strip: Option<StripRule>,
#[serde(default)]
pub transform: Option<TransformRule>,
#[serde(default)]
pub fallback: Option<WireFrom>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum HeaderMatcher {
Present,
PresentWithBody,
Absent,
Negotiated,
LatestVersionUid,
Pattern(String),
Literal(Template),
}
impl HeaderMatcher {
pub fn parse(raw: &str) -> Result<Self, String> {
Ok(match raw {
"present" => Self::Present,
"present?" => {
return Err(
"`present?` is the shorthand for `{ match: present, optional: true }` — \
in the mapping form declare `optional: true` instead"
.to_owned(),
);
}
"present-with-body" => Self::PresentWithBody,
"absent" => Self::Absent,
"negotiated" => Self::Negotiated,
"latest-version-uid" => Self::LatestVersionUid,
_ => {
if let Some(pattern) = raw.strip_prefix("pattern:") {
let probe = placeholder_wildcarded(pattern);
regex::Regex::new(&probe)
.map_err(|e| format!("header pattern {pattern:?} does not compile: {e}"))?;
Self::Pattern(pattern.to_owned())
} else {
Self::Literal(Template::parse(raw)?)
}
}
})
}
}
impl<'de> Deserialize<'de> for HeaderMatcher {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::parse(&s).map_err(D::Error::custom)
}
}
#[derive(Debug, Clone)]
pub struct HeaderExpectation {
pub matcher: HeaderMatcher,
pub optional: bool,
pub applies: Option<Applies>,
}
impl HeaderExpectation {
#[must_use]
pub fn new(matcher: HeaderMatcher) -> Self {
Self {
matcher,
optional: false,
applies: None,
}
}
}
impl<'de> Deserialize<'de> for HeaderExpectation {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Mapping {
#[serde(rename = "match")]
matcher: HeaderMatcher,
#[serde(default)]
optional: bool,
#[serde(default)]
applies: Option<Applies>,
}
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::String(s) => {
if s == "present?" {
return Ok(Self {
matcher: HeaderMatcher::Present,
optional: true,
applies: None,
});
}
HeaderMatcher::parse(&s)
.map(Self::new)
.map_err(D::Error::custom)
}
value @ serde_json::Value::Object(_) => {
let mapping: Mapping = serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Self {
matcher: mapping.matcher,
optional: mapping.optional,
applies: mapping.applies,
})
}
other => Err(D::Error::custom(format!(
"a header expectation is a matcher string or a \
{{ match, optional?, applies? }} mapping, got {other}"
))),
}
}
}
#[derive(Debug, Clone)]
pub struct HeaderExpectations(Vec<HeaderExpectation>);
impl HeaderExpectations {
#[must_use]
pub fn all(&self) -> &[HeaderExpectation] {
&self.0
}
}
impl From<Vec<HeaderExpectation>> for HeaderExpectations {
fn from(expectations: Vec<HeaderExpectation>) -> Self {
Self(expectations)
}
}
impl<'de> Deserialize<'de> for HeaderExpectations {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(deserializer)?;
let declared = match value {
serde_json::Value::Array(items) => {
if items.is_empty() {
return Err(D::Error::custom(
"a header's expectation sequence declares at least one matcher — \
an empty sequence asserts nothing",
));
}
items
.into_iter()
.map(|item| serde_json::from_value(item).map_err(D::Error::custom))
.collect::<Result<Vec<HeaderExpectation>, D::Error>>()?
}
single => vec![serde_json::from_value(single).map_err(D::Error::custom)?],
};
Ok(Self(declared))
}
}
#[must_use]
pub fn placeholder_names(pattern: &str) -> Vec<&str> {
let mut names = Vec::new();
let mut rest = pattern;
while let Some(start) = rest.find('<') {
let tail = rest.split_at(start).1;
let Some(end) = tail.find('>') else { break };
if let Some(name) = tail.get(1..end) {
names.push(name);
}
rest = tail.get(end + 1..).unwrap_or_default();
}
names
}
fn placeholder_wildcarded(pattern: &str) -> 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('>') {
out.push_str(".*");
rest = tail.get(end + 1..).unwrap_or_default();
} else {
out.push_str(tail);
rest = "";
}
}
out.push_str(rest);
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum BodySelector {
#[serde(rename = "prefer_conditional")]
PreferConditional,
#[serde(rename = "error_loose")]
ErrorLoose,
#[serde(rename = "result_set_body")]
ResultSetBody,
#[serde(rename = "negotiated")]
Negotiated,
#[serde(rename = "present")]
Present,
#[serde(rename = "absent")]
Absent,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WireExpectation {
pub status: StatusCode,
#[serde(default)]
pub alt_status: Option<Vec<StatusCode>>,
#[serde(default, deserialize_with = "crate::model::de::optional_ordered_map")]
pub headers: Option<Vec<(String, HeaderExpectations)>>,
#[serde(default)]
pub body: Option<BodySelector>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StatusCode(u16);
impl StatusCode {
#[must_use]
pub fn value(self) -> u16 {
self.0
}
}
impl<'de> Deserialize<'de> for StatusCode {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = u16::deserialize(deserializer)?;
if (100..=599).contains(&raw) {
Ok(Self(raw))
} else {
Err(D::Error::custom(format!("status {raw} outside 100..=599")))
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum RequestBody {
Named {
name: String,
optional: bool,
},
Structured(crate::model::value::TemplatedValue),
Patched {
from_capture: CaptureName,
set: Vec<(String, serde_json::Value)>,
},
}
impl<'de> Deserialize<'de> for RequestBody {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(deserializer)?;
if let serde_json::Value::Object(map) = &value
&& map.contains_key("from_capture")
{
let from = map
.get("from_capture")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| D::Error::custom("from_capture must be a capture name"))?;
let from_capture = CaptureName::parse(from).map_err(D::Error::custom)?;
let set = map
.get("set")
.and_then(serde_json::Value::as_object)
.ok_or_else(|| D::Error::custom("patched body requires a set mapping"))?
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
if map.len() != 2 {
return Err(D::Error::custom(
"patched body carries exactly from_capture + set",
));
}
return Ok(Self::Patched { from_capture, set });
}
match &value {
serde_json::Value::String(s) => {
let (name, optional) = match s.strip_suffix('?') {
Some(name) => (name, true),
None => (s.as_str(), false),
};
if name.is_empty() || name.contains(char::is_whitespace) {
return Err(D::Error::custom(format!(
"request body role {s:?} must be one token"
)));
}
Ok(Self::Named {
name: name.to_owned(),
optional,
})
}
serde_json::Value::Object(_) => crate::model::value::TemplatedValue::from_value(&value)
.map(Self::Structured)
.map_err(D::Error::custom),
_ => Err(D::Error::custom(
"request body must be a role name or a structured template",
)),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum QueryValue {
Single(Template),
Repeated(Vec<Template>),
}
impl QueryValue {
#[must_use]
pub fn templates(&self) -> &[Template] {
match self {
Self::Single(template) => std::slice::from_ref(template),
Self::Repeated(templates) => templates,
}
}
#[must_use]
pub fn is_repeated(&self) -> bool {
matches!(self, Self::Repeated(_))
}
}
impl<'de> Deserialize<'de> for QueryValue {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::String(s) => Template::parse(&s)
.map(Self::Single)
.map_err(D::Error::custom),
serde_json::Value::Array(items) => {
if items.is_empty() {
return Err(D::Error::custom(
"a repeated query parameter declares at least one member",
));
}
items
.iter()
.map(|item| match item {
serde_json::Value::String(s) => {
Template::parse(s).map_err(D::Error::custom)
}
_ => Err(D::Error::custom(
"a repeated query parameter's members are value templates",
)),
})
.collect::<Result<Vec<_>, _>>()
.map(Self::Repeated)
}
_ => Err(D::Error::custom(
"a query parameter is a value template or a sequence of them",
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathTemplate {
raw: String,
params: Vec<CaptureName>,
}
impl PathTemplate {
#[must_use]
pub fn raw(&self) -> &str {
&self.raw
}
#[must_use]
pub fn params(&self) -> &[CaptureName] {
&self.params
}
}
impl<'de> Deserialize<'de> for PathTemplate {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = String::deserialize(deserializer)?;
if !raw.starts_with('/') {
return Err(D::Error::custom(format!("path {raw:?} must start with /")));
}
let mut params = Vec::new();
let mut rest = raw.as_str();
while let Some(start) = rest.find('{') {
let tail = rest.get(start + 1..).unwrap_or_default();
let end = tail
.find('}')
.ok_or_else(|| D::Error::custom(format!("path {raw:?}: unterminated {{param}}")))?;
let name = tail.get(..end).unwrap_or_default();
params.push(CaptureName::parse(name).map_err(D::Error::custom)?);
rest = tail.get(end + 1..).unwrap_or_default();
}
Ok(Self { raw, params })
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RequestSpec {
pub method: HttpMethod,
pub path: PathTemplate,
#[serde(default, deserialize_with = "crate::model::de::optional_ordered_map")]
pub query: Option<Vec<(String, QueryValue)>>,
#[serde(default)]
pub body: Option<RequestBody>,
#[serde(default, deserialize_with = "crate::model::de::optional_ordered_map")]
pub headers: Option<Vec<(String, Template)>>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FormatHeaderReq {
Required,
Literal(Template),
}
impl<'de> Deserialize<'de> for FormatHeaderReq {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
if s == "required" {
Ok(Self::Required)
} else {
Template::parse(&s)
.map(Self::Literal)
.map_err(D::Error::custom)
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct UnrealizedDecl {
pub reason: String,
pub source: String,
pub ambiguity: crate::ids::AmbiguityId,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExtensionDecl {
pub family: String,
pub reason: String,
pub source: String,
pub ambiguity: crate::ids::AmbiguityId,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OperationBinding {
pub sm_operation: SmOperationRef,
pub its: ItsName,
#[serde(default)]
pub variant: Option<String>,
#[serde(default)]
pub applies: Option<Applies>,
#[serde(default)]
pub unrealized: Option<UnrealizedDecl>,
#[serde(default)]
pub extension: Option<ExtensionDecl>,
#[serde(default)]
pub request: Option<RequestSpec>,
#[serde(default)]
pub formats: Vec<FormatName>,
#[serde(default, deserialize_with = "crate::model::de::optional_ordered_map")]
pub format_headers: Option<Vec<(FormatKey, FormatHeaderMap)>>,
#[serde(default, deserialize_with = "crate::model::de::optional_ordered_map")]
pub outcomes: Option<Vec<(OutcomeKey, WireExpectation)>>,
#[serde(default, deserialize_with = "crate::model::de::optional_ordered_map")]
pub captures: Option<Vec<(CaptureName, WireCapture)>>,
#[serde(default)]
pub server_assigned: Vec<String>,
}
impl OperationBinding {
#[must_use]
pub fn is_unrealized(&self) -> bool {
self.unrealized.is_some()
}
#[must_use]
pub fn is_extension(&self) -> bool {
self.extension.is_some()
}
pub fn check_invariants(&self) -> Result<(), String> {
if self.unrealized.is_some() && self.extension.is_some() {
return Err(
"a binding is either unrealized or an extension realization, never both".to_owned(),
);
}
match (&self.unrealized, &self.request, &self.outcomes) {
(Some(_), None, None) | (None, Some(_), Some(_)) => Ok(()),
(Some(_), _, _) => Err("unrealized binding must carry no request/outcomes".to_owned()),
_ => Err("realized binding must carry request and outcomes".to_owned()),
}
}
#[must_use]
pub fn outcome(&self, kind: OutcomeKind) -> Option<&WireExpectation> {
self.outcomes
.as_deref()
.unwrap_or_default()
.iter()
.find(|(k, _)| k.0 == kind)
.map(|(_, e)| e)
}
#[must_use]
pub fn maps_capture(&self, name: &CaptureName) -> bool {
self.captures
.as_deref()
.unwrap_or_default()
.iter()
.any(|(n, _)| n == name)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OutcomeKey(pub OutcomeKind);
impl std::str::FromStr for OutcomeKey {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
OutcomeKind::from_token(s)
.map(Self)
.ok_or_else(|| format!("{s:?} is not an outcome kind"))
}
}
impl std::fmt::Display for OutcomeKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0.token())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FormatKey(pub FormatName);
impl std::str::FromStr for FormatKey {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_value(serde_json::Value::String(s.to_owned()))
.map(Self)
.map_err(|error| format!("{s:?} is not a format name: {error}"))
}
}
impl std::fmt::Display for FormatKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match serde_json::to_value(self.0) {
Ok(serde_json::Value::String(s)) => f.write_str(&s),
_ => f.write_str("?"),
}
}
}
#[derive(Debug, Clone)]
pub struct FormatHeaderMap(pub Vec<(String, FormatHeaderReq)>);
impl<'de> Deserialize<'de> for FormatHeaderMap {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
crate::model::de::ordered_map(deserializer).map(Self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::case::VersionRange;
#[test]
fn wire_from_grammar_is_closed() {
assert_eq!(
WireFrom::parse("header Location last-segment").unwrap(),
WireFrom::Header {
name: "Location".into(),
last_segment: true
}
);
assert_eq!(
WireFrom::parse("body \"ehr_id.value\"").unwrap(),
WireFrom::Body {
path: "ehr_id.value".into()
}
);
assert!(matches!(
WireFrom::parse("capture version_uid").unwrap(),
WireFrom::Capture(_)
));
assert!(WireFrom::parse("body ehr_id.value").is_err());
assert!(WireFrom::parse("body-or-location").is_err()); assert!(WireFrom::parse("jsonpath $.x").is_err());
}
#[test]
fn header_matchers_parse() {
let m: HeaderMatcher = serde_json::from_value(serde_json::json!(
"pattern:W/\"<versioned_object_uid>::<system_id>::1\""
))
.unwrap();
assert!(matches!(m, HeaderMatcher::Pattern(_)));
let m: HeaderMatcher =
serde_json::from_value(serde_json::json!("\"${preceding_version_uid}\"")).unwrap();
assert!(matches!(m, HeaderMatcher::Literal(_)));
assert!(
serde_json::from_value::<HeaderMatcher>(serde_json::json!("pattern:([unclosed"))
.is_err()
);
assert!(serde_json::from_value::<HeaderMatcher>(serde_json::json!("present?")).is_err());
}
#[test]
fn header_expectations_carry_the_two_modifiers() {
let short: HeaderExpectation =
serde_json::from_value(serde_json::json!("present")).unwrap();
assert_eq!(short.matcher, HeaderMatcher::Present);
assert!(!short.optional);
assert!(short.applies.is_none());
let sugar: HeaderExpectation =
serde_json::from_value(serde_json::json!("present?")).unwrap();
assert_eq!(sugar.matcher, HeaderMatcher::Present);
assert!(sugar.optional);
let long: HeaderExpectation = serde_json::from_value(serde_json::json!({
"match": "pattern:W/\"[^\"]+\"",
"optional": true,
"applies": { "its_rest": ">=1.1.0" }
}))
.unwrap();
assert!(matches!(long.matcher, HeaderMatcher::Pattern(_)));
assert!(long.optional);
let applies = long.applies.unwrap();
assert_eq!(applies.entries().len(), 1);
assert_eq!(
applies.its_rest.as_ref().map(VersionRange::raw),
Some(">=1.1.0")
);
assert!(
serde_json::from_value::<HeaderExpectation>(serde_json::json!({ "optional": true }))
.is_err()
);
assert!(
serde_json::from_value::<HeaderExpectation>(
serde_json::json!({ "match": "present", "since": "1.1.0" })
)
.is_err()
);
assert!(serde_json::from_value::<HeaderExpectation>(serde_json::json!(7)).is_err());
}
#[test]
fn present_with_body_is_a_vocabulary_member() {
let m: HeaderMatcher =
serde_json::from_value(serde_json::json!("present-with-body")).unwrap();
assert_eq!(m, HeaderMatcher::PresentWithBody);
let typo: HeaderMatcher =
serde_json::from_value(serde_json::json!("present-with-bodies")).unwrap();
assert!(matches!(typo, HeaderMatcher::Literal(_)), "{typo:?}");
}
#[test]
fn a_header_declares_one_expectation_or_a_sequence() {
let single: HeaderExpectations =
serde_json::from_value(serde_json::json!("present")).unwrap();
assert_eq!(single.all().len(), 1);
let many: HeaderExpectations = serde_json::from_value(serde_json::json!([
"latest-version-uid",
{ "match": "pattern:W/\"[^\"]+\"", "applies": { "its_rest": ">=1.1.0" } }
]))
.unwrap();
assert_eq!(many.all().len(), 2);
assert_eq!(many.all()[0].matcher, HeaderMatcher::LatestVersionUid);
assert!(many.all()[1].applies.is_some());
assert!(
serde_json::from_value::<HeaderExpectations>(serde_json::json!([])).is_err(),
"an empty sequence asserts nothing"
);
assert!(
serde_json::from_value::<HeaderExpectations>(serde_json::json!(["present", 7]))
.is_err()
);
}
#[test]
fn binding_parses_the_create_ehr_shape() {
let b: OperationBinding = serde_json::from_value(serde_json::json!({
"sm_operation": "I_EHR_SERVICE.create_ehr",
"its": "its-rest",
"request": {
"method": "POST",
"path": "/ehr",
"body": "ehr_status?",
"headers": { "Prefer": "return=representation" }
},
"formats": ["canonical-json", "canonical-xml"],
"outcomes": {
"created": { "status": 201,
"headers": { "ETag": "present", "Location": "present" },
"body": "prefer_conditional" },
"already_exists": { "status": 409 },
"validation_failed": { "status": 400 }
},
"captures": {
"ehr_id": { "from": "body \"ehr_id.value\"",
"fallback": "header Location last-segment" },
"version_uid": { "from": "header ETag", "strip": "weak-quotes" }
},
"server_assigned": ["ehr_id", "system_id", "time_created"]
}))
.unwrap();
assert_eq!(b.sm_operation.to_string(), "I_EHR_SERVICE.create_ehr");
assert!(b.outcome(OutcomeKind::Created).is_some());
assert!(b.outcome(OutcomeKind::NotFound).is_none());
assert!(b.maps_capture(&CaptureName::parse("ehr_id").unwrap()));
assert!(b.check_invariants().is_ok());
let request = b.request.clone().unwrap();
assert!(matches!(
request.body,
Some(RequestBody::Named { optional: true, .. })
));
assert_eq!(request.path.params().len(), 0);
}
#[test]
fn extension_realization_is_a_realized_binding_and_never_unrealized() {
let shape = |extra: serde_json::Value| {
let mut v = serde_json::json!({
"sm_operation": "I_PARTY_RELATIONSHIP.get_party_relationship",
"its": "its-rest",
"extension": {
"family": "party-relationship",
"reason": "the release surfaces no PARTY_RELATIONSHIP resource",
"source": "SM i_party_relationship.adoc vs ITS-REST demographic.openapi.yaml",
"ambiguity": "AMB-32"
},
"request": { "method": "GET", "path": "/demographic/party_relationship/{versioned_object_uid}" },
"outcomes": { "ok": { "status": 200 }, "not_found": { "status": 404 } }
});
if let (Some(object), Some(extra)) = (v.as_object_mut(), extra.as_object()) {
for (k, value) in extra {
object.insert(k.clone(), value.clone());
}
}
v
};
let b: OperationBinding = serde_json::from_value(shape(serde_json::json!({}))).unwrap();
assert!(b.is_extension());
assert!(!b.is_unrealized());
assert!(b.check_invariants().is_ok());
let mut both: OperationBinding = serde_json::from_value(shape(serde_json::json!({
"unrealized": {
"reason": "r", "source": "s", "ambiguity": "AMB-32"
}
})))
.unwrap();
assert!(both.check_invariants().is_err());
both.request = None;
both.outcomes = None;
assert!(both.check_invariants().is_err());
}
#[test]
fn path_params_extract() {
let r: RequestSpec = serde_json::from_value(serde_json::json!({
"method": "PUT",
"path": "/ehr/{ehr_id}/composition/{versioned_object_uid}"
}))
.unwrap();
assert_eq!(r.path.params().len(), 2);
assert!(
serde_json::from_value::<RequestSpec>(serde_json::json!({
"method": "GET", "path": "/ehr/{unclosed"
}))
.is_err()
);
}
#[test]
fn transforms_address_object_version_id_components() {
let uid = "8849182c-82ad-4088-a07f-48ead4180515::openEHRSys.example.com::1";
assert_eq!(
TransformRule::RootUid.apply(uid).as_deref(),
Some("8849182c-82ad-4088-a07f-48ead4180515")
);
assert_eq!(
TransformRule::CreatingSystemId.apply(uid).as_deref(),
Some("openEHRSys.example.com")
);
assert_eq!(
TransformRule::Uppercase.apply("a::b::1").as_deref(),
Some("A::B::1")
);
assert_eq!(
TransformRule::CreatingSystemId.apply("8849182c-82ad-4088-a07f-48ead4180515"),
None
);
assert_eq!(TransformRule::CreatingSystemId.apply("uid::"), None);
assert_eq!(TransformRule::RootUid.apply("uid").as_deref(), Some("uid"));
let spec: WireCapture = serde_json::from_value(serde_json::json!({
"from": "header ETag", "strip": "weak-quotes",
"transform": "creating-system-id"
}))
.unwrap();
assert_eq!(spec.transform, Some(TransformRule::CreatingSystemId));
assert!(
serde_json::from_value::<WireCapture>(serde_json::json!({
"from": "header ETag", "transform": "middle-segment"
}))
.is_err()
);
}
#[test]
fn query_values_are_single_or_repeated() {
let r: RequestSpec = serde_json::from_value(serde_json::json!({
"method": "DELETE",
"path": "/admin/ehr/all",
"query": { "ehr_id": ["${ehr_id_subset?}", "${ehr_id_subset_2?}"],
"fetch": "${url_fetch?}" }
}))
.unwrap();
let query = r.query.unwrap();
let repeated = &query.iter().find(|(name, _)| name == "ehr_id").unwrap().1;
assert!(repeated.is_repeated());
assert_eq!(repeated.templates().len(), 2);
let single = &query.iter().find(|(name, _)| name == "fetch").unwrap().1;
assert!(!single.is_repeated());
assert_eq!(single.templates().len(), 1);
for bad in [
serde_json::json!({ "method": "GET", "path": "/x", "query": { "p": [] } }),
serde_json::json!({ "method": "GET", "path": "/x", "query": { "p": [1] } }),
serde_json::json!({ "method": "GET", "path": "/x", "query": { "p": 1 } }),
serde_json::json!({ "method": "GET", "path": "/x", "query": { "p": ["${step2.body}"] } }),
] {
assert!(
serde_json::from_value::<RequestSpec>(bad.clone()).is_err(),
"{bad} must be rejected"
);
}
}
#[test]
fn duplicate_outcome_keys_rejected() {
let yaml = "sm_operation: I_EHR_SERVICE.create_ehr\nits: its-rest\nrequest: { method: POST, path: /ehr }\noutcomes:\n created: { status: 201 }\n created: { status: 200 }\n";
let result: Result<OperationBinding, _> = serde_saphyr::from_str(yaml);
assert!(result.is_err());
}
}