inferno_core 0.2.0 → 0.3.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b081a1dec3c7c5c80fc8d2423bb1f3b3799be30f8173c73b1dce28f7095a419
4
- data.tar.gz: d5c378788719ee610edfced2133abb8ff5a20637d28c1405f3ff19fa0219fe1a
3
+ metadata.gz: 2d689ccce031c3a119965c25c083606b6f5a789c61b29dca2b50971872205d47
4
+ data.tar.gz: 1be92b3799e22400b7d37ac1780de257c3104d23e809b712ee543b5437af8ecd
5
5
  SHA512:
6
- metadata.gz: 2cbdecf407e35cd80d5601173ef3b0e464c961e7f919fdec5e1b5b05449b67229e2fa34301bdccb53fc324768c5aab1cbb60e8cbe3bc635989d6cec27ef9708c
7
- data.tar.gz: 977fe89c0e3968ce13d80d771a45c936c66e427b0b39c4cc8af14f0992775590d37885bf135db58b91419fab1e371a0d3b9c86aaf2e96ce6d35dbfdbe4e90dcd
6
+ metadata.gz: 563d0d578f6cc039ef95e3e5cc81b2285da6a04779e6705462111a626e826cd47cb2c82dd26dbd00f048d6e4f7277761151351da27e30a0ba3cbb64f6086440e
7
+ data.tar.gz: cda50085e8e67e5737a06b3440b2533804251d9eea5aff2235e28c9039387ca352aaa72b45f46e00e859adb8d8bfc5d83f63a0851c58c2d71dff5844ef35d5a7
@@ -11,6 +11,36 @@ module Inferno
11
11
 
12
12
  PARAMS = [:test_session_id, :test_suite_id, :test_group_id, :test_id].freeze
13
13
 
14
+ def verify_runnable(runnable, inputs)
15
+ missing_inputs = runnable&.missing_inputs(inputs)
16
+ user_runnable = runnable&.user_runnable?
17
+ raise Inferno::Exceptions::RequiredInputsNotFound, missing_inputs if missing_inputs&.any?
18
+ raise Inferno::Exceptions::NotUserRunnableException unless user_runnable
19
+ end
20
+
21
+ def persist_inputs(params, test_run)
22
+ params[:inputs]&.each do |input_params|
23
+ input =
24
+ test_run.runnable.available_inputs
25
+ .find { |_, runnable_input| runnable_input.name == input_params[:name] }
26
+ &.last
27
+
28
+ if input.nil?
29
+ Inferno::Application['logger'].warning(
30
+ "Unknown input `#{input_params[:name]}` for #{test_run.runnable.id}: #{test_run.runnable.title}"
31
+ )
32
+ next
33
+ end
34
+
35
+ session_data_repo.save(
36
+ test_session_id: test_run.test_session_id,
37
+ name: input.name,
38
+ value: input_params[:value],
39
+ type: input.type
40
+ )
41
+ end
42
+ end
43
+
14
44
  def call(params)
15
45
  test_session = test_sessions_repo.find(params[:test_session_id])
16
46
 
@@ -21,24 +51,13 @@ module Inferno
21
51
  return
22
52
  end
23
53
 
24
- # TODO: This test run shouldn't be created until after the inputs
25
- # and runnable are validated
26
- test_run = repo.create(create_params(params).merge(status: 'queued'))
27
- missing_inputs = test_run.runnable.missing_inputs(params[:inputs])
54
+ verify_runnable(repo.build_entity(create_params(params)).runnable, params[:inputs])
28
55
 
29
- raise Inferno::Exceptions::RequiredInputsNotFound, missing_inputs if missing_inputs.any?
30
- raise Inferno::Exceptions::NotUserRunnableException unless test_run.runnable.user_runnable?
56
+ test_run = repo.create(create_params(params).merge(status: 'queued'))
31
57
 
32
58
  self.body = serialize(test_run)
33
59
 
34
- params[:inputs]&.each do |input|
35
- session_data_repo.save(
36
- test_session_id: test_session.id,
37
- name: input[:name],
38
- value: input[:value],
39
- type: input[:type]
40
- )
41
- end
60
+ persist_inputs(params, test_run)
42
61
 
