inferno_core 1.1.2 → 1.2.0

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/lib/inferno/apps/cli/execute_script.rb +918 -0
  3. data/lib/inferno/apps/cli/main.rb +46 -0
  4. data/lib/inferno/apps/cli/session/cancel_run.rb +47 -0
  5. data/lib/inferno/apps/cli/session/connection.rb +47 -0
  6. data/lib/inferno/apps/cli/session/create_session.rb +159 -0
  7. data/lib/inferno/apps/cli/session/errors.rb +45 -0
  8. data/lib/inferno/apps/cli/session/session_compare.rb +390 -0
  9. data/lib/inferno/apps/cli/session/session_data.rb +39 -0
  10. data/lib/inferno/apps/cli/session/session_details.rb +27 -0
  11. data/lib/inferno/apps/cli/session/session_results.rb +39 -0
  12. data/lib/inferno/apps/cli/session/session_status.rb +69 -0
  13. data/lib/inferno/apps/cli/session/start_run.rb +245 -0
  14. data/lib/inferno/apps/cli/session_commands.rb +66 -0
  15. data/lib/inferno/apps/cli/templates/%library_name%.gemspec.tt +1 -1
  16. data/lib/inferno/apps/cli/templates/.gitignore +4 -0
  17. data/lib/inferno/apps/cli/templates/README.md.tt +14 -0
  18. data/lib/inferno/apps/cli/templates/Rakefile.tt +13 -0
  19. data/lib/inferno/apps/cli/templates/execution_scripts/%library_name%_script.yaml.tt +20 -0
  20. data/lib/inferno/apps/cli/templates/execution_scripts/%library_name%_script_expected.json.tt +244 -0
  21. data/lib/inferno/apps/cli/templates/execution_scripts/README.md.tt +16 -0
  22. data/lib/inferno/dsl/fhir_resource_navigation.rb +145 -27
  23. data/lib/inferno/dsl/must_support_assessment.rb +93 -23
  24. data/lib/inferno/dsl/must_support_metadata_extractor.rb +139 -21
  25. data/lib/inferno/dsl/resume_test_route.rb +4 -3
  26. data/lib/inferno/exceptions.rb +6 -0
  27. data/lib/inferno/repositories/test_sessions.rb +3 -0
  28. data/lib/inferno/utils/execution_script_runner.rb +90 -0
  29. data/lib/inferno/utils/preset_processor.rb +2 -0
  30. data/lib/inferno/version.rb +1 -1
  31. metadata +18 -2
@@ -24,7 +24,7 @@ module Inferno
24
24
  elements = Array.wrap(elements)
25
25
  return elements if path.blank?
26
26
 
27
- paths = path.split(/(?<!hl7)\./)
27
+ paths = path_segments(path)
28
28
  segment = paths.first
29
29
  remaining_path = paths.drop(1).join('.')
30
30
 
@@ -42,22 +42,22 @@ module Inferno
42
42
  # @param given_element [FHIR::Model, Array<FHIR::Model>]
43
43
  # @param path [String]
44
44
  # @param include_dar [Boolean]
45
- # @return [Array<FHIR::Model>]
45
+ # @return a single matching value (which can include `false`) or `nil` if not found
46
46
  def find_a_value_at(given_element, path, include_dar: false, &block)
47
47
  return nil if given_element.nil?
48
48
 
49
49
  elements = Array.wrap(given_element)
50
50
  return find_in_elements(elements, include_dar:, &block) if path.empty?
51
51
 
52
- path_segments = path.split(/(?<!hl7)\./)
52
+ path_segments = path_segments(path)
53
53
 
54
- segment = path_segments.shift.delete_suffix('[x]').gsub(/^class$/, 'local_class').gsub('[x]:', ':').to_sym
54
+ segment = path_segments.shift
55
55
 
56
56
  remaining_path = path_segments.join('.')
57
57
  elements.each do |element|
58
58
  child = get_next_value(element, segment)
