pacio_inferno_core 0.1.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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/pacio_inferno_core/custom_groups/capability_statement/conformance_support_test.rb +41 -0
  4. data/lib/pacio_inferno_core/custom_groups/capability_statement/fhir_version_test.rb +15 -0
  5. data/lib/pacio_inferno_core/custom_groups/capability_statement/instantiate_test.rb +23 -0
  6. data/lib/pacio_inferno_core/custom_groups/capability_statement/json_support_test.rb +40 -0
  7. data/lib/pacio_inferno_core/date_search_validation.rb +112 -0
  8. data/lib/pacio_inferno_core/fhir_resource_navigation.rb +7 -0
  9. data/lib/pacio_inferno_core/generator/group_generator.rb +161 -0
  10. data/lib/pacio_inferno_core/generator/group_metadata.rb +114 -0
  11. data/lib/pacio_inferno_core/generator/group_metadata_extractor.rb +301 -0
  12. data/lib/pacio_inferno_core/generator/ig_loader.rb +118 -0
  13. data/lib/pacio_inferno_core/generator/ig_metadata.rb +60 -0
  14. data/lib/pacio_inferno_core/generator/ig_metadata_extractor.rb +88 -0
  15. data/lib/pacio_inferno_core/generator/ig_resources.rb +88 -0
  16. data/lib/pacio_inferno_core/generator/must_support_metadata_extractor.rb +8 -0
  17. data/lib/pacio_inferno_core/generator/must_support_test_generator.rb +138 -0
  18. data/lib/pacio_inferno_core/generator/naming.rb +50 -0
  19. data/lib/pacio_inferno_core/generator/read_test_generator.rb +102 -0
  20. data/lib/pacio_inferno_core/generator/reference_resolution_test_generator.rb +96 -0
  21. data/lib/pacio_inferno_core/generator/search_definition_metadata_extractor.rb +228 -0
  22. data/lib/pacio_inferno_core/generator/search_metadata_extractor.rb +78 -0
  23. data/lib/pacio_inferno_core/generator/search_test_generator.rb +298 -0
  24. data/lib/pacio_inferno_core/generator/special_cases.rb +61 -0
  25. data/lib/pacio_inferno_core/generator/suite_generator.rb +105 -0
  26. data/lib/pacio_inferno_core/generator/templates/group.rb.erb +27 -0
  27. data/lib/pacio_inferno_core/generator/templates/must_support.rb.erb +36 -0
  28. data/lib/pacio_inferno_core/generator/templates/read.rb.erb +34 -0
  29. data/lib/pacio_inferno_core/generator/templates/reference_resolution.rb.erb +40 -0
  30. data/lib/pacio_inferno_core/generator/templates/search.rb.erb +40 -0
  31. data/lib/pacio_inferno_core/generator/templates/validation.rb.erb +36 -0
  32. data/lib/pacio_inferno_core/generator/terminology_binding_metadata_extractor.rb +116 -0
  33. data/lib/pacio_inferno_core/generator/validation_test_generator.rb +146 -0
  34. data/lib/pacio_inferno_core/generator/value_extractor.rb +152 -0
  35. data/lib/pacio_inferno_core/generator.rb +130 -0
  36. data/lib/pacio_inferno_core/must_support_test.rb +20 -0
  37. data/lib/pacio_inferno_core/primitive_type.rb +5 -0
  38. data/lib/pacio_inferno_core/read_test.rb +103 -0
  39. data/lib/pacio_inferno_core/reference_resolution_test.rb +181 -0
  40. data/lib/pacio_inferno_core/request_logger.rb +46 -0
  41. data/lib/pacio_inferno_core/resource_search_param_checker.rb +136 -0
  42. data/lib/pacio_inferno_core/search_test.rb +859 -0
  43. data/lib/pacio_inferno_core/search_test_properties.rb +56 -0
  44. data/lib/pacio_inferno_core/validation_test.rb +55 -0
  45. data/lib/pacio_inferno_core/version.rb +4 -0
  46. data/lib/pacio_inferno_core/well_known_code_systems.rb +21 -0
  47. metadata +165 -0
