use std::cell::RefCell;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::rc::Rc;
use std::sync::Arc;
mod content;
pub(crate) mod members;
use fhir_types::codec::Object;
use fhir_types::codec::Value;
use fhir_types::definition::ElementDefinitionRead;
use fhir_types::schema::FieldSchema;
use fhir_types::schema::Kind;
use fhir_types::schema::Schemas;
use fhir_types::schema::TypeSchema;
use crate::issue::IssueType;
use crate::issue::MessageId;
use crate::issue::Severity;
use crate::release::Release;
use crate::report::Collector;
use crate::report::Report;
use crate::validate::Element;
use crate::validate::ValidateError;
use crate::validate::Validator;
use crate::validate::invariant::Subject;
use crate::validate::reference::Memo;
use crate::validate::reference::address;
use crate::validate::resolver::Indexes;
use crate::validate::resolver::Resolution;
use crate::validate::slicing::Item;
use crate::validate::slicing::Site;
use crate::validate::structure::Structure;
use crate::validate::terminology::binding::Bound;
use members::forms;
#[derive(Debug, Clone)]
pub(crate) struct Cursor<R: Release> {
pub(crate) structure: Arc<Structure<R>>,
pub(crate) prefix: String,
}
impl<R: Release> Cursor<R> {
pub(crate) fn new(structure: Arc<Structure<R>>, 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, R: Release> {
pub(crate) id: &'a str,
pub(crate) element: Option<&'a Element<R>>,
pub(crate) slices: &'a [String],
}
#[derive(Debug, Clone)]
pub(crate) struct Host<'i, R: Release> {
pub(crate) value: Option<&'i Value>,
pub(crate) element: Option<&'i Value>,
pub(crate) path: &'static str,
pub(crate) reference: Option<&'static str>,
pub(crate) code: Option<&'static str>,
pub(crate) structure: Arc<Structure<R>>,
pub(crate) ids: Vec<String>,
}
#[derive(Debug, Default)]
struct Shared {
targets: BTreeMap<(usize, usize), Memo>,
links: BTreeMap<usize, BTreeSet<usize>>,
}
#[derive(Debug)]
pub(crate) struct Walk<'v, 'set, 'i, R: Release> {
validator: &'v Validator<'set, R>,
out: Collector,
scope: Vec<&'i Value>,
hosts: Vec<Host<'i, R>>,
displayed: BTreeSet<String>,
invariants: BTreeSet<(String, String, String)>,
depth: u8,
claims: bool,
shared: Rc<RefCell<Shared>>,
indexes: Rc<Indexes<'i>>,
}
impl<'v, 'set, 'i, R: Release> Walk<'v, 'set, 'i, R> {
pub(crate) fn new(validator: &'v Validator<'set, R>) -> Self {
Self::nested(validator, 0)
}
pub(crate) fn nested(validator: &'v Validator<'set, R>, depth: u8) -> Self {
Self {
validator,
out: Collector::default(),
scope: Vec::new(),
hosts: Vec::new(),
displayed: BTreeSet::new(),
invariants: BTreeSet::new(),
depth,
claims: true,
shared: Rc::default(),
indexes: Rc::default(),
}
}
pub(crate) fn fork(&self) -> Self {
Self {
validator: self.validator,
out: Collector::default(),
scope: self.scope.clone(),
hosts: self.hosts.clone(),
displayed: BTreeSet::new(),
invariants: BTreeSet::new(),
depth: self.depth,
claims: self.claims,
shared: Rc::clone(&self.shared),
indexes: Rc::clone(&self.indexes),
}
}
pub(crate) fn fork_unclaimed(&self) -> Self {
Self {
claims: false,
..self.fork()
}
}
pub(crate) const fn follows_claims(&self) -> bool {
self.claims
}
pub(crate) fn memo(&self, key: (usize, usize)) -> Option<Memo> {
self.shared.borrow().targets.get(&key).cloned()
}
pub(crate) fn remember(&self, key: (usize, usize), memo: Memo) {
self.shared.borrow_mut().targets.insert(key, memo);
}
pub(crate) fn link(&self, from: &Value, to: &Value) {
self.shared
.borrow_mut()
.links
.entry(address(from))
.or_default()
.insert(address(to));
}
pub(crate) fn links(&self, from: usize) -> Vec<usize> {
self.shared
.borrow()
.links
.get(&from)
.map(|to| to.iter().copied().collect())
.unwrap_or_default()
}
pub(crate) fn indexes(&self) -> &Indexes<'i> {
&self.indexes
}
pub(crate) fn resolve_reference_in(
&self,
reference: &str,
scope: &[&'i Value],
) -> Resolution<'i> {
self.validator
.resolver()
.resolve(reference, scope, &self.indexes)
}
pub(crate) fn resolve_reference(&self, reference: &str) -> Resolution<'i> {
self.resolve_reference_in(reference, &self.scope)
}
pub(crate) fn bundle(&self) -> Option<&'i Value> {
self.validator.resolver().bundle(&self.scope, &self.indexes)
}
pub(crate) const fn validator(&self) -> &'v Validator<'set, R> {
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 first_display(&mut self, location: &str) -> bool {
self.displayed.insert(location.to_owned())
}
pub(crate) fn first_invariant(&mut self, location: &str, key: &str, source: &str) -> bool {
self.invariants
.insert((location.to_owned(), key.to_owned(), source.to_owned()))
}
pub(crate) const fn depth(&self) -> u8 {
self.depth
}
pub(crate) fn scope(&self) -> &[&'i Value] {
&self.scope
}
pub(crate) fn hosts(&self) -> &[Host<'i, R>] {
&self.hosts
}
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<R>>>,
) -> 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 schemas = R::schemas();
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(),
format!("resourceType names no {} resource", R::NAME),
);
return Ok(());
};
let location = location.unwrap_or(name).to_owned();
let mut structure = match profile {
Some(profile) => profile,
None => self.validator.core(name)?,
};
let inherited = matches!(structure.type_name(), "Resource" | "DomainResource")
&& schemas.specializes(name, structure.type_name());
if structure.type_name() != name && !inherited {
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)?;
}
let root = structure.type_name().to_owned();
self.scope.push(value);
self.hosts.push(Host {
value: Some(value),
element: None,
path: schema.name,
reference: None,
code: Some(schema.name),
structure: Arc::clone(&structure),
ids: vec![root.clone()],
});
let walked = self.object(
value,
object,
schema,
&Cursor::new(Arc::clone(&structure), &root),
&location,
true,
);
self.hosts.pop();
self.scope.pop();
walked?;
if name == "Bundle" {
self.document(value, &location);
}
self.claims(value, &structure, name, &location)
}
fn object(
&mut self,
value: &'i Value,
object: &'i Object,
schema: &'static TypeSchema,
cursor: &Cursor<R>,
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) {
let subject = Subject {
value: Some(value),
element: None,
type_name: schema.path,
location,
resource,
};
self.invariants(&cursor.structure, element, &subject);
if !cursor.prefix.contains('.') && schema.name == cursor.structure.type_name() {
let held = (cursor.prefix.as_str(), element);
let bound = Bound::Object(object);
self.binding(&cursor.structure, held, schema.name, bound, location)?;
}
}
for key in object.keys() {
if resource && key == "resourceType" {
continue;
}
if schema.member(key).is_none() {
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<R>,
location: &str,
) -> Result<(), ValidateError> {
if let Some(base) = self.base_cursor(schema, field, cursor)? {
return self.field(object, schema, field, &base, location);
}
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.types().is_empty()
&& !element.types().iter().any(|kind| kind.code == 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(())
}
fn base_cursor(
&self,
schema: &'static TypeSchema,
field: &'static FieldSchema,
cursor: &Cursor<R>,
) -> Result<Option<Cursor<R>>, ValidateError> {
if cursor.structure.mentions(
&format!("{}.{}", cursor.prefix, field.name),
matches!(field.kind, Kind::Choice(_)),
) {
return Ok(None);
}
let root = schema.path.split('.').next().unwrap_or(schema.path);
let core = self.validator.core(root)?;
if Arc::ptr_eq(&core, &cursor.structure) {
return Ok(None);
}
Ok(Some(Cursor::new(core, schema.path)))
}
pub(crate) fn apply(
&mut self,
schema: &'static TypeSchema,
field: &'static FieldSchema,
form: &Form,
occurrence: &Occurrence<'i>,
cursor: &Cursor<R>,
held: Held<'_, R>,
) -> Result<(), ValidateError> {
let structure = Arc::clone(&cursor.structure);
let code = form.choice.or_else(|| match form.kind {
Kind::Complex(name) => Some(name),
_ => field.types.first().copied(),
});
let mut content = held.id;
let mut element = held.element;
if let Some(base) = held.element {
self.checks(&structure, held.id, base, occurrence, code)?;
}
for slice in held.slices {
let Some(defined) = structure.element(slice) else {
continue;
};
self.checks(&structure, slice, defined, occurrence, code)?;
element = Some(defined);
if structure.has_children(slice) {
content = slice;
}
}
let target = Held {
id: content,
element,
slices: if content == held.id { &[] } else { held.slices },
};
let mut ids = vec![held.id.to_owned()];
ids.extend(held.slices.iter().cloned());
self.hosts.push(Host {
value: occurrence.value,
element: occurrence.element,
path: field.path,
reference: field.content_reference,
code,
structure: Arc::clone(&structure),
ids,
});
let walked = self.occurrence(schema, field, form, occurrence, cursor, target);
self.hosts.pop();
walked
}
pub(crate) fn checks(
&mut self,
structure: &Structure<R>,
id: &str,
element: &Element<R>,
occurrence: &Occurrence<'i>,
code: Option<&str>,
) -> Result<(), ValidateError> {
let type_name = occurrence_type(R::schemas(), occurrence.value, code);
if let Some(type_name) = &type_name {
let subject = Subject {
value: occurrence.value,
element: occurrence.element,
type_name,
location: &occurrence.location,
resource: false,
};
self.invariants(structure, element, &subject);
} else {
self.untyped_invariants(structure, id, element, &occurrence.location);
}
self.targets(structure, id, element, occurrence, code)?;
if let (Some(code), Some(value)) = (code, occurrence.value.and_then(Bound::of)) {
self.binding(structure, (id, element), code, value, &occurrence.location)?;
}
self.fixed_and_pattern(
element,
structure.url(),
id,
occurrence.value,
&occurrence.location,
)?;
self.max_length(
occurrence.value,
element.max_length(),
id,
&occurrence.location,
);
Ok(())
}
pub(crate) fn complex(
&mut self,
value: &'i Value,
name: &str,
cursor: &Cursor<R>,
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(
value,
object,
type_named(R::schemas(), name)?,
cursor,
location,
false,
)
}
}
pub(crate) fn occurrence_type(
schemas: &'static Schemas,
value: Option<&Value>,
code: Option<&str>,
) -> Option<String> {
let code = code?;
let named = value
.filter(|_| schemas.specializes(code, "Resource"))
.and_then(|value| value.get("resourceType"))
.and_then(Value::as_str)
.filter(|name| schemas.is_resource(name));
if let Some(name) = named {
return Some(name.to_owned());
}
Some(
schemas
.type_named(code)
.map_or(code, |schema| schema.path)
.to_owned(),
)
}
fn type_named(schemas: &'static Schemas, name: &str) -> Result<&'static TypeSchema, ValidateError> {
schemas
.type_named(name)
.ok_or_else(|| ValidateError::UnknownType {
name: name.to_owned(),
})
}