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,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FHIRPath
|
|
4
|
+
FunctionSpec = Struct.new(:name, :arity, :parameters, :receiver, :delayed,
|
|
5
|
+
:implementation, keyword_init: true) do
|
|
6
|
+
def initialize(**kwargs)
|
|
7
|
+
super
|
|
8
|
+
self.name = name.to_s.freeze
|
|
9
|
+
self.parameters = Array(parameters).map(&:to_sym).freeze
|
|
10
|
+
freeze
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class FunctionRegistry
|
|
15
|
+
STANDARD_NAMES = %w[
|
|
16
|
+
where select first last tail take skip
|
|
17
|
+
exists count empty not all
|
|
18
|
+
allTrue anyTrue allFalse anyFalse
|
|
19
|
+
sum avg max min
|
|
20
|
+
].freeze
|
|
21
|
+
DELAYED_NAMES = %w[where select exists all].freeze
|
|
22
|
+
|
|
23
|
+
def self.standard
|
|
24
|
+
@standard ||= new(STANDARD_NAMES.each_with_object({}) do |name, specs|
|
|
25
|
+
specs[name] = standard_spec(name)
|
|
26
|
+
end)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.standard_spec(name)
|
|
30
|
+
FunctionSpec.new(
|
|
31
|
+
name: name,
|
|
32
|
+
arity: standard_arity(name),
|
|
33
|
+
parameters: DELAYED_NAMES.include?(name) ? [:expression] : [],
|
|
34
|
+
receiver: :collection,
|
|
35
|
+
delayed: DELAYED_NAMES.include?(name)
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.standard_arity(name)
|
|
40
|
+
return 0..1 if name == 'exists'
|
|
41
|
+
return 1 if name == 'all' || %w[where select take skip].include?(name)
|
|
42
|
+
|
|
43
|
+
0
|
|
44
|
+
end
|
|
45
|
+
private_class_method :standard_arity, :standard_spec
|
|
46
|
+
|
|
47
|
+
def initialize(specs = {})
|
|
48
|
+
@specs = specs.transform_keys(&:to_s).dup.freeze
|
|
49
|
+
freeze
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def find(name)
|
|
53
|
+
@specs[name.to_s]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def include?(name)
|
|
57
|
+
!find(name).nil?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def register(spec)
|
|
61
|
+
key = spec.name.to_s
|
|
62
|
+
raise ArgumentError, "cannot replace standard function: #{key}" if STANDARD_NAMES.include?(key)
|
|
63
|
+
|
|
64
|
+
self.class.new(@specs.merge(key => spec))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def names
|
|
68
|
+
@specs.keys.freeze
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FHIRPath
|
|
4
|
+
# Explicit boundary for external constant lookup. Implementations may perform
|
|
5
|
+
# I/O, but only when an expression requests a constant through this object.
|
|
6
|
+
class ConstantProvider
|
|
7
|
+
def fetch(_name, mode:, context:)
|
|
8
|
+
raise NotImplementedError, 'constant providers must implement #fetch'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(name, mode:, context:)
|
|
12
|
+
fetch(name, mode: mode, context: context)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Optional host work is explicit and absent from pure evaluation by default.
|
|
17
|
+
class HostServices
|
|
18
|
+
attr_reader :constants, :constant_provider, :resolve_reference, :terminology, :element_children, :trace
|
|
19
|
+
|
|
20
|
+
def self.default
|
|
21
|
+
@default ||= new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# rubocop:disable Metrics/ParameterLists
|
|
25
|
+
def initialize(constants: nil, constant_provider: nil, resolve_reference: nil, terminology: nil,
|
|
26
|
+
element_children: nil, trace: nil)
|
|
27
|
+
raise ArgumentError, 'provide either constants or constant_provider, not both' if constants && constant_provider
|
|
28
|
+
|
|
29
|
+
@constant_provider = constant_provider || constants
|
|
30
|
+
@constants = @constant_provider
|
|
31
|
+
@resolve_reference = resolve_reference
|
|
32
|
+
@terminology = terminology
|
|
33
|
+
@element_children = element_children
|
|
34
|
+
@trace = trace
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def constant(name, mode: nil, context: nil)
|
|
39
|
+
unless constants
|
|
40
|
+
raise UnknownConstantError.new("unknown external constant: %#{name}",
|
|
41
|
+
code: :unknown_constant)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
begin
|
|
45
|
+
constants.call(name, mode: mode, context: context)
|
|
46
|
+
rescue StandardError
|
|
47
|
+
raise HostError.new('constant provider failed', code: :host_error), cause: nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
# rubocop:enable Metrics/ParameterLists
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FHIRPath
|
|
4
|
+
# Adapter boundary for navigating host objects without FHIR dependencies.
|
|
5
|
+
class ModelProvider
|
|
6
|
+
def property(_element, _logical_name)
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def root_type(_resource)
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Model type name of the value that `property` resolves for +logical_name+
|
|
15
|
+
# on +element+, or nil when the model does not declare one (e.g. a
|
|
16
|
+
# non-choice field or an absent choice). Safe default: no metadata, no
|
|
17
|
+
# access to the element contents.
|
|
18
|
+
def property_logical_type(_element, _logical_name)
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class PlainModel < ModelProvider
|
|
24
|
+
def root_type(resource)
|
|
25
|
+
return resource['resourceType'] if resource.is_a?(Hash) && resource.key?('resourceType')
|
|
26
|
+
return resource[:resourceType] if resource.is_a?(Hash) && resource.key?(:resourceType)
|
|
27
|
+
|
|
28
|
+
resource.class.name
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def property(element, logical_name)
|
|
32
|
+
case element
|
|
33
|
+
when Hash
|
|
34
|
+
return element[logical_name] if element.key?(logical_name)
|
|
35
|
+
|
|
36
|
+
symbol = logical_name.to_sym
|
|
37
|
+
element[symbol] if element.key?(symbol)
|
|
38
|
+
when Struct
|
|
39
|
+
symbol = logical_name.to_sym
|
|
40
|
+
element[symbol] if element.members.include?(symbol)
|
|
41
|
+
end
|
|
42
|
+
# No dynamic method dispatch: expression-selected paths must not trigger
|
|
43
|
+
# arbitrary application behavior. Custom object navigation belongs in a
|
|
44
|
+
# ModelProvider.
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FHIRPath
|
|
4
|
+
# Named, versioned model providers available to the public API.
|
|
5
|
+
module ModelRegistry
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def fetch(release)
|
|
9
|
+
case release.to_s.downcase
|
|
10
|
+
when 'r4', '4', '4.0.1'
|
|
11
|
+
FHIR::R4::ModelProvider.new
|
|
12
|
+
else
|
|
13
|
+
raise ArgumentError, "unknown FHIR model release: #{release}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FHIRPath
|
|
4
|
+
module FHIR
|
|
5
|
+
module R4
|
|
6
|
+
# Dependency-free adapter for the FHIR R4 JSON representation.
|
|
7
|
+
#
|
|
8
|
+
# The evaluator only knows the ModelProvider protocol. FHIR release
|
|
9
|
+
# metadata stays in this adapter so a different release can provide its
|
|
10
|
+
# own provider without changing the language core.
|
|
11
|
+
class ModelProvider < FHIRPath::ModelProvider
|
|
12
|
+
RELEASE = 'R4'
|
|
13
|
+
VERSION = '4.0.1'
|
|
14
|
+
|
|
15
|
+
CHOICE_FIELDS = {
|
|
16
|
+
'Observation' => {
|
|
17
|
+
'value' => %w[
|
|
18
|
+
valueQuantity valueCodeableConcept valueString valueBoolean
|
|
19
|
+
valueInteger valueRange valueRatio valueSampledData valueTime
|
|
20
|
+
valueDateTime valuePeriod
|
|
21
|
+
].freeze
|
|
22
|
+
}
|
|
23
|
+
}.freeze
|
|
24
|
+
CHOICE_TYPES = {
|
|
25
|
+
'Observation' => {
|
|
26
|
+
'value' => %w[
|
|
27
|
+
Quantity CodeableConcept string boolean integer Range Ratio SampledData time dateTime Period
|
|
28
|
+
].freeze
|
|
29
|
+
}
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
def root_type(resource)
|
|
33
|
+
return resource['resourceType'] if resource.is_a?(Hash) && resource.key?('resourceType')
|
|
34
|
+
return resource[:resourceType] if resource.is_a?(Hash) && resource.key?(:resourceType)
|
|
35
|
+
|
|
36
|
+
resource.class.name
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def property(element, logical_name)
|
|
40
|
+
return direct_property(element, logical_name) if direct_property?(element, logical_name)
|
|
41
|
+
|
|
42
|
+
choice_names = CHOICE_FIELDS.fetch(root_type(element).to_s, {}).fetch(logical_name.to_s, [])
|
|
43
|
+
choice_names.each do |choice_name|
|
|
44
|
+
return element[choice_name] if element.is_a?(Hash) && element.key?(choice_name)
|
|
45
|
+
return element[choice_name.to_sym] if element.is_a?(Hash) && element.key?(choice_name.to_sym)
|
|
46
|
+
end
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def choice_types(parent_type, logical_name)
|
|
51
|
+
CHOICE_TYPES.fetch(parent_type.to_s, {}).fetch(logical_name.to_s, []).freeze
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Model type name of the choice variant that `property` resolves for
|
|
55
|
+
# +logical_name+ on +element+, using the same key order as CHOICE_FIELDS
|
|
56
|
+
# so the concrete variant maps to its declared type (valueQuantity ->
|
|
57
|
+
# Quantity, valueString -> string). Returns nil when no choice variant
|
|
58
|
+
# is present or the property is not a declared choice.
|
|
59
|
+
def property_logical_type(element, logical_name)
|
|
60
|
+
choice_names = CHOICE_FIELDS.fetch(root_type(element).to_s, {}).fetch(logical_name.to_s, [])
|
|
61
|
+
return nil if choice_names.empty?
|
|
62
|
+
|
|
63
|
+
index = choice_names.index do |choice_name|
|
|
64
|
+
element.is_a?(Hash) &&
|
|
65
|
+
(element.key?(choice_name) || element.key?(choice_name.to_sym))
|
|
66
|
+
end
|
|
67
|
+
return nil if index.nil?
|
|
68
|
+
|
|
69
|
+
CHOICE_TYPES.fetch(root_type(element).to_s, {}).fetch(logical_name.to_s, [])[index]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def type_of(element)
|
|
73
|
+
root_type(element)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def direct_property?(element, logical_name)
|
|
79
|
+
element.is_a?(Hash) &&
|
|
80
|
+
(element.key?(logical_name.to_s) || element.key?(logical_name.to_sym))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def direct_property(element, logical_name)
|
|
84
|
+
return element[logical_name.to_s] if element.key?(logical_name.to_s)
|
|
85
|
+
|
|
86
|
+
element[logical_name.to_sym]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|