inferno_core 0.0.1

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 (86) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/bin/inferno-console +8 -0
  4. data/lib/inferno.rb +15 -0
  5. data/lib/inferno/apps/web/application.rb +12 -0
  6. data/lib/inferno/apps/web/controllers/controller.rb +34 -0
  7. data/lib/inferno/apps/web/controllers/requests/show.rb +16 -0
  8. data/lib/inferno/apps/web/controllers/test_runs/create.rb +39 -0
  9. data/lib/inferno/apps/web/controllers/test_runs/results/index.rb +18 -0
  10. data/lib/inferno/apps/web/controllers/test_runs/show.rb +16 -0
  11. data/lib/inferno/apps/web/controllers/test_sessions/create.rb +26 -0
  12. data/lib/inferno/apps/web/controllers/test_sessions/results/index.rb +17 -0
  13. data/lib/inferno/apps/web/controllers/test_sessions/show.rb +16 -0
  14. data/lib/inferno/apps/web/controllers/test_suites/index.rb +13 -0
  15. data/lib/inferno/apps/web/controllers/test_suites/show.rb +16 -0
  16. data/lib/inferno/apps/web/router.rb +27 -0
  17. data/lib/inferno/apps/web/serializers/header.rb +12 -0
  18. data/lib/inferno/apps/web/serializers/input.rb +16 -0
  19. data/lib/inferno/apps/web/serializers/message.rb +10 -0
  20. data/lib/inferno/apps/web/serializers/request.rb +26 -0
  21. data/lib/inferno/apps/web/serializers/result.rb +21 -0
  22. data/lib/inferno/apps/web/serializers/serializer.rb +16 -0
  23. data/lib/inferno/apps/web/serializers/test.rb +17 -0
  24. data/lib/inferno/apps/web/serializers/test_group.rb +23 -0
  25. data/lib/inferno/apps/web/serializers/test_run.rb +19 -0
  26. data/lib/inferno/apps/web/serializers/test_session.rb +17 -0
  27. data/lib/inferno/apps/web/serializers/test_suite.rb +18 -0
  28. data/lib/inferno/config/application.rb +22 -0
  29. data/lib/inferno/config/boot.rb +5 -0
  30. data/lib/inferno/config/boot/db.rb +23 -0
  31. data/lib/inferno/config/boot/logging.rb +16 -0
  32. data/lib/inferno/config/boot/suites.rb +27 -0
  33. data/lib/inferno/config/boot/web.rb +17 -0
  34. data/lib/inferno/db/migrations/001_create_initial_structure.rb +165 -0
  35. data/lib/inferno/dsl.rb +35 -0
  36. data/lib/inferno/dsl/assertions.rb +93 -0
  37. data/lib/inferno/dsl/fhir_client.rb +152 -0
  38. data/lib/inferno/dsl/fhir_client_builder.rb +56 -0
  39. data/lib/inferno/dsl/fhir_manipulation.rb +25 -0
  40. data/lib/inferno/dsl/fhir_validation.rb +111 -0
  41. data/lib/inferno/dsl/http_client.rb +122 -0
  42. data/lib/inferno/dsl/http_client_builder.rb +54 -0
  43. data/lib/inferno/dsl/request_storage.rb +101 -0
  44. data/lib/inferno/dsl/results.rb +54 -0
  45. data/lib/inferno/dsl/runnable.rb +250 -0
  46. data/lib/inferno/entities.rb +19 -0
  47. data/lib/inferno/entities/attributes.rb +9 -0
  48. data/lib/inferno/entities/entity.rb +15 -0
  49. data/lib/inferno/entities/header.rb +42 -0
  50. data/lib/inferno/entities/message.rb +22 -0
  51. data/lib/inferno/entities/request.rb +166 -0
  52. data/lib/inferno/entities/result.rb +49 -0
  53. data/lib/inferno/entities/test.rb +173 -0
  54. data/lib/inferno/entities/test_group.rb +87 -0
  55. data/lib/inferno/entities/test_input.rb +20 -0
  56. data/lib/inferno/entities/test_run.rb +63 -0
  57. data/lib/inferno/entities/test_session.rb +26 -0
  58. data/lib/inferno/entities/test_suite.rb +73 -0
  59. data/lib/inferno/exceptions.rb +42 -0
  60. data/lib/inferno/public/217.bundle.js +1 -0
  61. data/lib/inferno/public/assets.json +6 -0
  62. data/lib/inferno/public/bundle.js +2 -0
  63. data/lib/inferno/public/bundle.js.LICENSE.txt +65 -0
  64. data/lib/inferno/public/e09b16f5cb645eb05f90c8f38f3409fb.png +0 -0
  65. data/lib/inferno/public/favicon.ico +0 -0
  66. data/lib/inferno/public/logo192.png +0 -0
  67. data/lib/inferno/public/logo512.png +0 -0
  68. data/lib/inferno/repositories.rb +27 -0
  69. data/lib/inferno/repositories/headers.rb +22 -0
  70. data/lib/inferno/repositories/in_memory_repository.rb +41 -0
  71. data/lib/inferno/repositories/messages.rb +35 -0
  72. data/lib/inferno/repositories/repository.rb +106 -0
  73. data/lib/inferno/repositories/requests.rb +89 -0
  74. data/lib/inferno/repositories/results.rb +72 -0
  75. data/lib/inferno/repositories/test_groups.rb +9 -0
  76. data/lib/inferno/repositories/test_runs.rb +46 -0
  77. data/lib/inferno/repositories/test_sessions.rb +56 -0
  78. data/lib/inferno/repositories/test_suites.rb +9 -0
  79. data/lib/inferno/repositories/tests.rb +9 -0
  80. data/lib/inferno/repositories/validate_runnable_reference.rb +42 -0
  81. data/lib/inferno/spec_support.rb +9 -0
  82. data/lib/inferno/test_runner.rb +81 -0
  83. data/lib/inferno/utils/middleware/request_logger.rb +55 -0
  84. data/lib/inferno/version.rb +3 -0
  85. data/spec/support/factory_bot.rb +21 -0
  86. metadata +514 -0
