us_quality_core_test_kit 0.1.0 → 0.1.2

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/NOTICE.md +12 -0
  3. data/config/presets/{localhost_preset.json → inferno_reference_server_preset.json.erb} +2 -2
  4. data/lib/us_quality_core_test_kit/client/generator/example_client_generator.rb +0 -1
  5. data/lib/us_quality_core_test_kit/client/generator/tags_generator.rb +0 -1
  6. data/lib/us_quality_core_test_kit/client/server_proxy.rb +2 -6
  7. data/lib/us_quality_core_test_kit/date_search_validation.rb +122 -0
  8. data/lib/us_quality_core_test_kit/fhir_resource_navigation.rb +9 -0
  9. data/lib/us_quality_core_test_kit/generated/v0.5.0/devicenotrequested/devicenotrequested_validation_test.rb +3 -0
  10. data/lib/us_quality_core_test_kit/generated/v0.5.0/devicerequest/devicerequest_validation_test.rb +3 -0
  11. data/lib/us_quality_core_test_kit/generated/v0.5.0/patient/patient_validation_test.rb +4 -0
  12. data/lib/us_quality_core_test_kit/generated/v0.5.0/us_quality_core_test_suite.rb +1 -2
  13. data/lib/us_quality_core_test_kit/generator/group_generator.rb +69 -3
  14. data/lib/us_quality_core_test_kit/generator/group_metadata.rb +100 -4
  15. data/lib/us_quality_core_test_kit/generator/group_metadata_extractor.rb +184 -8
  16. data/lib/us_quality_core_test_kit/generator/ig_loader.rb +47 -3
  17. data/lib/us_quality_core_test_kit/generator/ig_metadata.rb +51 -2
  18. data/lib/us_quality_core_test_kit/generator/ig_metadata_extractor.rb +25 -6
  19. data/lib/us_quality_core_test_kit/generator/ig_resources.rb +27 -3
  20. data/lib/us_quality_core_test_kit/generator/must_support_metadata_extractor.rb +366 -9
  21. data/lib/us_quality_core_test_kit/generator/must_support_test_generator.rb +54 -3
  22. data/lib/us_quality_core_test_kit/generator/provenance_revinclude_search_test_generator.rb +149 -3
  23. data/lib/us_quality_core_test_kit/generator/read_test_generator.rb +54 -3
  24. data/lib/us_quality_core_test_kit/generator/reference_resolution_test_generator.rb +57 -3
  25. data/lib/us_quality_core_test_kit/generator/search_definition_metadata_extractor.rb +181 -5
  26. data/lib/us_quality_core_test_kit/generator/search_metadata_extractor.rb +61 -3
  27. data/lib/us_quality_core_test_kit/generator/search_test_generator.rb +186 -3
  28. data/lib/us_quality_core_test_kit/generator/suite_generator.rb +35 -8
  29. data/lib/us_quality_core_test_kit/generator/terminology_binding_metadata_extractor.rb +107 -3
  30. data/lib/us_quality_core_test_kit/generator/validation_test_generator.rb +83 -4
  31. data/lib/us_quality_core_test_kit/generator/value_extractor.rb +147 -3
  32. data/lib/us_quality_core_test_kit/must_support_test.rb +15 -3
  33. data/lib/us_quality_core_test_kit/primitive_type.rb +7 -0
  34. data/lib/us_quality_core_test_kit/read_test.rb +99 -4
  35. data/lib/us_quality_core_test_kit/reference_resolution_test.rb +164 -3
  36. data/lib/us_quality_core_test_kit/resource_search_param_checker.rb +138 -0
  37. data/lib/us_quality_core_test_kit/search_test.rb +681 -46
  38. data/lib/us_quality_core_test_kit/search_test_properties.rb +53 -3
  39. data/lib/us_quality_core_test_kit/validation_test.rb +51 -4
  40. data/lib/us_quality_core_test_kit/version.rb +2 -2
  41. data/lib/us_quality_core_test_kit/well_known_code_systems.rb +23 -0
  42. metadata +8 -17
@@ -1,79 +1,469 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'us_core_test_kit/search_test'
3
+ require_relative 'date_search_validation'
4
+ require_relative 'resource_search_param_checker'
5
+ require_relative 'search_test_properties'
6
+ require_relative 'well_known_code_systems'
7
+ require_relative 'fhir_resource_navigation'
4
8
 
5
9
  module USQualityCoreTestKit
