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
@@ -2,11 +2,330 @@
2
2
 
3
3
  require_relative 'value_extractor'
4
4
 
5
- require 'us_core_test_kit/generator/must_support_metadata_extractor'
6
-
7
5
  module USQualityCoreTestKit
8
6
  class Generator
9
- class MustSupportMetadataExtractor < USCoreTestKit::Generator::MustSupportMetadataExtractor
7
+ class MustSupportMetadataExtractor
8
+ attr_accessor :profile_elements, :profile, :resource, :ig_resources
9
+
10
+ def initialize(profile_elements, profile, resource, ig_resources)
11
+ self.profile_elements = profile_elements
12
+ self.profile = profile
13
+ self.resource = resource
14
+ self.ig_resources = ig_resources
15
+ end
16
+
17
+ def must_supports
18
+ @must_supports = {
19
+ extensions: must_support_extensions,
20
+ slices: must_support_slices,
21
+ elements: must_support_elements
22
+ }
23
+
24
+ handle_special_cases
25
+
26
+ @must_supports
27
+ end
28
+
29
+ def uscdi_requirement_element?(element)
30
+ element.extension.any? do |extension|
31
+ extension.url == 'http://hl7.org/fhir/us/core/StructureDefinition/uscdi-requirement' &&
32
+ extension.valueBoolean
33
+ end && !element.mustSupport
34
+ end
35
+
36
+ def must_support_extension_elements
37
+ all_must_support_elements.select { |element| element.path.end_with? 'extension' }
38
+ end
39
+
40
+ def must_support_slice_elements
41
+ all_must_support_elements.select { |element| !element.path.end_with?('extension') && element.sliceName.present? }
42
+ end
43
+
44
+ def sliced_element(slice)
45
+ profile_elements.find { |element| element.id == slice.path || element.id == slice.id.sub(":#{slice.sliceName}", '') }
46
+ end
47
+
48
+ def discriminators(slice)
49
+ slice.slicing.discriminator
50
+ end
51
+
52
+ def must_support_type_slice_elements
53
+ must_support_slice_elements.select do |element|
54
+ discriminators(sliced_element(element)).first.type == 'type'
55
+ end
56
+ end
57
+
58
+ def type_slices
59
+ must_support_type_slice_elements.map do |current_element|
60
+ discriminator = discriminators(sliced_element(current_element)).first
61
+ type_path = discriminator.path
62
+ type_path = '' if type_path == '$this'
63
+ type_element =
64
+ if type_path.present?
65
+ profile_elements.find { |element| element.id == "#{current_element.id}.#{type_path}" }
66
+ else
67
+ current_element
68
+ end
69
+
70
+ type_code = type_element.type.first.code
71
+
72
+ discriminator = {
73
+ type: 'type',
74
+ code: type_code.upcase_first
75
+ }
76
+
77
+ discriminator[:path] = type_path unless type_path.empty?
78
+
79
+ {
80
+ slice_id: current_element.id,
81
+ slice_name: current_element.sliceName,
82
+ path: current_element.path.gsub("#{resource}.", ''),
83
+ discriminator: discriminator
84
+ }.tap do |metadata|
85
+ metadata[:uscdi_only] = true if uscdi_requirement_element?(current_element)
86
+ end
87
+ end
88
+ end
89
+
90
+ def must_support_value_slice_elements
91
+ must_support_slice_elements.select do |element|
92
+ # 'pattern' is deparected in FHIR R5
93
+ # 'pattern' type is used in US Core v7 and all earlier versions.
94
+ # Since US Core v8, all 'patten' types are moved to 'value' type
95
+ %w[value pattern].include?(discriminators(sliced_element(element)).first.type)
96
+ end
97
+ end
98
+
99
+ def value_slices
100
+ must_support_value_slice_elements.map do |current_element|
101
+ {
102
+ slice_id: current_element.id,
103
+ slice_name: current_element.sliceName,
104
+ path: current_element.path.gsub("#{resource}.", '')
105
+ }.tap do |metadata|
106
+ fixed_values = []
107
+ pattern_value = {}
108
+
109
+ discriminators(sliced_element(current_element)).each do |discriminator|
110
+ discriminator_path = discriminator.path
111
+ discriminator_path = '' if discriminator_path == '$this'
112
+ pattern_element =
113
+ if discriminator_path.present?
114
+ sliced_target_path = "#{current_element.path}.#{discriminator_path}"
115
+
116
+ profile_elements.find do |element|
117
+ element.id.starts_with?(current_element.id) &&
118
+ element.path == sliced_target_path
119
+ end
120
+ else
121
+ current_element
122
+ end
123
+
124
+ if pattern_element.fixedCode.present? || pattern_element.fixedUri.present?
125
+ fixed_values << {
126
+ path: discriminator.path,
127
+ value: pattern_element.fixedUri || pattern_element.fixedCode
128
+ }
129
+ elsif pattern_value.present?
130
+ raise StandardError, "Found more than one pattern slices for the same element #{pattern_element}."
131
+ else
132
+ pattern_value = save_pattern_slice(pattern_element, discriminator_path, metadata)
133
+ end
134
+ end
135
+
136
+ if !fixed_values.empty?
137
+ metadata[:discriminator] = {
138
+ type: 'value',
139
+ values: fixed_values
140
+ }
141
+ elsif pattern_value.present?
142
+ metadata[:discriminator] = pattern_value
143
+ end
144
+
145
+ metadata[:uscdi_only] = true if uscdi_requirement_element?(current_element)
146
+ end
147
+ end
148
+ end
149
+
150
+ def save_pattern_slice(pattern_element, discriminator_path, metadata)
151
+ if pattern_element.patternCodeableConcept.present?
152
+ {
153
+ type: 'patternCodeableConcept',
154
+ path: discriminator_path,
155
+ code: pattern_element.patternCodeableConcept.coding.first.code,
156
+ system: pattern_element.patternCodeableConcept.coding.first.system
157
+ }
158
+ elsif pattern_element.patternCoding.present?
159
+ {
160
+ type: 'patternCoding',
161
+ path: discriminator_path,
162
+ code: pattern_element.patternCoding.code,
163
+ system: pattern_element.patternCoding.system
164
+ }
165
+ elsif pattern_element.patternIdentifier.present?
166
+ {
167
+ type: 'patternIdentifier',
168
+ path: discriminator_path,
169
+ system: pattern_element.patternIdentifier.system
170
+ }
171
+ elsif pattern_element.binding&.strength == 'required' &&
172
+ pattern_element.binding&.valueSet.present?
173
+
174
+ value_extractor = ValueExactor.new(ig_resources, resource, profile_elements)
175
+
176
+ values = value_extractor.codings_from_value_set_binding(pattern_element).presence ||
177
+ value_extractor.values_from_resource_metadata([metadata[:path]]).presence || []
178
+
179
+ {
180
+ type: 'requiredBinding',
181
+ path: discriminator_path,
182
+ values: values
183
+ }
184
+ else
185
+ raise StandardError, 'Unsupported discriminator pattern type'
186
+ end
187
+ end
188
+
189
+ def plain_must_support_elements
190
+ all_must_support_elements - must_support_extension_elements - must_support_slice_elements
191
+ end
192
+
193
+ def element_part_of_slice_discrimination?(element)
194
+ must_support_slice_elements.any? { |ms_slice| element.id.include?(ms_slice.id) }
195
+ end
196
+
197
+ def handle_fixed_values(metadata, element)
198
+ if element.fixedUri.present?
199
+ metadata[:fixed_value] = element.fixedUri
200
+ elsif element.patternCodeableConcept.present? && !element_part_of_slice_discrimination?(element)
201
+ metadata[:fixed_value] = element.patternCodeableConcept.coding.first.code
202
+ metadata[:path] += '.coding.code'
203
+ elsif element.fixedCode.present?
204
+ metadata[:fixed_value] = element.fixedCode
205
+ elsif element.patternIdentifier.present? && !element_part_of_slice_discrimination?(element)
206
+ metadata[:fixed_value] = element.patternIdentifier.system
207
+ metadata[:path] += '.system'
208
+ end
209
+ end
210
+
211
+ def type_must_support_extension?(extensions)
212
+ extensions&.any? do |extension|
213
+ extension.url == 'http://hl7.org/fhir/StructureDefinition/elementdefinition-type-must-support' &&
214
+ extension.valueBoolean
215
+ end
216
+ end
217
+
218
+ def save_type_code?(type)
219
+ type.code == 'Reference'
220
+ end
221
+
222
+ def get_type_must_support_metadata(current_metadata, current_element)
223
+ current_element.type.map do |type|
224
+ next unless type_must_support_extension?(type.extension)
225
+
226
+ metadata =
227
+ {
228
+ path: "#{current_metadata[:path].delete_suffix('[x]')}#{type.code.upcase_first}",
229
+ original_path: current_metadata[:path]
230
+ }
231
+ metadata[:types] = [type.code] if save_type_code?(type)
232
+ handle_type_must_support_target_profiles(type, metadata) if type.code == 'Reference'
233
+
234
+ metadata
235
+ end.compact
236
+ end
237
+
238
+ def handle_type_must_support_target_profiles(type, metadata)
239
+ target_profiles = []
240
+
241
+ if type.targetProfile&.length == 1
242
+ target_profiles << type.targetProfile.first
243
+ else
244
+ type.source_hash['_targetProfile']&.each_with_index do |hash, index|
245
+ if hash.present?
246
+ element = FHIR::Element.new(hash)
247
+ target_profiles << type.targetProfile[index] if type_must_support_extension?(element.extension)
248
+ end
249
+ end
250
+ end
251
+
252
+ # remove target_profile for FHIR Base resource type.
253
+ target_profiles.delete_if { |reference| reference.start_with?('http://hl7.org/fhir/StructureDefinition') }
254
+ metadata[:target_profiles] = target_profiles if target_profiles.present?
255
+ end
256
+
257
+ def handle_choice_type_in_sliced_element(current_metadata, must_support_elements_metadata)
258
+ choice_element_metadata = must_support_elements_metadata.find do |metadata|
259
+ metadata[:original_path].present? &&
260
+ current_metadata[:path].include?(metadata[:original_path])
261
+ end
262
+
263
+ return unless choice_element_metadata.present?
264
+
265
+ current_metadata[:original_path] = current_metadata[:path]
266
+ current_metadata[:path] = current_metadata[:path].sub(choice_element_metadata[:original_path], choice_element_metadata[:path])
267
+ end
268
+
269
+ #### SPECIAL CASE ####
270
+
271
+ def vital_sign?
272
+ [
273
+ 'http://hl7.org/fhir/StructureDefinition/vitalsigns',
274
+ 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-vital-signs'
275
+ ].include?(profile.baseDefinition)
276
+ end
277
+
278
+ def blood_pressure?
279
+ %w[bp us-core-blood-pressure us-core-average-blood-pressure].include?(profile.id)
280
+ end
281
+
282
+ # Exclude Observation.component from vital sign profiles except observation-bp and observation-pulse-ox
283
+
284
+ def remove_vital_sign_component
285
+ return if blood_pressure? || profile.name == 'USCorePulseOximetryProfile'
286
+
287
+ return unless vital_sign?
288
+
289
+ @must_supports[:elements].delete_if do |element|
290
+ element[:path].start_with?('component')
291
+ end
292
+ end
293
+
294
+ # Exclude Observation.value[x] from observation-bp
295
+
296
+ def remove_blood_pressure_value_data_absent_reason
297
+ return unless blood_pressure?
298
+
299
+ @must_supports[:elements].delete_if do |element|
300
+ element[:path].start_with?('value[x]') ||
301
+ element[:original_path]&.start_with?('value[x]') ||
302
+ element[:path] == ('dataAbsentReason')
303
+ end
304
+
305
+ @must_supports[:slices].delete_if do |slice|
306
+ slice[:path].start_with?('value[x]')
307
+ end
308
+ end
309
+
310
+ # ONC and US Core 4.0.0 both clarified that health IT developers that always provide HL7 FHIR "observation" values
311
+ # are not required to demonstrate Health IT Module support for "dataAbsentReason" elements.
312
+ # Remove MS check for dataAbsentReason and component.dataAbsentReason from vital sign profiles and observation lab profile
313
+ # Smoking status profile does not have MS on dataAbsentReason. It is safe to use profile.type == 'Observation'
314
+ # Since US Core 5.0.1, Blood Pressure profile restores component.dataAbsentReason as MustSupport.
315
+ # US Quality Core Test Kit has to keep this as there are still profiles either are us-core or inherit from us-core
316
+
317
+ def remove_observation_data_absent_reason
318
+ return if blood_pressure?
319
+
320
+ pattern = /(component(:[^.]+)?\.)?dataAbsentReason/
321
+
322
+ return unless profile.type == 'Observation'
323
+
324
+ @must_supports[:elements].delete_if do |element|
325
+ pattern.match?(element[:path])
326
+ end
327
+ end
328
+
10
329
  def uscdi_plus_quality_element?(element)
