Skip to main content

fml_engine/
expressions.rs

1//! FHIRPath, supplied by the host.
2//!
3//! The engine evaluates `where`, `check`, `log`, `default` and `evaluate` through [`Expressions`],
4//! so it builds without a FHIRPath implementation; the `fhirpath` feature provides one.
5
6use std::collections::HashSet;
7use std::future::Future;
8
9use fml_syntax::{AstNode, SyntaxKind, SyntaxNode, TextSize, parse};
10use serde_json::Value;
11
12/// A value passed to or returned by FHIRPath: FHIR JSON and its FHIR type where known
13/// (`Patient`, `HumanName`, `string`).
14#[derive(Debug, Clone, PartialEq)]
15pub struct Item {
16    pub json: Value,
17    pub type_name: Option<String>,
18}
19
20/// Evaluates FHIRPath expressions.
21pub trait Expressions {
22    /// Evaluates `expression` against `focus` (none for expressions that only use variables).
23    /// Variables are available as `%name`.
24    fn evaluate(
25        &self,
26        expression: &str,
27        focus: Option<&Item>,
28        variables: &[(String, Item)],
29    ) -> impl Future<Output = Result<Vec<Item>, String>>;
30}
31
32/// No FHIRPath: maps that use expressions fail with a clear error.
33#[derive(Debug, Default, Clone, Copy)]
34pub struct NoExpressions;
35
36impl Expressions for NoExpressions {
37    async fn evaluate(
38        &self,
39        expression: &str,
40        _focus: Option<&Item>,
41        _variables: &[(String, Item)],
42    ) -> Result<Vec<Item>, String> {
43        Err(format!(
44            "`{expression}` needs FHIRPath; build fml-engine with the `fhirpath` feature or supply an evaluator"
45        ))
46    }
47}
48
49/// Rewrites the map's variables to FHIRPath environment variables: in a StructureMap, a name
50/// that is a variable refers to it (the reference engine resolves such names before the focus),
51/// so `item.answer` with a variable `item` becomes `%item.answer`.
52pub fn bind_variables(expression: &str, variables: &HashSet<String>) -> String {
53    const PREFIX: &str = "let _ = ";
54    let source = format!("{PREFIX}{expression};");
55    let parse = parse(&source);
56    let Some(constant) = parse.tree().constants().next() else {
57        return expression.to_owned();
58    };
59    let Some(root) = constant.expression() else {
60        return expression.to_owned();
61    };
62    let mut positions: Vec<TextSize> = root
63        .syntax()
64        .descendants()
65        .filter(|node| node.kind() == SyntaxKind::NameRef && is_path_start(node))
66        .filter_map(|node| {
67            let token = node.first_token()?;
68            (token.kind() == SyntaxKind::Ident && variables.contains(token.text()))
69                .then(|| token.text_range().start())
70        })
71        .collect();
72    positions.sort();
73    let mut out = String::with_capacity(expression.len() + positions.len());
74    let mut last = PREFIX.len();
75    for position in positions {
76        let position = usize::from(position);
77        out.push_str(&source[last..position]);
78        out.push('%');
79        last = position;
80    }
81    out.push_str(&source[last..source.len() - 1]);
82    out
83}
84
85/// The names in `expression` that can refer to the map's variables: those that start a path
86/// (`item` and `x` in `item.answer.where(x > 1)`) and `%name` constants.
87pub fn path_start_names(expression: &str) -> HashSet<String> {
88    let source = format!("let _ = {expression};");
89    let parse = parse(&source);
90    let Some(root) = parse
91        .tree()
92        .constants()
93        .next()
94        .and_then(|constant| constant.expression())
95    else {
96        return HashSet::new();
97    };
98    let names = root
99        .syntax()
100        .descendants()
101        .filter(|node| node.kind() == SyntaxKind::NameRef && is_path_start(node))
102        .filter_map(|node| node.first_token())
103        .filter(|token| token.kind() == SyntaxKind::Ident)
104        .map(|token| token.text().to_owned());
105    // `%name` names a variable explicitly.
106    let constants = root
107        .syntax()
108        .descendants()
109        .filter(|node| node.kind() == SyntaxKind::ExternalConstant)
110        .filter_map(|node| {
111            node.children_with_tokens()
112                .filter_map(|e| e.into_token())
113                .find(|t| t.kind() == SyntaxKind::Ident)
114                .map(|t| t.text().to_owned())
115        });
116    names.chain(constants).collect()
117}
118
119/// Whether a name starts a path, rather than following a `.`.
120fn is_path_start(name: &SyntaxNode) -> bool {
121    match name.parent() {
122        Some(parent) if parent.kind() == SyntaxKind::MemberExpr => {
123            parent.children().next().as_ref() == Some(name)
124        }
125        _ => true,
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn binds_variables_at_path_starts() {
135        let variables: HashSet<String> = ["item", "src"].into_iter().map(String::from).collect();
136        assert_eq!(
137            bind_variables("item.answer.valueString", &variables),
138            "%item.answer.valueString"
139        );
140        assert_eq!(
141            bind_variables("linkId.value = 'x' and src.item.exists()", &variables),
142            "linkId.value = 'x' and %src.item.exists()"
143        );
144        assert_eq!(
145            bind_variables("answer.where(item = 1).item", &variables),
146            "answer.where(%item = 1).item"
147        );
148        assert_eq!(bind_variables("%item", &variables), "%item");
149    }
150
151    #[test]
152    fn finds_the_names_that_can_be_variables() {
153        let names = path_start_names("item.answer.where(x > 1) and %s1.substring(0) = y.z");
154        let mut names: Vec<_> = names.into_iter().collect();
155        names.sort();
156        assert_eq!(names, vec!["item", "s1", "x", "y"]);
157    }
158}