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.
- checksums.yaml +4 -4
- data/NOTICE.md +12 -0
- data/config/presets/{localhost_preset.json → inferno_reference_server_preset.json.erb} +2 -2
- data/lib/us_quality_core_test_kit/client/generator/example_client_generator.rb +0 -1
- data/lib/us_quality_core_test_kit/client/generator/tags_generator.rb +0 -1
- data/lib/us_quality_core_test_kit/client/server_proxy.rb +2 -6
- data/lib/us_quality_core_test_kit/date_search_validation.rb +122 -0
- data/lib/us_quality_core_test_kit/fhir_resource_navigation.rb +9 -0
- data/lib/us_quality_core_test_kit/generated/v0.5.0/devicenotrequested/devicenotrequested_validation_test.rb +3 -0
- data/lib/us_quality_core_test_kit/generated/v0.5.0/devicerequest/devicerequest_validation_test.rb +3 -0
- data/lib/us_quality_core_test_kit/generated/v0.5.0/patient/patient_validation_test.rb +4 -0
- data/lib/us_quality_core_test_kit/generated/v0.5.0/us_quality_core_test_suite.rb +1 -2
- data/lib/us_quality_core_test_kit/generator/group_generator.rb +69 -3
- data/lib/us_quality_core_test_kit/generator/group_metadata.rb +100 -4
- data/lib/us_quality_core_test_kit/generator/group_metadata_extractor.rb +184 -8
- data/lib/us_quality_core_test_kit/generator/ig_loader.rb +47 -3
- data/lib/us_quality_core_test_kit/generator/ig_metadata.rb +51 -2
- data/lib/us_quality_core_test_kit/generator/ig_metadata_extractor.rb +25 -6
- data/lib/us_quality_core_test_kit/generator/ig_resources.rb +27 -3
- data/lib/us_quality_core_test_kit/generator/must_support_metadata_extractor.rb +366 -9
- data/lib/us_quality_core_test_kit/generator/must_support_test_generator.rb +54 -3
- data/lib/us_quality_core_test_kit/generator/provenance_revinclude_search_test_generator.rb +149 -3
- data/lib/us_quality_core_test_kit/generator/read_test_generator.rb +54 -3
- data/lib/us_quality_core_test_kit/generator/reference_resolution_test_generator.rb +57 -3
- data/lib/us_quality_core_test_kit/generator/search_definition_metadata_extractor.rb +181 -5
- data/lib/us_quality_core_test_kit/generator/search_metadata_extractor.rb +61 -3
- data/lib/us_quality_core_test_kit/generator/search_test_generator.rb +186 -3
- data/lib/us_quality_core_test_kit/generator/suite_generator.rb +35 -8
- data/lib/us_quality_core_test_kit/generator/terminology_binding_metadata_extractor.rb +107 -3
- data/lib/us_quality_core_test_kit/generator/validation_test_generator.rb +83 -4
- data/lib/us_quality_core_test_kit/generator/value_extractor.rb +147 -3
- data/lib/us_quality_core_test_kit/must_support_test.rb +15 -3
- data/lib/us_quality_core_test_kit/primitive_type.rb +7 -0
- data/lib/us_quality_core_test_kit/read_test.rb +99 -4
- data/lib/us_quality_core_test_kit/reference_resolution_test.rb +164 -3
- data/lib/us_quality_core_test_kit/resource_search_param_checker.rb +138 -0
- data/lib/us_quality_core_test_kit/search_test.rb +681 -46
- data/lib/us_quality_core_test_kit/search_test_properties.rb +53 -3
- data/lib/us_quality_core_test_kit/validation_test.rb +51 -4
- data/lib/us_quality_core_test_kit/version.rb +2 -2
- data/lib/us_quality_core_test_kit/well_known_code_systems.rb +23 -0
- metadata +8 -17
|
@@ -1,10 +1,171 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative 'fhir_resource_navigation'
|
|
4
4
|
|
|
5
5
|
module USQualityCoreTestKit
|
|
6
6
|
module ReferenceResolutionTest
|
|
7
|
-
include
|
|
8
|
-
extend
|
|
7
|
+
include Inferno::DSL::FHIRResourceNavigation
|
|
8
|
+
extend Forwardable
|
|
9
|
+
|
|
10
|
+
def_delegators 'self.class', :metadata
|
|
11
|
+
|
|
12
|
+
def perform_reference_resolution_test(resources)
|
|
13
|
+
skip_if resources.blank?, no_resources_skip_message
|
|
14
|
+
|
|
15
|
+
pass if unresolved_references(resources).empty?
|
|
16
|
+
|
|
17
|
+
skip "Could not resolve and validate any Must Support references for #{unresolved_references_strings.join(', ')}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def unresolved_references_strings
|
|
21
|
+
unresolved_reference_hash =
|
|
22
|
+
unresolved_references.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |missing, hash|
|
|
23
|
+
hash[missing[:path]] << missing[:target_profile]
|
|
24
|
+
end
|
|
25
|
+
unresolved_reference_hash.map { |path, profiles| "#{path} element: Reference#{"(#{profiles.join('|')})" unless profiles.first.empty?}" }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def record_resolved_reference(reference, target_profile)
|
|
29
|
+
saved_reference = resolved_references.find { |item| item[:reference] == reference.reference }
|
|
30
|
+
|
|
31
|
+
if saved_reference.present?
|
|
32
|
+
saved_reference[:profiles] << target_profile if target_profile.present? && !saved_reference[:profiles].include?(target_profile)
|
|
33
|
+
else
|
|
34
|
+
saved_reference = {
|
|
35
|
+
reference: reference.reference,
|
|
36
|
+
profiles: []
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
saved_reference[:profiles] << target_profile if target_profile.present?
|
|
40
|
+
resolved_references.add(saved_reference)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def reference_resolved?(reference, target_profile)
|
|
45
|
+
resolved_references.any? do |item|
|
|
46
|
+
item[:reference] == reference.reference &&
|
|
47
|
+
(
|
|
48
|
+
target_profile.blank? || item[:profiles].include?(target_profile)
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def resolved_references
|
|
54
|
+
scratch[:resolved_references] ||= Set.new
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def no_resources_skip_message
|
|
58
|
+
"No #{resource_type} resources appear to be available. " \
|
|
59
|
+
'Please use patients with more information.'
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def must_support_references
|
|
63
|
+
metadata.must_supports[:elements].select { |element_definition| element_definition[:types]&.include?('Reference') }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def must_support_references_with_target_profile
|
|
67
|
+
# mapping array of target_profiles to array of {path, target_profile} pair
|
|
68
|
+
must_support_references.map do |element_definition|
|
|
69
|
+
(element_definition[:target_profiles] || ['']).map do |target_profile|
|
|
70
|
+
{
|
|
71
|
+
path: element_definition[:path],
|
|
72
|
+
target_profile: target_profile
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
end.flatten
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def unresolved_references(resources = [])
|
|
79
|
+
@unresolved_references ||=
|
|
80
|
+
must_support_references_with_target_profile.select do |reference_path_profile_pair|
|
|
81
|
+
path = reference_path_profile_pair[:path]
|
|
82
|
+
target_profile = reference_path_profile_pair[:target_profile]
|
|
83
|
+
|
|
84
|
+
found_one_reference = false
|
|
85
|
+
|
|
86
|
+
resolve_one_reference = resources.any? do |resource|
|
|
87
|
+
value_found = resolve_path(resource, path).select { |value| value.is_a?(FHIR::Reference) }
|
|
88
|
+
next if value_found.empty?
|
|
89
|
+
|
|
90
|
+
found_one_reference = true
|
|
91
|
+
|
|
92
|
+
value_found.any? do |reference|
|
|
93
|
+
validate_reference_resolution(resource, reference, target_profile)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
found_one_reference && !resolve_one_reference
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if metadata.must_supports[:choices].present?
|
|
101
|
+
@unresolved_references.delete_if do |reference|
|
|
102
|
+
choice_profiles = metadata.must_supports[:choices].find { |choice| choice[:target_profiles]&.include?(reference[:target_profile]) }
|
|
103
|
+
|
|
104
|
+
choice_profiles.present? &&
|
|
105
|
+
choice_profiles[:target_profiles]&.any? { |profile| @unresolved_references.none? { |element| element[:target_profile] == profile } }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
@unresolved_references
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def validate_reference_resolution(resource, reference, target_profile)
|
|
113
|
+
return true if reference_resolved?(reference, target_profile)
|
|
114
|
+
|
|
115
|
+
if reference.contained?
|
|
116
|
+
# if reference_id is blank it is referring to itself, so we know it exists
|
|
117
|
+
return true if reference.reference_id.blank?
|
|
118
|
+
|
|
119
|
+
return resource.contained.any? do |contained_resource|
|
|
120
|
+
contained_resource&.id == reference.reference_id &&
|
|
121
|
+
resource_is_valid_with_target_profile?(contained_resource, target_profile)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
reference_type = reference.resource_type
|
|
126
|
+
reference_id = reference.reference_id
|
|
127
|
+
|
|
128
|
+
resolved_resource = resolve_reference(reference)
|
|
129
|
+
|
|
130
|
+
return false if resolved_resource.nil?
|
|
131
|
+
|
|
132
|
+
return false unless resolved_resource.resourceType == reference_type && resolved_resource.id == reference_id
|
|
133
|
+
|
|
134
|
+
return false unless resource_is_valid_with_target_profile?(resolved_resource, target_profile)
|
|
135
|
+
|
|
136
|
+
record_resolved_reference(reference, target_profile)
|
|
137
|
+
true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def resolve_reference(reference)
|
|
141
|
+
reference_type = reference.resource_type
|
|
142
|
+
reference_id = reference.reference_id
|
|
143
|
+
|
|
144
|
+
begin
|
|
145
|
+
if reference.relative?
|
|
146
|
+
begin
|
|
147
|
+
reference.resource_class
|
|
148
|
+
rescue NameError
|
|
149
|
+
return nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
fhir_read(reference_type, reference_id)&.resource
|
|
153
|
+
elsif reference.base_uri.chomp('/') == fhir_client.instance_variable_get(:@base_service_url).chomp('/')
|
|
154
|
+
fhir_read(reference_type, reference_id)&.resource
|
|
155
|
+
else
|
|
156
|
+
get(reference.reference)&.resource
|
|
157
|
+
end
|
|
158
|
+
rescue StandardError => e
|
|
159
|
+
Inferno::Application['logger'].error("Unable to resolve reference #{reference.reference}")
|
|
160
|
+
Inferno::Application['logger'].error(e.full_message)
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def resource_is_valid_with_target_profile?(resource, target_profile)
|
|
166
|
+
return true if target_profile.blank?
|
|
167
|
+
|
|
168
|
+
resource_is_valid?(resource:, profile_url: target_profile, add_messages_to_runnable: false)
|
|
169
|
+
end
|
|
9
170
|
end
|
|
10
171
|
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'fhir_resource_navigation'
|
|
4
|
+
|
|
5
|
+
module USQualityCoreTestKit
|
|
6
|
+
module ResourceSearchParamChecker
|
|
7
|
+
include Inferno::DSL::FHIRResourceNavigation
|
|
8
|
+
|
|
9
|
+
def search_param_paths(name)
|
|
10
|
+
paths = metadata.search_definitions[name.to_sym][:paths]
|
|
11
|
+
paths[0] = 'local_class' if paths.first == 'class'
|
|
12
|
+
|
|
13
|
+
paths
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def element_has_valid_value?(element, include_system)
|
|
17
|
+
case element
|
|
18
|
+
when FHIR::Reference
|
|
19
|
+
element.reference.present?
|
|
20
|
+
when FHIR::CodeableConcept
|
|
21
|
+
if include_system
|
|
22
|
+
coding =
|
|
23
|
+
find_a_value_at(element, 'coding') { |a_coding| a_coding.code.present? && a_coding.system.present? }
|
|
24
|
+
coding.present?
|
|
25
|
+
else
|
|
26
|
+
find_a_value_at(element, 'coding.code').present?
|
|
27
|
+
end
|
|
28
|
+
when FHIR::Identifier
|
|
29
|
+
include_system ? element.value.present? && element.system.present? : element.value.present?
|
|
30
|
+
when FHIR::Coding
|
|
31
|
+
include_system ? element.code.present? && element.system.present? : element.code.present?
|
|
32
|
+
when FHIR::HumanName
|
|
33
|
+
(element.family || element.given&.first || element.text).present?
|
|
34
|
+
when FHIR::Address
|
|
35
|
+
(element.text || element.city || element.state || element.postalCode || element.country).present?
|
|
36
|
+
when FHIR::Element
|
|
37
|
+
false
|
|
38
|
+
else
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def resource_matches_param?(resource, search_param_name, escaped_search_value, values_found = [])
|
|
44
|
+
search_value = unescape_search_value(escaped_search_value)
|
|
45
|
+
paths = search_param_paths(search_param_name)
|
|
46
|
+
|
|
47
|
+
match_found = false
|
|
48
|
+
|
|
49
|
+
paths.each do |path|
|
|
50
|
+
type = metadata.search_definitions[search_param_name.to_sym][:type]
|
|
51
|
+
values_found =
|
|
52
|
+
resolve_path(resource, path)
|
|
53
|
+
.map do |value|
|
|
54
|
+
if value.is_a? FHIR::Reference
|
|
55
|
+
value.reference
|
|
56
|
+
else
|
|
57
|
+
value
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
match_found =
|
|
62
|
+
case type
|
|
63
|
+
when 'Period', 'date', 'instant', 'dateTime'
|
|
64
|
+
values_found.any? { |date| validate_date_search(search_value, date) }
|
|
65
|
+
when 'HumanName'
|
|
66
|
+
# When a string search parameter refers to the types HumanName and Address,
|
|
67
|
+
# the search covers the elements of type string, and does not cover elements such as use and period
|
|
68
|
+
# https://www.hl7.org/fhir/search.html#string
|
|
69
|
+
search_value_downcase = search_value.downcase
|
|
70
|
+
values_found.any? do |name|
|
|
71
|
+
name&.text&.downcase&.start_with?(search_value_downcase) ||
|
|
72
|
+
name&.family&.downcase&.start_with?(search_value_downcase) ||
|
|
73
|
+
name&.given&.any? { |given| given.downcase.start_with?(search_value_downcase) } ||
|
|
74
|
+
name&.prefix&.any? { |prefix| prefix.downcase.start_with?(search_value_downcase) } ||
|
|
75
|
+
name&.suffix&.any? { |suffix| suffix.downcase.start_with?(search_value_downcase) }
|
|
76
|
+
end
|
|
77
|
+
when 'Address'
|
|
78
|
+
search_value_downcase = search_value.downcase
|
|
79
|
+
values_found.any? do |address|
|
|
80
|
+
address&.text&.downcase&.start_with?(search_value_downcase) ||
|
|
81
|
+
address&.city&.downcase&.start_with?(search_value_downcase) ||
|
|
82
|
+
address&.state&.downcase&.start_with?(search_value_downcase) ||
|
|
83
|
+
address&.postalCode&.downcase&.start_with?(search_value_downcase) ||
|
|
84
|
+
address&.country&.downcase&.start_with?(search_value_downcase)
|
|
85
|
+
end
|
|
86
|
+
when 'CodeableConcept'
|
|
87
|
+
# FHIR token search (https://www.hl7.org/fhir/search.html#token): "When in doubt, servers SHOULD
|
|
88
|
+
# treat tokens in a case-insensitive manner, on the grounds that including undesired data has
|
|
89
|
+
# less safety implications than excluding desired behavior".
|
|
90
|
+
codings = values_found.flat_map(&:coding)
|
|
91
|
+
if search_value.include? '|'
|
|
92
|
+
system = search_value.split('|').first
|
|
93
|
+
code = search_value.split('|').last
|
|
94
|
+
codings&.any? { |coding| coding.system == system && coding.code&.casecmp?(code) }
|
|
95
|
+
else
|
|
96
|
+
codings&.any? { |coding| coding.code&.casecmp?(search_value) }
|
|
97
|
+
end
|
|
98
|
+
when 'Coding'
|
|
99
|
+
if search_value.include? '|'
|
|
100
|
+
system = search_value.split('|').first
|
|
101
|
+
code = search_value.split('|').last
|
|
102
|
+
values_found.any? { |coding| coding.system == system && coding.code&.casecmp?(code) }
|
|
103
|
+
else
|
|
104
|
+
values_found.any? { |coding| coding.code&.casecmp?(search_value) }
|
|
105
|
+
end
|
|
106
|
+
when 'Identifier'
|
|
107
|
+
if search_value.include? '|'
|
|
108
|
+
values_found.any? { |identifier| "#{identifier.system}|#{identifier.value}" == search_value }
|
|
109
|
+
else
|
|
110
|
+
values_found.any? { |identifier| identifier.value == search_value }
|
|
111
|
+
end
|
|
112
|
+
when 'string'
|
|
113
|
+
searched_values = search_value.downcase.split(/(?<!\\\\),/).map { |string| string.gsub('\\,', ',') }
|
|
114
|
+
values_found.any? do |value_found|
|
|
115
|
+
searched_values.any? { |searched_value| value_found.downcase.starts_with? searched_value }
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
# searching by patient requires special case because we are searching by a resource identifier
|
|
119
|
+
# references can also be URLs, so we may need to resolve those URLs
|
|
120
|
+
if %w[subject patient].include? search_param_name.to_s
|
|
121
|
+
id = search_value.split('Patient/').last
|
|
122
|
+
possible_values = [id, "Patient/#{id}", "#{url}/Patient/#{id}"]
|
|
123
|
+
values_found.any? do |reference|
|
|
124
|
+
possible_values.include? reference
|
|
125
|
+
end
|
|
126
|
+
else
|
|
127
|
+
search_values = search_value.split(/(?<!\\\\),/).map { |string| string.gsub('\\,', ',') }
|
|
128
|
+
values_found.any? { |value_found| search_values.include? value_found }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
break if match_found
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
match_found
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|