59
59
  element_found = find_a_value_at(child, remaining_path, include_dar:, &block)
60
- return element_found if element_found.present? || element_found == false
60
+ return element_found if value_not_empty?(element_found)
61
61
  end
62
62
 
63
63
  nil
@@ -78,25 +78,75 @@ module Inferno
78
78
 
79
79
  # @private
80
80
  def get_next_value(element, property)
81
+ property = property.to_s
81
82
  extension_url = property[/(?<=where\(url=').*(?='\))/]
82
- if extension_url.present?
83
- element.url == extension_url ? element : nil
84
- elsif property.to_s.include?(':') && !property.to_s.include?('url')
85
- find_slice_via_discriminator(element, property)
83
+ return extension_filter_value(element, extension_url) if extension_url.present?
84
+ return sliced_choice_value(element, property) if sliced_choice_path?(property)
85
+ return populated_choice_value(element, property) if choice_path?(property)
86
+ return find_slice_via_discriminator(element, property) if slice_path?(property)
86
87
 
87
- else
88
- local_name = local_field_name(property)
89
- value = element.send(local_name)
90
- primitive_value = get_primitive_type_value(element, property, value)
91
- primitive_value.present? ? primitive_value : value
92
- end
88
+ field_value(element, property)
93
89
  rescue NoMethodError
94
90
  nil
95
91
  end
96
92
 
93
+ # @private
94
+ def extension_filter_value(element, extension_url)
95
+ element.url == extension_url ? element : nil
96
+ end
97
+
98
+ # @private
99
+ def sliced_choice_path?(property)
100
+ property.include?('[x]:')
101
+ end
102
+
103
+ # @private
104
+ def choice_path?(property)
105
+ property.end_with?('[x]')
106
+ end
107
+
108
+ # @private
109
+ def slice_path?(property)
110
+ property.include?(':') && !property.include?('url')
111
+ end
112
+
113
+ # @private
114
+ def sliced_choice_value(element, property)
115
+ _choice_path, sliced_field = property.split(':', 2)
116
+ field_value(element, sliced_field)
117
+ end
118
+
119
+ # @private
120
+ def populated_choice_value(element, property)
121
+ choice_prefix = property.delete_suffix('[x]')
122
+ populated_field =
123
+ Array.wrap(element.to_hash&.keys)
124
+ .map(&:to_s)
125
+ .find do |field_name|
126
+ field_name.start_with?(choice_prefix) && value_not_empty?(field_value(element, field_name))
127
+ end
128
+
129
+ return nil if populated_field.blank?
130
+
131
+ field_value(element, populated_field)
132
+ end
133
+
134
+ # @private
135
+ def field_value(element, field_name)
136
+ local_name = local_field_name(field_name)
137
+ value = element.send(local_name)
138
+ primitive_value = get_primitive_type_value(element, field_name, value)
139
+ primitive_value.present? ? primitive_value : value
140
+ end
141
+
97
142
  # @private
98
143
  def get_primitive_type_value(element, property, value)
99
- source_value = element.source_hash["_#{property}"]
144
+ return nil unless element.respond_to?(:source_hash)
145
+
146
+ source_hash = element.source_hash
147
+ return nil unless source_hash.present?
148
+
149
+ source_value = source_hash["_#{property}"]
100
150
 
101
151
  return nil unless source_value.present?
102
152
 
@@ -116,6 +166,47 @@ module Inferno
116
166
  end
117
167
  end
118
168
 
