inferno_core 0.5.2 → 0.5.3

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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/lib/inferno/apps/cli/evaluate.rb +64 -0
  3. data/lib/inferno/apps/cli/main.rb +38 -0
  4. data/lib/inferno/apps/cli/templates/%library_name%.gemspec.tt +10 -3
  5. data/lib/inferno/apps/cli/templates/Dockerfile.tt +3 -2
  6. data/lib/inferno/apps/cli/templates/lib/%library_name%/metadata.rb.tt +18 -0
  7. data/lib/inferno/apps/cli/templates/lib/%library_name%/suite.rb.tt +59 -0
  8. data/lib/inferno/apps/cli/templates/lib/%library_name%/version.rb.tt +3 -0
  9. data/lib/inferno/apps/cli/templates/lib/%library_name%.rb.tt +1 -58
  10. data/lib/inferno/apps/web/serializers/input.rb +2 -1
  11. data/lib/inferno/apps/web/serializers/markdown_extractor.rb +16 -0
  12. data/lib/inferno/config/boot/presets.rb +18 -1
  13. data/lib/inferno/dsl/fhir_evaluation/config.rb +21 -0
  14. data/lib/inferno/dsl/fhir_evaluation/dataset_loader.rb +33 -0
  15. data/lib/inferno/dsl/fhir_evaluation/evaluation_context.rb +25 -0
  16. data/lib/inferno/dsl/fhir_evaluation/evaluation_result.rb +62 -0
  17. data/lib/inferno/dsl/fhir_evaluation/evaluator.rb +36 -0
  18. data/lib/inferno/dsl/fhir_evaluation/rule.rb +13 -0
  19. data/lib/inferno/dsl/suite_endpoint.rb +58 -58
  20. data/lib/inferno/dsl.rb +2 -0
  21. data/lib/inferno/entities/test_kit.rb +4 -2
  22. data/lib/inferno/entities/test_suite.rb +23 -3
  23. data/lib/inferno/entities.rb +1 -0
  24. data/lib/inferno/public/bundle.js +34 -34
  25. data/lib/inferno/repositories/presets.rb +12 -6
  26. data/lib/inferno/result_summarizer.rb +2 -0
  27. data/lib/inferno/utils/named_thor_actions.rb +5 -1
  28. data/lib/inferno/version.rb +1 -1
  29. metadata +14 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41fdda8d351bb039a654949835c70ed19999cbce0ccba735b4154774cdc61225
4
- data.tar.gz: 1c0a86692b2a231af565fd6695fcc229d0ea51c2acf55e4236a5ff551e0a83a9
3
+ metadata.gz: '090f68164e2fc19d1f97f62dd0fb13c12c77eb12c29bb9b528c9b9cb56f48770'
4
+ data.tar.gz: e239a1702ee11a85229db5355ad7c8a415d71b40018c052bb3e2c972c824d12c
5
5
  SHA512:
6
- metadata.gz: 5612c208f1aa22d56bdb168754e50c7dfb67d2b71a5b1699d42382f56994c782771d76865feb533cc1e87052d47540fd7d2fca5c708f5c2d02718c55b033c498
7
- data.tar.gz: 6dd48102d0ba87b339f5534312a949f390b69ad26741e2be73db1ec287b281efa9842c814ca6e8a4250745597909111b10759b2d171711686025c060194dad61
6
+ metadata.gz: 2491100649bba23576a442fdeeef561b407e5f7b0b89fbd0827b55ced37e1dcf29c1f5b908c316e44c38301a74d760b5fa8bba4b2cfdf5e039d36fab4c3c181f
7
+ data.tar.gz: e05eb12863387acecd5782267f25700d3a2e233ed8f581bda98627a76822c33b5e6a1f0ed8e770a6a9d971c88be610453f0aac9a13245b8412f2cb7673431327
@@ -0,0 +1,64 @@
1
+ require_relative '../../../inferno/dsl/fhir_evaluation/evaluator'
2
+
3
+ module Inferno
4
+ module CLI
5
+ class Evaluate
6
+ def run(ig_path, data_path, _log_level)
7
+ validate_args(ig_path, data_path)
8
+
9
+ # IG Import, rule execution, and result output below will be integrated at phase 2 and 3.
10
+
11
+ # @ig = File.join(__dir__, 'ig', ig_path)
12
+ # if data_path
13
+ # DatasetLoader.from_path(File.join(__dir__, data_path))
14
+ # else
15
+ # ig.examples
16
+ # end
17
+
18
+ # config = Config.new
19
+ # evaluator = Inferno::DSL::FHIREvaluation::Evaluator.new(data, config)
20
+
21
+ # results = evaluate()
22
+ # output_results(results, options[:output])
23
+ end
24
+
25
+ def validate_args(ig_path, data_path)
26
+ raise 'A path to an IG is required!' unless ig_path
27
+
28
+ return unless data_path && (!File.directory? data_path)
29
+
30
+ raise "Provided path '#{data_path}' is not a directory"
31
+ end
32
+
33
+ def output_results(results, output)
34
+ if output&.end_with?('json')
35
+ oo = FhirEvaluator::EvaluationResult.to_operation_outcome(results)
36
+ File.write(output, oo.to_json)
37
+ puts "Results written to #{output}"
38
+ else
39
+ counts = results.group_by(&:severity).transform_values(&:count)
40
+ print(counts, 'Result Count')
41
+ puts "\n"
42
+ puts results
43
+ end
44
+ end
45
+
46
+ def print(output_fields, title)
47
+ puts("╔══════════════ #{title} ═══════════════╗")
48
+ puts('║ ╭────────────────┬──────────────────────╮ ║')
49
+ output_fields.each_with_index do |(key, value), i|
50
+ field_name = pad(key, 14)
51
+ field_value = pad(value.to_s, 20)
52
+ puts("║ │ #{field_name} │ #{field_value} │ ║")
53
+ puts('║ ├────────────────┼──────────────────────┤ ║') unless i == output_fields.length - 1
54
+ end
55
+ puts('║ ╰────────────────┴──────────────────────╯ ║')
56
+ puts('╚═══════════════════════════════════════════╝')
57
+ end
58
+
59
+ def pad(string, length)
60
+ format("%#{length}.#{length}s", string)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,4 +1,5 @@
1
1
  require_relative 'console'
2
+ require_relative 'evaluate'
2
3
  require_relative 'migration'
3
4
  require_relative 'services'
4
5
  require_relative 'suite'
@@ -10,6 +11,43 @@ require_relative 'execute'
10
11
  module Inferno
11
12
  module CLI
12
13
  class Main < Thor
14
+ desc 'evaluate', 'Run a FHIR Data Evaluator.'
15
+ long_desc <<-LONGDESC
16
+ Evaluate FHIR data in the context of a given Implementation Guide,
17
+ by applying a set of predefined rules designed to check that datasets are comprehensive.
18
+ Issues identified will be printed to console or to a json file.
19
+
20
+ You must have background services running: `bundle exec inferno services start`
21
+
22
+ Run the evaluation CLI with
23
+
24
+ `bundle exec inferno evaluate ig_path`
25
+
26
+ Examples:
27
+
28
+ # Load the us core ig and evaluate the data in the provided example folder. If there are examples in the IG already, they will be ignored.
29
+ `bundle exec inferno evaluate ./uscore.tgz -d ./package/example`
30
+
31
+ # Loads the us core ig and evaluate the data included in the IG's example folder
32
+ `bundle exec inferno evaluate ./uscore.tgz`
33
+
34
+ # Loads the us core ig and evaluate the data included in the IG's example folder, with results redirected to outcome.json as an OperationOutcome
35
+ `bundle exec inferno evaluate ./uscore.tgz --output outcome.json`
36
+ LONGDESC
37
+ # TODO: Add options below as arguments
38
+ option :data_path,
39
+ aliases: ['-d'],
40
+ type: :string,
41
+ desc: 'Example FHIR data path'
42
+ # TODO: implement option of exporting result as OperationOutcome
43
+ option :output,
44
+ aliases: ['-o'],
45
+ type: :string,
46
+ desc: 'Export evaluation result to outcome.json as an OperationOutcome'
47
+ def evaluate(ig_path)
48
+ Evaluate.new.run(ig_path, options[:data_path], Logger::INFO)
49
+ end
50
+
13
51
  desc 'console', 'Start an interactive console session with Inferno'