11
330
  !element.mustSupport && element.extension.any? do |extension|
12
331
  extension.url.downcase.ends_with? 'uscdiplusquality'
@@ -23,20 +342,26 @@ module USQualityCoreTestKit
23
342
 
24
343
  def all_must_support_elements
25
344
  profile_elements.select do |element|
26
- element.mustSupport || uscdi_plus_quality_element?(element) || is_uscdi_requirement_element?(element)
345
+ element.mustSupport || uscdi_plus_quality_element?(element) || uscdi_requirement_element?(element)
27
346
  end
28
347
  end
29
348
 
30
349
  def must_support_extensions
31
- super.map do |extension|
32
- profile_element = must_support_extension_elements.find { |e| e.id == extension[:id] }
350
+ must_support_extension_elements.map do |element|
351
+ metadata = {
352
+ id: element.id,
353
+ path: element.path.gsub("#{resource}.", ''),
354
+ url: element.type.first.profile.first
355
+ }
356
+
357
+ metadata[:uscdi_only] = true if uscdi_requirement_element?(element)
33
358
 
34
- add_us_quality_coreuscdi_plus_quality_flag(profile_element, extension)
359
+ add_us_quality_coreuscdi_plus_quality_flag(element, metadata)
35
360
  end
36
361
  end
37
362
 
