Class: Inferno::DSL::MustSupportAssessment::InternalMustSupportLogic

Inherits:
Object
  • Object
show all
Includes:
FHIRResourceNavigation
Defined in:
lib/inferno/dsl/must_support_assessment.rb

Constant Summary

Constants included from FHIRResourceNavigation

FHIRResourceNavigation::DAR_EXTENSION_URL, FHIRResourceNavigation::PRIMITIVE_DATA_TYPES

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from FHIRResourceNavigation

#append_path_character, #choice_path?, #current_and_child_values_match?, #extension_filter_value, #field_value, #find_a_value_at, #find_in_elements, #find_slice_via_discriminator, #flatten_bundles, #get_next_value, #get_primitive_type_value, #local_field_name, #matching_pattern_codeable_concept_slice?, #matching_pattern_coding_slice?, #matching_pattern_identifier_slice?, #matching_required_binding_slice?, #matching_slice?, #matching_type_slice?, #matching_value_slice?, #path_segments, #populated_choice_value, #required_binding_codings, #resolve_path, #slice_path?, #sliced_choice_path?, #sliced_choice_value, #split_path_segment_or_append, #update_path_segment_state, #value_at_path_matches?, #value_not_empty?, #verify_slice_by_values

Constructor Details

#initialize(metadata = nil) ⇒ InternalMustSupportLogic

Returns a new instance of InternalMustSupportLogic.



68
69
70
# File 'lib/inferno/dsl/must_support_assessment.rb', line 68

def initialize( = nil)
  @metadata = 
end

Instance Attribute Details

#metadataObject

Returns the value of attribute metadata.



66
67
68
# File 'lib/inferno/dsl/must_support_assessment.rb', line 66

def 
  @metadata
end

Instance Method Details

#any_choice_supported?(choices) ⇒ Boolean

Returns:

  • (Boolean)


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

def any_choice_supported?(choices)
  return false unless choices.present?

  any_path_choice_supported?(choices) ||
    any_extension_ids_choice_supported?(choices) ||
    any_slice_names_choice_supported?(choices) ||
    any_elements_choice_supported?(choices)
end

#any_elements_choice_supported?(choices) ⇒ Boolean

Returns:

  • (Boolean)


174
175
176
177
178
179
180
181
182
# File 'lib/inferno/dsl/must_support_assessment.rb', line 174

def any_elements_choice_supported?(choices)
  return false unless choices[:elements].present?

  choices[:elements].any? do |choice|
    missing_elements.none? do |element|
      element[:path] == choice[:path] && element[:fixed_value] == choice[:fixed_value]
    end
  end
end

#any_extension_ids_choice_supported?(choices) ⇒ Boolean

Returns:

  • (Boolean)


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

def any_extension_ids_choice_supported?(choices)
  return false unless choices[:extension_ids].present?

  choices[:extension_ids].any? do |extension_id|
    missing_extensions.none? { |extension| extension[:id] == extension_id }
  end
end

#any_path_choice_supported?(choices) ⇒ Boolean

Returns:

  • (Boolean)


154
155
156
157
158
# File 'lib/inferno/dsl/must_support_assessment.rb', line 154

def any_path_choice_supported?(choices)
  return false unless choices[:paths].present?

  choices[:paths].any? { |path| missing_elements.none? { |element| element[:path] == path } }
end

#any_slice_names_choice_supported?(choices) ⇒ Boolean

Returns:

  • (Boolean)


168
169
170
171
172
# File 'lib/inferno/dsl/must_support_assessment.rb', line 168

def any_slice_names_choice_supported?(choices)
  return false unless choices[:slice_names].present?

  choices[:slice_names].any? { |slice_name| missing_slices.none? { |slice| slice[:name] == slice_name } }
end

#extension_definition_candidates(path_matching_extensions) ⇒ Object



287
288
289
290
291
292
293
# File 'lib/inferno/dsl/must_support_assessment.rb', line 287

