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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/lib/pacio_inferno_core/custom_groups/capability_statement/conformance_support_test.rb +41 -0
- data/lib/pacio_inferno_core/custom_groups/capability_statement/fhir_version_test.rb +15 -0
- data/lib/pacio_inferno_core/custom_groups/capability_statement/instantiate_test.rb +23 -0
- data/lib/pacio_inferno_core/custom_groups/capability_statement/json_support_test.rb +40 -0
- data/lib/pacio_inferno_core/date_search_validation.rb +112 -0
- data/lib/pacio_inferno_core/fhir_resource_navigation.rb +7 -0
- data/lib/pacio_inferno_core/generator/group_generator.rb +161 -0
- data/lib/pacio_inferno_core/generator/group_metadata.rb +114 -0
- data/lib/pacio_inferno_core/generator/group_metadata_extractor.rb +301 -0
- data/lib/pacio_inferno_core/generator/ig_loader.rb +118 -0
- data/lib/pacio_inferno_core/generator/ig_metadata.rb +60 -0
- data/lib/pacio_inferno_core/generator/ig_metadata_extractor.rb +88 -0
- data/lib/pacio_inferno_core/generator/ig_resources.rb +88 -0
- data/lib/pacio_inferno_core/generator/must_support_metadata_extractor.rb +8 -0
- data/lib/pacio_inferno_core/generator/must_support_test_generator.rb +138 -0
- data/lib/pacio_inferno_core/generator/naming.rb +50 -0
- data/lib/pacio_inferno_core/generator/read_test_generator.rb +102 -0
- data/lib/pacio_inferno_core/generator/reference_resolution_test_generator.rb +96 -0
- data/lib/pacio_inferno_core/generator/search_definition_metadata_extractor.rb +228 -0
- data/lib/pacio_inferno_core/generator/search_metadata_extractor.rb +78 -0
- data/lib/pacio_inferno_core/generator/search_test_generator.rb +298 -0
- data/lib/pacio_inferno_core/generator/special_cases.rb +61 -0
- data/lib/pacio_inferno_core/generator/suite_generator.rb +105 -0
- data/lib/pacio_inferno_core/generator/templates/group.rb.erb +27 -0
- data/lib/pacio_inferno_core/generator/templates/must_support.rb.erb +36 -0
- data/lib/pacio_inferno_core/generator/templates/read.rb.erb +34 -0
- data/lib/pacio_inferno_core/generator/templates/reference_resolution.rb.erb +40 -0
- data/lib/pacio_inferno_core/generator/templates/search.rb.erb +40 -0
- data/lib/pacio_inferno_core/generator/templates/validation.rb.erb +36 -0
- data/lib/pacio_inferno_core/generator/terminology_binding_metadata_extractor.rb +116 -0
- data/lib/pacio_inferno_core/generator/validation_test_generator.rb +146 -0
- data/lib/pacio_inferno_core/generator/value_extractor.rb +152 -0
- data/lib/pacio_inferno_core/generator.rb +130 -0
- data/lib/pacio_inferno_core/must_support_test.rb +20 -0
- data/lib/pacio_inferno_core/primitive_type.rb +5 -0
- data/lib/pacio_inferno_core/read_test.rb +103 -0
- data/lib/pacio_inferno_core/reference_resolution_test.rb +181 -0
- data/lib/pacio_inferno_core/request_logger.rb +46 -0
- data/lib/pacio_inferno_core/resource_search_param_checker.rb +136 -0
- data/lib/pacio_inferno_core/search_test.rb +859 -0
- data/lib/pacio_inferno_core/search_test_properties.rb +56 -0
- data/lib/pacio_inferno_core/validation_test.rb +55 -0
- data/lib/pacio_inferno_core/version.rb +4 -0
- data/lib/pacio_inferno_core/well_known_code_systems.rb +21 -0
- metadata +165 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
require_relative 'value_extractor'
|
|
2
|
+
|
|
3
|
+
module PacioInfernoCore
|
|
4
|
+
class Generator
|
|
5
|
+
class SearchDefinitionMetadataExtractor
|
|
6
|
+
attr_accessor :ig_resources, :name, :profile_elements, :group_metadata
|
|
7
|
+
|
|
8
|
+
def initialize(name, ig_resources, profile_elements, group_metadata)
|
|
9
|
+
self.name = name
|
|
10
|
+
self.ig_resources = ig_resources
|
|
11
|
+
self.profile_elements = profile_elements
|
|
12
|
+
self.group_metadata = group_metadata
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def search_definition
|
|
16
|
+
@search_definition ||=
|
|
17
|
+
{
|
|
18
|
+
paths: paths,
|
|
19
|
+
full_paths: full_paths,
|
|
20
|
+
comparators: comparators,
|
|
21
|
+
values: values,
|
|
22
|
+
type: type,
|
|
23
|
+
contains_multiple: contains_multiple?,
|
|
24
|
+
multiple_or: multiple_or_expectation,
|
|
25
|
+
chain: chain
|
|
26
|
+
}.compact
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def resource
|
|
30
|
+
group_metadata[:resource]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def param
|
|
34
|
+
@param ||= ig_resources.search_param_by_resource_and_name(resource, name)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def param_hash
|
|
38
|
+
param.source_hash
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def full_paths
|
|
42
|
+
@full_paths ||=
|
|
43
|
+
begin
|
|
44
|
+
path = param.expression.gsub(/.where\(resolve\((.*)/, '').gsub('url = \'', 'url=\'')
|
|
45
|
+
path = path[1..-2] if path.start_with?('(') && path.end_with?(')')
|
|
46
|
+
path.scan(/[. ]as[( ]([^)]*)[)]?/).flatten.map do |as_type|
|
|
47
|
+
path.gsub!(/[. ]as[( ](#{as_type}[^)]*)[)]?/, as_type.upcase_first) if as_type.present?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
path.gsub!('Resource.', "#{resource}.") if path.start_with?('Resource.')
|
|
51
|
+
|
|
52
|
+
full_paths = path.split('|')
|
|
53
|
+
|
|
54
|
+
# There is a bug in US Core 5 asserted-date search parameter. See FHIR-40573
|
|
55
|
+
if param.respond_to?(:version) && param.version == '5.0.1' && name == 'asserted-date'
|
|
56
|
+
remove_additional_extension_from_asserted_date(full_paths)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
full_paths
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def remove_additional_extension_from_asserted_date(full_paths)
|
|
64
|
+
full_paths.each do |full_path|
|
|
65
|
+
next unless full_path.include?('http://hl7.org/fhir/StructureDefinition/condition-assertedDate')
|
|
66
|
+
|
|
67
|
+
full_path.gsub!(/\).extension./, ').')
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def paths
|
|
72
|
+
@paths ||= full_paths.map { |a_path| a_path.gsub("#{resource}.", '') }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def extensions
|
|
76
|
+
@extensions ||= full_paths.select { |a_path| a_path.include?('extension.where') }
|
|
77
|
+
.map { |a_path| { url: a_path[/(?<=extension.where\(url=').*(?='\))/] } }
|
|
78
|
+
.presence
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def profile_element
|
|
82
|
+
@profile_element ||=
|
|
83
|
+
profile_elements.find { |element| full_paths.include?(element.id) } ||
|
|
84
|
+
extension_definition&.differential&.element&.find { |element| element.id == 'Extension.value[x]' }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def extension_definition
|
|
88
|
+
@extension_definition ||=
|
|
89
|
+
begin
|
|
90
|
+
ext_definition = nil
|
|
91
|
+
extensions&.each do |ext_metadata|
|
|
92
|
+
ext_definition = ig_resources.profile_by_url(ext_metadata[:url])
|
|
93
|
+
break if ext_definition.present?
|
|
94
|
+
end
|
|
95
|
+
ext_definition
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def comparator_expectation_extensions
|
|
100
|
+
@comparator_expectation_extensions ||= param_hash['_comparator'] || []
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def support_expectation(extension)
|
|
104
|
+
extension['extension'].first['valueCode']
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def comparator_expectation(extension)
|
|
108
|
+
if extension.nil?
|
|
109
|
+
'MAY'
|
|
110
|
+
else
|
|
111
|
+
support_expectation(extension)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def comparators
|
|
116
|
+
{}.tap do |comparators|
|
|
117
|
+
param.comparator&.each_with_index do |comparator, index|
|
|
118
|
+
comparators[comparator.to_sym] = comparator_expectation(comparator_expectation_extensions[index])
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def type
|
|
124
|
+
if profile_element.present?
|
|
125
|
+
profile_element.type.first.code
|
|
126
|
+
else
|
|
127
|
+
# search is a variable type, eg. Condition.onsetDateTime - element
|
|
128
|
+
# in profile def is Condition.onset[x]
|
|
129
|
+
param.type
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def contains_multiple?
|
|
134
|
+
if profile_element.present?
|
|
135
|
+
if profile_element.id.start_with?('Extension') && extension_definition.present?
|
|
136
|
+
# Find the extension instance in a US Core profile
|
|
137
|
+
target_element = profile_elements.find do |element|
|
|
138
|
+
element.type.any? { |type| type.code == 'Extension' && type.profile.include?(extension_definition.url) }
|
|
139
|
+
end
|
|
140
|
+
target_element&.max == '*'
|
|
141
|
+
else
|
|
142
|
+
profile_element.max == '*'
|
|
143
|
+
end
|
|
144
|
+
else
|
|
145
|
+
false
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def chain_extensions
|
|
150
|
+
param_hash['_chain']
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def chain_expectations
|
|
154
|
+
chain_extensions.map { |extension| support_expectation(extension) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def chain
|
|
158
|
+
return nil if param.chain.blank?
|
|
159
|
+
|
|
160
|
+
param.chain
|
|
161
|
+
.zip(chain_expectations)
|
|
162
|
+
.map { |chain, expectation| { chain: chain, expectation: expectation } }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def multiple_or_expectation
|
|
166
|
+
param_hash['_multipleOr'] ? param_hash['_multipleOr']['extension'].first['valueCode'] : 'MAY'
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def values
|
|
170
|
+
values_from_must_supports(profile_element).presence ||
|
|
171
|
+
value_extractor.values_from_fixed_codes(profile_element, type).presence ||
|
|
172
|
+
value_extractor.codes_from_value_set_binding(profile_element).presence ||
|
|
173
|
+
values_from_resource_metadata(paths).presence ||
|
|
174
|
+
[]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def values_from_must_supports(profile_element)
|
|
178
|
+
return if profile_element.nil?
|
|
179
|
+
|
|
180
|
+
short_path = profile_element.path.split('.', 2)[1]
|
|
181
|
+
|
|
182
|
+
values_from_must_support_slices(profile_element, short_path, true).presence ||
|
|
183
|
+
values_from_must_support_slices(profile_element, short_path, false).presence ||
|
|
184
|
+
values_from_must_support_elements(short_path).presence ||
|
|
185
|
+
[]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def values_from_must_support_slices(profile_element, short_path, mandatory_slice_only)
|
|
189
|
+
group_metadata[:must_supports][:slices]
|
|
190
|
+
.select { |slice| [short_path, "#{short_path}.coding"].include?(slice[:path]) }
|
|
191
|
+
.map do |slice|
|
|
192
|
+
slice_element = profile_elements.find { |element| slice[:slice_id] == element.id }
|
|
193
|
+
next if profile_element.min.positive? && slice_element.min.zero? && mandatory_slice_only
|
|
194
|
+
|
|
195
|
+
case slice[:discriminator][:type]
|
|
196
|
+
when 'patternCoding', 'patternCodeableConcept'
|
|
197
|
+
slice[:discriminator][:code]
|
|
198
|
+
when 'requiredBinding'
|
|
199
|
+
value_extractor.codes_from_system_code_pair(slice[:discriminator][:values])
|
|
200
|
+
when 'value'
|
|
201
|
+
slice[:discriminator][:values]
|
|
202
|
+
.select { |value| value[:path] == 'coding.code' }
|
|
203
|
+
.map { |value| value[:value] }
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
.compact.flatten
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def values_from_must_support_elements(short_path)
|
|
210
|
+
group_metadata[:must_supports][:elements]
|
|
211
|
+
.select { |element| element[:path] == "#{short_path}.coding.code" }
|
|
212
|
+
.map { |element| element[:fixed_value] }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def values_from_resource_metadata(paths)
|
|
216
|
+
if multiple_or_expectation == 'SHALL' || paths.any? { |path| path.downcase.include?('status') }
|
|
217
|
+
value_extractor.codes_from_system_code_pair(value_extractor.values_from_resource_metadata(paths))
|
|
218
|
+
else
|
|
219
|
+
[]
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def value_extractor
|
|
224
|
+
@value_extractor ||= ValueExactor.new(ig_resources, resource, profile_elements)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require_relative 'search_definition_metadata_extractor'
|
|
2
|
+
|
|
3
|
+
module PacioInfernoCore
|
|
4
|
+
class Generator
|
|
5
|
+
class SearchMetadataExtractor
|
|
6
|
+
COMBO_EXTENSION_URL =
|
|
7
|
+
'http://hl7.org/fhir/StructureDefinition/capabilitystatement-search-parameter-combination'.freeze
|
|
8
|
+
|
|
9
|
+
attr_accessor :resource_capabilities, :ig_resources, :profile_elements, :group_metadata
|
|
10
|
+
|
|
11
|
+
def initialize(resource_capabilities, ig_resources, profile_elements, group_metadata)
|
|
12
|
+
self.resource_capabilities = resource_capabilities
|
|
13
|
+
self.ig_resources = ig_resources
|
|
14
|
+
self.profile_elements = profile_elements
|
|
15
|
+
self.group_metadata = group_metadata
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def searches
|
|
19
|
+
@searches ||= basic_searches + combo_searches
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def conformance_expectation(search_param)
|
|
23
|
+
search_param.extension.first.valueCode # TODO: fix expectation extension finding
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def no_search_params?
|
|
27
|
+
resource_capabilities.searchParam.blank?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def basic_searches
|
|
31
|
+
return [] if no_search_params?
|
|
32
|
+
|
|
33
|
+
resource_capabilities.searchParam
|
|
34
|
+
.select do |search_param|
|
|
35
|
+
%w[SHALL
|
|
36
|
+
SHOULD].include? conformance_expectation(search_param)
|
|
37
|
+
end
|
|
38
|
+
.map do |search_param|
|
|
39
|
+
{
|
|
40
|
+
names: [search_param.name],
|
|
41
|
+
expectation: conformance_expectation(search_param)
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def search_extensions
|
|
47
|
+
resource_capabilities.extension
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def combo_searches
|
|
51
|
+
return [] if search_extensions.blank?
|
|
52
|
+
|
|
53
|
+
search_extensions
|
|
54
|
+
.select { |extension| extension.url == COMBO_EXTENSION_URL }
|
|
55
|
+
.select { |extension| %w[SHALL SHOULD].include? conformance_expectation(extension) }
|
|
56
|
+
.map do |extension|
|
|
57
|
+
names = extension.extension.select { |param| param.valueString.present? }.map(&:valueString)
|
|
58
|
+
{
|
|
59
|
+
expectation: conformance_expectation(extension),
|
|
60
|
+
names: names
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def search_param_names
|
|
66
|
+
searches.flat_map { |search| search[:names] }.uniq
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def search_definitions
|
|
70
|
+
search_param_names.each_with_object({}) do |name, definitions|
|
|
71
|
+
definitions[name.to_sym] =
|
|
72
|
+
SearchDefinitionMetadataExtractor.new(name, ig_resources, profile_elements,
|
|
73
|
+
group_metadata).search_definition
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
require_relative 'naming'
|
|
2
|
+
require_relative 'special_cases'
|
|
3
|
+
|
|
4
|
+
module PacioInfernoCore
|
|
5
|
+
class Generator
|
|
6
|
+
class SearchTestGenerator
|
|
7
|
+
class << self
|
|
8
|
+
def generate(ig_metadata, base_output_dir)
|
|
9
|
+
ig_metadata.groups
|
|
10
|
+
.reject { |group| SpecialCases.exclude_group? group }
|
|
11
|
+
.select { |group| group.searches.present? }
|
|
12
|
+
.each do |group|
|
|
13
|
+
group.searches.each do |search|
|
|
14
|
+
new(group, search, base_output_dir).generate
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_accessor :group_metadata, :search_metadata, :base_output_dir
|
|
21
|
+
|
|
22
|
+
def initialize(group_metadata, search_metadata, base_output_dir)
|
|
23
|
+
self.group_metadata = group_metadata
|
|
24
|
+
self.search_metadata = search_metadata
|
|
25
|
+
self.base_output_dir = base_output_dir
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def template
|
|
29
|
+
@template ||= File.read(File.join(__dir__, 'templates', 'search.rb.erb'))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def output
|
|
33
|
+
@output ||= ERB.new(template, trim_mode: '-').result(binding)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def base_output_file_name
|
|
37
|
+
"#{class_name.underscore}.rb"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def output_file_directory
|
|
41
|
+
File.join(base_output_dir, profile_identifier)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def output_file_name
|
|
45
|
+
File.join(output_file_directory, base_output_file_name)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def naming
|
|
49
|
+
self.class.module_parent::Naming
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def profile_identifier
|
|
53
|
+
naming.snake_case_for_profile(group_metadata)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_id
|
|
57
|
+
"#{naming.prefix}_#{group_metadata.reformatted_version}_#{profile_identifier}_#{search_identifier}_search_test"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def search_identifier
|
|
61
|
+
search_metadata[:names].join('_').tr('-', '_')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def search_title
|
|
65
|
+
search_identifier.camelize
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def class_name
|
|
69
|
+
"#{naming.upper_camel_case_for_profile(group_metadata)}#{search_title}SearchTest"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def module_name_with_version
|
|
73
|
+
"#{naming.module_name}#{group_metadata.reformatted_version.upcase}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def resource_type
|
|
77
|
+
group_metadata.resource
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def conformance_expectation
|
|
81
|
+
search_metadata[:expectation]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def search_params
|
|
85
|
+
@search_params ||=
|
|
86
|
+
search_metadata[:names].map do |name|
|
|
87
|
+
{
|
|
88
|
+
name: name,
|
|
89
|
+
path: search_definition(name)[:path]
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def first_search?
|
|
95
|
+
group_metadata.searches.first == search_metadata
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def fixed_value_search?
|
|
99
|
+
first_search? && search_metadata[:names] != ['patient'] &&
|
|
100
|
+
!group_metadata.delayed? && resource_type != 'Patient'
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def fixed_value_search_param_name
|
|
104
|
+
(search_metadata[:names] - [:patient]).first
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def search_param_name_string
|
|
108
|
+
search_metadata[:names].join(' + ')
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def needs_patient_id?
|
|
112
|
+
search_metadata[:names].include?('patient') ||
|
|
113
|
+
(resource_type == 'Patient' && search_metadata[:names].include?('_id'))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def search_param_names
|
|
117
|
+
search_params.map { |param| param[:name] }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def search_param_names_array
|
|
121
|
+
array_of_strings(search_param_names)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def path_for_value(path)
|
|
125
|
+
path == 'class' ? 'local_class' : path
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def required_comparators_for_param(name)
|
|
129
|
+
search_definition(name)[:comparators].select { |_comparator, expectation| expectation == 'SHALL' }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def required_comparators
|
|
133
|
+
@required_comparators ||=
|
|
134
|
+
search_param_names.each_with_object({}) do |name, comparators|
|
|
135
|
+
required_comparators = required_comparators_for_param(name)
|
|
136
|
+
comparators[name] = required_comparators if required_comparators.present?
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def optional?
|
|
141
|
+
conformance_expectation != 'SHALL' || !search_metadata[:must_support_or_mandatory]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def search_definition(name)
|
|
145
|
+
group_metadata.search_definitions[name.to_sym]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def saves_delayed_references?
|
|
149
|
+
first_search? && group_metadata.delayed_references.present?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def possible_status_search?
|
|
153
|
+
search_metadata[:names].none? { |name| name.include? 'status' } &&
|
|
154
|
+
group_metadata.search_definitions.keys.any? { |key| key.to_s.include? 'status' }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def token_search_params
|
|
158
|
+
@token_search_params ||=
|
|
159
|
+
search_param_names.select do |name|
|
|
160
|
+
%w[Identifier CodeableConcept Coding].include? group_metadata.search_definitions[name.to_sym][:type]
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def token_search_params_string
|
|
165
|
+
array_of_strings(token_search_params)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def required_multiple_or_search_params
|
|
169
|
+
@multiple_or_search_params ||=
|
|
170
|
+
search_param_names.select do |name|
|
|
171
|
+
search_definition(name)[:multiple_or] == 'SHALL'
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def required_multiple_or_search_params_string
|
|
176
|
+
array_of_strings(required_multiple_or_search_params)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def required_comparators_string
|
|
180
|
+
array_of_strings(required_comparators.keys)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def array_of_strings(array)
|
|
184
|
+
quoted_strings = array.map { |element| "'#{element}'" }
|
|
185
|
+
"[#{quoted_strings.join(', ')}]"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def test_reference_variants?
|
|
189
|
+
first_search? && search_param_names.include?('patient')
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def test_medication_inclusion?
|
|
193
|
+
%w[MedicationRequest MedicationDispense].include?(resource_type)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def test_post_search?
|
|
197
|
+
first_search?
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def search_properties
|
|
201
|
+
{}.tap do |properties|
|
|
202
|
+
properties[:first_search] = 'true' if first_search?
|
|
203
|
+
properties[:fixed_value_search] = 'true' if fixed_value_search?
|
|
204
|
+
properties[:resource_type] = "'#{resource_type}'"
|
|
205
|
+
properties[:search_param_names] = search_param_names_array
|
|
206
|
+
properties[:saves_delayed_references] = 'true' if saves_delayed_references?
|
|
207
|
+
properties[:possible_status_search] = 'true' if possible_status_search?
|
|
208
|
+
properties[:test_medication_inclusion] = 'true' if test_medication_inclusion?
|
|
209
|
+
properties[:token_search_params] = token_search_params_string if token_search_params.present?
|
|
210
|
+
properties[:test_reference_variants] = 'true' if test_reference_variants?
|
|
211
|
+
properties[:params_with_comparators] = required_comparators_string if required_comparators.present?
|
|
212
|
+
if required_multiple_or_search_params.present?
|
|
213
|
+
properties[:multiple_or_search_params] =
|
|
214
|
+
required_multiple_or_search_params_string
|
|
215
|
+
end
|
|
216
|
+
properties[:test_post_search] = 'true' if first_search?
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def ig_link
|
|
221
|
+
Naming.ig_link(group_metadata.version)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def search_test_properties_string
|
|
225
|
+
search_properties
|
|
226
|
+
.map { |key, value| "#{' ' * 8}#{key}: #{value}" }
|
|
227
|
+
.join(",\n")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def generate
|
|
231
|
+
FileUtils.mkdir_p(output_file_directory)
|
|
232
|
+
File.write(output_file_name, output)
|
|
233
|
+
|
|
234
|
+
group_metadata.add_test(
|
|
235
|
+
id: test_id,
|
|
236
|
+
file_name: base_output_file_name
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def reference_search_description
|
|
241
|
+
return '' unless test_reference_variants?
|
|
242
|
+
|
|
243
|
+
<<~REFERENCE_SEARCH_DESCRIPTION
|
|
244
|
+
This test verifies that the server supports searching by reference using
|
|
245
|
+
the form `patient=[id]` as well as `patient=Patient/[id]`. The two
|
|
246
|
+
different forms are expected to return the same number of results. US
|
|
247
|
+
Core requires that both forms are supported by US Core responders.
|
|
248
|
+
REFERENCE_SEARCH_DESCRIPTION
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def first_search_description
|
|
252
|
+
return '' unless first_search?
|
|
253
|
+
|
|
254
|
+
<<~FIRST_SEARCH_DESCRIPTION
|
|
255
|
+
Because this is the first search of the sequence, resources in the
|
|
256
|
+
response will be used for subsequent tests.
|
|
257
|
+
FIRST_SEARCH_DESCRIPTION
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def medication_inclusion_description
|
|
261
|
+
return '' unless test_medication_inclusion?
|
|
262
|
+
|
|
263
|
+
<<~MEDICATION_INCLUSION_DESCRIPTION
|
|
264
|
+
If any #{resource_type} resources use external references to
|
|
265
|
+
Medications, the search will be repeated with
|
|
266
|
+
`_include=#{resource_type}:medication`.
|
|
267
|
+
MEDICATION_INCLUSION_DESCRIPTION
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def post_search_description
|
|
271
|
+
return '' unless test_post_search?
|
|
272
|
+
|
|
273
|
+
<<~POST_SEARCH_DESCRIPTION
|
|
274
|
+
Additionally, this test will check that GET and POST search methods
|
|
275
|
+
return the same number of results. Search by POST is required by the
|
|
276
|
+
FHIR R4 specification, and these tests interpret search by GET as a
|
|
277
|
+
requirement of US Core #{group_metadata.version}.
|
|
278
|
+
POST_SEARCH_DESCRIPTION
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def description
|
|
282
|
+
<<~DESCRIPTION.gsub(/\n{3,}/, "\n\n")
|
|
283
|
+
A server #{conformance_expectation} support searching by
|
|
284
|
+
#{search_param_name_string} on the #{resource_type} resource. This test
|
|
285
|
+
will pass if resources are returned and match the search criteria. If
|
|
286
|
+
none are returned, the test is skipped.
|
|
287
|
+
|
|
288
|
+
#{medication_inclusion_description}
|
|
289
|
+
#{reference_search_description}
|
|
290
|
+
#{first_search_description}
|
|
291
|
+
#{post_search_description}
|
|
292
|
+
|
|
293
|
+
[US Core Server CapabilityStatement](#{ig_link}/CapabilityStatement-us-core-server.html)
|
|
294
|
+
DESCRIPTION
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module PacioInfernoCore
|
|
2
|
+
class Generator
|
|
3
|
+
module SpecialCases
|
|
4
|
+
# These resources are excluded from US Core Test Suite
|
|
5
|
+
RESOURCES_TO_EXCLUDE = {
|
|
6
|
+
'Location' => %w[v311 v400 v501 v610],
|
|
7
|
+
'Medication' => %w[v311 v400 v501 v610 v700 v800],
|
|
8
|
+
'PractitionerRole' => %w[v311 v400]
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
# These profiles are excluded from US Core Test Suite
|
|
12
|
+
PROFILES_TO_EXCLUDE = [
|
|
13
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-survey',
|
|
14
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-vital-signs'
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
OPTIONAL_RESOURCES = %w[
|
|
18
|
+
PractitionerRole
|
|
19
|
+
QuestionnaireResponse
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
OPTIONAL_PROFILES = {
|
|
23
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-simple-observation' => %w[v610 v700]
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
# These resources relies on references from other resources but they also have mandatory search tests.
|
|
27
|
+
SEARCHABLE_DELAYED_RESOURCES = {
|
|
28
|
+
'Location' => %w[v700 v800]
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
ALL_VERSION_CATEGORY_FIRST_PROFILES = [
|
|
32
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-careplan',
|
|
33
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-diagnosticreport-lab',
|
|
34
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-diagnosticreport-note',
|
|
35
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-clinical-result',
|
|
36
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-clinical-test',
|
|
37
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-imaging',
|
|
38
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab',
|
|
39
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-screening-assessment',
|
|
40
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-sdoh-assessment',
|
|
41
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-social-history',
|
|
42
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-survey',
|
|
43
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-simple-observation'
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
VERSION_SPECIFIC_CATEGORY_FIRST_PROFILES = {
|
|
47
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-condition-encounter-diagnosis' => %w[v610 v700
|
|
48
|
+
v800],
|
|
49
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-condition-problems-health-concerns' => %w[v610
|
|
50
|
+
v700 v800]
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
def exclude_group?(group)
|
|
55
|
+
RESOURCES_TO_EXCLUDE.key?(group.resource) &&
|
|
56
|
+
RESOURCES_TO_EXCLUDE[group.resource].include?(group.reformatted_version)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|