6
10
  module SearchTest
7
- include USCoreTestKit::SearchTest
8
- extend USCoreTestKit::SearchTest
11
+ extend Forwardable
12
+ include DateSearchValidation
13
+ include Inferno::DSL::FHIRResourceNavigation
14
+ include ResourceSearchParamChecker
15
+ include WellKnownCodeSystems
16
+
17
+ def_delegators 'self.class', :metadata, :provenance_metadata, :properties
18
+ def_delegators 'properties',
19
+ :resource_type,
20
+ :search_param_names,
21
+ :saves_delayed_references?,
22
+ :first_search?,
23
+ :fixed_value_search?,
24
+ :possible_status_search?,
25
+ :test_medication_inclusion?,
26
+ :test_post_search?,
27
+ :token_search_params,
28
+ :test_reference_variants?,
29
+ :params_with_comparators,
30
+ :multiple_or_search_params
31
+
32
+ def all_search_params
33
+ @all_search_params ||=
34
+ patient_id_list.each_with_object({}) do |patient_id, params|
35
+ params[patient_id] ||= []
36
+ new_params =
37
+ if fixed_value_search?
38
+ fixed_value_search_param_values.map { |value| fixed_value_search_params(value, patient_id) }
39
+ else
40
+ [search_params_with_values(search_param_names, patient_id)]
41
+ end
42
+ new_params.reject! do |a_params|
43
+ a_params.any? { |_key, value| value.blank? }
44
+ end
9
45
 
10
- EXTENSION_VALUE_PATH_REGEX = /
11
- \A
12
- (?<extension_type>modifierExtension|extension)
13
- \.where\(url='(?<url>[^']+)'\)
14
- \.(?<value_path>value(?:[A-Z]\w*|\[x\])?)
15
- \z
16
- /x
46
+ params[patient_id].concat(new_params)
47
+ end
48
+ end
17
49
 
18
- def patient_id_param?(name)
19
- name == 'patient' || name == 'subject' || (name == '_id' && resource_type == 'Patient')
50
+ def all_provenance_revinclude_search_params
51
+ @all_provenance_revinclude_search_params ||=
52
+ all_search_params.transform_values! do |params_list|
53
+ params_list.map { |params| params.merge(_revinclude: 'Provenance:target') }
54
+ end
20
55
  end
21
56
 
22
- def filter_devices(resources)
23
- resources # NOOP for US Quality Core
57
+ def any_valid_search_params?(search_params)
58
+ search_params.any? { |_patient_id, params| params.present? }
24
59
  end
25
60
 
26
- def get_next_value(element, property)
27
- return resolve_reference_value(element) if property.to_s == 'resolve()'
61
+ def run_provenance_revinclude_search_test
62
+ # TODO: skip if not supported?
63
+ skip_if !any_valid_search_params?(all_provenance_revinclude_search_params), unable_to_resolve_params_message
28
64
 
29
- super
65
+ provenance_resources =
66
+ all_provenance_revinclude_search_params.flat_map do |_patient_id, params_list|
67
+ params_list.flat_map do |params|
68
+ fhir_search resource_type, params:, tags: tags(params)
69
+
70
+ perform_search_with_status(params, patient_id) if response[:status] == 400 && possible_status_search?
71
+
72
+ check_search_response
73
+
74
+ # TODO: check that only provenance resources for resources matching
75
+ # granular scopes returned
76
+ fetch_and_assert_all_bundled_resources(additional_resource_types: ['Provenance'], params:)
77
+ .select { |resource| resource.resourceType == 'Provenance' }
78
+ end
79
+ end
80
+
81
+ scratch_provenance_resources[:all] ||= []
82
+ scratch_provenance_resources[:all].concat(provenance_resources)
83
+
84
+ save_delayed_references(provenance_resources, 'Provenance')
85
+
86
+ skip_if provenance_resources.empty?, no_resources_skip_message('Provenance')
30
87
  end
31
88
 
32
- def resolve_reference_value(element)
33
- reference = element&.reference
34
- return if reference.blank? || reference.start_with?('#')
89
+ def run_search_test
90
+ # TODO: skip if not supported?
91
+ skip_if !any_valid_search_params?(all_search_params), unable_to_resolve_params_message
35
92
 
36
- resolved_reference_cache[reference] ||= begin
37
- resource_type, resource_id = resource_type_and_id_from_reference(reference)
93
+ resources_returned =
94
+ all_search_params.flat_map do |patient_id, params_list|
95
+ params_list.flat_map { |params| perform_search(params, patient_id) }
96
+ end
38
97
 
