Skip to main content

ferrobridge_fhirpath/eval/
mod.rs

1// SPDX-FileCopyrightText: Cadasto B.V.
2// SPDX-License-Identifier: BUSL-1.1
3
4//! Evaluating a parsed expression against a document.
5//!
6//! An [`Environment`] carries the type model, the caller's environment
7//! variables and seams, and the clock; a [`Focus`] names the context node and
8//! the resources `%resource` and `%rootResource` stand for (FHIR R4
9//! §2.9.1.4). Evaluation returns a collection or an [`error::EvalError`],
10//! two distinct outcomes: an empty collection is never an error and an error
11//! is never an empty collection.
12
13pub mod convert;
14pub mod equality;
15pub mod error;
16mod functions;
17mod navigate;
18mod operators;
19mod order;
20
21use std::collections::BTreeMap;
22use std::fmt;
23use std::sync::Arc;
24
25use fhir_types::codec::Value;
26
27use crate::ast::{Expr, Literal};
28use crate::model::{Model, named_type, path_type};
29use crate::quantity::Quantity;
30use crate::seam::{Definitions, Profiles, Resolver, Terminology};
31use crate::value::node::{Doc, Node};
32use crate::value::{Collection, Item};
33use convert::UCUM;
34use error::EvalError;
35
36/// The functions FHIRPath 3.0.0 adds as Standard for Trial Use that the HL7
37/// FHIRPath test suite for FHIR R4 exercises
38/// (<http://hl7.org/fhirpath/STU3/>, each a section marked STU).
39///
40/// FHIR R4 cites FHIRPath release 2, N1 2.0.0 (fhirpath.html §2.9), which
41/// defines none of them, so an evaluation calls one only with
42/// [`Environment::with_fhirpath_stu3_functions`]; without it the call is
43/// [`EvalError::UnknownFunction`].
44pub const STU3_FUNCTIONS: [&str; 13] = [
45    "comparable",
46    "decode",
47    "encode",
48    "escape",
49    "highBoundary",
50    "join",
51    "lowBoundary",
52    "matchesFull",
53    "precision",
54    "sort",
55    "split",
56    "trim",
57    "unescape",
58];
59
60/// What an evaluation runs with: the type model, the environment variables
61/// and the seams the caller supplies.
62pub struct Environment<'a> {
63    model: &'a dyn Model,
64    variables: BTreeMap<String, Collection<'a>>,
65    resolver: Option<&'a dyn Resolver>,
66    terminology: Option<&'a dyn Terminology>,
67    profiles: Option<&'a dyn Profiles>,
68    definitions: Option<&'a dyn Definitions>,
69    now: Option<jiff::Timestamp>,
70    strict_ordering: bool,
71    stu3_functions: bool,
72}
73
74impl fmt::Debug for Environment<'_> {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        f.debug_struct("Environment")
77            .field("model", &self.model)
78            .field("variables", &self.variables.keys().collect::<Vec<_>>())
79            .field("resolver", &self.resolver.is_some())
80            .field("terminology", &self.terminology.is_some())
81            .field("profiles", &self.profiles.is_some())
82            .field("definitions", &self.definitions.is_some())
83            .field("now", &self.now)
84            .field("strict_ordering", &self.strict_ordering)
85            .field("stu3_functions", &self.stu3_functions)
86            .finish()
87    }
88}
89
90impl<'a> Environment<'a> {
91    /// An environment over `model` with no variables of its own and no seams.
92    #[must_use]
93    pub fn new(model: &'a dyn Model) -> Self {
94        Self {
95            model,
96            variables: BTreeMap::new(),
97            resolver: None,
98            terminology: None,
99            profiles: None,
100            definitions: None,
101            now: None,
102            strict_ordering: false,
103            stu3_functions: false,
104        }
105    }
106
107    /// Defines the environment variable `%name` (N1 §9), which takes
108    /// precedence over a variable of the same name N1 or FHIR R4 define.
109    #[must_use]
110    pub fn with_variable(mut self, name: impl Into<String>, value: Collection<'a>) -> Self {
111        self.variables.insert(name.into(), value);
112        self
113    }
114
115    /// Supplies the seam `resolve()` calls.
116    #[must_use]
117    pub fn with_resolver(mut self, resolver: &'a dyn Resolver) -> Self {
118        self.resolver = Some(resolver);
119        self
120    }
121
122    /// Supplies the seam `memberOf()`, `subsumes()` and `subsumedBy()` call.
123    #[must_use]
124    pub fn with_terminology(mut self, terminology: &'a dyn Terminology) -> Self {
125        self.terminology = Some(terminology);
126        self
127    }
128
129    /// Supplies the seam `conformsTo()` calls.
130    #[must_use]
131    pub fn with_profiles(mut self, profiles: &'a dyn Profiles) -> Self {
132        self.profiles = Some(profiles);
133        self
134    }
135
136    /// Supplies the seam `elementDefinition()` and `slice()` call.
137    #[must_use]
138    pub fn with_definitions(mut self, definitions: &'a dyn Definitions) -> Self {
139        self.definitions = Some(definitions);
140        self
141    }
142
143    /// Fixes the instant `now()`, `today()` and `timeOfDay()` answer; without
144    /// it they read the clock once per evaluation.
145    #[must_use]
146    pub const fn with_now(mut self, now: jiff::Timestamp) -> Self {
147        self.now = Some(now);
148        self
149    }
150
151    /// Refuses an order-dependent operation on a collection whose order is
152    /// undefined.
153    ///
154    /// N1 §3.1 leaves the order of `children()` and `descendants()`, and of
155    /// every collection derived from them, undefined, and lets an
156    /// implementation return an error when `first()`, `last()`, `tail()`,
157    /// `skip()`, `take()` or the indexer runs over such a collection. With
158    /// this mode on, that is [`EvalError::Unordered`]; without it, those
159    /// operations answer in the order the document holds.
160    #[must_use]
161    pub const fn with_strict_ordering(mut self) -> Self {
162        self.strict_ordering = true;
163        self
164    }
165
166    /// Admits the functions of [`STU3_FUNCTIONS`], which FHIRPath 3.0.0 adds
167    /// as Standard for Trial Use.
168    ///
169    /// Without it the evaluator is FHIRPath N1 2.0.0, the release FHIR R4
170    /// cites (fhirpath.html §2.9), and a call to one of them is
171    /// [`EvalError::UnknownFunction`]. Those this crate does not implement yet
172    /// stay that error with the option on.
173    #[must_use]
174    pub const fn with_fhirpath_stu3_functions(mut self) -> Self {
175        self.stu3_functions = true;
176        self
177    }
178
179    /// The type model.
180    #[must_use]
181    pub fn model(&self) -> &'a dyn Model {
182        self.model
183    }
184}
185
186/// The context an expression is evaluated on.
187#[derive(Clone)]
188enum Start<'a> {
189    /// A resource.
190    Resource(&'a Value),
191    /// An element of the named type, with a primitive's `_name` object and
192    /// the element path of its definition.
193    Element {
194        value: Option<&'a Value>,
195        element: Option<&'a Value>,
196        type_name: String,
197        definition: Option<String>,
198    },
199    /// Items from an earlier evaluation.
200    Items(Collection<'a>),
201}
202
203/// The context node and the resources `%resource` and `%rootResource` name.
204///
205/// Its `Debug` form names what kind of context it is and leaves the document
206/// out.
207#[derive(Clone)]
208pub struct Focus<'a> {
209    start: Start<'a>,
210    resource: Option<&'a Value>,
211    root: Option<&'a Value>,
212}
213
214impl fmt::Debug for Focus<'_> {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        let start = match &self.start {
217            Start::Resource(_) => String::from("resource"),
218            Start::Element { type_name, .. } => format!("element of {type_name}"),
219            Start::Items(items) => format!("{} items", items.len()),
220        };
221        f.debug_struct("Focus")
222            .field("start", &start)
223            .field("resource", &self.resource.is_some())
224            .field("root", &self.root.is_some())
225            .finish()
226    }
227}
228
229impl<'a> Focus<'a> {
230    /// A resource as the context, `%resource` and `%rootResource`.
231    #[must_use]
232    pub const fn resource(resource: &'a Value) -> Self {
233        Self {
234            start: Start::Resource(resource),
235            resource: Some(resource),
236            root: Some(resource),
237        }
238    }
239
240    /// A contained resource as the context and `%resource`, its container as
241    /// `%rootResource` (FHIR R4 §2.9.1.4).
242    #[must_use]
243    pub const fn contained(resource: &'a Value, container: &'a Value) -> Self {
244        Self {
245            start: Start::Resource(resource),
246            resource: Some(resource),
247            root: Some(container),
248        }
249    }
250
251    /// An element as the context, typed by `type_name` (a type name or an
252    /// element path such as `Patient.contact`), inside `resource`.
253    ///
254    /// A primitive element is its JSON value; [`Focus::primitive`] adds the
255    /// `_name` object.
256    #[must_use]
257    pub fn element(value: &'a Value, type_name: impl Into<String>, resource: &'a Value) -> Self {
258        Self {
259            start: Start::Element {
260                value: Some(value),
261                element: None,
262                type_name: type_name.into(),
263                definition: None,
264            },
265            resource: Some(resource),
266            root: Some(resource),
267        }
268    }
269
270    /// A primitive element as the context: its JSON value, its `_name`
271    /// object, or both, inside `resource`.
272    #[must_use]
273    pub fn primitive(
274        value: Option<&'a Value>,
275        element: Option<&'a Value>,
276        type_name: impl Into<String>,
277        resource: &'a Value,
278    ) -> Self {
279        Self {
280            start: Start::Element {
281                value,
282                element,
283                type_name: type_name.into(),
284                definition: None,
285            },
286            resource: Some(resource),
287            root: Some(resource),
288        }
289    }
290
291    /// Names the element definition an element context is an occurrence
292    /// of, by its element path (`RiskAssessment.prediction.probability[x]`);
293    /// any other context is left as it was.
294    ///
295    /// On an occurrence of a choice element, a member another type of the
296    /// choice defines navigates to an empty collection where the
297    /// occurrence's own type lacks it (N1 §3.2, FHIR R4 §2.9.1.1).
298    #[must_use]
299    pub fn defined_by(mut self, path: impl Into<String>) -> Self {
300        if let Start::Element { definition, .. } = &mut self.start {
301            *definition = Some(path.into());
302        }
303        self
304    }
305
306    /// Items from an earlier evaluation as the context.
307    #[must_use]
308    pub const fn items(items: Collection<'a>, resource: Option<&'a Value>) -> Self {
309        Self {
310            start: Start::Items(items),
311            resource,
312            root: resource,
313        }
314    }
315
316    /// Names the resources `%resource` and `%rootResource` stand for,
317    /// replacing what the constructor set (FHIR R4 §2.9.1.4): an element of
318    /// a contained resource has that resource as `%resource` and its
319    /// container as `%rootResource`, and `None` leaves a variable empty.
320    #[must_use]
321    pub fn within(mut self, resource: Option<&'a Value>, root: Option<&'a Value>) -> Self {
322        self.resource = resource;
323        self.root = root;
324        self
325    }
326
327    /// No context at all: every path is empty.
328    #[must_use]
329    pub const fn none() -> Self {
330        Self {
331            start: Start::Items(Vec::new()),
332            resource: None,
333            root: None,
334        }
335    }
336}
337
338/// The state of one evaluation.
339pub(crate) struct Engine<'e, 'a> {
340    env: &'e Environment<'a>,
341    context: Collection<'a>,
342    resource: Collection<'a>,
343    root: Collection<'a>,
344    now: jiff::Timestamp,
345}
346
347/// The variables of the expression being evaluated: the focus (`$this`), the
348/// iteration index and the aggregate total.
349#[derive(Clone)]
350pub(crate) struct Frame<'a> {
351    focus: Collection<'a>,
352    index: Option<usize>,
353    total: Option<Collection<'a>>,
354}
355
356impl<'a> Frame<'a> {
357    /// The frame of a top-level evaluation over `focus`.
358    pub(crate) const fn top(focus: Collection<'a>) -> Self {
359        Self {
360            focus,
361            index: None,
362            total: None,
363        }
364    }
365}
366
367/// Evaluates `expr` in `env` on `focus`.
368///
369/// # Errors
370///
371/// Returns what the evaluation signals, as [`EvalError`].
372pub(crate) fn evaluate<'a>(
373    expr: &Expr,
374    env: &Environment<'a>,
375    focus: Focus<'a>,
376) -> Result<Collection<'a>, EvalError> {
377    let model = env.model;
378    let as_resource = |value: Option<&'a Value>| -> Collection<'a> {
379        value
380            .map(|value| Item::Node(Node::resource(model, Doc::Borrowed(value))))
381            .into_iter()
382            .collect()
383    };
384    let context = match focus.start {
385        Start::Resource(value) => as_resource(Some(value)),
386        Start::Element {
387            value,
388            element,
389            type_name,
390            definition,
391        } => {
392            let ty = path_type(model, &type_name)
393                .or_else(|| named_type(model, &type_name))
394                .ok_or(EvalError::UnknownType { name: type_name })?;
395            let definition = match definition {
396                Some(path) => match model.schemas().element(&path) {
397                    Some((_, field)) => Some(field.path),
398                    None => return Err(EvalError::UnknownType { name: path }),
399                },
400                None => None,
401            };
402            let node = match (value, element) {
403                (Some(value), element) => Some(Node::typed(
404                    ty,
405                    Doc::Borrowed(value),
406                    element.map(Doc::Borrowed),
407                )),
408                (None, Some(element)) => Some(Node::typed(
409                    ty,
410                    Doc::Owned(Arc::new(Value::Null)),
411                    Some(Doc::Borrowed(element)),
412                )),
413                (None, None) => None,
414            };
415            node.map(|node| match definition {
416                Some(path) => node.with_definition(path),
417                None => node,
418            })
419            .map(Item::Node)
420            .into_iter()
421            .collect()
422        }
423        Start::Items(items) => items,
424    };
425    let engine = Engine {
426        env,
427        resource: as_resource(focus.resource),
428        root: as_resource(focus.root),
429        now: env.now.unwrap_or_else(jiff::Timestamp::now),
430        context: context.clone(),
431    };
432    engine.eval(expr, &Frame::top(context))
433}
434
435impl<'e, 'a> Engine<'e, 'a> {
436    /// The type model.
437    pub(crate) fn model(&self) -> &'a dyn Model {
438        self.env.model
439    }
440
441    /// Evaluates one node of the syntax tree.
442    pub(crate) fn eval(&self, expr: &Expr, frame: &Frame<'a>) -> Result<Collection<'a>, EvalError> {
443        match expr {
444            Expr::Literal(literal) => Ok(vec![literal_item(literal)]),
445            Expr::Empty => Ok(Vec::new()),
446            Expr::External(name) => self.external(name),
447            Expr::This => Ok(frame.focus.clone()),
448            Expr::Index => Ok(frame
449                .index
450                .and_then(|index| i64::try_from(index).ok())
451                .map(Item::Integer)
452                .into_iter()
453                .collect()),
454            Expr::Total => Ok(frame.total.clone().unwrap_or_default()),
455            Expr::Member { input, name } => {
456                let items = self.input(input.as_deref(), frame)?;
457                self.navigate(&items, name, input.is_none())
458            }
459            Expr::Call { input, name, args } => {
460                if order::ORDERED_FUNCTIONS.contains(&name.as_str()) {
461                    self.require_order(name, input.as_deref())?;
462                }
463                let items = self.input(input.as_deref(), frame)?;
464                self.call(name, items, args, frame)
465            }
466            Expr::Invocation { input, variable } => {
467                let items = self.eval(input, frame)?;
468                self.eval(variable, &Frame::top(items))
469            }
470            Expr::Indexer { input, index } => {
471                self.require_order("[]", Some(input))?;
472                let items = self.eval(input, frame)?;
473                let index =
474                    convert::integer(self.model(), &self.eval(index, frame)?, "the indexer")?;
475                Ok(index
476                    .and_then(|index| usize::try_from(index).ok())
477                    .and_then(|index| items.into_iter().nth(index))
478                    .into_iter()
479                    .collect())
480            }
481            Expr::Negate(operand) => operators::negate(self.model(), &self.eval(operand, frame)?),
482            Expr::Plus(operand) => operators::plus(self.model(), &self.eval(operand, frame)?),
483            Expr::Binary { op, left, right } => self.binary(*op, left, right, frame),
484            Expr::Is {
485                operand,
486                type_specifier,
487            } => {
488                let items = self.eval(operand, frame)?;
489                let ty = self.resolve_type(type_specifier)?;
490                match convert::single(&items, "is")? {
491                    Some(item) => Ok(vec![Item::Boolean(self.is_instance(item, &ty))]),
492                    None => Ok(Vec::new()),
493                }
494            }
495            Expr::As {
496                operand,
497                type_specifier,
498            } => {
499                let items = self.eval(operand, frame)?;
500                let ty = self.resolve_type(type_specifier)?;
501                match convert::single(&items, "as")? {
502                    Some(item) if self.is_type(item, &ty) => Ok(vec![item.clone()]),
503                    _ => Ok(Vec::new()),
504                }
505            }
506            // NOTE: the FHIRPath 3.0.0 grammar admits a direction only on a sort() key, and
507            // sort() reads it there; evaluated anywhere else the key is its selector.
508            Expr::SortKey { key, .. } => self.eval(key, frame),
509        }
510    }
511
512    /// Refuses the order-dependent `operation` on `input` in the strict
513    /// ordering mode when N1 §3.1 leaves the input's order undefined.
514    fn require_order(&self, operation: &str, input: Option<&Expr>) -> Result<(), EvalError> {
515        if self.env.strict_ordering && input.is_some_and(order::unordered) {
516            return Err(EvalError::Unordered {
517                operation: String::from(operation),
518            });
519        }
520        Ok(())
521    }
522
523    /// The input of a member or a call: the expression before the `.`, or
524    /// the focus at the start of a path.
525    fn input(&self, input: Option<&Expr>, frame: &Frame<'a>) -> Result<Collection<'a>, EvalError> {
526        match input {
527            Some(input) => self.eval(input, frame),
528            None => Ok(frame.focus.clone()),
529        }
530    }
531
532    /// An environment variable (N1 §9, FHIR R4 §2.9.1.4 and §2.9.1.7).
533    fn external(&self, name: &str) -> Result<Collection<'a>, EvalError> {
534        if let Some(value) = self.env.variables.get(name) {
535            return Ok(value.clone());
536        }
537        let text = |value: &str| Ok(vec![Item::String(String::from(value))]);
538        match name {
539            "ucum" => text(UCUM),
540            "sct" => text("http://snomed.info/sct"),
541            "loinc" => text("http://loinc.org"),
542            "context" => Ok(self.context.clone()),
543            "resource" => Ok(self.resource.clone()),
544            "rootResource" => Ok(self.root.clone()),
545            _ => {
546                if let Some(id) = name.strip_prefix("vs-") {
547                    text(&format!("http://hl7.org/fhir/ValueSet/{id}"))
548                } else if let Some(id) = name.strip_prefix("ext-") {
549                    text(&format!("http://hl7.org/fhir/StructureDefinition/{id}"))
550                } else {
551                    Err(EvalError::UnknownVariable {
552                        name: String::from(name),
553                    })
554                }
555            }
556        }
557    }
558
559    /// The `%resource` collection.
560    pub(crate) fn resource(&self) -> &Collection<'a> {
561        &self.resource
562    }
563
564    /// The `%rootResource` collection.
565    pub(crate) fn root(&self) -> &Collection<'a> {
566        &self.root
567    }
568
569    /// The instant of this evaluation.
570    pub(crate) const fn now(&self) -> jiff::Timestamp {
571        self.now
572    }
573
574    /// The environment.
575    pub(crate) const fn env(&self) -> &'e Environment<'a> {
576        self.env
577    }
578}
579
580/// The item a literal stands for.
581fn literal_item<'a>(literal: &Literal) -> Item<'a> {
582    match literal {
583        Literal::Boolean(value) => Item::Boolean(*value),
584        Literal::String(value) => Item::String(value.clone()),
585        Literal::Integer(value) => Item::Integer(*value),
586        Literal::Decimal(value) => Item::Decimal(*value),
587        Literal::Date(value) => Item::Date(*value),
588        Literal::DateTime(value) => Item::DateTime(*value),
589        Literal::Time(value) => Item::Time(*value),
590        Literal::Quantity(value, unit) => Item::Quantity(Quantity::new(*value, unit.clone())),
591    }
592}