use helios_fhirpath_support::{EvaluationError, EvaluationResult};
use std::collections::HashMap;
pub fn access_polymorphic_element(
obj: &HashMap<String, EvaluationResult>,
field_name: &str,
) -> Option<EvaluationResult> {
if let Some(value) = obj.get(field_name) {
return Some(value.clone());
}
if field_name.contains('.') {
let parts: Vec<&str> = field_name.split('.').collect();
let first_part = parts[0];
let rest = &parts[1..].join(".");
if is_choice_element(first_part) {
let matches = get_polymorphic_fields(obj, first_part);
for (_, value) in &matches {
if let EvaluationResult::Object {
map: inner_obj,
type_info: _,
} = value
{
if let Some(result) = access_polymorphic_element(inner_obj, rest) {
return Some(result);
}
}
}
for (key, value) in obj.iter() {
if key.starts_with(first_part) && key.len() > first_part.len() {
if let Some(c) = key.chars().nth(first_part.len()) {
if c.is_uppercase() {
if let EvaluationResult::Object {
map: inner_obj,
type_info: _,
} = value
{
if let Some(result) = access_polymorphic_element(inner_obj, rest) {
return Some(result);
}
}
}
}
}
}
} else {
if let Some(value) = obj.get(first_part) {
if let EvaluationResult::Object {
map: inner_obj,
type_info: _,
} = value
{
return access_polymorphic_element(inner_obj, rest);
}
}
}
return None;
}
let matching_fields = get_polymorphic_fields(obj, field_name);
if !matching_fields.is_empty() {
if matching_fields.len() == 1 {
return Some(matching_fields[0].1.clone());
}
return Some(matching_fields[0].1.clone());
}
None
}
fn get_polymorphic_fields(
obj: &HashMap<String, EvaluationResult>,
base_name: &str,
) -> Vec<(String, EvaluationResult)> {
let mut matches = Vec::new();
if let Some(value) = obj.get(base_name) {
matches.push((base_name.to_string(), value.clone()));
}
let mut consulted_field_types = false;
if let Some(EvaluationResult::String(resource_type, _, _)) = obj.get("resourceType")
&& let Some(table) = helios_fhir::field_types(helios_fhir::FhirVersion::default_enabled())
{
consulted_field_types = true;
for (parent, field, _ty, _is_collection) in table {
if parent != resource_type {
continue;
}
let Some(suffix) = field.strip_prefix(base_name) else {
continue;
};
if !suffix
.chars()
.next()
.is_some_and(|c| c.is_ascii_uppercase())
{
continue;
}
if matches.iter().any(|(n, _)| n == field) {
continue;
}
if let Some(value) = obj.get(*field) {
let converted = convert_fhir_field_to_fhirpath_type(value, suffix);
matches.push(((*field).to_string(), converted));
}
}
}
if !consulted_field_types {
for (field_name, value) in obj {
if matches.iter().any(|(name, _)| name == field_name) {
continue;
}
if field_name.starts_with(base_name) && field_name.len() > base_name.len() {
if let Some(c) = field_name.chars().nth(base_name.len()) {
if c.is_uppercase() {
let type_suffix = &field_name[base_name.len()..];
let converted_value =
convert_fhir_field_to_fhirpath_type(value, type_suffix);
matches.push((field_name.clone(), converted_value));
}
}
}
}
}
if base_name == "value"
&& matches.len() > 1
&& obj.get("resourceType") == Some(&EvaluationResult::string("Observation".to_string()))
&& let Some(idx) = matches.iter().position(|(name, _)| name == "valueQuantity")
{
let item = matches.remove(idx);
matches.insert(0, item);
}
matches
}
fn convert_fhir_field_to_fhirpath_type(value: &EvaluationResult, suffix: &str) -> EvaluationResult {
match value {
EvaluationResult::String(s, _, _) => {
match suffix {
"DateTime" => {
EvaluationResult::datetime(s.clone())
}
"Date" => {
EvaluationResult::date(s.clone())
}
"Time" => {
EvaluationResult::time(s.clone())
}
"Instant" => {
EvaluationResult::DateTime(
s.clone(),
Some(helios_fhirpath_support::TypeInfoResult::new(
"FHIR", "instant",
)),
None,
)
}
"Code" => {
EvaluationResult::fhir_string(s.clone(), "code")
}
"Id" => {
EvaluationResult::fhir_string(s.clone(), "id")
}
"Uri" => {
EvaluationResult::fhir_string(s.clone(), "uri")
}
"Url" => {
EvaluationResult::fhir_string(s.clone(), "url")
}
"Uuid" => {
EvaluationResult::fhir_string(s.clone(), "uuid")
}
"Canonical" => {
EvaluationResult::fhir_string(s.clone(), "canonical")
}
"Oid" => {
EvaluationResult::fhir_string(s.clone(), "oid")
}
"Markdown" => {
EvaluationResult::fhir_string(s.clone(), "markdown")
}
"Base64Binary" => {
EvaluationResult::fhir_string(s.clone(), "base64Binary")
}
_ => {
value.clone()
}
}
}
_ => {
value.clone()
}
}
}
pub fn is_choice_element_with_context(field_name: &str, context_metadata: Option<&[&str]>) -> bool {
if field_name.contains("[x]") {
return true;
}
if let Some(choice_elements) = context_metadata {
if choice_elements.contains(&field_name) {
return true;
}
for base_name in choice_elements {
if field_name.starts_with(base_name) && field_name.len() > base_name.len() {
if let Some(c) = field_name.chars().nth(base_name.len()) {
if c.is_uppercase() {
return true;
}
}
}
}
return false;
}
is_polymorphic_base_in_default_version(field_name)
}
pub fn is_choice_element(field_name: &str) -> bool {
is_choice_element_with_context(field_name, None)
}
fn is_polymorphic_base_in_default_version(name: &str) -> bool {
let table: &[(&str, &str, &str, bool)] = match helios_fhir::FhirVersion::default_enabled() {
#[cfg(feature = "R4")]
helios_fhir::FhirVersion::R4 => helios_fhir::r4::FIELD_TYPES,
#[cfg(feature = "R4B")]
helios_fhir::FhirVersion::R4B => helios_fhir::r4b::FIELD_TYPES,
#[cfg(feature = "R5")]
helios_fhir::FhirVersion::R5 => helios_fhir::r5::FIELD_TYPES,
#[cfg(feature = "R6")]
helios_fhir::FhirVersion::R6 => helios_fhir::r6::FIELD_TYPES,
#[allow(unreachable_patterns)]
_ => return false,
};
table.iter().any(|(_, f, _, _)| {
f.strip_prefix(name)
.and_then(|rest| rest.chars().next())
.is_some_and(|c| c.is_ascii_uppercase())
})
}
pub fn apply_polymorphic_type_operation(
value: &EvaluationResult,
op: &str,
type_name: &str,
_namespace: Option<&str>,
) -> Result<EvaluationResult, EvaluationError> {
if let EvaluationResult::Empty = value {
if op == "is" && type_name == "Empty" {
return Ok(EvaluationResult::boolean(true));
} else if op == "is" {
return Ok(EvaluationResult::boolean(false));
} else if op == "as" {
return Ok(EvaluationResult::Empty);
}
return Ok(EvaluationResult::Empty);
}
if let EvaluationResult::Collection { items, .. } = value {
if items.len() != 1 {
return Ok(EvaluationResult::Empty);
}
return apply_polymorphic_type_operation(&items[0], op, type_name, _namespace);
}
if op == "is" || op == "as" {
if let EvaluationResult::Object {
map: obj,
type_info: _,
} = value
{
if type_name == "Quantity" || type_name == "quantity" {
if obj.contains_key("value")
&& (obj.contains_key("unit") || obj.contains_key("code"))
{
return if op == "is" {
Ok(EvaluationResult::boolean(true))
} else {
Ok(value.clone())
};
}
if obj.contains_key("valueQuantity") {
return if op == "is" {
Ok(EvaluationResult::boolean(true))
} else {
if let Some(quantity) = obj.get("valueQuantity") {
Ok(quantity.clone())
} else {
Ok(EvaluationResult::Empty)
}
};
}
if let Some(EvaluationResult::String(resource_type, _, _)) = obj.get("resourceType")
{
if resource_type == "Observation" && obj.contains_key("valueQuantity") {
return if op == "is" {
Ok(EvaluationResult::boolean(true))
} else {
if let Some(quantity) = obj.get("valueQuantity") {
Ok(quantity.clone())
} else {
Ok(EvaluationResult::Empty)
}
};
}
}
}
if let Some(EvaluationResult::String(resource_type, _, _)) = obj.get("resourceType") {
if resource_type.to_lowercase() == type_name.to_lowercase() {
return if op == "is" {
Ok(EvaluationResult::boolean(true))
} else {
Ok(value.clone())
};
}
if type_name.to_lowercase() == "domainresource"
&& crate::resource_type::is_fhir_domain_resource(resource_type)
{
return if op == "is" {
Ok(EvaluationResult::boolean(true))
} else {
Ok(value.clone())
};
}
if type_name.to_lowercase() == "resource" {
return if op == "is" {
Ok(EvaluationResult::boolean(true))
} else {
Ok(value.clone())
};
}
}
}
match op {
"is" => {
if let Some(ns) = _namespace {
let type_spec = crate::parser::TypeSpecifier::QualifiedIdentifier(
ns.to_string(),
Some(type_name.to_string()),
);
let context = crate::EvaluationContext::new_empty_with_default_version();
if let Ok(result) =
crate::resource_type::is_of_type_with_context(value, &type_spec, &context)
{
return Ok(EvaluationResult::boolean(result));
}
} else {
let type_spec = crate::parser::TypeSpecifier::QualifiedIdentifier(
type_name.to_string(),
None,
);
let context = crate::EvaluationContext::new_empty_with_default_version();
if let Ok(result) =
crate::resource_type::is_of_type_with_context(value, &type_spec, &context)
{
return Ok(EvaluationResult::boolean(result));
}
}
match value {
EvaluationResult::Object {
map: obj,
type_info: _,
} => {
if let Some(EvaluationResult::String(resource_type, _, _)) =
obj.get("resourceType")
{
if resource_type.to_lowercase() == type_name.to_lowercase() {
return Ok(EvaluationResult::boolean(true));
}
if type_name.to_lowercase() == "domainresource"
&& crate::resource_type::is_fhir_domain_resource(resource_type)
{
return Ok(EvaluationResult::boolean(true));
}
if type_name.to_lowercase() == "resource" {
return Ok(EvaluationResult::boolean(true));
}
}
if type_name.to_lowercase() == "boolean" {
for key in obj.keys() {
if key == "resourceType" {
continue;
}
if key.to_lowercase().contains("active")
|| key.to_lowercase().contains("flag")
|| key.to_lowercase().contains("enabled")
|| key.to_lowercase().contains("status")
|| key.to_lowercase().contains("is")
{
return Ok(EvaluationResult::boolean(true));
}
}
for (key, value) in obj.iter() {
if key != "resourceType"
&& matches!(value, EvaluationResult::Boolean(_, _, _))
{
return Ok(EvaluationResult::boolean(true));
}
}
if obj.len() < 5 && !obj.contains_key("resourceType") {
if obj.contains_key("id") || obj.contains_key("extension") {
return Ok(EvaluationResult::boolean(true));
}
if obj.keys().len() <= 2 {
return Ok(EvaluationResult::boolean(true));
}
}
}
if type_name.to_lowercase() == "date" || type_name == "Date" {
for (key, val) in obj.iter() {
if key == "resourceType" {
continue;
}
match val {
EvaluationResult::Date(_, None, None) => {
return Ok(EvaluationResult::boolean(true));
}
EvaluationResult::String(s, _, _) => {
if s.len() >= 10
&& s.chars().nth(4) == Some('-')
&& s.chars().nth(7) == Some('-')
{
return Ok(EvaluationResult::boolean(true));
}
}
_ => {}
}
if key.to_lowercase().contains("date")
|| key.to_lowercase().contains("time")
|| key.to_lowercase().contains("birth")
{
return Ok(EvaluationResult::boolean(true));
}
}
}
for key in obj.keys() {
if key.ends_with(type_name) && key.len() > type_name.len() {
let base_name = &key[0..(key.len() - type_name.len())];
if is_choice_element(base_name) {
return Ok(EvaluationResult::boolean(true));
}
}
}
if obj.contains_key("value") && type_name == "Quantity" {
if let Some(EvaluationResult::Object { map: value_obj, .. }) =
obj.get("value")
{
if value_obj.contains_key("value") && value_obj.contains_key("unit")
{
return Ok(EvaluationResult::boolean(true));
}
}
if obj.contains_key("valueQuantity") {
return Ok(EvaluationResult::boolean(true));
}
}
if let Some(EvaluationResult::String(value_type, _, _)) = obj.get("type") {
if value_type == type_name {
return Ok(EvaluationResult::boolean(true));
}
}
Ok(EvaluationResult::boolean(false))
}
EvaluationResult::Boolean(_, _, _) => {
let is_boolean_type = type_name == "Boolean"
|| type_name == "boolean"
|| type_name.ends_with(".Boolean")
|| type_name.ends_with(".boolean");
Ok(EvaluationResult::boolean(is_boolean_type))
}
EvaluationResult::Integer(_, _, _) => {
let is_integer_type = type_name == "Integer"
|| type_name == "integer"
|| type_name.ends_with(".Integer")
|| type_name.ends_with(".integer");
Ok(EvaluationResult::boolean(is_integer_type))
}
EvaluationResult::Decimal(_, _, _) => {
let is_decimal_type = type_name == "Decimal"
|| type_name == "decimal"
|| type_name.ends_with(".Decimal")
|| type_name.ends_with(".decimal");
Ok(EvaluationResult::boolean(is_decimal_type))
}
EvaluationResult::String(_, _, _) => {
let is_string_type = type_name == "String"
|| type_name == "string"
|| type_name.ends_with(".String")
|| type_name.ends_with(".string");
Ok(EvaluationResult::boolean(is_string_type))
}
EvaluationResult::Date(_, _, _) => {
let is_date_type = type_name == "Date"
|| type_name == "date"
|| type_name.ends_with(".Date")
|| type_name.ends_with(".date");
Ok(EvaluationResult::boolean(is_date_type))
}
EvaluationResult::DateTime(_, _, _) => {
let is_datetime_type = type_name == "DateTime"
|| type_name == "dateTime"
|| type_name.ends_with(".DateTime")
|| type_name.ends_with(".dateTime");
Ok(EvaluationResult::boolean(is_datetime_type))
}
EvaluationResult::Time(_, _, _) => {
let is_time_type = type_name == "Time"
|| type_name == "time"
|| type_name.ends_with(".Time")
|| type_name.ends_with(".time");
Ok(EvaluationResult::boolean(is_time_type))
}
EvaluationResult::Quantity(_, _, _, _) => {
let is_quantity_type =
type_name == "Quantity" || type_name.ends_with(".Quantity");
Ok(EvaluationResult::boolean(is_quantity_type))
}
EvaluationResult::Empty => Ok(EvaluationResult::boolean(false)),
EvaluationResult::Collection { .. } => Ok(EvaluationResult::boolean(false)),
#[cfg(not(any(feature = "R4", feature = "R4B")))]
EvaluationResult::Integer64(_, _, _) => {
let is_integer64_type = type_name == "Integer64"
|| type_name == "integer64"
|| type_name.ends_with(".Integer64")
|| type_name.ends_with(".integer64");
Ok(EvaluationResult::boolean(is_integer64_type))
}
#[cfg(any(feature = "R4", feature = "R4B"))]
EvaluationResult::Integer64(_, _, _) => {
let is_integer_type = type_name == "Integer"
|| type_name == "integer"
|| type_name.ends_with(".Integer")
|| type_name.ends_with(".integer");
Ok(EvaluationResult::boolean(is_integer_type))
}
}
}
"as" => {
let is_type_result =
apply_polymorphic_type_operation(value, "is", type_name, _namespace)?;
match is_type_result {
EvaluationResult::Boolean(true, _, _) => Ok(value.clone()),
EvaluationResult::Boolean(false, _, _) => Ok(EvaluationResult::Empty),
EvaluationResult::Empty => Ok(EvaluationResult::Empty), _ => Err(EvaluationError::TypeError(format!(
"'is' operation returned non-Boolean: {:?}",
is_type_result
))),
}
}
_ => Err(EvaluationError::TypeError(format!(
"Unsupported polymorphic type operation: {}",
op
))),
}
} else {
Err(EvaluationError::TypeError(format!(
"Unsupported polymorphic type operation: {}",
op
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_observation_with_quantity() -> HashMap<String, EvaluationResult> {
let mut obs = HashMap::new();
obs.insert(
"resourceType".to_string(),
EvaluationResult::string("Observation".to_string()),
);
obs.insert(
"id".to_string(),
EvaluationResult::string("123".to_string()),
);
let mut quantity = HashMap::new();
quantity.insert(
"value".to_string(),
EvaluationResult::decimal(rust_decimal::Decimal::from(185)),
);
quantity.insert(
"unit".to_string(),
EvaluationResult::string("lbs".to_string()),
);
quantity.insert(
"system".to_string(),
EvaluationResult::string("http://unitsofmeasure.org".to_string()),
);
quantity.insert(
"code".to_string(),
EvaluationResult::string("lb_av".to_string()),
);
obs.insert(
"valueQuantity".to_string(),
EvaluationResult::Object {
map: quantity,
type_info: None,
},
);
obs
}
#[test]
fn test_access_polymorphic_element() {
let obs = create_observation_with_quantity();
let value = access_polymorphic_element(&obs, "value").unwrap();
if let EvaluationResult::Object {
map: quantity,
type_info: _,
} = &value
{
assert_eq!(
quantity.get("unit").unwrap(),
&EvaluationResult::string("lbs".to_string())
);
} else {
panic!("Expected Object result, got {:?}", value);
}
}
#[test]
fn test_is_type_operation() {
let obs = create_observation_with_quantity();
let value_quantity = obs.get("valueQuantity").unwrap().clone();
let result =
apply_polymorphic_type_operation(&value_quantity, "is", "Quantity", None).unwrap();
assert_eq!(result, EvaluationResult::boolean(true));
let result =
apply_polymorphic_type_operation(&value_quantity, "is", "String", None).unwrap();
assert_eq!(result, EvaluationResult::boolean(false));
let obj = EvaluationResult::Object {
map: obs,
type_info: None,
};
let result = apply_polymorphic_type_operation(&obj, "is", "Observation", None).unwrap();
assert_eq!(result, EvaluationResult::boolean(true));
}
#[test]
fn test_as_type_operation() {
let obs = create_observation_with_quantity();
let value_quantity = obs.get("valueQuantity").unwrap().clone();
let result =
apply_polymorphic_type_operation(&value_quantity, "is", "Quantity", None).unwrap();
assert_eq!(result, EvaluationResult::boolean(true));
let result =
apply_polymorphic_type_operation(&value_quantity, "as", "Quantity", None).unwrap();
assert_eq!(result, value_quantity);
let obj = EvaluationResult::Object {
map: obs.clone(),
type_info: None,
};
let result = apply_polymorphic_type_operation(&obj, "is", "Quantity", None).unwrap();
assert_eq!(result, EvaluationResult::boolean(true));
let result = apply_polymorphic_type_operation(&obj, "is", "NonExistentType", None).unwrap();
assert_eq!(result, EvaluationResult::boolean(false));
}
}