def extension_definition_candidates(path_matching_extensions)
  [
    [path_matching_extensions, :end_with?],
    [path_matching_extensions, :include?],
    [must_support_extensions, :end_with?]
  ]
end

#extension_urls(value) ⇒ Object



333
334
335
# File 'lib/inferno/dsl/must_support_assessment.rb', line 333

def extension_urls(value)
  Array.wrap(value.extension).map { |extension| normalized_extension_url(extension.url) }
end

#extract_metadata(profile, ig, requirement_extension: nil) ⇒ Object



89
90
91
# File 'lib/inferno/dsl/must_support_assessment.rb', line 89

def (profile, ig, requirement_extension: nil)
  MustSupportMetadataExtractor.new(profile.snapshot.element, profile, profile.type, ig, requirement_extension)
end

#find_missing_elements(resources, must_support_elements) ⇒ Object



233
234
235
236
237
# File 'lib/inferno/dsl/must_support_assessment.rb', line 233

def find_missing_elements(resources, must_support_elements)
  must_support_elements.select do |element_definition|
    resources.none? { |resource| resource_populates_element?(resource, element_definition) }
  end
end

#find_pattern_codeable_concept_slice(element, discriminator) ⇒ Object



390
391
392
393
394
395
# File 'lib/inferno/dsl/must_support_assessment.rb', line 390

def find_pattern_codeable_concept_slice(element, discriminator)
  coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
  find_a_value_at(element, coding_path) do |coding|
    coding.code == discriminator[:code] && coding.system == discriminator[:system]
  end
end

#find_pattern_coding_slice(element, discriminator) ⇒ Object



397
398
399
400
401
402
# File 'lib/inferno/dsl/must_support_assessment.rb', line 397

def find_pattern_coding_slice(element, discriminator)
  coding_path = discriminator[:path].present? ? discriminator[:path] : ''
  find_a_value_at(element, coding_path) do |coding|
    coding.code == discriminator[:code] && coding.system == discriminator[:system]
  end
end

#find_pattern_identifier_slice(element, discriminator) ⇒ Object



404
405
406
407
408
# File 'lib/inferno/dsl/must_support_assessment.rb', line 404

def find_pattern_identifier_slice(element, discriminator)
  find_a_value_at(element, discriminator[:path]) do |identifier|
    identifier.system == discriminator[:system]
  end
end

#find_required_binding_slice(element, discriminator) ⇒ Object



441
442
443
444
445
446
447
448
449
450
451
# File 'lib/inferno/dsl/must_support_assessment.rb', line 441

def find_required_binding_slice(element, discriminator)
  if element.is_a?(FHIR::Coding) && required_binding_value_match?(element, discriminator[:values])
    return element
  end

  coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'

  find_a_value_at(element, coding_path) do |coding|
    required_binding_value_match?(coding, discriminator[:values])
  end
end

#find_slice(resource, path, discriminator) ⇒ Object



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/inferno/dsl/must_support_assessment.rb', line 368

def find_slice(resource, path, discriminator)
  # TODO: there is a lot of similarity
  # between this and FHIRResourceNavigation.matching_slice?
  # Can these be combined?
  find_a_value_at(resource, path) do |element|
    case discriminator[:type]
    when 'patternCodeableConcept'
      find_pattern_codeable_concept_slice(element, discriminator)
    when 'patternCoding'
      find_pattern_coding_slice(element, discriminator)
    when 'patternIdentifier'
      find_pattern_identifier_slice(element, discriminator)
    when 'value'
      find_value_slice(element, discriminator)
    when 'type'
      find_type_slice(element, discriminator)
    when 'requiredBinding'
      find_required_binding_slice(element, discriminator)
    end
  end
end

#find_slice_by_values(element, value_definitions) ⇒ Object



453
454
455
# File 'lib/inferno/dsl/must_support_assessment.rb', line 453

def find_slice_by_values(element, value_definitions)
  Array.wrap(element).find { |el| verify_slice_by_values(el, value_definitions) }
end

#find_type_slice(element, discriminator) ⇒ Object



415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/inferno/dsl/must_support_assessment.rb', line 415

