inferno_core 0.6.6 → 0.6.8
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 +30 -4
- data/lib/inferno/dsl/auth_info.rb +3 -1
- data/lib/inferno/dsl/fhir_evaluation/profile_conformance_helper.rb +10 -4
- data/lib/inferno/dsl/fhir_evaluation/rules/all_defined_extensions_have_examples.rb +58 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_profiles_have_examples.rb +49 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_search_parameters_have_examples.rb +79 -0
- data/lib/inferno/public/bundle.js +3 -3
- data/lib/inferno/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6daa4c1864a7e7004ca9a166f3e516fcddc157c3f6b6e24722965e44330cc450
|
4
|
+
data.tar.gz: b158c90f63eaa8a157a27b92af68b7a3be7798e18aca2243ecbd1071c9d9bfdf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6298ba6c1f61a6ee3f5e50045ac58e1ebaa63a2ebc863755d1c610d3eed4b0e7ca76ea74ee12c506298624248c5f3b874582eaccc1c41600f731b33a601d72ce
|
7
|
+
data.tar.gz: 61f9b65616cf01dd42c838ab0b9be5c38490dea22bb2a3481a82c30a25c687e2aada97321498920f6219a62afe88445ad95e71b2300859fce5ed19af7cef1f78
|
@@ -1,6 +1,6 @@
|
|
1
|
-
require_relative '
|
2
|
-
require_relative '
|
3
|
-
require_relative '
|
1
|
+
require_relative '../../dsl/fhir_evaluation/evaluator'
|
2
|
+
require_relative '../../dsl/fhir_evaluation/config'
|
3
|
+
require_relative '../../entities'
|
4
4
|
require_relative '../../utils/ig_downloader'
|
5
5
|
|
6
6
|
require 'tempfile'
|
@@ -9,6 +9,12 @@ module Inferno
|
|
9
9
|
module CLI
|
10
10
|
class Evaluate < Thor::Group
|
11
11
|
def evaluate(ig_path, data_path, _log_level)
|
12
|
+
# NOTE: repositories is required here rather than at the top of the file because
|
13
|
+
# the tree of requires means that this file and its requires get required by every CLI app.
|
14
|
+
# Sequel::Model, used in some repositories, fetches the table schema at instantiation.
|
15
|
+
# This breaks the `migrate` task by fetching a table before the task runs/creates it.
|
16
|
+
require_relative '../../repositories'
|
17
|
+
|
12
18
|
validate_args(ig_path, data_path)
|
13
19
|
ig = Inferno::Repositories::IGs.new.find_or_load(ig_path)
|
14
20
|
|
@@ -21,7 +27,9 @@ module Inferno
|
|
21
27
|
ig.examples
|
22
28
|
end
|
23
29
|
|
24
|
-
|
30
|
+
validator = setup_validator(ig_path)
|
31
|
+
|
32
|
+
evaluator = Inferno::DSL::FHIREvaluation::Evaluator.new(ig, validator)
|
25
33
|
|
26
34
|
config = Inferno::DSL::FHIREvaluation::Config.new
|
27
35
|
results = evaluator.evaluate(data, config)
|
@@ -44,6 +52,24 @@ module Inferno
|
|
44
52
|
puts '**WARNING** The selected IG targets a FHIR version higher than 4.0.1, which is not supported by Inferno.'
|
45
53
|
end
|
46
54
|
|
55
|
+
def setup_validator(ig_path)
|
56
|
+
igs_directory = File.join(Dir.pwd, 'data', 'igs')
|
57
|
+
if File.exist?(ig_path) && !File.realpath(ig_path).start_with?(igs_directory)
|
58
|
+
puts "Copying #{File.basename(ig_path)} to data/igs so it is accessible to validator"
|
59
|
+
destination_file_path = File.join(igs_directory, File.basename(ig_path))
|
60
|
+
FileUtils.copy_file(ig_path, destination_file_path, true)
|
61
|
+
ig_path = "igs/#{File.basename(ig_path)}"
|
62
|
+
end
|
63
|
+
Inferno::DSL::FHIRResourceValidation::Validator.new(:default, 'evaluator_cli') do
|
64
|
+
igs(ig_path)
|
65
|
+
|
66
|
+
cli_context do
|
67
|
+
# For our purposes, code display mismatches should be warnings and not affect profile conformance
|
68
|
+
displayWarnings(true)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
47
73
|
def output_results(results, output)
|
48
74
|
if output&.end_with?('json')
|
49
75
|
oo = FhirEvaluator::EvaluationResult.to_operation_outcome(results)
|
@@ -132,7 +132,7 @@ module Inferno
|
|
132
132
|
# @!attribute [rw] name
|
133
133
|
|
134
134
|
# @private
|
135
|
-
def initialize(raw_attributes_hash)
|
135
|
+
def initialize(raw_attributes_hash) # rubocop:disable Metrics/CyclomaticComplexity
|
136
136
|
attributes_hash = raw_attributes_hash.symbolize_keys
|
137
137
|
|
138
138
|
invalid_keys = attributes_hash.keys - ATTRIBUTES
|
@@ -143,6 +143,8 @@ module Inferno
|
|
143
143
|
value = DateTime.parse(value) if name == :issue_time && value.is_a?(String)
|
144
144
|
|
145
145
|
instance_variable_set(:"@#{name}", value)
|
146
|
+
rescue Date::Error
|
147
|
+
Inferno::Application['logger'].error("Received invalid date: #{value.inspect}")
|
146
148
|
end
|
147
149
|
|
148
150
|
self.issue_time = DateTime.now if access_token.present? && issue_time.blank?
|
@@ -55,10 +55,16 @@ module Inferno
|
|
55
55
|
declared_profiles.include?(profile_url) || declared_profiles.include?(versioned_url)
|
56
56
|
end
|
57
57
|
|
58
|
-
#
|
59
|
-
|
60
|
-
|
61
|
-
|
58
|
+
# Check if the given resource validates against the given profile.
|
59
|
+
# "Validates" means the provided Validator instance returns no fatal or error messages.
|
60
|
+
# @param resource [FHIR::Resource]
|
61
|
+
# @param profile [FHIR::StructureDefinition]
|
62
|
+
# @param validator [Inferno::DSL::FHIRResourceValidation::Validator]
|
63
|
+
def validates_profile?(resource, profile, validator)
|
64
|
+
return false if validator.nil?
|
65
|
+
|
66
|
+
runnable = Inferno::Entities::Test.new
|
67
|
+
validator.resource_is_valid?(resource, profile.url, runnable, add_messages_to_runnable: false)
|
62
68
|
end
|
63
69
|
end
|
64
70
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module DSL
|
5
|
+
module FHIREvaluation
|
6
|
+
module Rules
|
7
|
+
class AllDefinedExtensionsHaveExamples < Rule
|
8
|
+
attr_accessor :used_extensions, :unused_extensions
|
9
|
+
|
10
|
+
def check(context)
|
11
|
+
@used_extensions = context.data.map { |e| extension_urls(e) }.flatten.uniq
|
12
|
+
@unused_extensions = []
|
13
|
+
|
14
|
+
get_unused_extensions(context.ig.extensions) do |extension|
|
15
|
+
next true if extension.context.any? do |ctx|
|
16
|
+
# Skip extensions that are defined for definitional artifacts.
|
17
|
+
# For example, US Core's uscdi-requirement extension is applied to
|
18
|
+
# the profiles and extensions of the IG, not data that conforms to the IG.
|
19
|
+
# There may eventually be cases where SD/ED are data, so this may become configurable.
|
20
|
+
ctx.expression == 'StructureDefinition' || ctx.expression == 'ElementDefinition'
|
21
|
+
end
|
22
|
+
|
23
|
+
versioned_url = "#{extension.url}|#{extension.version}"
|
24
|
+
used_extensions.include?(extension.url) || used_extensions.include?(versioned_url)
|
25
|
+
end
|
26
|
+
|
27
|
+
if unused_extensions.any?
|
28
|
+
message = "Found unused extensions defined in the IG: \n\t #{unused_extensions.join(', ')}"
|
29
|
+
result = EvaluationResult.new(message, rule: self)
|
30
|
+
else
|
31
|
+
message = 'All defined extensions are represented in examples'
|
32
|
+
result = EvaluationResult.new(message, severity: 'success', rule: self)
|
33
|
+
end
|
34
|
+
|
35
|
+
context.add_result result
|
36
|
+
end
|
37
|
+
|
38
|
+
def extension_urls(resource)
|
39
|
+
urls = []
|
40
|
+
resource.each_element do |value, _metadata, path|
|
41
|
+
path_elements = path.split('.')
|
42
|
+
next unless path_elements.length > 1
|
43
|
+
|
44
|
+
urls << value if path_elements[-2].include?('extension') && path_elements[-1] == 'url'
|
45
|
+
end
|
46
|
+
urls.uniq
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_unused_extensions(extensions, &extension_filter)
|
50
|
+
extensions.each do |extension|
|
51
|
+
unused_extensions.push extension.url unless extension_filter.call(extension)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Inferno
|
2
|
+
module DSL
|
3
|
+
module FHIREvaluation
|
4
|
+
module Rules
|
5
|
+
class AllProfilesHaveExamples < Rule
|
6
|
+
include ProfileConformanceHelper
|
7
|
+
|
8
|
+
attr_accessor :context, :unused_profile_urls, :all_resources
|
9
|
+
|
10
|
+
def check(context)
|
11
|
+
@context = context
|
12
|
+
@unused_profile_urls = []
|
13
|
+
@all_resources = []
|
14
|
+
options = context.config.data['Rule']['AllProfilesHaveExamples']['ConformanceOptions'].to_options
|
15
|
+
|
16
|
+
context.data.map { |entry| extract_resources(entry) }
|
17
|
+
all_resources.uniq!
|
18
|
+
|
19
|
+
context.ig.profiles.each do |profile|
|
20
|
+
profile_used = all_resources.any? do |resource|
|
21
|
+
conforms_to_profile?(resource, profile, options, context.validator)
|
22
|
+
end
|
23
|
+
unused_profile_urls << profile.url unless profile_used
|
24
|
+
end
|
25
|
+
|
26
|
+
unused_profile_urls.uniq!
|
27
|
+
|
28
|
+
if unused_profile_urls.any?
|
29
|
+
message = "Found profiles without examples: \n\t #{unused_profile_urls.join(', ')}"
|
30
|
+
result = EvaluationResult.new(message, rule: self)
|
31
|
+
else
|
32
|
+
message = 'All profiles have example instances.'
|
33
|
+
result = EvaluationResult.new(message, severity: 'success', rule: self)
|
34
|
+
end
|
35
|
+
|
36
|
+
context.add_result result
|
37
|
+
end
|
38
|
+
|
39
|
+
def extract_resources(resource)
|
40
|
+
all_resources << resource
|
41
|
+
return unless resource.resourceType == 'Bundle'
|
42
|
+
|
43
|
+
resource.entry.map { |entry| extract_resources(entry.resource) }.flatten
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require_relative '../../fhirpath_evaluation'
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module DSL
|
5
|
+
module FHIREvaluation
|
6
|
+
module Rules
|
7
|
+
class AllSearchParametersHaveExamples < Rule
|
8
|
+
include FhirpathEvaluation
|
9
|
+
|
10
|
+
def check(context)
|
11
|
+
unless ENV['FHIRPATH_URL']
|
12
|
+
message = 'FHIRPATH_URL is not found. Skipping rule AllSearchParametersHaveExamples.'
|
13
|
+
result = EvaluationResult.new(message, severity: 'warning', rule: self)
|
14
|
+
context.add_result result
|
15
|
+
return
|
16
|
+
end
|
17
|
+
|
18
|
+
unused_resource_urls = []
|
19
|
+
search_params = context.ig.resources_by_type['SearchParameter']
|
20
|
+
|
21
|
+
search_params.each do |search_param|
|
22
|
+
unused_resource_urls.push search_param.url unless param_is_used?(search_param, context)
|
23
|
+
end
|
24
|
+
|
25
|
+
if unused_resource_urls.any?
|
26
|
+
message = "Found SearchParameters with no searchable data: \n\t#{unused_resource_urls.join(' ,')}"
|
27
|
+
result = EvaluationResult.new(message, rule: self)
|
28
|
+
elsif !search_params.empty?
|
29
|
+
message = 'All SearchParameters have examples'
|
30
|
+
result = EvaluationResult.new(message, severity: 'success', rule: self)
|
31
|
+
else
|
32
|
+
message = 'IG contains no SearchParameter'
|
33
|
+
result = EvaluationResult.new(message, severity: 'information', rule: self)
|
34
|
+
end
|
35
|
+
|
36
|
+
context.add_result result
|
37
|
+
end
|
38
|
+
|
39
|
+
def param_is_used?(param, context)
|
40
|
+
# Assume that all params have an expression (fhirpath)
|
41
|
+
# This is not guaranteed since the field is 0..1
|
42
|
+
# but without it there's no other way to select a value
|
43
|
+
# Return warning if params don't include expression
|
44
|
+
unless param.expression
|
45
|
+
message = "Search parameter #{param.url} doesn't have an expression."
|
46
|
+
result = EvaluationResult.new(message, severity: 'warning', rule: self)
|
47
|
+
context.add_result result
|
48
|
+
return false
|
49
|
+
end
|
50
|
+
|
51
|
+
used = false
|
52
|
+
|
53
|
+
context.data.each do |resource|
|
54
|
+
next unless param.base.include? resource.resourceType
|
55
|
+
|
56
|
+
begin
|
57
|
+
result = evaluate_fhirpath(resource: resource, path: param.expression)
|
58
|
+
rescue StandardError => e
|
59
|
+
message = "SearchParameter #{param.url} failed to evaluate due to an error. " \
|
60
|
+
"Expression: #{param.expression}. #{e}"
|
61
|
+
result = EvaluationResult.new(message)
|
62
|
+
context.add_result result
|
63
|
+
|
64
|
+
used = true
|
65
|
+
break
|
66
|
+
end
|
67
|
+
|
68
|
+
if result.present?
|
69
|
+
used = true
|
70
|
+
break
|
71
|
+
end
|
72
|
+
end
|
73
|
+
used
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|