inferno_core 0.6.4 → 0.6.5
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 +1 -30
- data/lib/inferno/apps/cli/new.rb +1 -2
- data/lib/inferno/apps/cli/templates/.env.development +1 -0
- data/lib/inferno/apps/cli/templates/.env.production +1 -0
- data/lib/inferno/apps/cli/templates/.gitignore +1 -0
- data/lib/inferno/apps/cli/templates/data/igs/.keep +0 -0
- data/lib/inferno/apps/cli/templates/docker-compose.background.yml.tt +2 -2
- data/lib/inferno/apps/web/controllers/controller.rb +3 -1
- data/lib/inferno/apps/web/router.rb +12 -6
- data/lib/inferno/config/boot/ig_files.rb +47 -0
- data/lib/inferno/config/boot/validator.rb +1 -0
- data/lib/inferno/config/boot/web.rb +6 -2
- data/lib/inferno/dsl/assertions.rb +26 -0
- data/lib/inferno/dsl/fhir_client_builder.rb +1 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_must_supports_present.rb +13 -308
- data/lib/inferno/dsl/fhir_resource_validation.rb +34 -2
- data/lib/inferno/dsl/fhir_validation.rb +13 -0
- data/lib/inferno/dsl/must_support_assessment.rb +365 -0
- data/lib/inferno/dsl/results.rb +36 -4
- data/lib/inferno/dsl/runnable.rb +71 -0
- data/lib/inferno/dsl.rb +3 -1
- data/lib/inferno/entities/ig.rb +4 -1
- data/lib/inferno/exceptions.rb +6 -0
- data/lib/inferno/public/bundle.js +34 -34
- data/lib/inferno/public/bundle.js.LICENSE.txt +3 -3
- data/lib/inferno/repositories/igs.rb +122 -0
- data/lib/inferno/repositories/in_memory_repository.rb +7 -0
- data/lib/inferno/utils/ig_downloader.rb +17 -6
- data/lib/inferno/version.rb +1 -1
- data/spec/shared/test_kit_examples.rb +69 -0
- 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: 7c009d9329c63f4c737ce53c79d6b88af8db643550d2509784b6e3a43558d237
|
4
|
+
data.tar.gz: 5e0e2e2d7e861b38caa73ab2f6d83fc6a45a29a2e33744f67b8853df7766a5b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f47f2704474b9f7eea33679463bd284a91ef02466949f317ade4378b08c602a8f89646dc5914972002d707c3391d24e8b9b5332cc79b946bc2a975f6b0ca3693
|
7
|
+
data.tar.gz: 420d52d34c90f103375ed615a8d2e8d01b7cdfe1eac220b41c9458dcc33aa9ed2aafa54a8e0403966987b652e07f5e510f95f93caeaa79be6197ebf4dbe58d0e
|
@@ -8,12 +8,9 @@ require 'tempfile'
|
|
8
8
|
module Inferno
|
9
9
|
module CLI
|
10
10
|
class Evaluate < Thor::Group
|
11
|
-
include Thor::Actions
|
12
|
-
include Inferno::Utils::IgDownloader
|
13
|
-
|
14
11
|
def evaluate(ig_path, data_path, _log_level)
|
15
12
|
validate_args(ig_path, data_path)
|
16
|
-
ig =
|
13
|
+
ig = Inferno::Repositories::IGs.new.find_or_load(ig_path)
|
17
14
|
|
18
15
|
check_ig_version(ig)
|
19
16
|
|
@@ -39,24 +36,6 @@ module Inferno
|
|
39
36
|
raise "Provided path '#{data_path}' is not a directory"
|
40
37
|
end
|
41
38
|
|
42
|
-
def get_ig(ig_path)
|
43
|
-
if File.exist?(ig_path)
|
44
|
-
ig = Inferno::Entities::IG.from_file(ig_path)
|
45
|
-
elsif in_user_package_cache?(ig_path.sub('@', '#'))
|
46
|
-
# NPM syntax for a package identifier is id@version (eg, hl7.fhir.us.core@3.1.1)
|
47
|
-
# but in the cache the separator is # (hl7.fhir.us.core#3.1.1)
|
48
|
-
cache_directory = File.join(user_package_cache, ig_path.sub('@', '#'))
|
49
|
-
ig = Inferno::Entities::IG.from_file(cache_directory)
|
50
|
-
else
|
51
|
-
Tempfile.create(['package', '.tgz']) do |temp_file|
|
52
|
-
load_ig(ig_path, nil, { force: true }, temp_file.path)
|
53
|
-
ig = Inferno::Entities::IG.from_file(temp_file.path)
|
54
|
-
end
|
55
|
-
end
|
56
|
-
ig.add_self_to_repository
|
57
|
-
ig
|
58
|
-
end
|
59
|
-
|
60
39
|
def check_ig_version(ig)
|
61
40
|
versions = ig.ig_resource.fhirVersion
|
62
41
|
|
@@ -65,14 +44,6 @@ module Inferno
|
|
65
44
|
puts '**WARNING** The selected IG targets a FHIR version higher than 4.0.1, which is not supported by Inferno.'
|
66
45
|
end
|
67
46
|
|
68
|
-
def user_package_cache
|
69
|
-
File.join(Dir.home, '.fhir', 'packages')
|
70
|
-
end
|
71
|
-
|
72
|
-
def in_user_package_cache?(ig_identifier)
|
73
|
-
File.directory?(File.join(user_package_cache, ig_identifier))
|
74
|
-
end
|
75
|
-
|
76
47
|
def output_results(results, output)
|
77
48
|
if output&.end_with?('json')
|
78
49
|
oo = FhirEvaluator::EvaluationResult.to_operation_outcome(results)
|
data/lib/inferno/apps/cli/new.rb
CHANGED
@@ -112,9 +112,8 @@ module Inferno
|
|
112
112
|
end
|
113
113
|
|
114
114
|
def load_igs
|
115
|
-
config = { verbose: !options['quiet'] }
|
116
115
|
options['implementation_guide']&.each_with_index do |ig, idx|
|
117
|
-
uri = options['implementation_guide'].length == 1 ? load_ig(ig, nil
|
116
|
+
uri = options['implementation_guide'].length == 1 ? load_ig(ig, nil) : load_ig(ig, idx)
|
118
117
|
say_unless_quiet "Downloaded IG from #{uri}"
|
119
118
|
rescue OpenURI::HTTPError => e
|
120
119
|
say_unless_quiet "Failed to install implementation guide #{ig}", :red
|
File without changes
|
@@ -7,7 +7,7 @@ services:
|
|
7
7
|
# Negative values mean sessions never expire, 0 means sessions immediately expire
|
8
8
|
SESSION_CACHE_DURATION: -1
|
9
9
|
volumes:
|
10
|
-
-
|
10
|
+
- ./data/igs:/app/igs
|
11
11
|
# To let the service share your local FHIR package cache,
|
12
12
|
# uncomment the below line
|
13
13
|
# - ~/.fhir:/home/ktor/.fhir
|
@@ -15,7 +15,7 @@ services:
|
|
15
15
|
image: infernocommunity/fhir-validator-service
|
16
16
|
# Update this path to match your directory structure
|
17
17
|
volumes:
|
18
|
-
-
|
18
|
+
- ./data/igs:/home/igs
|
19
19
|
fhir_validator_app:
|
20
20
|
image: infernocommunity/fhir-validator-app
|
21
21
|
depends_on:
|
@@ -20,7 +20,9 @@ module Inferno
|
|
20
20
|
# Hanami Controller 2.0.0 removes the ability to set a default
|
21
21
|
# Content-Type response header, so set it manually if it hasn't been
|
22
22
|
# set.
|
23
|
-
subclass.after
|
23
|
+
subclass.after do |_req, res|
|
24
|
+
res.format = :json if res.format == :all
|
25
|
+
end
|
24
26
|
end
|
25
27
|
|
26
28
|
def self.resource_name
|
@@ -4,8 +4,12 @@ Dir.glob(File.join(__dir__, 'controllers', '**', '*.rb')).each { |path| require_
|
|
4
4
|
|
5
5
|
module Inferno
|
6
6
|
module Web
|
7
|
-
client_page = ERB.new(
|
8
|
-
|
7
|
+
client_page = ERB.new(
|
8
|
+
File.read(
|
9
|
+
File.join(Inferno::Application.root, 'lib', 'inferno', 'apps', 'web', 'index.html.erb')
|
10
|
+
)
|
11
|
+
).result
|
12
|
+
CLIENT_PAGE_RESPONSE = ->(_env) { [200, { 'Content-Type' => 'text/html' }, [client_page]] }
|
9
13
|
|
10
14
|
base_path = Application['base_path']&.delete_prefix('/')
|
11
15
|
|
@@ -47,12 +51,14 @@ module Inferno
|
|
47
51
|
|
48
52
|
get '/requests/:id', to: Inferno::Web::Controllers::Requests::Show, as: :requests_show
|
49
53
|
|
50
|
-
get '/version', to:
|
54
|
+
get '/version', to: lambda { |_env|
|
55
|
+
[200, { 'Content-Type' => 'application/json' }, [{ 'version' => Inferno::VERSION.to_s }.to_json]]
|
56
|
+
}, as: :version
|
51
57
|
end
|
52
58
|
|
53
59
|
# Should not need Content-Type header but GitHub Codespaces will not work without them.
|
54
60
|
# This could be investigated and likely removed if addressed properly elsewhere.
|
55
|
-
get '/', to:
|
61
|
+
get '/', to: CLIENT_PAGE_RESPONSE
|
56
62
|
get '/jwks.json', to: lambda { |_env|
|
57
63
|
[200, { 'Content-Type' => 'application/json' }, [Inferno::JWKS.jwks_json]]
|
58
64
|
}, as: :jwks
|
@@ -70,7 +76,7 @@ module Inferno
|
|
70
76
|
|
71
77
|
Inferno::Repositories::TestSuites.all.map { |suite| "/#{suite.id}" }.each do |suite_path|
|
72
78
|
Application['logger'].info("Registering suite route: #{suite_path}")
|
73
|
-
get suite_path, to:
|
79
|
+
get suite_path, to: CLIENT_PAGE_RESPONSE
|
74
80
|
end
|
75
81
|
|
76
82
|
get '/test_sessions/:id', to: Inferno::Web::Controllers::TestSessions::ClientShow, as: :client_session_show
|
@@ -83,7 +89,7 @@ module Inferno
|
|
83
89
|
if base_path.present?
|
84
90
|
Hanami::Router.new do
|
85
91
|
scope("#{base_path}/") do
|
86
|
-
get '/', to:
|
92
|
+
get '/', to: CLIENT_PAGE_RESPONSE
|
87
93
|
end
|
88
94
|
scope(base_path, &route_block)
|
89
95
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
Inferno::Application.register_provider(:ig_files) do
|
4
|
+
prepare do
|
5
|
+
# This process should only run once, so skipping it on workers will start it
|
6
|
+
# only once from the "web" process
|
7
|
+
next if Sidekiq.server?
|
8
|
+
|
9
|
+
target_container.start :logging
|
10
|
+
|
11
|
+
test_kit_gems = Bundler.definition.specs.select { |spec| spec.metadata['inferno_test_kit']&.casecmp? 'true' }
|
12
|
+
|
13
|
+
test_kit_ig_files = test_kit_gems.map do |test_kit|
|
14
|
+
files = Dir.glob(File.join(test_kit.full_gem_path, 'lib', '*', 'igs', '*.tgz'))
|
15
|
+
next if files.blank?
|
16
|
+
|
17
|
+
{
|
18
|
+
test_kit_name: test_kit.name,
|
19
|
+
files:
|
20
|
+
}
|
21
|
+
end.compact
|
22
|
+
|
23
|
+
local_ig_files = Dir.glob(File.join(Dir.pwd, 'lib', '*', 'igs', '*.tgz'))
|
24
|
+
|
25
|
+
if local_ig_files.present? && test_kit_gems.none? { |gem| gem.full_gem_path == Dir.pwd }
|
26
|
+
test_kit_ig_files += {
|
27
|
+
test_kit_name: 'current project',
|
28
|
+
files: local_ig_files
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
igs_directory = File.join(Dir.pwd, 'data', 'igs')
|
33
|
+
if File.exist? igs_directory
|
34
|
+
FileUtils.rm_f(Dir.glob(File.join(igs_directory, '*.tgz')))
|
35
|
+
|
36
|
+
test_kit_ig_files.each do |ig_files|
|
37
|
+
ig_files[:files].each do |source_file_path|
|
38
|
+
destination_file_path = File.join(igs_directory, File.basename(source_file_path))
|
39
|
+
Inferno::Application['logger'].info(
|
40
|
+
"Copying #{File.basename(source_file_path)} to data/igs from #{ig_files[:test_kit_name]}"
|
41
|
+
)
|
42
|
+
FileUtils.copy_file(source_file_path, destination_file_path, true)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
Inferno::Application.register_provider(:validator) do
|
2
2
|
prepare do
|
3
3
|
target_container.start :suites
|
4
|
+
target_container.start :ig_files
|
4
5
|
|
5
6
|
# This process should only run once, to start one job per validator,
|
6
7
|
# so skipping it on workers will start it only once from the "web" process
|
@@ -1,14 +1,18 @@
|
|
1
1
|
Inferno::Application.register_provider(:web) do |_app|
|
2
2
|
prepare do
|
3
3
|
require 'blueprinter'
|
4
|
-
require 'hanami/router'
|
5
|
-
require 'hanami/controller'
|
6
4
|
require 'oj'
|
7
5
|
|
8
6
|
Blueprinter.configure do |config|
|
9
7
|
config.generator = Oj
|
10
8
|
end
|
11
9
|
|
10
|
+
# Workers aren't connected to a web server, so they shouldn't be hosting
|
11
|
+
# routes
|
12
|
+
next if Sidekiq.server?
|
13
|
+
|
14
|
+
require 'hanami/router'
|
15
|
+
require 'hanami/controller'
|
12
16
|
require 'inferno/utils/middleware/request_logger'
|
13
17
|
require 'inferno/apps/web/application'
|
14
18
|
end
|
@@ -197,6 +197,32 @@ module Inferno
|
|
197
197
|
def bad_content_type_message(expected, received)
|
198
198
|
"Expected `Content-Type` to be `#{expected}`, but found `#{received}`"
|
199
199
|
end
|
200
|
+
|
201
|
+
# Check that all Must Support elements defined on the given profile are present in the given resources.
|
202
|
+
# Must Support elements are identified on the profile StructureDefinition and pre-parsed into metadata,
|
203
|
+
# which may be customized prior to the check by passing a block. Alternate metadata may be provided directly.
|
204
|
+
# Set test suite config flag debug_must_support_metadata: true to log the metadata to a file for debugging.
|
205
|
+
#
|
206
|
+
# @param resources [Array<FHIR::Resource>]
|
207
|
+
# @param profile_url [String]
|
208
|
+
# @param validator_name [Symbol] Name of the FHIR Validator that references the IG the profile is in
|
209
|
+
# @param metadata [Hash] MustSupport Metadata (optional),
|
210
|
+
# if provided the check will use this instead of re-generating metadata from the profile
|
211
|
+
# @param requirement_extension [String] Extension URL that implies "required" as an alternative to the MS flag
|
212
|
+
# @yield [Metadata] Customize the metadata before running the test
|
213
|
+
# @return [void]
|
214
|
+
def assert_must_support_elements_present(resources, profile_url, validator_name: :default, metadata: nil,
|
215
|
+
requirement_extension: nil, &)
|
216
|
+
missing_elements = missing_must_support_elements(resources, profile_url, validator_name:, metadata:,
|
217
|
+
requirement_extension:, &)
|
218
|
+
assert missing_elements.empty?, missing_must_support_elements_message(missing_elements, resources)
|
219
|
+
end
|
220
|
+
|
221
|
+
# @private
|
222
|
+
def missing_must_support_elements_message(missing_elements, resources)
|
223
|
+
"Could not find #{missing_elements.join(', ')} in the #{resources.length} " \
|
224
|
+
'provided resource(s)'
|
225
|
+
end
|
200
226
|
end
|
201
227
|
end
|
202
228
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require_relative '../../fhir_resource_navigation'
|
2
2
|
require_relative '../../must_support_metadata_extractor'
|
3
3
|
require_relative '../profile_conformance_helper'
|
4
|
+
require_relative '../../must_support_assessment'
|
4
5
|
|
5
6
|
module Inferno
|
6
7
|
module DSL
|
@@ -16,7 +17,9 @@ module Inferno
|
|
16
17
|
class AllMustSupportsPresent < Rule
|
17
18
|
include FHIRResourceNavigation
|
18
19
|
include ProfileConformanceHelper
|
19
|
-
|
20
|
+
include MustSupportAssessment
|
21
|
+
|
22
|
+
attr_accessor :metadata, :ig
|
20
23
|
|
21
24
|
# check is invoked from the evaluator CLI and applies the logic for this Rule to the provided data.
|
22
25
|
# At least one instance of every MustSupport element defined in the profiles must be populated in the data.
|
@@ -26,7 +29,8 @@ module Inferno
|
|
26
29
|
# @return [void]
|
27
30
|
def check(context)
|
28
31
|
missing_items_by_profile = {}
|
29
|
-
context.ig
|
32
|
+
ig = context.ig
|
33
|
+
ig.profiles.each do |profile|
|
30
34
|
resources = pick_resources_for_profile(profile, context)
|
31
35
|
if resources.blank?
|
32
36
|
missing_items_by_profile[profile.url] = ['No matching resources were found to check']
|
@@ -34,9 +38,8 @@ module Inferno
|
|
34
38
|
end
|
35
39
|
requirement_extension = context.config.data['Rule']['AllMustSupportsPresent']['RequirementExtensionUrl']
|
36
40
|
debug_metadata = context.config.data['Rule']['AllMustSupportsPresent']['WriteMetadataForDebugging']
|
37
|
-
|
38
|
-
|
39
|
-
|
41
|
+
missing_items = perform_must_support_assessment(profile, resources, ig, debug_metadata:,
|
42
|
+
requirement_extension:)
|
40
43
|
missing_items_by_profile[profile.url] = missing_items if missing_items.any?
|
41
44
|
end
|
42
45
|
|
@@ -68,309 +71,11 @@ module Inferno
|
|
68
71
|
end
|
69
72
|
end
|
70
73
|
|
71
|
-
#
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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) }
|
74
|
+
# @private
|
75
|
+
def find_ig_and_profile(profile_url, _validator_name)
|
76
|
+
# Normally this would be done by a Test's validator,
|
77
|
+
# but here we're outside the context of a Test.
|
78
|
+
[ig, ig.profile_by_url(profile_url)]
|
374
79
|
end
|
375
80
|
end
|
376
81
|
end
|