inferno_core 0.6.1 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
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