14
52
  def console
15
53
  Migration.new.run(Logger::INFO)
@@ -1,11 +1,15 @@
1
+ require_relative 'lib/<%= library_name %>/version'
2
+
1
3
  Gem::Specification.new do |spec|
2
4
  spec.name = '<%= library_name %>'
3
- spec.version = '0.0.1'
5
+ spec.version = <%= module_name %>::VERSION
4
6
  spec.authors = <%= authors %>
5
7
  # spec.email = ['TODO']
6
8
  spec.date = Time.now.utc.strftime('%Y-%m-%d')
7
- spec.summary = '<%= title_name %> Test Kit'
8
- spec.description = '<%= human_name %> Inferno test kit for FHIR'
9
+ spec.summary = '<%= title_name %>'
10
+ # spec.description = <<~DESCRIPTION
11
+ # This is a big markdown description of the test kit.
12
+ # DESCRIPTION
9
13
  # spec.homepage = 'TODO'
10
14
  spec.license = 'Apache-2.0'
11
15
  spec.add_runtime_dependency 'inferno_core', '~> <%= Inferno::VERSION %>'
@@ -14,11 +18,14 @@ Gem::Specification.new do |spec|
14
18
  spec.add_development_dependency 'rspec', '~> 3.10'
15
19
  spec.add_development_dependency 'webmock', '~> 3.11'
16
20
  spec.required_ruby_version = Gem::Requirement.new('>= 3.1.2')
21
+ spec.metadata['inferno_test_kit'] = 'true'
17
22
  # spec.metadata['homepage_uri'] = spec.homepage
18
23
  # spec.metadata['source_code_uri'] = 'TODO'
19
24
  spec.files = [
20
25
  Dir['lib/**/*.rb'],
21
26
  Dir['lib/**/*.json'],
27
+ Dir['config/presets/*.json'],
28
+ Dir['config/presets/*.json.erb'],
22
29
  'LICENSE'
23
30
  ].flatten
24
31
 
@@ -6,12 +6,13 @@ RUN mkdir -p $INSTALL_PATH
6
6
 
7
7
  WORKDIR $INSTALL_PATH
8
8
 
9
+ ADD lib/<%= library_name %>/metadata.rb $INSTALL_PATH/lib/<%= library_name %>/metadata.rb
9
10
  ADD *.gemspec $INSTALL_PATH
10
11
  ADD Gemfile* $INSTALL_PATH
11
12
  RUN gem install bundler
12
- # The below RUN line is commented out for development purposes, because any change to the
13
+ # The below RUN line is commented out for development purposes, because any change to the
13
14
  # required gems will break the dockerfile build process.
14
- # If you want to run in Deploy mode, just run `bundle install` locally to update
15
+ # If you want to run in Deploy mode, just run `bundle install` locally to update
15
16
  # Gemfile.lock, and uncomment the following line.
16
17
  # RUN bundle config set --local deployment 'true'
17
18
  RUN bundle install
