us_core_test_kit 0.11.0 → 0.11.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5c1144b0951c2ba438152f25553f8a4eff37301e232b17dc149030bbe5b23ff
4
- data.tar.gz: 0f790cf343a074d72240ffe6f83ebc8fdab641b7a08601b2d51f4d601d8f7889
3
+ metadata.gz: 7a024a9b164018f0dab07cef2db848b746cd08daf78b76555ddc76697e10be68
4
+ data.tar.gz: cffeb336d69befe4f7af954f0d1967c424132413a8a66cce740ebcba390effcb
5
5
  SHA512:
6
- metadata.gz: 7f4fbe3dd3524a0983715bdff2a6f6a03ee370392e4bd50346d95eaaca46144a566b6497c818265fb602a577a9329bfe2cb1ee63723eb68a4510e0990a407b27
7
- data.tar.gz: f52cd5e1baa310753831c0825f226e89c7b4fddf74e4fdaff0f797f17c6a5c30f3de24e8703e4a1ea6bf22f65144e12cd5f54bcb9732d8616e4685badf9e3d36
6
+ metadata.gz: c4dc45be569bb789b1e88db5e5ee7b84f90438363dcaeb690a64359a84d2e77bd9e6981482d95ef13dfef7fa451f6c0e6b827e8a6c66511a16e728be9d79a70e
7
+ data.tar.gz: 6fdcff078c88cf5f340b0e06e0901bb9d7797b0f79c7826cf37f0b650b676d8e6c39e0ba6c12808e501247d6018d8648ea16334858f862b0532ee1acd25f18cd
@@ -10419,10 +10419,6 @@
10419
10419
  :path: extension
10420
10420
  :url: http://hl7.org/fhir/us/core/StructureDefinition/us-core-sex
10421
10421
  :uscdi_only: true
10422
- - :id: Patient.extension:genderIdentity
10423
- :path: extension
10424
- :url: http://hl7.org/fhir/us/core/StructureDefinition/us-core-genderIdentity
10425
- :uscdi_only: true
10426
10422
  :slices: []
10427
10423
  :elements:
10428
10424
  - :path: identifier
@@ -188,10 +188,6 @@
188
188
  :path: extension
189
189
  :url: http://hl7.org/fhir/us/core/StructureDefinition/us-core-sex
190
190
  :uscdi_only: true
191
- - :id: Patient.extension:genderIdentity
192
- :path: extension
193
- :url: http://hl7.org/fhir/us/core/StructureDefinition/us-core-genderIdentity
194
- :uscdi_only: true
195
191
  :slices: []
196
192
  :elements:
197
193
  - :path: identifier
@@ -33,7 +33,6 @@ module USCoreTestKit
33
33
  * Patient.communication.language
34
34
  * Patient.deceasedDateTime
35
35
  * Patient.extension:ethnicity
36
- * Patient.extension:genderIdentity
37
36
  * Patient.extension:race
38
37
  * Patient.extension:sex
39
38
  * Patient.extension:tribalAffiliation
@@ -12229,10 +12229,6 @@
12229
12229
  :path: extension
12230
12230
  :url: http://hl7.org/fhir/us/core/StructureDefinition/us-core-sex
12231
12231
  :uscdi_only: true
12232
- - :id: Patient.extension:genderIdentity
12233
- :path: extension
12234
- :url: http://hl7.org/fhir/us/core/StructureDefinition/us-core-genderIdentity
12235
- :uscdi_only: true
12236
12232
  :slices: []
12237
12233
  :elements:
12238
12234
  - :path: identifier
@@ -188,10 +188,6 @@
188
188
  :path: extension
189
189
  :url: http://hl7.org/fhir/us/core/StructureDefinition/us-core-sex
190
190
  :uscdi_only: true
191
- - :id: Patient.extension:genderIdentity
192
- :path: extension
193
- :url: http://hl7.org/fhir/us/core/StructureDefinition/us-core-genderIdentity
194
- :uscdi_only: true
195
191
  :slices: []
196
192
  :elements:
197
193
  - :path: identifier
@@ -33,7 +33,6 @@ module USCoreTestKit
33
33
  * Patient.communication.language
34
34
  * Patient.deceasedDateTime
35
35
  * Patient.extension:ethnicity
36
- * Patient.extension:genderIdentity
37
36
  * Patient.extension:race
38
37
  * Patient.extension:sex
39
38
  * Patient.extension:tribalAffiliation
@@ -95,8 +95,7 @@ module USCoreTestKit
95
95
 
96
96
 
97
97
  def optional?
98
- SpecialCases::OPTIONAL_RESOURCES.include?(resource_type) ||
99
- SpecialCases::OPTIONAL_PROFILES.include?(profile_url)
98
+ SpecialCases::OPTIONAL_RESOURCES.include?(resource_type) || group_metadata.optional_profile?
100
99
  end
