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,114 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'us_core_test_kit/generator/terminology_binding_metadata_extractor'
4
-
5
3
  module USQualityCoreTestKit
6
4
  class Generator
7
- class TerminologyBindingMetadataExtractor < USCoreTestKit::Generator::TerminologyBindingMetadataExtractor
5
+ class TerminologyBindingMetadataExtractor
6
+ attr_accessor :profile_elements, :ig_resources, :resource
7
+
8
+ def initialize(profile_elements, ig_resources, resource)
9
+ self.profile_elements = profile_elements
10
+ self.ig_resources = ig_resources
11
+ self.resource = resource
12
+ end
13
+
14
+ def terminology_bindings
15
+ (element_terminology_bindings + extension_terminology_bindings).compact
16
+ end
17
+
18
+ def element_has_fixed_value?(element)
19
+ case element.type.first.code
20
+ when 'Quantity'
21
+ code = profile_elements.find { |e| e.path == "#{element.path}.code" }
22
+ system = profile_elements.find { |e| e.path == "#{element.path}.system" }
23
+ code&.fixedCode || system&.fixedUri
24
+ when 'code'
25
+ element.fixedCode.present?
26
+ end
27
+ end
28
+
29
+ def element_has_optional_binding_slice?(element)
30
+ element.sliceName.present? && element.min.zero?
31
+ end
32
+
33
+ def profile_elements_with_bindings
34
+ profile_elements
35
+ .select { |element| element.binding.present? && element.binding.strength == 'required' }
36
+ .reject { |element| element_has_fixed_value?(element) || element_has_optional_binding_slice?(element) }
37
+ end
38
+
39
+ def element_terminology_bindings
40
+ profile_elements_with_bindings.map do |element|
41
+ binding = {
42
+ type: element.type.first.code,
43
+ strength: element.binding.strength,
44
+ system: element.binding.valueSet&.split('|')&.first,
45
+ path: element.path.gsub('[x]', '').gsub("#{resource}.", '')
46
+ }
47
+
48
+ binding[:required_binding_slice] = true if element.sliceName.present? && element.min.positive?
49
+
50
+ binding
51
+ end
52
+ end
53
+
54
+ def extension_profile_elements
55
+ profile_elements
56
+ .select { |element| element.type&.first&.code == 'Extension' }
57
+ .select { |element| extension_profile_url(element).present? }
58
+ end
59
+
60
+ def extension_profile_url(extension)
61
+ extension.type.first.profile&.first
62
+ end
63
+
64
+ def extension_terminology_bindings
65
+ extension_profile_elements
66
+ .flat_map do |extension_profile_element|
67
+ url = extension_profile_url(extension_profile_element)
68
+ extension = ig_resources.profile_by_url(url)
69
+
70
+ # TODO: Temporaray fix for extension defined out of US Core. FI-1623
71
+ next if extension.nil?
72
+
73
+ elements = extension.snapshot.element
74
+ elements_with_bindings = elements.select do |element|
75
+ element.binding.present? && !element.id.include?('Extension.extension')
76
+ end
77
+
78
+ elements_with_bindings.map do |element|
79
+ {
80
+ type: element.type.first.code,
81
+ strength: element.binding.strength,
82
+ system: element.binding.valueSet&.split('|')&.first,
83
+ path: element.path.gsub('[x]', '').gsub('Extension.', ''),
84
+ extensions: [url]
85
+ }
86
+ end + nested_extension_terminology_bindings(elements, url)
87
+ end
88
+ end
89
+
90
+ def nested_extension_terminology_bindings(elements, extension_url)
91
+ nested_extensions = elements.select { |element| element.path == 'Extension.extension' }
92
+ nested_extensions.flat_map do |nested_extension|
93
+ nested_extension_element = elements.find { |element| element.id == "#{nested_extension.id}.url" }
94
+ next unless nested_extension_element.present?
95
+
96
+ nested_extension_url = nested_extension_element.fixedUri
97
+ nested_elements_with_bindings = elements.select do |element|
98
+ element.id.include?(nested_extension.id) && element.binding.present?
99
+ end
100
+
101
+ nested_elements_with_bindings.map do |element|
102
+ {
103
+ type: element.type.first.code,
104
+ strength: element.binding.strength,
105
+ system: element.binding.valueSet&.split('|')&.first,
106
+ path: element.path.gsub('[x]', '').gsub('Extension.extension.', ''),
107
+ extensions: [extension_url, nested_extension_url]
108
+ }
109
+ end
110
+ end
111
+ end
8
112
  end
9
113
  end
10
114
  end
