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,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: []