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