39
- if resource_type.blank? || resource_id.blank?
40
- nil
98
+ skip_if resources_returned.empty?, no_resources_skip_message
99
+
100
+ perform_multiple_or_search_test if multiple_or_search_params.present?
101
+ end
102
+
103
+ def perform_search(params, patient_id)
104
+ fhir_search resource_type, params:, tags: tags(params)
105
+
106
+ perform_search_with_status(params, patient_id) if response[:status] == 400 && possible_status_search?
107
+
108
+ check_search_response
109
+
110
+ resources_returned =
111
+ fetch_and_assert_all_bundled_resources(params:).select { |resource| resource.resourceType == resource_type }
112
+
113
+ return [] if resources_returned.blank?
114
+
115
+ perform_comparator_searches(params, patient_id) if params_with_comparators.present?
116
+
117
+ if first_search?
118
+ all_scratch_resources.concat(resources_returned).uniq!
119
+ scratch_resources_for_patient(patient_id).concat(resources_returned).uniq!
120
+ end
121
+
122
+ resources_returned.each do |resource|
123
+ check_resource_against_params(resource, params)
124
+ end
125
+
126
+ save_delayed_references(resources_returned) if saves_delayed_references?
127
+
128
+ return resources_returned if all_search_variants_tested?
129
+
130
+ perform_post_search(resources_returned, params) if test_post_search?
131
+ test_medication_inclusion(resources_returned, params, patient_id) if test_medication_inclusion?
132
+ perform_reference_with_type_search(params, resources_returned.count) if test_reference_variants?
133
+ perform_search_with_system(params, patient_id) if token_search_params.present?
134
+
135
+ resources_returned
136
+ end
137
+
138
+ def perform_post_search(get_search_resources, params)
139
+ fhir_search resource_type, params:, search_method: :post
140
+
141
+ check_search_response
142
+
143
+ post_search_resources = fetch_and_assert_all_bundled_resources.select do |resource|
144
+ resource.resourceType == resource_type
145
+ end
146
+
147
+ filter_devices(post_search_resources) if resource_type == 'Device'
148
+
149
+ get_resource_count = get_search_resources.length
150
+ post_resource_count = post_search_resources.length
151
+
152
+ search_variant_test_records[:post_variant] = true
153
+
154
+ assert get_resource_count == post_resource_count,
155
+ 'Expected search by POST to return the same results as search by GET, ' \
156
+ "but GET search returned #{get_resource_count} resources, and POST search " \
157
+ "returned #{post_resource_count} resources."
158
+ end
159
+
160
+ def excluded_code?(coding, codes_to_exclude)
161
+ codes_to_exclude.any? do |exclude_code|
162
+ if exclude_code.include?('|')
163
+ system, code = exclude_code.split('|')
164
+ coding.code == code && coding.system == system
41
165
  else
42
- fhir_read(resource_type, resource_id)&.resource
166
+ code = exclude_code
167
+ coding.code == code
43
168
  end
44
- rescue StandardError => e
45
- Inferno::Application['logger'].error("Unable to resolve reference #{reference}")
46
- Inferno::Application['logger'].error(e.full_message)
47
- nil
48
169
  end
49
170
  end
50
171
 
51
- def resolved_reference_cache
52
- @resolved_reference_cache ||= {}
172
+ def filter_adi_document_reference(resources)
173
+ resources.select! do |resource|
174
+ resource.category.any? do |category|
175
+ category.coding.any? do |coding|
176
+ metadata.search_definitions[:category][:values].include? coding.code
177
+ end
178
+ end
179
+ end
53
180
  end
54
181
 
55
- def resource_type_and_id_from_reference(reference)
56
- parts = reference.split('/')
57
- history_index = parts.index('_history')
58
- parts = parts.first(history_index) if history_index
182
+ def search_and_check_response(params, resource_type = self.resource_type)
183
+ fhir_search resource_type, params:, tags: tags(params)
59
184
 
60
- parts.last(2)
185
+ check_search_response
61
186
  end
62
187
 
63
- def no_resources_skip_message(resource_type = self.resource_type)
64
- "No #{resource_type} resources appear to be available. Please use patients with more information"
188
+ def check_search_response
189
+ assert_response_status(200)
190
+ assert_resource_type(:bundle)
191
+ # NOTE: how do we want to handle validating Bundles?
65
192
  end
