inferno_core 0.5.1 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/lib/inferno/apps/cli/evaluate.rb +64 -0
  3. data/lib/inferno/apps/cli/execute.rb +49 -4
  4. data/lib/inferno/apps/cli/main.rb +48 -2
  5. data/lib/inferno/apps/cli/templates/%library_name%.gemspec.tt +10 -3
  6. data/lib/inferno/apps/cli/templates/Dockerfile.tt +3 -2
  7. data/lib/inferno/apps/cli/templates/lib/%library_name%/metadata.rb.tt +18 -0
  8. data/lib/inferno/apps/cli/templates/lib/%library_name%/suite.rb.tt +59 -0
  9. data/lib/inferno/apps/cli/templates/lib/%library_name%/version.rb.tt +3 -0
  10. data/lib/inferno/apps/cli/templates/lib/%library_name%.rb.tt +1 -58
  11. data/lib/inferno/apps/web/application.rb +4 -0
  12. data/lib/inferno/apps/web/index.html.erb +12 -4
  13. data/lib/inferno/apps/web/serializers/input.rb +2 -1
  14. data/lib/inferno/apps/web/serializers/markdown_extractor.rb +16 -0
  15. data/lib/inferno/config/boot/executor.rb +1 -0
  16. data/lib/inferno/config/boot/presets.rb +18 -1
  17. data/lib/inferno/dsl/fhir_evaluation/config.rb +21 -0
  18. data/lib/inferno/dsl/fhir_evaluation/dataset_loader.rb +33 -0
  19. data/lib/inferno/dsl/fhir_evaluation/evaluation_context.rb +25 -0
  20. data/lib/inferno/dsl/fhir_evaluation/evaluation_result.rb +62 -0
  21. data/lib/inferno/dsl/fhir_evaluation/evaluator.rb +36 -0
  22. data/lib/inferno/dsl/fhir_evaluation/rule.rb +13 -0
  23. data/lib/inferno/dsl/suite_endpoint.rb +58 -58
  24. data/lib/inferno/dsl.rb +2 -0
  25. data/lib/inferno/entities/test_kit.rb +4 -2
  26. data/lib/inferno/entities/test_suite.rb +23 -3
  27. data/lib/inferno/entities.rb +1 -0
  28. data/lib/inferno/ext/json_parser.rb +11 -0
  29. data/lib/inferno/repositories/presets.rb +12 -6
  30. data/lib/inferno/result_summarizer.rb +2 -0
  31. data/lib/inferno/utils/named_thor_actions.rb +5 -1
  32. data/lib/inferno/version.rb +1 -1
  33. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2266ffdfdf00fea5f565f6b1d5f927ba0c6c64b4d5b5725b737bdb822fe231cd
4
- data.tar.gz: bfb17c44e6dabfb9ba46b153e25c1c62aaae6179eaf202505213ab920e4cbcca
3
+ metadata.gz: '090f68164e2fc19d1f97f62dd0fb13c12c77eb12c29bb9b528c9b9cb56f48770'
4
+ data.tar.gz: e239a1702ee11a85229db5355ad7c8a415d71b40018c052bb3e2c972c824d12c
5
5
  SHA512:
6
- metadata.gz: a12e012b24105c8f7e3feb22bd12612ccd4c0f4b345b3289d8fc0c946f1229581f476d1a984418e768b6c0baa65145fcd07f2dd27728ca76508328577de4812b
7
- data.tar.gz: f0aabd22881fddc9ad3292e15daf717dd5009fc63d46a947a2696c6d684b11ea239cd669d9e89a0efe0b0eb6233a04fe7e3ae4b8839fb024153a555d7b976fe7
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
@@ -44,10 +44,12 @@ module Inferno
44
44
 
45
45
  self.options = options
46
46
 
47
- outputter.print_start_message(options)
47
+ outputter.print_start_message(self.options)
48
+
49
+ load_preset_file_and_set_preset_id
48
50
 
49
51
  results = []
50
- outputter.print_around_run(options) do
52
+ outputter.print_around_run(self.options) do
51
53
  if all_selected_groups_and_tests.empty?
52
54
  test_run = create_test_run(suite)
53
55
  run_one(suite, test_run)
@@ -102,6 +104,18 @@ module Inferno
102
104
  @outputter ||= OUTPUTTERS[options[:outputter]].new
103
105
  end
104
106
 