169
+ # @private
170
+ def path_segments(path)
171
+ state = { current_segment: +'', segments: [], parentheses_depth: 0, in_quotes: false }
172
+ path.each_char { |char| update_path_segment_state(state, char) }
173
+ state[:segments] << state[:current_segment] unless state[:current_segment].empty?
174
+ state[:segments]
175
+ end
176
+
177
+ # @private
178
+ def update_path_segment_state(state, char)
179
+ case char
180
+ when "'"
181
+ state[:current_segment] << char
182
+ state[:in_quotes] = !state[:in_quotes]
183
+ when '('
184
+ append_path_character(state, char, depth_change: 1)
185
+ when ')'
186
+ append_path_character(state, char, depth_change: -1)
187
+ when '.'
188
+ split_path_segment_or_append(state, char)
189
+ else
190
+ state[:current_segment] << char
191
+ end
192
+ end
193
+
194
+ # @private
195
+ def append_path_character(state, char, depth_change:)
196
+ state[:current_segment] << char
197
+ state[:parentheses_depth] += depth_change unless state[:in_quotes]
198
+ end
199
+
200
+ # @private
201
+ def split_path_segment_or_append(state, char)
202
+ if state[:parentheses_depth].zero? && !state[:in_quotes]
203
+ state[:segments] << state[:current_segment].dup
204
+ state[:current_segment].clear
205
+ else
206
+ state[:current_segment] << char
207
+ end
208
+ end
209
+
119
210
  # @private
120
211
  def find_slice_via_discriminator(element, property)
121
212
  return unless metadata.present?
@@ -124,6 +215,8 @@ module Inferno
124
215
  slice_name = local_field_name(property.to_s.split(':')[1])
125
216
 
126
217
  slice_by_name = metadata.must_supports[:slices].find { |slice| slice[:slice_name] == slice_name }
218
+ return nil if slice_by_name.blank?
219
+
127
220
  discriminator = slice_by_name[:discriminator]
128
221
  slices = Array.wrap(element.send(element_name))
129
222
  slices.find { |slice| matching_slice?(slice, discriminator) }
@@ -168,7 +261,7 @@ module Inferno
168
261
 
169
262
  # @private
170
263
  def matching_value_slice?(slice, discriminator)
171
- values = discriminator[:values].map { |value| value.merge(path: value[:path].split('.')) }
264
+ values = discriminator[:values].map { |value| value.merge(path: path_segments(value[:path])) }
172
265
  verify_slice_by_values(slice, values)
173
266
  end
174
267
 
@@ -198,15 +291,29 @@ module Inferno
198
291
 
199
292
  # @private
200
293
  def matching_required_binding_slice?(slice, discriminator)
201
- slice_coding = discriminator[:path].present? ? slice.send((discriminator[:path]).to_s).coding : slice.coding
202
- slice_coding.any? do |coding|
203
- discriminator[:values].any? do |value|
204
- case value
205
- when String
206
- value == coding.code
207
- when Hash
208
- value[:system] == coding.system && value[:code] == coding.code
209
- end
294
+ slice_coding = required_binding_codings(slice, discriminator)
295
+ slice_coding.any? { |coding| required_binding_value_match?(coding, discriminator[:values]) }
296
+ end
297
+
298
+ # @private
299
+ def required_binding_codings(slice, discriminator)
300
+ if discriminator[:path].present?
301
+ Array.wrap(resolve_path(slice, discriminator[:path])).flat_map { |value| Array.wrap(value&.coding) }
302
+ elsif slice.is_a?(FHIR::Coding)
303
+ [slice]
304
+ else
305
+ Array.wrap(slice.coding)
306
+ end
307
+ end
308
+
309
+ # @private
310
+ def required_binding_value_match?(coding, values)
311
+ values.any? do |value|
312
+ case value
313
+ when String
314
+ value == coding.code
315
+ when Hash
316
+ value[:system] == coding.system && value[:code] == coding.code
210
317
  end
211
318
  end
212
319
  end
@@ -219,7 +326,7 @@ module Inferno
219
326
  value_definitions
220
327
  .select { |value_definition| value_definition[:path].first == path_prefix }
221
328
  .each { |value_definition| value_definition[:path].shift }
222
- find_a_value_at(element, path_prefix) do |el_found|
329
+ value_at_path_matches?(element, path_prefix) do |el_found|
223
330
  current_and_child_values_match?(el_found, value_definitions_for_path)
224
331
  end
