Module: Inferno::DSL::FHIRResourceNavigation

Included in:
Inferno::DSL::FHIREvaluation::Rules::AllMustSupportsPresent, MustSupportAssessment::InternalMustSupportLogic
Defined in:
lib/inferno/dsl/fhir_resource_navigation.rb

Overview

The FHIRResourceNavigation module is used to pick values from a FHIR resource, based on a profile. Originally intended for use for verifying the presence of Must Support elements on a resource and finding values to use for search parameters. The methods in this module related to slices expects pre-processed metadata defining the elements of the profile to be present in the attribute ‘metadata` in the including class.

Constant Summary collapse

DAR_EXTENSION_URL =
'http://hl7.org/fhir/StructureDefinition/data-absent-reason'.freeze
PRIMITIVE_DATA_TYPES =
FHIR::PRIMITIVES.keys

Instance Method Summary collapse

Instance Method Details

#append_path_character(state, char, depth_change:) ⇒ Object



195
196
197
198
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 195

def append_path_character(state, char, depth_change:)
  state[:current_segment] << char
  state[:parentheses_depth] += depth_change unless state[:in_quotes]
end

#choice_path?(property) ⇒ Boolean

Returns:

  • (Boolean)


104
105
106
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 104

def choice_path?(property)
  property.end_with?('[x]')
end

#current_and_child_values_match?(el_found, value_definitions_for_path) ⇒ Boolean

Returns:

  • (Boolean)


336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 336

def current_and_child_values_match?(el_found, value_definitions_for_path)
  child_element_value_definitions, current_element_value_definitions =
    value_definitions_for_path.partition { |value_definition| value_definition[:path].present? }

  current_element_values_match =
    current_element_value_definitions
      .all? { |value_definition| value_definition[:value] == el_found }

  child_element_values_match =
    if child_element_value_definitions.present?
      verify_slice_by_values(el_found, child_element_value_definitions)
    else
      true
    end
  current_element_values_match && child_element_values_match
end

#extension_filter_value(element, extension_url) ⇒ Object



94
95
96
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 94

def extension_filter_value(element, extension_url)
  element.url == extension_url ? element : nil
end

#field_value(element, field_name) ⇒ Object



135
136
137
138
139
140
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 135

def field_value(element, field_name)
  local_name = local_field_name(field_name)
  value = element.send(local_name)
  primitive_value = get_primitive_type_value(element, field_name, value)
  primitive_value.present? ? primitive_value : value
end

#find_a_value_at(given_element, path, include_dar: false, &block) ⇒ Object

Get a value from the given FHIR element(s), by navigating through the resource to the given path. Fields with a DataAbsentReason extension present will be excluded unless include_dar is true. To filter the resulting elements, a block may be passed in.

Parameters:

Returns:

  • a single matching value (which can include ‘false`) or `nil` if not found



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 46

def find_a_value_at(given_element, path, include_dar: false, &block)
  return nil if given_element.nil?

  elements = Array.wrap(given_element)
  return find_in_elements(elements, include_dar:, &block) if path.empty?

  path_segments = path_segments(path)

  segment = path_segments.shift

  remaining_path = path_segments.join('.')
  elements.each do |element|
    child = get_next_value(element, segment)
    element_found = find_a_value_at(child, remaining_path, include_dar:, &block)
    return element_found if value_not_empty?(element_found)
  end

  nil
end

#find_in_elements(elements, include_dar: false) ⇒ Object



67
68
69
70
71
72
73
74
75
76
77
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 67

def find_in_elements(elements, include_dar: false, &)
  unless include_dar
    elements = elements.reject do |el|
      el.respond_to?(:extension) && el.extension.any? { |ext| ext.url == DAR_EXTENSION_URL }
    end
  end

  return elements.find(&) if block_given?

  elements.first
end

#find_slice_via_discriminator(element, property) ⇒ Object



211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 211

def find_slice_via_discriminator(element, property)
  return unless .present?

  element_name = local_field_name(property.to_s.split(':')[0])
  slice_name = local_field_name(property.to_s.split(':')[1])

  slice_by_name = .must_supports[:slices].find { |slice| slice[:slice_name] == slice_name }
  return nil if slice_by_name.blank?

  discriminator = slice_by_name[:discriminator]
  slices = Array.wrap(element.send(element_name))
  slices.find { |slice| matching_slice?(slice, discriminator) }
end

#flatten_bundles(resources) ⇒ Object



365
366
367
368
369
370
371
372
373
374
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 365

