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.
- checksums.yaml +4 -4
- data/lib/inferno/apps/cli/execute_script.rb +918 -0
- data/lib/inferno/apps/cli/main.rb +46 -0
- data/lib/inferno/apps/cli/session/cancel_run.rb +47 -0
- data/lib/inferno/apps/cli/session/connection.rb +47 -0
- data/lib/inferno/apps/cli/session/create_session.rb +159 -0
- data/lib/inferno/apps/cli/session/errors.rb +45 -0
- data/lib/inferno/apps/cli/session/session_compare.rb +390 -0
- data/lib/inferno/apps/cli/session/session_data.rb +39 -0
- data/lib/inferno/apps/cli/session/session_details.rb +27 -0
- data/lib/inferno/apps/cli/session/session_results.rb +39 -0
- data/lib/inferno/apps/cli/session/session_status.rb +69 -0
- data/lib/inferno/apps/cli/session/start_run.rb +245 -0
- data/lib/inferno/apps/cli/session_commands.rb +66 -0
- data/lib/inferno/apps/cli/templates/%library_name%.gemspec.tt +1 -1
- data/lib/inferno/apps/cli/templates/.gitignore +4 -0
- data/lib/inferno/apps/cli/templates/README.md.tt +14 -0
- data/lib/inferno/apps/cli/templates/Rakefile.tt +13 -0
- data/lib/inferno/apps/cli/templates/execution_scripts/%library_name%_script.yaml.tt +20 -0
- data/lib/inferno/apps/cli/templates/execution_scripts/%library_name%_script_expected.json.tt +244 -0
- data/lib/inferno/apps/cli/templates/execution_scripts/README.md.tt +16 -0
- data/lib/inferno/dsl/fhir_resource_navigation.rb +145 -27
- data/lib/inferno/dsl/must_support_assessment.rb +93 -23
- data/lib/inferno/dsl/must_support_metadata_extractor.rb +139 -21
- data/lib/inferno/dsl/resume_test_route.rb +4 -3
- data/lib/inferno/exceptions.rb +6 -0
- data/lib/inferno/repositories/test_sessions.rb +3 -0
- data/lib/inferno/utils/execution_script_runner.rb +90 -0
- data/lib/inferno/utils/preset_processor.rb +2 -0
- data/lib/inferno/version.rb +1 -1
- 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
|
|
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
|
|
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
|
|
52
|
+
path_segments = path_segments(path)
|
|
53
53
|
|
|
54
|
-
segment = path_segments.shift
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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 =
|
|
202
|
-
slice_coding.any?
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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?
|
|
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 ==
|
|
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
|
-
|
|
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] == "#{
|
|
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 =
|
|
261
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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]
|
|
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]
|
|
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? '
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
|
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
|