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
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
|