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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 511a1a15fdcb49bcf9a58821b087d8a42394085f6c735971857a2e45710b7269
4
- data.tar.gz: 2635bf69d9662b8afbaea341348d8dbc446707c6804be1f491c555376b8e31a6
3
+ metadata.gz: 7789d88730cda670cc051b8d0d56d1a9d3f8fba3133628eda872eddebd57abf9
4
+ data.tar.gz: 7f617722c5dd12f6ed349dd474954b7af2fc499420815e4087a18af68dbe5024
5
5
  SHA512:
6
- metadata.gz: 3a31b3fe1f72ff67cc2be3c8ff11d7dfd7f6c7c203e84c55b2072f205343c6c522686accc2b927088d31ade270fa127d8ad741fbced1827497f4a3d8102d9df3
7
- data.tar.gz: 1bc84724a28ae55cecc8550b51143f3c2f4ff414891c8241012ca1f94e7ea20915a801d3c3616b4ab5a4667caf12f42a273f589ad2537ab13faec9aa36144c7e
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
- _ig = get_ig(ig_path)
16
+ ig = get_ig(ig_path)
16
17
 
17
- # Rule execution, and result output below will be integrated soon.
18
+ check_ig_version(ig)
18
19
 
19
- # if data_path
20
- # DatasetLoader.from_path(File.join(__dir__, data_path))
21
- # else
22
- # ig.examples
23
- # end
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
- # config = Config.new
26
- # evaluator = Inferno::DSL::FHIREvaluation::Evaluator.new(data, config)
27
+ evaluator = Inferno::DSL::FHIREvaluation::Evaluator.new(ig)
27
28
 
28
- # results = evaluate()
29
- # output_results(results, options[:output])
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) # rubocop:disable Naming/MethodParameterName
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) # rubocop:disable Naming/MethodParameterName
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