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/// A shared evaluator: one FHIRPath engine for many transforms.
33impl<T: Expressions> Expressions for &T {
34    fn evaluate(
35        &self,
36        expression: &str,
37        focus: Option<&Item>,
38        variables: &[(String, Item)],
39    ) -> impl Future<Output = Result<Vec<Item>, String>> {
40        (**self).evaluate(expression, focus, variables)
41    }
42}
43
44/// No FHIRPath: maps that use expressions fail with a clear error.
45#[derive(Debug, Default, Clone, Copy)]
46pub struct NoExpressions;
47
48impl Expressions for NoExpressions {
49    async fn evaluate(
50        &self,
51        expression: &str,
52        _focus: Option<&Item>,
53        _variables: &[(String, Item)],
54    ) -> Result<Vec<Item>, String> {
55        Err(format!(
56            "`{expression}` needs FHIRPath; build fml-engine with the `fhirpath` feature or supply an evaluator"
57        ))
58    }
59}
60
61/// Rewrites the map's variables to FHIRPath environment variables: in a StructureMap, a name
62/// that is a variable refers to it (the reference engine resolves such names before the focus),
63/// so `item.answer` with a variable `item` becomes `%item.answer`.
64pub fn bind_variables(expression: &str, variables: &HashSet<String>) -> String {
65    const PREFIX: &str = "let _ = ";
66    let source = format!("{PREFIX}{expression};");
67    let parse = parse(&source);
68    let Some(constant) = parse.tree().constants().next() else {
69        return expression.to_owned();
70    };
71    let Some(root) = constant.expression() else {
72        return expression.to_owned();
73    };
74    let mut positions: Vec<TextSize> = root
75        .syntax()
76        .descendants()
77        .filter(|node| node.kind() == SyntaxKind::NameRef && is_path_start(node))
78        .filter_map(|node| {
79            let token = node.first_token()?;
80            (token.kind() == SyntaxKind::Ident && variables.contains(token.text()))
81                .then(|| token.text_range().start())
82        })
83        .collect();
84    positions.sort();
85    let mut out = String::with_capacity(expression.len() + positions.len());
86    let mut last = PREFIX.len();
87    for position in positions {
88        let position = usize::from(position);
89        out.push_str(&source[last..position]);
90        out.push('%');
91        last = position;
92    }
93    out.push_str(&source[last..source.len() - 1]);
94    out
95}
96
97/// The names in `expression` that can refer to the map's variables: those that start a path
98/// (`item` and `x` in `item.answer.where(x > 1)`) and `%name` constants.
99pub fn path_start_names(expression: &str) -> HashSet<String> {
100    let source = format!("let _ = {expression};");
101    let parse = parse(&source);
102    let Some(root) = parse
103        .tree()
104        .constants()
105        .next()
106        .and_then(|constant| constant.expression())
107    else {
108        return HashSet::new();
109    };
110    let names = root
111        .syntax()
112        .descendants()
113        .filter(|node| node.kind() == SyntaxKind::NameRef && is_path_start(node))
114        .filter_map(|node| node.first_token())
115        .filter(|token| token.kind() == SyntaxKind::Ident)
116        .map(|token| token.text().to_owned());
117    // `%name` names a variable explicitly.
118    let constants = root
119        .syntax()
120        .descendants()
121        .filter(|node| node.kind() == SyntaxKind::ExternalConstant)
122        .filter_map(|node| {
123            node.children_with_tokens()
124                .filter_map(|e| e.into_token())
125                .find(|t| t.kind() == SyntaxKind::Ident)
126                .map(|t| t.text().to_owned())
127        });
128    names.chain(constants).collect()
129}
130
131/// Whether a name starts a path, rather than following a `.`.
132fn is_path_start(name: &SyntaxNode) -> bool {
133    match name.parent() {
134        Some(parent) if parent.kind() == SyntaxKind::MemberExpr => {
135            parent.children().next().as_ref() == Some(name)
136        }
137        _ => true,
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn binds_variables_at_path_starts() {
147        let variables: HashSet<String> = ["item", "src"].into_iter().map(String::from).collect();
148        assert_eq!(
149            bind_variables("item.answer.valueString", &variables),
150            "%item.answer.valueString"
151        );
152        assert_eq!(
153            bind_variables("linkId.value = 'x' and src.item.exists()", &variables),
154            "linkId.value = 'x' and %src.item.exists()"
155        );
156        assert_eq!(
157            bind_variables("answer.where(item = 1).item", &variables),
158            "answer.where(%item = 1).item"
159        );
160        assert_eq!(bind_variables("%item", &variables), "%item");
161    }
162
163    #[test]
164    fn finds_the_names_that_can_be_variables() {
165        let names = path_start_names("item.answer.where(x > 1) and %s1.substring(0) = y.z");
166        let mut names: Vec<_> = names.into_iter().collect();
167        names.sort();
168        assert_eq!(names, vec!["item", "s1", "x", "y"]);
169    }
170}