use std::sync::Arc;
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::report::Constraint;
use crate::report::Reason;
use crate::set::ResolveError;
use crate::validate::CORE;
use crate::validate::ValidateError;
use crate::validate::resolver::Resolution;
use crate::validate::resolver::Via;
use crate::validate::structure::Structure;
use crate::validate::walk::Occurrence;
use crate::validate::walk::Walk;
#[derive(Debug, Clone)]
pub(crate) enum Verdict {
Conforms,
Fails(String),
Unknown(Reason),
}
#[derive(Debug, Clone)]
pub(crate) enum Memo {
InProgress,
Done(Verdict),
}
pub(crate) fn literal(value: &Value) -> Option<&str> {
value.get("reference").and_then(Value::as_str)
}
pub(crate) fn address(value: &Value) -> usize {
std::ptr::from_ref(value).addr()
}
impl<'i, R: Release> Walk<'_, '_, 'i, R> {
pub(crate) fn reference(&mut self, value: &'i Value, location: &str) {
let Some(reference) = literal(value) else {
return;
};
match self.resolve_reference(reference) {
Resolution::Ambiguous => self.out().issue(
Severity::Error,
IssueType::MultipleMatches,
MessageId::AmbiguousReference,
location,
"the reference matches more than one entry of the Bundle".to_owned(),
),
Resolution::Found {
target,
via: Via::Entry { from },
} => self.link(from, target),
Resolution::Found {
via: Via::Contained,
..
}
| Resolution::Missing
| Resolution::OutOfScope => {}
}
}
pub(crate) fn targets(
&mut self,
structure: &Structure<R>,
id: &str,
element: &R::ElementDefinition,
occurrence: &Occurrence<'i>,
code: Option<&str>,
) -> Result<(), ValidateError> {
let mut profiles = Vec::new();
let mut modes = Vec::new();
let mut versioning = None;
let types = element.types();
for kind in types
.iter()
.filter(|kind| code.is_some() && kind.code == code)
{
versioning = versioning.or(kind.versioning);
let targets = kind
.target_profiles
.iter()
.map(|profile| (*profile).to_owned());
if code == Some("Reference") {
profiles.extend(targets);
modes.extend(kind.aggregation.iter().map(|mode| (*mode).to_owned()));
} else {
let listed: Vec<String> = targets.collect();
if !listed.is_empty() {
self.out().unevaluated(
structure.url(),
id,
Constraint::TargetProfile { profiles: listed },
&occurrence.location,
Reason::CanonicalTarget,
);
}
}
}
if let Some(rule) = versioning {
self.versioning(structure, id, rule, code, occurrence);
}
let Some(value) = occurrence.value.filter(|value| value.as_object().is_some()) else {
return Ok(());
};
if profiles.is_empty() && modes.is_empty() {
return Ok(());
}
let reference = literal(value);
let resolution = reference.map(|reference| self.resolve_reference(reference));
let location = &occurrence.location;
if !modes.is_empty() {
let local = reference.is_some_and(|reference| reference.starts_with('#'));
self.aggregation(structure, id, &modes, (local, resolution), location);
}
if !profiles.is_empty() {
self.target_profiles(structure, id, &profiles, resolution, location)?;
}
Ok(())
}
fn aggregation(
&mut self,
structure: &Structure<R>,
id: &str,
modes: &[String],
(local, resolution): (bool, Option<Resolution<'i>>),
location: &str,
) {
let allows = |mode: &str| modes.iter().any(|allowed| allowed == mode);
let fault = match resolution {
None => Some("it carries no literal reference, which an aggregation requires"),
Some(Resolution::Ambiguous) => None,
Some(Resolution::Found {
via: Via::Contained,
..
}) => (!allows("contained")).then_some("it names a contained resource"),
Some(Resolution::Missing) if local => Some(if allows("contained") {
"it names no contained resource"
} else {
"it is a local reference"
}),
Some(Resolution::Found {
via: Via::Entry { .. },
..
}) => (!allows("referenced") && !allows("bundled"))
.then_some("it names a resource outside the resource that holds it"),
Some(Resolution::Missing | Resolution::OutOfScope) => {
if allows("referenced") {
None
} else if !allows("bundled") {
Some("it names a resource outside the resource that holds it")
} else if self.bundle().is_some() {
Some("it names no entry of the Bundle around it")
} else {
self.out().unevaluated(
structure.url(),
id,
Constraint::Aggregation {
modes: modes.to_vec(),
},
location,
Reason::NoBundle,
);
None
}
}
};
if let Some(fault) = fault {
self.out().issue(
Severity::Error,
IssueType::Structure,
MessageId::AggregationMismatch,
location,
format!(
"{id} allows the aggregation {}, and {fault}",
modes.join(", ")
),
);
}
}
fn target_profiles(
&mut self,
structure: &Structure<R>,
id: &str,
profiles: &[String],
resolution: Option<Resolution<'i>>,
location: &str,
) -> Result<(), ValidateError> {
let missing = match resolution {
Some(Resolution::Found { target, .. }) => Err(target),
None => Ok(Reason::NoLiteralReference),
Some(Resolution::Missing) => Ok(Reason::UnresolvedReference),
Some(Resolution::OutOfScope) => Ok(Reason::ReferenceOutOfScope),
Some(Resolution::Ambiguous) => Ok(Reason::AmbiguousReference),
};
let target = match missing {
Ok(reason) => {
self.out().unevaluated(
structure.url(),
id,
Constraint::TargetProfile {
profiles: profiles.to_vec(),
},
location,
reason,
);
return Ok(());
}
Err(target) => target,
};
let mut failed = Vec::new();
let mut unknown = false;
for profile in profiles {
match self.target_conformance(target, profile, structure, location)? {
Verdict::Conforms => return Ok(()),
Verdict::Fails(first) => failed.push((profile, first)),
Verdict::Unknown(reason) => {
unknown = true;
self.out().unevaluated(
structure.url(),
id,
Constraint::TargetProfile {
profiles: vec![profile.clone()],
},
location,
reason,
);
}
}
}
if unknown {
return Ok(());
}
let found: Vec<String> = failed
.iter()
.map(|(profile, first)| format!("against {profile}: {first}"))
.collect();
self.out().issue(
Severity::Error,
IssueType::Structure,
MessageId::TargetProfileMismatch,
location,
format!(
"the target of {id} conforms to none of {}; {}",
profiles.join(", "),
found.join("; ")
),
);
Ok(())
}
fn target_conformance(
&mut self,
target: &'i Value,
profile: &str,
from: &Structure<R>,
location: &str,
) -> Result<Verdict, ValidateError> {
let structure = match self.validator().structure(profile, Some(from)) {
Ok(structure) => structure,
Err(ValidateError::Resolve(
ResolveError::NotFound { .. } | ResolveError::Fragment { .. },
)) => return Ok(Verdict::Unknown(Reason::ProfileNotFound)),
Err(error) => return Err(error),
};
if matches!(structure.type_name(), "Resource" | "DomainResource")
&& structure.url().starts_with(CORE)
{
return Ok(Verdict::Conforms);
}
let key = (address(target), Arc::as_ptr(&structure).addr());
match self.memo(key) {
Some(Memo::InProgress) => return Ok(Verdict::Conforms),
Some(Memo::Done(verdict)) => return Ok(verdict),
None => {}
}
self.remember(key, Memo::InProgress);
let mut fork = self.fork_unclaimed();
fork.resource(
target,
Some(&format!("{location}.resolve()")),
Some(structure),
)?;
let found = fork.into_out();
let verdict = found.first_error().map_or(Verdict::Conforms, |issue| {
Verdict::Fails(format!("{} at {}", issue.message, issue.expression))
});
self.remember(key, Memo::Done(verdict.clone()));
Ok(verdict)
}
}