use std::collections::BTreeSet;
use std::sync::Arc;
use fhir_types::codec::Value;
use fhir_types::definition::ElementDefinitionRead;
use fhir_types::schema::Schemas;
use crate::canonical::Canonical;
use crate::issue::IssueType;
use crate::issue::MessageId;
use crate::issue::Severity;
use crate::release::Release;
use crate::report::Collector;
use crate::report::Constraint;
use crate::report::Reason;
use crate::set::ResolveError;
use crate::validate::ValidateError;
use crate::validate::structure::Structure;
use crate::validate::walk::Cursor;
use crate::validate::walk::Held;
use crate::validate::walk::Occurrence;
use crate::validate::walk::Walk;
#[derive(Debug, Clone, Copy)]
pub(crate) enum Subject<'a, 'i> {
Complex {
value: &'i Value,
name: &'a str,
},
Resource {
value: &'i Value,
},
Primitive {
occurrence: &'a Occurrence<'i>,
code: &'a str,
},
}
#[derive(Debug)]
pub(crate) enum Conformance {
Checked(Collector),
Unknown(Reason),
}
pub(crate) fn profiles_for<E: ElementDefinitionRead>(
element: Option<&E>,
code: &str,
) -> Vec<String> {
element
.into_iter()
.flat_map(ElementDefinitionRead::types)
.filter(|kind| kind.code == Some(code))
.flat_map(|kind| kind.profiles)
.map(str::to_owned)
.collect()
}
pub(crate) fn resource_profiles<E: ElementDefinitionRead>(
schemas: &'static Schemas,
element: Option<&E>,
kind: &str,
) -> Vec<String> {
element
.into_iter()
.flat_map(ElementDefinitionRead::types)
.filter(|entry| {
entry
.code
.is_some_and(|code| schemas.specializes(kind, code))
})
.flat_map(|entry| entry.profiles)
.map(str::to_owned)
.collect()
}
impl<'i, R: Release> Walk<'_, '_, 'i, R> {
pub(crate) fn conformance(
&self,
subject: Subject<'_, 'i>,
(profile, from): (&str, &Structure<R>),
location: &str,
) -> Result<Conformance, ValidateError> {
let structure = match self.validator().structure(profile, Some(from)) {
Ok(structure) => structure,
Err(ValidateError::Resolve(
ResolveError::NotFound { .. } | ResolveError::Fragment { .. },
)) => {
return Ok(Conformance::Unknown(Reason::ProfileNotFound));
}
Err(error) => return Err(error),
};
let mut fork = self.fork();
let name = match subject {
Subject::Resource { value } => {
fork.resource(value, Some(location), Some(structure))?;
return Ok(Conformance::Checked(fork.into_out()));
}
Subject::Complex { name, .. } | Subject::Primitive { code: name, .. } => name,
};
if structure.type_name() != name {
fork.out().issue(
Severity::Error,
IssueType::Structure,
MessageId::ProfileTypeMismatch,
location,
format!(
"the profile {} constrains {}, not {name}",
structure.url(),
structure.type_name()
),
);
return Ok(Conformance::Checked(fork.into_out()));
}
let root = Cursor::new(Arc::clone(&structure), name);
match subject {
Subject::Complex { value, name } => fork.complex(value, name, &root, location)?,
Subject::Primitive { occurrence, code } => {
if let Some(element) = structure.element(code) {
fork.checks(&structure, code, element, occurrence, Some(code))?;
}
if let Some(element) = occurrence.element {
fork.complex(element, "Element", &root, location)?;
}
}
Subject::Resource { .. } => {}
}
Ok(Conformance::Checked(fork.into_out()))
}
pub(crate) fn type_profiles(
&mut self,
cursor: &Cursor<R>,
target: Held<'_, R>,
subject: Subject<'_, 'i>,
profiles: &[String],
location: &str,
) -> Result<bool, ValidateError> {
let mut checked = Vec::new();
for profile in profiles {
match self.conformance(subject, (profile, &cursor.structure), location)? {
Conformance::Checked(found) => checked.push((profile, found)),
Conformance::Unknown(reason) => self.out().unevaluated(
cursor.structure.url(),
target.id,
Constraint::TypeProfile {
profiles: vec![profile.clone()],
},
location,
reason,
),
}
}
if let Some(index) = checked.iter().position(|(_, found)| found.is_valid()) {
let (_, found) = checked.swap_remove(index);
self.out().absorb(found);
return Ok(false);
}
if let ([(_, _)], [_]) = (checked.as_slice(), profiles) {
if let Some((_, found)) = checked.pop() {
self.out().absorb(found);
}
return Ok(false);
}
if checked.is_empty() {
return Ok(true);
}
let all = profiles.join(", ");
for (profile, found) in checked {
let first = found.first_error().map_or_else(String::new, |issue| {
format!(": {} at {}", issue.message, issue.expression)
});
self.out().issue(
Severity::Error,
IssueType::Structure,
MessageId::TypeProfileMismatch,
location,
format!("the content conforms to none of {all}; against {profile}{first}"),
);
}
Ok(true)
}
pub(crate) fn claims(
&mut self,
value: &'i Value,
structure: &Structure<R>,
name: &str,
location: &str,
) -> Result<(), ValidateError> {
if !self.follows_claims() {
return Ok(());
}
let claimed = value
.get("meta")
.and_then(|meta| meta.get("profile"))
.and_then(Value::as_array)
.unwrap_or_default();
let mut seen = BTreeSet::new();
for url in claimed.iter().filter_map(Value::as_str) {
if Canonical::parse(url).url() == structure.url() || !seen.insert(url) {
continue;
}
let claim = match self.validator().structure(url, None) {
Ok(claim) => claim,
Err(ValidateError::Resolve(
ResolveError::NotFound { .. } | ResolveError::Fragment { .. },
)) => {
self.out().unevaluated(
url,
name,
Constraint::ClaimedProfile {
url: url.to_owned(),
},
location,
Reason::ProfileNotFound,
);
continue;
}
Err(error) => return Err(error),
};
let mut fork = self.fork_unclaimed();
fork.resource(value, Some(location), Some(Arc::clone(&claim)))?;
let found = fork.into_out();
if !found.is_valid() {
self.out().issue(
Severity::Error,
IssueType::Structure,
MessageId::ClaimedProfileMismatch,
location,
format!(
"the resource does not conform to {}, a profile it claims",
claim.url()
),
);
}
self.out().absorb_new(found);
}
Ok(())
}
pub(crate) fn extension_profiles(
&mut self,
cursor: &Cursor<R>,
target: Held<'_, R>,
value: &'i Value,
location: &str,
) -> Result<(), ValidateError> {
let profiles = profiles_for(target.element, "Extension");
let url = value.get("url").and_then(Value::as_str);
let named = url.is_some_and(|url| {
profiles
.iter()
.any(|profile| Canonical::parse(profile).url() == url)
});
if !profiles.is_empty() && !named {
let subject = Subject::Complex {
value,
name: "Extension",
};
self.type_profiles(cursor, target, subject, &profiles, location)?;
}
Ok(())
}
pub(crate) fn primitive_profiles(
&mut self,
cursor: &Cursor<R>,
target: Held<'_, R>,
occurrence: &Occurrence<'i>,
code: &str,
) -> Result<(), ValidateError> {
let profiles = profiles_for(target.element, code);
if !profiles.is_empty() {
let subject = Subject::Primitive { occurrence, code };
self.type_profiles(cursor, target, subject, &profiles, &occurrence.location)?;
}
Ok(())
}
}