pub mod r4;
pub mod stu3;
use std::collections::BTreeMap;
use std::fmt;
use fhir_types::schema::{FieldSchema, Kind, Schemas, TypeSchema, ValueKind};
pub trait Model: fmt::Debug + Send + Sync {
fn schemas(&self) -> &'static Schemas;
fn primitives(&self) -> &BTreeMap<&'static str, TypeKind>;
fn base(&self, name: &str) -> Option<&'static str> {
self.schemas().base_of(name)
}
fn primitive_base(&self, name: &str) -> Option<&'static str> {
self.schemas().primitive_base_of(name)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TypeKind {
Primitive(ValueKind),
Xhtml,
Complex(&'static TypeSchema),
Resource(&'static TypeSchema),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FhirType {
pub name: &'static str,
pub kind: TypeKind,
}
impl FhirType {
#[must_use]
pub const fn schema(&self) -> Option<&'static TypeSchema> {
match self.kind {
TypeKind::Complex(schema) | TypeKind::Resource(schema) => Some(schema),
TypeKind::Primitive(_) | TypeKind::Xhtml => None,
}
}
#[must_use]
pub fn is_backbone(&self) -> bool {
matches!(self.kind, TypeKind::Complex(schema) if schema.path != schema.name)
}
#[must_use]
pub const fn is_primitive(&self) -> bool {
matches!(self.kind, TypeKind::Primitive(_) | TypeKind::Xhtml)
}
}
#[must_use]
pub fn resource_type(model: &dyn Model, name: &str) -> Option<FhirType> {
let schemas = model.schemas();
let index = schemas.resources.binary_search(&name).ok()?;
let name = *schemas.resources.get(index)?;
let schema = schemas
.type_named(name)
.filter(|schema| schema.path == name)?;
Some(FhirType {
name,
kind: TypeKind::Resource(schema),
})
}
#[must_use]
pub fn named_type(model: &dyn Model, name: &str) -> Option<FhirType> {
if let Some(found) = resource_type(model, name) {
return Some(found);
}
let schemas = model.schemas();
if let Some(schema) = schemas
.type_named(name)
.filter(|schema| schema.path == schema.name)
{
let kind = if schemas.specializes(schema.name, "Resource") {
TypeKind::Resource(schema)
} else {
TypeKind::Complex(schema)
};
return Some(FhirType {
name: schema.name,
kind,
});
}
let (name, kind) = model.primitives().get_key_value(name)?;
Some(FhirType { name, kind: *kind })
}
#[must_use]
pub fn field_type(model: &dyn Model, field: &'static FieldSchema, kind: Kind) -> Option<FhirType> {
match kind {
Kind::Primitive(value) => Some(FhirType {
name: field.types.first().copied().unwrap_or("string"),
kind: TypeKind::Primitive(value),
}),
Kind::Attribute => Some(FhirType {
name: field.types.first().copied().unwrap_or("string"),
kind: TypeKind::Primitive(ValueKind::Text),
}),
Kind::Xhtml => Some(FhirType {
name: "xhtml",
kind: TypeKind::Xhtml,
}),
Kind::Complex(name) => model.schemas().type_named(name).map(|schema| FhirType {
name,
kind: TypeKind::Complex(schema),
}),
Kind::Resource | Kind::Choice(_) => None,
}
}
#[must_use]
pub fn choice_code(field: &'static FieldSchema, suffix: &str) -> Option<&'static str> {
field.types.iter().copied().find(|code| {
let mut chars = code.chars();
chars.next().is_some_and(|first| {
suffix.starts_with(first.to_ascii_uppercase())
&& suffix.get(first.len_utf8()..) == Some(chars.as_str())
})
})
}
#[must_use]
pub fn primitive_types(schemas: &'static Schemas) -> BTreeMap<&'static str, TypeKind> {
let mut types = BTreeMap::from([("xhtml", TypeKind::Xhtml)]);
let scalar = |kind: &Kind| match kind {
Kind::Primitive(value) => Some(*value),
Kind::Attribute => Some(ValueKind::Text),
_ => None,
};
for field in schemas.types.iter().flat_map(|schema| schema.fields) {
if let Some(value) = scalar(&field.kind)
&& let Some(code) = field.types.first()
{
types.entry(*code).or_insert(TypeKind::Primitive(value));
}
if let Kind::Choice(variants) = field.kind {
for (suffix, kind) in variants {
if let Some(value) = scalar(kind)
&& let Some(code) = choice_code(field, suffix)
{
types.entry(code).or_insert(TypeKind::Primitive(value));
}
}
}
}
types
}
#[must_use]
pub fn path_type(model: &dyn Model, path: &str) -> Option<FhirType> {
if !path.contains('.') {
return named_type(model, path);
}
let (_, field) = model.schemas().element(path)?;
field_type(model, field, field.kind)
}
#[must_use]
pub fn variant_type(
model: &dyn Model,
field: &'static FieldSchema,
suffix: &str,
kind: Kind,
) -> Option<FhirType> {
let found = field_type(model, field, kind)?;
Some(match (kind, choice_code(field, suffix)) {
(Kind::Complex(_), _) | (_, None) => found,
(_, Some(code)) => FhirType {
name: code,
..found
},
})
}
#[must_use]
pub fn choice_types(model: &dyn Model, field: &'static FieldSchema) -> Option<Vec<FhirType>> {
let Kind::Choice(variants) = field.kind else {
return None;
};
variants
.iter()
.map(|(suffix, kind)| variant_type(model, field, suffix, *kind))
.collect()
}
#[must_use]
pub fn path_types(model: &dyn Model, path: &str) -> Option<Vec<FhirType>> {
if let Some(field) = choice_element(model, path) {
return choice_types(model, field);
}
if let Some(ty) = path_type(model, path) {
return Some(vec![ty]);
}
walk(model, path)
}
fn walk(model: &dyn Model, path: &str) -> Option<Vec<FhirType>> {
let schemas = model.schemas();
let mut segments = path.split('.');
let mut reach = vec![named_type(model, segments.next()?)?];
for segment in segments {
let mut next: Vec<FhirType> = Vec::new();
for ty in &reach {
let owner = if ty.is_primitive() {
"Element"
} else {
ty.schema()?.path
};
let Some((_, field)) = schemas.element(&format!("{owner}.{segment}")) else {
continue;
};
let types = match field.kind {
Kind::Choice(_) => choice_types(model, field)?,
kind => vec![field_type(model, field, kind)?],
};
for found in types {
if !next.contains(&found) {
next.push(found);
}
}
}
if next.is_empty() {
return None;
}
reach = next;
}
Some(reach)
}
#[must_use]
pub fn choice_element(model: &dyn Model, path: &str) -> Option<&'static FieldSchema> {
if !path.contains('.') {
return None;
}
let (_, field) = model.schemas().element(path)?;
matches!(field.kind, Kind::Choice(_)).then_some(field)
}
#[must_use]
pub fn defines(model: &dyn Model, ty: &FhirType, name: &str) -> bool {
ty.schema()
.or_else(|| model.schemas().type_named("Element"))
.is_some_and(|schema| schema.fields.iter().any(|field| field.name == name))
}
#[must_use]
pub fn is_a(model: &dyn Model, ty: &FhirType, name: &str) -> bool {
let mut current = Some(ty.name);
let mut hops = 0_u8;
while let Some(candidate) = current {
if candidate == name {
return true;
}
hops = hops.saturating_add(1);
if hops > 16 {
return false;
}
current = model.base(candidate);
}
false
}
#[must_use]
pub fn specializes(model: &dyn Model, ty: &FhirType, name: &str) -> bool {
let mut current = Some(ty.name);
let mut hops = 0_u8;
while let Some(candidate) = current {
if candidate == name {
return true;
}
hops = hops.saturating_add(1);
if hops > 16 {
return false;
}
current = if model.primitives().contains_key(candidate) {
model.primitive_base(candidate)
} else {
model.base(candidate)
};
}
false
}