107
+ def load_preset_file_and_set_preset_id
108
+ return unless options[:preset_file]
109
+ raise StandardError, 'Cannot use `--preset-id` and `--preset-file` options together' if options[:preset_id]
110
+
111
+ raise StandardError, "File #{options[:preset_file]} not found" unless File.exist? options[:preset_file]
112
+
113
+ options[:preset_id] = JSON.parse(File.read(options[:preset_file]))['id']
114
+ raise StandardError, "Preset #{options[:preset_file]} is missing id" if options[:preset_id].nil?
115
+
116
+ presets_repo.insert_from_file(options[:preset_file])
117
+ end
118
+
105
119
  def all_selected_groups_and_tests
106
120
  @all_selected_groups_and_tests ||= runnables_by_short_id + groups + tests
107
121
  end
@@ -109,7 +123,7 @@ module Inferno
109
123
  def run_one(runnable, test_run)
110
124
  verify_runnable(
111
125
  runnable,
112
- thor_hash_to_inputs_array(options[:inputs]),
126
+ thor_hash_to_inputs_array(inputs_and_preset),
113
127
  test_session.suite_options
114
128
  )
115
129
 
@@ -118,6 +132,33 @@ module Inferno
118
132
  dispatch_job(test_run)
119
133
  end
120
134
 
135
+ def inputs_and_preset
136
+ if preset
137
+ preset_inputs = preset.inputs.to_h do |preset_input|
138
+ [preset_input[:name], preset_input[:value]]
139
+ end
140
+
141
+ options.fetch(:inputs, {}).reverse_merge(preset_inputs)
142
+ else
143
+ options.fetch(:inputs, {})
144
+ end
145
+ end
146
+
147
+ def preset
148
+ return unless options[:preset_id]
149
+
150
+ @preset ||= presets_repo.find(options[:preset_id])
151
+
152
+ raise StandardError, "Preset #{options[:preset_id]} not found" if @preset.nil?
153
+
154
+ unless presets_repo.presets_for_suite(suite.id).include?(@preset)
155
+ raise StandardError,
156
+ "Preset #{options[:preset_id]} is incompatible with suite #{suite.id}"
157
+ end
158
+
159
+ @preset
160
+ end
161
+
121
162
  def suite
122
163
  @suite ||= Inferno::Repositories::TestSuites.new.find(options[:suite])
123
164
 
@@ -156,6 +197,10 @@ module Inferno
156
197
  @session_data_repo ||= Inferno::Repositories::SessionData.new
157
198
  end
158
199
 
200
+ def presets_repo
201
+ @presets_repo ||= Inferno::Repositories::Presets.new
202
+ end
203
+
159
204
  def test_session
