fhirpathrs 0.1.0

Fhirpath implementation in Rust
Documentation
use serde::{Serialize, Serializer};
use serde_json::Value;

use crate::{error::FhirpathError, models::ModelDetails, parser::expression::Expression};

use super::{
    data_types::type_info::{Namespace, TypeInfo},
    nodes::resource_node::{PathDetails, ResourceNode},
    EvaluateResult,
};

#[derive(Clone, Debug)]
enum Cardinality {
    Single,
    Multiple,
}

impl TryFrom<&str> for Cardinality {
    type Error = FhirpathError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value {
            "1" => Ok(Cardinality::Single),
            "*" => Ok(Cardinality::Multiple),
            _ => Err(FhirpathError::EvaluateError { msg: format!("unknown cardinality: {}", value) })
        }
    }
}

#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
pub enum ReflectionType {
    SimpleTypeInfo(SimpleTypeInfo),
    ListTypeInfo(ListTypeInfo),
    ClassInfo(ClassInfo),
    TupleTypeInfo(TupleTypeInfo)
}

#[derive(Clone, Debug)]
pub struct TypeSpecifier {
    namespace: Namespace,
    name: String,
    cardinality: Cardinality
}

impl Serialize for TypeSpecifier {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let formatted: String = match self.cardinality {
            Cardinality::Single => format!("{}.{}", self.namespace, self.name),
            Cardinality::Multiple => format!("List<{}.{}>", self.namespace, self.name),
        };

        serializer.serialize_str(formatted.as_str())
    }
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SimpleTypeInfo {
    namespace: Namespace,
    name: String,
    base_type: Option<TypeSpecifier>,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ListTypeInfo {
    element_type: String,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClassInfoElement {
    name: String,
    r#type: Option<TypeSpecifier>,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TupleTypeInfo {
    element: Vec<TupleTypeInfoElement>
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TupleTypeInfoElement {
    name: String,
    r#type: Option<TypeSpecifier>,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClassInfo {
    namespace: Namespace,
    name: String,
    base_type: Option<TypeSpecifier>,
    element: Vec<ClassInfoElement>
}

impl TryFrom<&Value> for SimpleTypeInfo {
    type Error = FhirpathError;

    fn try_from(value: &Value) -> Result<Self, Self::Error> {
        let type_info = TypeInfo::try_from(value)?;

        if let Some(namespace) = type_info.namespace {
            Ok(SimpleTypeInfo {
                namespace,
                name: type_info.type_name,
                base_type: Some(TypeSpecifier {
                    namespace: Namespace::System,
                    name: "Any".to_string(),
                    cardinality: Cardinality::Single
                }),
            })
        } else {
            Err(FhirpathError::EvaluateError {
                msg: "No namespace for type".to_string(),
            })
        }
    }
}

fn get_class_reflection_type(
    value: &Value,
    fhir_type: &String,
    model: &ModelDetails,
) -> Option<ReflectionType> {
    let obj = match value {
        Value::Object(obj) => obj,
        _ => return None,
    };

    let element: Vec<ClassInfoElement> = obj.keys().filter_map(|elem| {
        let elem_path = format!("{}.{}", fhir_type, elem);

        let elem_type = model.path_to_type.get(elem_path.as_str())?;
        let cardinality = model.path_cardinality.get(elem_path.as_str())?;

        Some(ClassInfoElement {
            name: elem.clone(),
            r#type: Some(TypeSpecifier {
                name: elem_type.clone(),
                namespace: Namespace::Fhir,
                cardinality: Cardinality::try_from(cardinality.as_str()).ok()?
            }),
        })
    }).collect();

    let base_type = model.type_to_parent.get(fhir_type).and_then(|bt| {
        Some(TypeSpecifier { namespace: Namespace::Fhir, name: bt.to_string(), cardinality: Cardinality::Single })
    });

    Some(ReflectionType::ClassInfo(ClassInfo {
        namespace: Namespace::Fhir,
        base_type,
        name: fhir_type.clone(),
        element,
    }))
}

fn get_tuple_reflection_type(
    value: &Value,
    path: &String,
    model: &ModelDetails,
) -> Option<ReflectionType> {
    let obj = match value {
        Value::Object(obj) => obj,
        _ => return None,
    };

    let element: Vec<TupleTypeInfoElement> = obj.keys().filter_map(|elem| {
        let elem_path = format!("{}.{}", path, elem);

        let elem_type = model.path_to_type.get(elem_path.as_str())?;
        let cardinality = model.path_cardinality.get(elem_path.as_str())?;

        Some(TupleTypeInfoElement {
            name: elem.clone(),
            r#type: Some(TypeSpecifier {
                name: elem_type.clone(),
                namespace: Namespace::Fhir,
                cardinality: Cardinality::try_from(cardinality.as_str()).ok()?
            }),
        })
    }).collect();

    Some(ReflectionType::TupleTypeInfo(TupleTypeInfo {
        element
    }))
}