43
62
  Jobs.perform(Jobs::ExecuteTestRun, test_run.id)
44
63
  rescue Sequel::ValidationFailed, Sequel::ForeignKeyConstraintViolation,
@@ -25,11 +25,26 @@
25
25
  work correctly both with client-side routing and a non-root public URL.
26
26
  Learn how to configure a non-root public URL by running `npm run build`.
27
27
  -->
28
+ <style>
29
+ .wrapper {
30
+ height: 100%;
31
+ display: flex;
32
+ flex-direction: column;
33
+ }
34
+ .app {
35
+ height: 100%;
36
+ }
37
+ </style>
28
38
  <title>Inferno</title>
29
39
  </head>
30
40
  <body>
31
41
  <noscript>You need to enable JavaScript to run this app.</noscript>
32
- <div id="root"></div>
42
+ <div class='wrapper'>
43
+ <% if File.exist? (File.join(Dir.pwd, 'config', 'banner.html.erb')) %>
44
+ <div class='banner'><%= ERB.new(File.read(File.join(Dir.pwd, 'config', 'banner.html.erb'))).result %></div>
45
+ <% end %>
46
+ <div class='app' id="root"></div>
47
+ </div>
33
48
  <!--
34
49
  This HTML file is a template.
35
50
  If you open it directly in the browser, you will see an empty page.
@@ -18,6 +18,10 @@ module Inferno
18
18
  field :updated_at
19
19
  field :optional?, name: :optional
20
20
 
21
+ field :inputs do |result, _options|
22
+ result.input_json.present? ? JSON.parse(result.input_json) : []
23
+ end
24
+
21
25
  field :outputs do |result, _options|
22
26
  result.output_json.present? ? JSON.parse(result.output_json) : []
23
27
  end
@@ -7,7 +7,7 @@ module Inferno
7
7
  field :short_id
8
8
  field :title
9
9
  field :short_title
10
- field :input_definitions, name: :inputs, extractor: HashValueExtractor, blueprint: Input
10
+ field :available_inputs, name: :inputs, extractor: HashValueExtractor, blueprint: Input
11
11
  field :output_definitions, name: :outputs, extractor: HashValueExtractor
12
12
  field :description
13
13
  field :short_description
@@ -17,7 +17,7 @@ module Inferno
17
17
 
18
18
  association :groups, name: :test_groups, blueprint: TestGroup
19
19
  association :tests, blueprint: Test
20
- field :input_definitions, name: :inputs, extractor: HashValueExtractor, blueprint: Input
20
+ field :available_inputs, name: :inputs, extractor: HashValueExtractor, blueprint: Input
21
21
  field :output_definitions, name: :outputs, extractor: HashValueExtractor
22
22
  end
23
23
  end
@@ -18,6 +18,7 @@ module Inferno
18
18
  include_view :summary
19
19
  association :groups, name: :test_groups, blueprint: TestGroup
20
20
  field :configuration_messages
21
+ field :available_inputs, name: :inputs, extractor: HashValueExtractor, blueprint: Input
21
22
  end
22
23
  end
23
24
  end
@@ -1,3 +1,5 @@
1
+ require_relative '../entities/input'
2
+
1
3
  module Inferno
2
4
  module DSL
3
5
  # This module contains the DSL for managing runnable configuration.
@@ -35,7 +37,11 @@ module Inferno
35
37
  new_configuration
36
38
  end
37
39
 
38
- self.configuration = configuration.deep_merge(config_to_apply)
40
+ self.configuration = configuration.deep_merge(config_to_apply.reject { |key, _| key == :inputs })
41
+
42
+ config_to_apply[:inputs]&.each do |identifier, new_input|
43
+ add_input(identifier, new_input.to_hash)
44
+ end
39
45
  end
40
46
 
41
47
  def options
@@ -49,28 +55,36 @@ module Inferno
49
55
  end
50
56
 
51
57
  def add_input(identifier, new_config = {})