225
332
  end
@@ -243,6 +350,17 @@ module Inferno
243
350
  current_element_values_match && child_element_values_match
244
351
  end
245
352
 
353
+ # @private
354
+ def value_at_path_matches?(element, path, include_dar: false, &)
355
+ value_found = find_a_value_at(element, path, include_dar:, &)
356
+ value_not_empty?(value_found)
357
+ end
358
+
359
+ # @private
360
+ def value_not_empty?(value)
361
+ value.present? || value == false
362
+ end
363
+
246
364
  # @private
247
365
  def flatten_bundles(resources)
248
366
  resources.flat_map do |resource|
@@ -202,14 +202,18 @@ module Inferno
202
202
  def missing_extensions(resources = [])
203
203
  @missing_extensions ||=
204
204
  must_support_extensions.select do |extension_definition|
205
+ expected_url = normalized_extension_url(extension_definition[:url])
206
+
205
207
  resources.none? do |resource|
206
208
  path = extension_definition[:path]
207
209
 
208
210
  if path == 'extension'
209
- resource.extension.any? { |extension| extension.url == extension_definition[:url] }
211
+ Array.wrap(resource.extension).any? do |extension|
212
+ normalized_extension_url(extension.url) == expected_url
213
+ end
210
214
  else
211
215
  extension = find_a_value_at(resource, path) do |el|
212
- el.url == extension_definition[:url]
216
+ normalized_extension_url(el.url) == expected_url
213
217
  end
214
218
 
215
219
  extension.present?
@@ -233,12 +237,10 @@ module Inferno
233
237
  end
234
238
 
235
239
  def resource_populates_element?(resource, element_definition)
236
- path = element_definition[:path]
237
-
238
- # handle MustSupport element under extension: Ex: extension:supporting-info.value[x]
239
- resource, path = process_must_support_element_in_extension(resource, path) if path.start_with?('extension:')
240
+ raw_path = element_definition[:path]
241
+ path = navigation_compatible_must_support_path(raw_path)
240
242
 
241
- ms_extension_urls = must_support_extensions.select { |ex| ex[:path] == "#{path}.extension" }
243
+ ms_extension_urls = must_support_extensions.select { |ex| ex[:path] == "#{raw_path}.extension" }
242
244
  .map { |ex| ex[:url] }
243
245
 
244
246
  value_found = find_a_value_at(resource, path) do |potential_value|
@@ -249,6 +251,47 @@ module Inferno
249
251
  value_found.present? || value_found == false
250
252
  end
251
253
 
254
+ def navigation_compatible_must_support_path(path)
255
+ logical_segments = []
256
+
257
+ path_segments(path).map do |segment|
258
+ normalized_segment = normalized_must_support_path_segment(segment, logical_segments)
259
+ logical_segments << segment.split(':').first
260
+ normalized_segment
261
+ end.join('.')
262
+ end
263
+
264
+ def normalized_must_support_path_segment(segment, logical_segments)
265
+ extension_type, extension_name = segment.match(/\A(modifierExtension|extension):(.+)\z/)&.captures
266
+ return segment if extension_type.blank?
267
+
268
+ extension_path = [logical_segments.join('.'), extension_type].reject(&:blank?).join('.')
269
+ extension_definition = must_support_extension_definition(extension_path, extension_type, extension_name)
270
+ return segment if extension_definition.blank?
271
+
272
+ "#{extension_type}.where(url='#{normalized_extension_url(extension_definition[:url])}')"
273
+ end
274
+
275
+ def must_support_extension_definition(extension_path, extension_type, extension_name)
276
+ suffix = "#{extension_type}:#{extension_name}"
277
+ path_matching_extensions = must_support_extensions.select { |definition| definition[:path] == extension_path }
278
+
279
+ extension_definition_candidates(path_matching_extensions).each do |definitions, matcher|
280
+ match = definitions.find { |definition| definition[:id].public_send(matcher, suffix) }
281
+ return match if match.present?
282
+ end
283
+
284
+ nil
285
+ end
286
+
287
+ def extension_definition_candidates(path_matching_extensions)
288
+ [
289
+ [path_matching_extensions, :end_with?],
290
+ [path_matching_extensions, :include?],
291
+ [must_support_extensions, :end_with?]
292
+ ]
293
+ end
294
+
252
295
  def process_must_support_element_in_extension(resource, path)
