mod definition;
mod discriminate;
mod instance;
mod path;
use std::collections::BTreeSet;
use std::sync::Arc;
use fhir_types::r4::element_definition::ElementDefinition;
use fhir_types::schema::FieldSchema;
use fhir_types::schema::TypeSchema;
use crate::issue::IssueType;
use crate::issue::MessageId;
use crate::issue::Severity;
use crate::report::Constraint;
use crate::report::Reason;
use crate::validate::ValidateError;
use crate::validate::slicing::instance::Node;
use crate::validate::slicing::path::Step;
use crate::validate::structure::Structure;
use crate::validate::walk::Cursor;
use crate::validate::walk::Form;
use crate::validate::walk::Held;
use crate::validate::walk::Occurrence;
use crate::validate::walk::Walk;
#[derive(Debug, Clone, Copy)]
pub(crate) struct Site<'a> {
pub(crate) schema: &'static TypeSchema,
pub(crate) field: &'static FieldSchema,
pub(crate) cursor: &'a Cursor,
pub(crate) id: &'a str,
pub(crate) element: Option<&'a ElementDefinition>,
pub(crate) location: &'a str,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Item<'a, 'i> {
pub(crate) form: &'a Form,
pub(crate) occurrence: &'a Occurrence<'i>,
}
#[derive(Debug)]
pub(crate) struct Discriminator {
pub(crate) kind: String,
pub(crate) path: String,
pub(crate) steps: Option<Vec<Step>>,
}
impl Discriminator {
pub(crate) fn constraint(&self) -> Constraint {
Constraint::Discriminator {
kind: self.kind.clone(),
path: self.path.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Unknown {
pub(crate) constraint: Constraint,
pub(crate) reason: Reason,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Verdict {
Match,
NoMatch,
Unknown(Vec<Unknown>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Placement {
Slice(usize),
Unmatched,
Undecided,
}
#[derive(Debug)]
struct Plan<'p> {
structure: &'p Arc<Structure>,
entry_id: &'p str,
slices: &'p [(&'p str, &'p ElementDefinition)],
discriminators: &'p [Discriminator],
default: Option<usize>,
fallback: Option<usize>,
rules: &'p str,
ordered: bool,
}
type Selected<'i> = Result<Vec<Node<'i>>, Reason>;
impl<'i> Walk<'_, '_, 'i> {
pub(crate) fn slicing(
&mut self,
site: &Site<'_>,
entry: &ElementDefinition,
items: &[Item<'_, 'i>],
) -> Result<Vec<Vec<String>>, ValidateError> {
let mut chains = vec![Vec::new(); items.len()];
let all: Vec<usize> = (0..items.len()).collect();
self.level(site, site.id, entry, items, &all, &mut chains)?;
Ok(chains)
}
fn level(
&mut self,
site: &Site<'_>,
entry_id: &str,
entry: &ElementDefinition,
items: &[Item<'_, 'i>],
members: &[usize],
chains: &mut [Vec<String>],
) -> Result<(), ValidateError> {
let Some(slicing) = &entry.slicing else {
return Ok(());
};
let structure = Arc::clone(&site.cursor.structure);
let slices = structure.slices(entry_id);
let rules = slicing.rules.value.as_deref().unwrap_or("open");
let ordered = slicing
.ordered
.as_ref()
.and_then(|flag| flag.value)
.unwrap_or(false);
let discriminators: Vec<Discriminator> = slicing
.discriminator
.iter()
.map(|discriminator| {
let path = discriminator.path.value.clone().unwrap_or_default();
Discriminator {
kind: discriminator.r#type.value.clone().unwrap_or_default(),
steps: path::parse(&path),
path,
}
})
.collect();
let default = slices
.iter()
.position(|(id, _)| id.ends_with(":@default") || id.ends_with("/@default"));
let fallback = match default {
Some(_) if rules != "closed" => {
self.out().unevaluated(
structure.url(),
entry_id,
Constraint::Slicing,
site.location,
Reason::DefaultSliceNotClosed,
);
None
}
found => found,
};
let plan = Plan {
structure: &structure,
entry_id,
slices: &slices,
discriminators: &discriminators,
default,
fallback,
rules,
ordered,
};
let mut placements = Vec::with_capacity(members.len());
let mut undecided: BTreeSet<usize> = BTreeSet::new();
for &member in members {
let Some(item) = items.get(member) else {
continue;
};
let (placement, unknown) = self.place(site, &plan, *item)?;
undecided.extend(unknown);
placements.push((member, placement));
}
self.rules(&plan, items, &placements);
for (index, (slice_id, slice)) in slices.iter().enumerate() {
let inside: Vec<usize> = placements
.iter()
.filter(|(_, placement)| *placement == Placement::Slice(index))
.map(|(member, _)| *member)
.collect();
if !undecided.contains(&index) {
self.slice_cardinality(site, slice_id, slice, inside.len());
}
for &member in &inside {
if let Some(chain) = chains.get_mut(member) {
chain.push((*slice_id).to_owned());
}
}
if slice.slicing.is_some() {
self.level(site, slice_id, slice, items, &inside, chains)?;
}
}
Ok(())
}
fn place(
&mut self,
site: &Site<'_>,
plan: &Plan<'_>,
item: Item<'_, 'i>,
) -> Result<(Placement, Vec<usize>), ValidateError> {
let location = &item.occurrence.location;
let node = Node::item(site.field, item.form, item.occurrence);
let selected: Vec<Option<Selected<'i>>> = plan
.discriminators
.iter()
.map(|discriminator| {
let steps = discriminator.steps.as_ref()?;
let node = node?;
Some(self.follow(node, steps, &discriminator.path, location))
})
.collect();
let mut matched = Vec::new();
let mut unknown = Vec::new();
for (index, (slice_id, _)) in plan.slices.iter().enumerate() {
if Some(index) == plan.default {
continue;
}
let verdict = if plan.discriminators.is_empty() {
self.speculate(site, slice_id, item)?
} else {
let found = (plan.structure, *slice_id);
self.verdict(found, plan.discriminators, &selected, location)?
};
match verdict {
Verdict::Match => matched.push(index),
Verdict::NoMatch => {}
Verdict::Unknown(reasons) => {
for reason in reasons {
self.out().unevaluated(
plan.structure.url(),
plan.entry_id,
reason.constraint,
location,
reason.reason,
);
}
unknown.push(index);
}
}
}
let placement = match matched.as_slice() {
[first, second, ..] => {
let name = |index: &usize| plan.slices.get(*index).map_or("", |(id, _)| *id);
self.out().issue(
Severity::Error,
IssueType::Structure,
MessageId::OverlappingSlices,
location,
format!(
"the item matches the slices {} and {}, which overlap",
name(first),
name(second)
),
);
Placement::Slice(*first)
}
[only] => Placement::Slice(*only),
[] if !unknown.is_empty() => return Ok((Placement::Undecided, unknown)),
[] => plan.fallback.map_or(Placement::Unmatched, Placement::Slice),
};
Ok((placement, Vec::new()))
}
fn rules(
&mut self,
plan: &Plan<'_>,
items: &[Item<'_, 'i>],
placements: &[(usize, Placement)],
) {
let entry_id = plan.entry_id;
let name = |index: usize| plan.slices.get(index).map_or("", |(id, _)| *id);
let mut seen_unmatched = false;
let mut highest: Option<usize> = None;
for &(member, placement) in placements {
let Some(item) = items.get(member) else {
continue;
};
let location = &item.occurrence.location;
match placement {
Placement::Unmatched => {
if plan.rules == "closed" {
self.out().issue(
Severity::Error,
IssueType::Structure,
MessageId::UnmatchedSliceItem,
location,
format!(
"the item matches no slice of {entry_id}, whose slicing is closed"
),
);
}
seen_unmatched = true;
}
Placement::Slice(index) => {
if plan.rules == "openAtEnd" && seen_unmatched {
self.out().issue(
Severity::Error,
IssueType::Structure,
MessageId::OpenAtEnd,
location,
format!("the item of the slice {} follows an item in no slice, and {} is open only at the end", name(index), entry_id),
);
}
if plan.ordered
&& let Some(previous) = highest
&& index < previous
{
self.out().issue(
Severity::Error,
IssueType::Structure,
MessageId::SliceOrder,
location,
format!("the item of the slice {} follows an item of the slice {}, and the slicing is ordered", name(index), name(previous)),
);
}
highest = Some(highest.map_or(index, |previous| previous.max(index)));
}
Placement::Undecided => {}
}
}
}
fn slice_cardinality(
&mut self,
site: &Site<'_>,
slice_id: &str,
slice: &ElementDefinition,
count: usize,
) {
let min = slice.min.as_ref().and_then(|min| min.value).unwrap_or(0);
let max = match slice.max.as_ref().and_then(|max| max.value.as_deref()) {
Some("*") | None => None,
Some(max) => max.parse::<u32>().ok(),
};
if usize::try_from(min).is_ok_and(|min| count < min) {
self.out().issue(
Severity::Error,
IssueType::Required,
MessageId::MinCardinality,
site.location,
format!("{slice_id} occurs {count} times, below its minimum of {min}"),
);
}
if let Some(max) = max
&& usize::try_from(max).is_ok_and(|max| count > max)
{
self.out().issue(
Severity::Error,
IssueType::Structure,
MessageId::MaxCardinality,
&format!("{}.{}", site.location, site.field.name),
format!("{slice_id} occurs {count} times, above its maximum of {max}"),
);
}
}
fn speculate(
&mut self,
site: &Site<'_>,
slice_id: &str,
item: Item<'_, 'i>,
) -> Result<Verdict, ValidateError> {
let mut fork = self.fork();
let slices = [slice_id.to_owned()];
let held = Held {
id: site.id,
element: site.element,
slices: &slices,
};
fork.apply(
site.schema,
site.field,
item.form,
item.occurrence,
site.cursor,
held,
)?;
Ok(if fork.out().is_valid() {
Verdict::Match
} else {
Verdict::NoMatch
})
}
fn verdict(
&mut self,
(structure, slice_id): (&Arc<Structure>, &str),
discriminators: &[Discriminator],
selected: &[Option<Selected<'i>>],
location: &str,
) -> Result<Verdict, ValidateError> {
let mut unknown = Vec::new();
for (discriminator, nodes) in discriminators.iter().zip(selected) {
let nodes = match nodes {
None if discriminator.steps.is_none() => {
unknown.push(Unknown {
constraint: discriminator.constraint(),
reason: Reason::DiscriminatorPath,
});
continue;
}
None => {
unknown.push(Unknown {
constraint: discriminator.constraint(),
reason: Reason::NoDiscriminatorValue,
});
continue;
}
Some(Err(reason)) => {
unknown.push(Unknown {
constraint: discriminator.constraint(),
reason: *reason,
});
continue;
}
Some(Ok(nodes)) => nodes,
};
match self.discriminate(structure, slice_id, discriminator, nodes, location)? {
Verdict::Match => {}
Verdict::NoMatch => return Ok(Verdict::NoMatch),
Verdict::Unknown(found) => unknown.extend(found),
}
}
Ok(if unknown.is_empty() {
Verdict::Match
} else {
Verdict::Unknown(unknown)
})
}
}