52
- existing_config = input_config(identifier) || {}
53
- inputs[identifier] = default_input_config(identifier).merge(existing_config, new_config)
58
+ existing_config = input(identifier)
59
+
60
+ if existing_config.nil?
61
+ return inputs[identifier] = Entities::Input.new(default_input_params(identifier).merge(new_config))
62
+ end
63
+
64
+ inputs[identifier] =
65
+ Entities::Input
66
+ .new(existing_config.to_hash)
67
+ .merge(Entities::Input.new(new_config))
54
68
  end
55
69
 
56
- def default_input_config(identifier)
70
+ def default_input_params(identifier)
57
71
  { name: identifier, type: 'text' }
58
72
  end
59
73
 
60
- def input_config_exists?(identifier)
74
+ def input_exists?(identifier)
61
75
  inputs.key? identifier
62
76
  end
63
77
 
64
- def input_config(identifier)
78
+ def input(identifier)
65
79
  inputs[identifier]
66
80
  end
67
81
 
68
82
  def input_name(identifier)
69
- inputs.dig(identifier, :name) || identifier
83
+ inputs[identifier]&.name
70
84
  end
71
85
 
72
86
  def input_type(identifier)
73
- inputs.dig(identifier, :type)
87
+ inputs[identifier]&.type
74
88
  end
75
89
 
76
90
  ### Output Configuration ###
@@ -1,3 +1,5 @@
1
+ require_relative '../ext/fhir_models'
2
+
1
3
  module Inferno
2
4
  module DSL
3
5
  # This module contains the methods needed to configure a validator to
@@ -168,7 +170,7 @@ module Inferno
168
170
  def validate(resource, profile_url)
169
171
  RestClient.post(
170
172
  "#{url}/validate",
171
- resource.to_json,
173
+ resource.source_contents,
172
174
  params: { profile: profile_url }
173
175
  ).body
174
176
  end
