use fhir_types::codec::Value;
use fhir_types::definition::ElementDefinitionRead;
use crate::issue::IssueType;
use crate::issue::MessageId;
use crate::issue::Severity;
use crate::release::Release;
use crate::validate::ValidateError;
use crate::validate::walk::Walk;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Stem {
Fixed,
Pattern,
}
pub(crate) fn value_of<E: ElementDefinitionRead>(
element: &E,
stem: Stem,
) -> Result<Option<Value>, fhir_types::codec::EncodeError> {
let choice = match stem {
Stem::Fixed => element.fixed()?,
Stem::Pattern => element.pattern()?,
};
Ok(choice.and_then(|choice| choice.value))
}
pub(crate) fn equals_fixed(fixed: &Value, instance: &Value) -> bool {
match (fixed, instance) {
(Value::Object(fixed), Value::Object(instance)) => {
let extra = instance
.keys()
.any(|key| !fixed.contains_key(key) && !ignorable(key));
!extra
&& fixed.iter().all(|(key, value)| {
instance
.get(key)
.is_some_and(|held| equals_fixed(value, held))
})
}
(Value::Array(fixed), Value::Array(instance)) => {
fixed.len() == instance.len()
&& fixed
.iter()
.zip(instance)
.all(|(fixed, held)| equals_fixed(fixed, held))
}
_ => fixed == instance,
}
}
fn ignorable(key: &str) -> bool {
key == "id" || key == "extension" || key.starts_with('_')
}
pub(crate) fn contains_pattern(pattern: &Value, instance: &Value) -> bool {
match (pattern, instance) {
(Value::Object(pattern), Value::Object(instance)) => pattern.iter().all(|(key, value)| {
instance
.get(key)
.is_some_and(|held| contains_pattern(value, held))
}),
(Value::Array(pattern), Value::Array(instance)) => pattern
.iter()
.all(|wanted| instance.iter().any(|held| contains_pattern(wanted, held))),
_ => pattern == instance,
}
}
impl<R: Release> Walk<'_, '_, '_, R> {
pub(crate) fn fixed_and_pattern(
&mut self,
element: &R::ElementDefinition,
structure: &str,
id: &str,
value: Option<&Value>,
location: &str,
) -> Result<(), ValidateError> {
let encode = |source| ValidateError::Encode {
structure: structure.to_owned(),
element: id.to_owned(),
source,
};
if let Some(fixed) = value_of(element, Stem::Fixed).map_err(encode)?
&& !value.is_some_and(|held| equals_fixed(&fixed, held))
{
let shown = serde_json::to_string(&fixed).unwrap_or_default();
self.out().issue(
Severity::Error,
IssueType::Value,
MessageId::FixedMismatch,
location,
format!("the value is not the fixed value {shown} that {id} sets"),
);
}
if let Some(pattern) = value_of(element, Stem::Pattern).map_err(encode)?
&& !value.is_some_and(|held| contains_pattern(&pattern, held))
{
let shown = serde_json::to_string(&pattern).unwrap_or_default();
self.out().issue(
Severity::Error,
IssueType::Value,
MessageId::PatternMismatch,
location,
format!("the value does not contain the pattern {shown} that {id} sets"),
);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use fhir_types::codec::Value;
use super::contains_pattern;
use super::equals_fixed;
fn json(text: &str) -> Value {
serde_json::from_str(text).expect("test JSON")
}
#[test]
fn a_pattern_array_item_matches_any_instance_item() {
let pattern = json(r#"{"coding":[{"system":"http://loinc.org","code":"1-8"}]}"#);
let held = json(
r#"{"coding":[{"system":"urn:x","code":"a"},{"system":"http://loinc.org","code":"1-8","display":"d"}],"text":"t"}"#,
);
assert!(
contains_pattern(&pattern, &held),
"the second coding holds the pattern"
);
let missing = json(r#"{"coding":[{"system":"http://loinc.org","code":"2-6"}]}"#);
assert!(
!contains_pattern(&pattern, &missing),
"no coding holds the pattern"
);
}
#[test]
fn a_fixed_value_refuses_an_extra_property_and_ignores_an_id() {
let fixed = json(r#"{"system":"urn:x","code":"a"}"#);
assert!(
equals_fixed(&fixed, &json(r#"{"id":"c1","system":"urn:x","code":"a"}"#)),
"an id is no difference"
);
assert!(
!equals_fixed(
&fixed,
&json(r#"{"system":"urn:x","code":"a","display":"d"}"#)
),
"a display the fixed value lacks is a difference"
);
}
}