inferno_core 0.5.1 → 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 (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