160
205
  @test_session ||= test_sessions_repo.create({
161
206
  test_suite_id: suite.id,
@@ -169,7 +214,7 @@ module Inferno
169
214
  {
170
215
  test_session_id: test_session.id,
171
216
  runnable_id_key(runnable) => runnable.id,
172
- inputs: thor_hash_to_inputs_array(options[:inputs])
217
+ inputs: thor_hash_to_inputs_array(inputs_and_preset)
173
218
  }
174
219
  end
175
220
 
@@ -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)
@@ -125,7 +163,15 @@ module Inferno
125
163
  option :inputs,
126
164
  aliases: ['-i'],
127
165
  type: :hash,
128
- desc: 'Inputs (i.e: --inputs=foo:bar goo:baz)'
166
+ desc: 'Inputs (i.e: --inputs=foo:bar goo:baz); will merge and override preset inputs'
167
+ option :preset_id,
168
+ aliases: ['-P'],
169
+ type: :string,
170
+ desc: 'Inferno preset id; cannot be used with `--preset-file`'
171
+ option :preset_file,
172
+ aliases: ['-p'],
173
+ type: :string,
174
+ desc: 'Path to an Inferno preset file for inputs; cannot be used with `--preset-id`'
129
175
  option :outputter,
130
176
  aliases: ['-o'],
131
177
  default: 'console',
@@ -142,7 +188,7 @@ module Inferno
142
188
  desc: 'Display this message'
143
189
  def execute
144
190
  Execute.boot_full_inferno
145
- Execute.new.run(options)
191
+ Execute.new.run(options.dup) # dup to unfreeze Thor options
146
192
  end
147
193
 
148
194
  # https://github.com/rails/thor/issues/244 - Make Thor exit(1) on Errors/Exceptions
@@ -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,6 +1,10 @@
1
1
  require 'hanami/middleware/body_parser'
2
2
  require_relative 'router'
3
3
 
4
+ # Only required to monkey patch the JSON parser to support application/fhir+json
5
+ require 'hanami/middleware/body_parser/json_parser'
6
+ require_relative '../../ext/json_parser'
7
+
4
8
  module Inferno
5
9
  module Web
6
10
  def self.app
@@ -9,10 +9,18 @@
9
9
  <meta name="viewport" content="width=device-width, initial-scale=1" />
10
10
  <meta name="theme-color" content="#000000" />
11
11
  <meta id="base-path" name="base-path" content="<%= Inferno::Application['base_path'] %>">
12
- <meta
13
- name="description"
14
- content="FHIR Testing"
15
- />
12
+
13
+ <!-- Social media link unfurling meta tags -->
14
+ <title>Inferno Test Session</title>
15
+ <link rel="canonical" href="<%= Inferno::Application['base_url'] %>" />
16
+ <meta name="application-name" content="Inferno" />
17
+ <meta name="og:image" content="<%= Inferno::Application['inferno_host'] %><%= Inferno::Application['public_path'] %>/logo192.png" />
18
+ <meta name="og:type" content="website" />
19
+ <meta name="og:url" content="<%= Inferno::Application['base_url'] %>" />
20
+ <meta name="og:site_name" content="Inferno" />
21
+ <meta name="twitter:card" content="summary" />
22
+ <meta name="twitter:image" content="<%= Inferno::Application['inferno_host'] %><%= Inferno::Application['public_path'] %>/logo192.png" />
23
+
16
24
  <link rel="apple-touch-icon" href="<%= Inferno::Application['public_path'] %>/logo192.png" />
17
25
  <!--
18
26
  manifest.json provides metadata used when your web app is installed on a
@@ -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
@@ -10,5 +10,6 @@ Inferno::Application.register_provider(:executor) do
10
10
  end
11
11
 
12
12
  target_container.start :suites
13
+ target_container.start :presets
13
14
  end
14
15
  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
@@ -9,54 +9,54 @@ module Inferno
9
9
  # endpoint](https://github.com/hanami/controller/tree/v2.0.0).
10
10
  #
11
11
  # @example
12
- # class AuthorizedEndpoint < Inferno::DSL::SuiteEndpoint
13
- # # Identify the incoming request based on a bearer token
14
- # def test_run_identifier
15
- # request.header['authorization']&.delete_prefix('Bearer ')
16
- # end
12
+ # class AuthorizedEndpoint < Inferno::DSL::SuiteEndpoint
13
+ # # Identify the incoming request based on a bearer token
14
+ # def test_run_identifier
15
+ # request.header['authorization']&.delete_prefix('Bearer ')
16
+ # end
17
17
  #
18
- # # Return a json FHIR Patient resource
19
- # def make_response
20
- # response.status = 200
21
- # response.body = FHIR::Patient.new(id: 'abcdef').to_json
22
- # response.format = :json
23
- # end
18
+ # # Return a json FHIR Patient resource
19
+ # def make_response
20
+ # response.status = 200
21
+ # response.body = FHIR::Patient.new(id: 'abcdef').to_json
22
+ # response.format = :json
23
+ # end
24
24
  #
25
- # # Update the waiting test to pass when the incoming request is received.
26
- # # This will resume the test run.
27
- # def update_result
28
- # results_repo.update(result.id, result: 'pass')
29
- # end
25
+ # # Update the waiting test to pass when the incoming request is received.
26
+ # # This will resume the test run.
27
+ # def update_result
28
+ # results_repo.update(result.id, result: 'pass')
29
+ # end
30
30
  #
31
- # # Apply the 'authorized' tag to the incoming request so that it may be
32
- # # used by later tests.
33
- # def tags
34
- # ['authorized']
35
- # end
36
- # end
31
+ # # Apply the 'authorized' tag to the incoming request so that it may be
32
+ # # used by later tests.
33
+ # def tags
34
+ # ['authorized']
35
+ # end
36
+ # end
37
37
  #
38
- # class AuthorizedRequestSuite < Inferno::TestSuite
39
- # id :authorized_suite
40
- # suite_endpoint :get, '/authorized_endpoint', AuthorizedEndpoint
38
+ # class AuthorizedRequestSuite < Inferno::TestSuite
39
+ # id :authorized_suite
40
+ # suite_endpoint :get, '/authorized_endpoint', AuthorizedEndpoint
41
41
  #
42
- # group do
43
- # title 'Authorized Request Group'
42
+ # group do
43
+ # title 'Authorized Request Group'
44
44
  #
45
- # test do
46
- # title 'Wait for authorized request'
45
+ # test do
46
+ # title 'Wait for authorized request'
47
47
  #
48
- # input :bearer_token
48
+ # input :bearer_token
49
49
  #
50
- # run do
51
- # wait(
52
- # identifier: bearer_token,
53
- # message: "Waiting to receive a request with bearer_token: #{bearer_token}" \
54
- # "at `#{Inferno::Application['base_url']}/custom/authorized_suite/authorized_endpoint`"
55
- # )
50
+ # run do
51
+ # wait(
52
+ # identifier: bearer_token,
53
+ # message: "Waiting to receive a request with bearer_token: #{bearer_token}" \
54
+ # "at `#{Inferno::Application['base_url']}/custom/authorized_suite/authorized_endpoint`"
55
+ # )
56
+ # end
57
+ # end
56
58
  # end
57
59
  # end
58
- # end
59
- # end
60
60
  class SuiteEndpoint < Hanami::Action
61
61
  attr_reader :req, :res
62
62
 
@@ -69,11 +69,11 @@ module Inferno
69
69
  # @return [String]
70
70
  #
71
71
  # @example
72
- # def test_run_identifier
73
- # # Identify the test session of an incoming request based on the bearer
74
- # # token
75
- # request.headers['authorization']&.delete_prefix('Bearer ')
76
- # end
72
+ # def test_run_identifier
73
+ # # Identify the test session of an incoming request based on the bearer
74
+ # # token
75
+ # request.headers['authorization']&.delete_prefix('Bearer ')
76
+ # end
77
77
  def test_run_identifier
78
78
  nil
79
79
  end
@@ -83,11 +83,11 @@ module Inferno
83
83
  # @return [Void]
84
84
  #
85
85
  # @example
86
- # def make_response
87
- # response.status = 200
88
- # response.body = { abc: 123 }.to_json
89
- # response.format = :json
90
- # end
86
+ # def make_response
87
+ # response.status = 200
88
+ # response.body = { abc: 123 }.to_json
89
+ # response.format = :json
90
+ # end
91
91
  def make_response
92
92
  nil
93
93
  end
@@ -113,9 +113,9 @@ module Inferno
113
113
  # @return [Void]
114
114
  #
115
115
  # @example
116
- # def update_result
117
- # results_repo.update(result.id, result: 'pass')
118
- # end
116
+ # def update_result
117
+ # results_repo.update(result.id, result: 'pass')
118
+ # end
119
119
  def update_result
120
120
  nil
121
121
  end
@@ -165,9 +165,9 @@ module Inferno
165
165
  # @return [Hanami::Action::Request]
166
166
  #
167
167
  # @example
168
- # request.params # Get url/query params
169
- # request.body.read # Get body
170
- # request.headers['accept'] # Get Accept header
168
+ # request.params # Get url/query params
169
+ # request.body.read # Get body
170
+ # request.headers['accept'] # Get Accept header
171
171
  def request
172
172
  req
173
173
  end
@@ -178,10 +178,10 @@ module Inferno
178
178
  # @return [Hanami::Action::Response]
179
179
  #
180
180
  # @example
181
- # response.status = 200 # Set the status
182
- # response.body = 'Ok' # Set the body
183
- # # Set headers
184
- # response.headers.merge!('X-Custom-Header' => 'CUSTOM_HEADER_VALUE')
181
+ # response.status = 200 # Set the status
182
+ # response.body = 'Ok' # Set the body
183
+ # # Set headers
184
+ # response.headers.merge!('X-Custom-Header' => 'CUSTOM_HEADER_VALUE')
185
185
  def response
186
186
  res
187
187
  end
data/lib/inferno/dsl.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require_relative 'dsl/assertions'
2
2
  require_relative 'dsl/fhir_client'
3
3
  require_relative 'dsl/fhir_validation'
4
+ require_relative 'dsl/fhir_evaluation/evaluator'
4
5
  require_relative 'dsl/fhir_resource_validation'
5
6
  require_relative 'dsl/fhirpath_evaluation'
6
7
  require_relative 'dsl/http_client'
@@ -18,6 +19,7 @@ module Inferno
18
19
  HTTPClient,
19
20
  Results,
20
21
  FHIRValidation,
22
+ FHIREvaluation,
21
23
  FHIRResourceValidation,
22
24
  FhirpathEvaluation,
23
25
  Messages
@@ -6,7 +6,7 @@ module Inferno
6
6
  # @example
7
7
  #
8
8
  # module USCoreTestKit
9
- # class TestKit < Inferno::Entities::TestKit
9
+ # class Metadata < Inferno::Entities::TestKit
10
10
  # id :us_core
11
11
  # title 'US Core Test Kit'
12
12
  # description <<~DESCRIPTION
@@ -156,7 +156,7 @@ module Inferno
156
156
 
157
157
  # @private
158
158
  def repository
159
- @repository ||= Inferno::Repositories::TestKits
159
+ @repository ||= Inferno::Repositories::TestKits.new
160
160
  end
161
161
 
162
162
  # @private
@@ -168,4 +168,6 @@ module Inferno
168
168
  end
169
169
  end
170
170
  end
171
+
172
+ TestKit = Entities::TestKit
171
173
  end
@@ -84,15 +84,16 @@ module Inferno
84
84
  }
