mod seams;
use std::error::Error;
use std::sync::Arc;
use std::sync::PoisonError;
use ferrobridge_fhirpath::eval::Environment;
use ferrobridge_fhirpath::eval::Focus;
use ferrobridge_fhirpath::eval::convert;
use ferrobridge_fhirpath::expression::Expression;
use ferrobridge_fhirpath::model::choice_element;
use ferrobridge_fhirpath::model::named_type;
use ferrobridge_fhirpath::model::path_types;
use fhir_types::codec::Value;
use fhir_types::definition::ConstraintRead;
use fhir_types::definition::ElementDefinitionRead;
use crate::issue::Issue;
use crate::issue::IssueType;
use crate::issue::Rule;
use crate::issue::Severity;
use crate::release::Release;
use crate::report::Constraint;
use crate::report::InvariantStage;
use crate::report::Reason;
use crate::validate::BestPractice;
use crate::validate::Element;
use crate::validate::Validator;
use crate::validate::resolver::container;
use crate::validate::structure::Structure;
use crate::validate::walk::Walk;
use seams::Conformance;
use seams::References;
use seams::Terminology;
pub(crate) type Compiled = Result<Arc<Expression>, String>;
const DEFECTS: [(&str, &str, &str, &str); 3] = [
(
"R4",
"http://hl7.org/fhir/StructureDefinition/ChargeItemDefinition",
"cid-0",
"https://github.com/FerroHEALTH/FerroBRIDGE/issues/460",
),
(
"R4",
"http://hl7.org/fhir/StructureDefinition/allergyintolerance-substanceExposureRisk",
"inv-1",
"https://github.com/FerroHEALTH/FerroBRIDGE/issues/460",
),
(
"R4",
"http://hl7.org/fhir/StructureDefinition/DomainResource",
"dom-3",
"https://github.com/FerroHEALTH/FerroBRIDGE/issues/444",
),
];
pub(crate) struct Subject<'s, 'i> {
pub(crate) value: Option<&'i Value>,
pub(crate) element: Option<&'i Value>,
pub(crate) type_name: &'s str,
pub(crate) location: &'s str,
pub(crate) resource: bool,
}
fn source<'a, R: Release>(constraint: &ConstraintRead<'a>, structure: &'a Structure<R>) -> &'a str {
constraint.source.unwrap_or_else(|| structure.url())
}
fn versionless(url: &str) -> &str {
url.split_once('|').map_or(url, |(url, _)| url)
}
fn listed(constraint: &ConstraintRead<'_>) -> Constraint {
Constraint::Invariant {
key: constraint.key.unwrap_or_default().to_owned(),
severity: constraint.severity.unwrap_or_default().to_owned(),
}
}
fn best_practice(constraint: &ConstraintRead<'_>) -> bool {
constraint.best_practice == Some(true)
}
fn upstream(release: &str, source: &str, key: &str) -> Option<&'static str> {
DEFECTS
.iter()
.find(|(named, declared, defect, _)| {
*named == release && *declared == source && *defect == key
})
.map(|(_, _, _, report)| *report)
}
pub(super) fn chain(error: &dyn Error) -> String {
let mut text = error.to_string();
let mut cause = error.source();
while let Some(inner) = cause {
text.push_str(": ");
text.push_str(&inner.to_string());
cause = inner.source();
}
text
}
impl<R: Release> Validator<'_, R> {
pub(super) fn expression(&self, text: &str) -> Compiled {
let mut cache = self
.expressions
.lock()
.unwrap_or_else(PoisonError::into_inner);
cache
.entry(text.to_owned())
.or_insert_with(|| {
Expression::parse(text)
.map(Arc::new)
.map_err(|error| chain(&error))
})
.clone()
}
fn check(&self, expression: &Expression, context: &str) -> Result<(), String> {
let model = R::model();
let Some(contexts) =
path_types(model, context).or_else(|| named_type(model, context).map(|ty| vec![ty]))
else {
return Ok(());
};
let key = (expression.source().to_owned(), context.to_owned());
let mut cache = self.checked.lock().unwrap_or_else(PoisonError::into_inner);
cache
.entry(key)
.or_insert_with(|| {
expression
.check_any(model, &contexts)
.map_err(|error| chain(&error))
})
.clone()
}
fn check_types(&self, expression: &Expression, codes: &[&str]) -> Result<(), String> {
let contexts: Vec<_> = codes
.iter()
.filter_map(|code| named_type(R::model(), code))
.collect();
if contexts.len() != codes.len() {
return Ok(());
}
let key = (expression.source().to_owned(), codes.join("|"));
let mut cache = self.checked.lock().unwrap_or_else(PoisonError::into_inner);
cache
.entry(key)
.or_insert_with(|| {
expression
.check_any(R::model(), &contexts)
.map_err(|error| chain(&error))
})
.clone()
}
}
impl<'i, R: Release> Walk<'_, '_, 'i, R> {
pub(crate) fn invariants(
&mut self,
structure: &Structure<R>,
element: &Element<R>,
subject: &Subject<'_, 'i>,
) {
for constraint in &element.constraints() {
self.invariant(structure, element, constraint, subject);
}
}
pub(crate) fn untyped_invariants(
&mut self,
structure: &Structure<R>,
id: &str,
element: &Element<R>,
location: &str,
) {
for constraint in &element.constraints() {
let reason = Reason::InvariantRefused {
stage: InvariantStage::Check,
error: "the type of the element is not known".to_owned(),
upstream: None,
};
self.out()
.unevaluated(structure.url(), id, listed(constraint), location, reason);
}
}
fn invariant(
&mut self,
structure: &Structure<R>,
element: &Element<R>,
constraint: &ConstraintRead<'_>,
subject: &Subject<'_, 'i>,
) {
let key = constraint.key.unwrap_or_default();
let source = source(constraint, structure);
if !self.first_invariant(subject.location, key, source) {
return;
}
match self.answer(element, constraint, structure, subject) {
Ok(true) => {}
Ok(false) => {
let issue = self.failure(constraint, source, subject.location);
self.out().push(issue);
}
Err(reason) => {
let id = element.element_id().or(element.path()).unwrap_or_default();
self.out().unevaluated(
structure.url(),
id,
listed(constraint),
subject.location,
reason,
);
}
}
}
fn answer(
&self,
element: &Element<R>,
constraint: &ConstraintRead<'_>,
structure: &Structure<R>,
subject: &Subject<'_, 'i>,
) -> Result<bool, Reason> {
let key = constraint.key.unwrap_or_default();
let refuse = |stage, error| Reason::InvariantRefused {
stage,
error,
upstream: upstream(R::NAME, source(constraint, structure), key),
};
let text = constraint.expression.ok_or(Reason::NoExpression)?;
let expression = self
.validator()
.expression(text)
.map_err(|error| refuse(InvariantStage::Parse, error))?;
let choice = element
.path()
.filter(|path| choice_element(R::model(), path).is_some());
let types = element.types();
let narrowed: Vec<&str> = types.iter().filter_map(|kind| kind.code).collect();
let declared = versionless(source(constraint, structure)) == structure.url();
let checked = match choice {
Some(_) if declared && !narrowed.is_empty() => {
self.validator().check_types(&expression, &narrowed)
}
_ => self
.validator()
.check(&expression, choice.unwrap_or(subject.type_name)),
};
checked.map_err(|error| refuse(InvariantStage::Check, error))?;
self.evaluate(&expression, structure, subject, choice, None)
.map_err(|error| refuse(InvariantStage::Evaluate, error))?
.ok_or(Reason::InvariantEmpty)
}
pub(super) fn evaluate(
&self,
expression: &Expression,
structure: &Structure<R>,
subject: &Subject<'_, 'i>,
choice: Option<&str>,
extension: Option<&'i Value>,
) -> Result<Option<bool>, String> {
let scope = self.scope();
let resource = scope.last().copied();
let root = container(scope);
let Some(anchor) = subject.value.or(subject.element) else {
return Err("the occurrence carries nothing to evaluate on".to_owned());
};
let mut focus = if subject.resource {
Focus::resource(anchor)
} else {
Focus::primitive(subject.value, subject.element, subject.type_name, anchor)
}
.within(resource, root);
if let Some(path) = choice {
focus = focus.defined_by(path);
}
let references = References::new(self.validator(), scope, self.indexes());
let terminology = Terminology::new(self.validator(), structure.origin());
let conformance = Conformance::new(self.validator(), self.depth());
let mut environment = Environment::new(R::model())
.with_resolver(&references)
.with_terminology(&terminology)
.with_profiles(&conformance);
if let Some(extension) = extension {
let this = self.validator().expression("$this")?;
let focus = Focus::element(extension, "Extension", anchor).within(resource, root);
let items = this
.evaluate(&Environment::new(R::model()), focus)
.map_err(|error| chain(&error))?;
environment = environment.with_variable("extension", items);
}
expression
.evaluate(&environment, focus)
.and_then(|result| convert::boolean(R::model(), &result, "an invariant"))
.map_err(|error| chain(&error))
}
fn failure(&self, constraint: &ConstraintRead<'_>, source: &str, location: &str) -> Issue {
let key = constraint.key.unwrap_or_default().to_owned();
let severity = if best_practice(constraint) {
match self.validator().best_practice {
BestPractice::Warning => Severity::Warning,
BestPractice::Error => Severity::Error,
}
} else if constraint.severity == Some("warning") {
Severity::Warning
} else {
Severity::Error
};
let human = constraint.human.unwrap_or_default();
let message = format!("{key}: {human}");
Issue {
severity,
code: IssueType::Invariant,
expression: location.to_owned(),
rule: Rule::Constraint {
key,
source: source.to_owned(),
},
message,
}
}
}