inferno_core 0.0.1

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