inferno_core 0.0.5 → 0.0.8.pre2

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/bin/inferno +7 -0
  3. data/lib/inferno/apps/cli/console.rb +12 -0
  4. data/lib/inferno/apps/cli/main.rb +18 -0
  5. data/lib/inferno/apps/cli/migration.rb +14 -0
  6. data/lib/inferno/apps/cli.rb +8 -0
  7. data/lib/inferno/apps/web/controllers/test_runs/create.rb +15 -2
  8. data/lib/inferno/apps/web/index.html.erb +1 -0
  9. data/lib/inferno/apps/web/serializers/hash_value_extractor.rb +11 -0
  10. data/lib/inferno/apps/web/serializers/test.rb +3 -4
  11. data/lib/inferno/apps/web/serializers/test_group.rb +4 -6
  12. data/lib/inferno/config/application.rb +4 -0
  13. data/lib/inferno/config/boot/db.rb +1 -9
  14. data/lib/inferno/config/boot/logging.rb +2 -0
  15. data/lib/inferno/config/boot/suites.rb +4 -6
  16. data/lib/inferno/db/migrations/001_create_initial_structure.rb +1 -1
  17. data/lib/inferno/db/schema.rb +1 -1
  18. data/lib/inferno/dsl/assertions.rb +85 -0
  19. data/lib/inferno/dsl/configurable.rb +126 -0
  20. data/lib/inferno/dsl/fhir_client.rb +22 -16
  21. data/lib/inferno/dsl/fhir_client_builder.rb +3 -3
  22. data/lib/inferno/dsl/fhir_validation.rb +105 -1
  23. data/lib/inferno/dsl/http_client.rb +14 -12
  24. data/lib/inferno/dsl/http_client_builder.rb +3 -3
  25. data/lib/inferno/dsl/request_storage.rb +26 -17
  26. data/lib/inferno/dsl/results.rb +1 -1
  27. data/lib/inferno/dsl/resume_test_route.rb +10 -10
  28. data/lib/inferno/dsl/runnable.rb +104 -38
  29. data/lib/inferno/entities/header.rb +14 -7
  30. data/lib/inferno/entities/message.rb +16 -8
  31. data/lib/inferno/entities/request.rb +34 -21
  32. data/lib/inferno/entities/result.rb +36 -29
  33. data/lib/inferno/entities/session_data.rb +12 -6
  34. data/lib/inferno/entities/test.rb +29 -2
  35. data/lib/inferno/entities/test_group.rb +8 -0
  36. data/lib/inferno/entities/test_run.rb +29 -6
  37. data/lib/inferno/entities/test_session.rb +16 -10
  38. data/lib/inferno/exceptions.rb +12 -0
  39. data/lib/inferno/public/217.bundle.js +1 -1
  40. data/lib/inferno/public/bundle.js +154 -1
  41. data/lib/inferno/public/bundle.js.LICENSE.txt +15 -0
  42. data/lib/inferno/repositories/in_memory_repository.rb +1 -1
  43. data/lib/inferno/repositories/results.rb +1 -1
  44. data/lib/inferno/repositories/test_runs.rb +15 -0
  45. data/lib/inferno/spec_support.rb +1 -1
  46. data/lib/inferno/test_runner.rb +21 -19
  47. data/lib/inferno/utils/markdown_formatter.rb +15 -0
  48. data/lib/inferno/utils/middleware/request_logger.rb +9 -3
  49. data/lib/inferno/utils/migration.rb +17 -0
  50. data/lib/inferno/version.rb +1 -1
  51. data/lib/inferno.rb +0 -4
  52. data/spec/factories/request.rb +14 -7
  53. metadata +46 -10
  54. data/bin/inferno-console +0 -8
@@ -1,38 +1,113 @@
1
1
  module Inferno
2
2
  module DSL
3
+ # This module contains the methods needed to configure a validator to
4
+ # perform validation of FHIR resources. The actual validation is performed
5
+ # by an external FHIR validation service. Tests will typically rely on
6
+ # `assert_valid_resource` for validation rather than directly calling
7
+ # methods on a validator.
8
+ #
9
+ # @example
10
+ #
11
+ # validator do
12
+ # url 'http://example.com/validator'
13
+ # exclude_message { |message| message.type == 'info' }
14
+ # perform_additional_validation do |resource, profile_url|
15
+ # if something_is_wrong
16
+ # { type: 'error', message: 'something is wrong' }
17
+ # else
18
+ # { type: 'info', message: 'everything is ok' }
19
+ # end
20
+ # end
21
+ # end
3
22
  module FHIRValidation