66
193
 
67
- # Override to allow for multiple combination search parameters
68
- # in addition to the patient or subject
69
- def fixed_value_search_params(value, patient_id)
70
- return super unless value.is_a? Hash
194
+ def search_variant_test_records
195
+ @search_variant_test_records ||= initial_search_variant_test_records
196
+ end
71
197
 
72
- search_param_names.each_with_object({}) do |name, params|
73
- params[name] = patient_id_param?(name) ? patient_id : value[name]
198
+ def initial_search_variant_test_records
199
+ {}.tap do |records|
200
+ records[:post_variant] = false if test_post_search?
201
+ records[:medication_inclusion] = false if test_medication_inclusion?
202
+ records[:reference_variants] = false if test_reference_variants?
203
+ records[:token_variants] = false if token_search_params.present?
204
+ records[:comparator_searches] = Set.new if params_with_comparators.present?
205
+ end
206
+ end
207
+
208
+ def all_search_variants_tested?
209
+ search_variant_test_records.all? { |_variant, tested| tested.present? } &&
210
+ all_comparator_searches_tested?
211
+ end
212
+
213
+ def all_comparator_searches_tested?
214
+ return true if params_with_comparators.blank?
215
+
216
+ Set.new(params_with_comparators) == search_variant_test_records[:comparator_searches]
217
+ end
218
+
219
+ def date_comparator_value(comparator, date)
220
+ date = date.start || date.end if date.is_a? FHIR::Period
221
+ case comparator
222
+ when 'lt', 'le'
223
+ comparator + (DateTime.xmlschema(date) + 1).xmlschema
224
+ when 'gt', 'ge'
225
+ comparator + (DateTime.xmlschema(date) - 1).xmlschema
226
+ else
227
+ # ''
228
+ raise "Unsupported comparator '#{comparator}'"
74
229
  end
75
230
  end
76
231
 