85
85
  end
86
86
 
87
- # Set/get the version of this test suite.
87
+ # Set/get the version of this test suite. Defaults to the TestKit
88
+ # version.
88
89
  #
89
90
  # @param version [String]
90
91
  #
91
92
  # @return [String, nil]
92
93
  def version(version = nil)
93
- return @version if version.nil?
94
+ @version = version if version.present?
94
95
 
95
- @version = version
96
+ @version || test_kit&.version
96
97
  end
97
98
 
98
99
  # @private
@@ -186,6 +187,25 @@ module Inferno
186
187
 
187
188
  @suite_summary = format_markdown(suite_summary)
188
189
  end
190
+
191
+ # Get the TestKit this suite belongs to
192
+ #
193
+ # @return [Inferno::Entities::TestKit]
194
+ def test_kit
195
+ return @test_kit if @test_kit
196
+
197
+ module_name = name
198
+
199
+ while module_name.present? && @test_kit.nil?
200
+ module_name = module_name.deconstantize
201
+
202
+ next unless const_defined?("#{module_name}::Metadata")
203
+
204
+ @test_kit = const_get("#{module_name}::Metadata")
205
+ end
206
+
207
+ @test_kit
208
+ end
189
209
  end
190
210
  end
191
211
  end
@@ -8,6 +8,7 @@ require_relative 'entities/result'
8
8
  require_relative 'entities/session_data'