253
296
  return [resource, path] unless path.start_with?('extension:')
254
297
 
@@ -257,8 +300,11 @@ module Inferno
257
300
  extension_name = extension_split.first
258
301
  extension_path = extension_split.last
259
302
 
260
- found_extension_url = must_support_extensions.find { |ex| ex[:id].include?(extension_name) }[:url]
261
- ms_element_extension = resource.extension.find { |ex| ex.url == found_extension_url }
303
+ found_extension_url =
304
+ normalized_extension_url(must_support_extensions.find { |ex| ex[:id].include?(extension_name) }[:url])
305
+ ms_element_extension = resource.extension.find do |extension|
306
+ normalized_extension_url(extension.url) == found_extension_url
307
+ end
262
308
 
263
309
  if ms_element_extension.present?
264
310
  resource = ms_element_extension
@@ -269,17 +315,33 @@ module Inferno
269
315
  end
270
316
 
271
317
  def matching_without_extensions?(value, ms_extension_urls, fixed_value)
272
- if value.instance_of?(Inferno::DSL::PrimitiveType)
273
- urls = value.extension&.map(&:url)
274
- has_ms_extension = (urls & ms_extension_urls).present?
275
- value = value.value
276
- end
318
+ has_ms_extension = must_support_extension_present?(value, ms_extension_urls)
319
+
320
+ value = value.value if value.instance_of?(Inferno::DSL::PrimitiveType)
277
321
 
278
322
  return false unless has_ms_extension || value_without_extensions?(value)
279
323
 
280
324
  matches_fixed_value?(value, fixed_value)
281
325
  end
282
326
 
327
+ def must_support_extension_present?(value, ms_extension_urls)
328
+ return false unless value.respond_to?(:extension)
329
+
330
+ (extension_urls(value) & normalized_extension_urls(ms_extension_urls)).present?
331
+ end
332
+
333
+ def extension_urls(value)
334
+ Array.wrap(value.extension).map { |extension| normalized_extension_url(extension.url) }
335
+ end
336
+
337
+ def normalized_extension_urls(urls)
338
+ Array.wrap(urls).map { |url| normalized_extension_url(url) }
339
+ end
340
+
341
+ def normalized_extension_url(url)
342
+ url&.split('|')&.first
343
+ end
344
+
283
345
  def matches_fixed_value?(value, fixed_value)
284
346
  fixed_value.blank? || value == fixed_value
285
347
  end
@@ -346,7 +408,7 @@ module Inferno
346
408
  end
347
409
 
348
410
  def find_value_slice(element, discriminator)
349
- values = discriminator[:values].map { |value| value.merge(path: value[:path].split('.')) }
411
+ values = discriminator[:values].map { |value| value.merge(path: path_segments(value[:path])) }
350
412
  find_slice_by_values(element, values)
351
413
  end
352
414
 
@@ -377,23 +439,31 @@ module Inferno
377
439
  end
378
440
 
379
441
  def find_required_binding_slice(element, discriminator)
442
+ if element.is_a?(FHIR::Coding) && required_binding_value_match?(element, discriminator[:values])
443
+ return element
444
+ end
445
+
380
446
  coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
381
447
 
382
448
  find_a_value_at(element, coding_path) do |coding|
383
- discriminator[:values].any? do |value|
384
- case value
385
- when String
386
- value == coding.code
387
- when Hash
388
- value[:system] == coding.system && value[:code] == coding.code
389
- end
390
- end
449
+ required_binding_value_match?(coding, discriminator[:values])
391
450
  end