@@ -1,13 +1,71 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'us_core_test_kit/generator/validation_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 ValidationTestGenerator < USCoreTestKit::Generator::ValidationTestGenerator
8
+ class ValidationTestGenerator
9
+ attr_accessor :group_metadata, :medication_request_metadata, :base_output_dir
10
+
11
+ def initialize(group_metadata, medication_request_metadata = nil, base_output_dir:)
12
+ self.group_metadata = group_metadata
13
+ self.medication_request_metadata = medication_request_metadata
14
+ self.base_output_dir = base_output_dir
15
+ end
16
+
17
+ def output
18
+ @output ||= ERB.new(template, trim_mode: '-').result(binding)
19
+ end
20
+
21
+ def base_output_file_name
22
+ "#{class_name.underscore}.rb"
23
+ end
24
+
25
+ def output_file_directory
26
+ File.join(base_output_dir, directory_name)
27
+ end
28
+
29
+ def output_file_name
30
+ File.join(output_file_directory, base_output_file_name)
31
+ end
32
+
33
+ def profile_url
34
+ group_metadata.profile_url
35
+ end
36
+
37
+ def profile_name
38
+ group_metadata.profile_name
39
+ end
40
+
41
+ def profile_version
42
+ group_metadata.profile_version
43
+ end
44
+
45
+ def resource_type
46
+ group_metadata.resource
47
+ end
48
+
49
+ def conformance_expectation
50
+ read_interaction[:expectation]
51
+ end
52
+
53
+ def generate
54
+ FileUtils.mkdir_p(output_file_directory)
55
+ File.write(output_file_name, output)
56
+
57
+ test_metadata = {
58
+ id: test_id,
59
+ file_name: base_output_file_name
60
+ }
61
+
62
+ if resource_type == 'Medication'
63
+ medication_request_metadata.add_test(**test_metadata)
64
+ else
65
+ group_metadata.add_test(**test_metadata)
66
+ end
67
+ end
68
+
11
69
  class << self
12
70
  def generate(ig_metadata, base_output_dir)
13
71
  ig_metadata.groups
@@ -49,7 +107,7 @@ module USQualityCoreTestKit
49
107
  end
50
108
 
51
109
  def description
52
- <<~DESCRIPTION
110
+ base_description = <<~DESCRIPTION
53
111
  #{description_intro}
54
112
  It verifies the presence of mandatory elements and that elements with
55
113
  required bindings contain appropriate values. CodeableConcept element
@@ -57,6 +115,27 @@ module USQualityCoreTestKit
57
115
  to the bound ValueSet. Quantity, Coding, and code element bindings will
58
116
  fail if their code/system are not found in the valueset.
59
117
  DESCRIPTION
118
+
119
+ filter_note = validation_message_filter_note
120
+ return base_description unless filter_note
121
+
122
+ "#{base_description}\n#{filter_note}"
123
+ end
124
+
125
+ def validation_message_filter_note
126
+ case resource_type
127
+ when 'Patient'
128
+ <<~NOTE
129
+ Note: This test ignores validator messages for `Patient.extension`. This is a workaround for
130
+ a known validator package interaction where CQL can load US Core 7 definitions into the
131
+ validator session even though US Quality Core 0.5.0 is based on US Core 6.1.0.
132
+ NOTE
133
+ when 'DeviceRequest'
134
+ <<~NOTE
135
+ Note: This test ignores a known validator slicing resolution message for
136
+ `DeviceRequest.modifierExtension`.
137
+ NOTE
138
+ end
60
139
  end
61
140
 
62
141
  def description_intro
@@ -1,10 +1,154 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'us_core_test_kit/generator/value_extractor'
4
-
5
3
  module USQualityCoreTestKit
6
4
  class Generator