38
363
  def must_support_slices
39
- super.map do |slice|
364
+ (type_slices + value_slices).map do |slice|
40
365
  profile_element = must_support_slice_elements.find { |e| e.id == slice[:slice_id] }
41
366
 
42
367
  add_us_quality_coreuscdi_plus_quality_flag(profile_element, slice)
@@ -44,7 +369,38 @@ module USQualityCoreTestKit
44
369
  end
45
370
 
46
371
  def must_support_elements
47
- ms_elements = super
372
+ ms_elements =
373
+ plain_must_support_elements.each_with_object([]) do |current_element, must_support_elements_metadata|
374
+ {
375
+ path: current_element.id.gsub("#{resource}.", '')
376
+ }.tap do |current_metadata|
377
+ if current_element.id.include?(':') && !current_element.id.include?('extension')
378
+ slice_name = current_element.id.match(/:(\w+)/)[1]
379
+
380
+ next if must_support_slice_elements.none? { |slice| slice.sliceName == slice_name }
381
+ end
382
+
383
+ current_metadata[:uscdi_only] = true if uscdi_requirement_element?(current_element)
384
+
385
+ type_must_support_metadata = get_type_must_support_metadata(current_metadata, current_element)
386
+
387
+ if type_must_support_metadata.any?
388
+ must_support_elements_metadata.concat(type_must_support_metadata)
389
+ else
390
+ handle_choice_type_in_sliced_element(current_metadata, must_support_elements_metadata)
391
+
392
+ supported_types = current_element.type.select { |type| save_type_code?(type) }.map(&:code)
393
+ current_metadata[:types] = supported_types if supported_types.present?
394
+
395
+ handle_type_must_support_target_profiles(current_element.type.first, current_metadata) if current_element.type.first&.code == 'Reference'
396
+
397
+ handle_fixed_values(current_metadata, current_element)
398
+
399
+ must_support_elements_metadata << current_metadata
400
+ end
401
+ end
402
+ end.uniq
403
+
48
404
  return ms_elements unless profile.url.include?('us-quality-core')
