cancer_pathology_data_sharing_test_kit 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/config/presets/demo_report.json +14 -0
  4. data/config/presets/inferno_reference_server_preset.json.erb +39 -0
  5. data/lib/cancer_pathology_data_sharing_test_kit/bundle_parse.rb +53 -0
  6. data/lib/cancer_pathology_data_sharing_test_kit/docs/data_access_suite_description.md +83 -0
  7. data/lib/cancer_pathology_data_sharing_test_kit/docs/report_generation_suite_description.md +118 -0
  8. data/lib/cancer_pathology_data_sharing_test_kit/fhir_resource_navigation.rb +174 -0
  9. data/lib/cancer_pathology_data_sharing_test_kit/group_metadata.rb +82 -0
  10. data/lib/cancer_pathology_data_sharing_test_kit/igs/.keep +0 -0
  11. data/lib/cancer_pathology_data_sharing_test_kit/igs/README.md +21 -0
  12. data/lib/cancer_pathology_data_sharing_test_kit/metadata.rb +75 -0
  13. data/lib/cancer_pathology_data_sharing_test_kit/must_support_test.rb +239 -0
  14. data/lib/cancer_pathology_data_sharing_test_kit/primitive_type.rb +5 -0
  15. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/data_absent/data_absent_reason_code_system.rb +22 -0
  16. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/data_absent/data_absent_reason_extension.rb +20 -0
  17. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/data_absent/sending_hide_missing.rb +28 -0
  18. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/data_absent_group.rb +27 -0
  19. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/encounter/encounter_must_support_test.rb +53 -0
  20. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/encounter/encounter_validation_test.rb +48 -0
  21. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/encounter/metadata.yml +281 -0
  22. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/patient/metadata.yml +305 -0
  23. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/patient/patient_must_support_test.rb +58 -0
  24. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/patient/patient_validation_test.rb +47 -0
  25. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/practitioner_role/metadata.yml +44 -0
  26. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/practitioner_role/practitioner_role_must_support_test.rb +45 -0
  27. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/practitioner_role/practitioner_role_validation_test.rb +47 -0
  28. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/service_request/metadata.yml +75 -0
  29. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/service_request/service_request_must_support_test.rb +52 -0
  30. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/service_request/service_request_validation_test.rb +47 -0
  31. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/specimen/metadata.yml +51 -0
  32. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/specimen/specimen_must_support_test.rb +53 -0
  33. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/specimen/specimen_validation_test.rb +47 -0
  34. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/us_pathology_diagnostic_report/diagnostic_report_validation_test.rb +47 -0
  35. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/us_pathology_diagnostic_report/metadata.yml +77 -0
  36. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/us_pathology_diagnostic_report/us_pathology_diagnostic_report_must_support_test.rb +51 -0
  37. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/us_pathology_exchange_bundle/exchange_bundle_validation_test.rb +56 -0
  38. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/us_pathology_exchange_bundle/metadata.yml +78 -0
  39. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle/us_pathology_exchange_bundle/us_pathology_exchange_bundle_must_support_test.rb +41 -0
  40. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite/exchange_bundle_group.rb +104 -0
  41. data/lib/cancer_pathology_data_sharing_test_kit/report_generation_suite.rb +73 -0
  42. data/lib/cancer_pathology_data_sharing_test_kit/urls.rb +25 -0
  43. data/lib/cancer_pathology_data_sharing_test_kit/us_core_data_access_suite/us_core_group.rb +72 -0
  44. data/lib/cancer_pathology_data_sharing_test_kit/us_core_data_access_suite.rb +58 -0
  45. data/lib/cancer_pathology_data_sharing_test_kit/validation_test.rb +96 -0
  46. data/lib/cancer_pathology_data_sharing_test_kit/version.rb +4 -0
  47. data/lib/cancer_pathology_data_sharing_test_kit.rb +3 -0
  48. metadata +124 -0