@@ -0,0 +1,18 @@
1
+ require_relative 'version'
2
+
3
+ module <%= module_name %>
4
+ class Metadata < Inferno::TestKit
5
+ id :<%= test_kit_id %>
6
+ title '<%= title_name %>'
7
+ description <<~DESCRIPTION
8
+ This is a big markdown description of the test kit.
9
+ DESCRIPTION
10
+ suite_ids [:<%= test_suite_id %>]
11
+ # tags ['SMART App Launch', 'US Core']
12
+ # last_updated '2024-03-07'
13
+ version VERSION
14
+ maturity 'Low'
15
+ authors <%= authors %>
16
+ # repo 'TODO'
17
+ end
18
+ end
@@ -0,0 +1,59 @@
1
+ require_relative 'metadata'
2
+ require_relative 'patient_group'
3
+
4
+ module <%= module_name %>
5
+ class Suite < Inferno::TestSuite
6
+ id :<%= test_suite_id %>
7
+ title '<%= title_name %> Test Suite'
8
+ description '<%= human_name %> test suite.'
9
+
10
+ # These inputs will be available to all tests in this suite
11
+ input :url,
12
+ title: 'FHIR Server Base Url'
13
+
14
+ input :credentials,
15
+ title: 'OAuth Credentials',
16
+ type: :oauth_credentials,
17
+ optional: true
18
+
19
+ # All FHIR requests in this suite will use this FHIR client
20
+ fhir_client do
21
+ url :url
22
+ oauth_credentials :credentials
23
+ end
24
+
25
+ # All FHIR validation requests will use this FHIR validator
26
+ fhir_resource_validator do
27
+ # igs 'identifier#version' # Use this method for published IGs/versions
28
+ # igs 'igs/filename.tgz' # Use this otherwise
29
+
30
+ exclude_message do |message|
31
+ message.message.match?(/\A\S+: \S+: URL value '.*' does not resolve/)
32
+ end
33
+ end
34
+
35
+ # Tests and TestGroups can be defined inline
36
+ group do
37
+ id :capability_statement
38
+ title 'Capability Statement'
39
+ description 'Verify that the server has a CapabilityStatement'
40
+
41
+ test do
42
+ id :capability_statement_read
43
+ title 'Read CapabilityStatement'
44
+ description 'Read CapabilityStatement from /metadata endpoint'
45
+
46
+ run do
47
+ fhir_get_capability_statement
48
+
49
+ assert_response_status(200)
50
+ assert_resource_type(:capability_statement)
51
+ end
52
+ end
53
+ end
54
+
55
+ # Tests and TestGroups can be written in separate files and then included
56
+ # using their id
57
+ group from: :patient_group
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module <%= module_name %>
2
+ VERSION = '0.0.0'.freeze
3
+ end
@@ -1,58 +1 @@
1
- require_relative '<%= library_name %>/patient_group'
2
-
3
- module <%= module_name %>
4
- class Suite < Inferno::TestSuite
5
- id :<%= test_suite_id %>
6
- title '<%= title_name %> Test Suite'
7
- description '<%= human_name %> test suite.'
8
-
9
- # These inputs will be available to all tests in this suite
10
- input :url,
11
- title: 'FHIR Server Base Url'
12
-
13
- input :credentials,
14
- title: 'OAuth Credentials',
15
- type: :oauth_credentials,
16
- optional: true
17
-
18
- # All FHIR requests in this suite will use this FHIR client
19
- fhir_client do
20
- url :url
21
- oauth_credentials :credentials
22
- end
23
-
24
- # All FHIR validation requests will use this FHIR validator
25
- fhir_resource_validator do
26
- # igs 'identifier#version' # Use this method for published IGs/versions
27
- # igs 'igs/filename.tgz' # Use this otherwise
28
-
29
- exclude_message do |message|
30
- message.message.match?(/\A\S+: \S+: URL value '.*' does not resolve/)
31
- end
32
- end
33
-
34
- # Tests and TestGroups can be defined inline
35
- group do
36
- id :capability_statement
37
- title 'Capability Statement'
38
- description 'Verify that the server has a CapabilityStatement'
39
-
40
- test do
41
- id :capability_statement_read
42
- title 'Read CapabilityStatement'
43
- description 'Read CapabilityStatement from /metadata endpoint'
44
-
45
- run do
46
- fhir_get_capability_statement
47
-
48
- assert_response_status(200)
49
- assert_resource_type(:capability_statement)
50
- end
51
- end
52
- end
53
-
54
- # Tests and TestGroups can be written in separate files and then included
55
- # using their id
56
- group from: :patient_group
57
- end
58
- end
1
+ require_relative '<%= library_name %>/suite'
@@ -1,3 +1,4 @@
1
+ require_relative 'markdown_extractor'
1
2
  require_relative 'serializer'