4
23
  def self.included(klass)
5
24
  klass.extend ClassMethods
6
25
  end
7
26
 
27
+ # Perform validation, and add validation messages to the runnable
28
+ #
29
+ # @param resource [FHIR::Model]
30
+ # @param profile_url [String]
31
+ # @param validator [Symbol] the name of the validator to use
32
+ # @return [Boolean] whether the resource is valid
8
33
  def resource_is_valid?(resource: self.resource, profile_url: nil, validator: :default)
9
34
  find_validator(validator).resource_is_valid?(resource, profile_url, self)
10
35
  end
11
36
 
37
+ # Find a particular validator. Looks through a runnable's parents up to
38
+ # the suite to find a validator with a particular name
12
39
  def find_validator(validator_name)
13
40
  self.class.find_validator(validator_name)
14
41
  end
15
42
 
16
43
  class Validator
44
+ # @private
17
45
  def initialize(&block)
18
46
  instance_eval(&block)
19
47
  end
20
48
 
49
+ # @private
21
50
  def default_validator_url
22
51
  ENV.fetch('VALIDATOR_URL')
23
52
  end
24
53
 
54
+ # Set the url of the validator service
55
+ #
56
+ # @param url [String]
25
57
  def url(validator_url = nil)
26
58
  @url = validator_url if validator_url
27
59
 
28
60
  @url
29
61
  end
30
62
 
63
+ # @private
64
+ def additional_validations
65
+ @additional_validations ||= []
66
+ end
67
+
68
+ # Perform validation steps in addition to FHIR validation.
69
+ #
70
+ # @example
71
+ # perform_additional_validation do |resource, profile_url|
72
+ # if something_is_wrong
73
+ # { type: 'error', message: 'something is wrong' }
74
+ # else
75
+ # { type: 'info', message: 'everything is ok' }
76
+ # end
77
+ # end
78
+ # @yieldparam resource [FHIR::Model] the resource being validated
79
+ # @yieldparam profile_url [String] the profile the resource is being
80
+ # validated against
81
+ # @yieldreturn [Array<Hash<Symbol, String>>,Hash<Symbol, String>] The
82
+ # block should return a Hash or an Array of Hashes if any validation
83
+ # messages should be added. The Hash must contain two keys: `:type`
84
+ # and `:message`. `:type` can have a value of `'info'`, `'warning'`,
85
+ # or `'error'`. A type of `'error'` means the resource is invalid.
86
+ # `:message` contains the message string itself.
87
+ def perform_additional_validation(&block)
88
+ additional_validations << block
89
+ end
90
+
91
+ # @private
92
+ def additional_validation_messages(resource, profile_url)
93
+ additional_validations
94
+ .flat_map { |step| step.call(resource, profile_url) }
95
+ .select { |message| message.is_a? Hash }
96
+ end
97
+
98
+ # Filter out unwanted validation messages
99
+ #
100
+ # @example
101
+ # validator do
102
+ # exclude_message { |message| message.type == 'info' }
103
+ # end
104
+ # @yieldparam message [Inferno::Entities::Message]
31
105
  def exclude_message(&block)
32
106
  @exclude_message = block if block_given?
33
107
  @exclude_message
34
108
  end
35
109
 
110
+ # @see Inferno::DSL::FHIRValidation#resource_is_valid?
36
111
  def resource_is_valid?(resource, profile_url, runnable)
37
112
  profile_url ||= FHIR::Definitions.resource_definition(resource.resourceType).url
38
113
 
@@ -40,6 +115,8 @@ module Inferno
40
115
 
41
116
  message_hashes = outcome.issue&.map { |issue| message_hash_from_issue(issue) } || []
42
117
 
118
+ message_hashes.concat(additional_validation_messages(resource, profile_url))
119
+
43
120
  filter_messages(message_hashes)
44
121
 
