fhirpath 0.2.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '0759fa82ecea31aef7e8fb5ffc0c8a1fc023d834ffe49eb296d706bf42ff7ed8'
4
+ data.tar.gz: 0156774a1f73b8099001478f57704e8ee969738454f1019a09d398b9420aa64d
5
+ SHA512:
6
+ metadata.gz: 0cd68f2b5a80fd409a066965422a067d1a0664a2bd6c65c215b82c19655a8064ec4da47a54bc4b6767d0f65f7f38ee997815d63796a9097d43df3efcfe6ad448
7
+ data.tar.gz: 42a9413ad029f7a5e77bbfd2e796db4d37debd2152f3f9f1c3e28d4a74100027ef56abb6a63cd2a8bc383c83519546f6d18949f8db38a3dccf73d43aa7b4747b
data/.rubocop.yml ADDED
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 3.2
5
+ NewCops: enable
6
+ Exclude:
7
+ - "vendor/**/*"
8
+
9
+ Style/ArgumentsForwarding:
10
+ Enabled: false
11
+
12
+ Style/SuperArguments:
13
+ Enabled: false
14
+
15
+ Style/ReduceToHash:
16
+ Enabled: false
17
+
18
+ Style/SelectByKind:
19
+ Enabled: false
20
+
21
+ Style/FormatStringToken:
22
+ Enabled: false
23
+
24
+ # The project keeps development dependencies grouped by purpose in Gemfile.
25
+ Bundler/OrderedGems:
26
+ Enabled: false
27
+
28
+ Style/Documentation:
29
+ Enabled: false
30
+
31
+ Metrics/AbcSize:
32
+ Enabled: false
33
+
34
+ Metrics/MethodLength:
35
+ Enabled: false
36
+
37
+ # The public API and context intentionally expose the independently injectable
38
+ # architectural boundaries described in docs/architecture.md.
39
+ Metrics/ParameterLists:
40
+ Exclude:
41
+ - "lib/fhirpath.rb"
42
+ - "lib/fhirpath/evaluation_context.rb"
43
+ - "lib/fhirpath/conformance/importer.rb"
44
+
45
+ # Lexer/parser and evaluator are intentionally cohesive in this first scaffold;
46
+ # future slices can split them once their semantics stabilize.
47
+ Metrics/ClassLength:
48
+ Exclude:
49
+ - "lib/fhirpath/parser.rb"
50
+ - "lib/fhirpath/evaluator.rb"
51
+ - "test/foundation_test.rb"
52
+ - "test/parser_hardening_test.rb"
53
+ - "test/subsetting_functions_test.rb"
54
+ - "lib/fhirpath/conformance/importer.rb"
55
+ - "test/conformance_pipeline_test.rb"
56
+
57
+ Metrics/CyclomaticComplexity:
58
+ Exclude:
59
+ - "lib/fhirpath/parser.rb"
60
+ - "lib/fhirpath/evaluator.rb"
61
+ - "lib/fhirpath/vector_runner.rb"
62
+
63
+ Metrics/PerceivedComplexity:
64
+ Exclude:
65
+ - "lib/fhirpath/parser.rb"
66
+ - "lib/fhirpath/evaluator.rb"
67
+
68
+ # The conformance test deliberately exercises many cases in one readable block
69
+ # for pass/defect classification; split out per-case helpers later if needed.
70
+ Metrics/BlockLength:
71
+ Exclude:
72
+ - "test/conformance_pipeline_test.rb"
73
+ - "fhirpath.gemspec"
data/CHANGELOG.md ADDED
@@ -0,0 +1,77 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The project is pre-1.0; the API and supported behavior may change between releases.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.2.0.pre1] - 2026-09-05
8
+ - Add the `sum()`, `avg()`, `max()`, and `min()` aggregate functions to the
9
+ standard registry. These are FHIRPath 3.0.0 STU3 aggregate additions
10
+ (published 2026-07-28) and the first STU3-subset functions this project
11
+ ships; they are absent from normative FHIRPath 2.0.0 and from the 3.0.0
12
+ ballot. Semantics follow the published 3.0.0 text where it applies: empty
13
+ input returns the empty collection; non-numeric items raise `TypeError` with
14
+ code `expected_number`; `avg()` converts Integer items to Decimal before
15
+ dividing and always yields a Decimal; and `max()`/`min()` reuse the
16
+ comparison-operator semantics so numerics compare across Integer/Decimal and
17
+ strings order lexicographically, with incompatible item types raising
18
+ `TypeError` with code `incompatible_comparison`. The 3.0.0 text requires all
19
+ items to be the same type; this slice records a documented deviation for
20
+ numeric input instead of an exception: mixed Integer/Decimal input is summed
21
+ in Decimal, matching the engine's numeric promotion in arithmetic and
22
+ equality (`1 = 1.0` and `1 + 2.5` both hold), while all-Integer input keeps
23
+ an Integer `sum()` result. The subset ships by default in the standard
24
+ registry as a declared exception: `Capability.current` keeps `fhirpath`
25
+ `2.0.0` and reports the marker `stu3-aggregate-functions` in `trial_use`;
26
+ `docs/support-matrix.md`, `docs/api.md`, and the feature matrix document the
27
+ exception. `count()` (normative FHIRPath 2.0.0, existence functions)
28
+ already existed and now has focused expression tests and checked-in vectors
29
+ alongside the family. No inputs are mutated and every result is a fresh
30
+ frozen `Collection`; the general-purpose `aggregate()` function remains
31
+ deferred.
32
+
33
+ - Resolve the FHIR logical type of a choice value for the `is`/`as` type
34
+ operators when a model provider is active. `ModelProvider` gains an optional
35
+ `property_logical_type` accessor (safe `nil` default on the base class and
36
+ `PlainModel`); the R4 provider reports the resolved variant's declared type
37
+ from its choice metadata (`valueQuantity` -> `Quantity`, `valueString` ->
38
+ `string`). Navigation records that type per item on the produced collection
39
+ without wrapping or altering the value, so with `model: :r4`,
40
+ `Observation.value is Quantity` yields `[true]` and
41
+ `Observation.value as Quantity` passes the value through unchanged (an empty
42
+ collection when the cast fails). Primitive built-in type tests, empty
43
+ in/empty out for absent choices, and the plain-model default are unchanged.
44
+ `Observation is Resource`/`DomainResource` style tests and `ofType()` remain
45
+ deferred.
46
+ - Add the `tail()`, `take(n)`, and `skip(n)` member functions (FHIRPath 2.0.0, subsetting functions; §5.3.5–5.3.7): `tail()` returns all but the first item and is empty for empty/singleton input; `take(n)` returns the first `min(n, size)` items, empty for `n <= 0` or empty input, and the full collection when `n` exceeds the size; `skip(n)` returns the items after the first `n`, the full collection for `n <= 0`, and empty when no items remain. `take`/`skip` require an integer `n` and raise `TypeError` with code `expected_integer` for a non-integer argument.
47
+ - Add a dependency-free FHIR R4 model provider selectable with `model: :r4`.
48
+ It maps the R4 `Observation.value[x]` JSON variants `valueQuantity` and
49
+ `valueString` to logical `Observation.value`; an absent choice evaluates to
50
+ the empty collection. The provider is exposed as
51
+ `FHIRPath::FHIR::R4::ModelProvider` and advertised by the capability API.
52
+ Resource-type and release matching accept symbol/string and any-case forms.
53
+ - Add the `last()` member function (FHIRPath 2.0.0, existence/navigation functions): it returns the last item in the input collection, an empty collection when the input is empty, and requires zero arguments.
54
+ - Harden the external constant boundary with an injectable `ConstantProvider`, predictable `UnknownConstantError` behavior when absent, generic redacted `HostError` messages without retaining constant-provider exceptions as public causes, and no implicit provider I/O; resolve, terminology, tracing, and cache behavior remain deferred.
55
+ - Correct numeric arithmetic with a zero right operand: `+`, `-`, `*` treat zero as a normal operand, while `/`, `div`, `mod` divide and a zero divisor returns an empty collection (never an error).
56
+ - Correct `union` (`|`) to eliminate duplicate values from both operands using `=` equality in first-seen order, including numerically-equal integers and decimals.
57
+ - Accept a finite JSON `Float` as a `Decimal` for comparison, equality, arithmetic, and the `is Decimal` type test; reject `NaN`/`Infinity` as non-numeric.
58
+ - Correct `contains` so a multi-item right operand raises a singleton-required error even when the searched collection is empty, and so an empty right operand yields an empty result.
59
+ - Correct `&` string concatenation to treat empty operands as empty strings, with regression coverage for both operators' empty-collection behavior.
60
+ - Correct parser precedence for relational, union, and type operators to match the FHIRPath grammar.
61
+ - Harden documentation, package verification, CI, coverage reporting, and contributor workflows.
62
+ - Add the release support matrix, exact package capability metadata, versioning policy, and gated RubyGems/GitHub publication workflow.
63
+ - Declare the project under the MIT License and encode the license in gem metadata.
64
+ - Bound parser recursion: excessively nested expressions (parentheses, unary chains, function calls, indexers, and deep AST-building flat expressions) now raise `FHIRPath::ParseError` with code `nesting_depth_exceeded` instead of letting `SystemStackError` escape the `FHIRPath::Error` boundary.
65
+ - Stop freezing the caller-owned source string: `parse`/`compile` retain an internal frozen snapshot of the expression, so mutating the caller's String afterwards does not affect the compiled program.
66
+
67
+ ## [0.1.0.pre1] - 2026-09-04
68
+
69
+ - Added the Ruby-native parser, immutable AST, collection-first evaluator, plain model boundary, structured errors, capability object, and function registry.
70
+ - Added path navigation, literals, arithmetic, selected comparisons, Boolean operators, indexers, filtering, existence functions, and reusable compiled expressions.
71
+ - Added collection-aware equality/equivalence, normalized string and decimal equivalence, empty/not/all functions, string addition, union, membership, primitive type operators, comments, and strict string-escape handling.
72
+ - Added a small JSONL differential-vector runner and six checked-in compatibility vectors.
73
+ - Documented the prototype scope, architecture, limitations, and staged conformance plan.
74
+
75
+ [Unreleased]: https://github.com/niccoreyes/fhirpath-ruby/compare/v0.2.0.pre1...HEAD
76
+ [0.2.0.pre1]: https://github.com/niccoreyes/fhirpath-ruby/releases/tag/v0.2.0.pre1
77
+ [0.1.0.pre1]: https://github.com/niccoreyes/fhirpath-ruby/releases/tag/v0.1.0.pre1
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,67 @@
1
+ # Contributing to FHIRPath for Ruby
2
+
3
+ Thanks for helping improve the Ruby implementation. It is a pre-1.0 project with a deliberately narrow supported slice, so correctness and honest scope are more important than adding a large number of names quickly.
4
+
5
+ ## Development setup
6
+
7
+ Requirements:
8
+
9
+ - Ruby 3.2 or 3.3 (the supported CI matrix);
10
+ - Bundler; and
11
+ - Git.
12
+
13
+ From a fresh checkout:
14
+
15
+ ```sh
16
+ bundle install
17
+ bundle exec rake test
18
+ bundle exec rubocop
19
+ bundle exec rake vectors
20
+ bundle exec rake build
21
+ bundle exec ./script/verify_gem_install.sh pkg/fhirpath-*.gem
22
+ ```
23
+
24
+ Optional coverage:
25
+
26
+ ```sh
27
+ COVERAGE=1 bundle exec rake test
28
+ bundle exec ruby script/check_coverage.rb coverage/summary.json
29
+ ```
30
+
31
+ The repository intentionally does not require Python for its runtime or vector workflow.
32
+
33
+ ## Change workflow
34
+
35
+ 1. Open or find an issue describing the behavior or documentation change.
36
+ 2. Read `docs/architecture.md`, `docs/feature-matrix.md`, and `docs/conformance.md` before changing evaluator semantics.
37
+ 3. For behavior changes, write a focused failing test first (RED), implement the smallest change (GREEN), then run the complete checks above.
38
+ 4. Add or update a vector when behavior has an independent reference or specification case.
39
+ 5. Update the README, API docs, feature matrix, limitations, and changelog when the public contract changes.
40
+ 6. Keep parser, evaluator, model, and host boundaries explicit. Do not add expression-controlled Ruby method dispatch, global evaluator state, or network I/O.
41
+ 7. Keep commits focused and do not include build output, credentials, local state, or unrelated work.
42
+
43
+ ## Pull requests
44
+
45
+ A useful pull request explains:
46
+
47
+ - the user-visible behavior and the FHIRPath rule it implements;
48
+ - tests added, including expected RED-to-GREEN behavior where relevant;
49
+ - vector/conformance evidence and any host-dependent exclusions;
50
+ - supported Ruby versions exercised; and
51
+ - documentation or compatibility impact.
52
+
53
+ Before requesting review, verify `git diff --check`, `bundle exec rake test`, `bundle exec rubocop`, `bundle exec rake vectors`, `bundle exec rake build`, and the gem-install smoke test. Do not claim complete FHIRPath conformance unless the official suite and release evidence support that claim.
54
+
55
+ For release work, also review [`docs/support-matrix.md`](docs/support-matrix.md),
56
+ [`docs/release-checklist.md`](docs/release-checklist.md), and
57
+ [`docs/releasing.md`](docs/releasing.md). Version changes must update
58
+ `lib/fhirpath/version.rb` and the matching `CHANGELOG.md` section; do not push a
59
+ release tag from a dirty or unreviewed checkout.
60
+
61
+ ## Scope and licensing
62
+
63
+ The repository is licensed under the [MIT License](LICENSE), but remains
64
+ pre-release and does not claim complete FHIRPath conformance. Do not represent
65
+ it as a complete or production-ready engine; document compatibility evidence
66
+ and limitations in every behavior change. See `SECURITY.md` for vulnerability
67
+ reports.
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ # Keep development tooling out of the published gem metadata and use a current
8
+ # RuboCop line compatible with the supported Ruby versions.
9
+ gem 'benchmark', '~> 0.4'
10
+ gem 'minitest', '~> 6.0'
11
+ gem 'rake', '~> 13.0'
12
+ gem 'rubocop', '~> 1.90.0'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nicco Reyes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # FHIRPath for Ruby
2
+
3
+ [![CI](https://github.com/niccoreyes/fhirpath-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/niccoreyes/fhirpath-ruby/actions/workflows/ci.yml)
4
+ [![Release](https://github.com/niccoreyes/fhirpath-ruby/actions/workflows/release.yml/badge.svg)](https://github.com/niccoreyes/fhirpath-ruby/actions/workflows/release.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/niccoreyes/fhirpath-ruby/blob/main/LICENSE)
6
+ [![Ruby](https://img.shields.io/badge/Ruby-3.2%20%7C%203.3-blue.svg)](https://github.com/niccoreyes/fhirpath-ruby/blob/main/.github/workflows/ci.yml)
7
+
8
+ A Ruby-native implementation of the [HL7 FHIRPath](https://hl7.org/fhirpath/) expression language.
9
+
10
+ This repository is an intentionally small, pre-release implementation. It provides a tested compatibility slice with stable boundaries for the public API, lexer/parser, immutable AST, collections, evaluation context, plain-model navigation, a dependency-free FHIR R4 model adapter, values, structured errors, and function registration. It does not claim complete FHIRPath conformance or complete FHIR release-model support.
11
+
12
+ ## Status at a glance
13
+
14
+ - Version: `0.2.0.pre1`
15
+ - Normative language target: FHIRPath `2.0.0`
16
+ - Ruby support policy: Ruby `3.2` and `3.3` are tested in CI; newer Ruby versions are supported only after CI coverage is added.
17
+ - Release status: pre-release; not published to RubyGems.
18
+ - License: [MIT](LICENSE).
19
+
20
+ The exact release-facing target, capability identifiers, Ruby support matrix,
21
+ and host/model limitations are maintained in the [release support matrix](docs/support-matrix.md).
22
+
23
+ ## Installation
24
+
25
+ The gem is not yet published. To use a checkout:
26
+
27
+ ```sh
28
+ git clone https://github.com/niccoreyes/fhirpath-ruby.git
29
+ cd fhirpath-ruby
30
+ bundle install
31
+ bundle exec rake test
32
+ ```
33
+
34
+ Once a release is published, the intended consumer workflow will be:
35
+
36
+ ```ruby
37
+ # Gemfile
38
+ gem "fhirpath"
39
+ ```
40
+
41
+ The pre-release status and incomplete conformance scope still apply; review the
42
+ documented limitations before using this implementation in production.
43
+
44
+ ## Quick start
45
+
46
+ ```ruby
47
+ require "fhirpath"
48
+
49
+ patient = {
50
+ "resourceType" => "Patient",
51
+ "name" => [
52
+ { "use" => "official", "family" => "Lovelace", "given" => ["Ada", "Augusta"] },
53
+ { "use" => "nickname", "given" => ["Addie"] }
54
+ ]
55
+ }
56
+
57
+ FHIRPath.evaluate(patient, "Patient.name.where(use = 'official').given").to_a
58
+ # => ["Ada", "Augusta"]
59
+
60
+ FHIRPath.evaluate_first(patient, "Patient.name.family")
61
+ # => "Lovelace"
62
+ ```
63
+
64
+ FHIR R4 JSON can be selected explicitly through the versioned provider. The
65
+ adapter exposes the logical `Observation.value` property over R4 choice keys
66
+ such as `valueString` and `valueQuantity`:
67
+
68
+ ```ruby
69
+ observation = {
70
+ "resourceType" => "Observation",
71
+ "valueQuantity" => { "value" => 120, "unit" => "mmHg" }
72
+ }
73
+
74
+ FHIRPath.evaluate(observation, "Observation.value.value", model: :r4).to_a
75
+ # => [120]
76
+
77
+ # The choice variant carries its FHIR logical type, so `is`/`as` resolve
78
+ # against model metadata for the resolved value:
79
+ FHIRPath.evaluate(observation, "Observation.value is Quantity", model: :r4).to_a
80
+ # => [true]
81
+ FHIRPath.evaluate(observation, "Observation.value as Quantity", model: :r4).to_a
82
+ # => [{ "value" => 120, "unit" => "mmHg" }]
83
+ ```
84
+
85
+ The R4 adapter is dependency-free and does not perform Ruby method dispatch;
86
+ plain-model navigation remains the default. Its supported release and model
87
+ selection are visible through `FHIRPath::Capability.current` and
88
+ `FHIRPath.available_models`.
89
+
90
+ ## Public API
91
+
92
+ The API is intentionally Ruby-native rather than source-compatible with `fhirpath-py`:
93
+
94
+ ```ruby
95
+ FHIRPath.parse(expression, capability: FHIRPath::Capability.current)
96
+ FHIRPath.compile(expression, model: nil, capability: ..., functions: ...)
97
+ FHIRPath.evaluate(resource, expression, variables: {}, model: nil,
98
+ capability: ..., functions: ..., options: {}, host: nil)
99
+ FHIRPath.evaluate_first(resource, expression, variables: {}, model: nil,
100
+ capability: ..., functions: ..., options: {}, host: nil)
101
+ ```
102
+
103
+ A compiled expression is immutable and reusable:
104
+
105
+ ```ruby
106
+ program = FHIRPath.compile("Patient.name.family")
107
+ program.evaluate({ "resourceType" => "Patient", "name" => [{ "family" => "Lovelace" }] }).to_a
108
+ # => ["Lovelace"]
109
+ program.call({ "resourceType" => "Patient", "name" => [{ "family" => "Hopper" }] }).to_a
110
+ # => ["Hopper"]
111
+ ```
112
+
113
+ See [API reference](docs/api.md) for result, error, extension, and immutability contracts, and [architecture](docs/architecture.md) for the implementation boundaries.
114
+
115
+ ### Explicit host constants
116
+
117
+ External constant lookup is opt-in and stays behind an injected provider:
118
+
119
+ ```ruby
120
+ class TenantConstants < FHIRPath::ConstantProvider
121
+ def fetch(name, mode:, context:)
122
+ constants.fetch(name)
123
+ end
124
+
125
+ private
126
+
127
+ def constants
128
+ { 'tenant' => 'example' }
129
+ end
130
+ end
131
+
132
+ host = FHIRPath::HostServices.new(constant_provider: TenantConstants.new)
133
+ FHIRPath.evaluate({}, '%tenant', host: host).to_a
134
+ # => ["example"]
135
+ ```
136
+
137
+ `ConstantProvider#fetch` is the only boundary at which an embedding application may perform external work. The engine does not discover constants, read files, or make network requests. If no provider is supplied, an external constant raises `UnknownConstantError` with code `:unknown_constant`. Constant-provider failures raise a generic `HostError`; exceptions raised by the constant provider are not retained as public causes, and their messages are excluded from the public error serialization and `full_message`. `variables:` takes precedence over the provider, so explicitly supplied values do not trigger provider work.
138
+
139
+ ## Supported slice
140
+
141
+ The current tested slice includes:
142
+
143
+ - primitive string, Boolean, integer, decimal, and scientific-notation literals;
144
+ - empty and comma-separated collections;
145
+ - plain Ruby Hash/Array and simple object navigation, including resource-type roots such as `Patient`;
146
+ - dependency-free FHIR R4 model navigation selected with `model: :r4`, including the logical `Observation.value` choice property over `valueQuantity` and `valueString`;
147
+ - unary and numeric arithmetic (`+`, `-`, `*`, `/`, `div`, and `mod`), plus string `+` when both operands are strings; a zero divisor for `/`, `div`, `mod` returns an empty collection, while `+`, `-`, `*` treat zero as a normal operand;
148
+ - numeric/string relational comparison, collection-aware equality, equivalence, and empty-aware Boolean operators; a finite JSON `Float` is treated as a `Decimal`;
149
+ - union, string concatenation (`+` and `&`), membership (`in`/`contains`), and type operators (`is`/`as`); union removes duplicate values from both operands using `=` equality in first-seen order, and `in`/`contains` require a singleton operand; `is`/`as` test built-in primitive types directly and, with `model: :r4`, also resolve the FHIR logical type of a navigated choice value (for example `Observation.value is Quantity` over `valueQuantity`), returning the value unchanged on a successful `as` and the empty collection otherwise; and
150
+ - indexers with non-negative integer indexes;
151
+ - `where`, `select`, `first`, `last`, `tail`, `take`, `skip`, `exists`, `count`, `empty`, `not`, `all`, and Boolean aggregate functions;
152
+ - the FHIRPath 3.0.0 STU3 aggregate functions `sum()`, `avg()`, `max()`, and `min()` (empty input yields the empty collection; `sum`/`avg` require numeric items and `max`/`min` compare numeric and string items with comparison-operator semantics), shipped as a declared, documented exception: `Capability.current` keeps the FHIRPath 2.0.0 target and reports this subset in `trial_use` under the marker `stu3-aggregate-functions`;
153
+ - `$this`, `$index`, and `$total` focus variables;
154
+ - explicitly supplied external constants through `variables:` or an injected `FHIRPath::HostServices` constant provider; and
155
+ - immutable parse/compile boundaries with structured errors and source spans.
156
+
157
+ The [feature matrix](docs/feature-matrix.md) is the executable-scope companion to this list, and the [release support matrix](docs/support-matrix.md) is the publication contract. If a behavior is not listed as supported, callers should handle a specific `FHIRPath::Error` rather than assume permissive fallback.
158
+
159
+ ## Explicit limitations
160
+
161
+ This is not yet a complete FHIRPath engine. The following remain deferred or host-dependent:
162
+
163
+ - complete FHIRPath 2.0 conformance; the checked-in importer covers only the pinned official subset;
164
+ - broader FHIR R4 metadata such as primitive extensions, resource-level type tests (`Observation is Resource`/`DomainResource`), `ofType()`, and FHIR R5 model adapters; FHIR R4 `is`/`as` over a resolved choice value's logical type (e.g. `Quantity`) is supported, but terminology and `resolve()` remain host-dependent; and
165
+ - date/time and quantity/UCUM values;
166
+ - advanced conversion, math, string, regular-expression, and navigation functions, and the general-purpose `aggregate()` function;
167
+ - complex literals and additional standard value types;
168
+ - standard environment variables beyond explicitly supplied external constants;
169
+ - FHIRPath 3.0 STU3 features beyond the shipped `sum`/`avg`/`max`/`min` aggregate functions; capability recognition does not enable them silently;
170
+ - network I/O from pure evaluation and global evaluator state; and
171
+ - production support guarantees, until the support matrix and release gates are complete.
172
+
173
+ Unsupported operations raise `UnsupportedFeatureError`, `UnknownFunctionError`, or another specific `FHIRPath::Error`. Malformed or trailing source raises `ParseError`; the parser does not accept a valid prefix and silently ignore trailing input.
174
+
175
+ ## Development and verification
176
+
177
+ Install development dependencies and run the complete local checks:
178
+
179
+ ```sh
180
+ bundle install
181
+ bundle exec rake test
182
+ bundle exec rubocop
183
+ bundle exec rake vectors
184
+ bundle exec rake build
185
+ bundle exec ./script/verify_gem_install.sh pkg/fhirpath-*.gem
186
+ ```
187
+
188
+ The optional vector workflow is deterministic and does not require Python:
189
+
190
+ ```sh
191
+ bundle exec ruby script/run_vectors.rb conformance/core.jsonl
192
+ ```
193
+
194
+ It reports `pass`, `defect`, `unsupported`, `host-dependent`, and `not-run` separately. These hand-authored JSONL vectors are compatibility evidence, not a replacement for the official HL7 shared suite. See [Conformance workflow](docs/conformance.md).
195
+
196
+ Coverage is opt-in and uses Ruby's standard `Coverage` library:
197
+
198
+ ```sh
199
+ COVERAGE=1 bundle exec rake test
200
+ bundle exec ruby script/check_coverage.rb coverage/summary.json
201
+ ```
202
+
203
+ For a clean-checkout reproduction, use the exact commands in [CONTRIBUTING.md](CONTRIBUTING.md). CI runs the supported Ruby matrix, tests, RuboCop, vectors, package build, gem-install smoke test, and coverage report generation.
204
+
205
+ ## Contributing
206
+
207
+ Please read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request. New language behavior should be delivered as a small vertical slice: add a focused failing test, implement the smallest change, run the complete checks, and update the feature matrix and limitations when scope changes.
208
+
209
+ Security reports should follow [SECURITY.md](SECURITY.md). Release readiness,
210
+ versioning, publication, and remaining conformance gates are recorded in
211
+ [`docs/release-checklist.md`](docs/release-checklist.md) and
212
+ [`docs/releasing.md`](docs/releasing.md).
213
+
214
+ ## License
215
+
216
+ This project is licensed under the [MIT License](LICENSE). The gem is still a
217
+ pre-release and does not claim complete FHIRPath conformance; those limitations
218
+ are independent of the license.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |task|
7
+ task.libs << 'lib'
8
+ task.libs << 'test'
9
+ task.pattern = 'test/**/*_test.rb'
10
+ task.verbose = true
11
+ end
12
+
13
+ desc 'Run the checked-in compatibility vectors'
14
+ task :vectors do
15
+ ruby 'script/run_vectors.rb', 'conformance/core.jsonl'
16
+ ruby 'script/run_vectors.rb', 'conformance/r4.jsonl'
17
+ end
18
+
19
+ task default: :test
data/SECURITY.md ADDED
@@ -0,0 +1,20 @@
1
+ # Security policy
2
+
3
+ ## Scope
4
+
5
+ This project evaluates expressions against caller-supplied Ruby data. The supported pure-evaluation boundary does not perform network I/O and the plain model intentionally does not dispatch arbitrary Ruby methods selected by an expression.
6
+
7
+ Do not provide production credentials, personal data, or live clinical data in issues, vectors, fixtures, or pull requests.
8
+
9
+ ## Reporting a vulnerability
10
+
11
+ Please do not disclose an exploitable vulnerability in a public issue. Use GitHub's private vulnerability reporting or security-advisory channel for this repository when available. If that channel is unavailable, contact the repository owner through GitHub and provide only the minimum reproducible details needed to triage the issue.
12
+
13
+ Reports should include the affected version/commit, Ruby version, expression and fixture reduced to synthetic data, impact, and a suggested mitigation if known. Please allow time for investigation before public disclosure.
14
+
15
+ ## Supported versions
16
+
17
+ The project is MIT-licensed but pre-release. Security fixes are evaluated
18
+ against the current `main` branch and the latest published version, if any.
19
+ The gem is not yet published as a production release; see
20
+ `docs/release-checklist.md` for the remaining release gates.
@@ -0,0 +1,39 @@
1
+ {"id":"collection-equality-001","target":"2.0.0","expression":"left = same","resource":{"left":[1,2],"same":[1,2]},"variables":{},"expected":[true],"origin":{"suite":"fhirpath-py","commit":"19f6316","case":"manual"}}
2
+ {"id":"collection-equivalence-001","target":"2.0.0","expression":"left ~ permuted","resource":{"left":[1,2],"permuted":[2,1]},"variables":{},"expected":[true],"origin":{"suite":"fhirpath-py","commit":"19f6316","case":"manual"}}
3
+ {"id":"string-equivalence-001","target":"2.0.0","expression":"'ab c' ~ 'Ab C'","resource":{},"variables":{},"expected":[true],"origin":{"suite":"fhirpath-py","commit":"19f6316","case":"manual"}}
4
+ {"id":"empty-function-001","target":"2.0.0","expression":"{}.empty()","resource":{},"variables":{},"expected":[true],"origin":{"suite":"fhirpath-py","commit":"19f6316","case":"manual"}}
5
+ {"id":"string-addition-001","target":"2.0.0","expression":"'a' + 'b'","resource":{},"variables":{},"expected":["ab"],"origin":{"suite":"fhirpath-py","commit":"19f6316","case":"manual"}}
6
+ {"id":"type-operator-001","target":"2.0.0","expression":"1 as integer","resource":{},"variables":{},"expected":[1],"origin":{"suite":"fhirpath-py","commit":"19f6316","case":"manual"}}
7
+ {"id":"string-concat-empty-001","target":"2.0.0","expression":"'a' & {}","resource":{},"variables":{},"expected":["a"],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"string-concatenation"}}
8
+ {"id":"type-union-precedence-001","target":"2.0.0","expression":"1 as integer | 2","resource":{},"variables":{},"expected":[1,2],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"operator-precedence"}}
9
+ {"id":"union-dedupe-001","target":"2.0.0","expression":"(1 | 1) | 2","resource":{},"variables":{},"expected":[1,2],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"union-distinct"}}
10
+ {"id":"union-numeric-equality-001","target":"2.0.0","expression":"(1 | 1.0)","resource":{},"variables":{},"expected":[1],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"union-numeric-equality"}}
11
+ {"id":"division-by-zero-empty-001","target":"2.0.0","expression":"5 div 0","resource":{},"variables":{},"expected":[],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"division-by-zero"}}
12
+ {"id":"membership-empty-001","target":"2.0.0","expression":"1 in {}","resource":{},"variables":{},"expected":[false],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"membership-empty"}}
13
+ {"id":"membership-both-empty-001","target":"2.0.0","expression":"{} in {}","resource":{},"variables":{},"expected":[],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"membership-empty-singleton"}}
14
+ {"id":"contains-both-empty-001","target":"2.0.0","expression":"{} contains {}","resource":{},"variables":{},"expected":[],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"membership-empty-singleton"}}
15
+ {"id":"float-as-decimal-001","target":"2.0.0","expression":"value > 1","resource":{"value":1.5},"variables":{},"expected":[true],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"decimal-literal"}}
16
+ {"id":"last-function-001","target":"2.0.0","expression":"(1 | 2 | 3).last()","resource":{},"variables":{},"expected":[3],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"last-function"}}
17
+ {"id":"last-function-empty-001","target":"2.0.0","expression":"{}.last()","resource":{},"variables":{},"expected":[],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"last-function"}}
18
+ {"id":"tail-function-001","target":"2.0.0","expression":"(1 | 2 | 3).tail()","resource":{},"variables":{},"expected":[2,3],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"tail-function"}}
19
+ {"id":"tail-function-empty-001","target":"2.0.0","expression":"{}.tail()","resource":{},"variables":{},"expected":[],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"tail-function"}}
20
+ {"id":"take-function-001","target":"2.0.0","expression":"(1 | 2 | 3).take(2)","resource":{},"variables":{},"expected":[1,2],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"take-function"}}
21
+ {"id":"take-function-empty-001","target":"2.0.0","expression":"{}.take(2)","resource":{},"variables":{},"expected":[],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"take-function"}}
22
+ {"id":"skip-function-001","target":"2.0.0","expression":"(1 | 2 | 3).skip(1)","resource":{},"variables":{},"expected":[2,3],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"skip-function"}}
23
+ {"id":"skip-function-empty-001","target":"2.0.0","expression":"{}.skip(2)","resource":{},"variables":{},"expected":[],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"skip-function"}}
24
+ {"id":"count-function-001","target":"2.0.0","expression":"(1 | 2 | 3).count()","resource":{},"variables":{},"expected":[3],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"count-function"}}
25
+ {"id":"count-function-empty-001","target":"2.0.0","expression":"{}.count()","resource":{},"variables":{},"expected":[0],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"count-function"}}
26
+ {"id":"count-function-navigation-001","target":"2.0.0","expression":"g.count()","resource":{"g":[1,2,3]},"variables":{},"expected":[3],"origin":{"suite":"HL7-FHIRPath","commit":"master","case":"count-function"}}
27
+ {"id":"sum-decimals-001","target":"3.0.0","expression":"(1.0 | 2.0 | 3.0 | 4.0 | 5.0).sum()","resource":{},"variables":{},"expected":[15.0],"origin":{"suite":"FHIRPath-3.0.0","commit":"2026-07-28","case":"sum-spec-example"}}
28
+ {"id":"sum-integers-001","target":"3.0.0","expression":"(1 | 2 | 3).sum()","resource":{},"variables":{},"expected":[6],"origin":{"suite":"FHIRPath-3.0.0","commit":"2026-07-28","case":"sum-integers"}}
29
+ {"id":"sum-mixed-numeric-001","target":"3.0.0","expression":"(1 | 2.5).sum()","resource":{},"variables":{},"expected":[3.5],"origin":{"suite":"FHIRPath-3.0.0","commit":"2026-07-28","case":"sum-mixed-numeric-decision"}}
30
+ {"id":"sum-empty-001","target":"3.0.0","expression":"{}.sum()","resource":{},"variables":{},"expected":[],"origin":{"suite":"FHIRPath-3.0.0","commit":"2026-07-28","case":"sum-empty-decision"}}
31
+ {"id":"avg-decimals-001","target":"3.0.0","expression":"(5.5 | 4.7 | 4.8).avg()","resource":{},"variables":{},"expected":[5.0],"origin":{"suite":"FHIRPath-3.0.0","commit":"2026-07-28","case":"avg-spec-example"}}
32
+ {"id":"avg-integers-001","target":"3.0.0","expression":"(1 | 2 | 3 | 4).avg()","resource":{},"variables":{},"expected":[2.5],"origin":{"suite":"FHIRPath-3.0.0","commit":"2026-07-28","case":"avg-integers-as-decimal"}}
33
+ {"id":"avg-empty-001","target":"3.0.0","expression":"{}.avg()","resource":{},"variables":{},"expected":[],"origin":{"suite":"FHIRPath-3.0.0","commit":"2026-07-28","case":"avg-empty"}}
34
+ {"id":"min-numeric-001","target":"3.0.0","expression":"(2 | 4 | 8 | 6).min()","resource":{},"variables":{},"expected":[2],"origin":{"suite":"FHIRPath-3.0.0","commit":"2026-07-28","case":"min-spec-example"}}
35
+ {"id":"min-strings-001","target":"3.0.0","expression":"('b' | 'a' | 'c').min()","resource":{},"variables":{},"expected":["a"],"origin":{"suite":"FHIRPath-3.0.0","commit":"2026-07-28","case":"min-strings"}}
36
+ {"id":"min-empty-001","target":"3.0.0","expression":"{}.min()","resource":{},"variables":{},"expected":[],"origin":{"suite":"FHIRPath-3.0.0","commit":"2026-07-28","case":"min-empty"}}
37
+ {"id":"max-numeric-001","target":"3.0.0","expression":"(2 | 4 | 8 | 6).max()","resource":{},"variables":{},"expected":[8],"origin":{"suite":"FHIRPath-3.0.0","commit":"2026-07-28","case":"max-spec-example"}}
38
+ {"id":"max-strings-001","target":"3.0.0","expression":"('b' | 'a' | 'c').max()","resource":{},"variables":{},"expected":["c"],"origin":{"suite":"FHIRPath-3.0.0","commit":"2026-07-28","case":"max-strings"}}
39
+ {"id":"max-empty-001","target":"3.0.0","expression":"{}.max()","resource":{},"variables":{},"expected":[],"origin":{"suite":"FHIRPath-3.0.0","commit":"2026-07-28","case":"max-empty"}}
@@ -0,0 +1,15 @@
1
+ {
2
+ "schema": "fhirpath-conformance-v1",
3
+ "suite": "FHIR/fhir-test-cases",
4
+ "suite_commit": "2d595a506a91eeb311ab787f5fe2c8806be3cfd1",
5
+ "target": "2.0.0",
6
+ "model": "plain",
7
+ "source": "r4/fhirpath/tests-fhir-r4.xml",
8
+ "fixture_root": "r4",
9
+ "cases": [
10
+ "testPrecedence2",
11
+ "testUnion1",
12
+ "testPlus4",
13
+ "testLiteralTrue"
14
+ ]
15
+ }
@@ -0,0 +1,6 @@
1
+ {"id":"r4-observation-value-string-001","target":"2.0.0","model":"R4","expression":"Observation.value","resource":{"resourceType":"Observation","status":"final","valueString":"high"},"variables":{},"expected":["high"],"origin":{"suite":"HL7-FHIR","commit":"R4","case":"Observation.value[x]","url":"https://hl7.org/fhir/R4/observation.html"}}
2
+ {"id":"r4-observation-value-quantity-001","target":"2.0.0","model":"R4","expression":"Observation.value.value","resource":{"resourceType":"Observation","status":"final","valueQuantity":{"value":120,"unit":"mmHg"}},"variables":{},"expected":[120],"origin":{"suite":"HL7-FHIR","commit":"R4","case":"Observation.value[x]","url":"https://hl7.org/fhir/R4/observation.html"}}
3
+ {"id":"r4-observation-value-absent-001","target":"2.0.0","model":"R4","expression":"Observation.value","resource":{"resourceType":"Observation","status":"final"},"variables":{},"expected":[],"origin":{"suite":"HL7-FHIR","commit":"R4","case":"Observation.value[x] absent","url":"https://hl7.org/fhir/R4/observation.html"}}
4
+ {"id":"r4-observation-value-quantity-is-001","target":"2.0.0","model":"R4","expression":"Observation.value is Quantity","resource":{"resourceType":"Observation","status":"final","valueQuantity":{"value":120,"unit":"mmHg"}},"variables":{},"expected":[true],"origin":{"suite":"HL7-FHIR","commit":"R4","case":"Observation.value is Quantity (type operator over choice logical type)","url":"https://hl7.org/fhir/R4/observation.html"}}
5
+ {"id":"r4-observation-value-quantity-as-001","target":"2.0.0","model":"R4","expression":"Observation.value as Quantity","resource":{"resourceType":"Observation","status":"final","valueQuantity":{"value":120,"unit":"mmHg"}},"variables":{},"expected":[{"value":120,"unit":"mmHg"}],"origin":{"suite":"HL7-FHIR","commit":"R4","case":"Observation.value as Quantity passes the value through (type operator over choice logical type)","url":"https://hl7.org/fhir/R4/observation.html"}}
6
+ {"id":"r4-observation-value-absent-type-001","target":"2.0.0","model":"R4","expression":"Observation.value as Quantity","resource":{"resourceType":"Observation","status":"final"},"variables":{},"expected":[],"origin":{"suite":"HL7-FHIR","commit":"R4","case":"Observation.value as Quantity empty in empty out (absent choice)","url":"https://hl7.org/fhir/R4/observation.html"}}