davinci_us_drug_formulary_test_kit 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/davinci_us_drug_formulary_test_kit/custom_groups/capability_statement/conformance_support_test.rb +41 -0
  4. data/lib/davinci_us_drug_formulary_test_kit/custom_groups/capability_statement/fhir_version_test.rb +15 -0
  5. data/lib/davinci_us_drug_formulary_test_kit/custom_groups/capability_statement/instantiate_test.rb +19 -0
  6. data/lib/davinci_us_drug_formulary_test_kit/custom_groups/capability_statement/json_support_test.rb +40 -0
  7. data/lib/davinci_us_drug_formulary_test_kit/custom_groups/capability_statement/profile_support_test.rb +39 -0
  8. data/lib/davinci_us_drug_formulary_test_kit/custom_groups/v2.0.1/capability_statement_group.rb +78 -0
  9. data/lib/davinci_us_drug_formulary_test_kit/date_search_validation.rb +121 -0
  10. data/lib/davinci_us_drug_formulary_test_kit/fhir_resource_navigation.rb +155 -0
  11. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_code_search_test.rb +54 -0
  12. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_drug_tier_search_test.rb +43 -0
  13. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_formulary_include_search_test.rb +40 -0
  14. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_formulary_search_test.rb +43 -0
  15. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_id_search_test.rb +42 -0
  16. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_lastupdated_search_test.rb +42 -0
  17. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_must_support_test.rb +46 -0
  18. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_period_search_test.rb +42 -0
  19. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_pharmacy_benefit_type_search_test.rb +43 -0
  20. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_read_test.rb +26 -0
  21. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_reference_resolution_test.rb +40 -0
  22. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_status_search_test.rb +42 -0
  23. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_subject_include_search_test.rb +40 -0
  24. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_subject_search_test.rb +43 -0
  25. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/basic_validation_test.rb +39 -0
  26. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic/metadata.yml +292 -0
  27. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/basic_group.rb +107 -0
  28. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary/formulary_coverage_area_search_test.rb +45 -0
  29. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary/formulary_coverage_type_search_test.rb +45 -0
  30. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary/formulary_formulary_coverage_search_test.rb +45 -0
  31. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary/formulary_id_search_test.rb +42 -0
  32. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary/formulary_identifier_search_test.rb +43 -0
  33. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary/formulary_lastupdated_search_test.rb +42 -0
  34. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary/formulary_must_support_test.rb +42 -0
  35. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary/formulary_name_search_test.rb +42 -0
  36. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary/formulary_period_search_test.rb +42 -0
  37. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary/formulary_read_test.rb +26 -0
  38. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary/formulary_status_search_test.rb +53 -0
  39. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary/formulary_type_search_test.rb +43 -0
  40. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary/formulary_validation_test.rb +39 -0
  41. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary/metadata.yml +278 -0
  42. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/formulary_group.rb +104 -0
  43. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/location/location_address_city_search_test.rb +44 -0
  44. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/location/location_address_postalcode_search_test.rb +44 -0
  45. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/location/location_address_search_test.rb +42 -0
  46. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/location/location_address_state_search_test.rb +44 -0
  47. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/location/location_id_search_test.rb +56 -0
  48. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/location/location_lastupdated_search_test.rb +44 -0
  49. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/location/location_must_support_test.rb +37 -0
  50. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/location/location_read_test.rb +26 -0
  51. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/location/location_validation_test.rb +39 -0
  52. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/location/metadata.yml +177 -0
  53. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/location_group.rb +92 -0
  54. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/medication_knowledge/medication_knowledge_code_search_test.rb +43 -0
  55. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/medication_knowledge/medication_knowledge_doseform_search_test.rb +45 -0
  56. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/medication_knowledge/medication_knowledge_drug_name_search_test.rb +44 -0
  57. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/medication_knowledge/medication_knowledge_id_search_test.rb +42 -0
  58. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/medication_knowledge/medication_knowledge_lastupdated_search_test.rb +42 -0
  59. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/medication_knowledge/medication_knowledge_must_support_test.rb +47 -0
  60. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/medication_knowledge/medication_knowledge_read_test.rb +26 -0
  61. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/medication_knowledge/medication_knowledge_status_search_test.rb +53 -0
  62. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/medication_knowledge/medication_knowledge_validation_test.rb +39 -0
  63. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/medication_knowledge/metadata.yml +214 -0
  64. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/medication_knowledge_group.rb +91 -0
  65. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/metadata.yml +1192 -0
  66. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/metadata.yml +371 -0
  67. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_coverage_area_search_test.rb +43 -0
  68. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_coverage_type_search_test.rb +43 -0
  69. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_formulary_coverage_include_search_test.rb +40 -0
  70. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_formulary_coverage_search_test.rb +43 -0
  71. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_id_search_test.rb +42 -0
  72. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_identifier_search_test.rb +43 -0
  73. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_lastupdated_search_test.rb +42 -0
  74. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_must_support_test.rb +66 -0
  75. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_name_search_test.rb +42 -0
  76. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_period_search_test.rb +42 -0
  77. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_read_test.rb +26 -0
  78. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_reference_resolution_test.rb +40 -0
  79. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_status_search_test.rb +53 -0
  80. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_type_search_test.rb +43 -0
  81. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan/payer_insurance_plan_validation_test.rb +39 -0
  82. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/payer_insurance_plan_group.rb +108 -0
  83. data/lib/davinci_us_drug_formulary_test_kit/generated/v2.0.1/usdf_test_suite.rb +114 -0
  84. data/lib/davinci_us_drug_formulary_test_kit/generator/group_generator.rb +181 -0
  85. data/lib/davinci_us_drug_formulary_test_kit/generator/group_metadata.rb +79 -0
  86. data/lib/davinci_us_drug_formulary_test_kit/generator/group_metadata_extractor.rb +329 -0
  87. data/lib/davinci_us_drug_formulary_test_kit/generator/ig_loader.rb +77 -0
  88. data/lib/davinci_us_drug_formulary_test_kit/generator/ig_metadata.rb +33 -0
  89. data/lib/davinci_us_drug_formulary_test_kit/generator/ig_metadata_extractor.rb +40 -0
  90. data/lib/davinci_us_drug_formulary_test_kit/generator/ig_resources.rb +60 -0
  91. data/lib/davinci_us_drug_formulary_test_kit/generator/include_search_test_generator.rb +68 -0
  92. data/lib/davinci_us_drug_formulary_test_kit/generator/must_support_metadata_extractor.rb +384 -0
  93. data/lib/davinci_us_drug_formulary_test_kit/generator/must_support_test_generator.rb +117 -0
  94. data/lib/davinci_us_drug_formulary_test_kit/generator/naming.rb +28 -0
  95. data/lib/davinci_us_drug_formulary_test_kit/generator/read_test_generator.rb +92 -0
  96. data/lib/davinci_us_drug_formulary_test_kit/generator/reference_resolution_test_generator.rb +91 -0
  97. data/lib/davinci_us_drug_formulary_test_kit/generator/search_definition_metadata_extractor.rb +187 -0
  98. data/lib/davinci_us_drug_formulary_test_kit/generator/search_metadata_extractor.rb +59 -0
  99. data/lib/davinci_us_drug_formulary_test_kit/generator/search_test_generator.rb +270 -0
  100. data/lib/davinci_us_drug_formulary_test_kit/generator/suite_generator.rb +94 -0
  101. data/lib/davinci_us_drug_formulary_test_kit/generator/terminology_binding_metadata_extractor.rb +116 -0
  102. data/lib/davinci_us_drug_formulary_test_kit/generator/validation_test_generator.rb +102 -0
  103. data/lib/davinci_us_drug_formulary_test_kit/generator/value_extractor.rb +113 -0
  104. data/lib/davinci_us_drug_formulary_test_kit/generator.rb +94 -0
  105. data/lib/davinci_us_drug_formulary_test_kit/igs/package/r4_search-parameters.json +65408 -0
  106. data/lib/davinci_us_drug_formulary_test_kit/igs/package.tgz +0 -0
  107. data/lib/davinci_us_drug_formulary_test_kit/must_support_test.rb +224 -0
  108. data/lib/davinci_us_drug_formulary_test_kit/read_test.rb +62 -0
  109. data/lib/davinci_us_drug_formulary_test_kit/reference_resolution_test.rb +174 -0
  110. data/lib/davinci_us_drug_formulary_test_kit/request_logger.rb +46 -0
  111. data/lib/davinci_us_drug_formulary_test_kit/search_test.rb +767 -0
  112. data/lib/davinci_us_drug_formulary_test_kit/search_test_properties.rb +58 -0
  113. data/lib/davinci_us_drug_formulary_test_kit/validation_test.rb +56 -0
  114. data/lib/davinci_us_drug_formulary_test_kit/version.rb +5 -0
  115. data/lib/davinci_us_drug_formulary_test_kit.rb +1 -0
  116. metadata +245 -0