@@ -0,0 +1,105 @@
1
+ require_relative 'naming'
2
+ require_relative 'special_cases'
3
+
4
+ module PacioInfernoCore
5
+ class Generator
6
+ class SuiteGenerator
7
+ class << self
8
+ def generate(ig_metadata, base_output_dir)
9
+ new(ig_metadata, base_output_dir).generate
10
+ end
11
+ end
12
+
13
+ attr_accessor :ig_metadata, :base_output_dir
14
+
15
+ def initialize(ig_metadata, base_output_dir)
16
+ self.ig_metadata = ig_metadata
17
+ self.base_output_dir = base_output_dir
18
+ end
19
+
20
+ def version_specific_message_filters
21
+ []
22
+ end
23
+
24
+ def template
25
+ @template ||= File.read(File.join(__dir__, 'templates', 'suite.rb.erb'))
26
+ end
27
+
28
+ def output
29
+ @output ||= ERB.new(template, trim_mode: '-').result(binding)
30
+ end
31
+
32
+ def naming
33
+ self.class.module_parent::Naming
34
+ end
35
+
36
+ def base_output_file_name
37
+ "#{naming.prefix}_test_suite.rb"
38
+ end
39
+
40
+ def class_name
41
+ "#{naming.module_name}TestSuite"
42
+ end
43
+
44
+ def module_name
45
+ naming.module_name.to_s
46
+ end
47
+
48
+ def module_name_with_version
49
+ "#{naming.module_name}#{ig_metadata.reformatted_version.upcase}"
50
+ end
51
+
52
+ def output_file_name
53
+ File.join(base_output_dir, base_output_file_name)
54
+ end
55
+
56
+ def suite_id
57
+ "#{naming.prefix}_#{ig_metadata.reformatted_version}"
58
+ end
59
+
60
+ def fhir_api_group_id
61
+ "#{naming.prefix}_#{ig_metadata.reformatted_version}_fhir_api"
62
+ end
63
+
64
+ def title
65
+ "#{naming.long_name} #{ig_metadata.ig_version}"
66
+ end
67
+
68
+ def ig_identifier
69
+ version = ig_metadata.ig_version[1..] # Remove leading 'v'
70
+ "#{ig_metadata.ig_package_id}##{version}"
71
+ end
72
+
73
+ def ig_link
74
+ naming.ig_link(ig_metadata.ig_version)
75
+ end
76
+
77
+ def generate
78
+ File.write(output_file_name, output)
79
+ end
80
+
81
+ def groups
82
+ ig_metadata.ordered_groups
83
+ .reject { |group| SpecialCases.exclude_group? group }
84
+ end
85
+
86
+ def group_id_list
87
+ @group_id_list ||=
88
+ groups.map(&:id)
89
+ end
90
+
91
+ def group_file_list
92
+ @group_file_list ||=
93
+ groups.map { |group| group.file_name.delete_suffix('.rb') }
94
+ end
95
+
96
+ def capability_statement_file_name
97
+ "../../custom_groups/#{ig_metadata.ig_version}/capability_statement_group"
98
+ end
99
+
100
+ def capability_statement_group_id
101
+ "#{naming.prefix}_#{ig_metadata.reformatted_version}_capability_statement"
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,27 @@
1
+ <% test_file_list.each do |file_name| %>require_relative '<%= file_name %>'
2
+ <% end %>
3
+ module <%= naming.module_name %>TestKit
4
+ module <%= module_name_with_version %>
5
+ class <%= class_name %> < Inferno::TestGroup
6
+ title '<%= title %> Tests'
7
+ short_description <<~DESC
8
+ '<%= short_description %>'
9
+ DESC
10
+ description %(
11
+ <%= description %>
12
+ )
13
+
14
+ id :<%= group_id %>
15
+ run_as_group<% if optional? %>
16
+ optional<% end %>
17
+
18
+ def self.metadata
19
+ @metadata ||= Generator::GroupMetadata.new(
20
+ YAML.load_file(File.join(__dir__, '<%= profile_identifier %>', 'metadata.yml'), aliases: true)
21
+ )
22
+ end
23
+ <% test_id_list.each do |id| %>
24
+ test from: :<%= id %><% end %>
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ require_relative '../../../must_support_test'
2
+
3
+ module <%= naming.module_name %>TestKit
4
+ module <%= module_name_with_version %>
5
+ class <%= class_name %> < Inferno::Test
6
+ include PacioInfernoCore::MustSupportTest
7
+
8
+ title 'All must support elements are provided in the <%= resource_type %> resources returned'
9
+
10
+ description %(
11
+ This test will look through the <%= resource_type %> resources
12
+ found previously for the following must support elements:
13
+
14
+ <%= must_support_list_string %>
15
+ )
16
+
17
+ id :<%= test_id %>
18
+
19
+ def resource_type
20
+ '<%= resource_type %>'
21
+ end
22
+
23
+ def self.metadata
24
+ @metadata ||= Generator::GroupMetadata.new(YAML.load_file(File.join(__dir__, 'metadata.yml'), aliases: true))
25
+ end
26
+
27
+ def scratch_resources
28
+ scratch[:<%= profile_identifier %>_resources] ||= {}
29
+ end
30
+
31
+ run do
32
+ perform_must_support_test(<%= resource_collection_string %>)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,34 @@
1
+ require_relative '../../../read_test'
2
+
3
+ module <%= naming.module_name %>TestKit
4
+ module <%= module_name_with_version %>
5
+ class <%= class_name %> < Inferno::Test
6
+ include PacioInfernoCore::ReadTest
7
+
8
+ title 'Server returns correct <%= resource_type %> resource from <%= resource_type %> read interaction'
9
+ description 'A server <%= conformance_expectation %> support the <%= resource_type %> read interaction.'
10
+
11
+ id :<%= test_id %>
12
+ <% if input_resource_id? %>
13
+ input :<%= resource_id_input_string %>,
14
+ title: 'ID(s) for <%= group_title %> resources present on the server.',
15
+ description: %(
16
+ Comma separated list of <%= group_title %> ids that in sum contain
17
+ all MUST SUPPORT elements
18
+ )<% if optional_profile? %>,
19
+ optional: true<% end %>
20
+ <% end %>
21
+ def resource_type
22
+ '<%= resource_type %>'
23
+ end
24
+
25
+ def scratch_resources
26
+ scratch[:<%= profile_identifier %>_resources] ||= {}
27
+ end
28
+
29
+ run do
30
+ perform_read_test(<%= resource_collection_string %>)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,40 @@
1
+ require_relative '../../../reference_resolution_test'
2
+
3
+ module <%= naming.module_name %>TestKit
4
+ module <%= module_name_with_version %>
5
+ class <%= class_name %> < Inferno::Test
6
+ include PacioInfernoCore::ReferenceResolutionTest
7
+
8
+ title 'MustSupport references within <%= resource_type %> resources are valid'
9
+ description %(
10
+ This test will attempt to read external references provided within elements
11
+ marked as 'MustSupport', if any are available.
12
+
13
+ It verifies that at least one external reference for each MustSupport Reference element
14
+ can be accessed by the test client, and conforms to corresponding US Core profile.
15
+
16
+ Elements which may provide external references include:
17
+
18
+ <%= must_support_reference_list_string %>
19
+ )
20
+
21
+ id :<%= test_id %>
22
+
23
+ def resource_type
24
+ '<%= resource_type %>'
25
+ end
26
+
27
+ def self.metadata
28
+ @metadata ||= Generator::GroupMetadata.new(YAML.load_file(File.join(__dir__, 'metadata.yml'), aliases: true))
29
+ end
30
+
31
+ def scratch_resources
32
+ scratch[:<%= profile_identifier %>_resources] ||= {}
33
+ end
34
+
35
+ run do
36
+ perform_reference_resolution_test(<%= resource_collection_string %>)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,40 @@
1
+ require_relative '../../../search_test'
2
+ require_relative '../../../generator/group_metadata'
3
+
4
+ module <%= naming.module_name %>TestKit
5
+ module <%= module_name_with_version %>
6
+ class <%= class_name %> < Inferno::Test
7
+ include PacioInfernoCore::SearchTest
8
+
9
+ title 'Server returns valid results for <%= resource_type %> search by <%= search_param_name_string %>'
10
+ description %(
11
+ <%= description %>
12
+ )
13
+
14
+ id :<%= test_id %><% if optional? %>
15
+ optional
16
+ <% end %><% if needs_patient_id? %>
17
+ input :patient_ids,
18
+ title: 'Patient IDs',
19
+ description: 'Comma separated list of patient IDs that in sum contain all MUST SUPPORT elements'
20
+ <% end %>
21
+ def self.properties
22
+ @properties ||= SearchTestProperties.new(
23
+ <%= search_test_properties_string %>
24
+ )
25
+ end
26
+
27
+ def self.metadata
28
+ @metadata ||= Generator::GroupMetadata.new(YAML.load_file(File.join(__dir__, 'metadata.yml'), aliases: true))
29
+ end
30
+
31
+ def scratch_resources
32
+ scratch[:<%= profile_identifier %>_resources] ||= {}
33
+ end
34
+
35
+ run do
36
+ run_search_test
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,36 @@
1
+ require_relative '../../../validation_test'
2
+
3
+ module <%= naming.module_name %>TestKit
4
+ module <%= module_name_with_version %>
5
+ class <%= class_name %> < Inferno::Test
6
+ include PacioInfernoCore::ValidationTest
7
+
8
+ id :<%= test_id %>
9
+
10
+ title <<~DESC
11
+ <%= resource_type %> resources returned during previous tests conform to the <%= profile_name %>
12
+ DESC
13
+
14
+ description %(
15
+ <%= description %>
16
+ )
17
+
18
+ output :dar_code_found, :dar_extension_found
19
+
20
+ def resource_type
21
+ '<%= resource_type %>'
22
+ end
23
+
24
+ def scratch_resources
25
+ scratch[:<%= profile_identifier %>_resources] ||= {}
26
+ end
27
+
28
+ run do
29
+ perform_validation_test(scratch_resources[:all] || [],
30
+ '<%= profile_url %>',
31
+ '<%= profile_version %>',
32
+ skip_if_empty: <%= skip_if_empty %>)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,116 @@
1
+ module PacioInfernoCore
2
+ class Generator
3
+ class TerminologyBindingMetadataExtractor
4
+ attr_accessor :profile_elements, :ig_resources, :resource
5
+
6
+ def initialize(profile_elements, ig_resources, resource)
7
+ self.profile_elements = profile_elements
8
+ self.ig_resources = ig_resources
9
+ self.resource = resource
10
+ end
11
+
12
+ def terminology_bindings
13
+ (element_terminology_bindings + extension_terminology_bindings).compact
14
+ # add_terminology_bindings_from_extensions
15
+ # profile_elements.select { |element| element.type&.first&.code == 'Extension' }
16
+ # .each { |extension| add_terminology_bindings_from_extension(extension) }
17
+ end
18
+
19
+ def element_has_fixed_value?(element)
20
+ case element.type.first.code
21
+ when 'Quantity'
22
+ code = profile_elements.find { |e| e.path == "#{element.path}.code" }
23
+ system = profile_elements.find { |e| e.path == "#{element.path}.system" }
24
+ code&.fixedCode || system&.fixedUri
25
+ when 'code'
26
+ element.fixedCode.present?
27
+ end
28
+ end
29
+
30
+ def element_has_optional_binding_slice?(element)
31
+ element.sliceName.present? && element.min.zero?
32
+ end
33
+
34
+ def profile_elements_with_bindings
35
+ profile_elements
36
+ .select { |element| element.binding.present? && element.binding.strength == 'required' }
37
+ .reject { |element| element_has_fixed_value?(element) || element_has_optional_binding_slice?(element) }
38
+ end
39
+
40
+ def element_terminology_bindings
41
+ profile_elements_with_bindings.map do |element|
42
+ binding = {
43
+ type: element.type.first.code,
44
+ strength: element.binding.strength,
45
+ # Goal.target.detail has an unbound binding
46
+ system: element.binding.valueSet&.split('|')&.first,
47
+ path: element.path.gsub('[x]', '').gsub("#{resource}.", '')
48
+ }
49
+
50
+ binding[:required_binding_slice] = true if element.sliceName.present? && element.min.positive?
51
+
52
+ binding
53
+ end
54
+ end
55
+
56
+ def extension_profile_elements
57
+ profile_elements
58
+ .select { |element| element.type&.first&.code == 'Extension' }
59
+ .select { |element| extension_profile_url(element).present? }
60
+ end
61
+
62
+ def extension_profile_url(extension)
63
+ extension.type.first.profile&.first
64
+ end
65
+
66
+ def extension_terminology_bindings
67
+ extension_profile_elements
68
+ .flat_map do |extension_profile_element|
69
+ url = extension_profile_url(extension_profile_element)
70
+ extension = ig_resources.profile_by_url(url)
71
+
72
+ # TODO: Temporaray fix for extension defined out of US Core. FI-1623
73
+ next if extension.nil?
74
+
75
+ elements = extension.snapshot.element
76
+ elements_with_bindings = elements.select do |element|
77
+ element.binding.present? && !element.id.include?('Extension.extension')
78
+ end
79
+
80
+ elements_with_bindings.map do |element|
81
+ {
82
+ type: element.type.first.code,
83
+ strength: element.binding.strength,
84
+ system: element.binding.valueSet&.split('|')&.first,
85
+ path: element.path.gsub('[x]', '').gsub('Extension.', ''),
86
+ extensions: [url]
87
+ }
88
+ end + nested_extension_terminology_bindings(elements, url)
89
+ end
90
+ end
91
+
92
+ def nested_extension_terminology_bindings(elements, extension_url)
93
+ nested_extensions = elements.select { |element| element.path == 'Extension.extension' }
94
+ nested_extensions.flat_map do |nested_extension|
95
+ nested_extension_element = elements.find { |element| element.id == "#{nested_extension.id}.url" }
96
+ next unless nested_extension_element.present?
97
+
98
+ nested_extension_url = nested_extension_element.fixedUri
99
+ nested_elements_with_bindings = elements.select do |element|
100
+ element.id.include?(nested_extension.id) && element.binding.present?
101
+ end
102
+
103
+ nested_elements_with_bindings.map do |element|
104
+ {
105
+ type: element.type.first.code,
106
+ strength: element.binding.strength,
107
+ system: element.binding.valueSet&.split('|')&.first,
108
+ path: element.path.gsub('[x]', '').gsub('Extension.extension.', ''),
109
+ extensions: [extension_url, nested_extension_url]
110
+ }
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,146 @@
1
+ require_relative 'naming'
2
+ require_relative 'special_cases'
3
+
4
+ module PacioInfernoCore
5
+ class Generator
6
+ class ValidationTestGenerator
7
+ class << self
8
+ def generate(ig_metadata, base_output_dir)
9
+ ig_metadata.groups
10
+ .reject { |group| SpecialCases.exclude_group? group }
11
+ .each do |group|
12
+ new(group, base_output_dir: base_output_dir).generate
13
+ next unless group.resource == 'MedicationRequest'
14
+
15
+ # The Medication validation test lives in the MedicationRequest
16
+ # group, so we need to pass in that group's metadata
17
+ medication_group_metadata = ig_metadata.groups.find do |group|
18
+ group.resource == 'Medication'
19
+ end
20
+ new(medication_group_metadata, group, base_output_dir: base_output_dir).generate
21
+ end
22
+ end
23
+ end
24
+
25
+ attr_accessor :group_metadata, :medication_request_metadata, :base_output_dir
26
+
27
+ def initialize(group_metadata, medication_request_metadata = nil, base_output_dir:)
28
+ self.group_metadata = group_metadata
29
+ self.medication_request_metadata = medication_request_metadata
30
+ self.base_output_dir = base_output_dir
31
+ end
32
+
33
+ def template
34
+ @template ||= File.read(File.join(__dir__, 'templates', 'validation.rb.erb'))
35
+ end
36
+
37
+ def output
38
+ @output ||= ERB.new(template, trim_mode: '-').result(binding)
39
+ end
40
+
41
+ def naming
42
+ self.class.module_parent::Naming
43
+ end
44
+
45
+ def base_output_file_name
46
+ "#{class_name.underscore}.rb"
47
+ end
48
+
49
+ def output_file_directory
50
+ File.join(base_output_dir, directory_name)
51
+ end
52
+
53
+ def output_file_name
54
+ File.join(output_file_directory, base_output_file_name)
55
+ end
56
+
57
+ def directory_name
58
+ naming.snake_case_for_profile(medication_request_metadata || group_metadata)
59
+ end
60
+
61
+ def profile_identifier
62
+ naming.snake_case_for_profile(group_metadata)
63
+ end
64
+
65
+ def profile_url
66
+ group_metadata.profile_url
67
+ end
68
+
69
+ def profile_name
70
+ group_metadata.profile_name
71
+ end
72
+
73
+ def profile_version
74
+ group_metadata.profile_version
75
+ end
76
+
77
+ def test_id
78
+ "#{naming.prefix}_#{group_metadata.reformatted_version}_#{profile_identifier}_validation_test"
79
+ end
80
+
81
+ def class_name
82
+ "#{naming.upper_camel_case_for_profile(group_metadata)}ValidationTest"
83
+ end
84
+
85
+ def module_name_with_version
86
+ "#{naming.module_name}#{group_metadata.reformatted_version.upcase}"
87
+ end
88
+
89
+ def resource_type
90
+ group_metadata.resource
91
+ end
92
+
93
+ def conformance_expectation
94
+ read_interaction[:expectation]
95
+ end
96
+
97
+ def skip_if_empty
98
+ # Return true if a system must demonstrate at least one example of the resource type.
99
+ # This drives omit vs. skip result statuses in this test.
100
+ resource_type != 'Medication'
101
+ end
102
+
103
+ def generate
104
+ FileUtils.mkdir_p(output_file_directory)
105
+ File.write(output_file_name, output)
106
+
107
+ test_metadata = {
108
+ id: test_id,
109
+ file_name: base_output_file_name
110
+ }
111
+
112
+ if resource_type == 'Medication'
113
+ medication_request_metadata.add_test(**test_metadata)
114
+ else
115
+ group_metadata.add_test(**test_metadata)
116
+ end
117
+ end
118
+
119
+ def description
120
+ <<~DESCRIPTION
121
+ #{description_intro}
122
+ It verifies the presence of mandatory elements and that elements with
123
+ required bindings contain appropriate values. CodeableConcept element
124
+ bindings will fail if none of their codings have a code/system belonging
125
+ to the bound ValueSet. Quantity, Coding, and code element bindings will
126
+ fail if their code/system are not found in the valueset.
127
+ DESCRIPTION
128
+ end
129
+
130
+ def description_intro
131
+ if resource_type == 'Medication'
132
+ <<~MEDICATION_INTRO
133
+ This test verifies resources returned from previous tests conform to
134
+ the [#{profile_name}](#{profile_url}).
135
+ MEDICATION_INTRO
136
+ else
137
+ <<~GENERIC_INTRO
138
+ This test verifies resources returned from the first search conform to
139
+ the [#{profile_name}](#{profile_url}).
140
+ Systems must demonstrate at least one valid example in order to pass this test.
141
+ GENERIC_INTRO
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end