inferno_core 0.6.8 → 0.6.10
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/lib/inferno/apps/cli/evaluate/database.yml +15 -0
- data/lib/inferno/apps/cli/evaluate/docker-compose.evaluate.yml +16 -0
- data/lib/inferno/apps/cli/evaluate.rb +52 -4
- data/lib/inferno/apps/cli/main.rb +5 -1
- data/lib/inferno/apps/cli/requirements.rb +28 -0
- data/lib/inferno/apps/cli/requirements_exporter.rb +194 -0
- data/lib/inferno/apps/cli/suite.rb +21 -0
- data/lib/inferno/apps/cli/templates/lib/%library_name%/example_suite/patient_group.rb.tt +141 -0
- data/lib/inferno/apps/cli/templates/lib/%library_name%/example_suite.rb.tt +128 -0
- data/lib/inferno/apps/cli/templates/lib/%library_name%/metadata.rb.tt +65 -3
- data/lib/inferno/apps/cli/templates/lib/%library_name%/suite.rb.tt +2 -2
- data/lib/inferno/apps/cli/templates/lib/%library_name%/version.rb.tt +1 -0
- data/lib/inferno/apps/cli/templates/lib/%library_name%.rb.tt +1 -1
- data/lib/inferno/apps/web/controllers/requirements/show.rb +18 -0
- data/lib/inferno/apps/web/controllers/test_suites/requirements/index.rb +29 -0
- data/lib/inferno/apps/web/router.rb +7 -0
- data/lib/inferno/apps/web/serializers/input.rb +1 -0
- data/lib/inferno/apps/web/serializers/requirement.rb +18 -0
- data/lib/inferno/apps/web/serializers/requirement_set.rb +13 -0
- data/lib/inferno/apps/web/serializers/test.rb +1 -0
- data/lib/inferno/apps/web/serializers/test_group.rb +1 -0
- data/lib/inferno/apps/web/serializers/test_suite.rb +11 -0
- data/lib/inferno/config/boot/requirements.rb +40 -0
- data/lib/inferno/config/boot/suites.rb +3 -0
- data/lib/inferno/dsl/fhir_evaluation/default.yml +68 -0
- data/lib/inferno/dsl/fhir_evaluation/evaluator.rb +3 -5
- data/lib/inferno/dsl/fhir_evaluation/rules/all_defined_extensions_have_examples.rb +2 -2
- data/lib/inferno/dsl/fhir_evaluation/rules/all_extensions_used.rb +76 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_must_supports_present.rb +1 -1
- data/lib/inferno/dsl/fhir_evaluation/rules/all_profiles_have_examples.rb +1 -1
- data/lib/inferno/dsl/fhir_evaluation/rules/all_references_resolve.rb +2 -2
- data/lib/inferno/dsl/fhir_evaluation/rules/all_resources_reachable.rb +2 -2
- data/lib/inferno/dsl/fhir_evaluation/rules/all_search_parameters_have_examples.rb +22 -11
- data/lib/inferno/dsl/fhir_evaluation/rules/differential_content_has_examples.rb +124 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/value_sets_demonstrate.rb +233 -0
- data/lib/inferno/dsl/fhir_resource_navigation.rb +11 -2
- data/lib/inferno/dsl/fhir_resource_validation.rb +25 -3
- data/lib/inferno/dsl/fhirpath_evaluation.rb +25 -1
- data/lib/inferno/dsl/input_output_handling.rb +1 -0
- data/lib/inferno/dsl/must_support_assessment.rb +15 -3
- data/lib/inferno/dsl/requirement_set.rb +82 -0
- data/lib/inferno/dsl/runnable.rb +27 -0
- data/lib/inferno/dsl/short_id_manager.rb +55 -0
- data/lib/inferno/dsl/suite_requirements.rb +46 -0
- data/lib/inferno/entities/ig.rb +4 -0
- data/lib/inferno/entities/input.rb +14 -5
- data/lib/inferno/entities/requirement.rb +75 -0
- data/lib/inferno/entities/test.rb +3 -1
- data/lib/inferno/entities/test_group.rb +3 -1
- data/lib/inferno/entities/test_suite.rb +4 -0
- data/lib/inferno/exceptions.rb +6 -0
- data/lib/inferno/public/237.bundle.js +1 -1
- data/lib/inferno/public/bundle.js +54 -54
- data/lib/inferno/public/bundle.js.LICENSE.txt +3 -36
- data/lib/inferno/repositories/igs.rb +1 -2
- data/lib/inferno/repositories/requirements.rb +120 -0
- data/lib/inferno/version.rb +1 -1
- data/spec/shared/test_kit_examples.rb +32 -0
- metadata +36 -3
- data/lib/inferno/apps/cli/templates/lib/%library_name%/patient_group.rb.tt +0 -44
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module DSL
|
5
|
+
module FHIREvaluation
|
6
|
+
module Rules
|
7
|
+
class DifferentialContentHasExamples < Rule
|
8
|
+
def check(context)
|
9
|
+
unused_differential = Hash.new { |field, url| field[url] = Set.new }
|
10
|
+
collect_profile_differential_content(unused_differential, context.ig.profiles)
|
11
|
+
collect_profile_differential_content(unused_differential, context.ig.extensions)
|
12
|
+
remove_found_differential_content(unused_differential, context.data)
|
13
|
+
|
14
|
+
if unused_differential.any? { |_url, diff| !diff.empty? }
|
15
|
+
message = gen_differential_fail_message(unused_differential)
|
16
|
+
result = EvaluationResult.new(message, rule: self)
|
17
|
+
else
|
18
|
+
message = 'All differential fields are used in examples.'
|
19
|
+
result = EvaluationResult.new(message, severity: 'success', rule: self)
|
20
|
+
end
|
21
|
+
|
22
|
+
context.add_result result
|
23
|
+
end
|
24
|
+
|
25
|
+
def collect_profile_differential_content(unused_differential, profiles)
|
26
|
+
profiles.each do |profile|
|
27
|
+
profile.each_element do |value, _metadata, path|
|
28
|
+
next unless path.start_with? 'differential'
|
29
|
+
|
30
|
+
next unless value.is_a? FHIR::ElementDefinition
|
31
|
+
next unless value.id.include? '.'
|
32
|
+
|
33
|
+
# Skip fields that are disallowed by the profile (cardinality 0..0)
|
34
|
+
# Note that max is a string to allow for "*", not an int
|
35
|
+
next if value.max == '0'
|
36
|
+
|
37
|
+
# TODO: discriminate between extensions
|
38
|
+
# if you have field.extension:A and field.extension:B
|
39
|
+
# only field.extension is recorded and checked for
|
40
|
+
# if A and B are not defined in the IG,they may be missed
|
41
|
+
clean_val = clean_value(value)
|
42
|
+
|
43
|
+
unused_differential[profile.url].add(clean_val)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
49
|
+
def remove_found_differential_content(unused_differential, examples)
|
50
|
+
examples.each do |resource|
|
51
|
+
extension_base_path = ''
|
52
|
+
extension_url = ''
|
53
|
+
resource.each_element do |value, _metadata, path|
|
54
|
+
profiles = resource&.meta&.profile || []
|
55
|
+
profiles.each do |profile|
|
56
|
+
processed_path = plain_value(path)
|
57
|
+
processed_path = rm_brackets(processed_path)
|
58
|
+
|
59
|
+
if path.match?('extension\[\d+\]\.url$')
|
60
|
+
extension_base_path = path.rpartition('.').first
|
61
|
+
extension_url = value
|
62
|
+
unused_differential[extension_url].delete('url') if unused_differential.key?(extension_url)
|
63
|
+
unused_differential[extension_url].delete('extension') if unused_differential.key?(extension_url)
|
64
|
+
unused_differential.delete(extension_url) if unused_differential[extension_url].empty?
|
65
|
+
elsif path.start_with?(extension_base_path) && !extension_base_path.empty?
|
66
|
+
if unused_differential.key?(extension_url)
|
67
|
+
unused_differential[extension_url].delete(processed_path.rpartition('.').last)
|
68
|
+
end
|
69
|
+
unused_differential.delete(extension_url) if unused_differential[extension_url].empty?
|
70
|
+
else
|
71
|
+
unused_differential[profile].delete(processed_path) if unused_differential.key?(profile)
|
72
|
+
unused_differential.delete(profile) if unused_differential[profile].empty?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
79
|
+
|
80
|
+
def clean_value(value)
|
81
|
+
stripped_val = value.id.partition('.').last
|
82
|
+
stripped_val = stripped_val.partition('[').first if stripped_val.end_with? ']'
|
83
|
+
stripped_val.split('.').map do |field|
|
84
|
+
field = field.partition(':').first if field.include?(':')
|
85
|
+
field = field.partition('[').first if field.include?('[')
|
86
|
+
field
|
87
|
+
end.join('.')
|
88
|
+
end
|
89
|
+
|
90
|
+
def plain_value(path)
|
91
|
+
if path.include? '.'
|
92
|
+
path_array = path.split('.').map! do |field|
|
93
|
+
field.start_with?('value') ? 'value' : field
|
94
|
+
end
|
95
|
+
path_array.join('.')
|
96
|
+
elsif path.start_with?('value')
|
97
|
+
'value'
|
98
|
+
elsif path.end_with?(']')
|
99
|
+
path.rpartition('[').first
|
100
|
+
else
|
101
|
+
path
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def rm_brackets(path)
|
106
|
+
path_array = path.split('.').map! do |field|
|
107
|
+
field.include?('[') ? field.partition('[').first : field
|
108
|
+
end
|
109
|
+
path_array.join('.')
|
110
|
+
end
|
111
|
+
|
112
|
+
def gen_differential_fail_message(unused_differential)
|
113
|
+
"Found fields highlighted in the differential view, but not used in examples: #{
|
114
|
+
unused_differential.map do |url, field|
|
115
|
+
next if field.empty?
|
116
|
+
|
117
|
+
"\n Profile/Extension: #{url} \n\tFields: #{field.join(', ')}"
|
118
|
+
end.compact.join}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
require 'uri'
|
6
|
+
require 'base64'
|
7
|
+
|
8
|
+
module Inferno
|
9
|
+
module DSL
|
10
|
+
module FHIREvaluation
|
11
|
+
module Rules
|
12
|
+
# This rule evaluates if an IG defines a new valueset, the examples should
|
13
|
+
# demonstrate reasonable coverage of that valueset.
|
14
|
+
# Note this probably only makes sense for small valuesets
|
15
|
+
# such as status options, not something like disease codes from SNOMED.
|
16
|
+
|
17
|
+
# Algorithm:
|
18
|
+
# 1. Extract pairs of system and code from include in value sets in IG
|
19
|
+
# 2. If valueSet exists in include, retrieve the value sets from UMLS.
|
20
|
+
# Extract pairs of system and code from the result.
|
21
|
+
# 3. For each pair of system and code, check if any resources in the IG have instance of them.
|
22
|
+
# 4. Count total number of existences.
|
23
|
+
|
24
|
+
class ValueSetsDemonstrate < Rule
|
25
|
+
attr_accessor :config, :value_set_unevaluated, :value_set_used, :value_set_unused
|
26
|
+
|
27
|
+
def check(context)
|
28
|
+
@config = context.config
|
29
|
+
@value_set_used = []
|
30
|
+
@value_set_unused = []
|
31
|
+
@value_set_unevaluated = []
|
32
|
+
|
33
|
+
classify_valuesets(context)
|
34
|
+
|
35
|
+
context.add_result create_result_message
|
36
|
+
end
|
37
|
+
|
38
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
39
|
+
def classify_valuesets(context)
|
40
|
+
context.ig.value_sets.each do |valueset|
|
41
|
+
valueset_used_count = 0
|
42
|
+
system_codes = extract_systems_codes_from_valueset(valueset)
|
43
|
+
|
44
|
+
value_set_unevaluated << "#{valueset.url}: unable to find system and code" if system_codes.none?
|
45
|
+
value_set_unevaluated.uniq!
|
46
|
+
|
47
|
+
next if value_set_unevaluated.any? { |element| element.include?(valueset.url) }
|
48
|
+
|
49
|
+
resource_used = []
|
50
|
+
|
51
|
+
context.data.each do |resource|
|
52
|
+
system_codes.each do |system_code|
|
53
|
+
next unless !system_code.nil? && resource_uses_code(resource.to_hash, system_code[:system],
|
54
|
+
system_code[:code])
|
55
|
+
|
56
|
+
valueset_used_count += 1
|
57
|
+
resource_used << resource.id unless resource_used.include?(resource.id)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
if valueset_used_count.positive?
|
62
|
+
# rubocop:disable Layout/LineLength
|
63
|
+
value_set_used << "#{valueset.url} is used #{valueset_used_count} times in #{resource_used.count} resources"
|
64
|
+
# rubocop:enable Layout/LineLength
|
65
|
+
else
|
66
|
+
value_set_unused << valueset.url
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
71
|
+
|
72
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
73
|
+
def create_result_message
|
74
|
+
if value_set_unused.none?
|
75
|
+
message = 'All ValueSets are used in examples:'
|
76
|
+
value_set_used.map { |value_set| message += "\n\t#{value_set}" }
|
77
|
+
|
78
|
+
if value_set_unevaluated.any?
|
79
|
+
message += "\nThe following Value Sets were not able to be evaluated: "
|
80
|
+
value_set_unevaluated.map { |value_set| message += "\n\t#{value_set}" }
|
81
|
+
end
|
82
|
+
|
83
|
+
EvaluationResult.new(message, severity: 'success', rule: self)
|
84
|
+
else
|
85
|
+
message = 'Found ValueSets with all codes used (at least once) in examples:'
|
86
|
+
value_set_used.map { |url| message += "\n\t#{url}" }
|
87
|
+
|
88
|
+
message += "\nFound unused ValueSets: "
|
89
|
+
value_set_unused.map { |url| message += "\n\t#{url}" }
|
90
|
+
|
91
|
+
if value_set_unevaluated.any?
|
92
|
+
message += "\nFound unevaluated ValueSets: "
|
93
|
+
value_set_unevaluated.map { |url| message += "\n\t#{url}" }
|
94
|
+
end
|
95
|
+
|
96
|
+
EvaluationResult.new(message, rule: self)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
100
|
+
|
101
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
102
|
+
def extract_systems_codes_from_valueset(valueset)
|
103
|
+
system_codes = []
|
104
|
+
|
105
|
+
if valueset.to_hash['compose']
|
106
|
+
valueset.to_hash['compose']['include'].each do |include|
|
107
|
+
if include['valueSet']
|
108
|
+
include['valueSet'].each do |url|
|
109
|
+
retrieve_valueset_from_api(url)&.each { |system_code| system_codes << system_code }
|
110
|
+
end
|
111
|
+
next
|
112
|
+
end
|
113
|
+
|
114
|
+
system_url = include['system']
|
115
|
+
|
116
|
+
if system_url && include['concept']
|
117
|
+
include['concept'].each do |code|
|
118
|
+
system_codes << { system: system_url, code: code.to_hash['code'] }
|
119
|
+
end
|
120
|
+
next
|
121
|
+
end
|
122
|
+
|
123
|
+
if system_url
|
124
|
+
if system_url['http://hl7.org/fhir']
|
125
|
+
retrieve_valueset_from_api(system_url)&.each { |vs| system_codes << vs }
|
126
|
+
end
|
127
|
+
next
|
128
|
+
end
|
129
|
+
|
130
|
+
value_set_unevaluated << "#{valueset.url}: system url not provided" unless system_url
|
131
|
+
|
132
|
+
# Exclude if system is provided as Uniform Resource Name "urn:"
|
133
|
+
# Exclude filter
|
134
|
+
# Exclude only system is provided (e.g. http://loing.org)
|
135
|
+
exclusions = config.data['Rule']['ValueSetsDemonstrate']['Exclude']
|
136
|
+
if exclusions['URL'] && (system_url['urn'])
|
137
|
+
value_set_unevaluated << "#{valueset.url}: unable to handle Uniform Resource Name"
|
138
|
+
end
|
139
|
+
|
140
|
+
if exclusions['Filter'] && (system_url && include['filter'])
|
141
|
+
value_set_unevaluated << "#{valueset.url}: unable to handle filter"
|
142
|
+
end
|
143
|
+
|
144
|
+
if exclusions['SystemOnly'] && (system_url && !include['concept'] && !include['filter'])
|
145
|
+
value_set_unevaluated << "#{valueset.url}: unabe to handle SystemOnly"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
else
|
149
|
+
value_set_unevaluated << valueset.url
|
150
|
+
end
|
151
|
+
system_codes.flatten.uniq
|
152
|
+
end
|
153
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
154
|
+
|
155
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
156
|
+
def resource_uses_code(resource, system, code)
|
157
|
+
resource.each do |key, value|
|
158
|
+
next unless key == 'code' || ['value', 'valueCodeableConcept', 'valueString',
|
159
|
+
'valueQuantity', 'valueBoolean',
|
160
|
+
'valueInteger', 'valueRange', 'valueRatio',
|
161
|
+
'valueSampleData', 'valueDateTime',
|
162
|
+
'valuePeriod', 'valueTime'].include?(key)
|
163
|
+
next unless value.is_a?(Hash)
|
164
|
+
|
165
|
+
value['coding']&.each do |codeset|
|
166
|
+
return true if codeset.to_hash['system'] == system && codeset.to_hash['code'] == code
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
false
|
171
|
+
end
|
172
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
173
|
+
|
174
|
+
def extract_valueset_from_response(response)
|
175
|
+
value_set = JSON.parse(response.body)
|
176
|
+
|
177
|
+
if value_set['compose'] && value_set['compose']['include']
|
178
|
+
value_set['compose']['include'].map do |include|
|
179
|
+
include['concept']&.map { |concept| { system: include['system'], code: concept['code'] } }
|
180
|
+
end.flatten
|
181
|
+
else
|
182
|
+
puts 'No Value Set found in the response.'
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def retrieve_valueset_from_api(url)
|
187
|
+
url['http:'] = 'https:' if url['http:']
|
188
|
+
uri = URI.parse(url)
|
189
|
+
|
190
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
191
|
+
http.use_ssl = (uri.scheme == 'https')
|
192
|
+
|
193
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
194
|
+
|
195
|
+
username = config.data['Environment']['VSAC']['Username']
|
196
|
+
password = config.data['Environment']['VSAC']['Password']
|
197
|
+
encoded_credentials = Base64.strict_encode64("#{username}:#{password}")
|
198
|
+
request['Authorization'] = "Basic #{encoded_credentials}"
|
199
|
+
|
200
|
+
response = http.request(request)
|
201
|
+
|
202
|
+
content_type = response['content-type']
|
203
|
+
return unless content_type && !content_type.include?('text/html')
|
204
|
+
|
205
|
+
while response.is_a?(Net::HTTPRedirection)
|
206
|
+
redirect_url = response['location']
|
207
|
+
|
208
|
+
redirect_url['xml'] = 'json'
|
209
|
+
uri = URI.parse(redirect_url)
|
210
|
+
|
211
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
212
|
+
http.use_ssl = (uri.scheme == 'https')
|
213
|
+
|
214
|
+
response = http.request(Net::HTTP::Get.new(uri.request_uri))
|
215
|
+
end
|
216
|
+
|
217
|
+
if response.code.to_i == 200
|
218
|
+
extract_valueset_from_response(response)
|
219
|
+
else
|
220
|
+
unless config.data['Rule']['ValueSetsDemonstrate']['IgnoreUnloadableValueset']
|
221
|
+
raise StandardError, "Failed to retrieve external value set: #{url} HTTP Status code: #{response.code}"
|
222
|
+
end
|
223
|
+
|
224
|
+
value_set_unevaluated << "#{url}: Failed to retrieve. HTTP Status code: #{response.code}"
|
225
|
+
nil
|
226
|
+
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
@@ -177,8 +177,17 @@ module Inferno
|
|
177
177
|
end
|
178
178
|
|
179
179
|
def matching_required_binding_slice?(slice, discriminator)
|
180
|
-
discriminator[:path].present? ? slice.send((discriminator[:path]).to_s).coding : slice.coding
|
181
|
-
|
180
|
+
slice_coding = discriminator[:path].present? ? slice.send((discriminator[:path]).to_s).coding : slice.coding
|
181
|
+
slice_coding.any? do |coding|
|
182
|
+
discriminator[:values].any? do |value|
|
183
|
+
case value
|
184
|
+
when String
|
185
|
+
value == coding.code
|
186
|
+
when Hash
|
187
|
+
value[:system] == coding.system && value[:code] == coding.code
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
182
191
|
end
|
183
192
|
|
184
193
|
def verify_slice_by_values(element, value_definitions)
|
@@ -175,10 +175,10 @@ module Inferno
|
|
175
175
|
begin
|
176
176
|
response = call_validator(resource, profile_url)
|
177
177
|
rescue StandardError => e
|
178
|
-
# This could be a complete failure to connect (validator isn't running)
|
179
|
-
# or a timeout (validator took too long to respond).
|
180
178
|
runnable.add_message('error', e.message)
|
181
|
-
|
179
|
+
Application[:logger].error(e.message)
|
180
|
+
|
181
|
+
raise Inferno::Exceptions::ErrorInValidatorException, validator_error_message(e)
|
182
182
|
end
|
183
183
|
|
184
184
|
outcome = operation_outcome_from_validator_response(response, runnable)
|
@@ -340,6 +340,28 @@ module Inferno
|
|
340
340
|
'Validator response was an unexpected format. ' \
|
341
341
|
'Review Messages tab or validator service logs for more information.'
|
342
342
|
end
|
343
|
+
|
344
|
+
# Add a specific error message for specific network problems to help the user
|
345
|
+
#
|
346
|
+
# @private
|
347
|
+
# @param error [Exception] An error exception that happened during evaluator connection
|
348
|
+
# @return [String] A readable error message describing the specific network problem
|
349
|
+
def validator_error_message(error)
|
350
|
+
case error
|
351
|
+
when Faraday::ConnectionFailed
|
352
|
+
"Connection failed to validator at #{url}."
|
353
|
+
when Faraday::TimeoutError
|
354
|
+
"Timeout while connecting to validator at #{url}."
|
355
|
+
when Faraday::SSLError
|
356
|
+
"SSL error connecting to validator at #{url}."
|
357
|
+
when Faraday::ClientError # these are 400s
|
358
|
+
"Client error (4xx) connecting to validator at #{url}."
|
359
|
+
when Faraday::ServerError # these are 500s
|
360
|
+
"Server error (5xx) from validator at #{url}."
|
361
|
+
else
|
362
|
+
"Unable to connect to validator at #{url}."
|
363
|
+
end
|
364
|
+
end
|
343
365
|
end
|
344
366
|
|
345
367
|
# @private
|
@@ -67,7 +67,9 @@ module Inferno
|
|
67
67
|
# This could be a complete failure to connect (fhirpath service isn't running)
|
68
68
|
# or a timeout (fhirpath service took too long to respond).
|
69
69
|
runnable.add_message('error', e.message)
|
70
|
-
|
70
|
+
Application[:logger].error(e.message)
|
71
|
+
|
72
|
+
raise Inferno::Exceptions::ErrorInFhirpathException, evaluator_error_message(e)
|
71
73
|
end
|
72
74
|
|
73
75
|
sanitized_body = remove_invalid_characters(response.body)
|
@@ -108,6 +110,28 @@ module Inferno
|
|
108
110
|
def remove_invalid_characters(string)
|
109
111
|
string.gsub(/[^[:print:]\r\n]+/, '')
|
110
112
|
end
|
113
|
+
|
114
|
+
# Add a specific error message for specific network problems to help the user
|
115
|
+
#
|
116
|
+
# @private
|
117
|
+
# @param error [Exception] An error exception that happened during evaluator connection
|
118
|
+
# @return [String] A readable error message describing the specific network problem
|
119
|
+
def evaluator_error_message(error)
|
120
|
+
case error
|
121
|
+
when Faraday::ConnectionFailed
|
122
|
+
"Connection failed to evaluator at #{url}."
|
123
|
+
when Faraday::TimeoutError
|
124
|
+
"Timeout while connecting to evaluator at #{url}."
|
125
|
+
when Faraday::SSLError
|
126
|
+
"SSL error connecting to evaluator at #{url}."
|
127
|
+
when Faraday::ClientError # these are 400s
|
128
|
+
"Client error (4xx) connecting to evaluator at #{url}."
|
129
|
+
when Faraday::ServerError # these are 500s
|
130
|
+
"Server error (5xx) from evaluator at #{url}."
|
131
|
+
else
|
132
|
+
"Unable to connect to FHIRPath service at #{url}."
|
133
|
+
end
|
134
|
+
end
|
111
135
|
end
|
112
136
|
|
113
137
|
module ClassMethods
|
@@ -12,6 +12,7 @@ module Inferno
|
|
12
12
|
# @option input_params [String] :default The default value for the input
|
13
13
|
# @option input_params [Boolean] :optional Set to true to not require input for test execution
|
14
14
|
# @option input_params [Boolean] :locked If true, the user can not alter the value
|
15
|
+
# @option input_params [Boolean] :hidden If true, the input will not be visible to the user in the UI
|
15
16
|
# @option input_params [Hash] :options Possible input option formats based on input type
|
16
17
|
# @option options [Array] :list_options Array of options for input formats
|
17
18
|
# that require a list of possible values (radio and checkbox)
|
@@ -332,18 +332,23 @@ module Inferno
|
|
332
332
|
when 'Date'
|
333
333
|
begin
|
334
334
|
Date.parse(element)
|
335
|
-
rescue ArgumentError
|
335
|
+
rescue ArgumentError, TypeError
|
336
336
|
false
|
337
337
|
end
|
338
338
|
when 'DateTime'
|
339
339
|
begin
|
340
340
|
DateTime.parse(element)
|
341
|
-
rescue ArgumentError
|
341
|
+
rescue ArgumentError, TypeError
|
342
342
|
false
|
343
343
|
end
|
344
344
|
when 'String'
|
345
345
|
element.is_a? String
|
346
346
|
else
|
347
|
+
if element.is_a? FHIR::Bundle::Entry
|
348
|
+
# Special case for type slicing in a Bundle - look at the resource not the entry
|
349
|
+
element = element.resource
|
350
|
+
end
|
351
|
+
|
347
352
|
element.is_a? FHIR.const_get(discriminator[:code])
|
348
353
|
end
|
349
354
|
end
|
@@ -352,7 +357,14 @@ module Inferno
|
|
352
357
|
coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
|
353
358
|
|
354
359
|
find_a_value_at(element, coding_path) do |coding|
|
355
|
-
discriminator[:values].any?
|
360
|
+
discriminator[:values].any? do |value|
|
361
|
+
case value
|
362
|
+
when String
|
363
|
+
value == coding.code
|
364
|
+
when Hash
|
365
|
+
value[:system] == coding.system && value[:code] == coding.code
|
366
|
+
end
|
367
|
+
end
|
356
368
|
end
|
357
369
|
end
|
358
370
|
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Inferno
|
2
|
+
module DSL
|
3
|
+
# A `RequirementSet` represents the set of requirements which are tested by
|
4
|
+
# a TestSuite.
|
5
|
+
#
|
6
|
+
# @!attribute identifier [rw] The unique identifier for the source of
|
7
|
+
# requirements included in this `RequirementSet`
|
8
|
+
# @!attribute title [rw] A human-readable title for this `RequirementSet`
|
9
|
+
# @!attribute actor [rw] The actor whose requirements are included in this
|
10
|
+
# `RequirementSet`
|
11
|
+
# @!attribute requirements [rw] There are three options:
|
12
|
+
# * `"all"` (default) - Include all of the requirements for the specified
|
13
|
+
# actor from the requirement source
|
14
|
+
# * `"referenced"` - Only include requirements from this source if they
|
15
|
+
# are referenced by other included requirements
|
16
|
+
# * `"1,3,5-8"` - Only include the requirements from a comma-delimited
|
17
|
+
# list
|
18
|
+
# @!attribute suite_options [rw] A set of suite options which must be
|
19
|
+
# selected in order for this `RequirementSet` to be included
|
20
|
+
#
|
21
|
+
# @see Inferno::DSL::SuiteRequirements#requirement_sets
|
22
|
+
class RequirementSet
|
23
|
+
ATTRIBUTES = [
|
24
|
+
:identifier,
|
25
|
+
:title,
|
26
|
+
:actor,
|
27
|
+
:requirements,
|
28
|
+
:suite_options
|
29
|
+
].freeze
|
30
|
+
|
31
|
+
include Entities::Attributes
|
32
|
+
|
33
|
+
def initialize(raw_attributes_hash)
|
34
|
+
attributes_hash = raw_attributes_hash.symbolize_keys
|
35
|
+
|
36
|
+
invalid_keys = attributes_hash.keys - ATTRIBUTES
|
37
|
+
|
38
|
+
raise Exceptions::UnknownAttributeException.new(invalid_keys, self.class) if invalid_keys.present?
|
39
|
+
|
40
|
+
attributes_hash.each do |name, value|
|
41
|
+
if name == :suite_options
|
42
|
+
value = value&.map { |option_id, option_value| SuiteOption.new(id: option_id, value: option_value) }
|
43
|
+
end
|
44
|
+
|
45
|
+
instance_variable_set(:"@#{name}", value)
|
46
|
+
end
|
47
|
+
|
48
|
+
self.suite_options ||= []
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns true when the `RequirementSet` includes all of the requirements
|
52
|
+
# from the source for the specified actor
|
53
|
+
#
|
54
|
+
# @return [Boolean]
|
55
|
+
def complete?
|
56
|
+
requirements.blank? || requirements.casecmp?('all')
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns true when the `RequirementSet` only includes requirements
|
60
|
+
# referenced by other `RequirementSet`s
|
61
|
+
#
|
62
|
+
# @return [Boolean]
|
63
|
+
def referenced?
|
64
|
+
requirements&.casecmp? 'referenced'
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns true when the `RequirementSet` only includes requirements
|
68
|
+
# specified in a list
|
69
|
+
#
|
70
|
+
# @return [Boolean]
|
71
|
+
def filtered?
|
72
|
+
!complete? && !referenced?
|
73
|
+
end
|
74
|
+
|
75
|
+
# Expands the compressed comma-separated requirements list into an Array
|
76
|
+
# of full ids
|
77
|
+
def expand_requirement_ids
|
78
|
+
Entities::Requirement.expand_requirement_ids(requirements, identifier)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/inferno/dsl/runnable.rb
CHANGED
@@ -276,6 +276,21 @@ module Inferno
|
|
276
276
|
@input_instructions = format_markdown(new_input_instructions)
|
277
277
|
end
|
278
278
|
|
279
|
+
# Set/Get the IDs of requirements verified by this runnable
|
280
|
+
# Set with [] to clear the list
|
281
|
+
#
|
282
|
+
# @param requirements [Array<String>]
|
283
|
+
# @return [Array<String>] the requirement IDs
|
284
|
+
def verifies_requirements(*requirement_ids)
|
285
|
+
if requirement_ids.empty?
|
286
|
+
@requirement_ids || []
|
287
|
+
elsif requirement_ids == [[]]
|
288
|
+
@requirement_ids = []
|
289
|
+
else
|
290
|
+
@requirement_ids = requirement_ids
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
279
294
|
# Mark as optional. Tests are required by default.
|
280
295
|
#
|
281
296
|
# @param optional [Boolean]
|
@@ -331,6 +346,11 @@ module Inferno
|
|
331
346
|
@all_children ||= []
|
332
347
|
end
|
333
348
|
|
349
|
+
# @private
|
350
|
+
def all_descendants
|
351
|
+
children.flat_map { |child| [child] + child.all_descendants }
|
352
|
+
end
|
353
|
+
|
334
354
|
# @private
|
335
355
|
def suite
|
336
356
|
return self if ancestors.include? Inferno::Entities::TestSuite
|
@@ -550,6 +570,13 @@ module Inferno
|
|
550
570
|
end
|
551
571
|
end
|
552
572
|
|
573
|
+
# @private
|
574
|
+
def all_requirements(suite_options = [])
|
575
|
+
children(suite_options).flat_map do |child|
|
576
|
+
child.verifies_requirements + child.all_requirements(suite_options)
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
553
580
|
# @private
|
554
581
|
def inspect
|
555
582
|
non_dynamic_ancestor = ancestors.find { |ancestor| !ancestor.to_s.start_with? '#' }
|