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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'errors'
|
|
5
|
+
require_relative 'conformance/importer'
|
|
6
|
+
|
|
7
|
+
module FHIRPath
|
|
8
|
+
# Small, optional JSONL differential-vector runner. It is deliberately kept
|
|
9
|
+
# outside the evaluator's runtime path and does not require Python.
|
|
10
|
+
module VectorRunner
|
|
11
|
+
CLASSIFICATIONS = %w[pass defect unsupported host-dependent not-run].freeze
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def run(path, evaluator: nil)
|
|
16
|
+
cases = File.foreach(path).with_index(1).each_with_object([]) do |(line, line_number), results|
|
|
17
|
+
next if line.strip.empty?
|
|
18
|
+
|
|
19
|
+
vector = JSON.parse(line)
|
|
20
|
+
results << execute(vector, line_number, evaluator: evaluator)
|
|
21
|
+
end
|
|
22
|
+
counts = CLASSIFICATIONS.each_with_object({}) { |name, result| result[name] = 0 }
|
|
23
|
+
cases.each { |result| counts[result['classification']] += 1 }
|
|
24
|
+
{
|
|
25
|
+
total: cases.length,
|
|
26
|
+
counts: counts,
|
|
27
|
+
capability_totals: capability_totals(cases),
|
|
28
|
+
cases: cases
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def execute(vector, line_number, evaluator: nil)
|
|
33
|
+
return result_for(vector, line_number, 'not-run').merge('actual' => nil) if vector['classification'] == 'not-run'
|
|
34
|
+
|
|
35
|
+
values = evaluate(vector, evaluator).to_a
|
|
36
|
+
classification = if values == vector.fetch('expected', []) && !vector['error']
|
|
37
|
+
'pass'
|
|
38
|
+
else
|
|
39
|
+
'defect'
|
|
40
|
+
end
|
|
41
|
+
result_for(vector, line_number, classification).merge('actual' => values)
|
|
42
|
+
rescue UnsupportedFeatureError => e
|
|
43
|
+
error_result(vector, line_number, classify_error(vector, e, 'unsupported'), e)
|
|
44
|
+
rescue HostError => e
|
|
45
|
+
error_result(vector, line_number, classify_error(vector, e, 'host-dependent'), e)
|
|
46
|
+
rescue Error => e
|
|
47
|
+
error_result(vector, line_number, classify_error(vector, e, 'defect'), e)
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
error_result(vector, line_number, 'defect', e)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def evaluate(vector, evaluator = nil)
|
|
53
|
+
return evaluator.call(vector) if evaluator
|
|
54
|
+
|
|
55
|
+
# Conformance records spell the default out as `plain`; the public API
|
|
56
|
+
# treats `nil` as the PlainModel default and rejects unknown release
|
|
57
|
+
# strings, so map the explicit plain marker to nil before dispatch.
|
|
58
|
+
model = vector['model']
|
|
59
|
+
model = nil if model.to_s == 'plain'
|
|
60
|
+
FHIRPath.evaluate(vector['resource'] || {}, vector.fetch('expression'),
|
|
61
|
+
variables: vector['variables'] || {}, model: model)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def error_result(vector, line_number, classification, error)
|
|
65
|
+
result_for(vector, line_number, classification).merge(
|
|
66
|
+
'actual' => nil,
|
|
67
|
+
'actual_error' => serialize_error(error)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def expected_error_matches?(expected, error)
|
|
72
|
+
return error.is_a?(Error) if expected == true
|
|
73
|
+
return false unless expected
|
|
74
|
+
|
|
75
|
+
class_matches = !expected['class'] || expected['class'] == error.class.name
|
|
76
|
+
actual_code = error.respond_to?(:code) ? error.code.to_s : nil
|
|
77
|
+
code_matches = !expected['code'] || expected['code'].to_s == actual_code
|
|
78
|
+
class_matches && code_matches
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def classify_error(vector, error, fallback)
|
|
82
|
+
return 'pass' if expected_error_matches?(vector['error'], error)
|
|
83
|
+
return 'defect' if vector['error']
|
|
84
|
+
|
|
85
|
+
fallback
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def result_for(vector, line_number, classification)
|
|
89
|
+
vector.merge(result_fields(vector, line_number, classification))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def result_fields(vector, line_number, classification)
|
|
93
|
+
origin = vector['origin'] || {}
|
|
94
|
+
{
|
|
95
|
+
'line' => line_number,
|
|
96
|
+
'suite' => result_value(vector, 'suite', origin['suite']),
|
|
97
|
+
'suite_commit' => result_value(vector, 'suite_commit', origin['suite_commit'] || origin['commit']),
|
|
98
|
+
'input_fixture' => vector.fetch('input_fixture', nil),
|
|
99
|
+
'model' => result_value(vector, 'model', 'plain'),
|
|
100
|
+
'host_features' => result_value(vector, 'host_features', Capability.current.host_features),
|
|
101
|
+
'target' => result_value(vector, 'target', Capability.current.fhirpath),
|
|
102
|
+
'expected' => vector.fetch('expected', []),
|
|
103
|
+
'classification' => classification
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def result_value(vector, key, fallback)
|
|
108
|
+
vector[key] || fallback
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def serialize_error(error)
|
|
112
|
+
serialized = if error.respond_to?(:to_h)
|
|
113
|
+
{ 'class' => error.class.name }.merge(error.to_h.transform_keys(&:to_s))
|
|
114
|
+
else
|
|
115
|
+
{ 'class' => error.class.name, 'message' => error.message }
|
|
116
|
+
end
|
|
117
|
+
serialized['code'] = error.code.to_s if error.respond_to?(:code)
|
|
118
|
+
serialized
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def capability_totals(cases)
|
|
122
|
+
cases.group_by { |result| result['capability'] || 'unspecified' }
|
|
123
|
+
.transform_values do |capability_cases|
|
|
124
|
+
counts = CLASSIFICATIONS.each_with_object({}) { |name, result| result[name] = 0 }
|
|
125
|
+
capability_cases.each { |result| counts[result['classification']] += 1 }
|
|
126
|
+
{ total: capability_cases.length, counts: counts }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FHIRPath
|
|
4
|
+
VERSION = '0.2.0.pre1'
|
|
5
|
+
# A stable release requires the complete release gate to be deliberately
|
|
6
|
+
# promoted. Keep pre-release status explicit while the shared-suite and
|
|
7
|
+
# model-adapter work remains incomplete.
|
|
8
|
+
RELEASE_CHANNEL = 'pre-release'
|
|
9
|
+
end
|
data/lib/fhirpath.rb
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'fhirpath/version'
|
|
4
|
+
require_relative 'fhirpath/source_span'
|
|
5
|
+
require_relative 'fhirpath/errors'
|
|
6
|
+
require_relative 'fhirpath/capability'
|
|
7
|
+
require_relative 'fhirpath/collection'
|
|
8
|
+
require_relative 'fhirpath/types'
|
|
9
|
+
require_relative 'fhirpath/ast'
|
|
10
|
+
require_relative 'fhirpath/model'
|
|
11
|
+
require_relative 'fhirpath/models_r4'
|
|
12
|
+
require_relative 'fhirpath/model_registry'
|
|
13
|
+
require_relative 'fhirpath/host_services'
|
|
14
|
+
require_relative 'fhirpath/functions'
|
|
15
|
+
require_relative 'fhirpath/evaluation_context'
|
|
16
|
+
require_relative 'fhirpath/parser'
|
|
17
|
+
require_relative 'fhirpath/evaluator'
|
|
18
|
+
require_relative 'fhirpath/compiled_expression'
|
|
19
|
+
|
|
20
|
+
# Public entry point for the Ruby FHIRPath implementation.
|
|
21
|
+
module FHIRPath
|
|
22
|
+
class << self
|
|
23
|
+
def version
|
|
24
|
+
VERSION
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def parse(expression, capability: Capability.current)
|
|
28
|
+
Parser.parse(expression, capability: capability)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def model(release)
|
|
32
|
+
ModelRegistry.fetch(release)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def available_models
|
|
36
|
+
['R4'].freeze
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def compile(expression, model: nil, capability: Capability.current,
|
|
40
|
+
functions: FunctionRegistry.standard)
|
|
41
|
+
parsed = expression.is_a?(ParsedExpression) ? expression : parse(expression, capability: capability)
|
|
42
|
+
CompiledExpression.new(parsed: parsed, model: resolve_model(model, capability),
|
|
43
|
+
functions: functions, capability: capability)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def evaluate(resource, expression, variables: {}, model: nil,
|
|
47
|
+
capability: Capability.current, functions: FunctionRegistry.standard,
|
|
48
|
+
options: {}, host: nil)
|
|
49
|
+
compile(expression, model: model, capability: capability, functions: functions)
|
|
50
|
+
.evaluate(resource, variables: variables, host: host, options: options)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def evaluate_first(resource, expression, variables: {}, model: nil,
|
|
54
|
+
capability: Capability.current, functions: FunctionRegistry.standard,
|
|
55
|
+
options: {}, host: nil)
|
|
56
|
+
evaluate(resource, expression, variables: variables, model: model,
|
|
57
|
+
capability: capability, functions: functions, options: options, host: host)
|
|
58
|
+
.first_item
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def resolve_model(model, capability)
|
|
64
|
+
return PlainModel.new if model.nil?
|
|
65
|
+
return model unless model.is_a?(Symbol) || model.is_a?(String)
|
|
66
|
+
|
|
67
|
+
provider = ModelRegistry.fetch(model)
|
|
68
|
+
unless capability.supports_model?(provider.class::RELEASE)
|
|
69
|
+
raise UnsupportedFeatureError.new(
|
|
70
|
+
"FHIR model release is not enabled: #{provider.class::RELEASE}",
|
|
71
|
+
code: :unsupported_model_release
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
provider
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
6
|
+
require 'fhirpath/vector_runner'
|
|
7
|
+
|
|
8
|
+
source_root = ARGV.fetch(0) do
|
|
9
|
+
abort "usage: #{File.basename($PROGRAM_NAME)} FHIR_TEST_CASES_CHECKOUT [MANIFEST]"
|
|
10
|
+
end
|
|
11
|
+
manifest = ARGV[1] || File.expand_path('../conformance/official-r4-core.json', __dir__)
|
|
12
|
+
|
|
13
|
+
records = FHIRPath::Conformance::Importer.from_manifest(manifest, source_root: source_root).import
|
|
14
|
+
records.each { |record| puts JSON.generate(record) }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
6
|
+
require 'fhirpath'
|
|
7
|
+
require 'fhirpath/vector_runner'
|
|
8
|
+
|
|
9
|
+
path = ARGV.fetch(0) do
|
|
10
|
+
abort "usage: #{File.basename($PROGRAM_NAME)} PATH_TO_JSONL"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
report = FHIRPath::VectorRunner.run(path)
|
|
14
|
+
puts JSON.pretty_generate(report)
|
|
15
|
+
|
|
16
|
+
# A release may document unsupported or host-dependent cases, but it must not
|
|
17
|
+
# ship known defects or silently skipped cases as conformance evidence.
|
|
18
|
+
blocked = report[:counts].slice('defect', 'not-run')
|
|
19
|
+
exit 1 if blocked.values.any?(&:positive?)
|
metadata
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fhirpath
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0.pre1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nicco Reyes
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-05 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bigdecimal
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rexml
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.3'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.3'
|
|
41
|
+
description: A prototype Ruby-native scaffold for the HL7 FHIRPath expression language,
|
|
42
|
+
with a deliberately limited supported surface.
|
|
43
|
+
email:
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- ".rubocop.yml"
|
|
49
|
+
- CHANGELOG.md
|
|
50
|
+
- CONTRIBUTING.md
|
|
51
|
+
- Gemfile
|
|
52
|
+
- LICENSE
|
|
53
|
+
- README.md
|
|
54
|
+
- Rakefile
|
|
55
|
+
- SECURITY.md
|
|
56
|
+
- conformance/core.jsonl
|
|
57
|
+
- conformance/official-r4-core.json
|
|
58
|
+
- conformance/r4.jsonl
|
|
59
|
+
- docs/api.md
|
|
60
|
+
- docs/architecture.md
|
|
61
|
+
- docs/conformance.md
|
|
62
|
+
- docs/feature-matrix.md
|
|
63
|
+
- docs/first-slice.md
|
|
64
|
+
- docs/release-checklist.md
|
|
65
|
+
- docs/releasing.md
|
|
66
|
+
- docs/support-matrix.md
|
|
67
|
+
- lib/fhirpath.rb
|
|
68
|
+
- lib/fhirpath/ast.rb
|
|
69
|
+
- lib/fhirpath/capability.rb
|
|
70
|
+
- lib/fhirpath/collection.rb
|
|
71
|
+
- lib/fhirpath/compiled_expression.rb
|
|
72
|
+
- lib/fhirpath/conformance/importer.rb
|
|
73
|
+
- lib/fhirpath/errors.rb
|
|
74
|
+
- lib/fhirpath/evaluation_context.rb
|
|
75
|
+
- lib/fhirpath/evaluator.rb
|
|
76
|
+
- lib/fhirpath/functions.rb
|
|
77
|
+
- lib/fhirpath/host_services.rb
|
|
78
|
+
- lib/fhirpath/model.rb
|
|
79
|
+
- lib/fhirpath/model_registry.rb
|
|
80
|
+
- lib/fhirpath/models_r4.rb
|
|
81
|
+
- lib/fhirpath/parser.rb
|
|
82
|
+
- lib/fhirpath/source_span.rb
|
|
83
|
+
- lib/fhirpath/types.rb
|
|
84
|
+
- lib/fhirpath/vector_runner.rb
|
|
85
|
+
- lib/fhirpath/version.rb
|
|
86
|
+
- script/import_vectors.rb
|
|
87
|
+
- script/run_vectors.rb
|
|
88
|
+
homepage: https://github.com/niccoreyes/fhirpath-ruby
|
|
89
|
+
licenses:
|
|
90
|
+
- MIT
|
|
91
|
+
metadata:
|
|
92
|
+
release_status: pre-release
|
|
93
|
+
fhirpath_target: 2.0.0
|
|
94
|
+
capability_set: parser,immutable-ast,collection-evaluation,plain-model-navigation,primitive-values,arithmetic,comparison-and-equivalence,boolean-logic,union-membership-and-type-operators,collection-functions,focus-variables,external-constants,custom-functions,compiled-expression-reuse,fhir-r4-model,structured-errors
|
|
95
|
+
source_code_uri: https://github.com/niccoreyes/fhirpath-ruby
|
|
96
|
+
bug_tracker_uri: https://github.com/niccoreyes/fhirpath-ruby/issues
|
|
97
|
+
documentation_uri: https://github.com/niccoreyes/fhirpath-ruby/blob/main/docs/api.md
|
|
98
|
+
changelog_uri: https://github.com/niccoreyes/fhirpath-ruby/blob/main/CHANGELOG.md
|
|
99
|
+
support_matrix_uri: https://github.com/niccoreyes/fhirpath-ruby/blob/main/docs/support-matrix.md
|
|
100
|
+
rubygems_mfa_required: 'true'
|
|
101
|
+
post_install_message:
|
|
102
|
+
rdoc_options: []
|
|
103
|
+
require_paths:
|
|
104
|
+
- lib
|
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: 3.2.0
|
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '0'
|
|
115
|
+
requirements: []
|
|
116
|
+
rubygems_version: 3.5.22
|
|
117
|
+
signing_key:
|
|
118
|
+
specification_version: 4
|
|
119
|
+
summary: A Ruby foundation for FHIRPath
|
|
120
|
+
test_files: []
|