232
+ def required_comparators(name)
233
+ metadata
234
+ .search_definitions
235
+ .dig(name.to_sym, :comparators)
236
+ .select { |_comparator, expectation| expectation == 'SHALL' }
237
+ .keys
238
+ .map(&:to_s)
239
+ end
240
+
241
+ def perform_comparator_searches(params, patient_id)
242
+ params_with_comparators.each do |name|
243
+ next if search_variant_test_records[:comparator_searches].include? name
244
+
245
+ required_comparators(name).each do |comparator|
246
+ paths = search_param_paths(name).first
247
+ date_element = find_a_value_at(scratch_resources_for_patient(patient_id), paths)
248
+ params_with_comparator = params.merge(name => date_comparator_value(comparator, date_element))
249
+
250
+ search_and_check_response(params_with_comparator)
251
+
252
+ fetch_and_assert_all_bundled_resources(params: params_with_comparator).each do |resource|
253
+ check_resource_against_params(resource, params_with_comparator) if resource.resourceType == resource_type
254
+ end
255
+ end
256
+
257
+ search_variant_test_records[:comparator_searches] << name
258
+ end
259
+ end
260
+
261
+ def perform_reference_with_type_search(params, resource_count)
262
+ return if resource_count.zero?
263
+ return if search_variant_test_records[:reference_variants]
264
+
265
+ new_search_params = params.merge('patient' => "Patient/#{params['patient']}")
266
+ search_and_check_response(new_search_params)
267
+
268
+ reference_with_type_resources =
269
+ fetch_and_assert_all_bundled_resources(params: new_search_params)
270
+ .select { |resource| resource.resourceType == resource_type }
271
+
272
+ new_resource_count = reference_with_type_resources.count
273
+
274
+ assert new_resource_count == resource_count,
275
+ "Expected search by `#{params['patient']}` to to return the same results as searching " \
276
+ "by `#{new_search_params['patient']}`, but found #{resource_count} resources with " \
277
+ "`#{params['patient']}` and #{new_resource_count} with `#{new_search_params['patient']}`"
278
+
279
+ search_variant_test_records[:reference_variants] = true
280
+ end
281
+
282
+ def perform_search_with_system(params, patient_id)
283
+ return if search_variant_test_records[:token_variants]
284
+
285
+ new_search_params = search_params_with_values(token_search_params, patient_id, include_system: true)
286
+ return if new_search_params.any? { |_name, value| value.blank? }
287
+
288
+ search_params = params.merge(new_search_params)
289
+ search_and_check_response(search_params)
290
+
291
+ resources_returned =
292
+ fetch_and_assert_all_bundled_resources(params: search_params)
293
+ .select { |resource| resource.resourceType == resource_type }
294
+
295
+ assert resources_returned.present?, 'No resources were returned when searching by `system|code`'
296
+
297
+ search_variant_test_records[:token_variants] = true
298
+ end
299
+
300
+ def perform_search_with_status(
301
+ original_params,
302
+ _patient_id,
303
+ status_search_values: self.status_search_values,
304
+ resource_type: self.resource_type
305
+ )
306
+ assert resource.is_a?(FHIR::OperationOutcome), 'Server returned a status of 400 without an OperationOutcome'
307
+ # TODO: warn about documenting status requirements
308
+ status_search_values.flat_map do |status_value|
309
+ search_params = original_params.merge("#{status_search_param_name}": status_value)
310
+
311
+ search_and_check_response(search_params)
312
+
313
+ entries = resource.entry.select { |entry| entry.resource.resourceType == resource_type }
314
+
315
+ if entries.present?
316
+ original_params.merge!("#{status_search_param_name}": status_value)
317
+ break
318
+ end
319
+ end
320
+ end
321
+
322
+ def status_search_param_name
323
+ @status_search_param_name ||=
324
+ metadata.search_definitions.keys.find { |key| key.to_s.include? 'status' }
325
+ end
326
+
327
+ def status_search_values
328
+ default_search_values(status_search_param_name)
329
+ end
330
+
331
+ def default_search_values(param_name)
332
+ definition = metadata.search_definitions[param_name]
333
+ return [] if definition.blank?
334
+
335
+ definition[:multiple_or] == 'SHALL' ? [definition[:values].join(',')] : Array.wrap(definition[:values])
336
+ end
337
+
338
+ def perform_multiple_or_search_test
339
+ resolved_one = false
340
+
341
+ all_search_params.each do |patient_id, params_list|
342
+ next unless params_list.present?
343
+
344
+ search_params = params_list.first
345
+ existing_values = {}
346
+ missing_values = {}
347
+
348
+ multiple_or_search_params.each do |param_name|
349
+ search_value = default_search_values(param_name.to_sym)
350
+ search_params = search_params.merge(param_name.to_s => search_value)
351
+ existing_values[param_name.to_sym] =
352
+ scratch_resources_for_patient(patient_id).map(&param_name.to_sym).compact.uniq
353
+ end
354
+
355
+ # skip patient without multiple-or values
356
+ next if existing_values.values.any?(&:empty?)
357
+
358
+ resolved_one = true
359
+
360
+ search_and_check_response(search_params)
361
+
362
+ resources_returned =
363
+ fetch_and_assert_all_bundled_resources(params: search_params)
364
+ .select { |resource| resource.resourceType == resource_type }
365
+
366
+ multiple_or_search_params.each do |param_name|
367
+ missing_values[param_name.to_sym] =
368
+ existing_values[param_name.to_sym] - resources_returned.map(&param_name.to_sym)
369
+ end
370
+
371
+ missing_value_message = missing_values
372
+ .reject { |_param_name, missing_value| missing_value.empty? }
373
+ .map { |param_name, missing_value| "#{missing_value.join(',')} values from #{param_name}" }
374
+ .join(' and ')
375
+
376
+ assert missing_value_message.blank?,
377
+ "Could not find #{missing_value_message} in any of the resources returned for Patient/#{patient_id}"
378
+
379
+ break if resolved_one
380
+ end
381
+ end
382
+
383
+ def test_medication_inclusion(base_resources, params, patient_id)
384
+ return if search_variant_test_records[:medication_inclusion]
385
+
386
+ scratch[:medication_resources] ||= {}
387
+ scratch[:medication_resources][:all] ||= []
388
+ scratch[:medication_resources][patient_id] ||= []
389
+ scratch[:medication_resources][:contained] ||= []
390
+
391
+ base_resources_with_external_reference =
392
+ base_resources
393
+ .select { |request| request&.medicationReference&.present? }
394
+ .reject { |request| request&.medicationReference&.reference&.start_with? '#' }
395
+
396
+ contained_medications =
397
+ base_resources
398
+ .select { |request| request&.medicationReference&.reference&.start_with? '#' }
399
+ .flat_map(&:contained)
400
+ .select { |resource| resource.resourceType == 'Medication' }
401
+
402
+ scratch[:medication_resources][:all] += contained_medications
403
+ scratch[:medication_resources][patient_id] += contained_medications
404
+ scratch[:medication_resources][:contained] += contained_medications
405
+
406
+ return if base_resources_with_external_reference.blank?
407
+
408
+ search_params = params.merge(_include: "#{resource_type}:medication")
409
+
410
+ search_and_check_response(search_params)
411
+
412
+ medications =
413
+ fetch_and_assert_all_bundled_resources(params: search_params)
414
+ .select { |resource| resource.resourceType == 'Medication' }
415
+ assert medications.present?, 'No Medications were included in the search results'
416
+
417
+ included_medications = medications.map { |medication| "#{medication.resourceType}/#{medication.id}" }
418
+
419
+ matched_base_resources = base_resources_with_external_reference.select do |base_resource|
420
+ included_medications.any? do |medication_reference|
421
+ reference_match?(base_resource.medicationReference.reference, medication_reference)
422
+ end
423
+ end
424
+
425
+ not_matched_included_medications = included_medications.select do |medication_reference|
426
+ matched_base_resources.none? do |base_resource|
427
+ reference_match?(base_resource.medicationReference.reference, medication_reference)
428
+ end
429
+ end
430
+
431
+ not_matched_included_medications_string = not_matched_included_medications.join(',')
432
+ assert not_matched_included_medications.empty?,
433
+ "No #{resource_type} references #{not_matched_included_medications_string} in the search result."
434
+
435
+ medications.uniq!(&:id)
436
+
437
+ scratch[:medication_resources][:all] += medications
438
+ scratch[:medication_resources][patient_id] += medications
439
+
440
+ search_variant_test_records[:medication_inclusion] = true
441
+ end
442
+
443
+ def reference_match?(reference, local_reference)
444
+ regex_pattern = %r{^(#{Regexp.escape(local_reference)}|\S+/#{Regexp.escape(local_reference)}(?:[/|]\S+)*)$}
445
+ reference.match?(regex_pattern)
446
+ end
447
+
448
+ def all_scratch_resources
449
+ scratch_resources[:all] ||= []
450
+ end
451
+
452
+ def scratch_resources_for_patient(patient_id)
453
+ return all_scratch_resources if patient_id.nil?
454
+
455
+ scratch_resources[patient_id] ||= []
456
+ end
457
+
458
+ def references_to_save(resource_type = nil)
459
+ reference_metadata = resource_type == 'Provenance' ? provenance_metadata : metadata
460
+ reference_metadata.delayed_references
461
+ end
462
+
463
+ def fixed_value_search_param_name
464
+ (search_param_names - ['patient']).first
465
+ end
466
+
77
467
  def fixed_value_search_param_values
78
468
  names = fixed_value_search_param_names
79
469
  values = names.map { |name| Array(metadata.search_definitions.dig(name.to_sym, :values)) }
@@ -84,8 +474,118 @@ module USQualityCoreTestKit
84
474
  values.first.product(*values.drop(1)).map { |combination| names.zip(combination).to_h }
85
475
  end
86
476
 
87
- def fixed_value_search_param_names
88
- search_param_names.reject { |name| patient_id_param?(name) }
477
+ def fixed_value_search_params(value, patient_id)
478
+ search_param_names.each_with_object({}) do |name, params|
479
+ param_value = value.is_a?(Hash) ? value[name] : value
480
+ params[name] = patient_id_param?(name) ? patient_id : param_value
481
+ end
482
+ end
483
+
484
+ def search_params_with_values(search_param_names, patient_id, include_system: false)
485
+ resources = scratch_resources_for_patient(patient_id)
486
+
487
+ if resources.empty?
488
+ return search_param_names.each_with_object({}) do |name, params|
489
+ value = patient_id_param?(name) ? patient_id : nil
490
+ params[name] = value
491
+ end
492
+ end
493
+
494
+ resources.each_with_object({}) do |resource, outer_params|
495
+ results_from_one_resource = search_param_names.each_with_object({}) do |name, params|
496
+ value = if patient_id_param?(name)
497
+ patient_id
498
+ else
499
+ search_param_value(name, resource,
500
+ include_system: include_system)
501
+ end
502
+ params[name] = value
503
+ end
504
+
505
+ outer_params.merge!(results_from_one_resource)
506
+
507
+ # stop if all parameter values are found
508
+ return outer_params if outer_params.all? { |_key, value| value.present? }
509
+ end
510
+ end
511
+
512
+ def patient_id_list
513
+ return [nil] unless respond_to? :patient_ids
514
+
515
+ patient_ids.split(',').map(&:strip)
516
+ end
517
+
518
+ def patient_search?
519
+ search_param_names.any? { |name| patient_id_param? name }
520
+ end
521
+
522
+ def patient_id_param?(name)
523
+ name == 'patient' || name == 'subject' || (name == '_id' && resource_type == 'Patient')
524
+ end
525
+
526
+ def search_param_paths(name)
527
+ paths = metadata.search_definitions[name.to_sym][:paths]
528
+ paths[0] = 'local_class' if paths.first == 'class'
529
+
530
+ paths
531
+ end
532
+
533
+ def all_search_params_present?(params)
534
+ params.all? { |_name, value| value.present? }
535
+ end
536
+
537
+ def array_of_codes(array)
538
+ array.map { |name| "`#{name}`" }.join(', ')
539
+ end
540
+
541
+ def unable_to_resolve_params_message
542
+ "Could not find values for all search params #{array_of_codes(search_param_names)}"
543
+ end
544
+
545
+ def empty_search_params_message(empty_search_params)
546
+ "Could not find values for the search parameters #{array_of_codes(empty_search_params.keys)}"
547
+ end
548
+
549
+ def no_resources_skip_message(resource_type = self.resource_type)
550
+ "No #{resource_type} resources appear to be available. Please use patients with more information"
551
+ end
552
+
553
+ def fetch_and_assert_all_bundled_resources(
554
+ resource_type: self.resource_type,
555
+ reply_handler: nil,
556
+ max_pages: 20,
557
+ additional_resource_types: [],
558
+ params: nil
559
+ )
560
+ tags = tags(params)
561
+ bundle = resource
562
+ additional_resource_types << 'Medication' if %w[MedicationRequest MedicationDispense].include?(resource_type)
563
+
564
+ assert_handler = proc do |response|
565
+ assert_response_status(200, response: response)
566
+ assert_valid_json(response[:body], "Could not resolve bundle as JSON: #{response[:body]}")
567
+ end
568
+
569
+ reply_and_assert_handler = if reply_handler
570
+ proc do |response|
571
+ assert_handler.call(response)
572
+ reply_handler.call(response)
573
+ end
574
+ else
575
+ assert_handler
576
+ end
577
+
578
+ fetch_all_bundled_resources(resource_type:, bundle:, reply_handler: reply_and_assert_handler, max_pages:,
579
+ additional_resource_types:, tags:)
580
+ end
581
+
582
+ def prefer_well_known_code_system(element, include_system)
583
+ coding =
584
+ find_a_value_at(element, 'coding') { |c| c.code.present? && WellKnownCodeSystems.include?(c.system) }
585
+
586
+ return coding if coding.present?
587
+
588
+ find_a_value_at(element, 'coding') { |c| c.code.present? && (!include_system || c.system.present?) }
89
589
  end
90
590
 
91
591
  def search_param_value(name, resource, include_system: false)
@@ -143,6 +643,78 @@ module USQualityCoreTestKit
143
643
  search_value.to_s.gsub(',', '\\,')
144
644
  end
145
645
 
646
+ def element_has_valid_value?(element, include_system)
647
+ case element
648
+ when FHIR::Reference
649
+ element.reference.present?
650
+ when FHIR::CodeableConcept
651
+ coding = prefer_well_known_code_system(element, include_system)
652
+ coding.present?
653
+ when FHIR::Identifier
654
+ include_system ? element.value.present? && element.system.present? : element.value.present?
655
+ when FHIR::Coding
656
+ include_system ? element.code.present? && element.system.present? : element.code.present?
657
+ when FHIR::HumanName
658
+ (element.family || element.given&.first || element.text).present?
659
+ when FHIR::Address
660
+ (element.text || element.city || element.state || element.postalCode || element.country).present?
661
+ when Inferno::DSL::PrimitiveType
662
+ element.value.present?
663
+ else
664
+ true
665
+ end
666
+ end
667
+
668
+ def save_resource_reference(resource_type, reference, referencing_resource)
669
+ scratch[:references] ||= {}
670
+ scratch[:references][resource_type] ||= Set.new
671
+ scratch[:references][resource_type] << { reference: reference, referencing_resource: referencing_resource }
672
+ end
673
+
674
+ def save_delayed_references(resources, containing_resource_type = resource_type)
675
+ resources.each do |resource|
676
+ references_to_save(containing_resource_type).each do |reference_to_save|
677
+ references = resolve_path(resource, reference_to_save[:path]).select do |reference|
678
+ reference.is_a?(FHIR::Reference) &&
679
+ !reference.contained? && reference.reference.present?
680
+ end
681
+
682
+ references.each do |reference|
683
+ resource_type = reference.resource_class.name.demodulize
684
+ need_to_save = reference_to_save[:resources].include?(resource_type)
685
+ next unless need_to_save
686
+
687
+ reference_resource_type = resource.resourceType
688
+ reference_resource_id = resource.id
689
+
690
+ referencing_resource = "#{reference_resource_type}/#{reference_resource_id}"
691
+
692
+ save_resource_reference(resource_type, reference, referencing_resource)
693
+ end
694
+ end
695
+ end
696
+ end
697
+
698
+ #### RESULT CHECKING ####
699
+
700
+ def check_resource_against_params(resource, params)
701
+ params.each do |name, escaped_search_value|
702
+ values_found = []
703
+ search_value = unescape_search_value(escaped_search_value)
704
+
705
+ match_found = resource_matches_param?(resource, name, escaped_search_value, values_found)
706
+
707
+ assert match_found,
708
+ "#{resource_type}/#{resource.id} did not match the search parameters:\n" \
709
+ "* Expected: #{unescape_search_value(search_value)}\n" \
710
+ "* Found: #{values_found.map(&:inspect).join(', ')}"
711
+ end
712
+ end
713
+
714
+ def unescape_search_value(value)
715
+ value&.gsub('\\,', ',')
716
+ end
717
+
146
718
  def resource_matches_param?(resource, search_param_name, escaped_search_value, values_found = [])
147
719
  search_value = unescape_search_value(escaped_search_value)
148
720
  paths = search_param_paths(search_param_name)
@@ -220,6 +792,69 @@ module USQualityCoreTestKit
220
792
  match_found
221
793
  end
222
794
 
795
+ def tags(params)
796
+ return nil unless config.options[:tag_requests]
797
+
798
+ return nil if params.blank?
799
+
800
+ return [search_params_tag(params)] if %w[Condition DiagnosticReport DocumentReference Observation ServiceRequest].include? resource_type
801
+
802
+ nil
803
+ end
804
+
805
+ def search_params_tag(params)
806
+ "#{resource_type}?#{params.keys.join('&')}"
807
+ end
808
+
809
+ EXTENSION_VALUE_PATH_REGEX = /
810
+ \A
811
+ (?<extension_type>modifierExtension|extension)
812
+ \.where\(url='(?<url>[^']+)'\)
813
+ \.(?<value_path>value(?:[A-Z]\w*|\[x\])?)
814
+ \z
815
+ /x
816
+
817
+ def get_next_value(element, property)
818
+ return resolve_reference_value(element) if property.to_s == 'resolve()'
819
+
820
+ super
821
+ end
822
+
823
+ def resolve_reference_value(element)
824
+ reference = element&.reference
825
+ return if reference.blank? || reference.start_with?('#')
826
+
827
+ resolved_reference_cache[reference] ||= begin
828
+ resource_type, resource_id = resource_type_and_id_from_reference(reference)
829
+
830
+ if resource_type.blank? || resource_id.blank?
831
+ nil
832
+ else
833
+ fhir_read(resource_type, resource_id)&.resource
834
+ end
835
+ rescue StandardError => e
836
+ Inferno::Application['logger'].error("Unable to resolve reference #{reference}")
837
+ Inferno::Application['logger'].error(e.full_message)
838
+ nil
839
+ end
840
+ end
841
+
842
+ def resolved_reference_cache
843
+ @resolved_reference_cache ||= {}
844
+ end
845
+
846
+ def resource_type_and_id_from_reference(reference)
847
+ parts = reference.split('/')
848
+ history_index = parts.index('_history')
849
+ parts = parts.first(history_index) if history_index
850
+
851
+ parts.last(2)
852
+ end
853
+
854
+ def fixed_value_search_param_names
855
+ search_param_names.reject { |name| patient_id_param?(name) }
856
+ end
857
+
223
858
  def token_value_matches?(values_found, search_value)
224
859
  token_values = values_found.flat_map { |value| token_values_from_element(value) }
225
860
  search_tokens = split_escaped_search_values(search_value)