def flatten_bundles(resources)
  resources.flat_map do |resource|
    if resource&.resourceType == 'Bundle'
      # Recursive to consider that Bundles may contain Bundles
      flatten_bundles(resource.entry.map(&:resource))
    else
      resource
    end
  end
end

#get_next_value(element, property) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 80

def get_next_value(element, property)
  property = property.to_s
  extension_url = property[/(?<=where\(url=').*(?='\))/]
  return extension_filter_value(element, extension_url) if extension_url.present?
  return sliced_choice_value(element, property) if sliced_choice_path?(property)
  return populated_choice_value(element, property) if choice_path?(property)
  return find_slice_via_discriminator(element, property) if slice_path?(property)

  field_value(element, property)
rescue NoMethodError
  nil
end

#get_primitive_type_value(element, property, value) ⇒ Object



143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 143

def get_primitive_type_value(element, property, value)
  return nil unless element.respond_to?(:source_hash)

  source_hash = element.source_hash
  return nil unless source_hash.present?

  source_value = source_hash["_#{property}"]

  return nil unless source_value.present?

  primitive_value = PrimitiveType.new(source_value)
  primitive_value.value = value
  primitive_value
end

#local_field_name(field_name) ⇒ Object



159
160
161
162
163
164
165
166
167
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 159

def local_field_name(field_name)
  # fhir_models prepends fields whose names are reserved in ruby with "local_"
  # This should be used before `x.send(field_name)`
  if ['method', 'class'].include?(field_name.to_s)
    "local_#{field_name}"
  else
    field_name
  end
end

#matching_pattern_codeable_concept_slice?(slice, discriminator) ⇒ Boolean

Returns:

  • (Boolean)


244
245
246
247
248
249
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 244

def matching_pattern_codeable_concept_slice?(slice, discriminator)
  slice_value = discriminator[:path].present? ? slice.send((discriminator[:path]).to_s)&.coding : slice.coding
  slice_value&.any? do |coding|
    coding.code == discriminator[:code] && coding.system == discriminator[:system]
  end
end

#matching_pattern_coding_slice?(slice, discriminator) ⇒ Boolean

Returns:

  • (Boolean)


252
253
254
255
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 252

def matching_pattern_coding_slice?(slice, discriminator)
  slice_value = discriminator[:path].present? ? slice.send(discriminator[:path]) : slice
  slice_value&.code == discriminator[:code] && slice_value&.system == discriminator[:system]
end

#matching_pattern_identifier_slice?(slice, discriminator) ⇒ Boolean

Returns:

  • (Boolean)


258
259
260
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 258

def matching_pattern_identifier_slice?(slice, discriminator)
  slice.system == discriminator[:system]
end

#matching_required_binding_slice?(slice, discriminator) ⇒ Boolean

Returns:

  • (Boolean)


293
294
295
296
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 293

def matching_required_binding_slice?(slice, discriminator)
  slice_coding = required_binding_codings(slice, discriminator)
  slice_coding.any? { |coding| required_binding_value_match?(coding, discriminator[:values]) }
end

#matching_slice?(slice, discriminator) ⇒ Boolean

Returns:

  • (Boolean)


226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 226

def matching_slice?(slice, discriminator)
  case discriminator[:type]
  when 'patternCodeableConcept'
    matching_pattern_codeable_concept_slice?(slice, discriminator)
  when 'patternCoding'
    matching_pattern_coding_slice?(slice, discriminator)
  when 'patternIdentifier'
    matching_pattern_identifier_slice?(slice, discriminator)
  when 'value'
    matching_value_slice?(slice, discriminator)
  when 'type'
    matching_type_slice?(slice, discriminator)
  when 'requiredBinding'
    matching_required_binding_slice?(slice, discriminator)
  end
end

#matching_type_slice?(slice, discriminator) ⇒ Boolean

Returns:

  • (Boolean)


269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 269

def matching_type_slice?(slice, discriminator)
  slice_value = resolve_path(slice, discriminator[:path]).first

  case discriminator[:code]
  when 'Date'
    begin
      Date.parse(slice_value)
    rescue ArgumentError
      false
    end
  when 'DateTime'
    begin
      DateTime.parse(slice_value)
    rescue ArgumentError
      false
    end
  when 'String'
    slice_value.is_a? String
  else
    slice_value.is_a? FHIR.const_get(discriminator[:code])
  end
end

#matching_value_slice?(slice, discriminator) ⇒ Boolean

Returns:

  • (Boolean)


263
264
265
266
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 263

def matching_value_slice?(slice, discriminator)
  values = discriminator[:values].map { |value| value.merge(path: path_segments(value[:path])) }
  verify_slice_by_values(slice, values)