def find_type_slice(element, discriminator)
  case discriminator[:code]
  when 'Date'
    begin
      Date.parse(element)
    rescue ArgumentError, TypeError
      false
    end
  when 'DateTime'
    begin
      DateTime.parse(element)
    rescue ArgumentError, TypeError
      false
    end
  when 'String'
    element.is_a? String
  else
    if element.is_a? FHIR::Bundle::Entry
      # Special case for type slicing in a Bundle - look at the resource not the entry
      element = element.resource
    end

    element.is_a? FHIR.const_get(discriminator[:code])
  end
end

#find_value_slice(element, discriminator) ⇒ Object



410
411
412
413
# File 'lib/inferno/dsl/must_support_assessment.rb', line 410

def find_value_slice(element, discriminator)
  values = discriminator[:values].map { |value| value.merge(path: path_segments(value[:path])) }
  find_slice_by_values(element, values)
end

#handle_must_support_choicesObject



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

def handle_must_support_choices
  handle_must_support_element_choices
  handle_must_support_extension_choices
  handle_must_support_slice_choices
end

#handle_must_support_element_choicesObject



119
120
121
122
123
124
125
126
127
# File 'lib/inferno/dsl/must_support_assessment.rb', line 119

def handle_must_support_element_choices
  missing_elements.delete_if do |element|
    choices = .must_supports[:choices].find do |choice|
      choice[:paths]&.include?(element[:path]) ||
        choice[:elements]&.any? { |ms_element| ms_element[:path] == element[:path] }
    end
    any_choice_supported?(choices)
  end
end

#handle_must_support_extension_choicesObject



129
130
131
132
133
134
135
136
# File 'lib/inferno/dsl/must_support_assessment.rb', line 129

def handle_must_support_extension_choices
  missing_extensions.delete_if do |extension|
    choices = .must_supports[:choices].find do |choice|
      choice[:extension_ids]&.include?(extension[:id])
    end
    any_choice_supported?(choices)
  end
end

#handle_must_support_slice_choicesObject



138
139
140
141
142
143
# File 'lib/inferno/dsl/must_support_assessment.rb', line 138

def handle_must_support_slice_choices
  missing_slices.delete_if do |slice|
    choices = .must_supports[:choices].find { |choice| choice[:slice_names]&.include?(slice[:name]) }
    any_choice_supported?(choices)
  end
end

#matches_fixed_value?(value, fixed_value) ⇒ Boolean

Returns:

  • (Boolean)


345
346
347
# File 'lib/inferno/dsl/must_support_assessment.rb', line 345

def matches_fixed_value?(value, fixed_value)
  fixed_value.blank? || value == fixed_value
end

#matching_without_extensions?(value, ms_extension_urls, fixed_value) ⇒ Boolean

Returns:

  • (Boolean)


317
318
319
320
321
322
323
324
325
# File 'lib/inferno/dsl/must_support_assessment.rb', line 317

def matching_without_extensions?(value, ms_extension_urls, fixed_value)
  has_ms_extension = must_support_extension_present?(value, ms_extension_urls)

  value = value.value if value.instance_of?(Inferno::DSL::PrimitiveType)

  return false unless has_ms_extension || value_without_extensions?(value)

  matches_fixed_value?(value, fixed_value)
end

#missing_element_string(element_definition) ⇒ Object



190
191
192
193
194
195
196
# File 'lib/inferno/dsl/must_support_assessment.rb', line 190

def missing_element_string(element_definition)
  if element_definition[:fixed_value].present?
    "#{element_definition[:path]}:#{element_definition[:fixed_value]}"
  else
    element_definition[:path]
  end
end

#missing_elements(resources = []) ⇒ Object



229
230
231
# File 'lib/inferno/dsl/must_support_assessment.rb', line 229

def missing_elements(resources = [])
  @missing_elements ||= find_missing_elements(resources, must_support_elements)
end

#missing_extensions(resources = []) ⇒ Object



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/inferno/dsl/must_support_assessment.rb', line 202

