inferno_core 0.6.7 → 0.6.9
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 +6 -1
- data/lib/inferno/apps/cli/templates/lib/%library_name%/suite.rb.tt +2 -2
- data/lib/inferno/apps/web/controllers/requirements/show.rb +18 -0
- data/lib/inferno/apps/web/controllers/test_suites/requirements/index.rb +29 -0
- data/lib/inferno/apps/web/router.rb +7 -0
- data/lib/inferno/apps/web/serializers/requirement.rb +18 -0
- data/lib/inferno/apps/web/serializers/requirement_set.rb +13 -0
- data/lib/inferno/apps/web/serializers/test_suite.rb +10 -0
- data/lib/inferno/config/boot/requirements.rb +40 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_defined_extensions_have_examples.rb +58 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_extensions_used.rb +76 -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/dsl/fhir_evaluation/rules/differential_content_has_examples.rb +124 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/value_sets_demonstrate.rb +233 -0
- data/lib/inferno/dsl/fhir_resource_navigation.rb +11 -2
- data/lib/inferno/dsl/must_support_assessment.rb +15 -3
- data/lib/inferno/dsl/requirement_set.rb +82 -0
- data/lib/inferno/dsl/runnable.rb +22 -0
- data/lib/inferno/dsl/suite_requirements.rb +46 -0
- data/lib/inferno/entities/ig.rb +4 -0
- data/lib/inferno/entities/requirement.rb +63 -0
- data/lib/inferno/entities/test_suite.rb +2 -0
- data/lib/inferno/public/bundle.js +3 -3
- data/lib/inferno/repositories/igs.rb +1 -2
- data/lib/inferno/repositories/requirements.rb +116 -0
- data/lib/inferno/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5740a0c7e8d54767851b4f7e9ee7c5c6ca9ac175211d63b4990f99bbbe9266ca
|
4
|
+
data.tar.gz: 4fd1b9c5cb4f37c441a668ae58c8b6cd49aeea18ad393b2d7bbb9699d86a0060
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f6adc91f5db1ba2a6535c847c0fe7b691cd85455fb0f34a44bd525428dee3ddcd4c414d7989ea4e13fdd50b9c39778a19d26e97473f1617b85691b4683776f49
|
7
|
+
data.tar.gz: 463d68cf3e187f992bd134bbc177072bfaa9ec625a2006ca1bb23c49591a4bc9521ca027232c68c5d1ebcf499dcca84f3f722ca2afd9bd1b6038bbb19daf4835
|
@@ -1,7 +1,6 @@
|
|
1
1
|
require_relative '../../dsl/fhir_evaluation/evaluator'
|
2
2
|
require_relative '../../dsl/fhir_evaluation/config'
|
3
3
|
require_relative '../../entities'
|
4
|
-
require_relative '../../repositories'
|
5
4
|
require_relative '../../utils/ig_downloader'
|
6
5
|
|
7
6
|
require 'tempfile'
|
@@ -10,6 +9,12 @@ module Inferno
|
|
10
9
|
module CLI
|
11
10
|
class Evaluate < Thor::Group
|
12
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
|
+
|
13
18
|
validate_args(ig_path, data_path)
|
14
19
|
ig = Inferno::Repositories::IGs.new.find_or_load(ig_path)
|
15
20
|
|
@@ -13,13 +13,13 @@ module <%= module_name %>
|
|
13
13
|
|
14
14
|
input :credentials,
|
15
15
|
title: 'OAuth Credentials',
|
16
|
-
type: :
|
16
|
+
type: :auth_info,
|
17
17
|
optional: true
|
18
18
|
|
19
19
|
# All FHIR requests in this suite will use this FHIR client
|
20
20
|
fhir_client do
|
21
21
|
url :url
|
22
|
-
|
22
|
+
auth_info :credentials
|
23
23
|
end
|
24
24
|
|
25
25
|
# All FHIR validation requests will use this FHIR validator
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative '../../serializers/requirement'
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module Web
|
5
|
+
module Controllers
|
6
|
+
module Requirements
|
7
|
+
class Show < Controller
|
8
|
+
def handle(req, res)
|
9
|
+
requirement = repo.find(req.params[:id])
|
10
|
+
halt 404 if requirement.nil?
|
11
|
+
|
12
|
+
res.body = serialize(requirement)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Inferno
|
2
|
+
module Web
|
3
|
+
module Controllers
|
4
|
+
module TestSuites
|
5
|
+
module Requirements
|
6
|
+
class Index < Controller
|
7
|
+
include Import[test_suites_repo: 'inferno.repositories.test_suites']
|
8
|
+
include Import[test_sessions_repo: 'inferno.repositories.test_sessions']
|
9
|
+
|
10
|
+
def handle(req, res)
|
11
|
+
test_suite = test_suites_repo.find(req.params[:id])
|
12
|
+
halt 404, "Test Suite `#{req.params[:id]}` not found" if test_suite.nil?
|
13
|
+
|
14
|
+
test_session = nil
|
15
|
+
if req.params[:session_id]
|
16
|
+
test_session = test_sessions_repo.find(req.params[:session_id])
|
17
|
+
halt 404, "Test session `#{req.params[:session_id]}` not found" if test_session.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
requirements = repo.requirements_for_suite(test_suite.id, test_session&.id)
|
21
|
+
|
22
|
+
res.body = serialize(requirements)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -47,6 +47,13 @@ module Inferno
|
|
47
47
|
put '/:id/check_configuration',
|
48
48
|
to: Inferno::Web::Controllers::TestSuites::CheckConfiguration,
|
49
49
|
as: :check_configuration
|
50
|
+
get ':id/requirements',
|
51
|
+
to: Inferno::Web::Controllers::TestSuites::Requirements::Index,
|
52
|
+
as: :requirements
|
53
|
+
end
|
54
|
+
|
55
|
+
scope 'requirements' do
|
56
|
+
get '/:id', to: Inferno::Web::Controllers::Requirements::Show, as: :show
|
50
57
|
end
|
51
58
|
|
52
59
|
get '/requests/:id', to: Inferno::Web::Controllers::Requests::Show, as: :requests_show
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'serializer'
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module Web
|
5
|
+
module Serializers
|
6
|
+
class Requirement < Serializer
|
7
|
+
identifier :id
|
8
|
+
|
9
|
+
field :requirement
|
10
|
+
field :conformance
|
11
|
+
field :actor
|
12
|
+
field :sub_requirements
|
13
|
+
field :conditionality
|
14
|
+
field :url, if: :field_present?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative 'preset'
|
2
|
+
require_relative 'requirement_set'
|
2
3
|
require_relative 'suite_option'
|
3
4
|
require_relative 'test_group'
|
4
5
|
|
@@ -27,6 +28,7 @@ module Inferno
|
|
27
28
|
|
28
29
|
view :full do
|
29
30
|
include_view :summary
|
31
|
+
|
30
32
|
field :test_groups do |suite, options|
|
31
33
|
suite_options = options[:suite_options]
|
32
34
|
TestGroup.render_as_hash(suite.groups(suite_options), suite_options:)
|
@@ -36,6 +38,14 @@ module Inferno
|
|
36
38
|
suite_options = options[:suite_options]
|
37
39
|
Input.render_as_hash(suite.available_inputs(suite_options).values)
|
38
40
|
end
|
41
|
+
field :requirement_sets, if: :field_present? do |suite, options|
|
42
|
+
selected_options = options[:suite_options] || []
|
43
|
+
requirement_sets = suite.requirement_sets.select do |requirement_set|
|
44
|
+
requirement_set.suite_options.all? { |suite_option| selected_options.include? suite_option }
|
45
|
+
end
|
46
|
+
|
47
|
+
RequirementSet.render_as_hash(requirement_sets)
|
48
|
+
end
|
39
49
|
end
|
40
50
|
end
|
41
51
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative '../../repositories/requirements'
|
2
|
+
|
3
|
+
Inferno::Application.register_provider(:requirements) do
|
4
|
+
prepare do
|
5
|
+
target_container.start :suites
|
6
|
+
|
7
|
+
requirements_repo = Inferno::Repositories::Requirements.new
|
8
|
+
|
9
|
+
test_kit_gems =
|
10
|
+
Bundler
|
11
|
+
.definition
|
12
|
+
.specs
|
13
|
+
.select { |spec| spec.metadata.fetch('inferno_test_kit', 'false').casecmp? 'true' }
|
14
|
+
|
15
|
+
files_to_load = Dir.glob(['lib/*test_kit/requirements/*.csv'])
|
16
|
+
|
17
|
+
if ENV['LOAD_DEV_SUITES'].present?
|
18
|
+
ENV['LOAD_DEV_SUITES'].split(',').map(&:strip).reject(&:empty?).each do |suite|
|
19
|
+
files_to_load.concat Dir.glob(File.join(Inferno::Application.root, 'dev_suites', suite, 'requirements',
|
20
|
+
'*.csv'))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
files_to_load +=
|
25
|
+
test_kit_gems.flat_map do |gem|
|
26
|
+
[
|
27
|
+
Dir.glob([File.join(gem.full_gem_path, 'lib', '*test_kit', 'requirements', '*.csv')])
|
28
|
+
].flatten
|
29
|
+
end
|
30
|
+
|
31
|
+
files_to_load.compact!
|
32
|
+
files_to_load.reject! { |file| file.include?('out_of_scope') }
|
33
|
+
files_to_load.map! { |path| File.realpath(path) }
|
34
|
+
files_to_load.uniq!
|
35
|
+
|
36
|
+
files_to_load.each do |path|
|
37
|
+
requirements_repo.insert_from_file(path)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
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,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module DSL
|
5
|
+
module FHIREvaluation
|
6
|
+
module Rules
|
7
|
+
class AllExtensionsUsed < Rule
|
8
|
+
def check(context)
|
9
|
+
all_extensions = collect_profile_extensions(context.ig.profiles)
|
10
|
+
unused_extensions = remove_found_resource_extensions(all_extensions, context.data)
|
11
|
+
if unused_extensions.any? { |_profile, extensions| !extensions.empty? }
|
12
|
+
message = get_fail_message(unused_extensions)
|
13
|
+
result = EvaluationResult.new(message, rule: self)
|
14
|
+
else
|
15
|
+
message = 'All extensions specified in profiles are represented in instances.'
|
16
|
+
result = EvaluationResult.new(message, severity: 'success', rule: self)
|
17
|
+
end
|
18
|
+
|
19
|
+
context.add_result result
|
20
|
+
end
|
21
|
+
|
22
|
+
def collect_profile_extensions(profiles)
|
23
|
+
extensions = Hash.new { |extension, profile| extension[profile] = Set.new }
|
24
|
+
profiles.each do |profile|
|
25
|
+
profile.each_element do |value, metadata|
|
26
|
+
next unless metadata['type'] == 'ElementDefinition'
|
27
|
+
|
28
|
+
path_end = value.id.split('.')[-1]
|
29
|
+
next unless path_end.include?('extension')
|
30
|
+
|
31
|
+
value.type.each do |element_definition|
|
32
|
+
element_definition.profile.each do |extension_url|
|
33
|
+
extensions[profile.url].add(extension_url)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
extensions
|
39
|
+
end
|
40
|
+
|
41
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
42
|
+
def remove_found_resource_extensions(extensions, examples)
|
43
|
+
unused_extensions = extensions.dup
|
44
|
+
examples.each do |resource|
|
45
|
+
resource.each_element do |value, _metadata, path|
|
46
|
+
path_elements = path.split('.')
|
47
|
+
next unless path_elements.length > 1
|
48
|
+
|
49
|
+
next unless path_elements[-2].include?('extension') && path_elements[-1] == 'url'
|
50
|
+
|
51
|
+
profiles = resource&.meta&.profile || []
|
52
|
+
update_unused_extensions(profiles, value, unused_extensions, extensions)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
unused_extensions
|
56
|
+
end
|
57
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
58
|
+
|
59
|
+
def update_unused_extensions(profiles, value, unused_extensions, extensions)
|
60
|
+
profiles.each do |profile|
|
61
|
+
unused_extensions[profile].delete(value) if extensions.key?(profile)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_fail_message(extensions)
|
66
|
+
message = 'Found extensions specified in profiles, but not used in instances:'
|
67
|
+
extensions.each do |profile, extension|
|
68
|
+
message += "\n Profile: #{profile}, \n\tExtensions: #{extension.join(', ')}" unless extension.empty?
|
69
|
+
end
|
70
|
+
message
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
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
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module DSL
|
5
|
+
module FHIREvaluation
|
6
|
+
module Rules
|
7
|
+
class DifferentialContentHasExamples < Rule
|
8
|
+
def check(context)
|
9
|
+
unused_differential = Hash.new { |field, url| field[url] = Set.new }
|
10
|
+
collect_profile_differential_content(unused_differential, context.ig.profiles)
|
11
|
+
collect_profile_differential_content(unused_differential, context.ig.extensions)
|
12
|
+
remove_found_differential_content(unused_differential, context.data)
|
13
|
+
|
14
|
+
if unused_differential.any? { |_url, diff| !diff.empty? }
|
15
|
+
message = gen_differential_fail_message(unused_differential)
|
16
|
+
result = EvaluationResult.new(message, rule: self)
|
17
|
+
else
|
18
|
+
message = 'All differential fields are represented in instances'
|
19
|
+
result = EvaluationResult.new(message, severity: 'success', rule: self)
|
20
|
+
end
|
21
|
+
|
22
|
+
context.add_result result
|
23
|
+
end
|
24
|
+
|
25
|
+
def collect_profile_differential_content(unused_differential, profiles)
|
26
|
+
profiles.each do |profile|
|
27
|
+
profile.each_element do |value, _metadata, path|
|
28
|
+
next unless path.start_with? 'differential'
|
29
|
+
|
30
|
+
next unless value.is_a? FHIR::ElementDefinition
|
31
|
+
next unless value.id.include? '.'
|
32
|
+
|
33
|
+
# Skip fields that are disallowed by the profile (cardinality 0..0)
|
34
|
+
# Note that max is a string to allow for "*", not an int
|
35
|
+
next if value.max == '0'
|
36
|
+
|
37
|
+
# TODO: discriminate between extensions
|
38
|
+
# if you have field.extension:A and field.extension:B
|
39
|
+
# only field.extension is recorded and checked for
|
40
|
+
# if A and B are not defined in the IG,they may be missed
|
41
|
+
clean_val = clean_value(value)
|
42
|
+
|
43
|
+
unused_differential[profile.url].add(clean_val)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
49
|
+
def remove_found_differential_content(unused_differential, examples)
|
50
|
+
examples.each do |resource|
|
51
|
+
extension_base_path = ''
|
52
|
+
extension_url = ''
|
53
|
+
resource.each_element do |value, _metadata, path|
|
54
|
+
profiles = resource&.meta&.profile || []
|
55
|
+
profiles.each do |profile|
|
56
|
+
processed_path = plain_value(path)
|
57
|
+
processed_path = rm_brackets(processed_path)
|
58
|
+
|
59
|
+
if path.match?('extension\[\d+\]\.url$')
|
60
|
+
extension_base_path = path.rpartition('.').first
|
61
|
+
extension_url = value
|
62
|
+
unused_differential[extension_url].delete('url') if unused_differential.key?(extension_url)
|
63
|
+
unused_differential[extension_url].delete('extension') if unused_differential.key?(extension_url)
|
64
|
+
unused_differential.delete(extension_url) if unused_differential[extension_url].empty?
|
65
|
+
elsif path.start_with?(extension_base_path) && !extension_base_path.empty?
|
66
|
+
if unused_differential.key?(extension_url)
|
67
|
+
unused_differential[extension_url].delete(processed_path.rpartition('.').last)
|
68
|
+
end
|
69
|
+
unused_differential.delete(extension_url) if unused_differential[extension_url].empty?
|
70
|
+
else
|
71
|
+
unused_differential[profile].delete(processed_path) if unused_differential.key?(profile)
|
72
|
+
unused_differential.delete(profile) if unused_differential[profile].empty?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
79
|
+
|
80
|
+
def clean_value(value)
|
81
|
+
stripped_val = value.id.partition('.').last
|
82
|
+
stripped_val = stripped_val.partition('[').first if stripped_val.end_with? ']'
|
83
|
+
stripped_val.split('.').map do |field|
|
84
|
+
field = field.partition(':').first if field.include?(':')
|
85
|
+
field = field.partition('[').first if field.include?('[')
|
86
|
+
field
|
87
|
+
end.join('.')
|
88
|
+
end
|
89
|
+
|
90
|
+
def plain_value(path)
|
91
|
+
if path.include? '.'
|
92
|
+
path_array = path.split('.').map! do |field|
|
93
|
+
field.start_with?('value') ? 'value' : field
|
94
|
+
end
|
95
|
+
path_array.join('.')
|
96
|
+
elsif path.start_with?('value')
|
97
|
+
'value'
|
98
|
+
elsif path.end_with?(']')
|
99
|
+
path.rpartition('[').first
|
100
|
+
else
|
101
|
+
path
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def rm_brackets(path)
|
106
|
+
path_array = path.split('.').map! do |field|
|
107
|
+
field.include?('[') ? field.partition('[').first : field
|
108
|
+
end
|
109
|
+
path_array.join('.')
|
110
|
+
end
|
111
|
+
|
112
|
+
def gen_differential_fail_message(unused_differential)
|
113
|
+
"Found fields highlighted in the differential view, but not used in instances: #{
|
114
|
+
unused_differential.map do |url, field|
|
115
|
+
next if field.empty?
|
116
|
+
|
117
|
+
"\n Profile/Extension: #{url} \n\tFields: #{field.join(', ')}"
|
118
|
+
end.compact.join}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|