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,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