use std::sync::Arc;
mod content;
pub(crate) mod members;
use fhir_types::codec::Object;
use fhir_types::codec::Value;
use fhir_types::r4::element_definition::ElementDefinition;
use fhir_types::r4::schema::SCHEMAS;
use fhir_types::schema::FieldSchema;
use fhir_types::schema::Kind;
use fhir_types::schema::TypeSchema;
use crate::canonical::Canonical;
use crate::issue::IssueType;
use crate::issue::MessageId;
use crate::issue::Severity;
use crate::report::Collector;
use crate::report::Constraint;
use crate::report::Reason;
use crate::report::Report;
use crate::validate::ValidateError;
use crate::validate::Validator;
use crate::validate::slicing::Item;
use crate::validate::slicing::Site;
use crate::validate::structure::Structure;
use members::forms;
use members::knows;
#[derive(Debug, Clone)]
pub(crate) struct Cursor {
pub(crate) structure: Arc<Structure>,
pub(crate) prefix: String,
}
impl Cursor {
pub(crate) fn new(structure: Arc<Structure>, prefix: &str) -> Self {
Self {
structure,
prefix: prefix.to_owned(),
}
}
}
#[derive(Debug)]
pub(crate) struct Occurrence<'v> {
pub(crate) value: Option<&'v Value>,
pub(crate) element: Option<&'v Value>,
pub(crate) location: String,
}
#[derive(Debug, Clone)]
pub(crate) struct Form {
pub(crate) key: String,
pub(crate) kind: Kind,
pub(crate) choice: Option<&'static str>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Held<'a> {
pub(crate) id: &'a str,
pub(crate) element: Option<&'a ElementDefinition>,
pub(crate) slices: &'a [String],
}
#[derive(Debug)]
pub(crate) struct Walk<'v, 'set, 'i> {
validator: &'v Validator<'set>,
out: Collector,
scope: Vec<&'i Value>,
}
impl<'v, 'set, 'i> Walk<'v, 'set, 'i> {
pub(crate) fn new(validator: &'v Validator<'set>) -> Self {
Self {
validator,
out: Collector::default(),
scope: Vec::new(),
}
}
pub(crate) fn fork(&self) -> Self {
Self {
validator: self.validator,
out: Collector::default(),
scope: self.scope.clone(),
}
}
pub(crate) const fn validator(&self) -> &'v Validator<'set> {
self.validator
}
pub(crate) const fn out(&mut self) -> &mut Collector {
&mut self.out
}
pub(crate) fn into_out(self) -> Collector {
self.out
}
pub(crate) fn scope(&self) -> &[&'i Value] {
&self.scope
}
pub(crate) fn run(
mut self,
resource: &'i Value,
profile: Option<&str>,
) -> Result<Report, ValidateError> {
let profile = profile
.map(|profile| self.validator.structure(profile, None))
.transpose()?;
self.resource(resource, None, profile)?;
Ok(self.out.finish())
}
pub(crate) fn resource(
&mut self,
value: &'i Value,
location: Option<&str>,
profile: Option<Arc<Structure>>,
) -> Result<(), ValidateError> {
let severity = if location.is_none() {
Severity::Fatal
} else {
Severity::Error
};
let Some(object) = value.as_object() else {
self.out.issue(
severity,
IssueType::Structure,
MessageId::NotAnObject,
location.unwrap_or_default(),
"a resource is a JSON object".to_owned(),
);
return Ok(());
};
let resource_type = object.get("resourceType").and_then(Value::as_str);
let Some((name, schema)) = resource_type
.filter(|name| SCHEMAS.is_resource(name))
.and_then(|name| Some((name, SCHEMAS.type_named(name)?)))
else {
self.out.issue(
severity,
IssueType::Structure,
MessageId::UnknownResourceType,
location.unwrap_or_default(),
"resourceType names no R4 resource".to_owned(),
);
return Ok(());
};
let location = location.unwrap_or(name).to_owned();
let mut structure = match profile {
Some(profile) => profile,
None => self.validator.core(name)?,
};
if structure.type_name() != name {
self.out.issue(
Severity::Error,
IssueType::Structure,
MessageId::ProfileTypeMismatch,
&location,
format!(
"the profile {} constrains {}, not {name}",
structure.url(),
structure.type_name()
),
);
structure = self.validator.core(name)?;
}
self.claims(object, &structure, name, &location);
self.scope.push(value);
let walked = self.object(
object,
schema,
&Cursor::new(structure, name),
&location,
true,
);
self.scope.pop();
walked
}
fn claims(&mut self, object: &Object, structure: &Structure, name: &str, location: &str) {
let claimed = object
.get("meta")
.and_then(|meta| meta.get("profile"))
.and_then(Value::as_array)
.unwrap_or_default();
for url in claimed.iter().filter_map(Value::as_str) {
if Canonical::parse(url).url() != structure.url() {
self.out.unevaluated(
url,
name,
Constraint::ClaimedProfile {
url: url.to_owned(),
},
location,
Reason::Profiles,
);
}
}
}
fn object(
&mut self,
object: &'i Object,
schema: &'static TypeSchema,
cursor: &Cursor,
location: &str,
resource: bool,
) -> Result<(), ValidateError> {
if object.is_empty() {
self.out.issue(
Severity::Error,
IssueType::Structure,
MessageId::EmptyObject,
location,
"the object is empty".to_owned(),
);
}
if let Some(element) = cursor.structure.element(&cursor.prefix) {
self.list(&cursor.structure, &cursor.prefix, element, location);
}
for key in object.keys() {
if resource && key == "resourceType" {
continue;
}
if !knows(schema, key) {
self.out.issue(
Severity::Error,
IssueType::Structure,
MessageId::UnknownElement,
&format!("{location}.{key}"),
format!("{} defines no element {key}", schema.path),
);
}
}
for field in schema.fields {
self.field(object, schema, field, cursor, location)?;
}
Ok(())
}
fn field(
&mut self,
object: &'i Object,
schema: &'static TypeSchema,
field: &'static FieldSchema,
cursor: &Cursor,
location: &str,
) -> Result<(), ValidateError> {
let forms = forms(field, object);
if forms.len() > 1 {
self.out.issue(
Severity::Error,
IssueType::Structure,
MessageId::DuplicateChoice,
&format!("{location}.{}", field.name),
format!("{} carries more than one type", field.path),
);
}
let segment = field.path.rsplit('.').next().unwrap_or(field.name);
let element_id = format!("{}.{segment}", cursor.prefix);
let mut count = 0_usize;
let mut walked = Vec::new();
for form in forms {
let occurrences = self.occurrences(object, field, &form, location);
count = count.saturating_add(occurrences.len());
walked.push((form, occurrences));
}
let profiled = cursor.structure.element(&element_id);
self.cardinality(field, profiled, count, &element_id, location);
let items: Vec<Item<'_, 'i>> = walked
.iter()
.flat_map(|(form, occurrences)| {
occurrences
.iter()
.map(move |occurrence| Item { form, occurrence })
})
.collect();
let site = Site {
schema,
field,
cursor,
id: &element_id,
element: profiled,
location,
};
let mut chains = match profiled.filter(|element| element.slicing.is_some()) {
Some(entry) => self.slicing(&site, entry, &items)?,
None => Vec::new(),
}
.into_iter();
for (form, occurrences) in &walked {
let renamed = format!("{}.{}", cursor.prefix, form.key);
let (id, element) = match cursor.structure.element(&renamed) {
Some(element) if form.choice.is_some() => (renamed, Some(element)),
_ => (element_id.clone(), profiled),
};
if let (Some(code), Some(element)) = (form.choice, element)
&& !element.r#type.is_empty()
&& !element
.r#type
.iter()
.any(|kind| kind.code.value.as_deref() == Some(code))
{
let at = occurrences
.first()
.map_or(location, |first| first.location.as_str());
self.out.issue(
Severity::Error,
IssueType::Structure,
MessageId::ChoiceTypeNotAllowed,
at,
format!("{id} does not allow the type {code}"),
);
}
for occurrence in occurrences {
let slices = chains.next().unwrap_or_default();
let held = Held {
id: &id,
element,
slices: &slices,
};
self.apply(schema, field, form, occurrence, cursor, held)?;
}
}
Ok(())
}
pub(crate) fn apply(
&mut self,
schema: &'static TypeSchema,
field: &'static FieldSchema,
form: &Form,
occurrence: &Occurrence<'i>,
cursor: &Cursor,
held: Held<'_>,
) -> Result<(), ValidateError> {
let structure = Arc::clone(&cursor.structure);
let mut content = held.id;
let mut element = held.element;
if let Some(base) = held.element {
self.checks(&structure, held.id, base, occurrence)?;
}
for slice in held.slices {
let Some(defined) = structure.element(slice) else {
continue;
};
self.checks(&structure, slice, defined, occurrence)?;
element = Some(defined);
if structure.has_children(slice) {
content = slice;
}
}
let target = Held {
id: content,
element,
slices: if content == held.id { &[] } else { held.slices },
};
self.occurrence(schema, field, form, occurrence, cursor, target)
}
pub(crate) fn checks(
&mut self,
structure: &Structure,
id: &str,
element: &ElementDefinition,
occurrence: &Occurrence<'_>,
) -> Result<(), ValidateError> {
self.list(structure, id, element, &occurrence.location);
self.fixed_and_pattern(
element,
structure.url(),
id,
occurrence.value,
&occurrence.location,
)?;
let limit = element.max_length.as_ref().and_then(|length| length.value);
self.max_length(occurrence.value, limit, id, &occurrence.location);
Ok(())
}
pub(crate) fn complex(
&mut self,
value: &'i Value,
name: &str,
cursor: &Cursor,
location: &str,
) -> Result<(), ValidateError> {
let Some(object) = value.as_object() else {
self.out.issue(
Severity::Error,
IssueType::Structure,
MessageId::WrongJsonType,
location,
format!("a {name} is written as a JSON object"),
);
return Ok(());
};
self.object(object, type_named(name)?, cursor, location, false)
}
}
fn type_named(name: &str) -> Result<&'static TypeSchema, ValidateError> {
SCHEMAS
.type_named(name)
.ok_or_else(|| ValidateError::UnknownType {
name: name.to_owned(),
})
}