inferno_core 0.6.4 → 0.6.5
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.rb +1 -30
- data/lib/inferno/apps/cli/new.rb +1 -2
- data/lib/inferno/apps/cli/templates/.env.development +1 -0
- data/lib/inferno/apps/cli/templates/.env.production +1 -0
- data/lib/inferno/apps/cli/templates/.gitignore +1 -0
- data/lib/inferno/apps/cli/templates/data/igs/.keep +0 -0
- data/lib/inferno/apps/cli/templates/docker-compose.background.yml.tt +2 -2
- data/lib/inferno/apps/web/controllers/controller.rb +3 -1
- data/lib/inferno/apps/web/router.rb +12 -6
- data/lib/inferno/config/boot/ig_files.rb +47 -0
- data/lib/inferno/config/boot/validator.rb +1 -0
- data/lib/inferno/config/boot/web.rb +6 -2
- data/lib/inferno/dsl/assertions.rb +26 -0
- data/lib/inferno/dsl/fhir_client_builder.rb +1 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_must_supports_present.rb +13 -308
- data/lib/inferno/dsl/fhir_resource_validation.rb +34 -2
- data/lib/inferno/dsl/fhir_validation.rb +13 -0
- data/lib/inferno/dsl/must_support_assessment.rb +365 -0
- data/lib/inferno/dsl/results.rb +36 -4
- data/lib/inferno/dsl/runnable.rb +71 -0
- data/lib/inferno/dsl.rb +3 -1
- data/lib/inferno/entities/ig.rb +4 -1
- data/lib/inferno/exceptions.rb +6 -0
- data/lib/inferno/public/bundle.js +34 -34
- data/lib/inferno/public/bundle.js.LICENSE.txt +3 -3
- data/lib/inferno/repositories/igs.rb +122 -0
- data/lib/inferno/repositories/in_memory_repository.rb +7 -0
- data/lib/inferno/utils/ig_downloader.rb +17 -6
- data/lib/inferno/version.rb +1 -1
- data/spec/shared/test_kit_examples.rb +69 -0
- metadata +5 -2
@@ -9,7 +9,7 @@ module Inferno
|
|
9
9
|
#
|
10
10
|
# @example
|
11
11
|
#
|
12
|
-
#
|
12
|
+
# fhir_resource_validator do
|
13
13
|
# url 'http://example.com/validator'
|
14
14
|
# exclude_message { |message| message.type == 'info' }
|
15
15
|
# perform_additional_validation do |resource, profile_url|
|
@@ -19,12 +19,24 @@ module Inferno
|
|
19
19
|
# { type: 'info', message: 'everything is ok' }
|
20
20
|
# end
|
21
21
|
# end
|
22
|
+
# cli_context do
|
23
|
+
# noExtensibleBindingMessages true
|
24
|
+
# allowExampleUrls true
|
25
|
+
# txServer nil
|
26
|
+
# end
|
22
27
|
# end
|
23
28
|
module FHIRResourceValidation
|
24
29
|
def self.included(klass)
|
25
30
|
klass.extend ClassMethods
|
26
31
|
end
|
27
32
|
|
33
|
+
# Find a particular profile StructureDefinition and the IG it belongs to.
|
34
|
+
# Looks through a runnable's parents up to the suite to find a validator with a particular name,
|
35
|
+
# then finds the profile by looking through its defined igs.
|
36
|
+
def find_ig_and_profile(profile_url, validator_name)
|
37
|
+
self.class.find_ig_and_profile(profile_url, validator_name)
|
38
|
+
end
|
39
|
+
|
28
40
|
class Validator
|
29
41
|
attr_reader :requirements
|
30
42
|
attr_accessor :session_id, :name, :test_suite_id
|
@@ -62,7 +74,7 @@ module Inferno
|
|
62
74
|
# igs("hl7.fhir.us.core#3.1.1", "hl7.fhir.us.core#6.0.0")
|
63
75
|
# @param validator_igs [Array<String>]
|
64
76
|
def igs(*validator_igs)
|
65
|
-
cli_context(igs: validator_igs) if validator_igs
|
77
|
+
cli_context(igs: validator_igs) if validator_igs.any?
|
66
78
|
|
67
79
|
cli_context.igs
|
68
80
|
end
|
@@ -193,8 +205,14 @@ module Inferno
|
|
193
205
|
'Error occurred in the validator. Review Messages tab or validator service logs for more information.'
|
194
206
|
end
|
195
207
|
|
208
|
+
# @private
|
209
|
+
def exclude_unresolved_url_message
|
210
|
+
proc { |message| message.message.match?(/\A\S+: [^:]+: URL value '.*' does not resolve/) }
|
211
|
+
end
|
212
|
+
|
196
213
|
# @private
|
197
214
|
def filter_messages(message_hashes)
|
215
|
+
message_hashes.reject! { |message| exclude_unresolved_url_message.call(Entities::Message.new(message)) }
|
198
216
|
message_hashes.reject! { |message| exclude_message.call(Entities::Message.new(message)) } if exclude_message
|
199
217
|
end
|
200
218
|
|
@@ -392,6 +410,20 @@ module Inferno
|
|
392
410
|
|
393
411
|
fhir_validators[name] = current_validators
|
394
412
|
end
|
413
|
+
|
414
|
+
# @private
|
415
|
+
def find_ig_and_profile(profile_url, validator_name)
|
416
|
+
validator = find_validator(validator_name)
|
417
|
+
if validator.is_a? Inferno::DSL::FHIRResourceValidation::Validator
|
418
|
+
validator.igs.each do |ig_id|
|
419
|
+
ig = Inferno::Repositories::IGs.new.find_or_load(ig_id)
|
420
|
+
profile = ig.profile_by_url(profile_url)
|
421
|
+
return ig, profile if profile
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
raise "Unable to find profile #{profile_url} in any IG defined for validator #{validator_name}"
|
426
|
+
end
|
395
427
|
end
|
396
428
|
end
|
397
429
|
end
|
@@ -7,6 +7,7 @@ module Inferno
|
|
7
7
|
# `assert_valid_resource` for validation rather than directly calling
|
8
8
|
# methods on a validator.
|
9
9
|
#
|
10
|
+
# @deprecated Use {Inferno::DSL::FHIRResourceValidation} instead
|
10
11
|
# @example
|
11
12
|
#
|
12
13
|
# validator do
|
@@ -152,8 +153,14 @@ module Inferno
|
|
152
153
|
'Error occurred in the validator. Review Messages tab or validator service logs for more information.'
|
153
154
|
end
|
154
155
|
|
156
|
+
# @private
|
157
|
+
def exclude_unresolved_url_message
|
158
|
+
proc { |message| message.message.match?(/\A\S+: [^:]+: URL value '.*' does not resolve/) }
|
159
|
+
end
|
160
|
+
|
155
161
|
# @private
|
156
162
|
def filter_messages(message_hashes)
|
163
|
+
message_hashes.reject! { |message| exclude_unresolved_url_message.call(Entities::Message.new(message)) }
|
157
164
|
message_hashes.reject! { |message| exclude_message.call(Entities::Message.new(message)) } if exclude_message
|
158
165
|
end
|
159
166
|
|
@@ -243,6 +250,8 @@ module Inferno
|
|
243
250
|
end
|
244
251
|
|
245
252
|
# Define a validator
|
253
|
+
# @deprecated Use
|
254
|
+
# {Inferno::DSL::FHIRResourceValidation::ClassMethods#fhir_resource_validator} instead
|
246
255
|
# @example
|
247
256
|
# validator do
|
248
257
|
# url 'http://example.com/validator'
|
@@ -261,6 +270,10 @@ module Inferno
|
|
261
270
|
# @param required_suite_options [Hash] suite options that must be
|
262
271
|
# selected in order to use this validator
|
263
272
|
def validator(name = :default, required_suite_options: nil, &)
|
273
|
+
Inferno::Application['logger'].warn(
|
274
|
+
"'validator' in '#{suite.id}' TestSuite is deprecated and will be removed in an upcoming release. " \
|
275
|
+
"Use 'fhir_resource_validator' instead."
|
276
|
+
)
|
264
277
|
current_validators = fhir_validators[name] || []
|
265
278
|
|
266
279
|
new_validator = Inferno::DSL::FHIRValidation::Validator.new(required_suite_options, &)
|
@@ -0,0 +1,365 @@
|
|
1
|
+
module Inferno
|
2
|
+
module DSL
|
3
|
+
# The MustSupportAssessment module contains the logic for tests
|
4
|
+
# that check "All Must Support elements are present".
|
5
|
+
# Generally, test authors should use `assert_must_support_elements_present`
|
6
|
+
# or `missing_must_support_elements` DSL methods.
|
7
|
+
# A few additional methods are exposed to support the transition of existing tests that
|
8
|
+
# call into these methods directly.
|
9
|
+
module MustSupportAssessment
|
10
|
+
# Find any Must Support elements defined on the given profile that are missing in the given resources.
|
11
|
+
# Must Support elements are identified on the profile StructureDefinition and pre-parsed into metadata,
|
12
|
+
# which may be customized prior to the check by passing a block. Alternate metadata may be provided directly.
|
13
|
+
# Set test suite config flag debug_must_support_metadata: true to log the metadata to a file for debugging.
|
14
|
+
#
|
15
|
+
# @param resources [Array<FHIR::Resource>]
|
16
|
+
# @param profile_url [String]
|
17
|
+
# @param validator_name [Symbol] Name of the FHIR Validator that references the IG the profile is in
|
18
|
+
# @param metadata [Hash] MustSupport Metadata (optional),
|
19
|
+
# if provided the check will use this instead of re-generating metadata from the profile
|
20
|
+
# @param requirement_extension [String] Extension URL that implies "required" as an alternative to the MS flag
|
21
|
+
# @yield [Metadata] Customize the metadata before running the test
|
22
|
+
# @return [Array<String>] List of missing elements
|
23
|
+
def missing_must_support_elements(resources, profile_url, validator_name: :default, metadata: nil,
|
24
|
+
requirement_extension: nil, &)
|
25
|
+
debug_metadata = config.options[:debug_must_support_metadata]
|
26
|
+
|
27
|
+
if metadata.present?
|
28
|
+
InternalMustSupportLogic.new.perform_must_support_test_with_metadata(resources, metadata, debug_metadata:)
|
29
|
+
else
|
30
|
+
ig, profile = find_ig_and_profile(profile_url, validator_name)
|
31
|
+
perform_must_support_assessment(profile, resources, ig, debug_metadata:, requirement_extension:, &)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# perform_must_support_assessment allows customizing the metadata with a block.
|
36
|
+
# Customizing the metadata may add, modify, or remove items.
|
37
|
+
# For instance, US Core 3.1.1 Patient "Previous Name" is defined as MS only in narrative.
|
38
|
+
# Choices are also defined only in narrative.
|
39
|
+
# @param profile [FHIR::StructureDefinition]
|
40
|
+
# @param resources [Array<FHIR::Model>]
|
41
|
+
# @param ig [Inferno::Entities::IG]
|
42
|
+
# @param debug_metadata [Boolean] if true, write out the final metadata used to a temporary file
|
43
|
+
# @param requirement_extension [String] Extension URL that implies "required" as an alternative to the MS flag
|
44
|
+
# @yield [Metadata] Customize the metadata before running the test
|
45
|
+
# @return [Array<String>] list of elements that were not found in the provided resources
|
46
|
+
def perform_must_support_assessment(profile, resources, ig, debug_metadata: false, requirement_extension: nil)
|
47
|
+
test_impl = InternalMustSupportLogic.new
|
48
|
+
profile_metadata = test_impl.extract_metadata(profile, ig, requirement_extension:)
|
49
|
+
yield profile_metadata if block_given?
|
50
|
+
|
51
|
+
test_impl.perform_must_support_test_with_metadata(resources, profile_metadata, debug_metadata:)
|
52
|
+
end
|
53
|
+
|
54
|
+
def find_missing_elements(resources, must_support_elements)
|
55
|
+
InternalMustSupportLogic.new(metadata).find_missing_elements(resources, must_support_elements)
|
56
|
+
end
|
57
|
+
|
58
|
+
def missing_element_string(element_definition)
|
59
|
+
InternalMustSupportLogic.new.missing_element_string(element_definition)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @private
|
63
|
+
class InternalMustSupportLogic
|
64
|
+
include FHIRResourceNavigation
|
65
|
+
|
66
|
+
attr_accessor :metadata
|
67
|
+
|
68
|
+
def initialize(metadata = nil)
|
69
|
+
@metadata = metadata
|
70
|
+
end
|
71
|
+
|
72
|
+
# perform_must_support_test_with_metadata is invoked from check and perform_must_support_test,
|
73
|
+
# with the metadata to be used as the basis for the test.
|
74
|
+
# It may also be invoked directly from a test if you want to completely overwrite the metadata.
|
75
|
+
# @param resources [Array<FHIR::Model>]
|
76
|
+
# @param profile_metadata [Metadata] Metadata object with must_supports field
|
77
|
+
# @param debug_metadata [Boolean] if true, write out the final metadata used to a temporary file
|
78
|
+
# @return [Array<String>] list of elements that were not found in the provided resources
|
79
|
+
def perform_must_support_test_with_metadata(resources, profile_metadata, debug_metadata: false)
|
80
|
+
return if resources.blank?
|
81
|
+
|
82
|
+
@metadata = profile_metadata
|
83
|
+
|
84
|
+
write_metadata_for_debugging if debug_metadata
|
85
|
+
|
86
|
+
perform_test(resources)
|
87
|
+
end
|
88
|
+
|
89
|
+
def extract_metadata(profile, ig, requirement_extension: nil)
|
90
|
+
MustSupportMetadataExtractor.new(profile.snapshot.element, profile, profile.type, ig, requirement_extension)
|
91
|
+
end
|
92
|
+
|
93
|
+
def write_metadata_for_debugging
|
94
|
+
outfile = "#{metadata.profile&.id}-#{SecureRandom.uuid}.yml"
|
95
|
+
|
96
|
+
File.open(File.join(Dir.tmpdir, outfile), 'w') do |f|
|
97
|
+
writable_metadata = { must_supports: @metadata.must_supports.to_hash }
|
98
|
+
f.write(YAML.dump(writable_metadata))
|
99
|
+
puts "Wrote MustSupport metadata to #{f.path}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def perform_test(resources)
|
104
|
+
missing_elements(resources)
|
105
|
+
missing_slices(resources)
|
106
|
+
missing_extensions(resources)
|
107
|
+
|
108
|
+
handle_must_support_choices if metadata.must_supports[:choices].present?
|
109
|
+
|
110
|
+
missing_must_support_strings
|
111
|
+
end
|
112
|
+
|
113
|
+
def handle_must_support_choices
|
114
|
+
handle_must_support_element_choices
|
115
|
+
handle_must_support_extension_choices
|
116
|
+
handle_must_support_slice_choices
|
117
|
+
end
|
118
|
+
|
119
|
+
def handle_must_support_element_choices
|
120
|
+
missing_elements.delete_if do |element|
|
121
|
+
choices = metadata.must_supports[:choices].find do |choice|
|
122
|
+
choice[:paths]&.include?(element[:path]) ||
|
123
|
+
choice[:elements]&.any? { |ms_element| ms_element[:path] == element[:path] }
|
124
|
+
end
|
125
|
+
any_choice_supported?(choices)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def handle_must_support_extension_choices
|
130
|
+
missing_extensions.delete_if do |extension|
|
131
|
+
choices = metadata.must_supports[:choices].find do |choice|
|
132
|
+
choice[:extension_ids]&.include?(extension[:id])
|
133
|
+
end
|
134
|
+
any_choice_supported?(choices)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def handle_must_support_slice_choices
|
139
|
+
missing_slices.delete_if do |slice|
|
140
|
+
choices = metadata.must_supports[:choices].find { |choice| choice[:slice_names]&.include?(slice[:name]) }
|
141
|
+
any_choice_supported?(choices)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def any_choice_supported?(choices)
|
146
|
+
return false unless choices.present?
|
147
|
+
|
148
|
+
any_path_choice_supported?(choices) ||
|
149
|
+
any_extension_ids_choice_supported?(choices) ||
|
150
|
+
any_slice_names_choice_supported?(choices) ||
|
151
|
+
any_elements_choice_supported?(choices)
|
152
|
+
end
|
153
|
+
|
154
|
+
def any_path_choice_supported?(choices)
|
155
|
+
return false unless choices[:paths].present?
|
156
|
+
|
157
|
+
choices[:paths].any? { |path| missing_elements.none? { |element| element[:path] == path } }
|
158
|
+
end
|
159
|
+
|
160
|
+
def any_extension_ids_choice_supported?(choices)
|
161
|
+
return false unless choices[:extension_ids].present?
|
162
|
+
|
163
|
+
choices[:extension_ids].any? do |extension_id|
|
164
|
+
missing_extensions.none? { |extension| extension[:id] == extension_id }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def any_slice_names_choice_supported?(choices)
|
169
|
+
return false unless choices[:slice_names].present?
|
170
|
+
|
171
|
+
choices[:slice_names].any? { |slice_name| missing_slices.none? { |slice| slice[:name] == slice_name } }
|
172
|
+
end
|
173
|
+
|
174
|
+
def any_elements_choice_supported?(choices)
|
175
|
+
return false unless choices[:elements].present?
|
176
|
+
|
177
|
+
choices[:elements].any? do |choice|
|
178
|
+
missing_elements.none? do |element|
|
179
|
+
element[:path] == choice[:path] && element[:fixed_value] == choice[:fixed_value]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def missing_must_support_strings
|
185
|
+
missing_elements.map { |element_definition| missing_element_string(element_definition) } +
|
186
|
+
missing_slices.map { |slice_definition| slice_definition[:slice_id] } +
|
187
|
+
missing_extensions.map { |extension_definition| extension_definition[:id] }
|
188
|
+
end
|
189
|
+
|
190
|
+
def missing_element_string(element_definition)
|
191
|
+
if element_definition[:fixed_value].present?
|
192
|
+
"#{element_definition[:path]}:#{element_definition[:fixed_value]}"
|
193
|
+
else
|
194
|
+
element_definition[:path]
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def must_support_extensions
|
199
|
+
metadata.must_supports[:extensions]
|
200
|
+
end
|
201
|
+
|
202
|
+
def missing_extensions(resources = [])
|
203
|
+
@missing_extensions ||=
|
204
|
+
must_support_extensions.select do |extension_definition|
|
205
|
+
resources.none? do |resource|
|
206
|
+
path = extension_definition[:path]
|
207
|
+
|
208
|
+
if path == 'extension'
|
209
|
+
resource.extension.any? { |extension| extension.url == extension_definition[:url] }
|
210
|
+
else
|
211
|
+
extension = find_a_value_at(resource, path) do |el|
|
212
|
+
el.url == extension_definition[:url]
|
213
|
+
end
|
214
|
+
|
215
|
+
extension.present?
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def must_support_elements
|
222
|
+
metadata.must_supports[:elements]
|
223
|
+
end
|
224
|
+
|
225
|
+
def missing_elements(resources = [])
|
226
|
+
@missing_elements ||= find_missing_elements(resources, must_support_elements)
|
227
|
+
end
|
228
|
+
|
229
|
+
def find_missing_elements(resources, must_support_elements)
|
230
|
+
must_support_elements.select do |element_definition|
|
231
|
+
resources.none? { |resource| resource_populates_element?(resource, element_definition) }
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def resource_populates_element?(resource, element_definition)
|
236
|
+
path = element_definition[:path]
|
237
|
+
ms_extension_urls = must_support_extensions.select { |ex| ex[:path] == "#{path}.extension" }
|
238
|
+
.map { |ex| ex[:url] }
|
239
|
+
|
240
|
+
value_found = find_a_value_at(resource, path) do |potential_value|
|
241
|
+
matching_without_extensions?(potential_value, ms_extension_urls, element_definition[:fixed_value])
|
242
|
+
end
|
243
|
+
|
244
|
+
# Note that false.present? => false, which is why we need to add this extra check
|
245
|
+
value_found.present? || value_found == false
|
246
|
+
end
|
247
|
+
|
248
|
+
def matching_without_extensions?(value, ms_extension_urls, fixed_value)
|
249
|
+
if value.instance_of?(Inferno::DSL::PrimitiveType)
|
250
|
+
urls = value.extension&.map(&:url)
|
251
|
+
has_ms_extension = (urls & ms_extension_urls).present?
|
252
|
+
value = value.value
|
253
|
+
end
|
254
|
+
|
255
|
+
return false unless has_ms_extension || value_without_extensions?(value)
|
256
|
+
|
257
|
+
matches_fixed_value?(value, fixed_value)
|
258
|
+
end
|
259
|
+
|
260
|
+
def matches_fixed_value?(value, fixed_value)
|
261
|
+
fixed_value.blank? || value == fixed_value
|
262
|
+
end
|
263
|
+
|
264
|
+
def value_without_extensions?(value)
|
265
|
+
value_without_extensions = value.respond_to?(:to_hash) ? value.to_hash.except('extension') : value
|
266
|
+
value_without_extensions.present? || value_without_extensions == false
|
267
|
+
end
|
268
|
+
|
269
|
+
def must_support_slices
|
270
|
+
metadata.must_supports[:slices]
|
271
|
+
end
|
272
|
+
|
273
|
+
def missing_slices(resources = [])
|
274
|
+
@missing_slices ||=
|
275
|
+
must_support_slices.select do |slice|
|
276
|
+
resources.none? do |resource|
|
277
|
+
path = slice[:path]
|
278
|
+
find_slice(resource, path, slice[:discriminator]).present?
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def find_slice(resource, path, discriminator)
|
284
|
+
# TODO: there is a lot of similarity
|
285
|
+
# between this and FHIRResourceNavigation.matching_slice?
|
286
|
+
# Can these be combined?
|
287
|
+
find_a_value_at(resource, path) do |element|
|
288
|
+
case discriminator[:type]
|
289
|
+
when 'patternCodeableConcept'
|
290
|
+
find_pattern_codeable_concept_slice(element, discriminator)
|
291
|
+
when 'patternCoding'
|
292
|
+
find_pattern_coding_slice(element, discriminator)
|
293
|
+
when 'patternIdentifier'
|
294
|
+
find_pattern_identifier_slice(element, discriminator)
|
295
|
+
when 'value'
|
296
|
+
find_value_slice(element, discriminator)
|
297
|
+
when 'type'
|
298
|
+
find_type_slice(element, discriminator)
|
299
|
+
when 'requiredBinding'
|
300
|
+
find_required_binding_slice(element, discriminator)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def find_pattern_codeable_concept_slice(element, discriminator)
|
306
|
+
coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
|
307
|
+
find_a_value_at(element, coding_path) do |coding|
|
308
|
+
coding.code == discriminator[:code] && coding.system == discriminator[:system]
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def find_pattern_coding_slice(element, discriminator)
|
313
|
+
coding_path = discriminator[:path].present? ? discriminator[:path] : ''
|
314
|
+
find_a_value_at(element, coding_path) do |coding|
|
315
|
+
coding.code == discriminator[:code] && coding.system == discriminator[:system]
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def find_pattern_identifier_slice(element, discriminator)
|
320
|
+
find_a_value_at(element, discriminator[:path]) do |identifier|
|
321
|
+
identifier.system == discriminator[:system]
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def find_value_slice(element, discriminator)
|
326
|
+
values = discriminator[:values].map { |value| value.merge(path: value[:path].split('.')) }
|
327
|
+
find_slice_by_values(element, values)
|
328
|
+
end
|
329
|
+
|
330
|
+
def find_type_slice(element, discriminator)
|
331
|
+
case discriminator[:code]
|
332
|
+
when 'Date'
|
333
|
+
begin
|
334
|
+
Date.parse(element)
|
335
|
+
rescue ArgumentError
|
336
|
+
false
|
337
|
+
end
|
338
|
+
when 'DateTime'
|
339
|
+
begin
|
340
|
+
DateTime.parse(element)
|
341
|
+
rescue ArgumentError
|
342
|
+
false
|
343
|
+
end
|
344
|
+
when 'String'
|
345
|
+
element.is_a? String
|
346
|
+
else
|
347
|
+
element.is_a? FHIR.const_get(discriminator[:code])
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
def find_required_binding_slice(element, discriminator)
|
352
|
+
coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
|
353
|
+
|
354
|
+
find_a_value_at(element, coding_path) do |coding|
|
355
|
+
discriminator[:values].any? { |value| value[:system] == coding.system && value[:code] == coding.code }
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def find_slice_by_values(element, value_definitions)
|
360
|
+
Array.wrap(element).find { |el| verify_slice_by_values(el, value_definitions) }
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
data/lib/inferno/dsl/results.rb
CHANGED
@@ -20,12 +20,28 @@ module Inferno
|
|
20
20
|
raise Exceptions::PassException, message if test
|
21
21
|
end
|
22
22
|
|
23
|
-
# Halt execution of the current test and mark it as skipped.
|
23
|
+
# Halt execution of the current test and mark it as skipped. This method
|
24
|
+
# can also take a block with an assertion, and if the assertion fails, the
|
25
|
+
# test will skip rather than fail. The message parameter is ignored if a
|
26
|
+
# block is provided.
|
24
27
|
#
|
25
28
|
# @param message [String]
|
26
29
|
# @return [void]
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# if some_precondition_not_met?
|
33
|
+
# skip('Some precondition was not met.')
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# skip do
|
37
|
+
# assert false, 'This test will skip rather than fail'
|
38
|
+
# end
|
27
39
|
def skip(message = '')
|
28
|
-
raise Exceptions::SkipException, message
|
40
|
+
raise Exceptions::SkipException, message unless block_given?
|
41
|
+
|
42
|
+
yield
|
43
|
+
rescue Exceptions::AssertionException => e
|
44
|
+
raise Exceptions::SkipException, e.message
|
29
45
|
end
|
30
46
|
|
31
47
|
# Halt execution of the current test and mark it as skipped if a condition
|
@@ -38,12 +54,28 @@ module Inferno
|
|
38
54
|
raise Exceptions::SkipException, message if test
|
39
55
|
end
|
40
56
|
|
41
|
-
# Halt execution of the current test and mark it as omitted.
|
57
|
+
# Halt execution of the current test and mark it as omitted. This method
|
58
|
+
# can also take a block with an assertion, and if the assertion fails, the
|
59
|
+
# test will omit rather than fail. The message parameter is ignored if a
|
60
|
+
# block is provided.
|
42
61
|
#
|
43
62
|
# @param message [String]
|
44
63
|
# @return [void]
|
64
|
+
#
|
65
|
+
# @example
|
66
|
+
# if behavior_does_not_need_to_be_tested?
|
67
|
+
# omit('Behavior does not need to be tested.')
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# omit do
|
71
|
+
# assert false, 'This test will omit rather than fail'
|
72
|
+
# end
|
45
73
|
def omit(message = '')
|
46
|
-
raise Exceptions::OmitException, message
|
74
|
+
raise Exceptions::OmitException, message unless block_given?
|
75
|
+
|
76
|
+
yield
|
77
|
+
rescue Exceptions::AssertionException => e
|
78
|
+
raise Exceptions::OmitException, e.message
|
47
79
|
end
|
48
80
|
|
49
81
|
# Halt execution of the current test and mark it as omitted if a condition
|
data/lib/inferno/dsl/runnable.rb
CHANGED
@@ -81,6 +81,12 @@ module Inferno
|
|
81
81
|
repository.insert(self)
|
82
82
|
end
|
83
83
|
|
84
|
+
# @private
|
85
|
+
def remove_self_from_repository
|
86
|
+
repository.remove(self)
|
87
|
+
children.each(&:remove_self_from_repository)
|
88
|
+
end
|
89
|
+
|
84
90
|
# An instance of the repository for the class using this module
|
85
91
|
# @private
|
86
92
|
def repository
|
@@ -464,6 +470,71 @@ module Inferno
|
|
464
470
|
end
|
465
471
|
end
|
466
472
|
|
473
|
+
# Move a child test/group to a new position within the children list.
|
474
|
+
#
|
475
|
+
# @param child_id [Symbol, String] The ID of the child to be moved.
|
476
|
+
# @param new_index [Integer] The new position for the child.
|
477
|
+
# @example
|
478
|
+
# reorder(:test3, 1) # Moves `test3` to index 1
|
479
|
+
#
|
480
|
+
def reorder(child_id, new_index)
|
481
|
+
index = children.find_index { |child| child.id.to_s.end_with? child_id.to_s }
|
482
|
+
raise Exceptions::RunnableChildNotFoundException.new(child_id, self) unless index
|
483
|
+
|
484
|
+
unless new_index.between?(0, children.length - 1)
|
485
|
+
Inferno::Application[:logger].error <<~ERROR
|
486
|
+
Error trying to reorder children for #{self}:
|
487
|
+
new_index #{new_index} for #{child_id} is out of range
|
488
|
+
(must be between 0 and #{children.length - 1})
|
489
|
+
ERROR
|
490
|
+
return
|
491
|
+
end
|
492
|
+
|
493
|
+
child = children.delete_at(index)
|
494
|
+
children.insert(new_index, child)
|
495
|
+
end
|
496
|
+
|
497
|
+
# Replace a child test/group
|
498
|
+
#
|
499
|
+
# @param id_to_replace [Symbol, String] The ID of the child to be replaced.
|
500
|
+
# @param replacement_id [Symbol, String] The global ID of the group/test that will take the
|
501
|
+
# place of the child being replaced.
|
502
|
+
# @yield [Inferno::TestGroup, Inferno::Test] Optional block executed in the
|
503
|
+
# context of the replacement child for additional configuration.
|
504
|
+
# @example
|
505
|
+
# replace :test2, :test4 do
|
506
|
+
# id :new_test_id
|
507
|
+
# config(...)
|
508
|
+
# end
|
509
|
+
def replace(id_to_replace, replacement_id, &)
|
510
|
+
index = children.find_index { |child| child.id.to_s.end_with? id_to_replace.to_s }
|
511
|
+
raise Exceptions::RunnableChildNotFoundException.new(id_to_replace, self) unless index
|
512
|
+
|
513
|
+
if children[index] < Inferno::TestGroup
|
514
|
+
group(from: replacement_id, &)
|
515
|
+
else
|
516
|
+
test(from: replacement_id, &)
|
517
|
+
end
|
518
|
+
|
519
|
+
remove(id_to_replace)
|
520
|
+
children.insert(index, children.pop)
|
521
|
+
end
|
522
|
+
|
523
|
+
# Remove a child test/group
|
524
|
+
#
|
525
|
+
# @param id_to_remove [Symbol, String]
|
526
|
+
# @example
|
527
|
+
# test from: :test1
|
528
|
+
# test from: :test2
|
529
|
+
# test from: :test3
|
530
|
+
#
|
531
|
+
# remove :test2
|
532
|
+
def remove(id_to_remove)
|
533
|
+
removed = children.select { |child| child.id.to_s.end_with? id_to_remove.to_s }
|
534
|
+
children.reject! { |child| child.id.to_s.end_with? id_to_remove.to_s }
|
535
|
+
removed.each(&:remove_self_from_repository)
|
536
|
+
end
|
537
|
+
|
467
538
|
# @private
|
468
539
|
def children(selected_suite_options = [])
|
469
540
|
return all_children if selected_suite_options.blank?
|
data/lib/inferno/dsl.rb
CHANGED
@@ -6,6 +6,7 @@ require_relative 'dsl/fhir_evaluation/evaluator'
|
|
6
6
|
require_relative 'dsl/fhir_resource_validation'
|
7
7
|
require_relative 'dsl/fhirpath_evaluation'
|
8
8
|
require_relative 'dsl/http_client'
|
9
|
+
require_relative 'dsl/must_support_assessment'
|
9
10
|
require_relative 'dsl/results'
|
10
11
|
require_relative 'dsl/runnable'
|
11
12
|
require_relative 'dsl/suite_endpoint'
|
@@ -23,7 +24,8 @@ module Inferno
|
|
23
24
|
FHIREvaluation,
|
24
25
|
FHIRResourceValidation,
|
25
26
|
FhirpathEvaluation,
|
26
|
-
Messages
|
27
|
+
Messages,
|
28
|
+
MustSupportAssessment
|
27
29
|
].freeze
|
28
30
|
|
29
31
|
EXTENDABLE_DSL_MODULES = [
|
data/lib/inferno/entities/ig.rb
CHANGED
@@ -15,7 +15,8 @@ module Inferno
|
|
15
15
|
ATTRIBUTES = [
|
16
16
|
:id,
|
17
17
|
:resources_by_type,
|
18
|
-
:examples
|
18
|
+
:examples,
|
19
|
+
:source_path
|
19
20
|
].freeze
|
20
21
|
|
21
22
|
include Inferno::Entities::Attributes
|
@@ -66,6 +67,7 @@ module Inferno
|
|
66
67
|
end
|
67
68
|
|
68
69
|
ig.id = extract_package_id(ig.ig_resource)
|
70
|
+
ig.source_path = ig_path
|
69
71
|
|
70
72
|
ig
|
71
73
|
end
|
@@ -89,6 +91,7 @@ module Inferno
|
|
89
91
|
end
|
90
92
|
|
91
93
|
ig.id = extract_package_id(ig.ig_resource)
|
94
|
+
ig.source_path = ig_directory
|
92
95
|
|
93
96
|
ig
|
94
97
|
end
|
data/lib/inferno/exceptions.rb
CHANGED
@@ -125,5 +125,11 @@ module Inferno
|
|
125
125
|
super("ID '#{id}' already exists. Ensure the uniqueness of the IDs.")
|
126
126
|
end
|
127
127
|
end
|
128
|
+
|
129
|
+
class RunnableChildNotFoundException < StandardError
|
130
|
+
def initialize(id, runnable)
|
131
|
+
super("Could not find a child with an ID ending in '#{id}' for '#{runnable}'.")
|
132
|
+
end
|
133
|
+
end
|
128
134
|
end
|
129
135
|
end
|