9
9
  require_relative 'entities/test'
10
10
  require_relative 'entities/test_group'
11
+ require_relative 'entities/test_kit'
11
12
  require_relative 'entities/test_run'
12
13
  require_relative 'entities/test_session'
13
14
  require_relative 'entities/test_suite'
@@ -0,0 +1,11 @@
1
+ module Hanami
2
+ module Middleware
3
+ class BodyParser
4
+ class JsonParser
5
+ def self.mime_types
6
+ ['application/json', 'application/vnd.api+json', 'application/fhir+json']
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -8,14 +8,20 @@ module Inferno
8
8
  # Repository that deals with persistence for the `Preset` entity.
9
9
  class Presets < InMemoryRepository
10
10
  def insert_from_file(path)
11
- case path
12
- when /\.json$/
13
- preset_hash = JSON.parse(File.read(path))
14
- when /\.erb$/
15
- templated = ERB.new(File.read(path)).result
16
- preset_hash = JSON.parse(templated)
11
+ raw_contents =
12
+ case path
13
+ when /\.json$/
14
+ File.read(path)
15
+ when /\.erb$/
16
+ ERB.new(File.read(path)).result
17
+ end
18
+
19
+ if Application['base_url'].start_with? 'https://inferno-qa.healthit.gov'
20
+ raw_contents.gsub!('https://inferno.healthit.gov', 'https://inferno-qa.healthit.gov')
17
21
  end
18
22
 
23
+ preset_hash = JSON.parse(raw_contents)
24
+
19
25
  preset_hash.deep_symbolize_keys!
20
26
  preset_hash[:id] ||= SecureRandom.uuid
21
27
  preset = Entities::Preset.new(preset_hash)
@@ -12,6 +12,8 @@ module Inferno
12
12
  end
13
13
 
14
14
  def summarize
15
+ return 'wait' if results.any? { |result| result.result == 'wait' }
16
+
15
17
  return 'pass' if optional_results_passing_criteria_met?
