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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/inferno/apps/cli/evaluate.rb +1 -30
  3. data/lib/inferno/apps/cli/new.rb +1 -2
  4. data/lib/inferno/apps/cli/templates/.env.development +1 -0
  5. data/lib/inferno/apps/cli/templates/.env.production +1 -0
  6. data/lib/inferno/apps/cli/templates/.gitignore +1 -0
  7. data/lib/inferno/apps/cli/templates/data/igs/.keep +0 -0
  8. data/lib/inferno/apps/cli/templates/docker-compose.background.yml.tt +2 -2
  9. data/lib/inferno/apps/web/controllers/controller.rb +3 -1
  10. data/lib/inferno/apps/web/router.rb +12 -6
  11. data/lib/inferno/config/boot/ig_files.rb +47 -0
  12. data/lib/inferno/config/boot/validator.rb +1 -0
  13. data/lib/inferno/config/boot/web.rb +6 -2
  14. data/lib/inferno/dsl/assertions.rb +26 -0
  15. data/lib/inferno/dsl/fhir_client_builder.rb +1 -0
  16. data/lib/inferno/dsl/fhir_evaluation/rules/all_must_supports_present.rb +13 -308
  17. data/lib/inferno/dsl/fhir_resource_validation.rb +34 -2
  18. data/lib/inferno/dsl/fhir_validation.rb +13 -0
  19. data/lib/inferno/dsl/must_support_assessment.rb +365 -0
  20. data/lib/inferno/dsl/results.rb +36 -4
  21. data/lib/inferno/dsl/runnable.rb +71 -0
  22. data/lib/inferno/dsl.rb +3 -1
  23. data/lib/inferno/entities/ig.rb +4 -1
  24. data/lib/inferno/exceptions.rb +6 -0
  25. data/lib/inferno/public/bundle.js +34 -34
  26. data/lib/inferno/public/bundle.js.LICENSE.txt +3 -3
  27. data/lib/inferno/repositories/igs.rb +122 -0
  28. data/lib/inferno/repositories/in_memory_repository.rb +7 -0
  29. data/lib/inferno/utils/ig_downloader.rb +17 -6
  30. data/lib/inferno/version.rb +1 -1
  31. data/spec/shared/test_kit_examples.rb +69 -0
  32. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 404dc10cd08aff285dbac620bdcf7330938e1f97ae2933e3c87dc399a2551551
4
- data.tar.gz: 1fed11cfea855b9b3543f01e19053cea9bd98dd589d18abe3310b13f7d519172
3
+ metadata.gz: 7c009d9329c63f4c737ce53c79d6b88af8db643550d2509784b6e3a43558d237
4
+ data.tar.gz: 5e0e2e2d7e861b38caa73ab2f6d83fc6a45a29a2e33744f67b8853df7766a5b1
5
5
  SHA512:
6
- metadata.gz: 9160481ca996a9ce316e8192c57bd72c15ff51a3eaff09fddec90283fbfd2ec8e1959fed8636744f79531ffffffbdf6addd0da165a24eed2bc741394c3285ae3
7
- data.tar.gz: 455f609148df1220216d6dba1862d8361719372d4a26e46a8303e864ab4041cfa9da3ac726cb6909aac204679b31a1c28f5d5a1d2c8a1fb5f1fcf77c7631c0ac
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 = get_ig(ig_path)
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)
@@ -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, config) : load_ig(ig, idx, config)
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
@@ -1,3 +1,4 @@
1
1
  FHIR_RESOURCE_VALIDATOR_URL=http://localhost/hl7validatorapi
2
2
  REDIS_URL=redis://localhost:6379/0
3
3
  FHIRPATH_URL=http://localhost/fhirpath
4
+ INFERNO_HOST=http://localhost:4567
@@ -1,3 +1,4 @@
1
1
  REDIS_URL=redis://redis:6379/0
2
2
  FHIR_RESOURCE_VALIDATOR_URL=http://hl7_validator_service:3500
3
3
  FHIRPATH_URL=http://fhirpath:6789
4
+ INFERNO_HOST=http://localhost
@@ -1,4 +1,5 @@
1
1
  /data/*.db
2
+ /data/igs/*.tgz
2
3
  /data/redis/**/*.rdb
3
4
  /data/redis/**/*.aof
4
5
  /data/redis/**/*.manifest
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
- - ./<%= ig_path %>:/app/igs
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
- - ./<%= ig_path %>:/home/igs
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 { |_req, res| res.format = :json if res.format == :all && res.body&.first&.first == '{' }
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(File.read(File.join(Inferno::Application.root, 'lib', 'inferno', 'apps', 'web',
8
- 'index.html.erb'))).result
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: ->(_env) { [200, {}, [{ 'version' => Inferno::VERSION.to_s }.to_json]] }, as: :version
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: ->(_env) { [200, { 'Content-Type' => 'text/html' }, [client_page]] }
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: ->(_env) { [200, {}, [client_page]] }
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: ->(_env) { [200, { 'Content-Type' => 'text/html' }, [client_page]] }
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
@@ -43,6 +43,7 @@ module Inferno
43
43
  instance_exec(self, &block)
44
44
 
45
45
  FHIR::Client.new(url).tap do |client|
46
+ client.use_accept_charset = false
46
47
  client.additional_headers = headers if headers
47
48
  client.default_json
48
49
  client.set_bearer_token bearer_token if bearer_token
@@ -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
- attr_accessor :metadata
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.profiles.each do |profile|
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
- profile_metadata = extract_metadata(profile, context.ig, requirement_extension:)
38
- missing_items = perform_must_support_test_with_metadata(resources, profile_metadata, debug_metadata:)
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
- # perform_must_support_test is invoked from DSL assertions, allows customizing the metadata with a block.
72
- # Customizing the metadata may add, modify, or remove items.
73
- # For instance, US Core 3.1.1 Patient "Previous Name" is defined as MS only in narrative.
74
- # Choices are also defined only in narrative.
75
- # @param profile [FHIR::StructureDefinition]
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