@@ -0,0 +1,35 @@
1
+ require_relative 'dsl/assertions'
2
+ require_relative 'dsl/fhir_client'
3
+ require_relative 'dsl/fhir_manipulation'
4
+ require_relative 'dsl/fhir_validation'
5
+ require_relative 'dsl/http_client'
6
+ require_relative 'dsl/results'
7
+ require_relative 'dsl/runnable'
8
+
9
+ module Inferno
10
+ # The DSL for writing tests.
11
+ module DSL
12
+ INCLUDABLE_DSL_MODULES = [
13
+ Assertions,
14
+ FHIRClient,
15
+ HTTPClient,
16
+ Results,
17
+ FHIRValidation,
18
+ FHIRManipulation
19
+ ].freeze
20
+
21
+ EXTENDABLE_DSL_MODULES = [
22
+ Runnable
23
+ ].freeze
24
+
25
+ def self.included(klass)
26
+ INCLUDABLE_DSL_MODULES.each do |dsl_module|
27
+ klass.include dsl_module
28
+ end
29
+
30
+ EXTENDABLE_DSL_MODULES.each do |dsl_module|
31
+ klass.extend dsl_module
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,93 @@
1
+ require_relative '../exceptions'
2
+
3
+ module Inferno
4
+ module DSL
5
+ module Assertions
6
+ def assert(test, message = '')
7
+ raise Exceptions::AssertionException, message unless test
8
+ end
9
+
10
+ def bad_response_status_message(expected, received)
11
+ "Bad response status: expected #{Array.wrap(expected).join(', ')}, but received #{received}"
12
+ end
13
+
14
+ def assert_response_status(status, response: self.response)
15
+ assert Array.wrap(status).include?(response[:status]), bad_response_status_message(status, response[:status])
16
+ end
17
+
18
+ def bad_resource_type_message(expected, received)
19
+ "Bad resource type received: expected #{expected}, but received #{received}"
20
+ end
21
+
22
+ def assert_resource_type(resource_type, resource: self.resource)
23
+ resource_type_name = normalize_resource_type(resource_type)
24
+
25
+ assert resource&.resourceType == resource_type_name,
26
+ bad_resource_type_message(resource_type_name, resource&.resourceType)
27
+ end
28
+
29
+ def invalid_resource_message(profile_url)
30
+ "Resource does not conform to the profile: #{profile_url}"
31
+ end
32
+
33
+ def assert_valid_resource(resource: self.resource, profile_url: nil)
34
+ assert resource_is_valid?(resource: resource, profile_url: profile_url),
35
+ invalid_resource_message(profile_url)
36
+ end
37
+
38
+ def assert_valid_bundle_entries(bundle: resource, resource_types: {})
39
+ assert_resource_type('Bundle', resource: bundle)
40
+
41
+ types_to_check = normalize_types_to_check(resource_types)
42
+
43
+ invalid_resources =
44
+ bundle
45
+ .entry
46
+ .map(&:resource)
47
+ .select { |resource| types_to_check.empty? || types_to_check.include?(resource.resourceType) }
48
+ .reject do |resource|
49
+ validation_params = { resource: resource }
50
+ profile = types_to_check[resource.resourceType]
51
+ validation_params[:profile_url] = profile if profile
52
+
53
+ resource_is_valid?(**validation_params)
54
+ end
55
+
56
+ assert invalid_resources.empty?, invalid_bundle_entries_message(invalid_resources)
57
+ end
58
+
59
+ def invalid_bundle_entries_message(invalid_resources)
60
+ identifier_strings =
61
+ invalid_resources
62
+ .map { |resource| "#{resource.resourceType}##{resource.id}" }
63
+ .join(', ')
64
+ "The following bundle entries are invalid: #{identifier_strings}"
65
+ end
66
+
67
+ def normalize_resource_type(resource_type)
68
+ if resource_type.is_a? Class
69
+ resource_type.name.demodulize
70
+ else
71
+ resource_type.to_s.camelize
72
+ end
73
+ end
74
+
75
+ def normalize_types_to_check(resource_types)
76
+ case resource_types
77
+ when Hash
78
+ resource_types.transform_keys { |type| normalize_resource_type(type) }
79
+ when String
80
+ { normalize_resource_type(resource_types) => nil }
81
+ when Array
82
+ resource_types.each_with_object({}) { |type, types| types[normalize_resource_type(type)] = nil }
83
+ end
84
+ end
85
+
86
+ def assert_valid_json(maybe_json_string, message = '')
87
+ assert JSON.parse(maybe_json_string)
88
+ rescue JSON::ParserError
89
+ assert false, "Invalid JSON. #{message}"
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,152 @@
1
+ require_relative 'request_storage'
2
+
3
+ module Inferno
4
+ module DSL
5
+ # This module contains the FHIR DSL available to test writers.
6
+ #
7
+ # @example
8
+ # class MyTestGroup < Inferno::TestGroup
9
+ # # create a "default" client for a group
10
+ # fhir_client do
11
+ # url 'https://example.com/fhir'
12
+ # end
13
+ #
14
+ # # create a named client for a group
15
+ # fhir_client :with_custom_header do
16
+ # url 'https://example.com/fhir'
17
+ # headers { 'X-my-custom-header': 'ABC123' }
18
+ # end
19
+ #
20
+ # test :some_test do
21
+ # run do
22
+ # # uses the default client
23
+ # fhir_read('Patient', 5)
24
+ #
25
+ # # uses a named client
26
+ # fhir_read('Patient', 5, client: :with_custom_header)
27
+ #
28
+ # request # the most recent request
29
+ # response # the most recent response
30
+ # resource # the resource from the most recent response
31
+ # requests # all of the requests which have been made in this test
32
+ # end
33
+ # end
34
+ # end
35
+ # @see Inferno::FHIRClientBuilder Documentation for the client
36
+ # configuration DSL
37
+ module FHIRClient
38
+ # @api private
39
+ def self.included(klass)
40
+ klass.extend ClassMethods
41
+ klass.extend Forwardable
42
+ klass.include RequestStorage
43
+
44
+ klass.def_delegators 'self.class', :profile_url, :validator_url
45
+ end
46
+
47
+ # Return a previously defined FHIR client
48
+ #
49
+ # @param client [Symbol] the name of the client
50
+ # @return [FHIR::Client]
51
+ # @see Inferno::FHIRClientBuilder
52
+ def fhir_client(client = :default)
53
+ fhir_clients[client] ||=
54
+ FHIRClientBuilder.new.build(self, self.class.fhir_client_definitions[client])
55
+ end
56
+
57
+ # @api private
58
+ def fhir_clients
59
+ @fhir_clients ||= {}
60
+ end
61
+
62
+ # Perform a FHIR operation
63
+ #
64
+ # @note This is a placeholder method until the FHIR::Client supports
65
+ # general operations
66
+ #
67
+ # @param path [String]
68
+ # @param body [FHIR::Parameters]
69
+ # @param client [Symbol]
70
+ # @param name [Symbol] Name for this request to allow it to be used by
71
+ # other tests
72
+ # @param _options [Hash] TODO
73
+ # @return [Inferno::Entities::Request]
74
+ def fhir_operation(path, body: nil, client: :default, name: nil, **_options)
75
+ store_request('outgoing', name) do
76
+ headers = fhir_client(client).fhir_headers
77
+ headers.merge!('Content-Type' => 'application/fhir+json') if body.present?
78
+ fhir_client(client).send(:post, path, body, headers)
79
+ end
80
+ end
81
+
82
+ # Fetch the capability statement.
83
+ #
84
+ # @param client [Symbol]
85
+ # @param name [Symbol] Name for this request to allow it to be used by
86
+ # other tests
87
+ # @param _options [Hash] TODO
88
+ # @return [Inferno::Entities::Request]
89
+ def fhir_get_capability_statement(client: :default, name: nil, **_options)
90
+ store_request('outgoing', name) do
91
+ fhir_client(client).conformance_statement
92
+ fhir_client(client).reply
93
+ end
94
+ end
95
+
96
+ # Perform a FHIR read interaction.
97
+ #
98
+ # @param resource_type [String, Symbol, Class]
99
+ # @param id [String]
100
+ # @param client [Symbol]
101
+ # @param name [Symbol] Name for this request to allow it to be used by
102
+ # other tests
103
+ # @param _options [Hash] TODO
104
+ # @return [Inferno::Entities::Request]
105
+ def fhir_read(resource_type, id, client: :default, name: nil, **_options)
106
+ store_request('outgoing', name) do
107
+ fhir_client(client).read(fhir_class_from_resource_type(resource_type), id)
108
+ end
109
+ end
110
+
111
+ # Perform a FHIR search interaction.
112
+ #
113
+ # @param resource_type [String, Symbol, Class]
114
+ # @param client [Symbol]
115
+ # @param params [Hash] the search params
116
+ # @param name [Symbol] Name for this request to allow it to be used by
117
+ # other tests
118
+ # @param _options [Hash] TODO
119
+ # @return [Inferno::Entities::Request]
120
+ def fhir_search(resource_type, client: :default, params: {}, name: nil, **_options)
121
+ store_request('outgoing', name) do
122
+ fhir_client(client)
123
+ .search(fhir_class_from_resource_type(resource_type), search: { parameters: params })
124
+ end
125
+ end
126
+
127
+ # @todo Make this a FHIR class method? Something like
128
+ # FHIR.class_for(resource_type)
129
+ # @api private
130
+ def fhir_class_from_resource_type(resource_type)
131
+ FHIR.const_get(resource_type.to_s.camelize)
132
+ end
133
+
134
+ module ClassMethods
135
+ # @api private
136
+ def fhir_client_definitions
137
+ @fhir_client_definitions ||= {}
138
+ end
139
+
140
+ # Define a FHIR client to be used by a Runnable.
141
+ #
142
+ # @param name [Symbol] a name used to reference this particular client
143
+ # @param block a block to configure the client
144
+ # @see Inferno::FHIRClientBuilder Documentation for the client
145
+ # configuration DSL
146
+ def fhir_client(name = :default, &block)
147
+ fhir_client_definitions[name] = block
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,56 @@
1
+ require 'fhir_client'
2
+
3
+ module Inferno
4
+ module DSL
5
+ # DSL for configuring FHIR clients
6
+ class FHIRClientBuilder
7
+ attr_accessor :runnable
8
+
9
+ # @api private
10
+ def build(runnable, block)
11
+ self.runnable = runnable
12
+ instance_exec(self, &block)
13
+
14
+ FHIR::Client.new(url).tap do |client|
15
+ client.additional_headers = headers if headers
16
+ client.default_json
17
+ end
18
+ end
19
+
20
+ # Define the base FHIR url for a client. A string or symbol can be provided.
21
+ # A string is interpreted as a url. A symbol is interpreted as the name of
22
+ # an input to the Runnable.
23
+ #
24
+ # @param url [String, Symbol]
25
+ # @return [void]
26
+ def url(url = nil)
27
+ @url ||=
28
+ if url.is_a? Symbol
29
+ runnable.send(url)
30
+ else
31
+ url
32
+ end
33
+ end
34
+
35
+ # Define custom headers for a client
36
+ #
37
+ # @param headers [Hash]
38
+ # @return [void]
39
+ def headers(headers = nil)
40
+ @headers ||= headers
41
+ end
42
+
43
+ # @api private
44
+ def method_missing(name, *args, &block)
45
+ return runnable.call(name, *args, &block) if runnable.respond_to? name
46
+
47
+ super
48
+ end
49
+
50
+ # @api private
51
+ def respond_to_missing?(name)
52
+ runnable.respond_to?(name) || super
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,25 @@
1
+ module Inferno
2
+ module DSL
3
+ module FHIRManipulation
4
+ def walk_resource(resource, path = nil, &block)
5
+ resource.class::METADATA.each do |field_name, meta|
6
+ local_name = meta.fetch :local_name, field_name
7
+ values = [resource.instance_variable_get("@#{local_name}")].flatten.compact
8
+ next if values.empty?
9
+
10
+ values.each_with_index do |value, i|
11
+ child_path = if path.nil?
12
+ field_name
13
+ elsif meta['max'] > 1
14
+ "#{path}.#{field_name}[#{i}]"
15
+ else
16
+ "#{path}.#{field_name}"
17
+ end
18
+ yield value, meta, child_path
19
+ walk_resource value, child_path, &block unless FHIR::PRIMITIVES.include? meta['type']
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,111 @@
1
+ module Inferno
2
+ module DSL
3
+ module FHIRValidation
4
+ def self.included(klass)
5
+ klass.extend ClassMethods
6
+ end
7
+
8
+ def resource_is_valid?(resource: self.resource, profile_url: nil, validator: :default)
9
+ find_validator(validator).resource_is_valid?(resource, profile_url, self)
10
+ end
11
+
12
+ def find_validator(validator_name)
13
+ self.class.find_validator(validator_name)
14
+ end
15
+
16
+ class Validator
17
+ def initialize(&block)
18
+ instance_eval(&block)
19
+ end
20
+
21
+ def default_validator_url
22
+ ENV.fetch('VALIDATOR_URL')
23
+ end
24
+
25
+ def url(validator_url = nil)
26
+ @url = validator_url if validator_url
27
+
28
+ @url
29
+ end
30
+
31
+ def exclude_message(&block)
32
+ @exclude_message = block if block_given?
33
+ @exclude_message
34
+ end
35
+
36
+ def resource_is_valid?(resource, profile_url, runnable)
37
+ profile_url ||= FHIR::Definitions.resource_definition(resource.resourceType).url
38
+
39
+ outcome = FHIR::OperationOutcome.new(JSON.parse(validate(resource, profile_url)))
40
+
41
+ message_hashes = outcome.issue&.map { |issue| message_hash_from_issue(issue) } || []
42
+
43
+ filter_messages(message_hashes)
44
+
45
+ message_hashes
46
+ .each { |message_hash| runnable.add_message(message_hash[:type], message_hash[:message]) }
47
+ .none? { |message_hash| message_hash[:type] == 'error' }
48
+ end
49
+
50
+ def filter_messages(message_hashes)
51
+ message_hashes.reject! { |message| exclude_message.call(Entities::Message.new(message)) } if exclude_message
52
+ end
53
+
54
+ def message_hash_from_issue(issue)
55
+ {
56
+ type: issue_severity(issue),
57
+ message: issue_message(issue)
58
+ }
59
+ end
60
+
61
+ def issue_severity(issue)
62
+ case issue.severity
63
+ when 'warning'
64
+ 'warning'
65
+ when 'information'
66
+ 'info'
67
+ else
68
+ 'error'
69
+ end
70
+ end
71
+
72
+ def issue_message(issue)
73
+ location = if issue.respond_to?(:expression)
74
+ issue.expression&.join(', ')
75
+ else
76
+ issue.location&.join(', ')
77
+ end
78
+
79
+ "#{location}: #{issue&.details&.text}"
80
+ end
81
+
82
+ def validate(resource, profile_url)
83
+ RestClient.post(
84
+ "#{url}/validate",
85
+ resource.to_json,
86
+ params: { profile: profile_url }
87
+ ).body
88
+ end
89
+ end
90
+
91
+ module ClassMethods
92
+ # @api private
93
+ def fhir_validators
94
+ @fhir_validators ||= {}
95
+ end
96
+
97
+ def validator(name = :default, &block)
98
+ fhir_validators[name] = Inferno::DSL::FHIRValidation::Validator.new(&block)
99
+ end
100
+
101
+ def find_validator(validator_name)
102
+ validator = fhir_validators[validator_name] || parent&.find_validator(validator_name)
103
+
104
+ raise Exceptions::ValidatorNotFoundException, validator_name if validator.nil?
105
+
106
+ validator
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end