2
3
 
3
4
  module Inferno
@@ -7,7 +8,7 @@ module Inferno
7
8
  identifier :name
8
9
 
9
10
  field :title, if: :field_present?
10
- field :description, if: :field_present?
11
+ field :description, extractor: MarkdownExtractor, if: :field_present?
11
12
  field :type, if: :field_present?
12
13
  field :default, if: :field_present?
13
14
  field :optional, if: :field_present?
@@ -0,0 +1,16 @@
1
+ require 'blueprinter'
2
+ require_relative '../../../utils/markdown_formatter'
3
+
4
+ module Inferno
5
+ module Web
6
+ module Serializers
7
+ class MarkdownExtractor < Blueprinter::Extractor
8
+ include Inferno::Utils::MarkdownFormatter
9
+
10
+ def extract(field_name, object, _local_options, _options = {})
11
+ format_markdown(object.send(field_name))
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -4,9 +4,26 @@ Inferno::Application.register_provider(:presets) do
4
4
  prepare do
5
5
  target_container.start :suites
6
6
 
7
+ presets_repo = Inferno::Repositories::Presets.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
+
7
15
  files_to_load = Dir.glob(['config/presets/*.json', 'config/presets/*.json.erb'])
16
+ files_to_load +=
17
+ test_kit_gems.flat_map do |gem|
18
+ [
19
+ Dir.glob([File.join(gem.full_gem_path, 'config', 'presets', '*.json')]),
20
+ Dir.glob([File.join(gem.full_gem_path, 'config', 'presets', '*.json.erb')])
21
+ ].flatten
22
+ end
23
+
24
+ files_to_load.compact!
25
+ files_to_load.uniq!
8
26
  files_to_load.map! { |path| File.realpath(path) }
9
- presets_repo = Inferno::Repositories::Presets.new
10
27
 
11
28
  files_to_load.each do |path|
12
29
  presets_repo.insert_from_file(path)