@@ -0,0 +1,185 @@
1
+ module Inferno
2
+ module DSL
3
+ module InputOutputHandling
4
+ # Define inputs
5
+ #
6
+ # @param identifier [Symbol] identifier for the input
7
+ # @param other_identifiers [Symbol] array of symbols if specifying multiple inputs
8
+ # @param input_params [Hash] options for input such as type, description, or title
9
+ # @option input_params [String] :title Human readable title for input
10
+ # @option input_params [String] :description Description for the input
11
+ # @option input_params [String] :type text | textarea | radio
12
+ # @option input_params [String] :default The default value for the input
13
+ # @option input_params [Boolean] :optional Set to true to not require input for test execution
14
+ # @option input_params [Hash] :options Possible input option formats based on input type
15
+ # @option options [Array] :list_options Array of options for input formats that require a list of possible values
16
+ # @return [void]
17
+ # @example
18
+ # input :patient_id, title: 'Patient ID', description: 'The ID of the patient being searched for',
19
+ # default: 'default_patient_id'
20
+ # @example
21
+ # input :textarea, title: 'Textarea Input Example', type: 'textarea', optional: true
22
+ def input(identifier, *other_identifiers, **input_params)
23
+ if other_identifiers.present?
24
+ [identifier, *other_identifiers].compact.each do |input_identifier|
25
+ inputs << input_identifier
26
+ config.add_input(input_identifier)
27
+ end
28
+ else
29
+ inputs << identifier
30
+ config.add_input(identifier, input_params)
31
+ end
32
+ end
33
+
34
+ # Define outputs
35
+ #
36
+ # @param identifier [Symbol] identifier for the output
37
+ # @param other_identifiers [Symbol] array of symbols if specifying multiple outputs
38
+ # @param output_definition [Hash] options for output
39
+ # @option output_definition [String] :type text | textarea | oauth_credentials
40
+ # @return [void]
41
+ # @example
42
+ # output :patient_id, :condition_id, :observation_id
43
+ # @example
44
+ # output :oauth_credentials, type: 'oauth_credentials'
45
+ def output(identifier, *other_identifiers, **output_definition)
46
+ if other_identifiers.present?
47
+ [identifier, *other_identifiers].compact.each do |output_identifier|
48
+ outputs << output_identifier
49
+ config.add_output(output_identifier)
50
+ end
51
+ else
52
+ outputs << identifier
53
+ config.add_output(identifier, output_definition)
54
+ end
55
+ end
56
+
57
+ # @private
58
+ def inputs
59
+ @inputs ||= []
60
+ end
61
+
62
+ # @private
63
+ def outputs
64
+ @outputs ||= []
65
+ end
66
+
67
+ # @private
68
+ def output_definitions
69
+ config.outputs.slice(*outputs)
70
+ end
71
+
72
+ # @private
73
+ def required_inputs
74
+ available_inputs
75
+ .reject { |_, input| input.optional }
76
+ .map { |_, input| input.name }
77
+ end
78
+
79
+ # @private
80
+ def missing_inputs(submitted_inputs)
81
+ submitted_inputs = [] if submitted_inputs.nil?
82
+
83
+ required_inputs.map(&:to_s) - submitted_inputs.map { |input| input[:name] }
84
+ end
85
+
86
+ # Define a particular order for inputs to be presented in the API/UI
87
+ # @example
88
+ # group do
89
+ # input :input1, :input2, :input3
90
+ # input_order :input3, :input2, :input1
91
+ # end
92
+ # @param new_input_order [Array<String,Symbol>]
93
+ # @return [Array<String, Symbol>]
94
+ def input_order(*new_input_order)
95
+ return @input_order = new_input_order if new_input_order.present?
96
+
97
+ @input_order ||= []
98
+ end
99
+
100
+ # @private
101
+ def order_available_inputs(original_inputs)
102
+ input_names = original_inputs.map { |_, input| input.name }.join(', ')
103
+
104
+ ordered_inputs =
105
+ input_order.each_with_object({}) do |input_name, inputs|
106
+ key, input = original_inputs.find { |_, input| input.name == input_name.to_s }
107
+ if input.nil?
108
+ Inferno::Application[:logger].error <<~ERROR
109
+ Error trying to order inputs in #{id}: #{title}:
110
+ - Unable to find input #{input_name} in available inputs: #{input_names}
111
+ ERROR
112
+ next
113
+ end
114
+ inputs[key] = original_inputs.delete(key)
115
+ end
116
+
117
+ original_inputs.each do |key, input|
118
+ ordered_inputs[key] = input
119
+ end
120
+
121
+ ordered_inputs
122
+ end
123
+
124
+ # @private
125
+ def all_outputs
126
+ outputs
127
+ .map { |output_identifier| config.output_name(output_identifier) }
128
+ .concat(children.flat_map(&:all_outputs))
129
+ .uniq
130
+ end
131
+
132
+ # @private
133
+ # Inputs available for this runnable's children. A running list of outputs
134
+ # created by the children is used to exclude any inputs which are provided
135
+ # by an earlier child's output.
136
+ def children_available_inputs
137
+ @children_available_inputs ||=
138
+ begin
139
+ child_outputs = []
140
+ children.each_with_object({}) do |child, definitions|
141
+ new_definitions = child.available_inputs.map(&:dup)
142
+ new_definitions.each do |input, new_definition|
143
+ existing_definition = definitions[input]
144
+
145
+ updated_definition =
146
+ if existing_definition.present?
147
+ existing_definition.merge_with_child(new_definition)
148
+ else
149
+ new_definition
150
+ end
151
+
152
+ next if child_outputs.include?(updated_definition.name.to_sym)
153
+
154
+ definitions[updated_definition.name.to_sym] = updated_definition
155
+ end
156
+
157
+ child_outputs.concat(child.all_outputs).uniq!
158
+ end
159
+ end
160
+ end
161
+
162
+ # @private
163
+ # Inputs available for the user for this runnable and all its children.
164
+ def available_inputs
165
+ @available_inputs ||=
166
+ begin
167
+ available_inputs =
168
+ config.inputs
169
+ .slice(*inputs)
170
+ .each_with_object({}) do |(_, input), inputs|
171
+ inputs[input.name.to_sym] = input
172
+ end
173
+
174
+ available_inputs.each do |input, current_definition|
175
+ child_definition = children_available_inputs[input]
176
+ current_definition.merge_with_child(child_definition)
177
+ end
178
+
179
+ available_inputs = children_available_inputs.merge(available_inputs)
180
+ order_available_inputs(available_inputs)
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -1,4 +1,5 @@
1
1
  require_relative 'configurable'