7
- class ValueExactor < USCoreTestKit::Generator::ValueExactor
5
+ class ValueExactor
6
+ attr_accessor :ig_resources, :resource, :profile_elements
7
+
8
+ def initialize(ig_resources, resource, profile_elements)
9
+ self.ig_resources = ig_resources
10
+ self.resource = resource
11
+ self.profile_elements = profile_elements
12
+ end
13
+
14
+ def values_from_fixed_codes(profile_element, type)
15
+ return [] unless type == 'CodeableConcept'
16
+
17
+ elements = profile_elements.select do |element|
18
+ element.path == "#{profile_element.path}.coding.code" && element.fixedCode.present?
19
+ end
20
+
21
+ elements.map(&:fixedCode)
22
+ end
23
+
24
+ def values_from_pattern_coding(profile_element, type)
25
+ return [] unless type == 'CodeableConcept'
26
+
27
+ elements = profile_elements.select do |element|
28
+ element.path == "#{profile_element.path}.coding" && element.patternCoding.present?
29
+ end
30
+
31
+ elements.map { |element| element.patternCoding.code }
32
+ end
33
+
34
+ def values_from_pattern_codeable_concept(profile_element, type)
35
+ return [] unless type == 'CodeableConcept'
36
+
37
+ elements = profile_elements.select do |element|
38
+ element.path == profile_element.path && element.patternCodeableConcept.present? && element.min.positive?
39
+ end
40
+
41
+ elements.map { |element| element.patternCodeableConcept.coding.first.code }
42
+ end
43
+
44
+ def value_set_binding(the_element)
45
+ the_element&.binding
46
+ end
47
+
48
+ def value_set(the_element)
49
+ binding = value_set_binding(the_element)
50
+ target_valueset = binding&.valueSet
51
+
52
+ additional_binding_ext = binding&.extension&.find do |ext|
53
+ ext.url == 'http://hl7.org/fhir/tools/StructureDefinition/additional-binding' &&
54
+ ext.extension.any? { |sub_ext| sub_ext.url == 'purpose' && sub_ext.valueCode == 'minimum' }
55
+ end
56
+
57
+ min_valueset_ext = binding&.extension&.find do |ext|
58
+ ext.url == 'http://hl7.org/fhir/StructureDefinition/elementdefinition-minValueSet'
59
+ end
60
+
61
+ if additional_binding_ext.present?
62
+ min_valueset = additional_binding_ext.extension.find { |ext| ext.url == 'valueSet' }
63
+
64
+ target_valueset = min_valueset.valueCanonical if min_valueset.present?
65
+ elsif min_valueset_ext.present?
66
+ target_valueset = min_valueset_ext.valueCanonical
67
+ end
68
+
69
+ ig_resources.value_set_by_url(target_valueset)
70
+ end
71
+
72
+ def bound_systems(the_element)
73
+ bound_systems_from_valueset(value_set(the_element))
74
+ end
75
+
76
+ def bound_systems_from_valueset(value_set)
77
+ value_set&.compose&.include&.map do |include_element|
78
+ if include_element.concept.present?
79
+ include_element
80
+ elsif include_element.system.present? && include_element.filter&.empty?
81
+ # Cannot process intensional value set with filters
82
+ ig_resources.code_system_by_url(include_element.system)
83
+ elsif include_element.valueSet.present?
84
+ include_element.valueSet.map do |vs|
85
+ a_value_set = ig_resources.value_set_by_url(vs)
86
+ bound_systems_from_valueset(a_value_set)
87
+ end
88
+ end
89
+ end&.flatten&.compact
90
+ end
91
+
92
+ def codes_from_value_set_binding(the_element)
93
+ codes_from_system_code_pair(codings_from_value_set_binding(the_element))
94
+ end
95
+
96
+ def codes_from_system_code_pair(codings)
97
+ codings.present? ? codings.map { |coding| coding[:code] }.compact.uniq : []
98
+ end
99
+
100
+ def codings_from_value_set_binding(the_element)
101
+ return [] if the_element.nil?
102
+
103
+ bound_systems = bound_systems(the_element)
104
+
105
+ return codings_from_bound_systems(bound_systems) if bound_systems.present?
106
+
107
+ expansion_contains = value_set_expansion_contains(the_element)
108
+
109
+ return [] if expansion_contains.blank?
110
+
111
+ expansion_contains.map { |contains| { system: contains.system, code: contains.code } }.compact.uniq
112
+ end
113
+
114
+ def codings_from_bound_systems(bound_systems)
115
+ return [] unless bound_systems.present?
116
+
117
+ bound_systems.flat_map do |bound_system|
118
+ case bound_system
119
+ when FHIR::ValueSet::Compose::Include
120
+ bound_system.concept.map { |concept| { system: bound_system.system, code: concept.code } }
121
+ when FHIR::CodeSystem
122
+ bound_system.concept.map { |concept| { system: bound_system.url, code: concept.code } }
123
+ else
124
+ []
125
+ end
126
+ end.uniq
127
+ end
128
+
129
+ def value_set_expansion_contains(element)
130
+ value_set(element)&.expansion&.contains
131
+ end
132
+
133
+ def fhir_metadata(current_path)
134
+ FHIR.const_get(resource)::METADATA[current_path]
135
+ end
136
+
137
+ def values_from_resource_metadata(paths)
138
+ values = []
139
+
140
+ paths.each do |current_path|
141
+ current_metadata = fhir_metadata(current_path)
142
+
143
+ next unless current_metadata&.dig('valid_codes').present?
144
+
145
+ values += current_metadata['valid_codes'].flat_map do |system, codes|
146
+ codes.map { |code| { system:, code: } }
147
+ end
148
+ end
149
+
150
+ values
151
+ end
8
152
  end
9
153
  end
10
154
  end
@@ -1,10 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'us_core_test_kit/must_support_test'
3
+ require_relative 'fhir_resource_navigation'
4
4
 
5
5
  module USQualityCoreTestKit