end

#path_segments(path) ⇒ Object



170
171
172
173
174
175
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 170

def path_segments(path)
  state = { current_segment: +'', segments: [], parentheses_depth: 0, in_quotes: false }
  path.each_char { |char| update_path_segment_state(state, char) }
  state[:segments] << state[:current_segment] unless state[:current_segment].empty?
  state[:segments]
end

#populated_choice_value(element, property) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 120

def populated_choice_value(element, property)
  choice_prefix = property.delete_suffix('[x]')
  populated_field =
    Array.wrap(element.to_hash&.keys)
      .map(&:to_s)
      .find do |field_name|
        field_name.start_with?(choice_prefix) && value_not_empty?(field_value(element, field_name))
      end

  return nil if populated_field.blank?

  field_value(element, populated_field)
end

#required_binding_codings(slice, discriminator) ⇒ Object



299
300
301
302
303
304
305
306
307
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 299

def required_binding_codings(slice, discriminator)
  if discriminator[:path].present?
    Array.wrap(resolve_path(slice, discriminator[:path])).flat_map { |value| Array.wrap(value&.coding) }
  elsif slice.is_a?(FHIR::Coding)
    [slice]
  else
    Array.wrap(slice.coding)
  end
end

#required_binding_value_match?(coding, values) ⇒ Boolean

Returns:

  • (Boolean)


310
311
312
313
314
315
316
317
318
319
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 310

def required_binding_value_match?(coding, values)
  values.any? do |value|
    case value
    when String
      value == coding.code
    when Hash
      value[:system] == coding.system && value[:code] == coding.code
    end
  end
end

#resolve_path(elements, path) ⇒ Array<FHIR::Model>

Get a value from the given FHIR element(s) by walking the given path through the element.

Parameters:

Returns:



23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 23

def resolve_path(elements, path)
  elements = Array.wrap(elements)
  return elements if path.blank?

  paths = path_segments(path)
  segment = paths.first
  remaining_path = paths.drop(1).join('.')

  elements.flat_map do |element|
    child = get_next_value(element, segment)
    resolve_path(child, remaining_path)
  end.compact
end

#slice_path?(property) ⇒ Boolean

Returns:

  • (Boolean)


109
110
111
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 109

def slice_path?(property)
  property.include?(':') && !property.include?('url')
end

#sliced_choice_path?(property) ⇒ Boolean

Returns:

  • (Boolean)


99
100
101
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 99

def sliced_choice_path?(property)
  property.include?('[x]:')
end

#sliced_choice_value(element, property) ⇒ Object



114
115
116
117
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 114

def sliced_choice_value(element, property)
  _choice_path, sliced_field = property.split(':', 2)
  field_value(element, sliced_field)
end

#split_path_segment_or_append(state, char) ⇒ Object



201
202
203
204
205
206
207
208
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 201

def split_path_segment_or_append(state, char)
  if state[:parentheses_depth].zero? && !state[:in_quotes]
    state[:segments] << state[:current_segment].dup
    state[:current_segment].clear
  else
    state[:current_segment] << char
  end
end

#update_path_segment_state(state, char) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 178

def update_path_segment_state(state, char)
  case char
  when "'"
    state[:current_segment] << char
    state[:in_quotes] = !state[:in_quotes]
  when '('
    append_path_character(state, char, depth_change: 1)
  when ')'
    append_path_character(state, char, depth_change: -1)
  when '.'
    split_path_segment_or_append(state, char)
  else
    state[:current_segment] << char
  end
end

#value_at_path_matches?(element, path, include_dar: false) ⇒ Boolean

Returns:

  • (Boolean)


354
355
356
357
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 354

def value_at_path_matches?(element, path, include_dar: false, &)
  value_found = find_a_value_at(element, path, include_dar:, &)
  value_not_empty?(value_found)
end

#value_not_empty?(value) ⇒ Boolean

Returns:

  • (Boolean)


360
361
362
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 360

def value_not_empty?(value)
  value.present? || value == false
end

#verify_slice_by_values(element, value_definitions) ⇒ Object



322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 322

def verify_slice_by_values(element, value_definitions)
  path_prefixes = value_definitions.map { |value_definition| value_definition[:path].first }.uniq
  path_prefixes.all? do |path_prefix|
    value_definitions_for_path =
      value_definitions
        .select { |value_definition| value_definition[:path].first == path_prefix }
        .each { |value_definition| value_definition[:path].shift }
    value_at_path_matches?(element, path_prefix) do |el_found|
      current_and_child_values_match?(el_found, value_definitions_for_path)
    end
  end
end