49
405
 
50
406
  ms_elements.map do |element|
@@ -58,6 +414,7 @@ module USQualityCoreTestKit
58
414
  end
59
415
 
60
416
  # Aligns US Core v6.1.0 must support requirements to [USCDI v3.1](https://isp.healthit.gov/united-states-core-data-interoperability-uscdi#uscdi-v3-1)
417
+
61
418
  def remove_patient_gender_identity
62
419
  return unless profile.type == 'Patient'
63
420
  return unless @must_supports && @must_supports[:extensions]
@@ -1,13 +1,64 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'us_core_test_kit/generator/must_support_test_generator'
4
-
5
3
  require_relative 'naming'
6
4
  require_relative 'special_cases'
7
5
 
8
6
  module USQualityCoreTestKit
9
7
  class Generator
10
- class MustSupportTestGenerator < USCoreTestKit::Generator::MustSupportTestGenerator
8
+ class MustSupportTestGenerator
9
+ attr_accessor :group_metadata, :base_output_dir
10
+
11
+ def initialize(group_metadata, base_output_dir)
12
+ self.group_metadata = group_metadata
13
+ self.base_output_dir = base_output_dir
14
+ end
15
+
16
+ def output
17
+ @output ||= ERB.new(template, trim_mode: '-').result(binding)
18
+ end
19
+
20
+ def base_output_file_name
21
+ "#{class_name.underscore}.rb"
22
+ end
23
+
24
+ def output_file_directory
25
+ File.join(base_output_dir, profile_identifier)
26
+ end
27
+
28
+ def output_file_name
29
+ File.join(output_file_directory, base_output_file_name)
30
+ end
31
+
32
+ def read_interaction
33
+ self.class.read_interaction(group_metadata)
34
+ end
35
+
36
+ def resource_type
37
+ group_metadata.resource
38
+ end
39
+
40
+ def resource_collection_string
41
+ 'all_scratch_resources'
42
+ end
43
+
44
+ def must_support_list_string
45
+ build_must_support_list_string(false)
46
+ end
47
+
48
+ def uscdi_list_string
49
+ build_must_support_list_string(true)
50
+ end
51
+
52
+ def generate
53
+ FileUtils.mkdir_p(output_file_directory)
54
+ File.write(output_file_name, output)
55
+
56
+ group_metadata.add_test(
57
+ id: test_id,
58
+ file_name: base_output_file_name
59
+ )
60
+ end
61
+
11
62
  class << self
