use std::fmt;
use std::sync::Arc;
use fhir_types::codec::Value;
use fhir_types::schema::{FieldSchema, Kind};
use super::{Collection, Item};
use crate::eval::error::EvalError;
use crate::model::{
FhirType, Model, TypeKind, choice_element, choice_types, defines, field_type, resource_type,
variant_type,
};
#[derive(Clone)]
pub(crate) enum Doc<'a> {
Borrowed(&'a Value),
Owned(Arc<Value>),
}
impl PartialEq for Doc<'_> {
fn eq(&self, other: &Self) -> bool {
self.get() == other.get()
}
}
impl Doc<'_> {
pub(crate) fn get(&self) -> &Value {
match self {
Self::Borrowed(value) => value,
Self::Owned(value) => value,
}
}
fn entries(&self, key: &str) -> Vec<Option<Self>> {
let each = |member: &Value| match member {
Value::Array(items) => items.len(),
_ => 1,
};
let Some(count) = self.get().get(key).map(each) else {
return Vec::new();
};
(0..count).map(|index| self.entry(key, index)).collect()
}
fn entry(&self, key: &str, index: usize) -> Option<Self> {
match self {
Self::Borrowed(value) => pick(value, key, index).map(Self::Borrowed),
Self::Owned(value) => {
pick(value, key, index).map(|item| Self::Owned(Arc::new(item.clone())))
}
}
}
}
fn pick<'v>(value: &'v Value, key: &str, index: usize) -> Option<&'v Value> {
let item = match value.get(key)? {
Value::Array(items) => items.get(index)?,
other => other,
};
(!item.is_null()).then_some(item)
}
#[derive(Clone, PartialEq)]
enum Data<'a> {
Complex(Doc<'a>),
Primitive {
value: Option<Doc<'a>>,
element: Option<Doc<'a>>,
},
}
#[derive(Clone, PartialEq)]
pub struct Node<'a> {
data: Data<'a>,
ty: Option<FhirType>,
definition: Option<&'static str>,
}
impl fmt::Debug for Node<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let present = match &self.data {
Data::Complex(_) => "complex",
Data::Primitive { value: Some(_), .. } => "primitive with a value",
Data::Primitive { value: None, .. } => "primitive without a value",
};
f.debug_struct("Node")
.field("type", &self.ty.map(|ty| ty.name))
.field("definition", &self.definition)
.field("holds", &present)
.finish()
}
}
impl fmt::Display for Node<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.ty {
Some(ty) => write!(f, "FHIR.{}", ty.name),
None => f.write_str("FHIR.(untyped)"),
}
}
}
impl<'a> Node<'a> {
pub(crate) fn resource(model: &dyn Model, doc: Doc<'a>) -> Self {
let ty = doc
.get()
.get("resourceType")
.and_then(Value::as_str)
.and_then(|name| resource_type(model, name));
Self {
definition: ty.map(|ty| ty.name),
data: Data::Complex(doc),
ty,
}
}
pub(crate) fn typed(ty: FhirType, value: Doc<'a>, element: Option<Doc<'a>>) -> Self {
let data = if ty.is_primitive() {
Data::Primitive {
value: (!value.get().is_null()).then_some(value),
element,
}
} else {
Data::Complex(value)
};
Self {
data,
ty: Some(ty),
definition: None,
}
}
pub(crate) const fn with_definition(mut self, path: &'static str) -> Self {
self.definition = Some(path);
self
}
#[must_use]
pub const fn fhir_type(&self) -> Option<&FhirType> {
self.ty.as_ref()
}
#[must_use]
pub const fn definition(&self) -> Option<&'static str> {
self.definition
}
#[must_use]
pub fn json(&self) -> Option<&Value> {
match &self.data {
Data::Complex(doc) => Some(doc.get()),
Data::Primitive { value, .. } => value.as_ref().map(Doc::get),
}
}
#[must_use]
pub fn primitive_element(&self) -> Option<&Value> {
match &self.data {
Data::Complex(_) => None,
Data::Primitive { element, .. } => element.as_ref().map(Doc::get),
}
}
#[must_use]
pub fn is_resource(&self) -> bool {
matches!(
self.ty,
Some(FhirType {
kind: TypeKind::Resource(_),
..
})
) || (self.ty.is_none()
&& self
.json()
.and_then(|json| json.get("resourceType"))
.is_some())
}
fn object(&self) -> Option<&Doc<'a>> {
match &self.data {
Data::Complex(doc) => Some(doc),
Data::Primitive { element, .. } => element.as_ref(),
}
}
pub fn member(
&self,
model: &dyn Model,
name: &str,
) -> Result<Option<Collection<'a>>, EvalError> {
let Some(ty) = self.ty else {
return Ok(Some(self.untyped_member(model, name)));
};
let schema = match ty.kind {
TypeKind::Complex(schema) | TypeKind::Resource(schema) => schema,
TypeKind::Primitive(_) | TypeKind::Xhtml => {
match model.schemas().type_named("Element") {
Some(element) => element,
None => return Ok(None),
}
}
};
let Some(field) = schema.fields.iter().find(|field| field.name == name) else {
let admitted = self
.definition
.and_then(|path| choice_element(model, path))
.and_then(|choice| choice_types(model, choice))
.is_some_and(|types| types.iter().any(|ty| defines(model, ty, name)));
return Ok(admitted.then(Vec::new));
};
let Some(object) = self.object() else {
return Ok(Some(Vec::new()));
};
read_field(model, object, field).map(Some)
}
fn untyped_member(&self, model: &dyn Model, name: &str) -> Collection<'a> {
let Some(object) = self.object() else {
return Vec::new();
};
object
.entries(name)
.into_iter()
.flatten()
.map(|doc| untyped(model, doc))
.collect()
}
pub fn named_children(&self, model: &dyn Model) -> Result<Vec<(String, Item<'a>)>, EvalError> {
let mut children = Vec::new();
let schema = match self.ty.map(|ty| ty.kind) {
Some(TypeKind::Complex(schema) | TypeKind::Resource(schema)) => schema,
Some(TypeKind::Primitive(_) | TypeKind::Xhtml) => {
if let Some(value) = crate::eval::convert::primitive_value(self)? {
children.push((String::from("value"), value));
}
match model.schemas().type_named("Element") {
Some(element) => element,
None => return Ok(children),
}
}
None => {
if let Some(Value::Object(object)) = self.object().map(Doc::get) {
for key in object.keys().filter(|key| *key != "resourceType") {
for item in self.untyped_member(model, key) {
children.push((key.clone(), item));
}
}
}
return Ok(children);
}
};
let Some(object) = self.object() else {
return Ok(children);
};
for field in schema.fields {
for item in read_field(model, object, field)? {
children.push((String::from(field.name), item));
}
}
Ok(children)
}
}
fn read_field<'a>(
model: &dyn Model,
object: &Doc<'a>,
field: &'static FieldSchema,
) -> Result<Collection<'a>, EvalError> {
match field.kind {
Kind::Choice(variants) => {
let mut items = Vec::new();
for (suffix, kind) in variants {
let key = format!("{}{suffix}", field.name);
let ty = variant_type(model, field, suffix, *kind);
items.extend(read_key(model, object, &key, field, ty)?);
}
Ok(items)
}
Kind::Resource => Ok(object
.entries(field.name)
.into_iter()
.flatten()
.map(|doc| {
let mut node = Node::resource(model, doc);
node.definition = Some(field.path);
Item::Node(node)
})
.collect()),
kind => read_key(
model,
object,
field.name,
field,
field_type(model, field, kind),
),
}
}
fn read_key<'a>(
model: &dyn Model,
object: &Doc<'a>,
key: &str,
field: &'static FieldSchema,
ty: Option<FhirType>,
) -> Result<Collection<'a>, EvalError> {
let Some(ty) = ty else {
return Ok(object
.entries(key)
.into_iter()
.flatten()
.map(|doc| untyped(model, doc))
.collect());
};
let node = |data| {
Item::Node(Node {
data,
ty: Some(ty),
definition: Some(field.path),
})
};
if ty.is_primitive() {
let values = object.entries(key);
let elements = object.entries(&format!("_{key}"));
let count = values.len().max(elements.len());
let mut items = Vec::with_capacity(count);
for index in 0..count {
let value = values.get(index).cloned().flatten();
let element = elements.get(index).cloned().flatten();
if let Some(value) = &value
&& matches!(value.get(), Value::Object(_) | Value::Array(_))
{
return Err(EvalError::Document {
element: String::from(field.path),
reason: String::from("a primitive holds an object or an array"),
});
}
if value.is_some() || element.is_some() {
items.push(node(Data::Primitive { value, element }));
}
}
return Ok(items);
}
let mut items = Vec::new();
for doc in object.entries(key).into_iter().flatten() {
if !matches!(doc.get(), Value::Object(_)) {
return Err(EvalError::Document {
element: String::from(field.path),
reason: format!("the element holds no object for its type {}", ty.name),
});
}
items.push(node(Data::Complex(doc)));
}
Ok(items)
}
fn untyped<'a>(model: &dyn Model, doc: Doc<'a>) -> Item<'a> {
match doc.get() {
Value::Bool(flag) => Item::Boolean(*flag),
Value::String(text) => Item::String(text.clone()),
Value::Number(number) => number
.as_i64()
.map(Item::Integer)
.or_else(|| number.as_str().parse().ok().map(Item::Decimal))
.unwrap_or_else(|| Item::String(String::from(number.as_str()))),
Value::Object(object) if object.contains_key("resourceType") => {
Item::Node(Node::resource(model, doc))
}
_ => Item::Node(Node {
data: Data::Complex(doc),
ty: None,
definition: None,
}),
}
}