Skip to main content

helios_fhirpath/
functions.rs

1//! A curated, hand-maintained catalog of the FHIRPath functions this crate's
2//! evaluator implements.
3//!
4//! [`evaluator`](crate::evaluator) dispatches function calls through a single
5//! large `match` on the function name — there is no registry a caller could
6//! introspect at runtime. Tooling that needs to answer "what functions exist"
7//! (a completion endpoint, a linter suggesting a fix) has nowhere else to
8//! look, so this module is that list, kept in sync with the evaluator by
9//! hand and guarded by a test (`builtin_functions_are_all_known_to_the_evaluator`)
10//! that calls every cataloged name and asserts the evaluator recognizes it.
11//!
12//! Only functions callable with FHIRPath's `name(args)` invocation syntax are
13//! listed here — infix type operators (`is`, `as`) and other grammar-level
14//! operators are a different part of the language and are out of scope.
15
16/// A closed set of groupings for [`FunctionInfo::category`], mirroring the
17/// section structure of the [FHIRPath functions specification](https://hl7.org/fhirpath/2025Jan/#functions)
18/// plus the FHIR- and SQL-on-FHIR-specific extensions this crate adds.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum FunctionCategory {
21    /// Existence checks and cardinality (`empty`, `exists`, `count`, ...).
22    Existence,
23    /// Filtering and projection (`where`, `select`, `ofType`, ...).
24    Filtering,
25    /// Subsetting a collection (`first`, `skip`, `single`, ...).
26    Subsetting,
27    /// Combining two collections (`union`, `combine`).
28    Combining,
29    /// Type conversion (`toString`, `convertsToInteger`, `iif`, ...).
30    Conversion,
31    /// String manipulation (`substring`, `replace`, `join`, ...).
32    String,
33    /// Arithmetic and numeric aggregation (`abs`, `round`, `sum`, ...).
34    Math,
35    /// Tree navigation (`children`, `descendants`, `extension`).
36    Tree,
37    /// General-purpose utilities (`trace`, `now`, `defineVariable`, ...).
38    Utility,
39    /// Boolean logic functions (`not`).
40    Boolean,
41    /// Date/time interval arithmetic (`duration`, `difference`).
42    Datetime,
43    /// Type reflection (`type`).
44    Types,
45    /// FHIRPath extensions specific to SQL-on-FHIR ViewDefinitions
46    /// (`getResourceKey`, `getReferenceKey`) — not part of the base
47    /// FHIRPath specification.
48    SqlOnFhir,
49    /// Terminology operations (`memberOf`).
50    Terminology,
51    /// Doesn't fit the other categories cleanly (`resolve`, `comparable`).
52    Other,
53}
54
55impl FunctionCategory {
56    /// The wire/display form of this category, as used by API consumers
57    /// (e.g. a JSON `category` field). Stable — treat as part of the public
58    /// contract.
59    pub const fn as_str(self) -> &'static str {
60        match self {
61            FunctionCategory::Existence => "existence",
62            FunctionCategory::Filtering => "filtering",
63            FunctionCategory::Subsetting => "subsetting",
64            FunctionCategory::Combining => "combining",
65            FunctionCategory::Conversion => "conversion",
66            FunctionCategory::String => "string",
67            FunctionCategory::Math => "math",
68            FunctionCategory::Tree => "tree",
69            FunctionCategory::Utility => "utility",
70            FunctionCategory::Boolean => "boolean",
71            FunctionCategory::Datetime => "datetime",
72            FunctionCategory::Types => "types",
73            FunctionCategory::SqlOnFhir => "sql-on-fhir",
74            FunctionCategory::Terminology => "terminology",
75            FunctionCategory::Other => "other",
76        }
77    }
78}
79
80impl std::fmt::Display for FunctionCategory {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.write_str(self.as_str())
83    }
84}
85
86/// One entry in the [`builtin_functions`] catalog.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct FunctionInfo {
89    /// The function's bare name, as written before the argument list (e.g.
90    /// `"where"` for `where(criteria)`).
91    pub name: &'static str,
92    /// A human-readable call signature, in `name(args)` form. Optional
93    /// arguments are bracketed (e.g. `"round([precision])"`); a trailing
94    /// `...` marks a variadic argument (e.g. `"coalesce(value, ...)"`).
95    /// Not machine-parsed — for display only.
96    pub signature: &'static str,
97    /// The functional grouping this function belongs to.
98    pub category: FunctionCategory,
99}
100
101/// Returns the catalog of FHIRPath functions the evaluator implements,
102/// sorted alphabetically by [`FunctionInfo::name`] (case-insensitively —
103/// `today` sorts before `toDecimal`, matching how the names read as words
104/// rather than raw byte order, which would otherwise interleave on case).
105///
106/// Includes every function from the [FHIRPath 3.0.0 specification](https://hl7.org/fhirpath/2025Jan/#functions)
107/// this crate's evaluator implements, plus the FHIR- and SQL-on-FHIR-specific
108/// extensions it also supports (`extension`, `getResourceKey`,
109/// `getReferenceKey`, `memberOf`, `hasValue`, `comparable`, `resolve`).
110///
111/// Deliberately excludes:
112/// - Infix operators (`is`, `as`, `in`, `contains`, `and`, `or`, `xor`,
113///   `implies`, `div`, `mod`) — a different part of the grammar, not
114///   `name(args)` invocations.
115/// - Terminology functions only reachable as `%terminologies.<name>(...)`
116///   (`expand`, `lookup`, `validateVS`, `validateCS`, `subsumes`,
117///   `translate`) — calling `{}.<name>()` for these does not resolve to the
118///   same function and would misrepresent what a bare call does.
119///
120/// A test (`builtin_functions_are_all_known_to_the_evaluator`) keeps this
121/// list from drifting out of sync with the evaluator: every name here must
122/// be callable without producing the evaluator's "unknown function" error.
123pub fn builtin_functions() -> &'static [FunctionInfo] {
124    use FunctionCategory::*;
125
126    const CATALOG: &[FunctionInfo] = &[
127        FunctionInfo {
128            name: "abs",
129            signature: "abs()",
130            category: Math,
131        },
132        FunctionInfo {
133            name: "aggregate",
134            signature: "aggregate(aggregator, [init])",
135            category: Utility,
136        },
137        FunctionInfo {
138            name: "all",
139            signature: "all([criteria])",
140            category: Existence,
141        },
142        FunctionInfo {
143            name: "allFalse",
144            signature: "allFalse()",
145            category: Existence,
146        },
147        FunctionInfo {
148            name: "allTrue",
149            signature: "allTrue()",
150            category: Existence,
151        },
152        FunctionInfo {
153            name: "anyFalse",
154            signature: "anyFalse()",
155            category: Existence,
156        },
157        FunctionInfo {
158            name: "anyTrue",
159            signature: "anyTrue()",
160            category: Existence,
161        },
162        FunctionInfo {
163            name: "avg",
164            signature: "avg()",
165            category: Math,
166        },
167        FunctionInfo {
168            name: "ceiling",
169            signature: "ceiling()",
170            category: Math,
171        },
172        FunctionInfo {
173            name: "children",
174            signature: "children()",
175            category: Tree,
176        },
177        FunctionInfo {
178            name: "coalesce",
179            signature: "coalesce(value, ...)",
180            category: Filtering,
181        },
182        FunctionInfo {
183            name: "combine",
184            signature: "combine(other)",
185            category: Combining,
186        },
187        FunctionInfo {
188            name: "comparable",
189            signature: "comparable(other)",
190            category: Other,
191        },
192        FunctionInfo {
193            name: "contains",
194            signature: "contains(substring)",
195            category: String,
196        },
197        FunctionInfo {
198            name: "convertsToBoolean",
199            signature: "convertsToBoolean()",
200            category: Conversion,
201        },
202        FunctionInfo {
203            name: "convertsToDate",
204            signature: "convertsToDate()",
205            category: Conversion,
206        },
207        FunctionInfo {
208            name: "convertsToDateTime",
209            signature: "convertsToDateTime()",
210            category: Conversion,
211        },
212        FunctionInfo {
213            name: "convertsToDecimal",
214            signature: "convertsToDecimal()",
215            category: Conversion,
216        },
217        FunctionInfo {
218            name: "convertsToInteger",
219            signature: "convertsToInteger()",
220            category: Conversion,
221        },
222        FunctionInfo {
223            name: "convertsToLong",
224            signature: "convertsToLong()",
225            category: Conversion,
226        },
227        FunctionInfo {
228            name: "convertsToQuantity",
229            signature: "convertsToQuantity([unit])",
230            category: Conversion,
231        },
232        FunctionInfo {
233            name: "convertsToString",
234            signature: "convertsToString()",
235            category: Conversion,
236        },
237        FunctionInfo {
238            name: "convertsToTime",
239            signature: "convertsToTime()",
240            category: Conversion,
241        },
242        FunctionInfo {
243            name: "count",
244            signature: "count()",
245            category: Existence,
246        },
247        FunctionInfo {
248            name: "decode",
249            signature: "decode(format)",
250            category: String,
251        },
252        FunctionInfo {
253            name: "defineVariable",
254            signature: "defineVariable(name, [expr])",
255            category: Utility,
256        },
257        FunctionInfo {
258            name: "descendants",
259            signature: "descendants()",
260            category: Tree,
261        },
262        FunctionInfo {
263            name: "difference",
264            signature: "difference(value, precision)",
265            category: Datetime,
266        },
267        FunctionInfo {
268            name: "distinct",
269            signature: "distinct()",
270            category: Existence,
271        },
272        FunctionInfo {
273            name: "duration",
274            signature: "duration(value, precision)",
275            category: Datetime,
276        },
277        FunctionInfo {
278            name: "empty",
279            signature: "empty()",
280            category: Existence,
281        },
282        FunctionInfo {
283            name: "encode",
284            signature: "encode(format)",
285            category: String,
286        },
287        FunctionInfo {
288            name: "endsWith",
289            signature: "endsWith(suffix)",
290            category: String,
291        },
292        FunctionInfo {
293            name: "escape",
294            signature: "escape(target)",
295            category: String,
296        },
297        FunctionInfo {
298            name: "exclude",
299            signature: "exclude(other)",
300            category: Subsetting,
301        },
302        FunctionInfo {
303            name: "exists",
304            signature: "exists([criteria])",
305            category: Existence,
306        },
307        FunctionInfo {
308            name: "exp",
309            signature: "exp()",
310            category: Math,
311        },
312        FunctionInfo {
313            name: "extension",
314            signature: "extension(url)",
315            category: Tree,
316        },
317        FunctionInfo {
318            name: "first",
319            signature: "first()",
320            category: Subsetting,
321        },
322        FunctionInfo {
323            name: "floor",
324            signature: "floor()",
325            category: Math,
326        },
327        FunctionInfo {
328            name: "getReferenceKey",
329            signature: "getReferenceKey([type])",
330            category: SqlOnFhir,
331        },
332        FunctionInfo {
333            name: "getResourceKey",
334            signature: "getResourceKey()",
335            category: SqlOnFhir,
336        },
337        FunctionInfo {
338            name: "hasValue",
339            signature: "hasValue()",
340            category: Existence,
341        },
342        FunctionInfo {
343            name: "highBoundary",
344            signature: "highBoundary([precision])",
345            category: Utility,
346        },
347        FunctionInfo {
348            name: "iif",
349            signature: "iif(criterion, trueResult, [otherwiseResult])",
350            category: Conversion,
351        },
352        FunctionInfo {
353            name: "indexOf",
354            signature: "indexOf(substring)",
355            category: String,
356        },
357        FunctionInfo {
358            name: "intersect",
359            signature: "intersect(other)",
360            category: Subsetting,
361        },
362        FunctionInfo {
363            name: "isDistinct",
364            signature: "isDistinct()",
365            category: Existence,
366        },
367        FunctionInfo {
368            name: "join",
369            signature: "join([separator])",
370            category: String,
371        },
372        FunctionInfo {
373            name: "last",
374            signature: "last()",
375            category: Subsetting,
376        },
377        FunctionInfo {
378            name: "lastIndexOf",
379            signature: "lastIndexOf(substring)",
380            category: String,
381        },
382        FunctionInfo {
383            name: "length",
384            signature: "length()",
385            category: String,
386        },
387        FunctionInfo {
388            name: "ln",
389            signature: "ln()",
390            category: Math,
391        },
392        FunctionInfo {
393            name: "log",
394            signature: "log(base)",
395            category: Math,
396        },
397        FunctionInfo {
398            name: "lowBoundary",
399            signature: "lowBoundary([precision])",
400            category: Utility,
401        },
402        FunctionInfo {
403            name: "lower",
404            signature: "lower()",
405            category: String,
406        },
407        FunctionInfo {
408            name: "matches",
409            signature: "matches(regex, [flags])",
410            category: String,
411        },
412        FunctionInfo {
413            name: "matchesFull",
414            signature: "matchesFull(regex, [flags])",
415            category: String,
416        },
417        FunctionInfo {
418            name: "max",
419            signature: "max()",
420            category: Math,
421        },
422        FunctionInfo {
423            name: "memberOf",
424            signature: "memberOf(valueSet)",
425            category: Terminology,
426        },
427        FunctionInfo {
428            name: "min",
429            signature: "min()",
430            category: Math,
431        },
432        FunctionInfo {
433            name: "not",
434            signature: "not()",
435            category: Boolean,
436        },
437        FunctionInfo {
438            name: "now",
439            signature: "now()",
440            category: Utility,
441        },
442        FunctionInfo {
443            name: "ofType",
444            signature: "ofType(type)",
445            category: Filtering,
446        },
447        FunctionInfo {
448            name: "power",
449            signature: "power(exponent)",
450            category: Math,
451        },
452        FunctionInfo {
453            name: "precision",
454            signature: "precision()",
455            category: Utility,
456        },
457        FunctionInfo {
458            name: "repeat",
459            signature: "repeat(projection)",
460            category: Filtering,
461        },
462        FunctionInfo {
463            name: "repeatAll",
464            signature: "repeatAll(projection)",
465            category: Filtering,
466        },
467        FunctionInfo {
468            name: "replace",
469            signature: "replace(pattern, substitution)",
470            category: String,
471        },
472        FunctionInfo {
473            name: "replaceMatches",
474            signature: "replaceMatches(regex, substitution, [flags])",
475            category: String,
476        },
477        FunctionInfo {
478            name: "resolve",
479            signature: "resolve()",
480            category: Other,
481        },
482        FunctionInfo {
483            name: "round",
484            signature: "round([precision])",
485            category: Math,
486        },
487        FunctionInfo {
488            name: "select",
489            signature: "select(projection)",
490            category: Filtering,
491        },
492        FunctionInfo {
493            name: "single",
494            signature: "single()",
495            category: Subsetting,
496        },
497        FunctionInfo {
498            name: "skip",
499            signature: "skip(num)",
500            category: Subsetting,
501        },
502        FunctionInfo {
503            name: "sort",
504            signature: "sort([criteria])",
505            category: Filtering,
506        },
507        FunctionInfo {
508            name: "split",
509            signature: "split(separator)",
510            category: String,
511        },
512        FunctionInfo {
513            name: "sqrt",
514            signature: "sqrt()",
515            category: Math,
516        },
517        FunctionInfo {
518            name: "startsWith",
519            signature: "startsWith(prefix)",
520            category: String,
521        },
522        FunctionInfo {
523            name: "subsetOf",
524            signature: "subsetOf(other)",
525            category: Existence,
526        },
527        FunctionInfo {
528            name: "substring",
529            signature: "substring(start, [length])",
530            category: String,
531        },
532        FunctionInfo {
533            name: "sum",
534            signature: "sum()",
535            category: Math,
536        },
537        FunctionInfo {
538            name: "supersetOf",
539            signature: "supersetOf(other)",
540            category: Existence,
541        },
542        FunctionInfo {
543            name: "tail",
544            signature: "tail()",
545            category: Subsetting,
546        },
547        FunctionInfo {
548            name: "take",
549            signature: "take(num)",
550            category: Subsetting,
551        },
552        FunctionInfo {
553            name: "timeOfDay",
554            signature: "timeOfDay()",
555            category: Utility,
556        },
557        FunctionInfo {
558            name: "toBoolean",
559            signature: "toBoolean()",
560            category: Conversion,
561        },
562        FunctionInfo {
563            name: "toChars",
564            signature: "toChars()",
565            category: String,
566        },
567        FunctionInfo {
568            name: "toDate",
569            signature: "toDate([format])",
570            category: Conversion,
571        },
572        FunctionInfo {
573            name: "toDateTime",
574            signature: "toDateTime([format])",
575            category: Conversion,
576        },
577        FunctionInfo {
578            name: "today",
579            signature: "today()",
580            category: Utility,
581        },
582        FunctionInfo {
583            name: "toDecimal",
584            signature: "toDecimal()",
585            category: Conversion,
586        },
587        FunctionInfo {
588            name: "toInteger",
589            signature: "toInteger()",
590            category: Conversion,
591        },
592        FunctionInfo {
593            name: "toLong",
594            signature: "toLong()",
595            category: Conversion,
596        },
597        FunctionInfo {
598            name: "toQuantity",
599            signature: "toQuantity([unit])",
600            category: Conversion,
601        },
602        FunctionInfo {
603            name: "toString",
604            signature: "toString([format])",
605            category: Conversion,
606        },
607        FunctionInfo {
608            name: "toTime",
609            signature: "toTime()",
610            category: Conversion,
611        },
612        FunctionInfo {
613            name: "trace",
614            signature: "trace(name, [projection])",
615            category: Utility,
616        },
617        FunctionInfo {
618            name: "trim",
619            signature: "trim()",
620            category: String,
621        },
622        FunctionInfo {
623            name: "truncate",
624            signature: "truncate()",
625            category: Math,
626        },
627        FunctionInfo {
628            name: "type",
629            signature: "type()",
630            category: Types,
631        },
632        FunctionInfo {
633            name: "unescape",
634            signature: "unescape(target)",
635            category: String,
636        },
637        FunctionInfo {
638            name: "union",
639            signature: "union(other)",
640            category: Combining,
641        },
642        FunctionInfo {
643            name: "upper",
644            signature: "upper()",
645            category: String,
646        },
647        FunctionInfo {
648            name: "where",
649            signature: "where(criteria)",
650            category: Filtering,
651        },
652    ];
653    CATALOG
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659
660    #[test]
661    fn catalog_is_sorted_case_insensitively_by_name() {
662        let names: Vec<String> = builtin_functions()
663            .iter()
664            .map(|f| f.name.to_lowercase())
665            .collect();
666        let mut sorted = names.clone();
667        sorted.sort();
668        assert_eq!(
669            names, sorted,
670            "builtin_functions() must be sorted alphabetically (case-insensitively) by name"
671        );
672    }
673
674    #[test]
675    fn catalog_has_no_duplicate_names() {
676        let mut names: Vec<&str> = builtin_functions().iter().map(|f| f.name).collect();
677        let original_len = names.len();
678        names.sort_unstable();
679        names.dedup();
680        assert_eq!(names.len(), original_len, "duplicate function name found");
681    }
682
683    #[test]
684    fn every_category_string_is_from_the_closed_set() {
685        const ALLOWED: &[&str] = &[
686            "existence",
687            "filtering",
688            "subsetting",
689            "combining",
690            "conversion",
691            "string",
692            "math",
693            "tree",
694            "utility",
695            "boolean",
696            "datetime",
697            "types",
698            "sql-on-fhir",
699            "terminology",
700            "other",
701        ];
702        for f in builtin_functions() {
703            assert!(
704                ALLOWED.contains(&f.category.as_str()),
705                "function {:?} has category {:?} outside the closed set",
706                f.name,
707                f.category.as_str()
708            );
709        }
710    }
711}