inferno_core 0.2.0 → 0.3.0.rc1

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.
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