45
122
  message_hashes
@@ -47,10 +124,12 @@ module Inferno
47
124
  .none? { |message_hash| message_hash[:type] == 'error' }
48
125
  end
49
126
 
127
+ # @private
50
128
  def filter_messages(message_hashes)
51
129
  message_hashes.reject! { |message| exclude_message.call(Entities::Message.new(message)) } if exclude_message
52
130
  end
53
131
 
132
+ # @private
54
133
  def message_hash_from_issue(issue)
55
134
  {
56
135
  type: issue_severity(issue),
@@ -58,6 +137,7 @@ module Inferno
58
137
  }
59
138
  end
60
139
 
140
+ # @private
61
141
  def issue_severity(issue)
62
142
  case issue.severity
63
143
  when 'warning'
@@ -69,6 +149,7 @@ module Inferno
69
149
  end
70
150
  end
71
151
 
152
+ # @private
72
153
  def issue_message(issue)
73
154
  location = if issue.respond_to?(:expression)
74
155
  issue.expression&.join(', ')
@@ -79,6 +160,11 @@ module Inferno
79
160
  "#{location}: #{issue&.details&.text}"
80
161
  end
81
162
 
163
+ # Post a resource to the validation service for validating.
164
+ #
165
+ # @param resource [FHIR::Model]
166
+ # @param profile_url [String]
167
+ # @return [String] the body of the validation response
82
168
  def validate(resource, profile_url)