392
451
  end
393
452
 
394
453
  def find_slice_by_values(element, value_definitions)
395
454
  Array.wrap(element).find { |el| verify_slice_by_values(el, value_definitions) }
396
455
  end
456
+
457
+ def required_binding_value_match?(coding, values)
458
+ values.any? do |value|
459
+ case value
460
+ when String
461
+ value == coding.code
462
+ when Hash
463
+ value[:system] == coding.system && value[:code] == coding.code
464
+ end
465
+ end
466
+ end
397
467
  end
398
468
  end
399
469
  end
@@ -46,7 +46,7 @@ module Inferno
46
46
  end
47
47
 
48
48
  def must_support_extension_elements
49
- all_must_support_elements.select { |element| element.path.end_with? 'extension' }
49
+ all_must_support_elements.select { |element| element.path.end_with? 'xtension' }
50
50
  end
51
51
 
52
52
  def must_support_extensions
@@ -54,16 +54,23 @@ module Inferno
54
54
  {
55
55
  id: element.id,
56
56
  path: element.path.gsub("#{resource}.", ''),
57
- url: element.type.first.profile.first
57
+ url: canonical_url_without_version(element.type.first.profile.first),
58
+ modifier_extension: element.path.end_with?('modifierExtension')
58
59
  }.tap do |metadata|
59
60
  metadata[:by_requirement_extension_only] = true if by_requirement_extension_only?(element)
60
61
  end
61
62
  end
62
63
  end
63
64
 
65
+ def canonical_url_without_version(url)
66
+ url&.split('|')&.first
67
+ end
68
+
64
69
  def must_support_slice_elements
65
- all_must_support_elements.select do |element|
66
- !element.path.end_with?('extension') && element.sliceName.present?
70
+ profile_elements.select do |element|
71
+ next false if element.sliceName.blank? || element.path.end_with?('xtension')
72
+
73
+ all_must_support_elements.include?(element) || slice_has_must_support_descendants?(element)
67
74
  end
68
75
  end
69
76
 
@@ -77,47 +84,136 @@ module Inferno
77
84
  slice&.slicing&.discriminator
78
85
  end
79
86
 
87
+ def slice_has_must_support_descendants?(slice)
88
+ all_must_support_elements.any? do |element|
89
+ element.id.start_with?("#{slice.id}.")
90
+ end
91
+ end
92
+
80
93
  def find_element_by_discriminator_path(current_element, discriminator_path)