16
18
 
17
19
  prioritized_result_strings.find { |result_string| unique_result_strings.include? result_string }
@@ -25,8 +25,12 @@ module Inferno
25
25
  human_name.split.map(&:capitalize).join(' ')
26
26
  end
27
27
 
28
+ def test_kit_id
29
+ library_name.delete_suffix('_test_kit')
30
+ end
31
+
28
32
  def test_suite_id
29
- "#{library_name}_test_suite"
33
+ test_kit_id
30
34
  end
31
35
  end
32
36
  end
@@ -1,4 +1,4 @@
1
1
  module Inferno
2
2
  # Standard patterns for gem versions: https://guides.rubygems.org/patterns/
3
- VERSION = '0.5.1'.freeze
3
+ VERSION = '0.5.3'.freeze
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inferno_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen MacVicar
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2024-11-26 00:00:00.000000000 Z
13
+ date: 2024-12-17 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -390,6 +390,7 @@ files:
390
390
  - lib/inferno.rb
391
391
  - lib/inferno/apps/cli.rb
392
392
  - lib/inferno/apps/cli/console.rb
393
+ - lib/inferno/apps/cli/evaluate.rb
393
394
  - lib/inferno/apps/cli/execute.rb
394
395
  - lib/inferno/apps/cli/execute/console_outputter.rb
395
396
  - lib/inferno/apps/cli/execute/json_outputter.rb
@@ -432,7 +433,10 @@ files:
432
433
  - lib/inferno/apps/cli/templates/lib/%library_name%/igs/.keep
433
434
  - lib/inferno/apps/cli/templates/lib/%library_name%/igs/README.md
434
435
  - lib/inferno/apps/cli/templates/lib/%library_name%/igs/put_ig_package_dot_tgz_here
436
+ - lib/inferno/apps/cli/templates/lib/%library_name%/metadata.rb.tt
435
437
  - lib/inferno/apps/cli/templates/lib/%library_name%/patient_group.rb.tt
438
+ - lib/inferno/apps/cli/templates/lib/%library_name%/suite.rb.tt
439
+ - lib/inferno/apps/cli/templates/lib/%library_name%/version.rb.tt
436
440
  - lib/inferno/apps/cli/templates/run.sh
437
441
  - lib/inferno/apps/cli/templates/setup.sh
438
442
  - lib/inferno/apps/cli/templates/spec/%library_name%/patient_group_spec.rb.tt
@@ -461,6 +465,7 @@ files:
461
465
  - lib/inferno/apps/web/serializers/hash_value_extractor.rb
462
466
  - lib/inferno/apps/web/serializers/header.rb
463
467
  - lib/inferno/apps/web/serializers/input.rb
468
+ - lib/inferno/apps/web/serializers/markdown_extractor.rb
464
469
  - lib/inferno/apps/web/serializers/message.rb
465
470
  - lib/inferno/apps/web/serializers/preset.rb
466
471
  - lib/inferno/apps/web/serializers/request.rb
@@ -500,6 +505,12 @@ files:
500
505
  - lib/inferno/dsl/configurable.rb
501
506
  - lib/inferno/dsl/fhir_client.rb
502
507
  - lib/inferno/dsl/fhir_client_builder.rb
508
+ - lib/inferno/dsl/fhir_evaluation/config.rb
509
+ - lib/inferno/dsl/fhir_evaluation/dataset_loader.rb
510
+ - lib/inferno/dsl/fhir_evaluation/evaluation_context.rb
511
+ - lib/inferno/dsl/fhir_evaluation/evaluation_result.rb
512
+ - lib/inferno/dsl/fhir_evaluation/evaluator.rb
513
+ - lib/inferno/dsl/fhir_evaluation/rule.rb
503
514
  - lib/inferno/dsl/fhir_resource_validation.rb
504
515
  - lib/inferno/dsl/fhir_validation.rb
505
516
  - lib/inferno/dsl/fhirpath_evaluation.rb
@@ -539,6 +550,7 @@ files:
539
550
  - lib/inferno/exceptions.rb
540
551
  - lib/inferno/ext/fhir_client.rb
541
552
  - lib/inferno/ext/fhir_models.rb
553
+ - lib/inferno/ext/json_parser.rb
542
554
  - lib/inferno/ext/rack.rb
543
555
  - lib/inferno/jobs.rb
544
556
  - lib/inferno/jobs/execute_test_run.rb