Skip to main content

haste_x_fhir_query/
lib.rs

1use haste_fhir_model::r4::generated::terminology::IssueType;
2use haste_fhir_operation_error::OperationOutcomeError;
3use haste_fhirpath::{Config, FPEngine};
4use haste_reflect::MetaValue;
5use regex::Regex;
6use std::sync::{Arc, LazyLock};
7
8use crate::conversion::stringify_meta_value;
9
10pub mod conversion;
11
12static FP_EXPRESSION_REGEX: LazyLock<Regex> =
13    LazyLock::new(|| Regex::new(r"\{\{([^}]*)\}\}").expect("Failed to compile regex"));
14
15/// Evaluates `FHIRPath` expressions embedded in an x-fhir-query string.
16///
17/// Replaces each `FHIRPath` expression found in the query with the evaluated
18/// string representation of its result.
19///
20/// # Errors
21///
22/// Returns an [`OperationOutcomeError`] if:
23/// - the embedded `FHIRPath` expression is empty,
24/// - `FHIRPath` evaluation fails,
25/// - a result value cannot be converted into a string representation.
26pub async fn evaluation<'a, 'b>(
27    x_fhir_query: &str,
28    values: Vec<&'a dyn MetaValue>,
29    config: Arc<Config<'b>>,
30) -> Result<String, OperationOutcomeError>
31where
32    'a: 'b,
33{
34    let engine = FPEngine::new();
35
36    let mut result = x_fhir_query.to_string();
37
38    for expression in FP_EXPRESSION_REGEX.captures_iter(x_fhir_query) {
39        let full_match = expression.get(0).map_or("", |m| m.as_str());
40
41        let expr = expression.get(1).map_or("", |m| m.as_str());
42
43        println!("Evaluating FHIRPath expression: '{expr}'");
44
45        if expr.is_empty() {
46            return Err(OperationOutcomeError::fatal(
47                IssueType::invalid(),
48                "FHIRPath expression is empty.".to_string(),
49            ));
50        }
51
52        let fp_result = engine
53            .evaluate_with_config(expr, values.clone(), config.clone())
54            .await
55            .map_err(|e| {
56                OperationOutcomeError::fatal(
57                    IssueType::invalid(),
58                    format!("FHIRPath evaluation error: {e}"),
59                )
60            })?;
61
62        let fp_string_result = fp_result
63            .iter()
64            .map(stringify_meta_value)
65            .collect::<Result<Vec<String>, OperationOutcomeError>>()?
66            .join(",");
67
68        result = result.replace(full_match, &fp_string_result);
69    }
70
71    Ok(result)
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use haste_fhir_model::r4::generated::{
78        resources::Patient,
79        types::{FHIRString, HumanName},
80    };
81    #[tokio::test]
82    async fn test_simple_eval() {
83        let patient = Patient {
84            id: Some("example".to_string()),
85
86            ..Default::default()
87        };
88        let result = evaluation(
89            "Patient/{{$this.id}}",
90            vec![&patient],
91            Arc::new(Config::default()),
92        )
93        .await
94        .expect("Evaluation failed");
95
96        assert_eq!(result, "Patient/example");
97    }
98
99    #[tokio::test]
100    async fn test_multiple() {
101        let patient = Patient {
102            id: Some("example".to_string()),
103            name: Some(vec![HumanName {
104                family: Some(Box::new(FHIRString {
105                    value: Some("Doe".to_string()),
106                    ..Default::default()
107                })),
108                ..Default::default()
109            }]),
110            ..Default::default()
111        };
112        let result = evaluation(
113            "Patient/{{$this.id}}/{{$this.name.family.value}}",
114            vec![&patient],
115            Arc::new(Config::default()),
116        )
117        .await
118        .expect("Evaluation failed");
119
120        assert_eq!(result, "Patient/example/Doe");
121    }
122}