81
- if discriminator_path.present?
82
- profile_elements.find { |element| element.id == "#{current_element.id}.#{discriminator_path}" } ||
83
- profile_elements.find { |element| element.id == "#{current_element.path}.#{discriminator_path}" }
84
- else
85
- current_element
94
+ target_element = current_element
95
+ remaining_path = discriminator_path
96
+
97
+ while remaining_path.present?
98
+ target_element, remaining_path = take_discriminator_step(target_element, remaining_path)
99
+ return nil if target_element.nil?
100
+ end
101
+
102
+ target_element
103
+ end
104
+
105
+ def take_discriminator_step(current_element, path)
106
+ return take_extension_discriminator_step(current_element, path) if extension_discriminator_step?(path)
107
+
108
+ take_standard_discriminator_step(current_element, path)
109
+ end
110
+
111
+ def extension_discriminator_step?(path)
112
+ path.start_with?('extension(') || path.start_with?('modifierExtension(')
113
+ end
114
+
115
+ def take_extension_discriminator_step(current_element, path)
116
+ extension_type = path.start_with?('modifierExtension(') ? 'modifierExtension' : 'extension'
117
+ ext_url, remaining_path = path.delete_prefix("#{extension_type}('").split("')", 2)
118
+ next_element = profile_elements.find do |element|
119
+ element.path == "#{current_element.path}.#{extension_type}" &&
120
+ element.id.start_with?("#{current_element.id}.#{extension_type}:") &&
121
+ element.type.any? { |type| extension_profile_matches?(type, ext_url) }
122
+ end
123
+
124
+ [next_element, remaining_path&.delete_prefix('.')]
125
+ end
126
+
127
+ def extension_profile_matches?(type, ext_url)
128
+ type.code == 'Extension' && type.profile.any? { |profile| profile.start_with?(ext_url) }
129
+ end
130
+
131
+ def take_standard_discriminator_step(current_element, path)
132
+ step_path, remaining_path = path.split('.', 2)
133
+ if remaining_path&.start_with?('ofType(')
134
+ return take_choice_discriminator_step(current_element, step_path, remaining_path)
135
+ end
136
+ if legacy_choice_discriminator_step?(step_path)
137
+ return take_legacy_choice_discriminator_step(current_element, step_path, remaining_path)
138
+ end
139
+
140
+ next_element =
141
+ profile_elements.find { |element| element.id == "#{current_element.id}.#{step_path}" } ||
142
+ profile_elements.find { |element| element.id == "#{current_element.path}.#{step_path}" }
143
+
144
+ [next_element, remaining_path&.delete_prefix('.')]
145
+ end
146
+
147
+ def legacy_choice_discriminator_step?(path)
148
+ path.match?(/\A.+\s+as\s+[[:word:]]+\z/)
149
+ end
150
+
151
+ def take_choice_discriminator_step(current_element, step_path, remaining_path)
152
+ target_element = "#{step_path}[x]"
153
+ target_type, remaining_path = remaining_path.delete_prefix('ofType(').split(')', 2)
154
+ next_element = profile_elements.find do |element|
155
+ element.id == "#{current_element.id}.#{target_element}" &&
156
+ element.type.any? { |type| type.code.casecmp?(target_type) }
86
157
  end
158
+
159
+ [next_element, remaining_path&.delete_prefix('.')]
160
+ end
161
+
162
+ def take_legacy_choice_discriminator_step(current_element, step_path, remaining_path)
163
+ target_element, target_type = step_path.split(/\s+as\s+/, 2)
164
+ target_element = "#{target_element}[x]" unless target_element.end_with?('[x]')
165
+ next_element = find_choice_element(current_element, target_element, target_type)
166
+
167
+ [next_element, remaining_path&.delete_prefix('.')]
168
+ end
169
+
170
+ def find_choice_element(current_element, target_element, target_type)
171
+ [current_element.id, current_element.path].filter_map do |base_path|
172
+ profile_elements.find do |element|
173
+ element_matches_choice_type?(element, "#{base_path}.#{target_element}", target_type)
174
+ end
175
+ end.first
176
+ end
177
+
178
+ def element_matches_choice_type?(element, target_path, target_type)
179
+ [element.id, element.path].include?(target_path) &&
180
+ element.type.any? { |type| type.code.casecmp?(target_type) }
87
181
  end
88
182
 
89
183
  def save_pattern_slice(pattern_element, discriminator_path, metadata)
184
+ runtime_path = navigation_compatible_discriminator_path(discriminator_path)
185
+
90
186
  if pattern_element.patternCodeableConcept
91
187
  {
92
188
  type: 'patternCodeableConcept',
93
- path: discriminator_path,
189
+ path: runtime_path,
94
190
  code: pattern_element.patternCodeableConcept.coding.first.code,
95
191
  system: pattern_element.patternCodeableConcept.coding.first.system
96
192
  }
97
193
  elsif pattern_element.patternCoding
98
194
  {
99
195
  type: 'patternCoding',
100
- path: discriminator_path,
196
+ path: runtime_path,
101
197
  code: pattern_element.patternCoding.code,
102
198
  system: pattern_element.patternCoding.system
103
199
  }
104
200
  elsif pattern_element.patternIdentifier
105
201
  {
106
202
  type: 'patternIdentifier',
107
- path: discriminator_path,
203
+ path: runtime_path,
108
204
  system: pattern_element.patternIdentifier.system
109
205
  }