def missing_extensions(resources = [])
  @missing_extensions ||=
    must_support_extensions.select do |extension_definition|
      expected_url = normalized_extension_url(extension_definition[:url])

      resources.none? do |resource|
        path = extension_definition[:path]

        if path == 'extension'
          Array.wrap(resource.extension).any? do |extension|
            normalized_extension_url(extension.url) == expected_url
          end
        else
          extension = find_a_value_at(resource, path) do |el|
            normalized_extension_url(el.url) == expected_url
          end

          extension.present?
        end
      end
    end
end

#missing_must_support_stringsObject



184
185
186
187
188
# File 'lib/inferno/dsl/must_support_assessment.rb', line 184

def missing_must_support_strings
  missing_elements.map { |element_definition| missing_element_string(element_definition) } +
    missing_slices.map { |slice_definition| slice_definition[:slice_id] } +
    missing_extensions.map { |extension_definition| extension_definition[:id] }
end

#missing_slices(resources = []) ⇒ Object



358
359
360
361
362
363
364
365
366
# File 'lib/inferno/dsl/must_support_assessment.rb', line 358

def missing_slices(resources = [])
  @missing_slices ||=
    must_support_slices.select do |slice|
      resources.none? do |resource|
        path = slice[:path]
        find_slice(resource, path, slice[:discriminator]).present?
      end
    end
end

#must_support_elementsObject



225
226
227
# File 'lib/inferno/dsl/must_support_assessment.rb', line 225

def must_support_elements
  .must_supports[:elements]
end

#must_support_extension_definition(extension_path, extension_type, extension_name) ⇒ Object



275
276
277
278
279
280
281
282
283
284
285
# File 'lib/inferno/dsl/must_support_assessment.rb', line 275

def must_support_extension_definition(extension_path, extension_type, extension_name)
  suffix = "#{extension_type}:#{extension_name}"
  path_matching_extensions = must_support_extensions.select { |definition| definition[:path] == extension_path }

  extension_definition_candidates(path_matching_extensions).each do |definitions, matcher|
    match = definitions.find { |definition| definition[:id].public_send(matcher, suffix) }
    return match if match.present?
  end

  nil
end

#must_support_extension_present?(value, ms_extension_urls) ⇒ Boolean

Returns:

  • (Boolean)


327
328
329
330
331
# File 'lib/inferno/dsl/must_support_assessment.rb', line 327

def must_support_extension_present?(value, ms_extension_urls)
  return false unless value.respond_to?(:extension)

  (extension_urls(value) & normalized_extension_urls(ms_extension_urls)).present?
end

#must_support_extensionsObject



198
199
200
# File 'lib/inferno/dsl/must_support_assessment.rb', line 198

def must_support_extensions
  .must_supports[:extensions]
end

#must_support_slicesObject



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

def must_support_slices
  .must_supports[:slices]
end


254
255
256
257
258
259
260
261
262
# File 'lib/inferno/dsl/must_support_assessment.rb', line 254

def navigation_compatible_must_support_path(path)
  logical_segments = []

  path_segments(path).map do |segment|
    normalized_segment = normalized_must_support_path_segment(segment, logical_segments)
    logical_segments << segment.split(':').first
    normalized_segment
  end.join('.')
end

#normalized_extension_url(url) ⇒ Object



341
342
343
# File 'lib/inferno/dsl/must_support_assessment.rb', line 341

def normalized_extension_url(url)
  url&.split('|')&.first
end

#normalized_extension_urls(urls) ⇒ Object



337
338
339
# File 'lib/inferno/dsl/must_support_assessment.rb', line 337

def normalized_extension_urls(urls)
  Array.wrap(urls).map { |url| normalized_extension_url(url) }
end

#normalized_must_support_path_segment(segment, logical_segments) ⇒ Object



264
265
266
267
268
269
270
271
272
273
# File 'lib/inferno/dsl/must_support_assessment.rb', line 264

