inferno_core 1.4.4 → 1.4.5
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 +4 -4
- data/lib/inferno/apps/cli/templates/%library_name%.gemspec.tt +1 -1
- data/lib/inferno/apps/cli/templates/.gitignore +1 -1
- data/lib/inferno/apps/cli/templates/execution_scripts/%library_name%_script_expected.json.tt +0 -8
- data/lib/inferno/apps/cli/templates/spec/%library_name%/patient_group_spec.rb.tt +3 -3
- data/lib/inferno/config/boot/suites.rb +35 -0
- data/lib/inferno/dsl/input_output_handling.rb +101 -5
- data/lib/inferno/dsl/must_support_metadata_extractor.rb +19 -1
- data/lib/inferno/entities/input.rb +57 -0
- data/lib/inferno/public/bundle.js +13 -13
- data/lib/inferno/test_runner.rb +5 -1
- data/lib/inferno/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8cffc2d9b151af63840789863f04e8551fd82077351f85d356b5a88f103f28f2
|
|
4
|
+
data.tar.gz: c6687758255cf5acadb700dfd268ee65f6ab03713dbc17ec412d1240d7f352b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f7ca455177f14a4f0ad5dae5852a7b7441e7ef867e554678b8d55410d623cb7b1d0e4e4432dbd8f1c8cdc085943801d2b98601d9ea776cd1e93f6482a02e3b9
|
|
7
|
+
data.tar.gz: d4160ba11c06d24fc185c2b4cde86fc36c519499f383c79053246114b2e69d0e5be4504e87ea476f462e1439383f8c1a07a5efcab2701449cbd6458939faa13b
|
|
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
|
11
11
|
# DESCRIPTION
|
|
12
12
|
# spec.homepage = 'TODO'
|
|
13
13
|
spec.license = 'Apache-2.0'
|
|
14
|
-
spec.add_dependency 'inferno_core', '~> <%= Inferno::VERSION %>'
|
|
14
|
+
spec.add_dependency 'inferno_core', '~> <%= Inferno::VERSION.split('.').first(2).join('.') %>', '>= <%= Inferno::VERSION %>'
|
|
15
15
|
spec.required_ruby_version = Gem::Requirement.new('>= 3.3.6')
|
|
16
16
|
spec.metadata['inferno_test_kit'] = 'true'
|
|
17
17
|
# spec.metadata['homepage_uri'] = spec.homepage
|
data/lib/inferno/apps/cli/templates/execution_scripts/%library_name%_script_expected.json.tt
CHANGED
|
@@ -126,18 +126,10 @@
|
|
|
126
126
|
"message": "Patient/85: Patient.extension[0]: Unknown extension http://hl7.org/fhir/us/core/StructureDefinition/us-core-race",
|
|
127
127
|
"type": "info"
|
|
128
128
|
},
|
|
129
|
-
{
|
|
130
|
-
"message": "Patient/85: Patient.extension[0].extension[0].value.ofType(Coding): The definition for the Code System with URI 'urn:oid:2.16.840.1.113883.6.238' doesn't provide any codes so the code cannot be validated",
|
|
131
|
-
"type": "info"
|
|
132
|
-
},
|
|
133
129
|
{
|
|
134
130
|
"message": "Patient/85: Patient.extension[1]: Unknown extension http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity",
|
|
135
131
|
"type": "info"
|
|
136
132
|
},
|
|
137
|
-
{
|
|
138
|
-
"message": "Patient/85: Patient.extension[1].extension[0].value.ofType(Coding): The definition for the Code System with URI 'urn:oid:2.16.840.1.113883.6.238' doesn't provide any codes so the code cannot be validated",
|
|
139
|
-
"type": "info"
|
|
140
|
-
},
|
|
141
133
|
{
|
|
142
134
|
"message": "Patient/85: Patient.extension[2]: Unknown extension http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex",
|
|
143
135
|
"type": "info"
|
|
@@ -46,7 +46,7 @@ RSpec.describe <%= module_name %>::PatientGroup do
|
|
|
46
46
|
result = run(test, url: url, patient_id: patient_id)
|
|
47
47
|
|
|
48
48
|
expect(result.result).to eq('fail'), result.result_message
|
|
49
|
-
expect(result.result_message).to
|
|
49
|
+
expect(result.result_message).to include('200')
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
it 'fails if a Patient is not received' do
|
|
@@ -57,7 +57,7 @@ RSpec.describe <%= module_name %>::PatientGroup do
|
|
|
57
57
|
result = run(test, url: url, patient_id: patient_id)
|
|
58
58
|
|
|
59
59
|
expect(result.result).to eq('fail'), result.result_message
|
|
60
|
-
expect(result.result_message).to
|
|
60
|
+
expect(result.result_message).to include('Patient')
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
it 'fails if the id received does not match the one requested' do
|
|
@@ -68,7 +68,7 @@ RSpec.describe <%= module_name %>::PatientGroup do
|
|
|
68
68
|
result = run(test, url: url, patient_id: patient_id)
|
|
69
69
|
|
|
70
70
|
expect(result.result).to eq('fail'), result.result_message
|
|
71
|
-
expect(result.result_message).to
|
|
71
|
+
expect(result.result_message).to include('resource with id')
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
module Inferno
|
|
2
|
+
module Config
|
|
3
|
+
module Boot
|
|
4
|
+
# Extracted from the :suites provider below so the enable_when
|
|
5
|
+
# validation can be unit tested against plain Test/TestGroup/TestSuite
|
|
6
|
+
# classes, instead of requiring a full application boot (which only
|
|
7
|
+
# happens once per process) just to exercise the raise.
|
|
8
|
+
module Suites
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# A circular or undefined enable_when dependency means the input
|
|
12
|
+
# involved can never be enabled, so this is treated as fatal at load
|
|
13
|
+
# time rather than left for developers to discover once the suite is
|
|
14
|
+
# already running. `enable_when` is uninheritable, so a problem local
|
|
15
|
+
# to a group or test can be hidden from the suite's own merged view if
|
|
16
|
+
# a parent re-declares the same input name without repeating
|
|
17
|
+
# `enable_when` -- checking every runnable in the tree, not just the
|
|
18
|
+
# suite, is what catches that case.
|
|
19
|
+
def check_enable_when_errors!(descendant)
|
|
20
|
+
[*descendant.all_descendants.reverse, descendant].each do |runnable|
|
|
21
|
+
enable_when_errors = runnable.enable_when_problem_messages
|
|
22
|
+
next if enable_when_errors.empty?
|
|
23
|
+
|
|
24
|
+
raise StandardError,
|
|
25
|
+
"Error initializing test suite #{descendant.name} (id: #{descendant.id}) " \
|
|
26
|
+
"in '#{runnable.title || runnable.id}' (id: #{runnable.id}):\n- #{enable_when_errors.join("\n- ")}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
1
34
|
Inferno::Application.register_provider(:suites) do
|
|
2
35
|
prepare do
|
|
3
36
|
target_container.start :logging
|
|
@@ -46,6 +79,8 @@ Inferno::Application.register_provider(:suites) do
|
|
|
46
79
|
raise StandardError, "Error initializing test suite #{descendant.name}: test suite ID is not set"
|
|
47
80
|
end
|
|
48
81
|
|
|
82
|
+
Inferno::Config::Boot::Suites.check_enable_when_errors!(descendant)
|
|
83
|
+
|
|
49
84
|
# This will lock the short IDs if a short ID map for this suite is present
|
|
50
85
|
descendant.assign_short_ids
|
|
51
86
|
end
|
|
@@ -16,7 +16,8 @@ module Inferno
|
|
|
16
16
|
# @option input_params [Hash] :options Possible input option formats based on input type
|
|
17
17
|
# @option options [Array] :list_options Array of options for input formats
|
|
18
18
|
# that require a list of possible values (radio and checkbox)
|
|
19
|
-
# @option input_params [Hash] :enable_when Conditions for showing the input
|
|
19
|
+
# @option input_params [Hash] :enable_when Conditions for showing the input and determining
|
|
20
|
+
# whether it is required. Must be a Hash
|
|
20
21
|
# with String :input_name (the name of the controlling input) and String :value (the value
|
|
21
22
|
# that triggers visibility). For checkbox inputs the value must be a JSON-encoded sorted
|
|
22
23
|
# array, e.g. '["a","b"]'.
|
|
@@ -97,9 +98,12 @@ module Inferno
|
|
|
97
98
|
end
|
|
98
99
|
|
|
99
100
|
# @private
|
|
100
|
-
def required_inputs(selected_suite_options)
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
def required_inputs(selected_suite_options, submitted_inputs = nil)
|
|
102
|
+
input_values = (submitted_inputs || []).to_h { |input| [input[:name].to_s, input[:value]] }
|
|
103
|
+
|
|
104
|
+
available = available_inputs(selected_suite_options)
|
|
105
|
+
available
|
|
106
|
+
.reject { |_, input| input.optional || !input.enabled?(input_values, available) }
|
|
103
107
|
.map { |_, input| input.name }
|
|
104
108
|
end
|
|
105
109
|
|
|
@@ -107,7 +111,10 @@ module Inferno
|
|
|
107
111
|
def missing_inputs(submitted_inputs, selected_suite_options)
|
|
108
112
|
submitted_inputs = [] if submitted_inputs.nil?
|
|
109
113
|
|
|
110
|
-
required_inputs(selected_suite_options).map(&:to_s)
|
|
114
|
+
required_input_names = required_inputs(selected_suite_options, submitted_inputs).map(&:to_s)
|
|
115
|
+
submitted_input_names = submitted_inputs.map { |input| input[:name] }
|
|
116
|
+
|
|
117
|
+
required_input_names - submitted_input_names
|
|
111
118
|
end
|
|
112
119
|
|
|
113
120
|
# Define a particular order for inputs to be presented in the API/UI
|
|
@@ -204,6 +211,95 @@ module Inferno
|
|
|
204
211
|
|
|
205
212
|
order_available_inputs(child_inputs.merge(available_inputs))
|
|
206
213
|
end
|
|
214
|
+
|
|
215
|
+
# @private
|
|
216
|
+
# Automatically detect, among this runnable's own inputs: circular
|
|
217
|
+
# `enable_when` dependencies (e.g. input `a` is enabled_when `b`, which
|
|
218
|
+
# is itself enabled_when `a`), and `enable_when` conditions that
|
|
219
|
+
# reference an input not declared on this same runnable (e.g. a typo'd
|
|
220
|
+
# `input_name`). Both silently evaluate as permanently disabled at
|
|
221
|
+
# runtime with no other diagnostic (see `Entities::Input#enabled?`'s
|
|
222
|
+
# `visited` guard and its `input_values.key?` check), so they're
|
|
223
|
+
# treated as fatal at load time instead. Note `enable_when` is an
|
|
224
|
+
# uninheritable attribute -- it only has meaning, and can only
|
|
225
|
+
# reference another input, at the level it's declared -- so a problem
|
|
226
|
+
# local to a group or test can be masked if a parent re-declares the
|
|
227
|
+
# same input name without repeating `enable_when`; checking every
|
|
228
|
+
# runnable in the tree (not just the suite) is what catches those.
|
|
229
|
+
def enable_when_problem_messages
|
|
230
|
+
inputs_by_name = enable_when_local_inputs
|
|
231
|
+
edges = enable_when_edges(inputs_by_name)
|
|
232
|
+
|
|
233
|
+
enable_when_undefined_reference_messages(edges, inputs_by_name) + enable_when_calculated_cycle_messages(edges)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# @private
|
|
237
|
+
# Flags any `enable_when.input_name` that doesn't name an input
|
|
238
|
+
# declared on this same runnable.
|
|
239
|
+
def enable_when_undefined_reference_messages(edges, inputs_by_name)
|
|
240
|
+
local_names = inputs_by_name.values.map(&:name)
|
|
241
|
+
|
|
242
|
+
edges.filter_map do |name, referenced_name|
|
|
243
|
+
next if local_names.include?(referenced_name)
|
|
244
|
+
|
|
245
|
+
"Input '#{name}' has an enable_when condition that references '#{referenced_name}', " \
|
|
246
|
+
'which is not an input defined on the same test/group/suite.'
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# @private
|
|
251
|
+
def enable_when_calculated_cycle_messages(edges)
|
|
252
|
+
globally_visited = {}
|
|
253
|
+
|
|
254
|
+
edges.each_key.filter_map do |start_name|
|
|
255
|
+
next if globally_visited[start_name]
|
|
256
|
+
|
|
257
|
+
cycle = follow_enable_when_chain(start_name, edges, globally_visited)
|
|
258
|
+
next unless cycle
|
|
259
|
+
|
|
260
|
+
chain = (cycle + [cycle.first]).join(' -> ')
|
|
261
|
+
"Circular enable_when dependency detected in input '#{cycle.first}': #{chain}. "
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# @private
|
|
266
|
+
# Maps each input's name to the name of the input it depends on via
|
|
267
|
+
# `enable_when`, for every input in this runnable that has one.
|
|
268
|
+
# `enable_when` is uninheritable (see `Entities::Input::UNINHERITABLE_ATTRIBUTES`),
|
|
269
|
+
# so it's unaffected by the child merge in `available_inputs` -- reading
|
|
270
|
+
# straight from this runnable's own local input config avoids that
|
|
271
|
+
# merge's subtree walk entirely.
|
|
272
|
+
def enable_when_edges(inputs_by_name = enable_when_local_inputs)
|
|
273
|
+
inputs_by_name.each_with_object({}) do |(_, input), depends_on|
|
|
274
|
+
depends_on[input.name] = input.enable_when[:input_name] if input.enable_when.present?
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# @private
|
|
279
|
+
def enable_when_local_inputs
|
|
280
|
+
config.inputs.slice(*inputs)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# @private
|
|
284
|
+
# Follows the `enable_when` chain starting at `start_name`, marking
|
|
285
|
+
# every node visited along the way. Returns the cycle (as an array of
|
|
286
|
+
# input names) if one is found, otherwise nil.
|
|
287
|
+
def follow_enable_when_chain(start_name, depends_on, globally_visited)
|
|
288
|
+
path = []
|
|
289
|
+
node = start_name
|
|
290
|
+
|
|
291
|
+
until node.nil? || globally_visited[node]
|
|
292
|
+
break if path.index(node)
|
|
293
|
+
|
|
294
|
+
path << node
|
|
295
|
+
node = depends_on[node]
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
path.each { |name| globally_visited[name] = true }
|
|
299
|
+
|
|
300
|
+
cycle_start_index = path.index(node)
|
|
301
|
+
cycle_start_index && path[cycle_start_index..]
|
|
302
|
+
end
|
|
207
303
|
end
|
|
208
304
|
end
|
|
209
305
|
end
|
|
@@ -75,7 +75,20 @@ module Inferno
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def all_must_support_elements
|
|
78
|
-
profile_elements.select
|
|
78
|
+
profile_elements.select do |element|
|
|
79
|
+
(element.mustSupport || by_requirement_extension_only?(element)) && !prohibited_by_cardinality?(element)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# A profile can inherit mustSupport flags from a base profile while constraining the element to a max
|
|
84
|
+
# cardinality of 0. Such an element, and its descendants, cannot appear in a conformant resource.
|
|
85
|
+
def prohibited_by_cardinality?(element)
|
|
86
|
+
profile_elements.any? do |profile_element|
|
|
87
|
+
profile_element.max == '0' &&
|
|
88
|
+
(element.id == profile_element.id ||
|
|
89
|
+
element.id.start_with?("#{profile_element.id}.") ||
|
|
90
|
+
element.id.start_with?("#{profile_element.id}:"))
|
|
91
|
+
end
|
|
79
92
|
end
|
|
80
93
|
|
|
81
94
|
def must_support_extension_elements
|
|
@@ -364,6 +377,11 @@ module Inferno
|
|
|
364
377
|
path: navigation_compatible_discriminator_path(discriminator_path),
|
|
365
378
|
value: pattern_element.fixed
|
|
366
379
|
}
|
|
380
|
+
elsif pattern_element.patternString
|
|
381
|
+
fixed_values << {
|
|
382
|
+
path: navigation_compatible_discriminator_path(discriminator_path),
|
|
383
|
+
value: pattern_element.patternString
|
|
384
|
+
}
|
|
367
385
|
elsif pattern_value.present?
|
|
368
386
|
raise StandardError, "Found more than one pattern slices for the same element #{pattern_element}."
|
|
369
387
|
else
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
1
3
|
require_relative 'attributes'
|
|
2
4
|
require_relative '../exceptions'
|
|
3
5
|
|
|
@@ -91,6 +93,30 @@ module Inferno
|
|
|
91
93
|
type_is_hash && input_name_string_exists && value_string_exists
|
|
92
94
|
end
|
|
93
95
|
|
|
96
|
+
# @private
|
|
97
|
+
# Whether this input is enabled for a map of submitted input values.
|
|
98
|
+
# An input without an enable_when condition is always enabled. An input
|
|
99
|
+
# whose controlling input is itself conditionally disabled (i.e. has its
|
|
100
|
+
# own enable_when condition that is not met) is never enabled, regardless
|
|
101
|
+
# of the controlling input's current value.
|
|
102
|
+
# @param visited [Array<Symbol>] names of inputs already visited in this
|
|
103
|
+
# evaluation chain, used to guard against circular enable_when references.
|
|
104
|
+
def enabled?(input_values, available_inputs, visited = [])
|
|
105
|
+
return true if enable_when.blank?
|
|
106
|
+
return false if visited.include?(name)
|
|
107
|
+
return false unless input_values.key?(enable_when[:input_name])
|
|
108
|
+
|
|
109
|
+
controlling_input = available_inputs[enable_when[:input_name].to_sym]
|
|
110
|
+
if controlling_input && !controlling_input.enabled?(input_values, available_inputs, visited + [name])
|
|
111
|
+
return false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
return checkbox_group_enabled?(input_values[enable_when[:input_name]]) if checkbox_group?(controlling_input)
|
|
115
|
+
|
|
116
|
+
normalize_enable_when_value(input_values[enable_when[:input_name]]) ==
|
|
117
|
+
normalize_enable_when_value(enable_when[:value])
|
|
118
|
+
end
|
|
119
|
+
|
|
94
120
|
# @private
|
|
95
121
|
# Merge this input with an input belonging to a child. Fields defined on
|
|
96
122
|
# this input take precedence over those defined on the child input.
|
|
@@ -211,6 +237,37 @@ module Inferno
|
|
|
211
237
|
|
|
212
238
|
ATTRIBUTES.all? { |attribute| send(attribute) == other.send(attribute) }
|
|
213
239
|
end
|
|
240
|
+
|
|
241
|
+
private
|
|
242
|
+
|
|
243
|
+
def checkbox_group?(input)
|
|
244
|
+
input&.type.to_s == 'checkbox' && input.options&.dig(:list_options)&.any?
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def checkbox_group_enabled?(value)
|
|
248
|
+
actual_values = normalize_checkbox_values(value)
|
|
249
|
+
expected_values = normalize_checkbox_values(enable_when[:value])
|
|
250
|
+
!actual_values.nil? && actual_values == expected_values
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def normalize_checkbox_values(value)
|
|
254
|
+
value = JSON.parse(value) if value.is_a?(String)
|
|
255
|
+
return unless value.is_a?(Array) && value.all? { |item| item.is_a?(String) }
|
|
256
|
+
|
|
257
|
+
value.sort
|
|
258
|
+
rescue JSON::ParserError
|
|
259
|
+
nil
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Normalize values using the same rules as the client-side enable_when
|
|
263
|
+
# comparison for non-checkbox-group inputs.
|
|
264
|
+
def normalize_enable_when_value(value)
|
|
265
|
+
return '' if value.nil?
|
|
266
|
+
return JSON.generate(value.sort_by(&:to_s)) if value.is_a?(Array)
|
|
267
|
+
return JSON.generate(value) if value.is_a?(Hash)
|
|
268
|
+
|
|
269
|
+
value.to_s
|
|
270
|
+
end
|
|
214
271
|
end
|
|
215
272
|
end
|
|
216
273
|
end
|