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 +7 -0
- data/.rubocop.yml +73 -0
- data/CHANGELOG.md +77 -0
- data/CONTRIBUTING.md +67 -0
- data/Gemfile +12 -0
- data/LICENSE +21 -0
- data/README.md +218 -0
- data/Rakefile +19 -0
- data/SECURITY.md +20 -0
- data/conformance/core.jsonl +39 -0
- data/conformance/official-r4-core.json +15 -0
- data/conformance/r4.jsonl +6 -0
- data/docs/api.md +170 -0
- data/docs/architecture.md +507 -0
- data/docs/conformance.md +67 -0
- data/docs/feature-matrix.md +49 -0
- data/docs/first-slice.md +57 -0
- data/docs/release-checklist.md +57 -0
- data/docs/releasing.md +87 -0
- data/docs/support-matrix.md +102 -0
- data/lib/fhirpath/ast.rb +128 -0
- data/lib/fhirpath/capability.rb +57 -0
- data/lib/fhirpath/collection.rb +94 -0
- data/lib/fhirpath/compiled_expression.rb +35 -0
- data/lib/fhirpath/conformance/importer.rb +317 -0
- data/lib/fhirpath/errors.rb +76 -0
- data/lib/fhirpath/evaluation_context.rb +30 -0
- data/lib/fhirpath/evaluator.rb +690 -0
- data/lib/fhirpath/functions.rb +71 -0
- data/lib/fhirpath/host_services.rb +52 -0
- data/lib/fhirpath/model.rb +47 -0
- data/lib/fhirpath/model_registry.rb +17 -0
- data/lib/fhirpath/models_r4.rb +91 -0
- data/lib/fhirpath/parser.rb +487 -0
- data/lib/fhirpath/source_span.rb +25 -0
- data/lib/fhirpath/types.rb +73 -0
- data/lib/fhirpath/vector_runner.rb +130 -0
- data/lib/fhirpath/version.rb +9 -0
- data/lib/fhirpath.rb +77 -0
- data/script/import_vectors.rb +14 -0
- data/script/run_vectors.rb +19 -0
- metadata +120 -0
data/docs/first-slice.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# FHIRPath first parity slice
|
|
2
|
+
|
|
3
|
+
Status: implemented prototype slice
|
|
4
|
+
Target: normative FHIRPath 2.0.0 core expressions
|
|
5
|
+
|
|
6
|
+
This slice exercises the complete Ruby path from source text through lexer/parser, immutable AST, per-evaluation context, and collection-first evaluator. It is intentionally a small compatibility slice, not a complete FHIRPath implementation.
|
|
7
|
+
|
|
8
|
+
## Supported behavior
|
|
9
|
+
|
|
10
|
+
- Boolean, string, integer, decimal, and scientific-notation numeric literals
|
|
11
|
+
- Empty and comma-separated collection literals (`{}` and `{1, 2}`)
|
|
12
|
+
- Parentheses and unary `+`/`-`
|
|
13
|
+
- Numeric `+`, `-`, `*`, `/`, `div`, and `mod`; string `+` and `&` concatenation semantics
|
|
14
|
+
- Numeric and string relational comparisons (`<`, `<=`, `>`, `>=`)
|
|
15
|
+
- Collection-aware equality (`=`, `!=`) and equivalence (`~`, `!~`), including order-independent duplicate matching
|
|
16
|
+
- Union (`|`) and string concatenation (`+` propagates empty operands; `&` treats empty operands as `''`)
|
|
17
|
+
- Membership (`in`, `contains`) and built-in primitive type operators (`is`, `as`)
|
|
18
|
+
- Empty-aware `and`, `or`, `xor`, and `implies`
|
|
19
|
+
- Plain Hash/Array/object member navigation with collection flattening
|
|
20
|
+
- Expression indexers, including empty, out-of-range, and singleton/type checks
|
|
21
|
+
- `where`, `select`, `first`, `exists`, `count`, `empty`, `not`, `all`, and Boolean aggregate functions
|
|
22
|
+
- `$this`, `$index`, and `$total` focus variables inside delayed predicates
|
|
23
|
+
- `%name` external constants through the existing variable/host boundary
|
|
24
|
+
- Complete-input parsing with source spans on syntax and evaluation errors, comments, and strict Unicode escapes
|
|
25
|
+
- Reusable compiled expressions with no retained resource or evaluation context
|
|
26
|
+
|
|
27
|
+
Every public evaluation returns `FHIRPath::Collection`; `evaluate_first` remains the explicit scalar convenience API.
|
|
28
|
+
|
|
29
|
+
## Intentional deviations and deferrals
|
|
30
|
+
|
|
31
|
+
- Union, string concatenation, membership, and primitive type operators are implemented for the supported built-in value subset; complex types and FHIR model metadata remain deferred.
|
|
32
|
+
- Equivalence normalizes case and runs of whitespace for strings and rounds numeric operands to their least precise decimal place. Full equivalence semantics for all FHIRPath types remain deferred with the broader value system.
|
|
33
|
+
- Division always produces an exact `BigDecimal`; temporal, quantity, UCUM, and advanced numeric semantics are deferred.
|
|
34
|
+
- Empty arithmetic and relational operands return the empty collection. Empty equality follows FHIRPath's empty-aware result policy; equivalence of two empty collections returns `true`.
|
|
35
|
+
- dependency-free FHIR R4 JSON choice navigation for `Observation.value[x]`;
|
|
36
|
+
- broader FHIR-specific model metadata, primitive extensions, terminology, `resolve()`, temporal/quantity values, advanced functions, and the official shared-suite importer remain deferred.
|
|
37
|
+
- Trial-use FHIRPath 3.0 features remain disabled by the normative 2.0.0 capability.
|
|
38
|
+
|
|
39
|
+
## Verification
|
|
40
|
+
|
|
41
|
+
Focused coverage is in `test/parity_slice_test.rb`; the existing foundation tests were updated where expression indexers now accept the standard expression form. The slice covers successful arithmetic/comparison/logic, empty propagation, nested predicate focus, index validation, type and singleton errors, malformed/trailing input, unsupported operators, compiled-expression reuse, and plain-model end-to-end navigation.
|
|
42
|
+
|
|
43
|
+
Run:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
bundle exec rake test
|
|
47
|
+
bundle exec rubocop
|
|
48
|
+
git diff --check
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Compatibility references
|
|
52
|
+
|
|
53
|
+
- HL7 FHIRPath specification: https://raw.githubusercontent.com/HL7/FHIRPath/master/input/pages/index.md
|
|
54
|
+
- HL7 FHIRPath grammar: https://raw.githubusercontent.com/HL7/FHIRPath/master/input/images/fhirpath.g4
|
|
55
|
+
- HL7 FHIRPath shared tests: https://raw.githubusercontent.com/HL7/FHIRPath/master/input/pages/tests.md
|
|
56
|
+
- fhirpath-py public API: https://raw.githubusercontent.com/beda-software/fhirpath-py/master/fhirpathpy/__init__.py
|
|
57
|
+
- fhirpath-py evaluator and invocation registry: https://raw.githubusercontent.com/beda-software/fhirpath-py/master/fhirpathpy/engine/__init__.py
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Release checklist
|
|
2
|
+
|
|
3
|
+
This checklist is the gate for a reusable public release. It is intentionally explicit because a successful gem build is not the same as a legally releasable or conformant package.
|
|
4
|
+
|
|
5
|
+
## Current assessment (`0.2.0.pre1`)
|
|
6
|
+
|
|
7
|
+
| Criterion | Status | Evidence / rationale |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `require "fhirpath"` from a clean install | Met | `script/verify_gem_install.sh` and API tests |
|
|
10
|
+
| Documented parse/compile/evaluate API | Met | `README.md`, `docs/api.md` |
|
|
11
|
+
| Explicit supported Ruby policy | Met | gemspec and CI matrix target Ruby 3.2/3.3; publication contract is `docs/support-matrix.md` |
|
|
12
|
+
| Exact FHIRPath target and capability set in package metadata | Met | `Capability.current`, gemspec metadata, and `script/verify_release.rb` |
|
|
13
|
+
| Tests, static checks, package build in CI | Met | `.github/workflows/ci.yml` |
|
|
14
|
+
| Reproducible checked-in vector workflow | Met | `docs/conformance.md`, `conformance/core.jsonl` |
|
|
15
|
+
| Coverage summary | Met | `COVERAGE=1` test workflow and `script/check_coverage.rb` |
|
|
16
|
+
| Tagged release workflow and checksummed assets | Met | `.github/workflows/release.yml` and `docs/releasing.md` |
|
|
17
|
+
| RubyGems publication and external readback | Ready | Trusted Publishing environment gate and `gem push`; requires one-time RubyGems publisher setup |
|
|
18
|
+
| Contributor and security guidance | Met | `CONTRIBUTING.md`, `SECURITY.md` |
|
|
19
|
+
| Changelog/release notes | Met | `CHANGELOG.md` and this checklist |
|
|
20
|
+
| Complete official HL7 shared-suite conformance | Partial | importer and full suite remain deferred |
|
|
21
|
+
| FHIR release model adapter | Partial | `PlainModel` only; adapters are host-dependent |
|
|
22
|
+
| License selected and encoded in package metadata | Met | MIT License text is checked in at `LICENSE`; `fhirpath.gemspec` declares `MIT` |
|
|
23
|
+
| RubyGems/public reusable release | Pre-release only | The workflow permits prerelease tags; stable promotion remains blocked by the channel and incomplete conformance/model gates |
|
|
24
|
+
|
|
25
|
+
## Required before publication
|
|
26
|
+
|
|
27
|
+
- Audit runtime and development dependency licenses and any future grammar/fixture provenance.
|
|
28
|
+
- Confirm the supported Ruby matrix and run it on the tagged source revision.
|
|
29
|
+
- Build from a clean committed checkout; inspect gem contents and metadata.
|
|
30
|
+
- Run the complete tests, RuboCop, vectors, coverage, and gem-install smoke test.
|
|
31
|
+
- Generate release notes with scope, limitations, conformance counts, known host/model exclusions, and checksum/provenance.
|
|
32
|
+
- Create a reviewed version tag and use `.github/workflows/release.yml` with the approved `release` environment; do not publish from a dirty tree.
|
|
33
|
+
- Verify the RubyGems readback, pushed commit, tag, GitHub release metadata, and uploaded artifacts after each external action.
|
|
34
|
+
|
|
35
|
+
The license decision is complete. This implementation adds the gated workflow,
|
|
36
|
+
but does not commit, push, create a tag, or publish a release.
|
|
37
|
+
|
|
38
|
+
## Integration countercheck (2026-09-05)
|
|
39
|
+
|
|
40
|
+
The post-hardening working tree was independently reviewed against the HL7 FHIRPath operator documentation and exercised locally. This pass corrected two implementation gaps: `&` now treats an empty operand as `''`, and relational/union/type parser precedence now follows the normative ordering. The nonstandard `\\UXXXXXXXX` string escape is rejected; valid UTF-16 `\\uXXXX` surrogate pairs are combined.
|
|
41
|
+
|
|
42
|
+
Verification on Homebrew Ruby 4.0.4 (the repository's local bundle contains native extensions for this runtime):
|
|
43
|
+
|
|
44
|
+
- `bundle exec rake test`: 50 runs, 191 assertions, 0 failures, 0 errors, 0 skips;
|
|
45
|
+
- `bundle exec rubocop`: 31 files inspected, no offenses;
|
|
46
|
+
- `bundle exec rake vectors`: 8 total, 8 pass, 0 defect, 0 unsupported, 0 host-dependent, 0 not-run;
|
|
47
|
+
- `bundle exec rake build`: `pkg/fhirpath-0.2.0.pre1.gem` built;
|
|
48
|
+
- `script/verify_gem_install.sh`: clean isolated install and `1 + 2` smoke test passed;
|
|
49
|
+
- `COVERAGE=1 bundle exec rake test` plus `script/check_coverage.rb`: 88.9% (829/932 executable lines); and
|
|
50
|
+
- `git diff --check`: passed.
|
|
51
|
+
|
|
52
|
+
The project is usable as a clearly scoped MIT-licensed pre-release core slice,
|
|
53
|
+
but it is not yet a complete FHIRPath implementation or production release.
|
|
54
|
+
The official HL7 shared-suite importer, temporal/quantity semantics, FHIR model
|
|
55
|
+
adapters, and broader standard function families remain explicitly deferred.
|
|
56
|
+
No commit, push, tag, publication, or external repository setting change was
|
|
57
|
+
performed.
|
data/docs/releasing.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Releasing FHIRPath for Ruby
|
|
2
|
+
|
|
3
|
+
This document defines the versioning, release-gate, RubyGems, and GitHub
|
|
4
|
+
publication workflow. Publication is performed only from a reviewed, pushed
|
|
5
|
+
tag; the release workflow never packages a dirty local checkout.
|
|
6
|
+
|
|
7
|
+
## Versioning policy
|
|
8
|
+
|
|
9
|
+
- The gem name is `fhirpath` and tags use the form `vVERSION`.
|
|
10
|
+
- Versions follow RubyGems-compatible Semantic Versioning. While the API and
|
|
11
|
+
conformance surface are pre-1.0, use `0.MINOR.PATCH` and a prerelease suffix
|
|
12
|
+
such as `0.1.0.pre1` for published previews.
|
|
13
|
+
- `lib/fhirpath/version.rb`, `CHANGELOG.md`, and the release tag must agree.
|
|
14
|
+
- `FHIRPath::RELEASE_CHANNEL` remains `pre-release` until the complete
|
|
15
|
+
promotion decision is made. The release verifier rejects a stable-looking
|
|
16
|
+
version while that channel is still pre-release.
|
|
17
|
+
- Every release updates the `Unreleased` section into a dated version section,
|
|
18
|
+
adds the comparison link, and preserves explicit supported, unsupported, and
|
|
19
|
+
host-dependent behavior.
|
|
20
|
+
|
|
21
|
+
## Required gates
|
|
22
|
+
|
|
23
|
+
A release tag advances only when all of these checks pass on the tagged source:
|
|
24
|
+
|
|
25
|
+
1. Ruby 3.2 and Ruby 3.3 CI matrix;
|
|
26
|
+
2. full Minitest suite and RuboCop;
|
|
27
|
+
3. checked-in compatibility vectors, with no `defect` or `not-run` cases;
|
|
28
|
+
4. coverage generation and summary validation;
|
|
29
|
+
5. `gem build` from the tag;
|
|
30
|
+
6. isolated local installation and `1 + 2` API smoke test;
|
|
31
|
+
7. package metadata verification, including version, MIT license, exact
|
|
32
|
+
FHIRPath target, capability set, release status, and support-matrix URI;
|
|
33
|
+
8. release notes generated from the feature/support matrix and current vector
|
|
34
|
+
evidence; and
|
|
35
|
+
9. a SHA-256 manifest covering the published gem and conformance report.
|
|
36
|
+
|
|
37
|
+
Unsupported and host-dependent behavior may remain in a pre-release, but it
|
|
38
|
+
must be listed in the support matrix and release notes. A defect or silently
|
|
39
|
+
skipped case is a hard failure.
|
|
40
|
+
|
|
41
|
+
## RubyGems publication
|
|
42
|
+
|
|
43
|
+
The `publish` job supports two authentication modes:
|
|
44
|
+
|
|
45
|
+
1. **Repository API key (default when configured):** store the owner's RubyGems
|
|
46
|
+
API key as the `RUBYGEMS_API_KEY` repository secret (`gh secret set
|
|
47
|
+
RUBYGEMS_API_KEY --repo niccoreyes/fhirpath-ruby`). The workflow passes it as
|
|
48
|
+
`GEM_HOST_API_KEY` and runs `gem push` directly.
|
|
49
|
+
2. **RubyGems Trusted Publishing (fallback):** when the secret is absent, the
|
|
50
|
+
workflow configures short-lived OIDC credentials and pushes with them.
|
|
51
|
+
Configure a pending trusted publisher (for a new gem) or a trusted publisher
|
|
52
|
+
(for an existing gem) with:
|
|
53
|
+
|
|
54
|
+
- owner: `niccoreyes`;
|
|
55
|
+
- repository: `fhirpath-ruby`;
|
|
56
|
+
- workflow name: `Release`;
|
|
57
|
+
- environment: `release`.
|
|
58
|
+
|
|
59
|
+
The workflow's `publish` job has only `id-token: write` and read access to
|
|
60
|
+
repository contents. It downloads the exact artifact produced by the gated
|
|
61
|
+
package job, pushes the exact gem, and reads the RubyGems API back until the
|
|
62
|
+
exact version is visible.
|
|
63
|
+
|
|
64
|
+
## GitHub release publication
|
|
65
|
+
|
|
66
|
+
After RubyGems confirms the exact version, the workflow creates a GitHub release
|
|
67
|
+
for the existing tag and attaches:
|
|
68
|
+
|
|
69
|
+
- the `.gem` artifact;
|
|
70
|
+
- `SHA256SUMS.txt`; and
|
|
71
|
+
- the JSON compatibility/conformance report.
|
|
72
|
+
|
|
73
|
+
The generated notes include the FHIRPath target, capability set, supported
|
|
74
|
+
behavior, explicit unsupported/deferred behavior, host-dependent behavior,
|
|
75
|
+
verification evidence, and artifact provenance. The GitHub release is marked
|
|
76
|
+
as a prerelease while the gem version is prerelease.
|
|
77
|
+
|
|
78
|
+
## Manual operator procedure
|
|
79
|
+
|
|
80
|
+
1. Update `lib/fhirpath/version.rb` and the `Unreleased` changelog section.
|
|
81
|
+
2. Run every command in `CONTRIBUTING.md`, inspect the gem contents, and review
|
|
82
|
+
`docs/support-matrix.md` and `docs/release-checklist.md`.
|
|
83
|
+
3. Open and merge the review PR; do not tag an unreviewed or dirty checkout.
|
|
84
|
+
4. Create and push the matching `vVERSION` tag from the reviewed commit.
|
|
85
|
+
5. Approve the `release` environment if required by repository settings.
|
|
86
|
+
6. Verify the workflow, RubyGems version, GitHub release, assets, checksums, and
|
|
87
|
+
notes. If any external readback disagrees, stop and investigate before retrying.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Release support matrix
|
|
2
|
+
|
|
3
|
+
Status: `pre-release` (`0.2.0.pre1`)
|
|
4
|
+
|
|
5
|
+
This is the release-facing support matrix. It is deliberately narrower than
|
|
6
|
+
"FHIRPath-compatible": every supported claim must have executable evidence, and
|
|
7
|
+
every deferred or host-dependent behavior remains a limitation in release notes.
|
|
8
|
+
The detailed behavior table is maintained in [`feature-matrix.md`](feature-matrix.md).
|
|
9
|
+
|
|
10
|
+
## Language and package declaration
|
|
11
|
+
|
|
12
|
+
| Field | Declared value |
|
|
13
|
+
|---|---|
|
|
14
|
+
| Gem | `fhirpath` |
|
|
15
|
+
| Normative FHIRPath target | `2.0.0` |
|
|
16
|
+
| Release channel | `pre-release` |
|
|
17
|
+
| License | MIT |
|
|
18
|
+
| Supported Ruby CI matrix | Ruby `3.2`, Ruby `3.3` |
|
|
19
|
+
| Model support | Model-independent plain Ruby Hash/Array/object navigation plus dependency-free FHIR R4 JSON adapter |
|
|
20
|
+
| FHIR model releases | R4 (`4.0.1`), selectable with `model: :r4` |
|
|
21
|
+
| Trial-use features | One declared exception only: the FHIRPath 3.0 STU3 aggregate functions `sum`/`avg`/`max`/`min`, shipped by default in the standard registry (see [Declared STU3-subset exception](#declared-stu3-subset-exception)) |
|
|
22
|
+
|
|
23
|
+
The packaged gem repeats the target and capability declaration in gem metadata:
|
|
24
|
+
`fhirpath_target`, `capability_set`, `release_status`, and
|
|
25
|
+
`support_matrix_uri`. `FHIRPath::Capability.current` is the runtime source for
|
|
26
|
+
the same target and capability-set values.
|
|
27
|
+
|
|
28
|
+
## Published capability set
|
|
29
|
+
|
|
30
|
+
These stable identifiers describe the behavior included in the current package:
|
|
31
|
+
|
|
32
|
+
- `parser` — complete-input parsing, immutable AST nodes, and source spans;
|
|
33
|
+
- `immutable-ast` — immutable parse/compile boundaries;
|
|
34
|
+
- `collection-evaluation` — explicit empty, singleton, and multi-item results;
|
|
35
|
+
- `plain-model-navigation` — Hash, Array, and safe simple-object navigation;
|
|
36
|
+
- `primitive-values` — strings, Booleans, integers, decimals, and scientific notation;
|
|
37
|
+
- `arithmetic` — unary/numeric arithmetic and string `+`;
|
|
38
|
+
- `comparison-and-equivalence` — relational comparison, equality, and equivalence;
|
|
39
|
+
- `boolean-logic` — empty-aware Boolean operators;
|
|
40
|
+
- `union-membership-and-type-operators` — union, `in`, `contains`, `is`, and `as`;
|
|
41
|
+
- `collection-functions` — `where`, `select`, `first`, `exists`, `count`, `empty`, `not`, `all`, and Boolean aggregates;
|
|
42
|
+
- `focus-variables` — `$this`, `$index`, and `$total`;
|
|
43
|
+
- `external-constants` — explicitly supplied `%name` values;
|
|
44
|
+
- `custom-functions` — explicitly registered functions;
|
|
45
|
+
- `compiled-expression-reuse` — immutable reusable compiled expressions; and
|
|
46
|
+
- `structured-errors` — typed errors with machine-readable codes and source spans.
|
|
47
|
+
|
|
48
|
+
### Declared STU3-subset exception
|
|
49
|
+
|
|
50
|
+
The normative target stays FHIRPath `2.0.0` and the published capability set
|
|
51
|
+
above stays unchanged, but the standard registry ships one STU3 subset by
|
|
52
|
+
default as a deliberate, documented exception:
|
|
53
|
+
|
|
54
|
+
- the FHIRPath 3.0.0 STU3 aggregate functions `sum()`, `avg()`, `max()`, and
|
|
55
|
+
`min()` (published 2026-07-28; absent from normative 2.0.0, which contains
|
|
56
|
+
only `count()`, and absent from the 3.0.0 ballot);
|
|
57
|
+
- `FHIRPath::Capability.current` keeps `fhirpath: '2.0.0'` and reports this
|
|
58
|
+
subset in `trial_use` under the marker `stu3-aggregate-functions`, so the
|
|
59
|
+
capability report (`Capability#to_h`) always names the STU3 behavior it
|
|
60
|
+
ships and never folds it silently into the normative claims;
|
|
61
|
+
- `trial_use` is a surface declaration, not a registry gate: the marker does
|
|
62
|
+
not disable or enable functions in `FHIRPath::FunctionRegistry.standard`.
|
|
63
|
+
A caller that needs a hard 2.0.0-only function set must supply its own
|
|
64
|
+
registry built without the aggregate functions; that enforcement is not
|
|
65
|
+
implemented in the current capability surface.
|
|
66
|
+
|
|
67
|
+
The functions are supported; see [`feature-matrix.md`](feature-matrix.md) for
|
|
68
|
+
behavioral evidence and [`docs/api.md`](api.md) for the runtime declaration.
|
|
69
|
+
|
|
70
|
+
## Explicitly unsupported or deferred
|
|
71
|
+
|
|
72
|
+
The release must continue to state these limitations:
|
|
73
|
+
|
|
74
|
+
- complete official HL7 shared-suite conformance and its importer;
|
|
75
|
+
- date/time literals and values;
|
|
76
|
+
- quantity and UCUM semantics;
|
|
77
|
+
- advanced conversion, math, string, regular-expression, and navigation functions, and the general-purpose `aggregate()` function;
|
|
78
|
+
- complex literals and additional standard value types;
|
|
79
|
+
- standard environment variables beyond explicitly supplied external constants;
|
|
80
|
+
- FHIRPath `3.0` STU3 features beyond the declared `stu3-aggregate-functions` subset; and
|
|
81
|
+
- network I/O and global evaluator state in the pure evaluation boundary.
|
|
82
|
+
|
|
83
|
+
## Host-dependent behavior
|
|
84
|
+
|
|
85
|
+
The gem bundles the dependency-free R4 JSON adapter described above. Callers
|
|
86
|
+
must supply and validate these remaining adapters or services at the host
|
|
87
|
+
boundary before claiming support:
|
|
88
|
+
|
|
89
|
+
- FHIR R5 and broader release-specific model adapters;
|
|
90
|
+
- broader choice-element metadata and primitive extensions; and
|
|
91
|
+
- `resolve()` and terminology services.
|
|
92
|
+
|
|
93
|
+
## Evidence and promotion policy
|
|
94
|
+
|
|
95
|
+
Each tagged release runs the Ruby 3.2/3.3 matrix, tests, RuboCop, checked-in
|
|
96
|
+
compatibility vectors, package build, isolated installed-gem smoke test, and
|
|
97
|
+
coverage validation. Defect or silently skipped vector cases fail the release;
|
|
98
|
+
unsupported and host-dependent cases are retained as classified evidence.
|
|
99
|
+
|
|
100
|
+
The project remains pre-release until the release channel is deliberately
|
|
101
|
+
promoted after the official conformance and remaining model/host gates are
|
|
102
|
+
resolved. A successful build alone never changes the support claims.
|
data/lib/fhirpath/ast.rb
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FHIRPath
|
|
4
|
+
module AST
|
|
5
|
+
class Node
|
|
6
|
+
attr_reader :span
|
|
7
|
+
|
|
8
|
+
def initialize(span:)
|
|
9
|
+
@span = span
|
|
10
|
+
freeze
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Literal < Node
|
|
15
|
+
attr_reader :value
|
|
16
|
+
|
|
17
|
+
def initialize(value:, span:)
|
|
18
|
+
@value = freeze_value(value)
|
|
19
|
+
super(span: span)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def freeze_value(value)
|
|
25
|
+
case value
|
|
26
|
+
when Array
|
|
27
|
+
value.map { |item| freeze_value(item) }.freeze
|
|
28
|
+
when Hash
|
|
29
|
+
value.each_with_object({}) do |(key, item), copy|
|
|
30
|
+
copy[freeze_value(key)] = freeze_value(item)
|
|
31
|
+
end.freeze
|
|
32
|
+
when String
|
|
33
|
+
value.dup.freeze
|
|
34
|
+
else
|
|
35
|
+
value
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class CollectionLiteral < Node
|
|
41
|
+
attr_reader :elements
|
|
42
|
+
|
|
43
|
+
def initialize(elements:, span:)
|
|
44
|
+
@elements = Array(elements).dup.freeze
|
|
45
|
+
super(span: span)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class Identifier < Node
|
|
50
|
+
attr_reader :name
|
|
51
|
+
|
|
52
|
+
def initialize(name:, span:)
|
|
53
|
+
@name = name.freeze
|
|
54
|
+
super(span: span)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class Variable < Node
|
|
59
|
+
attr_reader :name
|
|
60
|
+
|
|
61
|
+
def initialize(name:, span:)
|
|
62
|
+
@name = name.freeze
|
|
63
|
+
super(span: span)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class ExternalConstant < Node
|
|
68
|
+
attr_reader :name
|
|
69
|
+
|
|
70
|
+
def initialize(name:, span:)
|
|
71
|
+
@name = name.freeze
|
|
72
|
+
super(span: span)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class MemberInvocation < Node
|
|
77
|
+
attr_reader :receiver, :name
|
|
78
|
+
|
|
79
|
+
def initialize(receiver:, name:, span:)
|
|
80
|
+
@receiver = receiver
|
|
81
|
+
@name = name.freeze
|
|
82
|
+
super(span: span)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class Indexer < Node
|
|
87
|
+
attr_reader :receiver, :index
|
|
88
|
+
|
|
89
|
+
def initialize(receiver:, index:, span:)
|
|
90
|
+
@receiver = receiver
|
|
91
|
+
@index = index
|
|
92
|
+
super(span: span)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class FunctionInvocation < Node
|
|
97
|
+
attr_reader :receiver, :name, :arguments
|
|
98
|
+
|
|
99
|
+
def initialize(receiver:, name:, arguments:, span:)
|
|
100
|
+
@receiver = receiver
|
|
101
|
+
@name = name.freeze
|
|
102
|
+
@arguments = Array(arguments).dup.freeze
|
|
103
|
+
super(span: span)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class UnaryExpression < Node
|
|
108
|
+
attr_reader :operator, :operand
|
|
109
|
+
|
|
110
|
+
def initialize(operator:, operand:, span:)
|
|
111
|
+
@operator = operator.to_sym
|
|
112
|
+
@operand = operand
|
|
113
|
+
super(span: span)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class BinaryExpression < Node
|
|
118
|
+
attr_reader :left, :operator, :right
|
|
119
|
+
|
|
120
|
+
def initialize(left:, operator:, right:, span:)
|
|
121
|
+
@left = left
|
|
122
|
+
@operator = operator.to_sym
|
|
123
|
+
@right = right
|
|
124
|
+
super(span: span)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FHIRPath
|
|
4
|
+
# Immutable declaration of the language and host features in use.
|
|
5
|
+
class Capability
|
|
6
|
+
CAPABILITY_SET = %w[
|
|
7
|
+
parser immutable-ast collection-evaluation plain-model-navigation
|
|
8
|
+
primitive-values arithmetic comparison-and-equivalence boolean-logic
|
|
9
|
+
union-membership-and-type-operators collection-functions focus-variables
|
|
10
|
+
external-constants custom-functions compiled-expression-reuse fhir-r4-model
|
|
11
|
+
structured-errors
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
# Named FHIRPath 3.0.0 STU3 subset the standard registry ships by default.
|
|
15
|
+
# `Capability.current` reports it in `trial_use` (not in `capability_set`),
|
|
16
|
+
# so the capability report never mixes STU3 behavior silently into the
|
|
17
|
+
# normative 2.0.0 claims: `fhirpath` stays `2.0.0` and the capability set
|
|
18
|
+
# above stays unchanged. The marker declares the shipped surface; it does
|
|
19
|
+
# not gate `FunctionRegistry.standard` (see docs/api.md Capability).
|
|
20
|
+
STU3_AGGREGATE_FUNCTIONS = 'stu3-aggregate-functions'
|
|
21
|
+
|
|
22
|
+
attr_reader :fhirpath, :trial_use, :model_releases, :host_features, :capability_set
|
|
23
|
+
|
|
24
|
+
def self.current
|
|
25
|
+
@current ||= new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(fhirpath: '2.0.0', trial_use: [STU3_AGGREGATE_FUNCTIONS], model_releases: ['R4'], host_features: [],
|
|
29
|
+
capability_set: CAPABILITY_SET)
|
|
30
|
+
@fhirpath = fhirpath.to_s.freeze
|
|
31
|
+
@trial_use = Array(trial_use).map(&:to_s).freeze
|
|
32
|
+
@model_releases = Array(model_releases).map(&:to_s).freeze
|
|
33
|
+
@host_features = Array(host_features).map(&:to_s).freeze
|
|
34
|
+
@capability_set = Array(capability_set).map(&:to_s).freeze
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def supports?(feature)
|
|
39
|
+
trial_use.include?(feature.to_s) || host_features.include?(feature.to_s) ||
|
|
40
|
+
supports_model?(feature)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def supports_model?(release)
|
|
44
|
+
model_releases.any? { |configured_release| configured_release.casecmp?(release.to_s) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_h
|
|
48
|
+
{
|
|
49
|
+
fhirpath: fhirpath,
|
|
50
|
+
capability_set: capability_set,
|
|
51
|
+
trial_use: trial_use,
|
|
52
|
+
model_releases: model_releases,
|
|
53
|
+
host_features: host_features
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FHIRPath
|
|
4
|
+
# Ordered FHIRPath collection. Empty is represented by an object, not nil.
|
|
5
|
+
class Collection
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
def self.empty
|
|
9
|
+
new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.from(value)
|
|
13
|
+
return value if value.is_a?(self)
|
|
14
|
+
return empty if value.nil?
|
|
15
|
+
|
|
16
|
+
new(value.is_a?(Array) ? value : [value])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :items, :types
|
|
20
|
+
|
|
21
|
+
def initialize(items = [], types: nil)
|
|
22
|
+
@items = flatten_items(Array(items)).freeze
|
|
23
|
+
@types = validate_types(types, @items.length)
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def each(&block)
|
|
28
|
+
return enum_for(:each) unless block
|
|
29
|
+
|
|
30
|
+
items.each(&block)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def empty?
|
|
34
|
+
items.empty?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def singleton?
|
|
38
|
+
items.length == 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def singleton!
|
|
42
|
+
return items.first if singleton?
|
|
43
|
+
|
|
44
|
+
raise SingletonError
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def first_item
|
|
48
|
+
items.first
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def count
|
|
52
|
+
items.length
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_a
|
|
56
|
+
items.dup
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def +(other)
|
|
60
|
+
Collection.new(items + Collection.from(other).items)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def map_items(&block)
|
|
64
|
+
Collection.new(map(&block).flat_map { |value| Collection.from(value).items })
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def inspect
|
|
68
|
+
"#<#{self.class} #{items.inspect}>"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Optional positional model-type metadata parallel to +items+ (entries are
|
|
74
|
+
# logical type names or nil). Without metadata the collection behaves
|
|
75
|
+
# exactly as before; item values are never wrapped or altered.
|
|
76
|
+
def validate_types(types, length)
|
|
77
|
+
return nil if types.nil?
|
|
78
|
+
|
|
79
|
+
raise ArgumentError, 'types must match the number of items' unless types.length == length
|
|
80
|
+
|
|
81
|
+
types.freeze
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def flatten_items(values)
|
|
85
|
+
values.each_with_object([]) do |value, flattened|
|
|
86
|
+
if value.is_a?(Array)
|
|
87
|
+
flattened.concat(flatten_items(value))
|
|
88
|
+
else
|
|
89
|
+
flattened << value
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FHIRPath
|
|
4
|
+
# Reusable immutable parse/evaluation boundary.
|
|
5
|
+
class CompiledExpression
|
|
6
|
+
attr_reader :source, :parsed, :ast, :model, :functions, :capability
|
|
7
|
+
|
|
8
|
+
def initialize(parsed:, model:, functions:, capability:)
|
|
9
|
+
@parsed = parsed
|
|
10
|
+
@source = parsed.source
|
|
11
|
+
@ast = parsed.ast
|
|
12
|
+
@model = model
|
|
13
|
+
@functions = functions
|
|
14
|
+
@capability = capability
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def evaluate(resource, variables: {}, host: nil, options: {})
|
|
19
|
+
context = EvaluationContext.new(
|
|
20
|
+
root: resource,
|
|
21
|
+
variables: variables,
|
|
22
|
+
model: model,
|
|
23
|
+
host: host,
|
|
24
|
+
functions: functions,
|
|
25
|
+
capability: capability,
|
|
26
|
+
options: options.merge(expression: source)
|
|
27
|
+
)
|
|
28
|
+
Evaluator.new.evaluate(ast, context)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call(resource, **kwargs)
|
|
32
|
+
evaluate(resource, **kwargs)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|