use std::collections::{BTreeMap, BTreeSet, HashMap};
use octofhir_fhirschema::types::{FhirSchemaConstraint, FhirSchemaSliceMatch, FhirSchemaSlicing};
use octofhir_fhirschema::{FhirSchema, FhirSchemaElement, FhirVersion, StructureDefinition};
use serde::Serialize;
use serde_json::Value;
use crate::package::{Definition, Loaded};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Strength {
Example,
Preferred,
Extensible,
Required,
}
impl Strength {
fn parse(text: &str) -> Self {
match text {
"required" => Self::Required,
"extensible" => Self::Extensible,
"preferred" => Self::Preferred,
_ => Self::Example,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Example => "example",
Self::Preferred => "preferred",
Self::Extensible => "extensible",
Self::Required => "required",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Binding {
pub strength: Strength,
pub value_set: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Constraint {
pub severity: String,
pub expression: String,
pub human: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Slicing {
pub rules: String,
pub ordered: bool,
pub discriminators: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Node {
pub min: u32,
pub max: Option<u32>,
pub types: BTreeSet<String>,
pub targets: BTreeSet<String>,
pub must_support: bool,
pub is_modifier: bool,
pub binding: Option<Binding>,
pub constraints: BTreeMap<String, Constraint>,
pub pattern: Option<Value>,
pub slicing: Option<Slicing>,
pub slices: BTreeMap<String, Node>,
pub children: BTreeMap<String, Node>,
pub extension_url: Option<String>,
}
impl Node {
pub fn unconstrained() -> Self {
Self {
min: 0,
max: None,
types: BTreeSet::new(),
targets: BTreeSet::new(),
must_support: false,
is_modifier: false,
binding: None,
constraints: BTreeMap::new(),
pattern: None,
slicing: None,
slices: BTreeMap::new(),
children: BTreeMap::new(),
extension_url: None,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Profile {
pub url: String,
pub version: Option<String>,
pub name: String,
pub title: Option<String>,
pub type_name: String,
pub base: Option<String>,
pub kind: String,
pub file: String,
#[serde(skip)]
pub tree: Node,
}
pub struct Pool {
by_url: HashMap<String, FhirSchema>,
core: &'static HashMap<String, FhirSchema>,
own: Vec<(String, FhirSchema, Definition)>,
pub skipped: Vec<(String, String)>,
pub missing: std::cell::RefCell<BTreeSet<String>>,
pub borrowed: BTreeSet<String>,
}
const CORE_PREFIX: &str = "http://hl7.org/fhir/StructureDefinition/";
pub fn release(fhir_versions: &[String]) -> FhirVersion {
let version = fhir_versions.first().map(String::as_str).unwrap_or("4.0.1");
if version.starts_with("4.3") {
FhirVersion::R4B
} else if version.starts_with("5.") {
FhirVersion::R5
} else if version.starts_with("6.") {
FhirVersion::R6
} else {
FhirVersion::R4
}
}
impl Pool {
pub fn new(loaded: &Loaded) -> Self {
let core = octofhir_fhirschema::get_schemas(release(&loaded.package.fhir_versions));
let mut pool = Self {
by_url: HashMap::new(),
core,
own: Vec::new(),
skipped: Vec::new(),
missing: Default::default(),
borrowed: BTreeSet::new(),
};
for package in &loaded.dependencies {
for definition in &package.definitions {
if let Ok(schema) = convert(&definition.json) {
pool.by_url.insert(schema.url.clone(), schema);
}
}
}
for definition in &loaded.package.definitions {
match convert(&definition.json) {
Ok(schema) => {
pool.by_url.insert(schema.url.clone(), schema.clone());
if definition.json["derivation"] == "constraint" {
pool.own
.push((schema.url.clone(), schema, definition.clone()));
}
}
Err(error) => pool.skipped.push((definition.file.clone(), error)),
}
}
pool
}
pub fn pair(old: &Loaded, new: &Loaded) -> (Self, Self) {
let (mut old_pool, mut new_pool) = (Self::new(old), Self::new(new));
old_pool.borrow_bases(&new_pool);
new_pool.borrow_bases(&old_pool);
(old_pool, new_pool)
}
pub fn borrow_bases(&mut self, other: &Pool) {
let mut pending: Vec<String> = self
.own
.iter()
.filter_map(|(_, schema, _)| schema.base.clone())
.collect();
let mut seen = BTreeSet::new();
while let Some(base) = pending.pop() {
let plain = base.split('|').next().unwrap_or(&base).to_owned();
if !seen.insert(plain.clone()) {
continue;
}
let schema = match self.schema(&plain) {
Some(schema) => schema.clone(),
None => match other.by_url.get(&plain) {
Some(schema) => {
self.by_url.insert(plain.clone(), schema.clone());
self.borrowed.insert(plain);
schema.clone()
}
None => continue,
},
};
pending.extend(schema.base);
}
}
fn schema(&self, url: &str) -> Option<&FhirSchema> {
let plain = url.split('|').next().unwrap_or(url);
self.by_url.get(plain).or_else(|| {
plain
.strip_prefix(CORE_PREFIX)
.and_then(|name| self.core.get(name))
})
}
pub fn profiles(&self) -> Vec<Profile> {
self.own
.iter()
.map(|(url, schema, definition)| Profile {
url: url.clone(),
version: schema.version.clone(),
name: schema.name.clone(),
title: definition.json["title"].as_str().map(str::to_owned),
type_name: schema.type_name.clone(),
base: schema.base.clone(),
kind: schema.kind.clone(),
file: definition.file.clone(),
tree: self.effective(schema),
})
.collect()
}
pub fn type_of(&self, url: &str) -> Option<String> {
let plain = url.split('|').next().unwrap_or(url);
match self.by_url.get(plain) {
Some(schema) => Some(schema.type_name.clone()),
None => plain.strip_prefix(CORE_PREFIX).map(str::to_owned),
}
}
pub fn base_node(&self, type_name: &str, child: &str) -> Option<Node> {
let schema = self.core.get(type_name)?;
let parent = level(schema);
let element = parent.elements.as_ref()?.get(child)?;
Some(node(self.core, element, Some(&parent), child))
}
pub fn effective(&self, schema: &FhirSchema) -> Node {
let mut chain = vec![level(schema)];
let mut seen = BTreeSet::from([schema.url.clone()]);
let mut current = schema.base.clone();
while let Some(base) = current {
if !seen.insert(base.clone()) {
break;
}
let Some(base_schema) = self.schema(&base) else {
if let Some(core) = self.core.get(&schema.type_name)
&& seen.insert(core.url.clone())
{
self.missing.borrow_mut().insert(base.clone());
chain.push(level(core));
}
break;
};
chain.push(level(base_schema));
current = base_schema.base.clone();
}
let merged = chain
.into_iter()
.rev()
.reduce(|base, overlay| merge(&base, &overlay))
.unwrap_or_default();
let mut root = node(self.core, &merged, None, "");
root.min = 0;
root.max = None;
root.types = BTreeSet::from([schema.type_name.clone()]);
root
}
}
fn convert(json: &Value) -> Result<FhirSchema, String> {
let definition: StructureDefinition =
serde_json::from_value(json.clone()).map_err(|e| e.to_string())?;
octofhir_fhirschema::translate(definition, None).map_err(|e| e.to_string())
}
fn level(schema: &FhirSchema) -> FhirSchemaElement {
FhirSchemaElement {
type_name: Some(schema.type_name.clone()),
elements: schema.elements.clone(),
required: schema.required.clone(),
excluded: schema.excluded.clone(),
constraint: schema.constraint.clone(),
extensions: schema.extensions.clone(),
..Default::default()
}
}
fn merge(base: &FhirSchemaElement, overlay: &FhirSchemaElement) -> FhirSchemaElement {
let mut result = base.clone();
macro_rules! take {
($($field:ident),*) => {$(
if overlay.$field.is_some() {
result.$field = overlay.$field.clone();
}
)*};
}
take!(
type_name,
array,
min,
max,
refers,
binding,
pattern,
choices,
choice_of,
url,
must_support,
is_modifier,
short
);
if let Some(constraints) = &overlay.constraint {
let merged = result.constraint.get_or_insert_with(HashMap::new);
for (key, constraint) in constraints {
merged.insert(key.clone(), constraint.clone());
}
}
for (target, source) in [
(&mut result.required, &overlay.required),
(&mut result.excluded, &overlay.excluded),
] {
if let Some(names) = source {
let list = target.get_or_insert_with(Vec::new);
for name in names {
if !list.contains(name) {
list.push(name.clone());
}
}
}
}
if let Some(elements) = &overlay.elements {
let merged = result.elements.get_or_insert_with(HashMap::new);
for (name, element) in elements {
let next = match merged.get(name) {
Some(existing) => merge(existing, element),
None => element.clone(),
};
merged.insert(name.clone(), next);
}
}
if let Some(slicing) = &overlay.slicing {
result.slicing = Some(merge_slicing(result.slicing.as_ref(), slicing));
}
result
}
fn merge_slicing(
base: Option<&FhirSchemaSlicing>,
overlay: &FhirSchemaSlicing,
) -> FhirSchemaSlicing {
let Some(base) = base else {
return overlay.clone();
};
let mut result = base.clone();
if overlay.discriminator.is_some() {
result.discriminator = overlay.discriminator.clone();
}
if overlay.rules.is_some() {
result.rules = overlay.rules.clone();
}
if overlay.ordered.is_some() {
result.ordered = overlay.ordered;
}
if let Some(slices) = &overlay.slices {
let merged = result.slices.get_or_insert_with(HashMap::new);
for (name, slice) in slices {
let next = match merged.get(name) {
Some(existing) => FhirSchemaSliceMatch {
match_value: slice.match_value.clone().or(existing.match_value.clone()),
schema: match (&existing.schema, &slice.schema) {
(Some(a), Some(b)) => Some(merge(a, b)),
(a, b) => b.clone().or(a.clone()),
},
min: slice.min.or(existing.min),
max: slice.max.or(existing.max),
},
None => slice.clone(),
};
merged.insert(name.clone(), next);
}
}
result
}
fn node(
core: &HashMap<String, FhirSchema>,
element: &FhirSchemaElement,
parent: Option<&FhirSchemaElement>,
name: &str,
) -> Node {
let listed =
|list: Option<&Vec<String>>| list.is_some_and(|names| names.iter().any(|n| n == name));
let required = parent.is_some_and(|p| listed(p.required.as_ref()));
let excluded = parent.is_some_and(|p| listed(p.excluded.as_ref()));
let min = element.min.unwrap_or(0).max(0) as u32;
let min = if required { min.max(1) } else { min };
let max = if excluded {
Some(0)
} else if element.array == Some(true) {
element.max.map(|max| max.max(0) as u32)
} else {
Some(element.max.map_or(1, |max| max.max(0) as u32))
};
let types = match &element.choices {
Some(choices) => choices.iter().cloned().collect(),
None => element.type_name.iter().cloned().collect(),
};
let type_schema = element.type_name.as_deref().and_then(|t| core.get(t));
let mut scope = element.clone();
if let Some(type_schema) = type_schema {
for (target, source) in [
(&mut scope.required, &type_schema.required),
(&mut scope.excluded, &type_schema.excluded),
] {
for name in source.iter().flatten() {
let list = target.get_or_insert_with(Vec::new);
if !list.contains(name) {
list.push(name.clone());
}
}
}
}
let mut children: BTreeMap<String, Node> = element
.elements
.iter()
.flatten()
.map(|(child, schema)| {
let inherited = element
.type_name
.as_deref()
.and_then(|type_name| core.get(type_name))
.and_then(|type_schema| type_schema.elements.as_ref()?.get(child));
let schema = match inherited {
Some(base) => merge(base, schema),
None => schema.clone(),
};
(child.clone(), node(core, &schema, Some(&scope), child))
})
.collect();
normalize_choices(&mut children);
let (slicing, slices) = match &element.slicing {
Some(slicing) => (
Some(Slicing {
rules: slicing.rules.clone().unwrap_or_else(|| "open".to_owned()),
ordered: slicing.ordered.unwrap_or(false),
discriminators: slicing
.discriminator
.iter()
.flatten()
.map(|d| format!("{}:{}", d.type_name, d.path))
.collect(),
}),
slicing
.slices
.iter()
.flatten()
.map(|(slice_name, slice)| {
let extension = matches!(name, "extension" | "modifierExtension")
.then(|| parent.and_then(|p| p.extensions.as_ref()?.get(slice_name)))
.flatten();
(
slice_name.clone(),
slice_node(core, element, slice, extension),
)
})
.collect(),
),
None => (None, BTreeMap::new()),
};
Node {
min,
max,
types,
targets: element.refers.iter().flatten().cloned().collect(),
must_support: element.must_support.unwrap_or(false),
is_modifier: element.is_modifier.unwrap_or(false),
binding: element.binding.as_ref().map(|binding| Binding {
strength: Strength::parse(&binding.strength),
value_set: binding.value_set.clone(),
}),
constraints: element
.constraint
.iter()
.flatten()
.map(|(key, constraint)| (key.clone(), constraint_of(constraint)))
.collect(),
pattern: element
.pattern
.as_ref()
.map(|pattern| pattern.value.clone()),
slicing,
slices,
children,
extension_url: element.url.clone(),
}
}
fn normalize_choices(children: &mut BTreeMap<String, Node>) {
let choices: Vec<(String, Vec<String>)> = children
.iter()
.filter(|(_, node)| {
node.types.len() > 1 || node.types.iter().any(|t| looks_like_variant(t))
})
.map(|(name, node)| (name.clone(), node.types.iter().cloned().collect()))
.collect();
for (name, variants) in choices {
let present: Vec<String> = variants
.iter()
.filter(|variant| children.contains_key(*variant))
.cloned()
.collect();
if present.is_empty() {
continue;
}
let required: Vec<&String> = present
.iter()
.filter(|variant| children[*variant].min >= 1)
.collect();
let excluded: BTreeSet<String> = present
.iter()
.filter(|variant| children[*variant].max == Some(0))
.cloned()
.collect();
let only = (required.len() == 1).then(|| required[0].clone());
let sliced = !children[&name].slices.is_empty();
let variant_must_support = !sliced
&& only
.as_ref()
.is_some_and(|variant| children[variant].must_support);
if let Some(choice) = children.get_mut(&name) {
if let Some(only) = only {
choice.min = choice.min.max(1);
choice.types = BTreeSet::from([only]);
choice.must_support |= variant_must_support;
}
choice.types.retain(|variant| !excluded.contains(variant));
}
}
}
fn looks_like_variant(name: &str) -> bool {
let mut chars = name.chars();
chars.next().is_some_and(char::is_lowercase) && name.chars().any(char::is_uppercase)
}
pub fn is_variant(siblings: &BTreeMap<String, Node>, name: &str) -> bool {
siblings
.values()
.any(|node| node.types.contains(name) && node.types.iter().all(|t| looks_like_variant(t)))
}
fn slice_node(
core: &HashMap<String, FhirSchema>,
sliced: &FhirSchemaElement,
slice: &FhirSchemaSliceMatch,
extension: Option<&Value>,
) -> Node {
let mut base = sliced.clone();
base.slicing = None;
base.min = None;
base.max = None;
let schema = match &slice.schema {
Some(schema) => merge(&base, schema),
None => base,
};
let mut slice_node = node(core, &schema, None, "");
let number = |value: Option<&Value>| match value {
Some(Value::Number(n)) => n.as_u64().map(|n| n as u32),
Some(Value::String(s)) => s.parse().ok(),
_ => None,
};
let unbounded = |value: Option<&Value>| matches!(value, Some(Value::String(s)) if s == "*");
slice_node.min = slice
.min
.map(|min| min.max(0) as u32)
.or_else(|| number(extension.and_then(|e| e.get("min"))))
.unwrap_or(0);
slice_node.max = match slice.max {
Some(max) => Some(max.max(0) as u32),
None => match extension.and_then(|e| e.get("max")) {
max if unbounded(max) => None,
Some(max) => number(Some(max)),
None => slice
.schema
.as_ref()
.and_then(|s| s.max)
.map(|max| max.max(0) as u32),
},
};
slice_node
}
fn constraint_of(constraint: &FhirSchemaConstraint) -> Constraint {
Constraint {
severity: constraint.severity.clone(),
expression: constraint.expression.clone(),
human: constraint.human.clone(),
}
}