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.
@@ -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.
@@ -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