use std::collections::HashSet;
use std::future::Future;
use fml_syntax::{AstNode, SyntaxKind, SyntaxNode, TextSize, parse};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq)]
pub struct Item {
pub json: Value,
pub type_name: Option<String>,
}
pub trait Expressions {
fn evaluate(
&self,
expression: &str,
focus: Option<&Item>,
variables: &[(String, Item)],
) -> impl Future<Output = Result<Vec<Item>, String>>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoExpressions;
impl Expressions for NoExpressions {
async fn evaluate(
&self,
expression: &str,
_focus: Option<&Item>,
_variables: &[(String, Item)],
) -> Result<Vec<Item>, String> {
Err(format!(
"`{expression}` needs FHIRPath; build fml-engine with the `fhirpath` feature or supply an evaluator"
))
}
}
pub fn bind_variables(expression: &str, variables: &HashSet<String>) -> String {
const PREFIX: &str = "let _ = ";
let source = format!("{PREFIX}{expression};");
let parse = parse(&source);
let Some(constant) = parse.tree().constants().next() else {
return expression.to_owned();
};
let Some(root) = constant.expression() else {
return expression.to_owned();
};
let mut positions: Vec<TextSize> = root
.syntax()
.descendants()
.filter(|node| node.kind() == SyntaxKind::NameRef && is_path_start(node))
.filter_map(|node| {
let token = node.first_token()?;
(token.kind() == SyntaxKind::Ident && variables.contains(token.text()))
.then(|| token.text_range().start())
})
.collect();
positions.sort();
let mut out = String::with_capacity(expression.len() + positions.len());
let mut last = PREFIX.len();
for position in positions {
let position = usize::from(position);
out.push_str(&source[last..position]);
out.push('%');
last = position;
}
out.push_str(&source[last..source.len() - 1]);
out
}
pub fn path_start_names(expression: &str) -> HashSet<String> {
let source = format!("let _ = {expression};");
let parse = parse(&source);
let Some(root) = parse
.tree()
.constants()
.next()
.and_then(|constant| constant.expression())
else {
return HashSet::new();
};
let names = root
.syntax()
.descendants()
.filter(|node| node.kind() == SyntaxKind::NameRef && is_path_start(node))
.filter_map(|node| node.first_token())
.filter(|token| token.kind() == SyntaxKind::Ident)
.map(|token| token.text().to_owned());
let constants = root
.syntax()
.descendants()
.filter(|node| node.kind() == SyntaxKind::ExternalConstant)
.filter_map(|node| {
node.children_with_tokens()
.filter_map(|e| e.into_token())
.find(|t| t.kind() == SyntaxKind::Ident)
.map(|t| t.text().to_owned())
});
names.chain(constants).collect()
}
fn is_path_start(name: &SyntaxNode) -> bool {
match name.parent() {
Some(parent) if parent.kind() == SyntaxKind::MemberExpr => {
parent.children().next().as_ref() == Some(name)
}
_ => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn binds_variables_at_path_starts() {
let variables: HashSet<String> = ["item", "src"].into_iter().map(String::from).collect();
assert_eq!(
bind_variables("item.answer.valueString", &variables),
"%item.answer.valueString"
);
assert_eq!(
bind_variables("linkId.value = 'x' and src.item.exists()", &variables),
"linkId.value = 'x' and %src.item.exists()"
);
assert_eq!(
bind_variables("answer.where(item = 1).item", &variables),
"answer.where(%item = 1).item"
);
assert_eq!(bind_variables("%item", &variables), "%item");
}
#[test]
fn finds_the_names_that_can_be_variables() {
let names = path_start_names("item.answer.where(x > 1) and %s1.substring(0) = y.z");
let mut names: Vec<_> = names.into_iter().collect();
names.sort();
assert_eq!(names, vec!["item", "s1", "x", "y"]);
}
}