@@ -0,0 +1,767 @@
1
+ require_relative 'date_search_validation'
2
+ require_relative 'fhir_resource_navigation'
3
+ require_relative 'search_test_properties'
4
+
5
+ module DaVinciUSDrugFormularyTestKit
6
+ module SearchTest
7
+ extend Forwardable
8
+ include DateSearchValidation
9
+ include FHIRResourceNavigation
10
+
11
+ def_delegators 'self.class', :metadata, :provenance_metadata, :properties
12
+ def_delegators 'properties',
13
+ :resource_type,
14
+ :search_param_names,
15
+ :include_param,
16
+ :saves_delayed_references?,
17
+ :first_search?,
18
+ :fixed_value_search?,
19
+ :test_post_search?,
20
+ :token_search_params,
21
+ :test_reference_variants?,
22
+ :params_with_comparators,
23
+ :multiple_or_search_params
24
+
25
+ def all_search_params
26
+ param_values = if resource_type == 'Location' && first_search?
27
+ resource_id_list
28
+ else
29
+ resource_status
30
+ end
31
+
32
+ @all_search_params ||=
33
+ param_values.each_with_object({}) do |value, params|
34
+ params[value] ||= []
35
+ new_params =
36
+ if fixed_value_search?
37
+ fixed_value_search_param_values.map { |fixed_value| fixed_value_search_params(fixed_value, value) }
38
+ else
39
+ [search_params_with_values(search_param_names, value)]
40
+ end
41
+ new_params.reject! do |params|
42
+ params.any? { |_key, value| value.blank? }
43
+ end
44
+ params[value].concat(new_params)
45
+ end
46
+ end
47
+
48
+ def all_include_search_params
49
+ @all_include_search_params ||=
50
+ search_param_names.each_with_object({}) do |value, params|
51
+ params[value] ||= fixed_value_search_param_values.filter_map do |fixed_value|
52
+ fixed_value_search_params(fixed_value, value).merge('_include' => include_param)
53
+ end
54
+ end
55
+ end
56
+
57
+ def any_valid_search_params?(search_params)
58
+ search_params.any? { |_resource_id, params| params.present? }
59
+ end
60
+
61
+ def run_search_test
62
+ # TODO: skip if not supported?
63
+
64
+ skip_if !any_valid_search_params?(all_search_params), unable_to_resolve_params_message
65
+ resources_returned =
66
+ all_search_params.flat_map do |resource_input, params_list|
67
+ params_list.flat_map { |params| perform_search(params, resource_input) }
68
+ end
69
+
70
+ skip_if resources_returned.empty?, no_resources_skip_message
71
+
72
+ perform_multiple_or_search_test if multiple_or_search_params.present?
73
+ end
74
+
75
+ def run_include_search_test
76
+ any_include_references = false
77
+
78
+ all_include_search_params.each_value do |params_list|
79
+ params_list.each do |params|
80
+ fhir_search(resource_type, params:)
81
+ check_search_response
82
+
83
+ page_count = 1
84
+ bundle = resource
85
+
86
+ until bundle.nil? || page_count == 20
87
+ matched_resources = bundle&.entry&.filter_map do |entry|
88
+ entry&.resource if entry&.search&.mode == 'match' && entry&.resource&.resourceType == resource_type
89
+ end
90
+
91
+ included_resources = bundle&.entry&.filter_map { |entry| entry.resource if entry.search&.mode == 'include' }
92
+
93
+ # For resources in the bundle with the relevant reference, check the bundle for the referenced resource
94
+ matched_resources&.each do |match|
95
+ search_param_paths(include_param).each do |include_ref_path|
96
+ include_ref = resolve_path(match, include_ref_path).first
97
+ next unless include_ref.present?
98
+
99
+ any_include_references = true
100
+ include_ref_id = include_ref.reference_id
101
+ include_target_present = included_resources.any? { |resource| resource.id == include_ref_id }
102
+
103
+ assert(include_target_present,
104
+ "#{include_ref.type}/#{include_ref_id} referenced by #{resource_type}/#{match.id} " \
105
+ 'was not included in the bundle')
106
+ end
107
+ end
108
+
109
+ # Fetch the next bundle page
110
+ next_bundle_link = bundle&.link&.find { |link| link.relation == 'next' }&.url
111
+ break if next_bundle_link.blank?
112
+
113
+ reply = fhir_client.raw_read_url(next_bundle_link)
114
+ store_request('outgoing') { reply }
115
+
116
+ bundle = fhir_client.parse_reply(FHIR::Bundle, fhir_client.default_format, reply)
117
+ page_count += 1
118
+ end
119
+ end
120
+ end
121
+
122
+ skip_if !any_include_references,
123
+ "Could not find any resources with a reference matching the include param #{include_param}"
124
+ end
125
+
126
+ def perform_search(params, resource_input)
127
+ fhir_search(resource_type, params:)
128
+
129
+ check_search_response
130
+
131
+ resources_returned =
132
+ fetch_all_bundled_resources.select { |resource| resource.resourceType == resource_type }
133
+
134
+ return [] if resources_returned.blank?
135
+
136
+ perform_comparator_searches(params, resource_input) if params_with_comparators.present?
137
+
138
+ filter_insurance_plan(resources_returned) if resource_type == 'InsurancePlan'
139
+
140
+ if first_search?
141
+ all_scratch_resources.concat(resources_returned).uniq!
142
+ scratch_resources_for_group(metadata.profile_url).concat(resources_returned).uniq!
143
+ # scratch_resources_for_group(resource_input).concat(resources_returned).uniq!
144
+
145
+ end
146
+
147
+ resources_returned.each do |resource|
148
+ check_resource_against_params(resource, params)
149
+ end
150
+
151
+ save_delayed_references(resources_returned) if saves_delayed_references?
152
+
153
+ return resources_returned if all_search_variants_tested?
154
+
155
+ perform_post_search(resources_returned, params) if test_post_search?
156
+ perform_reference_with_type_search(params, resources_returned.count) if test_reference_variants?
157
+ perform_search_with_system(params, resource_input) if token_search_params.present?
158
+
159
+ resources_returned
160
+ end
161
+
162
+ def perform_post_search(get_search_resources, params)
163
+ fhir_search resource_type, params:, search_method: :post
164
+
165
+ check_search_response
166
+
167
+ post_search_resources = fetch_all_bundled_resources.select { |resource| resource.resourceType == resource_type }
168
+
169
+ filter_insurance_plan(post_search_resources) if resource_type == 'InsurancePlan'
170
+
171
+ get_resource_count = get_search_resources.length
172
+ post_resource_count = post_search_resources.length
173
+
174
+ search_variant_test_records[:post_variant] = true
175
+
176
+ assert get_resource_count == post_resource_count,
177
+ 'Expected search by POST to return the same results as search by GET, ' \
178
+ "but GET search returned #{get_resource_count} resources, and POST search " \
179
+ "returned #{post_resource_count} resources."
180
+ end
181
+
182
+ def filter_insurance_plan(resources)
183
+ if metadata.profile_name.include?('Formulary')
184
+ resources.select! { |resource| formulary_insurance_plan? resource }
185
+ else
186
+ resources.select! { |resource| payer_insurance_plan? resource }
187
+ end
188
+ end
189
+
190
+ def formulary_insurance_plan?(resource)
191
+ resource&.type&.first&.coding&.any? { |coding| drugpol_coding? coding }
192
+ end
193
+
194
+ def payer_insurance_plan?(resource)
195
+ return false if formulary_insurance_plan? resource
196
+
197
+ true
198
+ end
199
+
200
+ def drugpol_coding?(coding)
201
+ coding&.code == 'DRUGPOL' && coding&.system == 'http://terminology.hl7.org/CodeSystem/v3-ActCode'
202
+ end
203
+
204
+ def search_and_check_response(params, resource_type = self.resource_type)
205
+ fhir_search(resource_type, params:)
206
+
207
+ check_search_response
208
+ end
209
+
210
+ def check_search_response
211
+ assert_response_status(200)
212
+ assert_resource_type(:bundle)
213
+ # NOTE: how do we want to handle validating Bundles?
214
+ end
215
+
216
+ def search_variant_test_records
217
+ @search_variant_test_records ||= initial_search_variant_test_records
218
+ end
219
+
220
+ def initial_search_variant_test_records
221
+ {}.tap do |records|
222
+ records[:post_variant] = false if test_post_search?
223
+ records[:reference_variants] = false if test_reference_variants?
224
+ records[:token_variants] = false if token_search_params.present?
225
+ records[:comparator_searches] = Set.new if params_with_comparators.present?
226
+ end
227
+ end
228
+
229
+ def all_search_variants_tested?
230
+ search_variant_test_records.all? { |_variant, tested| tested.present? } &&
231
+ all_comparator_searches_tested?
232
+ end
233
+
234
+ def all_comparator_searches_tested?
235
+ return true if params_with_comparators.blank?
236
+
237
+ Set.new(params_with_comparators) == search_variant_test_records[:comparator_searches]
238
+ end
239
+
240
+ def date_comparator_value(comparator, date)
241
+ date = date.start || date.end if date.is_a? FHIR::Period
242
+ case comparator
243
+ when 'lt', 'le'
244
+ comparator + (DateTime.xmlschema(date) + 1).xmlschema
245
+ when 'gt', 'ge'
246
+ comparator + (DateTime.xmlschema(date) - 1).xmlschema
247
+ else
248
+ # ''
249
+ raise "Unsupported comparator '#{comparator}'"
250
+ end
251
+ end
252
+
253
+ def required_comparators(name)
254
+ metadata
255
+ .search_definitions
256
+ .dig(name.to_sym, :comparators)
257
+ .select { |_comparator, expectation| expectation == 'SHALL' }
258
+ .keys
259
+ .map(&:to_s)
260
+ end
261
+
262
+ def perform_comparator_searches(params, _resource_input)
263
+ params_with_comparators.each do |name|
264
+ next if search_variant_test_records[:comparator_searches].include? name
265
+
266
+ required_comparators(name).each do |comparator|
267
+ paths = search_param_paths(name).first
268
+ date_element = find_a_value_at(scratch_resources_for_group(metadata.profile_url), paths)
269
+ params_with_comparator = params.merge(name => date_comparator_value(comparator, date_element))
270
+
271
+ search_and_check_response(params_with_comparator)
272
+
273
+ fetch_all_bundled_resources.each do |resource|
274
+ check_resource_against_params(resource, params_with_comparator) if resource.resourceType == resource_type
275
+ end
276
+ end
277
+
278
+ search_variant_test_records[:comparator_searches] << name
279
+ end
280
+ end
281
+
282
+ def perform_reference_with_type_search(params, resource_count)
283
+ return if resource_count.zero?
284
+ return if search_variant_test_records[:reference_variants]
285
+
286
+ new_search_params = params.each_with_object({}) do |(name, value), new_params|
287
+ definition = metadata.search_definitions[name.to_sym]
288
+ if definition[:type] != 'Reference'
289
+ new_params[name] = value
290
+ next
291
+ end
292
+
293
+ target_type = metadata.search_definitions[name.to_sym][:target]
294
+ new_value =
295
+ if value.start_with?("#{target_type}/")
296
+ value.delete_prefix("#{target_type}/")
297
+ else
298
+ "#{target_type}/#{value}"
299
+ end
300
+ new_params[name] = new_value
301
+ end
302
+
303
+ search_and_check_response(new_search_params)
304
+
305
+ reference_with_type_resources = fetch_all_bundled_resources.select do |resource|
306
+ resource.resourceType == resource_type
307
+ end
308
+
309
+ filter_insurance_plan(reference_with_type_resources) if resource_type == 'InsurancePlan'
310
+
311
+ new_resource_count = reference_with_type_resources.count
312
+
313
+ assert new_resource_count == resource_count,
314
+ "Expected search by `#{params['patient']}` to to return the same results as searching " \
315
+ "by `#{new_search_params['patient']}`, but found #{resource_count} resources with " \
316
+ "`#{params['patient']}` and #{new_resource_count} with `#{new_search_params['patient']}`"
317
+
318
+ search_variant_test_records[:reference_variants] = true
319
+ end
320
+
321
+ def perform_search_with_system(params, resource_input)
322
+ return if search_variant_test_records[:token_variants]
323
+
324
+ new_search_params = search_params_with_values(token_search_params, resource_input, include_system: true)
325
+ return if new_search_params.any? { |_name, value| value.blank? }
326
+
327
+ search_params = params.merge(new_search_params)
328
+ search_and_check_response(search_params)
329
+
330
+ resources_returned =
331
+ fetch_all_bundled_resources
332
+ .select { |resource| resource.resourceType == resource_type }
333
+
334
+ assert resources_returned.present?, 'No resources were returned when searching by `system|code`'
335
+
336
+ search_variant_test_records[:token_variants] = true
337
+ end
338
+
339
+ def status_search_param_name
340
+ @status_search_param_name ||=
341
+ metadata.search_definitions.keys.find { |key| key.to_s.include? 'status' }
342
+ end
343
+
344
+ def status_search_values
345
+ default_search_values(status_search_param_name)
346
+ end
347
+
348
+ def default_search_values(param_name)
349
+ definition = metadata.search_definitions[param_name]
350
+ return [] if definition.blank?
351
+
352
+ definition[:multiple_or] == 'SHALL' ? [definition[:values].join(',')] : Array.wrap(definition[:values])
353
+ end
354
+
355
+ def perform_multiple_or_search_test
356
+ resolved_one = false
357
+
358
+ all_search_params.each_value do |params_list|
359
+ next unless params_list.present?
360
+
361
+ search_params = params_list.first
362
+ existing_values = {}
363
+ missing_values = {}
364
+
365
+ multiple_or_search_params.each do |param_name|
366
+ search_value = default_search_values(param_name.to_sym)
367
+ search_params = search_params.merge(param_name.to_s => search_value)
368
+ existing_values[param_name.to_sym] =
369
+ scratch_resources_for_group(metadata.profile_url).map(&param_name.to_sym).compact.uniq
370
+ end
371
+
372
+ # skip patient without multiple-or values
373
+ next if existing_values.values.any?(&:empty?)
374
+
375
+ resolved_one = true
376
+
377
+ search_and_check_response(search_params)
378
+
379
+ resources_returned =
380
+ fetch_all_bundled_resources
381
+ .select { |resource| resource.resourceType == resource_type }
382
+
383
+ multiple_or_search_params.each do |param_name|
384
+ missing_values[param_name.to_sym] =
385
+ existing_values[param_name.to_sym] - resources_returned.map(&param_name.to_sym)
386
+ end
387
+
388
+ missing_value_message = missing_values
389
+ .reject { |_param_name, missing_value| missing_value.empty? }
390
+ .map { |param_name, missing_value| "#{missing_value.join(',')} values from #{param_name}" }
391
+ .join(' and ')
392
+
393
+ assert missing_value_message.blank?,
394
+ "Could not find #{missing_value_message} in any of the resources returned for #{metadata.profile_name}"
395
+
396
+ break if resolved_one
397
+ end
398
+ end
399
+
400
+ def all_scratch_resources
401
+ scratch_resources[:all] ||= []
402
+ end
403
+
404
+ def scratch_resources_for_group(resource_value)
405
+ return all_scratch_resources if resource_value.nil?
406
+
407
+ scratch_resources[resource_value] ||= []
408
+ end
409
+
410
+ def references_to_save(_resource_type = nil)
411
+ metadata.delayed_references
412
+ end
413
+
414
+ def fixed_value_search_param_name
415
+ (search_param_names - ['patient']).first
416
+ end
417
+
418
+ def fixed_value_search_param_values
419
+ metadata.search_definitions[fixed_value_search_param_name.to_sym][:values]
420
+ end
421
+
422
+ def fixed_value_search_params(value, resource_input)
423
+ search_param_names.each_with_object({}) do |name, params|
424
+ params[name] = resource_id_param?(name) ? resource_input : value
425
+ end
426
+ end
427
+
428
+ def search_params_with_values(search_param_names, input_value, include_system: false)
429
+ resources = scratch_resources_for_group(metadata.profile_url)
430
+
431
+ if resources.empty?
432
+ return search_param_names.each_with_object({}) do |name, params|
433
+ # if you get an input value, map that to name {{"_id"=>"StateOfCTLocation"}}
434
+ value = (input_value if status_param?(name) || resource_id_param?(name))
435
+ params[name] = value
436
+ end
437
+ end
438
+ resources.each_with_object({}) do |resource, outer_params|
439
+ results_from_one_resource = search_param_names.each_with_object({}) do |name, params|
440
+ value = if status_param?(name)
441
+ input_value
442
+ else
443
+ search_param_value(name, resource,
444
+ include_system:)
445
+ end
446
+ params[name] = value
447
+ end
448
+
449
+ outer_params.merge!(results_from_one_resource)
450
+
451
+ # stop if all parameter values are found
452
+ return outer_params if outer_params.all? { |_key, value| value.present? }
453
+ end
454
+ end
455
+
456
+ def resource_status
457
+ ['active']
458
+ end
459
+
460
+ def resource_id_list
461
+ return [] unless respond_to? :input_ids
462
+
463
+ input_ids.split(',').map(&:strip)
464
+ # return [] if input_ids.blank? || !respond_to?(input_ids)
465
+ # send(input_ids).split(',').map(&:strip)
466
+ end
467
+
468
+ def resource_id_param?(name)
469
+ name == '_id'
470
+ # name == 'patient' || (name == '_id' && resource_type == 'Patient')
471
+ end
472
+
473
+ def status_param?(name)
474
+ name == 'status'
475
+ end
476
+
477
+ def search_param_paths(name)
478
+ paths = metadata.search_definitions[name.to_sym][:paths]
479
+ paths[0] = 'local_class' if paths.first == 'class'
480
+ paths.map { |path| path.delete_prefix('Resource.') }
481
+ end
482
+
483
+ def all_search_params_present?(params)
484
+ params.all? { |_name, value| value.present? }
485
+ end
486
+
487
+ def array_of_codes(array)
488
+ array.map { |name| "`#{name}`" }.join(', ')
489
+ end
490
+
491
+ def unable_to_resolve_params_message
492
+ "Could not find values for all search params #{array_of_codes(search_param_names)}"
493
+ end
494
+
495
+ def empty_search_params_message(empty_search_params)
496
+ "Could not find values for the search parameters #{array_of_codes(empty_search_params.keys)}"
497
+ end
498
+
499
+ def no_resources_skip_message(resource_type = self.resource_type)
500
+ msg = "No #{resource_type} resources appear to be available"
501
+
502
+ if resource_type == 'Device' && implantable_device_codes.present?
503
+ msg.concat(" with the following Device Type Code filter: #{implantable_device_codes}")
504
+ end
505
+
506
+ "#{msg}. Please use resources with more information"
507
+ end
508
+
509
+ def fetch_all_bundled_resources(
510
+ reply_handler: nil,
511
+ max_pages: 20,
512
+ additional_resource_types: [],
513
+ resource_type: self.resource_type
514
+ )
515
+ page_count = 1
516
+ resources = []
517
+ bundle = resource
518
+
519
+ until bundle.nil? || page_count == max_pages
520
+ resources += bundle&.entry&.map { |entry| entry&.resource }
521
+ next_bundle_link = bundle&.link&.find { |link| link.relation == 'next' }&.url
522
+ reply_handler&.call(response)
523
+
524
+ break if next_bundle_link.blank?
525
+
526
+ reply = fhir_client.raw_read_url(next_bundle_link)
527
+
528
+ store_request('outgoing') { reply }
529
+ error_message = cant_resolve_next_bundle_message(next_bundle_link)
530
+
531
+ assert_response_status(200)
532
+ assert_valid_json(reply.body, error_message)
533
+
534
+ bundle = fhir_client.parse_reply(FHIR::Bundle, fhir_client.default_format, reply)
535
+
536
+ page_count += 1
537
+ end
538
+
539
+ valid_resource_types = [resource_type, 'OperationOutcome'].concat(additional_resource_types)
540
+ valid_resource_types << 'Medication' if resource_type == 'MedicationRequest'
541
+
542
+ invalid_resource_types =
543
+ resources.reject { |entry| valid_resource_types.include? entry.resourceType }
544
+ .map(&:resourceType)
545
+ .uniq
546
+
547
+ if invalid_resource_types.any?
548
+ info "Received resource type(s) #{invalid_resource_types.join(', ')} in search bundle, " \
549
+ "but only expected resource types #{valid_resource_types.join(', ')}. " \
550
+ 'This is unusual but allowed if the server believes additional resource types are relevant.'
551
+ end
552
+
553
+ resources
554
+ end
555
+
556
+ def cant_resolve_next_bundle_message(link)
557
+ "Could not resolve next bundle: #{link}"
558
+ end
559
+
560
+ def search_param_value(name, resource, include_system: false)
561
+ paths = search_param_paths(name)
562
+ search_value = nil
563
+ paths.each do |path|
564
+ element = find_a_value_at(resource, path) { |element| element_has_valid_value?(element, include_system) }
565
+ search_value =
566
+ case element
567
+ when FHIR::Period
568
+ if element.start.present?
569
+ "gt#{(DateTime.xmlschema(element.start) - 1).xmlschema}"
570
+ else
571
+ end_datetime = get_fhir_datetime_range(element.end)[:end]
572
+ "lt#{(end_datetime + 1).xmlschema}"
573
+ end
574
+ when FHIR::Reference
575
+ element.reference
576
+ when FHIR::CodeableConcept
577
+ if include_system
578
+ coding =
579
+ find_a_value_at(element, 'coding') { |coding| coding.code.present? && coding.system.present? }
580
+ "#{coding.system}|#{coding.code}"
581
+ else
582
+ find_a_value_at(element, 'coding.code')
583
+ end
584
+ when FHIR::Identifier
585
+ include_system ? "#{element.system}|#{element.value}" : element.value
586
+ when FHIR::Coding
587
+ include_system ? "#{element.system}|#{element.code}" : element.code
588
+ when FHIR::HumanName
589
+ element.family || element.given&.first || element.text
590
+ when FHIR::Address
591
+ element.text || element.city || element.state || element.postalCode || element.country
592
+ else
593
+ if metadata.version != 'v3.1.1' &&
594
+ metadata.search_definitions[name.to_sym][:type] == 'date' &&
595
+ params_with_comparators&.include?(name)
596
+ # convert date search to greath-than comparator search with correct precision
597
+ # For all date search parameters:
598
+ # Patient.birthDate does not mandate comparators so cannot be converted
599
+ # Goal.target-date has day precision
600
+ # All others have second + time offset precision
601
+ if /^\d{4}(-\d{2})?$/.match?(element) || # YYYY or YYYY-MM
602
+ (/^\d{4}-\d{2}-\d{2}$/.match?(element) && resource_type != 'Goal') # YYY-MM-DD AND Resource is NOT Goal
603
+ "gt#{(DateTime.xmlschema(element) - 1).xmlschema}"
604
+ else
605
+ element
606
+ end
607
+ else
608
+ element
609
+ end
610
+ end
611
+
612
+ break if search_value.present?
613
+ end
614
+
615
+ search_value&.gsub(',', '\\,')
616
+ end
617
+
618
+ def element_has_valid_value?(element, include_system)
619
+ case element
620
+ when FHIR::Reference
621
+ element.reference.present?
622
+ when FHIR::CodeableConcept
623
+ if include_system
624
+ coding =
625
+ find_a_value_at(element, 'coding') { |coding| coding.code.present? && coding.system.present? }
626
+ coding.present?
627
+ else
628
+ find_a_value_at(element, 'coding.code').present?
629
+ end
630
+ when FHIR::Identifier
631
+ include_system ? element.value.present? && element.system.present? : element.value.present?
632
+ when FHIR::Coding
633
+ include_system ? element.code.present? && element.system.present? : element.code.present?
634
+ when FHIR::HumanName
635
+ (element.family || element.given&.first || element.text).present?
636
+ when FHIR::Address
637
+ (element.text || element.city || element.state || element.postalCode || element.country).present?
638
+ else
639
+ true
640
+ end
641
+ end
642
+
643
+ def save_resource_reference(resource_type, reference)
644
+ scratch[:references] ||= {}
645
+ scratch[:references][resource_type] ||= Set.new
646
+ scratch[:references][resource_type] << reference
647
+ end
648
+
649
+ def save_delayed_references(resources, containing_resource_type = resource_type)
650
+ resources.each do |resource|
651
+ references_to_save(containing_resource_type).each do |reference_to_save|
652
+ resolve_path(resource, reference_to_save[:path])
653
+ .select { |reference| reference.is_a?(FHIR::Reference) && !reference.contained? }
654
+ .each do |reference|
655
+ resource_type = reference.resource_class.name.demodulize
656
+ need_to_save = reference_to_save[:resources].include?(resource_type)
657
+ next unless need_to_save
658
+
659
+ save_resource_reference(resource_type, reference)
660
+ end
661
+ end
662
+ end
663
+ end
664
+
665
+ #### RESULT CHECKING ####
666
+
667
+ def check_resource_against_params(resource, params)
668
+ params.each do |name, escaped_search_value|
669
+ # unescape search value
670
+ search_value = escaped_search_value&.gsub('\\,', ',')
671
+ paths = search_param_paths(name)
672
+ match_found = false
673
+ values_found = []
674
+
675
+ paths.each do |path|
676
+ type = metadata.search_definitions[name.to_sym][:type]
677
+ values_found =
678
+ resolve_path(resource, path)
679
+ .map do |value|
680
+ if value.is_a? FHIR::Reference
681
+ value.reference
682
+ else
683
+ value
684
+ end
685
+ end
686
+ match_found =
687
+ case type
688
+ when 'Period', 'date', 'instant', 'dateTime'
689
+ values_found.any? { |date| validate_date_search(search_value, date) }
690
+ when 'HumanName'
691
+ # When a string search parameter refers to the types HumanName and Address,
692
+ # the search covers the elements of type string, and does not cover elements such as use and period
693
+ # https://www.hl7.org/fhir/search.html#string
694
+ search_value_downcase = search_value.downcase
695
+ values_found.any? do |name|
696
+ name&.text&.downcase&.start_with?(search_value_downcase) ||
697
+ name&.family&.downcase&.start_with?(search_value_downcase) ||
698
+ name&.given&.any? { |given| given.downcase.start_with?(search_value_downcase) } ||
699
+ name&.prefix&.any? { |prefix| prefix.downcase.start_with?(search_value_downcase) } ||
700
+ name&.suffix&.any? { |suffix| suffix.downcase.start_with?(search_value_downcase) }
701
+ end
702
+ when 'Address'
703
+ search_value_downcase = search_value.downcase
704
+ values_found.any? do |address|
705
+ address&.text&.downcase&.start_with?(search_value_downcase) ||
706
+ address&.city&.downcase&.start_with?(search_value_downcase) ||
707
+ address&.state&.downcase&.start_with?(search_value_downcase) ||
708
+ address&.postalCode&.downcase&.start_with?(search_value_downcase) ||
709
+ address&.country&.downcase&.start_with?(search_value_downcase)
710
+ end
711
+ when 'CodeableConcept'
712
+ # FHIR token search (https://www.hl7.org/fhir/search.html#token): "When in doubt, servers SHOULD
713
+ # treat tokens in a case-insensitive manner, on the grounds that including undesired data has
714
+ # less safety implications than excluding desired behavior".
715
+ codings = values_found.flat_map(&:coding)
716
+ if search_value.include? '|'
717
+ system = search_value.split('|').first
718
+ code = search_value.split('|').last
719
+ codings&.any? { |coding| coding.system == system && coding.code&.casecmp?(code) }
720
+ else
721
+ codings&.any? { |coding| coding.code&.casecmp?(search_value) }
722
+ end
723
+ when 'Coding'
724
+ if search_value.include? '|'
725
+ system = search_value.split('|').first
726
+ code = search_value.split('|').last
727
+ values_found.any? { |coding| coding.system == system && coding.code&.casecmp?(code) }
728
+ else
729
+ values_found.any? { |coding| coding.code&.casecmp?(search_value) }
730
+ end
731
+ when 'Identifier'
732
+ if search_value.include? '|'
733
+ values_found.any? { |identifier| "#{identifier.system}|#{identifier.value}" == search_value }
734
+ else
735
+ values_found.any? { |identifier| identifier.value == search_value }
736
+ end
737
+ when 'string'
738
+ searched_values = search_value.downcase.split(/(?<!\\\\),/).map { |string| string.gsub('\\,', ',') }
739
+ values_found.any? do |value_found|
740
+ searched_values.any? { |searched_value| value_found.downcase.starts_with? searched_value }
741
+ end
742
+ else
743
+ # searching by patient requires special case because we are searching by a resource identifier
744
+ # references can also be URLs, so we may need to resolve those URLs
745
+ if ['subject', 'patient'].include? name.to_s
746
+ id = search_value.split('Patient/').last
747
+ possible_values = [id, "Patient/#{id}", "#{url}/Patient/#{id}"]
748
+ values_found.any? do |reference|
749
+ possible_values.include? reference
750
+ end
751
+ else
752
+ search_values = search_value.split(/(?<!\\\\),/).map { |string| string.gsub('\\,', ',') }
753
+ values_found.any? { |value_found| search_values.include? value_found }
754
+ end
755
+ end
756
+
757
+ break if match_found
758
+ end
759
+
760
+ assert match_found,
761
+ "#{resource_type}/#{resource.id} did not match the search parameters:\n" \
762
+ "* Expected: #{search_value}\n" \
763
+ "* Found: #{values_found.map(&:inspect).join(', ')}"
764
+ end
765
+ end
766
+ end
767
+ end