2
+ require_relative 'input_output_handling'
2
3
  require_relative 'resume_test_route'
3
4
  require_relative '../utils/markdown_formatter'
4
5
 
@@ -20,6 +21,7 @@ module Inferno
20
21
  def self.extended(extending_class)
21
22
  super
22
23
  extending_class.extend Configurable
24
+ extending_class.extend InputOutputHandling
23
25
 
24
26
  extending_class.define_singleton_method(:inherited) do |subclass|
25
27
  copy_instance_variables(subclass)
@@ -39,28 +41,35 @@ module Inferno
39
41
 
40
42
  # Class instance variables are used to hold the metadata for Runnable
41
43
  # classes. When inheriting from a Runnable class, these class instance
42
- # variables need to be copied. Any child Runnable classes will themselves
43
- # need to be subclassed so that their parent can be updated.
44
+ # variables need to be copied. Some instance variables should not be
45
+ # copied, and will need to be repopulated from scratch on the new class.
46
+ # Any child Runnable classes will themselves need to be subclassed so that
47
+ # their parent can be updated.
48
+ VARIABLES_NOT_TO_COPY = [
49
+ :@id, # New runnable will have a different id
50
+ :@parent, # New runnable unlikely to have the same parent
51
+ :@children, # New subclasses have to be made for each child
52
+ :@test_count, # Needs to be recalculated
53
+ :@config, # Needs to be set by calling .config, which does extra work
54
+ :@available_inputs, # Needs to be recalculated
55
+ :@children_available_inputs # Needs to be recalculated
56
+ ].freeze
57
+
44
58
  # @private
45
59
  def copy_instance_variables(subclass)
46
- instance_variables.each do |variable|
47
- next if [:@id, :@groups, :@tests, :@parent, :@children, :@test_count, :@config].include?(variable)
48
-
49
- subclass.instance_variable_set(variable, instance_variable_get(variable).dup)
50
- end
60
+ instance_variables
61
+ .reject { |variable| VARIABLES_NOT_TO_COPY.include? variable }
62
+ .each { |variable| subclass.instance_variable_set(variable, instance_variable_get(variable).dup) }
51
63
 
52
64
  subclass.config(config)
53
65
 
54
- child_types.each do |child_type|
55
- new_children = send(child_type).map do |child|
56
- Class.new(child).tap do |subclass_child|
57
- subclass_child.parent = subclass
58
- end
66
+ new_children = children.map do |child|
67
+ Class.new(child).tap do |subclass_child|
68
+ subclass_child.parent = subclass
59
69
  end
60
-
61
- subclass.instance_variable_set(:"@#{child_type}", new_children)
62
- subclass.children.concat(new_children)
63
70
  end
71
+
72
+ subclass.instance_variable_set(:@children, new_children)
64
73
  end
65
74
 
66
75
  # @private
@@ -253,36 +262,6 @@ module Inferno
253
262
  @input_instructions = format_markdown(new_input_instructions)
254
263
  end
255
264
 
256
- # Define inputs
257
- #
258
- # @param identifier [Symbol] identifier for the input
259
- # @param other_identifiers [Symbol] array of symbols if specifying multiple inputs
260
- # @param input_definition [Hash] options for input such as type, description, or title
261
- # @option input_definition [String] :title Human readable title for input
262
- # @option input_definition [String] :description Description for the input
263
- # @option input_definition [String] :type text | textarea | radio
264
- # @option input_definition [String] :default The default value for the input
265
- # @option input_definition [Boolean] :optional Set to true to not require input for test execution
266
- # @option input_definition [Hash] :options Possible input option formats based on input type
267
- # @option options [Array] :list_options Array of options for input formats that require a list of possible values
268
- # @return [void]
269
- # @example
270
- # input :patient_id, title: 'Patient ID', description: 'The ID of the patient being searched for',
271
- # default: 'default_patient_id'
272
- # @example
273
- # input :textarea, title: 'Textarea Input Example', type: 'textarea', optional: true
274
- def input(identifier, *other_identifiers, **input_definition)
275
- if other_identifiers.present?
276
- [identifier, *other_identifiers].compact.each do |input_identifier|
277
- inputs << input_identifier
278
- config.add_input(input_identifier)
279
- end
280
- else
281
- inputs << identifier
282
- config.add_input(identifier, input_definition)
283
- end
284
- end
285
-
286
265
  # Mark as optional. Tests are required by default.