12
63
  def generate(ig_metadata, base_output_dir)
13
64
  ig_metadata.groups
@@ -1,13 +1,159 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'us_core_test_kit/generator/provenance_revinclude_search_test_generator'
4
-
5
3
  require_relative 'naming'
6
4
  require_relative 'special_cases'
7
5
 
8
6
  module USQualityCoreTestKit
9
7
  class Generator
10
- class ProvenanceRevincludeSearchTestGenerator < USCoreTestKit::Generator::ProvenanceRevincludeSearchTestGenerator
8
+ class ProvenanceRevincludeSearchTestGenerator
9
+ class << self
10
+ def generate(ig_metadata, base_output_dir)
11
+ ig_metadata.groups
12
+ .reject { |group| SpecialCases.exclude_group? group }
13
+ .select { |group| group.revincludes.include? 'Provenance:target' }
14
+ .each { |group| new(group, group.searches.first, base_output_dir).generate }
15
+ end
16
+ end
17
+
18
+ attr_accessor :group_metadata, :search_metadata, :base_output_dir
19
+
20
+ def initialize(group_metadata, search_metadata, base_output_dir)
21
+ self.group_metadata = group_metadata
22
+ self.search_metadata = search_metadata
23
+ self.base_output_dir = base_output_dir
24
+ end
25
+
26
+ def output
27
+ @output ||= ERB.new(template, trim_mode: '-').result(binding)
28
+ end
29
+
30
+ def base_output_file_name
31
+ "#{class_name.underscore}.rb"
32
+ end
33
+
34
+ def output_file_directory
35
+ File.join(base_output_dir, profile_identifier)
36
+ end
37
+
38
+ def output_file_name
39
+ File.join(output_file_directory, base_output_file_name)
40
+ end
41
+
42
+ def search_identifier
43
+ 'provenance_revinclude'
44
+ end
45
+
46
+ def search_title
47
+ search_identifier.camelize
48
+ end
49
+
50
+ def resource_type
51
+ group_metadata.resource
52
+ end
53
+
54
+ def search_params
55
+ @search_params ||=
56
+ search_metadata[:names].map do |name|
57
+ {
58
+ name: name,
59
+ path: search_definition(name)[:path]
60
+ }
61
+ end
62
+ end
63
+
64
+ def first_search?
65
+ group_metadata.searches.first == search_metadata
66
+ end
67
+
68
+ def fixed_value_search_param_name
69
+ (search_metadata[:names] - [:patient]).first
70
+ end
71
+
72
+ def search_param_name_string
73
+ "#{search_metadata[:names].join(' + ')} + revInclude:Provenance:target"
74
+ end
75
+
76
+ def search_param_names
77
+ search_params.map { |param| param[:name] }
78
+ end
79
+
80
+ def search_param_names_array
81
+ array_of_strings(search_param_names)
82
+ end
83
+
84
+ def path_for_value(path)
85
+ path == 'class' ? 'local_class' : path
86
+ end
87
+
88
+ def required_comparators_for_param(name)
89
+ search_definition(name)[:comparators].select { |_comparator, expectation| expectation == 'SHALL' }
90
+ end
91
+
92
+ def required_comparators
93
+ @required_comparators ||=
94
+ search_param_names.each_with_object({}) do |name, comparators|
95
+ required_comparators = required_comparators_for_param(name)
96
+ comparators[name] = required_comparators if required_comparators.present?
97
+ end
98
+ end
99
+
100
+ def search_definition(name)
101
+ group_metadata.search_definitions[name.to_sym]
102
+ end
103
+
104
+ def saves_delayed_references?
105
+ first_search? && group_metadata.delayed_references.present?
106
+ end
107
+
108
+ def possible_status_search?
109
+ !search_metadata[:names].include?('status') && group_metadata.search_definitions.key?(:status)
110
+ end
111
+
112
+ def token_search_params
113
+ @token_search_params ||=
114
+ search_param_names.select do |name|
115
+ %w[Identifier CodeableConcept Coding].include? group_metadata.search_definitions[name.to_sym][:type]
116
+ end
117
+ end
118
+
119
+ def token_search_params_string
120
+ array_of_strings(token_search_params)
121
+ end
122
+
123
+ def required_comparators_string
124
+ array_of_strings(required_comparators.keys)
125
+ end
126
+
127
+ def array_of_strings(array)
128
+ quoted_strings = array.map { |element| "'#{element}'" }
129
+ "[#{quoted_strings.join(', ')}]"
130
+ end
131
+
132
+ def search_properties
133
+ {}.tap do |properties|
134
+ properties[:fixed_value_search] = 'true' if fixed_value_search?
135
+ properties[:resource_type] = "'#{resource_type}'"
136
+ properties[:search_param_names] = search_param_names_array
137
+ properties[:possible_status_search] = 'true' if possible_status_search?
138
+ end
139
+ end
140
+
141
+ def search_test_properties_string
142
+ search_properties
143
+ .map { |key, value| "#{' ' * 8}#{key}: #{value}" }
144
+ .join(",\n")
145
+ end
146
+
147
+ def generate
148
+ FileUtils.mkdir_p(output_file_directory)
149
+ File.write(output_file_name, output)
150
+
151
+ group_metadata.add_test(
152
+ id: test_id,
153
+ file_name: base_output_file_name
154
+ )
155
+ end
156
+
11
157
  def template
12
158
  @template ||= File.read(File.join(__dir__, 'templates', 'provenance_revinclude_search.rb.erb'))
13
159
  end