def normalized_must_support_path_segment(segment, logical_segments)
  extension_type, extension_name = segment.match(/\A(modifierExtension|extension):(.+)\z/)&.captures
  return segment if extension_type.blank?

  extension_path = [logical_segments.join('.'), extension_type].reject(&:blank?).join('.')
  extension_definition = must_support_extension_definition(extension_path, extension_type, extension_name)
  return segment if extension_definition.blank?

  "#{extension_type}.where(url='#{normalized_extension_url(extension_definition[:url])}')"
end

#perform_must_support_test_with_metadata(resources, profile_metadata, debug_metadata: false) ⇒ Array<String>

perform_must_support_test_with_metadata is invoked from check and perform_must_support_test, with the metadata to be used as the basis for the test. It may also be invoked directly from a test if you want to completely overwrite the metadata.

Parameters:

  • resources (Array<FHIR::Model>)
  • profile_metadata (Metadata)

    Metadata object with must_supports field

  • debug_metadata (Boolean) (defaults to: false)

    if true, write out the final metadata used to a temporary file

Returns:

  • (Array<String>)

    list of elements that were not found in the provided resources



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

def (resources, , debug_metadata: false)
  return if resources.blank?

  @metadata = 

   if 

  perform_test(resources)
end

#perform_test(resources) ⇒ Object



103
104
105
106
107
108
109
110
111
# File 'lib/inferno/dsl/must_support_assessment.rb', line 103

def perform_test(resources)
  missing_elements(resources)
  missing_slices(resources)
  missing_extensions(resources)

  handle_must_support_choices if .must_supports[:choices].present?

  missing_must_support_strings
end

#process_must_support_element_in_extension(resource, path) ⇒ Object



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/inferno/dsl/must_support_assessment.rb', line 295

def process_must_support_element_in_extension(resource, path)
  return [resource, path] unless path.start_with?('extension:')

  path_without_prefix = path.delete_prefix('extension:')
  extension_split = path_without_prefix.split('.')
  extension_name = extension_split.first
  extension_path = extension_split.last

  found_extension_url =
    normalized_extension_url(must_support_extensions.find { |ex| ex[:id].include?(extension_name) }[:url])
  ms_element_extension = resource.extension.find do |extension|
    normalized_extension_url(extension.url) == found_extension_url
  end

  if ms_element_extension.present?
    resource = ms_element_extension
    path = extension_path
  end

  [resource, path]
end

#required_binding_value_match?(coding, values) ⇒ Boolean

Returns:

  • (Boolean)


457
458
459
460
461
462
463
464
465
466
# File 'lib/inferno/dsl/must_support_assessment.rb', line 457

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

#resource_populates_element?(resource, element_definition) ⇒ Boolean

Returns:

  • (Boolean)


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

def resource_populates_element?(resource, element_definition)
  raw_path = element_definition[:path]
  path = navigation_compatible_must_support_path(raw_path)

  ms_extension_urls = must_support_extensions.select { |ex| ex[:path] == "#{raw_path}.extension" }
    .map { |ex| ex[:url] }

  value_found = find_a_value_at(resource, path) do |potential_value|
    matching_without_extensions?(potential_value, ms_extension_urls, element_definition[:fixed_value])
  end

  # Note that false.present? => false, which is why we need to add this extra check
  value_found.present? || value_found == false
end

#value_without_extensions?(value) ⇒ Boolean

Returns:

  • (Boolean)


349
350
351
352
# File 'lib/inferno/dsl/must_support_assessment.rb', line 349

def value_without_extensions?(value)
  value_without_extensions = value.respond_to?(:to_hash) ? value.to_hash.except('extension') : value
  value_without_extensions.present? || value_without_extensions == false
end

#write_metadata_for_debuggingObject



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

def 
  outfile = "#{.profile&.id}-#{SecureRandom.uuid}.yml"

  File.open(File.join(Dir.tmpdir, outfile), 'w') do |f|
     = { must_supports: @metadata.must_supports.to_hash }
    f.write(YAML.dump())
    puts "Wrote MustSupport metadata to #{f.path}"
  end
end