110
206
  elsif required_binding_pattern?(pattern_element)
111
207
  {
112
208
  type: 'requiredBinding',
113
- path: discriminator_path,
209
+ path: runtime_path,
114
210
  values: extract_required_binding_values(pattern_element, metadata)
115
211
  }
116
212
  else
117
213
  # prevent errors in case an IG does something different
118
214
  {
119
215
  type: 'unsupported',
120
- path: discriminator_path
216
+ path: runtime_path
121
217
  }
122
218
  end
123
219
  end
@@ -139,6 +235,16 @@ module Inferno
139
235
  end
140
236
  end
141
237
 
238
+ def navigation_compatible_discriminator_path(discriminator_path)
239
+ normalized_path = discriminator_path&.gsub(/(modifierExtension|extension)\('([^']+)'\)/, "\\1.where(url='\\2')")
240
+ normalized_path = normalized_path&.gsub(/([[:word:]\[\]]+)\.ofType\(([^)]+)\)/) do
241
+ "#{Regexp.last_match(1).delete_suffix('[x]')}#{Regexp.last_match(2).upcase_first}"
242
+ end
243
+ normalized_path&.gsub(/([[:word:]\[\]]+)\s+as\s+([[:word:]]+)/) do
244
+ "#{Regexp.last_match(1).delete_suffix('[x]')}#{Regexp.last_match(2).upcase_first}"
245
+ end
246
+ end
247
+
142
248
  def discriminator_path(discriminator)
143
249
  if discriminator.path == '$this'
144
250
  ''
@@ -156,15 +262,18 @@ module Inferno
156
262
  type_element = find_element_by_discriminator_path(current_element, type_path)
157
263
 
158
264
  type_code = type_element.type.first.code
265
+ discriminator_metadata = {
266
+ type: 'type',
267
+ code: type_code.upcase_first
268
+ }
269
+ runtime_path = navigation_compatible_discriminator_path(type_path)
270
+ discriminator_metadata[:path] = runtime_path if runtime_path.present?
159
271
 
160
272
  {
161
273
  slice_id: current_element.id,
162
274
  slice_name: current_element.sliceName,
163
275
  path: current_element.path.gsub("#{resource}.", ''),
164
- discriminator: {
165
- type: 'type',
166
- code: type_code.upcase_first
167
- }
276
+ discriminator: discriminator_metadata
168
277
  }.tap do |metadata|
169
278
  metadata[:by_requirement_extension_only] = true if by_requirement_extension_only?(current_element)
170
279
  end
@@ -202,9 +311,14 @@ module Inferno
202
311
  # and in subsequent versions of the profile, the bad discriminator was removed.
203
312
  next if pattern_element.nil? && element_discriminators.length > 1
204
313
 
205
- if pattern_element.fixed.present?
314
+ if pattern_element.nil?
315
+ pattern_value = {
316
+ type: 'unsupported',
317
+ path: navigation_compatible_discriminator_path(discriminator_path)
318
+ }
319
+ elsif value_not_empty?(pattern_element.fixed)
206
320
  fixed_values << {
207
- path: discriminator_path,
321
+ path: navigation_compatible_discriminator_path(discriminator_path),
208
322
  value: pattern_element.fixed
209
323
  }
210
324
  elsif pattern_value.present?
@@ -240,8 +354,12 @@ module Inferno
240
354
  must_support_slice_elements.any? { |ms_slice| element.id.include?(ms_slice.id) }
241
355
  end
242
356
 
357
+ def value_not_empty?(value)
358
+ value.present? || value == false
359
+ end
360
+
243
361
  def handle_fixed_values(metadata, element)
244
- if element.fixed.present?
362
+ if value_not_empty?(element.fixed)
245
363
  metadata[:fixed_value] = element.fixed
246
364
  elsif element.patternCodeableConcept.present? && !element_part_of_slice_discrimination?(element)
247
365
  metadata[:fixed_value] = element.patternCodeableConcept.coding.first.code