pub fn get_reflection_type(
    path_details: &Option<PathDetails>,
    value: &Value,
    model: &Option<ModelDetails>,
) -> Option<ReflectionType> {
    if let (Some(pd), Some(model_details)) = (path_details, model) {
        let fhir_type = model_details.path_to_type.get(&pd.path)?;

        match fhir_type.as_str() {
            "BackboneElement" =>  get_tuple_reflection_type(
                value,
                &pd.path,
                model_details,
            ),
            _ => get_class_reflection_type(
                value,
                fhir_type,
                model_details,
            ),
        }
    } else {
        return Some(ReflectionType::SimpleTypeInfo(
            SimpleTypeInfo::try_from(value).ok()?,
        ));
    }
}

pub fn reflection_type<'a, 'b>(
    input: &'a ResourceNode<'a, 'b>,
    _expressions: &Vec<Box<Expression>>,
) -> EvaluateResult<ResourceNode<'a, 'b>> {
    let reflection_types = input.get_reflection_types();

    Ok(ResourceNode::from_node(
        input,
        serde_json::to_value(reflection_types).map_err(|err| FhirpathError::EvaluateError {
            msg: format!("Failed to Serialize TypeInfo: {}", err.to_string()),
        })?,
    ))
}

#[cfg(test)]
mod test {
    use serde_json::json;

    use crate::{
        evaluate::{
            test::test::{run_tests, TestCase},
            EvaluateOptions,
        },
        models::{get_model_details, ModelType},
    };
    use crate::evaluate::test::test::Expected;

    #[test]
    fn test_type_path() {
        let test_cases: Vec<TestCase> = vec![
            TestCase {
                path: "('John' | 'Mary').type()".to_string(),
                input: json!({}),
                expected: Expected::Value(json!([
                    { "namespace": "System", "name": "String", "baseType": "System.Any" },
                    { "namespace": "System", "name": "String", "baseType": "System.Any" }
                ])),
                options: Some(EvaluateOptions {
                    model: Some(get_model_details(ModelType::Stu3).unwrap()),
                    vars: None,
                    now: None,
                    trace_function: None,
                }),
            },
            TestCase {
                path: "Patient.address.type()".to_string(),
                input: json!({ "resourceType": "Patient", "address": [{ "text": "abc" }] }),
                expected: Expected::Value(json!([
                    {
                        "namespace": "FHIR",
                        "name": "Address",
                        "baseType": "FHIR.Element",
                        "element": [
                            {
                                "name": "text",
                                "type": "FHIR.string"
                            }
                        ]
                    }
                ])),
                options: Some(EvaluateOptions {
                    model: Some(get_model_details(ModelType::R4).unwrap()),
                    vars: None,
                    now: None,
                    trace_function: None,
                }),
            },
            TestCase {
                path: "Patient.maritalStatus.type()".to_string(),
                input: json!({ "resourceType": "Patient", "maritalStatus": { "coding": [{ "system": "system", "code": "code" }], "text": "text" } }),
                expected: Expected::Value(json!([
                    {
                        "namespace": "FHIR",
                        "name": "CodeableConcept",
                        "baseType": "FHIR.Element",
                        "element": [
                           { "name": "coding", "type": "List<FHIR.Coding>" },
                           { "name": "text", "type": "FHIR.string" }
                        ]
                    }
                ])),
                options: Some(EvaluateOptions {
                    model: Some(get_model_details(ModelType::R4).unwrap()),
                    vars: None,
                    now: None,
                    trace_function: None,
                }),
            },
            TestCase {
                path: "Patient.contact.single().type()".to_string(),
                input: json!({ "resourceType": "Patient", "contact": [{ "gender": "male" }]}),
                expected: Expected::Value(json!([
                    {
                        "element": [
                           { "name": "gender", "type": "FHIR.code" },
                        ]
                    }
                ])),
                options: Some(EvaluateOptions {
                    model: Some(get_model_details(ModelType::R4).unwrap()),
                    vars: None,
                    now: None,
                    trace_function: None,
                }),
            }
        ];

        run_tests(test_cases);
    }
}