@@ -0,0 +1,239 @@
1
+ require_relative 'fhir_resource_navigation'
2
+
3
+ module CancerPathologyDataSharingTestKit
4
+ module MustSupportTest
5
+ extend Forwardable
6
+ include FHIRResourceNavigation
7
+
8
+ def_delegators 'self.class', :metadata
9
+
10
+ def all_scratch_resources
11
+ scratch_resources[:all]
12
+ end
13
+
14
+ def perform_must_support_test(resources)
15
+ skip_if resources.blank?, "No #{resource_type} resources were found"
16
+
17
+ missing_elements(resources)
18
+ missing_slices(resources)
19
+ missing_extensions(resources)
20
+
21
+ handle_must_support_choices if metadata.must_supports[:choices].present?
22
+
23
+ assert (missing_elements + missing_slices + missing_extensions).empty?, "Could not find #{missing_must_support_strings.join(', ')} " \
24
+ "in the #{resources.length} " \
25
+ "provided #{resource_type} resource(s)"
26
+ end
27
+
28
+ def handle_must_support_choices # rubocop:disable Metrics/CyclomaticComplexity
29
+ missing_elements.delete_if do |element|
30
+ choices = metadata.must_supports[:choices].find { |choice| choice[:paths]&.include?(element[:path]) }
31
+ is_any_choice_supported?(choices)
32
+ end
33
+
34
+ missing_extensions.delete_if do |extension|
35
+ choices = metadata.must_supports[:choices].find { |choice| choice[:extension_ids]&.include?(extension[:id]) }
36
+ is_any_choice_supported?(choices)
37
+ end
38
+
39
+ missing_slices.delete_if do |slice|
40
+ choices = metadata.must_supports[:choices].find { |choice| choice[:slice_names]&.include?(slice[:name]) }
41
+ is_any_choice_supported?(choices)
42
+ end
43
+ end
44
+
45
+ def is_any_choice_supported?(choices) # rubocop:disable Metrics/CyclomaticComplexity,Naming/PredicateName
46
+ choices.present? &&
47
+ (
48
+ choices[:paths]&.any? { |path| missing_elements.none? { |element| element[:path] == path } } ||
49
+ choices[:extension_ids]&.any? { |extension_id| missing_extensions.none? { |extension| extension[:id] == extension_id } } ||
50
+ choices[:slice_names]&.any? { |slice_name| missing_slices.none? { |slice| slice[:name] == slice_name } }
51
+ )
52
+ end
53
+
54
+ def missing_must_support_strings
55
+ missing_elements.map { |element_definition| missing_element_string(element_definition) } +
56
+ missing_slices.map { |slice_definition| slice_definition[:slice_id] } +
57
+ missing_extensions.map { |extension_definition| extension_definition[:id] }
58
+ end
59
+
60
+ def missing_element_string(element_definition)
61
+ if element_definition[:fixed_value].present?
62
+ "#{element_definition[:path]}:#{element_definition[:fixed_value]}"
63
+ else
64
+ element_definition[:path]
65
+ end
66
+ end
67
+
68
+ def exclude_uscdi_only_test?
69
+ config.options[:exclude_uscdi_only_test] == true
70
+ end
71
+
72
+ def must_support_extensions
73
+ if exclude_uscdi_only_test?
74
+ metadata.must_supports[:extensions].reject { |extension| extension[:uscdi_only] }
75
+ else
76
+ metadata.must_supports[:extensions]
77
+ end
78
+ end
79
+
80
+ def missing_extensions(resources = [])
81
+ @missing_extensions ||=
82
+ must_support_extensions.select do |extension_definition|
83
+ resources.none? do |resource|
84
+ path = extension_definition[:path]
85
+
86
+ if path == 'extension'
87
+ resource.extension.any? { |extension| extension.url == extension_definition[:url] }
88
+ else
89
+ extension = find_a_value_at(resource, path) do |el|
90
+ el.url == extension_definition[:url]
91
+ end
92
+
93
+ extension.present?
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ def must_support_elements
100
+ if exclude_uscdi_only_test?
101
+ metadata.must_supports[:elements].reject { |element| element[:uscdi_only] }
102
+ else
103
+ metadata.must_supports[:elements]
104
+ end
105
+ end
106
+
107
+ def missing_elements(resources = [])
108
+ @missing_elements ||= find_missing_elements(resources, must_support_elements)
109
+ @missing_elements
110
+ end
111
+
112
+ def find_missing_elements(resources, must_support_elements) # rubocop:disable Metrics/CyclomaticComplexity
113
+ must_support_elements.select do |element_definition|
114
+ resources.none? do |resource|
115
+ path = element_definition[:path]
116
+ ms_extension_urls = must_support_extensions.select { |ex| ex[:path] == "#{path}.extension" }
117
+ .map { |ex| ex[:url] }
118
+
119
+ value_found = find_a_value_at(resource, path) do |value|
120
+ if value.instance_of?(CancerPathologyDataSharingTestKit::PrimitiveType) && ms_extension_urls.present?
121
+ urls = value.extension&.map(&:url)
122
+ has_ms_extension = (urls & ms_extension_urls).present?
123
+ end
124
+
125
+ unless has_ms_extension
126
+ value = value.value if value.instance_of?(CancerPathologyDataSharingTestKit::PrimitiveType)
127
+ value_without_extensions =
128
+ value.respond_to?(:to_hash) ? value.to_hash.except('extension') : value
129
+ end
130
+
131
+ (has_ms_extension || value_without_extensions.present? || value_without_extensions == false) &&
132
+ (element_definition[:fixed_value].blank? || value == element_definition[:fixed_value])
133
+ end
134
+ # Note that false.present? => false, which is why we need to add this extra check
135
+ value_found.present? || value_found == false
136
+ end
137
+ end
138
+ end
139
+
140
+ def must_support_slices
141
+ if exclude_uscdi_only_test?
142
+ metadata.must_supports[:slices].reject { |slice| slice[:uscdi_only] }
143
+ else
144
+ metadata.must_supports[:slices]
145
+ end
146
+ end
147
+
148
+ def missing_slices(resources = [])
149
+ @missing_slices ||=
150
+ must_support_slices.select do |slice|
151
+ resources.none? do |resource|
152
+ path = slice[:path] # .delete_suffix('[x]')
153
+ find_slice(resource, path, slice[:discriminator]).present?
154
+ end
155
+ end
156
+ end
157
+
158
+ def find_slice(resource, path, discriminator) # rubocop:disable Metrics/CyclomaticComplexity
159
+ find_a_value_at(resource, path) do |element|
160
+ case discriminator[:type]
161
+ when 'patternCodeableConcept'
162
+ coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
163
+ find_a_value_at(element, coding_path) do |coding|
164
+ coding.code == discriminator[:code] && coding.system == discriminator[:system]
165
+ end
166
+ when 'patternCoding'
167
+ coding_path = discriminator[:path].present? ? discriminator[:path] : ''
168
+ find_a_value_at(element, coding_path) do |coding|
169
+ coding.code == discriminator[:code] && coding.system == discriminator[:system]
170
+ end
171
+ when 'patternIdentifier'
172
+ find_a_value_at(element, discriminator[:path]) { |identifier| identifier.system == discriminator[:system] }
173
+ when 'value'
174
+ values = discriminator[:values].map { |value| value.merge(path: value[:path].split('.')) }
175
+ find_slice_by_values(element, values)
176
+ when 'type'
177
+ case discriminator[:code]
178
+ when 'Date'
179
+ begin
180
+ Date.parse(element)
181
+ rescue ArgumentError
182
+ false
183
+ end
184
+ when 'DateTime'
185
+ begin
186
+ DateTime.parse(element)
187
+ rescue ArgumentError
188
+ false
189
+ end
190
+ when 'String'
191
+ element.is_a? String
192
+ else
193
+ if element.is_a? FHIR::Bundle::Entry
194
+ element.resource.resourceType == discriminator[:code]
195
+ else
196
+ element.is_a? FHIR.const_get(discriminator[:code])
197
+ end
198
+ end
199
+ when 'requiredBinding'
200
+ coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
201
+
202
+ find_a_value_at(element, coding_path) do |coding|
203
+ discriminator[:values].any? { |value| value[:system] == coding.system && value[:code] == coding.code }
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ def find_slice_by_values(element, value_definitions) # rubocop:disable Metrics/CyclomaticComplexity
210
+ path_prefixes = value_definitions.map { |value_definition| value_definition[:path].first }.uniq
211
+ Array.wrap(element).find do |el|
212
+ path_prefixes.all? do |path_prefix|
213
+ value_definitions_for_path =
214
+ value_definitions
215
+ .select { |value_definition| value_definition[:path].first == path_prefix }
216
+ .each { |value_definition| value_definition[:path].shift }
217
+
218
+ find_a_value_at(el, path_prefix) do |el_found|
219
+ child_element_value_definitions, current_element_value_definitions =
220
+ value_definitions_for_path.partition { |value_definition| value_definition[:path].present? }
221
+
222
+ current_element_values_match =
223
+ current_element_value_definitions
224
+ .all? { |value_definition| value_definition[:value] == el_found }
225
+
226
+ child_element_values_match =
227
+ if child_element_value_definitions.present?
228
+ find_slice_by_values(el_found, child_element_value_definitions)
229
+ else
230
+ true
231
+ end
232
+
233
+ current_element_values_match && child_element_values_match
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,5 @@
1
+ module CancerPathologyDataSharingTestKit
2
+ class PrimitiveType < FHIR::Element
3
+ attr_accessor :value
4
+ end
5
+ end
@@ -0,0 +1,22 @@
1
+ module CancerPathologyDataSharingTestKit
2
+ class DataAbsentReasonCodeSystem < Inferno::Test
3
+ id :cpds_data_absent_reason_code_system
4
+ title 'Server represents missing data with the DataAbsentReason CodeSystem'
5
+ description %(
6
+ For coded data elements with example, preferred, or extensible binding
7
+ strengths to ValueSets which do not include an appropriate "unknown"
8
+ code, servers SHALL use the "unknown" code from the DataAbsentReason
9
+ CodeSystem.
10
+ )
11
+ input :dar_code_found,
12
+ title: 'Data Absent Reason CodeSystem Found',
13
+ locked: true,
14
+ optional: true,
15
+ default: 'false'
16
+
17
+ run do
18
+ assert dar_code_found == 'true',
19
+ 'No resources using the DataAbsentReason CodeSystem have been found'
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ module CancerPathologyDataSharingTestKit
2
+ class DataAbsentReasonExtension < Inferno::Test
3
+ id :cpds_data_absent_reason_extension
4
+ title 'Server represents missing data with the DataAbsentReason Extension'
5
+ description %(
6
+ For non-coded data elements, servers SHALL use the DataAbsentReason
7
+ Extension to represent missing data in a required field
8
+ )
9
+ input :dar_extension_found,
10
+ title: 'Data Absent Reason Extension Found',
11
+ locked: true,
12
+ optional: true,
13
+ default: 'false'
14
+
15
+ run do
16
+ assert dar_extension_found == 'true',
17
+ 'No resources using the DataAbsentReason Extension have been found'
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ module CancerPathologyDataSharingTestKit
2
+ class SendingHideMissing < Inferno::Test
3
+ id :cpds_data_absent_sending_hide_missing
4
+ title 'Sender does not send data element(s) with missing information'
5
+ description %(
6
+ In situations where information on a particular data element is not present, the Sender SHALL NOT include the data element
7
+ in the resource instance if the cardinality is 0..n.
8
+ )
9
+
10
+ run do
11
+ identifier = SecureRandom.hex(32)
12
+ wait(
13
+ identifier:,
14
+ message: <<~MESSAGE
15
+ The system under test has demonstrated that it meets the following [requirement](https://hl7.org/fhir/us/cancer-reporting/STU1.0.1/specification.html#must-support-and-missing-data):
16
+
17
+ [Specifically](https://hl7.org/fhir/us/cancer-reporting/STU1.0.1/specification.html#must-support-and-missing-data)
18
+
19
+ > Sending Systems [of the [US Pathology Bundle](https://hl7.org/fhir/us/cancer-reporting/STU1.0.1/StructureDefinition-us-pathology-exchange-bundle.html)] In situations where information on a particular data element is not present, the Sender SHALL NOT include the data element in the resource instance if the cardinality is 0..n.
20
+
21
+ [Click here](#{resume_pass_url}?token=#{identifier}) if the system **meets** this requirement.
22
+
23
+ [Click here](#{resume_fail_url}?token=#{identifier}) if the system **does not meet** this requirement.
24
+ MESSAGE
25
+ )
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ require_relative 'data_absent/sending_hide_missing'
2
+ require_relative 'data_absent/data_absent_reason_extension'
3
+ require_relative 'data_absent/data_absent_reason_code_system'
4
+
5
+ module CancerPathologyDataSharingTestKit
6
+ class DataAbsentGroup < Inferno::TestGroup
7
+ id :cpds_data_absent_reason
8
+ title 'Missing Data Tests'
9
+ short_description 'Verify that the server is capable of representing missing data.'
10
+
11
+ description %(
12
+ The [CPDS Missing Data
13
+ Guidance](https://hl7.org/fhir/us/cancer-reporting/STU1.0.1/specification.html#must-support-and-missing-data)
14
+ gives instructions on how to represent various types of missing data.
15
+
16
+ In the previous resource tests, each resource returned from the server was
17
+ checked for the presence of missing data. These tests will pass if the
18
+ specified method of representing missing data was observed in the earlier
19
+ tests.
20
+ )
21
+ run_as_group
22
+
23
+ test from: :cpds_data_absent_sending_hide_missing
24
+ test from: :cpds_data_absent_reason_extension
25
+ test from: :cpds_data_absent_reason_code_system
26
+ end
27
+ end
@@ -0,0 +1,53 @@
1
+ require_relative '../../../must_support_test'
2
+ require_relative '../../../group_metadata'
3
+
4
+ module CancerPathologyDataSharingTestKit
5
+ module V101
6
+ class EncounterMustSupportTest < Inferno::Test
7
+ include CancerPathologyDataSharingTestKit::MustSupportTest
8
+
9
+ title 'All must support elements are provided in the US Core Encounter resources'
10
+ description %(
11
+ Report generators SHALL be capable of populating all must support data elements
12
+ defined in the [US Pathology ExchangeBundle profile](https://hl7.org/fhir/us/cancer-reporting/STU1.0.1/StructureDefinition-us-pathology-exchange-bundle.html)
13
+ and profiles it references. This test will look through the Encounter resources
14
+ found in the provided report Bundles for the following must support elements:
15
+
16
+ * Encounter.class
17
+ * Encounter.hospitalization
18
+ * Encounter.hospitalization.dischargeDisposition
19
+ * Encounter.identifier
20
+ * Encounter.identifier.system
21
+ * Encounter.identifier.value
22
+ * Encounter.location
23
+ * Encounter.location.location
24
+ * Encounter.participant
25
+ * Encounter.participant.individual
26
+ * Encounter.participant.period
27
+ * Encounter.participant.type
28
+ * Encounter.period
29
+ * Encounter.reasonCode
30
+ * Encounter.status
31
+ * Encounter.subject
32
+ * Encounter.type
33
+ )
34
+
35
+ id :cpds_v101_us_core_v311_encounter_must_support_test
36
+
37
+ def resource_type
38
+ 'Encounter'
39
+ end
40
+
41
+ def self.metadata
42
+ @metadata ||= GroupMetadata.new(YAML.load_file(File.join(__dir__, 'metadata.yml'), aliases: true))
43
+ end
44
+
45
+ run do
46
+ all_resources = scratch[:cpds_resources]&.values&.map do |bundle_resources|
47
+ bundle_resources['Encounter'] || []
48
+ end
49
+ perform_must_support_test(all_resources&.flatten)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,48 @@
1
+ require_relative '../../../bundle_parse'
2
+ require_relative '../../../validation_test'
3
+
4
+ module CancerPathologyDataSharingTestKit
5
+ class EncounterValidationTest < Inferno::Test
6
+ include CancerPathologyDataSharingTestKit::ValidationTest
7
+ include CancerPathologyDataSharingTestKit::BundleParse
8
+
9
+ title 'Encounter resource in each bundle conforms to the US Core Encounter profile'
10
+ description %(
11
+ This test verifies that any Encounter resource returned from each bundle conforms to
12
+ the [US Core Encounter Profile](http://hl7.org/fhir/us/core/StructureDefinition/us-core-encounter).
13
+
14
+ It verifies the presence of mandatory elements and that elements with
15
+ required bindings contain appropriate values. CodeableConcept element
16
+ bindings will fail if none of their codings have a code/system belonging
17
+ to the bound ValueSet. Quantity, Coding, and code element bindings will
18
+ fail if their code/system are not found in the valueset.
19
+ )
20
+
21
+ id :cpds_encounter_validation_test
22
+
23
+ def resource_type
24
+ 'Encounter'
25
+ end
26
+
27
+ output :dar_code_found, :dar_extension_found
28
+
29
+ run do
30
+ invalid_bundles = []
31
+ total_resources = 0
32
+ scratch[:cpds_resources].each do |bundle_id, bundle_resources|
33
+ resources = bundle_resources['Encounter'] || []
34
+ total_resources += resources.length
35
+
36
+ profile_url = PE_BUNDLE_SLICE_RESOURCES['Encounter']
37
+ invalid_bundles << bundle_id if perform_strict_validation_test('Encounter', bundle_id, resources, profile_url,
38
+ '5.0.1', skip_if_empty: false,
39
+ restriction: 'no_more_than_one')
40
+ end
41
+
42
+ skip_if total_resources.zero?,
43
+ "No #{resource_type} resources found in any of the given bundles"
44
+
45
+ check_for_errors(invalid_bundles)
46
+ end
47
+ end
48
+ end