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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8c3149fd821b0530f7481d6a01bdef6da32263f1cca74f7b852567edb67af43
4
- data.tar.gz: df8cd12ccb8054d182655c7537c730cd0a1f6fa538552e98171b93c43300d17b
3
+ metadata.gz: 8cffc2d9b151af63840789863f04e8551fd82077351f85d356b5a88f103f28f2
4
+ data.tar.gz: c6687758255cf5acadb700dfd268ee65f6ab03713dbc17ec412d1240d7f352b7
5
5
  SHA512:
6
- metadata.gz: f6d6c9b29f44b0f3169bee590aac204520aa3fc24d651443b5136958cecfffc889d71172881b4c97323b6b17b65d78afd8fd8027a0b8d933037023345fed975d
7
- data.tar.gz: 196c88889027e64aa05e3e7a9997749c9dfc5fab9fecdfa1a40437005d2cfd9a5390945f3efc3382ae4c2a3ea3f208ebc5720b5837ca1ad46fd5f08dfdab8678
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
@@ -1,4 +1,4 @@
1
- /data/*.db
1
+ /data/*.db*
2
2
  /data/igs/*.tgz
3
3
  /data/redis/**/*.rdb
4
4
  /data/redis/**/*.aof
@@ -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 match(/200/)
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 match(/Patient/)
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 match(/resource with id/)
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. Must be a Hash
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
- available_inputs(selected_suite_options)
102
- .reject { |_, input| input.optional }
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) - submitted_inputs.map { |input| input[:name] }
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 { |element| element.mustSupport || by_requirement_extension_only?(element) }
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