287
266
  #
288
267
  # @param optional [Boolean]
@@ -317,62 +296,11 @@ module Inferno
317
296
  !optional?
318
297
  end
319
298
 
320
- # Define outputs
321
- #
322
- # @param identifier [Symbol] identifier for the output
323
- # @param other_identifiers [Symbol] array of symbols if specifying multiple outputs
324
- # @param output_definition [Hash] options for output
325
- # @option output_definition [String] :type text | textarea | oauth_credentials
326
- # @return [void]
327
- # @example
328
- # output :patient_id, :condition_id, :observation_id
329
- # @example
330
- # output :oauth_credentials, type: 'oauth_credentials'
331
- def output(identifier, *other_identifiers, **output_definition)
332
- if other_identifiers.present?
333
- [identifier, *other_identifiers].compact.each do |output_identifier|
334
- outputs << output_identifier
335
- config.add_output(output_identifier)
336
- end
337
- else
338
- outputs << identifier
339
- config.add_output(identifier, output_definition)
340
- end
341
- end
342
-
343
299
  # @private
344
300
  def default_id
345
301
  to_s
346
302
  end
347
303
 
348
- # @private
349
- def inputs
350
- @inputs ||= []
351
- end
352
-
353
- # @private
354
- def input_definitions
355
- config.inputs.slice(*inputs)
356
- end
357
-
358
- # @private
359
- def output_definitions
360
- config.outputs.slice(*outputs)
361
- end
362
-
363
- # @private
364
- def outputs
365
- @outputs ||= []
366
- end
367
-
368
- # @private
369
- def child_types
370
- return [] if ancestors.include? Inferno::Entities::Test
371
- return [:groups] if ancestors.include? Inferno::Entities::TestSuite
372
-
373
- [:groups, :tests]
374
- end
375
-
376
304
  # @private
377
305
  def children
378
306
  @children ||= []
@@ -454,49 +382,12 @@ module Inferno
454
382
  @test_count ||= children&.reduce(0) { |sum, child| sum + child.test_count } || 0
455
383
  end
456
384
 
457
- # @private
458
- def required_inputs(prior_outputs = [])
459
- required_inputs =
460
- inputs
461
- .reject { |input| input_definitions[input][:optional] }
462
- .map { |input| config.input_name(input) }
463
- .reject { |input| prior_outputs.include?(input) }
464
- children_required_inputs = children.flat_map { |child| child.required_inputs(prior_outputs) }
465
- prior_outputs.concat(outputs.map { |output| config.output_name(output) })
466
- (required_inputs + children_required_inputs).flatten.uniq
467
- end
468
-
469
- # @private
470
- def missing_inputs(submitted_inputs)
471
- submitted_inputs = [] if submitted_inputs.nil?
472
-
473
- required_inputs.map(&:to_s) - submitted_inputs.map { |input| input[:name] }
474
- end
475
-
476
385
  # @private
477
386
  def user_runnable?
478
387
  @user_runnable ||= parent.nil? ||
479
388
  !parent.respond_to?(:run_as_group?) ||
480
389
  (parent.user_runnable? && !parent.run_as_group?)
481
390
  end
482
-
483
- # @private
484
- def available_input_definitions(prior_outputs = [])
485
- available_input_definitions =
486
- inputs
487
- .each_with_object({}) do |input, definitions|
488
- definitions[config.input_name(input)] =
489
- config.input_config(input)
490
- end
491
- available_input_definitions.reject! { |input, _| prior_outputs.include? input }
492
-
493
- children_available_input_definitions =
494
- children.each_with_object({}) do |child, definitions|
495
- definitions.merge!(child.available_input_definitions(prior_outputs))
496
- end
497
- prior_outputs.concat(outputs.map { |output| config.output_name(output) })
498
- children_available_input_definitions.merge(available_input_definitions)
499
- end
500
391
  end
501
392
  end
502
393
  end