101
100
 
102
101
  def generate
@@ -63,6 +63,10 @@ module USCoreTestKit
63
63
  SpecialCases::NON_USCDI_RESOURCES.key?(resource) && SpecialCases::NON_USCDI_RESOURCES[resource].include?(reformatted_version)
64
64
  end
65
65
 
66
+ def optional_profile?
67
+ SpecialCases::OPTIONAL_PROFILES.key?(profile_url) && SpecialCases::OPTIONAL_PROFILES[profile_url].include?(reformatted_version)
68
+ end
69
+
66
70
  def searchable_delayed_resource?
67
71
  SpecialCases::SEARCHABLE_DELAYED_RESOURCES.key?(resource) && SpecialCases::SEARCHABLE_DELAYED_RESOURCES[resource].include?(reformatted_version)
68
72
  end
@@ -30,6 +30,7 @@ module USCoreTestKit
30
30
  update_smoking_status_effective
31
31
  remove_practitioner_address
32
32
  remove_diagnosticreport_media
33
+ remove_patient_gender_identity
33
34
  end
34
35
 
35
36
  def add_must_support_choices
@@ -106,6 +107,13 @@ module USCoreTestKit
106
107
  return unless profile.id == 'us-core-diagnosticreport-note'
107
108
  must_supports[:elements].delete_if { |element| element[:path].start_with?('media') }
108
109
  end
110
+
111
+ # genderIdentify is removed as directed by ASTP/ONC enforcement discretion issued on March 21, 2025:
112
+ # https://www.healthit.gov/topic/certification-ehrs/enforcement-discretion
113
+ def remove_patient_gender_identity
114
+ return unless profile.type == 'Patient'
115
+ must_supports[:extensions].delete_if { |extension| extension[:id] == 'Patient.extension:genderIdentity' }
116
+ end
109
117
  end
110
118
  end
111
119
  end
@@ -20,6 +20,7 @@ module USCoreTestKit
20
20
  us_core_6_extractor.add_patient_uscdi_elements
21
21
  add_must_support_choices
22
22
  us_core_6_extractor.remove_practitioner_address
23
+ us_core_6_extractor.remove_patient_gender_identity
23
24
  end
24
25
 
25
26
  def add_must_support_choices
@@ -17,9 +17,9 @@ module USCoreTestKit
17
17
  'QuestionnaireResponse'
18
18
  ].freeze
19
19
 
20
- OPTIONAL_PROFILES = [
21
- 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-simple-observation'
22
- ].freeze
20
+ OPTIONAL_PROFILES = {
21
+ 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-simple-observation' => ['v610', 'v700']
22
+ }.freeze
23
23
 
