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
@@ -1,10 +1,171 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'us_core_test_kit/reference_resolution_test'
3
+ require_relative 'fhir_resource_navigation'
4
4
 
5
5
  module USQualityCoreTestKit
6
6
  module ReferenceResolutionTest
7
- include USCoreTestKit::ReferenceResolutionTest
8
- extend USCoreTestKit::ReferenceResolutionTest
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