inferno_core 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/inferno/apps/cli/evaluate.rb +22 -12
- data/lib/inferno/config/boot/presets.rb +1 -1
- data/lib/inferno/dsl/fhir_client.rb +66 -0
- data/lib/inferno/dsl/fhir_evaluation/evaluation_context.rb +4 -2
- data/lib/inferno/dsl/fhir_evaluation/evaluator.rb +8 -3
- data/lib/inferno/dsl/fhir_evaluation/profile_conformance_helper.rb +66 -0
- data/lib/inferno/dsl/fhir_evaluation/reference_extractor.rb +61 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_must_supports_present.rb +379 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_references_resolve.rb +53 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_resources_reachable.rb +63 -0
- data/lib/inferno/dsl/fhir_resource_navigation.rb +226 -0
- data/lib/inferno/dsl/must_support_metadata_extractor.rb +366 -0
- data/lib/inferno/dsl/primitive_type.rb +9 -0
- data/lib/inferno/dsl/value_extractor.rb +136 -0
- data/lib/inferno/entities/ig.rb +46 -24
- data/lib/inferno/public/bundle.js +16 -16
- data/lib/inferno/version.rb +1 -1
- data/spec/shared/test_kit_examples.rb +23 -1
- metadata +11 -2
@@ -0,0 +1,379 @@
|
|
1
|
+
require_relative '../../fhir_resource_navigation'
|
2
|
+
require_relative '../../must_support_metadata_extractor'
|
3
|
+
require_relative '../profile_conformance_helper'
|
4
|
+
|
5
|
+
module Inferno
|
6
|
+
module DSL
|
7
|
+
module FHIREvaluation
|
8
|
+
module Rules
|
9
|
+
# AllMustSupportsPresent checks that at least one instance of every MustSupport element
|
10
|
+
# defined in the given profiles is populated in the given data.
|
11
|
+
# MustSupport elements include plain elements, extensions, and slices.
|
12
|
+
# The basis of the test is metadata generated in a first pass that processes the profile into a list of fields,
|
13
|
+
# then the second pass check that all elements in the list are present.
|
14
|
+
# This metadata approach allows for customizing what is checked, for example elements may be added or removed,
|
15
|
+
# or choices may be defined where only one choice of multiple must be populated to demonstrate support.
|
16
|
+
class AllMustSupportsPresent < Rule
|
17
|
+
include FHIRResourceNavigation
|
18
|
+
include ProfileConformanceHelper
|
19
|
+
attr_accessor :metadata
|
20
|
+
|
21
|
+
# check is invoked from the evaluator CLI and applies the logic for this Rule to the provided data.
|
22
|
+
# At least one instance of every MustSupport element defined in the profiles must be populated in the data.
|
23
|
+
# Findings from the rule will be added to context.results.
|
24
|
+
# The logic is configurable with a few options, but this method does not support customizing the metadata.
|
25
|
+
# @param context [Inferno::DSL::FHIREvaluation::EvaluationContext]
|
26
|
+
# @return [void]
|
27
|
+
def check(context)
|
28
|
+
missing_items_by_profile = {}
|
29
|
+
context.ig.profiles.each do |profile|
|
30
|
+
resources = pick_resources_for_profile(profile, context)
|
31
|
+
if resources.blank?
|
32
|
+
missing_items_by_profile[profile.url] = ['No matching resources were found to check']
|
33
|
+
next
|
34
|
+
end
|
35
|
+
requirement_extension = context.config.data['Rule']['AllMustSupportsPresent']['RequirementExtensionUrl']
|
36
|
+
debug_metadata = context.config.data['Rule']['AllMustSupportsPresent']['WriteMetadataForDebugging']
|
37
|
+
profile_metadata = extract_metadata(profile, context.ig, requirement_extension:)
|
38
|
+
missing_items = perform_must_support_test_with_metadata(resources, profile_metadata, debug_metadata:)
|
39
|
+
|
40
|
+
missing_items_by_profile[profile.url] = missing_items if missing_items.any?
|
41
|
+
end
|
42
|
+
|
43
|
+
if missing_items_by_profile.count.zero?
|
44
|
+
result = EvaluationResult.new('All MustSupports are present', severity: 'success', rule: self)
|
45
|
+
else
|
46
|
+
message = 'Found Profiles with not all MustSupports represented:'
|
47
|
+
missing_items_by_profile.each do |profile_url, missing_items|
|
48
|
+
message += "\n\t\t#{profile_url}: #{missing_items.join(', ')}"
|
49
|
+
end
|
50
|
+
result = EvaluationResult.new(message, rule: self)
|
51
|
+
end
|
52
|
+
context.add_result result
|
53
|
+
end
|
54
|
+
|
55
|
+
def pick_resources_for_profile(profile, context)
|
56
|
+
conformance_options = context.config.data['Rule']['AllMustSupportsPresent']['ConformanceOptions'].to_options
|
57
|
+
|
58
|
+
# Unless specifically looking for Bundles, break them out into the resources they include
|
59
|
+
all_resources =
|
60
|
+
if profile.type == 'Bundle'
|
61
|
+
context.data
|
62
|
+
else
|
63
|
+
flatten_bundles(context.data)
|
64
|
+
end
|
65
|
+
|
66
|
+
all_resources.filter do |r|
|
67
|
+
conforms_to_profile?(r, profile, conformance_options, context.validator)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# perform_must_support_test is invoked from DSL assertions, allows customizing the metadata with a block.
|
72
|
+
# Customizing the metadata may add, modify, or remove items.
|
73
|
+
# For instance, US Core 3.1.1 Patient "Previous Name" is defined as MS only in narrative.
|
74
|
+
# Choices are also defined only in narrative.
|
75
|
+
# @param profile [FHIR::StructureDefinition]
|
76
|
+
# @param resources [Array<FHIR::Model>]
|
77
|
+
# @param ig [Inferno::Entities::IG]
|
78
|
+
# @param debug_metadata [Boolean] if true, write out the final metadata used to a temporary file
|
79
|
+
# @param requirement_extension [String] Extension URL that implies "required" as an alternative to the MS flag
|
80
|
+
# @yield [Metadata] Customize the metadata before running the test
|
81
|
+
# @return [Array<String>] list of elements that were not found in the provided resources
|
82
|
+
def perform_must_support_test(profile, resources, ig, debug_metadata: false, requirement_extension: nil)
|
83
|
+
profile_metadata = extract_metadata(profile, ig, requirement_extension:)
|
84
|
+
yield profile_metadata if block_given?
|
85
|
+
|
86
|
+
perform_must_support_test_with_metadata(resources, profile_metadata, debug_metadata:)
|
87
|
+
end
|
88
|
+
|
89
|
+
# perform_must_support_test_with_metadata is invoked from check and perform_must_support_test,
|
90
|
+
# with the metadata to be used as the basis for the test.
|
91
|
+
# It may also be invoked directly from a test if you want to completely overwrite the metadata.
|
92
|
+
# @param resources [Array<FHIR::Model>]
|
93
|
+
# @param profile_metadata [Metadata] Metadata object with must_supports field
|
94
|
+
# @param debug_metadata [Boolean] if true, write out the final metadata used to a temporary file
|
95
|
+
# @return [Array<String>] list of elements that were not found in the provided resources
|
96
|
+
def perform_must_support_test_with_metadata(resources, profile_metadata, debug_metadata: false)
|
97
|
+
return if resources.blank?
|
98
|
+
|
99
|
+
@metadata = profile_metadata
|
100
|
+
|
101
|
+
write_metadata_for_debugging if debug_metadata
|
102
|
+
|
103
|
+
missing_elements(resources)
|
104
|
+
missing_slices(resources)
|
105
|
+
missing_extensions(resources)
|
106
|
+
|
107
|
+
handle_must_support_choices if metadata.must_supports[:choices].present?
|
108
|
+
|
109
|
+
missing_must_support_strings
|
110
|
+
end
|
111
|
+
|
112
|
+
def extract_metadata(profile, ig, requirement_extension: nil)
|
113
|
+
MustSupportMetadataExtractor.new(profile.snapshot.element, profile, profile.type, ig, requirement_extension)
|
114
|
+
end
|
115
|
+
|
116
|
+
def write_metadata_for_debugging
|
117
|
+
outfile = "#{metadata.profile&.id}-#{SecureRandom.uuid}.yml"
|
118
|
+
|
119
|
+
File.open(File.join(Dir.tmpdir, outfile), 'w') do |f|
|
120
|
+
writable_metadata = { must_supports: @metadata.must_supports.to_hash }
|
121
|
+
f.write(YAML.dump(writable_metadata))
|
122
|
+
puts "Wrote MustSupport metadata to #{f.path}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def handle_must_support_choices
|
127
|
+
handle_must_support_element_choices
|
128
|
+
handle_must_support_extension_choices
|
129
|
+
handle_must_support_slice_choices
|
130
|
+
end
|
131
|
+
|
132
|
+
def handle_must_support_element_choices
|
133
|
+
missing_elements.delete_if do |element|
|
134
|
+
choices = metadata.must_supports[:choices].find do |choice|
|
135
|
+
choice[:paths]&.include?(element[:path]) ||
|
136
|
+
choice[:elements]&.any? { |ms_element| ms_element[:path] == element[:path] }
|
137
|
+
end
|
138
|
+
any_choice_supported?(choices)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def handle_must_support_extension_choices
|
143
|
+
missing_extensions.delete_if do |extension|
|
144
|
+
choices = metadata.must_supports[:choices].find do |choice|
|
145
|
+
choice[:extension_ids]&.include?(extension[:id])
|
146
|
+
end
|
147
|
+
any_choice_supported?(choices)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def handle_must_support_slice_choices
|
152
|
+
missing_slices.delete_if do |slice|
|
153
|
+
choices = metadata.must_supports[:choices].find { |choice| choice[:slice_names]&.include?(slice[:name]) }
|
154
|
+
any_choice_supported?(choices)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def any_choice_supported?(choices)
|
159
|
+
return false unless choices.present?
|
160
|
+
|
161
|
+
any_path_choice_supported?(choices) ||
|
162
|
+
any_extension_ids_choice_supported?(choices) ||
|
163
|
+
any_slice_names_choice_supported?(choices) ||
|
164
|
+
any_elements_choice_supported?(choices)
|
165
|
+
end
|
166
|
+
|
167
|
+
def any_path_choice_supported?(choices)
|
168
|
+
return false unless choices[:paths].present?
|
169
|
+
|
170
|
+
choices[:paths].any? { |path| missing_elements.none? { |element| element[:path] == path } }
|
171
|
+
end
|
172
|
+
|
173
|
+
def any_extension_ids_choice_supported?(choices)
|
174
|
+
return false unless choices[:extension_ids].present?
|
175
|
+
|
176
|
+
choices[:extension_ids].any? do |extension_id|
|
177
|
+
missing_extensions.none? { |extension| extension[:id] == extension_id }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def any_slice_names_choice_supported?(choices)
|
182
|
+
return false unless choices[:slice_names].present?
|
183
|
+
|
184
|
+
choices[:slice_names].any? { |slice_name| missing_slices.none? { |slice| slice[:name] == slice_name } }
|
185
|
+
end
|
186
|
+
|
187
|
+
def any_elements_choice_supported?(choices)
|
188
|
+
return false unless choices[:elements].present?
|
189
|
+
|
190
|
+
choices[:elements].any? do |choice|
|
191
|
+
missing_elements.none? do |element|
|
192
|
+
element[:path] == choice[:path] && element[:fixed_value] == choice[:fixed_value]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def missing_must_support_strings
|
198
|
+
missing_elements.map { |element_definition| missing_element_string(element_definition) } +
|
199
|
+
missing_slices.map { |slice_definition| slice_definition[:slice_id] } +
|
200
|
+
missing_extensions.map { |extension_definition| extension_definition[:id] }
|
201
|
+
end
|
202
|
+
|
203
|
+
def missing_element_string(element_definition)
|
204
|
+
if element_definition[:fixed_value].present?
|
205
|
+
"#{element_definition[:path]}:#{element_definition[:fixed_value]}"
|
206
|
+
else
|
207
|
+
element_definition[:path]
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def must_support_extensions
|
212
|
+
metadata.must_supports[:extensions]
|
213
|
+
end
|
214
|
+
|
215
|
+
def missing_extensions(resources = [])
|
216
|
+
@missing_extensions ||=
|
217
|
+
must_support_extensions.select do |extension_definition|
|
218
|
+
resources.none? do |resource|
|
219
|
+
path = extension_definition[:path]
|
220
|
+
|
221
|
+
if path == 'extension'
|
222
|
+
resource.extension.any? { |extension| extension.url == extension_definition[:url] }
|
223
|
+
else
|
224
|
+
extension = find_a_value_at(resource, path) do |el|
|
225
|
+
el.url == extension_definition[:url]
|
226
|
+
end
|
227
|
+
|
228
|
+
extension.present?
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def must_support_elements
|
235
|
+
metadata.must_supports[:elements]
|
236
|
+
end
|
237
|
+
|
238
|
+
def missing_elements(resources = [])
|
239
|
+
@missing_elements ||= find_missing_elements(resources, must_support_elements)
|
240
|
+
end
|
241
|
+
|
242
|
+
def find_missing_elements(resources, must_support_elements)
|
243
|
+
must_support_elements.select do |element_definition|
|
244
|
+
resources.none? { |resource| resource_populates_element?(resource, element_definition) }
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def resource_populates_element?(resource, element_definition)
|
249
|
+
path = element_definition[:path]
|
250
|
+
ms_extension_urls = must_support_extensions.select { |ex| ex[:path] == "#{path}.extension" }
|
251
|
+
.map { |ex| ex[:url] }
|
252
|
+
|
253
|
+
value_found = find_a_value_at(resource, path) do |potential_value|
|
254
|
+
matching_without_extensions?(potential_value, ms_extension_urls, element_definition[:fixed_value])
|
255
|
+
end
|
256
|
+
|
257
|
+
# Note that false.present? => false, which is why we need to add this extra check
|
258
|
+
value_found.present? || value_found == false
|
259
|
+
end
|
260
|
+
|
261
|
+
def matching_without_extensions?(value, ms_extension_urls, fixed_value)
|
262
|
+
if value.instance_of?(Inferno::DSL::PrimitiveType)
|
263
|
+
urls = value.extension&.map(&:url)
|
264
|
+
has_ms_extension = (urls & ms_extension_urls).present?
|
265
|
+
value = value.value
|
266
|
+
end
|
267
|
+
|
268
|
+
return false unless has_ms_extension || value_without_extensions?(value)
|
269
|
+
|
270
|
+
matches_fixed_value?(value, fixed_value)
|
271
|
+
end
|
272
|
+
|
273
|
+
def matches_fixed_value?(value, fixed_value)
|
274
|
+
fixed_value.blank? || value == fixed_value
|
275
|
+
end
|
276
|
+
|
277
|
+
def value_without_extensions?(value)
|
278
|
+
value_without_extensions = value.respond_to?(:to_hash) ? value.to_hash.except('extension') : value
|
279
|
+
value_without_extensions.present? || value_without_extensions == false
|
280
|
+
end
|
281
|
+
|
282
|
+
def must_support_slices
|
283
|
+
metadata.must_supports[:slices]
|
284
|
+
end
|
285
|
+
|
286
|
+
def missing_slices(resources = [])
|
287
|
+
@missing_slices ||=
|
288
|
+
must_support_slices.select do |slice|
|
289
|
+
resources.none? do |resource|
|
290
|
+
path = slice[:path]
|
291
|
+
find_slice(resource, path, slice[:discriminator]).present?
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def find_slice(resource, path, discriminator)
|
297
|
+
# TODO: there is a lot of similarity
|
298
|
+
# between this and FHIRResourceNavigation.matching_slice?
|
299
|
+
# Can these be combined?
|
300
|
+
find_a_value_at(resource, path) do |element|
|
301
|
+
case discriminator[:type]
|
302
|
+
when 'patternCodeableConcept'
|
303
|
+
find_pattern_codeable_concept_slice(element, discriminator)
|
304
|
+
when 'patternCoding'
|
305
|
+
find_pattern_coding_slice(element, discriminator)
|
306
|
+
when 'patternIdentifier'
|
307
|
+
find_pattern_identifier_slice(element, discriminator)
|
308
|
+
when 'value'
|
309
|
+
find_value_slice(element, discriminator)
|
310
|
+
when 'type'
|
311
|
+
find_type_slice(element, discriminator)
|
312
|
+
when 'requiredBinding'
|
313
|
+
find_required_binding_slice(element, discriminator)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def find_pattern_codeable_concept_slice(element, discriminator)
|
319
|
+
coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
|
320
|
+
find_a_value_at(element, coding_path) do |coding|
|
321
|
+
coding.code == discriminator[:code] && coding.system == discriminator[:system]
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def find_pattern_coding_slice(element, discriminator)
|
326
|
+
coding_path = discriminator[:path].present? ? discriminator[:path] : ''
|
327
|
+
find_a_value_at(element, coding_path) do |coding|
|
328
|
+
coding.code == discriminator[:code] && coding.system == discriminator[:system]
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
def find_pattern_identifier_slice(element, discriminator)
|
333
|
+
find_a_value_at(element, discriminator[:path]) do |identifier|
|
334
|
+
identifier.system == discriminator[:system]
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def find_value_slice(element, discriminator)
|
339
|
+
values = discriminator[:values].map { |value| value.merge(path: value[:path].split('.')) }
|
340
|
+
find_slice_by_values(element, values)
|
341
|
+
end
|
342
|
+
|
343
|
+
def find_type_slice(element, discriminator)
|
344
|
+
case discriminator[:code]
|
345
|
+
when 'Date'
|
346
|
+
begin
|
347
|
+
Date.parse(element)
|
348
|
+
rescue ArgumentError
|
349
|
+
false
|
350
|
+
end
|
351
|
+
when 'DateTime'
|
352
|
+
begin
|
353
|
+
DateTime.parse(element)
|
354
|
+
rescue ArgumentError
|
355
|
+
false
|
356
|
+
end
|
357
|
+
when 'String'
|
358
|
+
element.is_a? String
|
359
|
+
else
|
360
|
+
element.is_a? FHIR.const_get(discriminator[:code])
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
def find_required_binding_slice(element, discriminator)
|
365
|
+
coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
|
366
|
+
|
367
|
+
find_a_value_at(element, coding_path) do |coding|
|
368
|
+
discriminator[:values].any? { |value| value[:system] == coding.system && value[:code] == coding.code }
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def find_slice_by_values(element, value_definitions)
|
373
|
+
Array.wrap(element).find { |el| verify_slice_by_values(el, value_definitions) }
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../reference_extractor'
|
4
|
+
|
5
|
+
module Inferno
|
6
|
+
module DSL
|
7
|
+
module FHIREvaluation
|
8
|
+
module Rules
|
9
|
+
class AllReferencesResolve < Rule
|
10
|
+
def check(context)
|
11
|
+
extractor = Inferno::DSL::FHIREvaluation::ReferenceExtractor.new
|
12
|
+
resource_type_ids = extractor.extract_resource_type_ids(context.data)
|
13
|
+
resource_ids = Set.new(resource_type_ids.values.flatten.uniq)
|
14
|
+
reference_map = extractor.extract_references(context.data)
|
15
|
+
|
16
|
+
unresolved_references = Hash.new { |reference, id| reference[id] = [] }
|
17
|
+
|
18
|
+
reference_map.each do |resource_id, references|
|
19
|
+
references.each do |reference|
|
20
|
+
if reference[:type] == ''
|
21
|
+
unresolved_references[resource_id] << reference unless resource_ids.include?(reference[:id])
|
22
|
+
elsif !resource_type_ids[reference[:type]].include?(reference[:id])
|
23
|
+
unresolved_references[resource_id] << reference
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
if unresolved_references.any?
|
29
|
+
message = gen_reference_fail_message(unresolved_references)
|
30
|
+
result = EvaluationResult.new(message, rule: self)
|
31
|
+
else
|
32
|
+
message = 'All references resolve'
|
33
|
+
result = EvaluationResult.new(message, severity: 'success', rule: self)
|
34
|
+
end
|
35
|
+
|
36
|
+
context.add_result result
|
37
|
+
end
|
38
|
+
|
39
|
+
def gen_reference_fail_message(unresolved_references)
|
40
|
+
result_message = unresolved_references.map do |resource_id, references|
|
41
|
+
reference_detail = references.map do |reference|
|
42
|
+
" \n\tpath: #{reference[:path]}, type: #{reference[:type]}, id: #{reference[:id]}"
|
43
|
+
end.join(',')
|
44
|
+
"\n Resource (id): #{resource_id} #{reference_detail}"
|
45
|
+
end.join(',')
|
46
|
+
|
47
|
+
"Found unresolved references: #{result_message}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../reference_extractor'
|
4
|
+
|
5
|
+
module Inferno
|
6
|
+
module DSL
|
7
|
+
module FHIREvaluation
|
8
|
+
module Rules
|
9
|
+
class AllResourcesReachable < Rule
|
10
|
+
attr_accessor :config, :referenced_resources, :referencing_resources, :resource_ids, :resource_type_ids
|
11
|
+
|
12
|
+
def check(context)
|
13
|
+
@config = context.config
|
14
|
+
@referenced_resources = Set.new
|
15
|
+
@referencing_resources = Set.new
|
16
|
+
|
17
|
+
extractor = Inferno::DSL::FHIREvaluation::ReferenceExtractor.new
|
18
|
+
@resource_type_ids = extractor.extract_resource_type_ids(context.data)
|
19
|
+
@resource_ids = Set.new(resource_type_ids.values.flatten.uniq)
|
20
|
+
reference_map = extractor.extract_references(context.data)
|
21
|
+
|
22
|
+
reference_map.each do |resource_id, references|
|
23
|
+
assess_reachability(resource_id, references)
|
24
|
+
end
|
25
|
+
|
26
|
+
island_resources = resource_ids - referenced_resources - referencing_resources
|
27
|
+
island_resources.to_a.sort!
|
28
|
+
|
29
|
+
if island_resources.any?
|
30
|
+
message = "Found resources that have no resolved references and are not referenced: #{
|
31
|
+
island_resources.join(', ')}"
|
32
|
+
result = EvaluationResult.new(message, rule: self)
|
33
|
+
else
|
34
|
+
message = 'All resources are reachable'
|
35
|
+
result = EvaluationResult.new(message, severity: 'success', rule: self)
|
36
|
+
end
|
37
|
+
|
38
|
+
context.add_result result
|
39
|
+
end
|
40
|
+
|
41
|
+
def assess_reachability(resource_id, references)
|
42
|
+
makes_resolvable_reference = false
|
43
|
+
references.each do |reference|
|
44
|
+
type = reference[:type]
|
45
|
+
referenced_id = reference[:id]
|
46
|
+
|
47
|
+
if type == ''
|
48
|
+
if resource_ids.include?(referenced_id)
|
49
|
+
makes_resolvable_reference = true
|
50
|
+
referenced_resources.add(referenced_id)
|
51
|
+
end
|
52
|
+
elsif resource_type_ids[type].include?(referenced_id)
|
53
|
+
makes_resolvable_reference = true
|
54
|
+
referenced_resources.add(referenced_id)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
referencing_resources.add(resource_id) if makes_resolvable_reference
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|