24
24
  NON_USCDI_RESOURCES = {
25
25
  'Encounter' => ['v311', 'v400'],
@@ -22,7 +22,7 @@ module USCoreTestKit
22
22
  "No #{resource_type} reads found"
23
23
  previous_resources_for_reads = previous_request_resources.values.flatten
24
24
 
25
- resource_specific_granular_scope_search_params.each do |scope|
25
+ resource_specific_granular_scope_search_params.each do |scope|
26
26
  current_scope = granular_scopes.find {|granular| granular.include?(scope[:name]) && granular.include?(scope[:value])}
27
27
 
28
28
  resource_matching_scope = previous_resources_for_reads.find do |prev_resource|
@@ -37,19 +37,26 @@ module USCoreTestKit
37
37
  end
38
38
 
39
39
  nonmatching_resource = previous_resources_for_reads.find do |prev_resource|
40
- resource_specific_granular_scope_search_params.none? do |scope|
40
+ resource_specific_granular_scope_search_params.none? do |scope|
41
41
  resource_matches_param?(prev_resource, scope[:name], scope[:value])
42
42
  end
43
43
  end
44
44
  if nonmatching_resource
45
45
  fhir_read resource_type, nonmatching_resource.id
46
46
  assert (response && response[:status]) != 200, "Server incorrectly responded with a successful status, read should fail due to scopes."
47
- assert (resource && resource.resourceType != resource_type), "Server incorrectly returned a #{resource_type}, read should fail due to scopes"
47
+
48
+ begin
49
+ res = resource
50
+ rescue NoMethodError
51
+ res = nil
52
+ end
53
+
54
+ assert (!res || res.resourceType == 'OperationOutcome'), "Read shall fail due to scopes. Server shall return 4xx status code with an optional OperationOutcome payload."
48
55
  else
49
56
  info "Unable to find a resource that does not match scopes."
50
57
  end
51
58
  end
52
-
59
+
53
60
  def load_previous_requests
54
61
  params = metadata.searches.first[:names]
55
62
  search_params_as_hash = params.each_with_object({}) do |name, hash|
@@ -14,221 +14,7 @@ module USCoreTestKit
14
14
  def perform_must_support_test(resources)
15
15
  skip_if resources.blank?, "No #{resource_type} resources were found"
16
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
- if (missing_elements + missing_slices + missing_extensions).length.zero?
24
- pass
25
- end
26
- skip "Could not find #{missing_must_support_strings.join(', ')} in the #{resources.length} " \
27
- "provided #{resource_type} resource(s)"
28
- end
29
-
30
- def handle_must_support_choices
31
- missing_elements.delete_if do |element|
32
- choices = metadata.must_supports[:choices].find { |choice| choice[:paths]&.include?(element[:path]) }
33
- is_any_choice_supported?(choices)
34
- end
35
-
36
- missing_extensions.delete_if do |extension|
37
- choices = metadata.must_supports[:choices].find { |choice| choice[:extension_ids]&.include?(extension[:id]) }
38
- is_any_choice_supported?(choices)
39
- end
40
-
41
- missing_slices.delete_if do |slice|
42
- choices = metadata.must_supports[:choices].find { |choice| choice[:slice_names]&.include?(slice[:name]) }
43
- is_any_choice_supported?(choices)
44
- end
45
- end
46
-
47
- def is_any_choice_supported? (choices)
48
- choices.present? &&
49
- (
50
- choices[:paths]&.any? { |path| missing_elements.none? { |element| element[:path] == path } } ||
51
- choices[:extension_ids]&.any? { |extension_id| missing_extensions.none? { |extension| extension[:id] == extension_id} } ||
52
- choices[:slice_names]&.any? { |slice_name| missing_slices.none? { |slice| slice[:name] == slice_name} }
53
- )
54
- end
55
-
56
- def missing_must_support_strings
57
- missing_elements.map { |element_definition| missing_element_string(element_definition) } +
58
- missing_slices.map { |slice_definition| slice_definition[:slice_id] } +
59
- missing_extensions.map { |extension_definition| extension_definition[:id] }
60
- end
61
-
62
- def missing_element_string(element_definition)
63
- if element_definition[:fixed_value].present?
64
- "#{element_definition[:path]}:#{element_definition[:fixed_value]}"
65
- else
66
- element_definition[:path]
67
- end
68
- end
69
-
70
- def exclude_uscdi_only_test?
71
- config.options[:exclude_uscdi_only_test] == true
72
- end
73
-
74
- def must_support_extensions
75
- if exclude_uscdi_only_test?
76
- metadata.must_supports[:extensions].reject{ |extension| extension[:uscdi_only] }
77
- else
78
- metadata.must_supports[:extensions]
79
- end
80
- end
81
-
82
- def missing_extensions(resources = [])
83
- @missing_extensions ||=
84
- must_support_extensions.select do |extension_definition|
85
- resources.none? do |resource|
86
- path = extension_definition[:path]
87
-
88
- if path == 'extension'
89
- resource.extension.any? { |extension| extension.url == extension_definition[:url] }
90
- else
91
- extension = find_a_value_at(resource, path) do |el|
92
- el.url == extension_definition[:url]
93
- end
94
-
95
- extension.present?
96
- end
97
- end
98
- end
99
- end
100
-
101
- def must_support_elements
102
- if exclude_uscdi_only_test?
103
- metadata.must_supports[:elements].reject{ |element| element[:uscdi_only] }
104
- else
105
- metadata.must_supports[:elements]
106
- end
107
- end
108
-
109
- def missing_elements(resources = [])
110
- @missing_elements ||= find_missing_elements(resources, must_support_elements)
111
- @missing_elements
112
- end
113
-
114
- def find_missing_elements(resources, must_support_elements)
115
- must_support_elements.select do |element_definition|
116
- resources.none? do |resource|
117
- path = element_definition[:path]
118
- ms_extension_urls = must_support_extensions.select { |ex| ex[:path] == "#{path}.extension" }
119
- .map { |ex| ex[:url] }
120
-
121
- value_found = find_a_value_at(resource, path) do |value|
122
- if value.instance_of?(USCoreTestKit::PrimitiveType) && ms_extension_urls.present?
123
- urls = value.extension&.map(&:url)
124
- has_ms_extension = (urls & ms_extension_urls).present?
125
- end
126
-
127
- unless has_ms_extension
128
- value = value.value if value.instance_of?(USCoreTestKit::PrimitiveType)
129
- value_without_extensions =
130
- value.respond_to?(:to_hash) ? value.to_hash.reject { |key, _| key == 'extension' } : value
131
- end
132
-
133
- (has_ms_extension || value_without_extensions.present? || value_without_extensions == false) &&
134
- (element_definition[:fixed_value].blank? || value == element_definition[:fixed_value])
135
- end
136
- # Note that false.present? => false, which is why we need to add this extra check
137
- value_found.present? || value_found == false
138
- end
139
- end
140
- end
141
-
142
- def must_support_slices
143
- if exclude_uscdi_only_test?
144
- metadata.must_supports[:slices].reject{ |slice| slice[:uscdi_only] }
145
- else
146
- metadata.must_supports[:slices]
147
- end
148
- end
149
-
150
- def missing_slices(resources = [])
151
- @missing_slices ||=
152
- must_support_slices.select do |slice|
153
- resources.none? do |resource|
154
- path = slice[:path] # .delete_suffix('[x]')
155
- find_slice(resource, path, slice[:discriminator]).present?
156
- end
157
- end
158
- end
159
-
160
- def find_slice(resource, path, discriminator)
161
- find_a_value_at(resource, path) do |element|
162
- case discriminator[:type]
163
- when 'patternCodeableConcept'
164
- coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
165
- find_a_value_at(element, coding_path) do |coding|
166
- coding.code == discriminator[:code] && coding.system == discriminator[:system]
167
- end
168
- when 'patternCoding'
169
- coding_path = discriminator[:path].present? ? discriminator[:path] : ''
170
- find_a_value_at(element, coding_path) do |coding|
171
- coding.code == discriminator[:code] && coding.system == discriminator[:system]
172
- end
173
- when 'patternIdentifier'
174
- find_a_value_at(element, discriminator[:path]) { |identifier| identifier.system == discriminator[:system] }
175
- when 'value'
176
- values = discriminator[:values].map { |value| value.merge(path: value[:path].split('.')) }
177
- find_slice_by_values(element, values)
178
- when 'type'
179
- case discriminator[:code]
180
- when 'Date'
181
- begin
182
- Date.parse(element)
183
- rescue ArgumentError
184
- false
185
- end
186
- when 'DateTime'
187
- begin
188
- DateTime.parse(element)
189
- rescue ArgumentError
190
- false
191
- end
192
- when 'String'
193
- element.is_a? String
194
- else
195
- element.is_a? FHIR.const_get(discriminator[:code])
196
- end
197
- when 'requiredBinding'
198
- coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
199
-
200
- find_a_value_at(element, coding_path) do |coding|
201
- discriminator[:values].any? { |value| value[:system] == coding.system && value[:code] == coding.code }
202
- end
203
- end
204
- end
205
- end
206
-
207
- def find_slice_by_values(element, value_definitions)
208
- path_prefixes = value_definitions.map { |value_definition| value_definition[:path].first }.uniq
209
- Array.wrap(element).find do |el|
210
- path_prefixes.all? do |path_prefix|
211
- value_definitions_for_path =
212
- value_definitions
213
- .select { |value_definition| value_definition[:path].first == path_prefix }
214
- .each { |value_definition| value_definition[:path].shift }
215
-
216
- find_a_value_at(el, path_prefix) do |el_found|
217
- child_element_value_definitions, current_element_value_definitions =
218
- value_definitions_for_path.partition { |value_definition| value_definition[:path].present? }
219
-
220
- current_element_values_match =
221
- current_element_value_definitions
222
- .all? { |value_definition| value_definition[:value] == el_found }
223
-
224
- child_element_values_match =
225
- child_element_value_definitions.present? ?
226
- find_slice_by_values(el_found, child_element_value_definitions) : true
227
-
228
- current_element_values_match && child_element_values_match
229
- end
230
- end
231
- end
17
+ skip { assert_must_support_elements_present(resources, nil, metadata:) }
232
18
  end
233
19
  end
234
20
  end
@@ -25,7 +25,7 @@ module USCoreTestKit
25
25
 
26
26
  def verify_practitioner_address
27
27
  references = scratch.dig(:references, 'Practitioner')
28
- assert references.any?, 'No Pracitioner references found.'
28
+ assert references.any?, 'No Practitioner references found.'
29
29
 
30
30
  @missing_elements = MUST_SUPPORT_ELEMENTS
31
31
  practitioners = []
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module USCoreTestKit
4
- VERSION = '0.11.0'
5
- LAST_UPDATED = '2025-03-18'
4
+ VERSION = '0.11.2'
5
+ LAST_UPDATED = '2025-05-05'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: us_core_test_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.11.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen MacVicar
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-19 00:00:00.000000000 Z
11
+ date: 2025-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inferno_core
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.6.4
19
+ version: 0.6.8
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.6.4
26
+ version: 0.6.8
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: smart_app_launch_test_kit
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 0.6.0
33
+ version: 0.6.1
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 0.6.0
40
+ version: 0.6.1
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: tls_test_kit
43
43
  requirement: !ruby/object:Gem::Requirement