inferno_core 0.6.1 → 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7789d88730cda670cc051b8d0d56d1a9d3f8fba3133628eda872eddebd57abf9
|
4
|
+
data.tar.gz: 7f617722c5dd12f6ed349dd474954b7af2fc499420815e4087a18af68dbe5024
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 47b1907c5b14d12b59466d131addf56ba5579e99c7ad96ed8d7e5a7de8e8b83825f4599e39f01aa9abecd263b2fb4945c8e53efc11d247fb7eb969771ede9a06
|
7
|
+
data.tar.gz: 04d10803787c55903e58131832b8586a5592e27608f46b17831abf30ff4ce560fed2061e9db203965f967109e393641a3d5c17a048845df8ab1b3fba664adacc
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative '../../../inferno/dsl/fhir_evaluation/evaluator'
|
2
|
+
require_relative '../../../inferno/dsl/fhir_evaluation/config'
|
2
3
|
require_relative '../../../inferno/entities'
|
3
4
|
require_relative '../../utils/ig_downloader'
|
4
5
|
|
@@ -12,21 +13,22 @@ module Inferno
|
|
12
13
|
|
13
14
|
def evaluate(ig_path, data_path, _log_level)
|
14
15
|
validate_args(ig_path, data_path)
|
15
|
-
|
16
|
+
ig = get_ig(ig_path)
|
16
17
|
|
17
|
-
|
18
|
+
check_ig_version(ig)
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
data =
|
21
|
+
if data_path
|
22
|
+
DatasetLoader.from_path(File.join(__dir__, data_path))
|
23
|
+
else
|
24
|
+
ig.examples
|
25
|
+
end
|
24
26
|
|
25
|
-
|
26
|
-
# evaluator = Inferno::DSL::FHIREvaluation::Evaluator.new(data, config)
|
27
|
+
evaluator = Inferno::DSL::FHIREvaluation::Evaluator.new(ig)
|
27
28
|
|
28
|
-
|
29
|
-
|
29
|
+
config = Inferno::DSL::FHIREvaluation::Config.new
|
30
|
+
results = evaluator.evaluate(data, config)
|
31
|
+
output_results(results, options[:output])
|
30
32
|
end
|
31
33
|
|
32
34
|
def validate_args(ig_path, data_path)
|
@@ -46,7 +48,7 @@ module Inferno
|
|
46
48
|
cache_directory = File.join(user_package_cache, ig_path.sub('@', '#'))
|
47
49
|
ig = Inferno::Entities::IG.from_file(cache_directory)
|
48
50
|
else
|
49
|
-
Tempfile.create('package.tgz') do |temp_file|
|
51
|
+
Tempfile.create(['package', '.tgz']) do |temp_file|
|
50
52
|
load_ig(ig_path, nil, { force: true }, temp_file.path)
|
51
53
|
ig = Inferno::Entities::IG.from_file(temp_file.path)
|
52
54
|
end
|
@@ -55,6 +57,14 @@ module Inferno
|
|
55
57
|
ig
|
56
58
|
end
|
57
59
|
|
60
|
+
def check_ig_version(ig)
|
61
|
+
versions = ig.ig_resource.fhirVersion
|
62
|
+
|
63
|
+
return unless versions.any? { |v| v > '4.0.1' }
|
64
|
+
|
65
|
+
puts '**WARNING** The selected IG targets a FHIR version higher than 4.0.1, which is not supported by Inferno.'
|
66
|
+
end
|
67
|
+
|
58
68
|
def user_package_cache
|
59
69
|
File.join(Dir.home, '.fhir', 'packages')
|
60
70
|
end
|
@@ -22,8 +22,8 @@ Inferno::Application.register_provider(:presets) do
|
|
22
22
|
end
|
23
23
|
|
24
24
|
files_to_load.compact!
|
25
|
-
files_to_load.uniq!
|
26
25
|
files_to_load.map! { |path| File.realpath(path) }
|
26
|
+
files_to_load.uniq!
|
27
27
|
|
28
28
|
files_to_load.each do |path|
|
29
29
|
presets_repo.insert_from_file(path)
|
@@ -327,6 +327,58 @@ module Inferno
|
|
327
327
|
end
|
328
328
|
end
|
329
329
|
|
330
|
+
# Fetch all resources from a paginated FHIR bundle
|
331
|
+
#
|
332
|
+
# @param resource_type [String] The expected resource type to fetch.
|
333
|
+
# @param bundle [FHIR::Bundle] The initial FHIR bundle to process. Defaults to `self.resource`.
|
334
|
+
# @param reply_handler [Proc, nil] A handler for processing replies. Optional.
|
335
|
+
# @param client [Symbol] Defaults to `:default`.
|
336
|
+
# @param max_pages [Integer] Maximum number of pages to fetch. Defaults to 20.
|
337
|
+
# @param additional_resource_types [Array<String>] Additional resource types acceptable in the results.
|
338
|
+
# @param tags [Array<String>] for request tagging. Optional.
|
339
|
+
#
|
340
|
+
# @return [Array<FHIR::Resource>] An array of fetched FHIR resources.
|
341
|
+
def fetch_all_bundled_resources( # rubocop:disable Metrics/CyclomaticComplexity
|
342
|
+
resource_type:,
|
343
|
+
bundle: resource,
|
344
|
+
reply_handler: nil,
|
345
|
+
client: :default,
|
346
|
+
max_pages: 20,
|
347
|
+
additional_resource_types: [],
|
348
|
+
tags: []
|
349
|
+
)
|
350
|
+
page_count = 1
|
351
|
+
resources = []
|
352
|
+
|
353
|
+
while bundle && page_count <= max_pages
|
354
|
+
resources += bundle.entry&.map { |entry| entry&.resource } || []
|
355
|
+
reply_handler&.call(response)
|
356
|
+
|
357
|
+
break if next_bundle_link(bundle).blank?
|
358
|
+
|
359
|
+
bundle = fetch_next_bundle(bundle, client, tags)
|
360
|
+
|
361
|
+
page_count += 1
|
362
|
+
end
|
363
|
+
|
364
|
+
valid_resource_types = [resource_type, 'OperationOutcome'].concat(additional_resource_types)
|
365
|
+
|
366
|
+
invalid_resource_types =
|
367
|
+
resources.reject { |entry| valid_resource_types.include? entry.resourceType }
|
368
|
+
.map(&:resourceType)
|
369
|
+
.uniq
|
370
|
+
if invalid_resource_types.any?
|
371
|
+
info "Received resource type(s) #{invalid_resource_types.join(', ')} in search bundle, " \
|
372
|
+
"but only expected resource types #{valid_resource_types.join(', ')}. " \
|
373
|
+
'This is unusual but allowed if the server believes additional resource types are relevant.'
|
374
|
+
end
|
375
|
+
|
376
|
+
resources
|
377
|
+
rescue JSON::ParserError
|
378
|
+
Inferno::Application[:logger].error "Could not resolve next bundle: #{next_bundle_link(bundle)}"
|
379
|
+
resources
|
380
|
+
end
|
381
|
+
|
330
382
|
# @todo Make this a FHIR class method? Something like
|
331
383
|
# FHIR.class_for(resource_type)
|
332
384
|
# @private
|
@@ -371,6 +423,20 @@ module Inferno
|
|
371
423
|
Inferno::Application[:logger].error "Unable to refresh token: #{e.message}"
|
372
424
|
end
|
373
425
|
|
426
|
+
# @private
|
427
|
+
def fetch_next_bundle(bundle, client, tags)
|
428
|
+
reply = fhir_client(client).raw_read_url(next_bundle_link(bundle))
|
429
|
+
store_request('outgoing', tags:) { reply }
|
430
|
+
return unless request.status == 200
|
431
|
+
|
432
|
+
fhir_client(client).parse_reply(FHIR::Bundle, fhir_client(client).default_format, reply)
|
433
|
+
end
|
434
|
+
|
435
|
+
# @private
|
436
|
+
def next_bundle_link(bundle)
|
437
|
+
bundle&.link&.find { |link| link.relation == 'next' }&.url
|
438
|
+
end
|
439
|
+
|
374
440
|
module ClassMethods
|
375
441
|
# @private
|
376
442
|
def fhir_client_definitions
|
@@ -6,14 +6,16 @@ module Inferno
|
|
6
6
|
# - The data being evaluated
|
7
7
|
# - A summary/characterization of the data
|
8
8
|
# - Evaluation results
|
9
|
+
# - A Validator instance, configured to point to the given IG
|
9
10
|
class EvaluationContext
|
10
|
-
attr_reader :ig, :data, :results, :config
|
11
|
+
attr_reader :ig, :data, :results, :config, :validator
|
11
12
|
|
12
|
-
def initialize(ig, data, config)
|
13
|
+
def initialize(ig, data, config, validator)
|
13
14
|
@ig = ig
|
14
15
|
@data = data
|
15
16
|
@results = []
|
16
17
|
@config = config
|
18
|
+
@validator = validator
|
17
19
|
end
|
18
20
|
|
19
21
|
def add_result(result)
|
@@ -6,18 +6,23 @@ require_relative 'evaluation_context'
|
|
6
6
|
require_relative 'evaluation_result'
|
7
7
|
require_relative 'dataset_loader'
|
8
8
|
|
9
|
+
Dir.glob(File.join(__dir__, 'rules', '*.rb')).each do |file|
|
10
|
+
require_relative file
|
11
|
+
end
|
12
|
+
|
9
13
|
module Inferno
|
10
14
|
module DSL
|
11
15
|
module FHIREvaluation
|
12
16
|
class Evaluator
|
13
|
-
attr_accessor :ig
|
17
|
+
attr_accessor :ig, :validator
|
14
18
|
|
15
|
-
def initialize(ig
|
19
|
+
def initialize(ig, validator = nil)
|
16
20
|
@ig = ig
|
21
|
+
@validator = validator
|
17
22
|
end
|
18
23
|
|
19
24
|
def evaluate(data, config = Config.new)
|
20
|
-
context = EvaluationContext.new(@ig, data, config)
|
25
|
+
context = EvaluationContext.new(@ig, data, config, validator)
|
21
26
|
|
22
27
|
active_rules = []
|
23
28
|
config.data['Rule'].each do |rulename, rule_details|
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Inferno
|
2
|
+
module DSL
|
3
|
+
module FHIREvaluation
|
4
|
+
# This module is used to decide whether a resource instantiates a given profile.
|
5
|
+
# Aligning resources to profiles is necessary when evaluating the comprehensiveness
|
6
|
+
# of the resources with respect to those profiles, unfortunately it's impossible to
|
7
|
+
# programmatically determine intent. (i.e, is this resource supposed to instantiate this profile?)
|
8
|
+
# This module offers some approaches to make that determination.
|
9
|
+
module ProfileConformanceHelper
|
10
|
+
DEFAULT_OPTIONS = {
|
11
|
+
considerMetaProfile: true,
|
12
|
+
considerValidationResults: false,
|
13
|
+
considerOnlyResourceType: false
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
# Check whether the given resource conforms to the given profile, using the given options
|
17
|
+
# to select which approaches are considered.
|
18
|
+
# Current options:
|
19
|
+
# - If the resource is the right resourceType
|
20
|
+
# - If the resource claims conformance in resource.meta.profile
|
21
|
+
# - If the resource validates against the profile using the FHIR validator (NOT YET IMPLEMENTED)
|
22
|
+
# - If the resource meets other criteria defined in the block.
|
23
|
+
# As an example, the block may look for the presence of certain codes, such as LOINC "8867-4"
|
24
|
+
# in an Observation category code suggests that the resource intended to conform to a "Heart Rate" profile
|
25
|
+
# @param resource [FHIR::Resource]
|
26
|
+
# @param profile [FHIR::StructureDefinition]
|
27
|
+
# @param options [Hash] Hash of boolean-valued options. See DEFAULT_OPTIONS for defaults and keys
|
28
|
+
# @param validator [Inferno::DSL::FHIRResourceValidation::Validator]
|
29
|
+
# @yieldparam resource [FHIR::Resource] The original resource
|
30
|
+
# @yieldreturn [Boolean]
|
31
|
+
# @return [Boolean]
|
32
|
+
def conforms_to_profile?(resource, profile, options = DEFAULT_OPTIONS, validator = nil) # rubocop:disable Metrics/CyclomaticComplexity
|
33
|
+
return false if resource.resourceType != profile.type
|
34
|
+
|
35
|
+
return true if options[:considerOnlyResourceType]
|
36
|
+
|
37
|
+
return true if options[:considerMetaProfile] && declares_meta_profile?(resource, profile)
|
38
|
+
|
39
|
+
return true if options[:considerValidationResults] && validates_profile?(resource, profile, validator)
|
40
|
+
|
41
|
+
return true if block_given? && yield(resource)
|
42
|
+
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
# Check if the given resource claims conformance to the profile, versioned or unversioned,
|
47
|
+
# based on resource.meta.profile.
|
48
|
+
# @param resource [FHIR::Resource]
|
49
|
+
# @param profile [FHIR::StructureDefinition]
|
50
|
+
def declares_meta_profile?(resource, profile)
|
51
|
+
declared_profiles = resource&.meta&.profile || []
|
52
|
+
profile_url = profile.url
|
53
|
+
versioned_url = "#{profile_url}|#{profile.version}"
|
54
|
+
|
55
|
+
declared_profiles.include?(profile_url) || declared_profiles.include?(versioned_url)
|
56
|
+
end
|
57
|
+
|
58
|
+
# @private until implemented
|
59
|
+
def validates_profile?(_resource, _profile, _validator)
|
60
|
+
raise 'Profile validation is not yet implemented. ' \
|
61
|
+
'Set considerValidationResults=false.'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Inferno
|
2
|
+
module DSL
|
3
|
+
module FHIREvaluation
|
4
|
+
class ReferenceExtractor
|
5
|
+
attr_accessor :resource_type_ids, :references
|
6
|
+
|
7
|
+
def extract_resource_type_ids(resources)
|
8
|
+
@resource_type_ids = Hash.new { |type, id| type[id] = [] }
|
9
|
+
|
10
|
+
resources.each do |resource|
|
11
|
+
resource.each_element do |value, metadata, path|
|
12
|
+
next unless metadata['type'] == 'id'
|
13
|
+
next if path.include?('contained')
|
14
|
+
|
15
|
+
type = metadata['path'].partition('.').first.downcase
|
16
|
+
resource_type_ids[type] << value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
resource_type_ids
|
21
|
+
end
|
22
|
+
|
23
|
+
def extract_references(resources)
|
24
|
+
@references = Hash.new { |reference, id| reference[id] = [] }
|
25
|
+
|
26
|
+
resources.each do |resource|
|
27
|
+
extract_references_from_resource(resource)
|
28
|
+
end
|
29
|
+
|
30
|
+
references
|
31
|
+
end
|
32
|
+
|
33
|
+
def extract_references_from_resource(resource)
|
34
|
+
resource.each_element do |value, metadata, path|
|
35
|
+
if metadata['type'] == 'Reference' && !value.reference.nil?
|
36
|
+
if value.reference.start_with?('#')
|
37
|
+
next
|
38
|
+
elsif value.reference.include? '/'
|
39
|
+
add_parsed_reference(resource, value, path)
|
40
|
+
elsif value.reference.start_with? 'urn:uuid:'
|
41
|
+
references[resource.id] << { path: path, type: '', id: value.reference[9..] }
|
42
|
+
else
|
43
|
+
references[resource.id] << { path: path, type: '', id: value.reference }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_parsed_reference(resource, value, path)
|
50
|
+
type = value.reference.split('/')[-2].downcase
|
51
|
+
id = value.reference.split('/')[-1]
|
52
|
+
references[resource.id] << if resource_type_ids.key?(type)
|
53
|
+
{ path: path, type: type, id: id }
|
54
|
+
else
|
55
|
+
{ path: path, type: '', id: value.reference }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|