inferno_core 0.5.2 → 0.5.3

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