6
6
  module MustSupportTest
7
- include USCoreTestKit::MustSupportTest
8
- extend USCoreTestKit::MustSupportTest
7
+ include Inferno::DSL::FHIRResourceNavigation
8
+ extend Forwardable
9
+
10
+ def_delegators 'self.class', :metadata
11
+
12
+ def all_scratch_resources
13
+ scratch_resources[:all]
14
+ end
15
+
16
+ def perform_must_support_test(resources)
17
+ skip_if resources.blank?, "No #{resource_type} resources were found"
18
+
19
+ skip { assert_must_support_elements_present(resources, nil, metadata:) }
20
+ end
9
21
  end
10
22
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USQualityCoreTestKit
4
+ # This functionality has been moved to Inferno Core, but this reference is
5
+ # retained to avoid a breaking change.
6
+ PrimitiveType = Inferno::DSL::PrimitiveType
7
+ end
@@ -1,10 +1,105 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'us_core_test_kit/read_test'
4
-
5
3
  module USQualityCoreTestKit
6
4
  module ReadTest
7
- include USCoreTestKit::ReadTest
8
- extend USCoreTestKit::ReadTest
5
+ def all_scratch_resources
6
+ scratch_resources[:all] ||= []
7
+ end
8
+
9
+ def perform_read_test(resources, _reply_handler = nil, delayed_reference: false)
10
+ skip_if resources.blank?, no_resources_skip_message
11
+
12
+ resources_to_read = if delayed_reference
13
+ readable_references(resources)
14
+ else
15
+ readable_resources(resources)
16
+ end
17
+
18
+ assert resources_to_read.present?, "No #{resource_type} id found."
19
+
20
+ if config.options[:read_all_resources]
21
+ if delayed_reference
22
+ all_referencing_resources = referencing_resources(resources_to_read)
23
+ info %(
24
+ The #{resource_type} references used for this test were pulled from the following resources:
25
+ #{all_referencing_resources}
26
+ )
27
+
28
+ resources_to_read.map! { |resource| resource[:reference] }
29
+ end
30
+
31
+ resources_to_read.each do |resource|
32
+ read_and_validate(resource)
33
+ end
34
+ else
35
+ first_resource = resources_to_read.first
36
+ if delayed_reference.present?
37
+ info %(
38
+ The #{resource_type} reference used for this test was pulled from resource
39
+ #{first_resource[:referencing_resource]}
40
+ )
41
+ first_resource = first_resource[:reference]
42
+ end
43
+ read_and_validate(first_resource)
44
+ end
45
+ end
46
+
47
+ def referencing_resources(readable_resources)
48
+ readable_resources
49
+ .map { |resource| resource[:referencing_resource] }
50
+ .join(', ')
51
+ end
52
+
53
+ def readable_references(resources)
54
+ references = resources.filter_map do |resource|
55
+ next unless resource[:reference].present? && resource[:reference].is_a?(FHIR::Reference)
56
+
57
+ reference_id = resource[:reference].reference&.split('/')&.last
58
+ next unless reference_id&.present?
59
+
60
+ resource
61
+ end
62
+
63
+ references.uniq { |resource| resource[:reference].reference.split('/').last }
64
+ end
65
+
66
+ def readable_resources(resources)
67
+ resources
68
+ .select { |resource| resource.is_a?(resource_class) || resource.is_a?(FHIR::Reference) }
69
+ .select { |resource| (resource.is_a?(FHIR::Reference) ? resource.reference.split('/').last : resource.id).present? }
70
+ .compact
71
+ .uniq { |resource| resource.is_a?(FHIR::Reference) ? resource.reference.split('/').last : resource.id }
72
+ end
73
+
74
+ def read_and_validate(resource_to_read)
75
+ id = resource_id(resource_to_read)
76
+
77
+ fhir_read resource_type, id
78
+
79
+ assert_response_status(200)
80
+ assert_resource_type(resource_type)
81
+ assert resource.id.present? && resource.id == id, bad_resource_id_message(id)
82
+
83
+ return unless resource_to_read.is_a? FHIR::Reference
84
+
85
+ all_scratch_resources << resource
86
+ end
87
+
88
+ def resource_id(resource)
89
+ resource.is_a?(FHIR::Reference) ? resource.reference.split('/').last : resource.id
90
+ end
91
+
92
+ def no_resources_skip_message
93
+ "No #{resource_type} resources appear to be available. " \
94
+ 'Please use patients with more information.'
95
+ end
96
+
97
+ def bad_resource_id_message(expected_id)
98
+ "Expected resource to have id: `#{expected_id.inspect}`, but found `#{resource.id.inspect}`"
99
+ end
100
+
101
+ def resource_class
102
+ FHIR.const_get(resource_type)
103
+ end
9
104
  end
10
105
  end