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



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

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)


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

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

#current_and_child_values_match?(el_found, value_definitions_for_path) ⇒ Boolean

Returns:

  • (Boolean)


346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 346

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



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

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

#field_value(element, field_name) ⇒ Object



145
146
147
148
149
150
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 145

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

  • given_element (FHIR::Model, Array<FHIR::Model>)
  • path (String)
  • include_dar (Boolean) (defaults to: false)
  • recursive_segments (Array<String>) (defaults to: [])

    names of path segments that are self-referential (eg. item in Questionnaire.item.item, via a FHIR contentReference). When a segment matches, the search also continues into deeper repeats of that segment (item.item.item...) if the value isn't found at the literal path depth.

Returns:

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



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 51

def find_a_value_at(given_element, path, include_dar: false, recursive_segments: [], &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:, recursive_segments:, &block)
    return element_found if value_not_empty?(element_found)

    next unless recursive_segments.include?(segment)

    nested_found = find_a_value_at(child, path, include_dar:, recursive_segments:, &block)
    return nested_found if value_not_empty?(nested_found)
  end

  nil
end

#find_in_elements(elements, include_dar: false) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 77

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



221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 221

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



375
376
377
378
379
380
381
382
383
384
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 375

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



90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 90

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



153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 153

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



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

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)


254
255
256
257
258
259
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 254

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)


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

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)


268
269
270
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 268

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

#matching_required_binding_slice?(slice, discriminator) ⇒ Boolean

Returns:

  • (Boolean)


303
304
305
306
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 303

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)


236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 236

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)


279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 279

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)


273
274
275
276
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 273

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



180
181
182
183
184
185
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 180

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



130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 130

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



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

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)


320
321
322
323
324
325
326
327
328
329
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 320

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)


119
120
121
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 119

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

#sliced_choice_path?(property) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#sliced_choice_value(element, property) ⇒ Object



124
125
126
127
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 124

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



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

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



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 188

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)


364
365
366
367
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 364

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)


370
371
372
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 370

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

#verify_slice_by_values(element, value_definitions) ⇒ Object



332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/inferno/dsl/fhir_resource_navigation.rb', line 332

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