use fhir_types::codec::Object;
use fhir_types::codec::Value;
use fhir_types::r4::schema::SCHEMAS;
use fhir_types::schema::Kind;
use fhir_types::schema::TypeSchema;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Step {
Key(String),
Index(usize),
}
#[derive(Debug, Clone)]
pub(crate) enum Coded<'d> {
Code(&'d str),
Coding(&'d Object),
Concept(&'d Object),
}
#[derive(Debug, Clone)]
pub(crate) struct Occurrence<'d> {
pub(crate) location: String,
pub(crate) steps: Vec<Step>,
pub(crate) element: &'static str,
pub(crate) value: Coded<'d>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{location} is an element the R4 element table does not describe")]
pub(crate) struct Unwalkable {
pub(crate) location: String,
}
pub(crate) fn coded<'d>(
resource: &'d Object,
location: &str,
nested: bool,
) -> Result<Vec<Occurrence<'d>>, Unwalkable> {
let mut walk = Walk {
nested,
found: Vec::new(),
};
walk.resource(resource, location, &[])?;
Ok(walk.found)
}
struct Walk<'d> {
nested: bool,
found: Vec<Occurrence<'d>>,
}
impl<'d> Walk<'d> {
fn resource(
&mut self,
resource: &'d Object,
location: &str,
steps: &[Step],
) -> Result<(), Unwalkable> {
let kind = resource
.get("resourceType")
.and_then(Value::as_str)
.ok_or_else(|| unwalkable(location))?;
let schema = SCHEMAS
.type_named(kind)
.filter(|schema| schema.path == kind)
.ok_or_else(|| unwalkable(location))?;
self.object(schema, resource, location, steps, true)
}
fn object(
&mut self,
schema: &TypeSchema,
node: &'d Object,
location: &str,
steps: &[Step],
is_resource: bool,
) -> Result<(), Unwalkable> {
for (key, value) in node {
if is_resource && key == "resourceType" {
continue;
}
let at = format!("{location}.{key}");
let mut here = steps.to_vec();
here.push(Step::Key(key.clone()));
if let Some(primitive) = key.strip_prefix('_') {
field(schema, primitive).ok_or_else(|| unwalkable(&at))?;
let element = SCHEMAS
.type_named("Element")
.ok_or_else(|| unwalkable(&at))?;
for (place, path, item) in items(&at, &here, value) {
if let Some(object) = item.as_object() {
self.object(element, object, &place, &path, false)?;
}
}
continue;
}
let (kind, element, type_code) = field(schema, key).ok_or_else(|| unwalkable(&at))?;
for (place, path, item) in items(&at, &here, value) {
match kind {
Kind::Complex(name) => {
let object = item.as_object().ok_or_else(|| unwalkable(&place))?;
let coded = match name {
"Coding" => Some(Coded::Coding(object)),
"CodeableConcept" => Some(Coded::Concept(object)),
_ => None,
};
if let Some(value) = coded {
self.found.push(Occurrence {
location: place.clone(),
steps: path.clone(),
element,
value,
});
}
let inner = SCHEMAS.type_named(name).ok_or_else(|| unwalkable(&place))?;
self.object(inner, object, &place, &path, false)?;
}
Kind::Resource if self.nested => {
let object = item.as_object().ok_or_else(|| unwalkable(&place))?;
self.resource(object, &place, &path)?;
}
Kind::Primitive(_) if type_code == "code" => {
if let Some(code) = item.as_str() {
self.found.push(Occurrence {
location: place,
steps: path,
element,
value: Coded::Code(code),
});
}
}
Kind::Resource | Kind::Primitive(_) | Kind::Attribute | Kind::Xhtml => {}
Kind::Choice(_) => return Err(unwalkable(&place)),
}
}
}
Ok(())
}
}
fn field(schema: &TypeSchema, key: &str) -> Option<(Kind, &'static str, String)> {
schema.fields.iter().find_map(|field| {
if field.name == key {
return match field.kind {
Kind::Choice(_) => None,
kind => Some((
kind,
field.path,
field.types.first().copied().unwrap_or_default().to_owned(),
)),
};
}
let Kind::Choice(variants) = field.kind else {
return None;
};
let suffix = key.strip_prefix(field.name)?;
variants
.iter()
.find(|(candidate, _)| *candidate == suffix)
.map(|(candidate, kind)| (*kind, field.path, uncapitalized(candidate)))
})
}
fn uncapitalized(name: &str) -> String {
let mut characters = name.chars();
characters.next().map_or_else(String::new, |first| {
first.to_lowercase().chain(characters).collect()
})
}
fn items<'v>(at: &str, steps: &[Step], value: &'v Value) -> Vec<(String, Vec<Step>, &'v Value)> {
match value {
Value::Array(items) => items
.iter()
.enumerate()
.map(|(index, item)| {
let mut path = steps.to_vec();
path.push(Step::Index(index));
(format!("{at}[{index}]"), path, item)
})
.collect(),
other => vec![(at.to_owned(), steps.to_vec(), other)],
}
}
fn unwalkable(location: &str) -> Unwalkable {
Unwalkable {
location: location.to_owned(),
}
}