@@ -0,0 +1,21 @@
1
+ module Inferno
2
+ module DSL
3
+ module FHIREvaluation
4
+ class Config
5
+ DEFAULT_FILE = File.join(__dir__, 'default.yml')
6
+ attr_accessor :data
7
+
8
+ # To-do: add config_file as arguments
9
+ def initialize(config_file = nil)
10
+ @data = if config_file.nil?
11
+ YAML.load_file(File.absolute_path(DEFAULT_FILE))
12
+ else
13
+ YAML.load_file(File.absolute_path(config_file))
14
+ end
15
+
16
+ raise(TypeError, 'Malformed configuration') unless @data.is_a?(Hash)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,33 @@
1
+ module Inferno
2
+ module DSL
3
+ module FHIREvaluation
4
+ module DatasetLoader
5
+ def self.from_contents(source_array)
6
+ dataset = []
7
+
8
+ source_array.each do |json|
9
+ resource = FHIR::Json.from_json(json)
10
+ next if resource.nil?
11
+
12
+ dataset.push resource
13
+ end
14
+
15
+ dataset
16
+ end
17
+
18
+ def self.from_path(path)
19
+ dataset = []
20
+
21
+ Dir["#{path}/*.json"].each do |f|
22
+ resource = FHIR::Json.from_json(File.read(f))
23
+ next if resource.nil?
24
+
25
+ dataset.push resource
26
+ end
27
+
28
+ dataset
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ module Inferno
2
+ module DSL
3
+ module FHIREvaluation
4
+ # EvaluationContext is a wrapper class around the concepts needed to perform an evaluation:
5
+ # - The IG used as the basis for evaluation
6
+ # - The data being evaluated
7
+ # - A summary/characterization of the data
8
+ # - Evaluation results
9
+ class EvaluationContext
10
+ attr_reader :ig, :data, :results, :config
11
+
12
+ def initialize(ig, data, config) # rubocop:disable Naming/MethodParameterName
13
+ @ig = ig
14
+ @data = data
15
+ @results = []
16
+ @config = config
17
+ end
18
+
19
+ def add_result(result)
20
+ results.push result
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,62 @@
1
+ module Inferno
2
+ module DSL
3
+ module FHIREvaluation
4
+ # The result of a Rule evaluating a data set.
5
+ class EvaluationResult
6
+ attr_accessor :message,
7
+ :severity, # fatal | error | warning | information | success
8
+ :issue_type, # https://www.hl7.org/fhir/valueset-issue-type.html
9
+ :threshold, # quantitative value that a rule checks for
10
+ :value, # actual observed value
11
+ :rule # Rule that produced this result
12
+
13
+ def initialize(message, severity: 'warning', issue_type: 'business-rule', threshold: nil, value: nil, rule: nil)
14
+ @message = message
15
+ @severity = severity
16
+ @issue_type = issue_type
17
+ @threshold = threshold
18
+ @value = value
19
+ @rule = rule
20
+ end
21
+
22
+ def to_s
23
+ "#{severity.upcase}: #{message}"
24
+ end
25
+
26
+ def to_oo_issue
27
+ issue = {
28
+ severity:,
29
+ code: issue_type,
30
+ details: { text: message }
31
+ }
32
+
33
+ if threshold
34
+ issue[:extension] ||= []
35
+ issue[:extension].push({
36
+ # TODO: pick real extension for this
37
+ url: 'https://inferno-framework.github.io/fhir_evaluator/StructureDefinition/operationoutcome-issue-threshold',
38
+ valueDecimal: threshold
39
+ })
40
+ end
41
+
42
+ if value
43
+ issue[:extension] ||= []
44
+ issue[:extension].push({
45
+ # TODO: pick real extension for this
46
+ url: 'https://inferno-framework.github.io/fhir_evaluator/StructureDefinition/operationoutcome-issue-value',
47
+ valueDecimal: value
48
+ })
49
+ end
50
+
51
+ issue
52
+ end
53
+
54
+ def self.to_operation_outcome(results)
55
+ FHIR::OperationOutcome.new({
56
+ issue: results.map(&:to_oo_issue)
57
+ })
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'config'
4
+ require_relative 'rule'
5
+ require_relative 'evaluation_context'
6
+ require_relative 'evaluation_result'
7
+ require_relative 'dataset_loader'
8
+
9
+ module Inferno
10
+ module DSL
11
+ module FHIREvaluation
12
+ class Evaluator
13
+ attr_accessor :ig
14
+
15
+ def initialize(ig) # rubocop:disable Naming/MethodParameterName
16
+ @ig = ig
17
+ end
18
+
19
+ def evaluate(data, config = Config.new)
20
+ context = EvaluationContext.new(@ig, data, config)
21
+
22
+ active_rules = []
23
+ config.data['Rule'].each do |rulename, rule_details|
24
+ active_rules << rulename if rule_details['Enabled']
25
+ end
26
+
27
+ Rule.descendants.each do |rule|
28
+ rule.new.check(context) if active_rules.include?(rule.name.demodulize)
29
+ end
30
+
31
+ context.results
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inferno
4
+ module DSL
5
+ module FHIREvaluation
6
+ class Rule
7
+ def check(_context)
8
+ raise 'not implemented'
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end