83
169
  RestClient.post(
84
170
  "#{url}/validate",
@@ -89,15 +175,33 @@ module Inferno
89
175
  end
90
176
 
91
177
  module ClassMethods
92
- # @api private
178
+ # @private
93
179
  def fhir_validators
94
180
  @fhir_validators ||= {}
95
181
  end
96
182
 
183
+ # Define a validator
184
+ # @example
185
+ # validator do
186
+ # url 'http://example.com/validator'
187
+ # exclude_message { |message| message.type == 'info' }
188
+ # perform_additional_validation do |resource, profile_url|
189
+ # if something_is_wrong
190
+ # { type: 'error', message: 'something is wrong' }
191
+ # else
192
+ # { type: 'info', message: 'everything is ok' }
193
+ # end
194
+ # end
195
+ # end
196
+ #
197
+ # @param name [Symbol] the name of the validator, only needed if you are
198
+ # using multiple validators
97
199
  def validator(name = :default, &block)
98
200
  fhir_validators[name] = Inferno::DSL::FHIRValidation::Validator.new(&block)
99
201
  end
100
202
 
203
+ # Find a particular validator. Looks through a runnable's parents up to
204
+ # the suite to find a validator with a particular name
101
205
  def find_validator(validator_name)
102
206
  validator = fhir_validators[validator_name] || parent&.find_validator(validator_name)
103
207
 
@@ -16,7 +16,7 @@ module Inferno
16
16
  # # performs a GET to https://example.com
17
17
  # get
18
18
  # # performs a GET to https://example.com/abc
19
- # get('abc')
19
+ # get('abc')
20
20
  #
21
21
  # request # the most recent request
22
22
  # response # the most recent response
@@ -27,7 +27,7 @@ module Inferno
27
27
  # @see Inferno::FHIRClientBuilder Documentation for the client
28
28
  # configuration DSL
29
29
  module HTTPClient
30
- # @api private
30
+ # @private
31
31
  def self.included(klass)
32
32
  klass.extend ClassMethods
33
33
  klass.include RequestStorage
@@ -47,7 +47,7 @@ module Inferno
47
47
  http_clients[client] = HTTPClientBuilder.new.build(self, definition)
48
48
  end
49
49
 
50
- # @api private
50
+ # @private
51
51
  def http_clients
52
52
  @http_clients ||= {}
53
53
  end
@@ -60,16 +60,17 @@ module Inferno
60
60
  # @param client [Symbol]
61
61
  # @param name [Symbol] Name for this request to allow it to be used by
62
62
  # other tests
63
- # @param _options [Hash] TODO
63
+ # @option options [Hash] Input headers here - headers are optional and
64
+ # must be entered as the last piece of input to this method
64
65
  # @return [Inferno::Entities::Request]
65
- def get(url = '', client: :default, name: nil, **_options)
66
+ def get(url = '', client: :default, name: nil, **options)
66
67
  store_request('outgoing', name) do
67
68
  client = http_client(client)
68
69
 
69
70
  if client
70
- client.get(url)
71
+ client.get(url, nil, options[:headers])
71
72
  elsif url.match?(%r{\Ahttps?://})
72
- Faraday.get(url)
73
+ Faraday.get(url, nil, options[:headers])
73
74
  else
74
75
  raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
75
76
  end
@@ -85,16 +86,17 @@ module Inferno
85
86
  # @param client [Symbol]
86
87
  # @param name [Symbol] Name for this request to allow it to be used by
87
88
  # other tests
88
- # @param _options [Hash] TODO
89
+ # @option options [Hash] Input headers here - headers are optional and
90
+ # must be entered as the last piece of input to this method
89
91
  # @return [Inferno::Entities::Request]
90
- def post(url = '', body: nil, client: :default, name: nil, **_options)
92
+ def post(url = '', body: nil, client: :default, name: nil, **options)
91
93
  store_request('outgoing', name) do
92
94
  client = http_client(client)
93
95
 
94
96
  if client
95
- client.post(url, body)
97
+ client.post(url, body, options[:headers])
96
98
  elsif url.match?(%r{\Ahttps?://})
97
- Faraday.post(url, body)
99
+ Faraday.post(url, body, options[:headers])
98
100
  else
99
101
  raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
100
102
  end
@@ -102,7 +104,7 @@ module Inferno
102
104
  end
103
105
 
104
106
  module ClassMethods
105
- # @api private
107
+ # @private
106
108
  def http_client_definitions
107
109
  @http_client_definitions ||= {}
108
110
  end
@@ -4,7 +4,7 @@ module Inferno
4
4
  class HTTPClientBuilder
5
5
  attr_accessor :runnable
6
6
 
7
- # @api private
7
+ # @private
8
8
  def build(runnable, block)
9
9
  self.runnable = runnable
10
10
  instance_exec(self, &block)
@@ -38,14 +38,14 @@ module Inferno
38
38
  @headers ||= headers
39
39
  end
40
40
 
41
- # @api private
41
+ # @private
42
42
  def method_missing(name, *args, &block)
43
43
  return runnable.call(name, *args, &block) if runnable.respond_to? name
44
44
 
45
45
  super
46
46
  end
47
47
 
48
- # @api private
48
+ # @private
49
49
  def respond_to_missing?(name)
50
50
  runnable.respond_to?(name) || super
51
51
  end
@@ -38,13 +38,14 @@ module Inferno
38
38
 
39
39
  # TODO: do a check in the test runner
40
40
  def named_request(name)
41
- requests.find { |request| request.name == name.to_sym }
41
+ requests.find { |request| request.name == self.class.config.request_name(name.to_sym) }
42
42
  end
43
43
 
44
- # @api private
44
+ # @private
45
45
  def store_request(direction, name = nil, &block)
46
46
  response = block.call
47
47
 
48
+ name = self.class.config.request_name(name)
48
49
  request =
49
50
  if response.is_a? FHIR::ClientReply
50
51
  Entities::Request.from_fhir_client_reply(
@@ -60,52 +61,60 @@ module Inferno
60
61
  request
61
62
  end
62
63
 
63
- # @api private
64
+ # @private
64
65
  def load_named_requests
65
66
  requests_repo = Inferno::Repositories::Requests.new
66
67
  self.class.named_requests_used.map do |request_name|
67
- request = requests_repo.find_named_request(test_session_id, request_name)
68
- raise StandardError, "Unable to find '#{request_name}' request" if request.nil?
68
+ request_alias = self.class.config.request_name(request_name)
69
+ request = requests_repo.find_named_request(test_session_id, request_alias)
70
+ raise StandardError, "Unable to find '#{request_alias}' request" if request.nil?
69
71
 
70
72
  requests << request
71
73
  end
72
74
  end
73
75
 
74
76
  module ClassMethods
75
- # @api private
77
+ # @private
76
78
  def named_requests_made
77
79
  @named_requests_made ||= []
78
80
  end
79
81
 
80
- # @api private
82
+ # @private
81
83
  def named_requests_used
82
84
  @named_requests_used ||= []
83
85
  end
84
86
 
85
87
  # Specify the named requests made by a test
86
88
  #
87
- # @param *names [Symbol] one or more Symbols
88
- def makes_request(*names)
89
- named_requests_made.concat(names)
89
+ # @param identifiers [Symbol] one or more request identifiers
90
+ def makes_request(*identifiers)
91
+ named_requests_made.concat(identifiers).uniq!
92
+ identifiers.each do |identifier|
93
+ config.add_request(identifier)
94
+ end
90
95
  end
91
96
 
92
97
  # Specify the name for a request received by a test
93
98
  #
94
- # @param *names [Symbol] one or more Symbols
95
- def receives_request(name)
96
- @incoming_request_name = name
99
+ # @param identifier [Symbol]
100
+ def receives_request(identifier)
101
+ config.add_request(identifier)
102
+ @incoming_request_name = identifier
97
103
  end
98
104
 
99
- # @api private
105
+ # @private
100
106
  def incoming_request_name
101
107
  @incoming_request_name
102
108
  end
103
109
 
104
110
  # Specify the named requests used by a test
105
111
  #
106
- # @param *names [Symbol] one or more Symbols
107
- def uses_request(*names)
108
- named_requests_used.concat(names)
112
+ # @param identifiers [Symbol] one or more request identifiers
113
+ def uses_request(*identifiers)
114
+ named_requests_used.concat(identifiers).uniq!
115
+ identifiers.each do |identifier|
116
+ config.add_request(identifier)
117
+ end
109
118
  end
110
119
  end
111
120
  end
@@ -94,7 +94,7 @@ module Inferno
94
94
  # and should not be used in real tests.
95
95
  #
96
96
  # @param message [String]
97
- # @api private
97
+ # @private
98
98
  def cancel(message = '')
99
99
  raise Exceptions::CancelException, message
100
100
  end
@@ -4,7 +4,7 @@ module Inferno
4
4
  module DSL
5
5
  # A base class for creating routes to resume test execution upon receiving
6
6
  # an incoming request.
7
- # @api private
7
+ # @private
8
8
  # @see Inferno::DSL::Runnable#resume_test_route
9
9
  class ResumeTestRoute
10
10
  include Hanami::Action
@@ -26,49 +26,49 @@ module Inferno
26
26
  @request ||= Inferno::Entities::Request.from_rack_env(@params.env)
27
27
  end
28
28
 
29
- # @api private
29
+ # @private
30
30
  def test_run
31
31
  @test_run ||=
32
32
  test_runs_repo.find_latest_waiting_by_identifier(test_run_identifier)
33
33
  end
34
34
 
35
- # @api private
35
+ # @private
36
36
  def waiting_result
37
37
  @waiting_result ||= results_repo.find_waiting_result(test_run_id: test_run.id)
38
38
  end
39
39
 
40
- # @api private
40
+ # @private
41
41
  def update_result
42
42
  results_repo.pass_waiting_result(waiting_result.id)
43
43
  end
44
44
 
45
- # @api private
45
+ # @private
46
46
  def persist_request
47
47
  requests_repo.create(
48
48
  request.to_hash.merge(
49
49
  test_session_id: test_run.test_session_id,
50
50
  result_id: waiting_result.id,
51
- name: test.incoming_request_name
51
+ name: test.config.request_name(test.incoming_request_name)
52
52
  )
53
53
  )
54
54
  end
55
55
 
56
- # @api private
56
+ # @private
57
57
  def redirect_route
58
58
  "/test_sessions/#{test_run.test_session_id}##{waiting_group_id}"
59
59
  end
60
60
 
61
- # @api private
61
+ # @private
62
62
  def test
63
63
  @test ||= tests_repo.find(waiting_result.test_id)
64
64
  end
65
65
 
66
- # @api private
66
+ # @private
67
67
  def waiting_group_id
68
68
  test.parent.id
69
69
  end
70
70
 
71
- # @api private
71
+ # @private
72
72
  def call(_params)
73
73
  if test_run.nil?
74
74
  status(500, "Unable to find test run with identifier '#{test_run_identifier}'.")