inferno_core 1.4.6 → 1.6.0

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: 0ed690c6df4e1cfcb7f4ec361fb8326af28107839c20fbb9de6069ccdc3e950b
4
- data.tar.gz: 1c35c4afdf97fc1f8e053d213bdc643099c79defd0b09e50fccb34977a2a497d
3
+ metadata.gz: ebcc6f348889f26a45097656da0a40a89b233bc7a216f06c8d538092362419a1
4
+ data.tar.gz: dfddeba814ef81eaddbc3b54635903cc01b9a7016a00a06eb6c723a01af2adf7
5
5
  SHA512:
6
- metadata.gz: '048ca08826b17e50cbbd7452d600e996287167ec72d7993ed3a7686849bc6bbcd61221138df2dd5520ee9156deda55c8d577d99f9ade542d17cda31857d82837'
7
- data.tar.gz: b1878c6aedf0f99732536a02fad0cbc2cf2913d21e8cae56586764a04c428346303fd9fb95c462538a96a3968fee7cac3cb0e27bd8b28998e293597aa94d7fd9
6
+ metadata.gz: b97797a253c6d9419ce3a715e56a97464a4ce04f0e48944067c9fbe72b1c645c907d11c33cca7cbe5bfa2771f25908dfdd7ead1f59d63ef5f0850755d2733ce4
7
+ data.tar.gz: '0184d977bbb3e25ef6ef3d9b02ec69e7f17f90cec4f00b54b19e3eab87a5621bec611d3fd2b9a20079503e873ec1141999c1df357d4cb0937da48c36d5a05814'
@@ -23,6 +23,7 @@ module Inferno
23
23
  field :optional?, name: :optional
24
24
  field :simulation_verification?, name: :is_simulation_verification
25
25
  field :attestation?, name: :is_attestation
26
+ field :tester_initiated_exchange?, name: :is_tester_initiated_exchange
26
27
  field :verifies_requirements, if: :field_present?, extractor: RequirementsFilteringExtractor
27
28
  end
28
29
  end
@@ -18,6 +18,7 @@ module Inferno
18
18
  field :user_runnable?, name: :user_runnable
19
19
  field :optional?, name: :optional
20
20
  field :simulation_verification?, name: :is_simulation_verification
21
+ field :tester_initiated_exchange?, name: :is_tester_initiated_exchange
21
22
 
22
23
  field :test_count do |group, options|
23
24
  group.test_count(options[:suite_options])
@@ -145,11 +145,15 @@
145
145
  </div>
146
146
  <div class="modal-body">
147
147
  <p>
148
- Test Kits are broadly grouped into three levels of maturity:
149
- <strong>High</strong>, <strong>Moderate</strong>, and
150
- <strong>Low</strong>. This is intended to provide guidance to users
151
- on the level of confidence they should place in the accuracy and
152
- completeness of these tests.
148
+ Test Kits are broadly grouped into four levels of maturity:
149
+ <strong>Certification</strong>, <strong>High</strong>,
150
+ <strong>Medium</strong>, and <strong>Low</strong>. This is intended
151
+ to provide guidance to users on the level of confidence they should
152
+ place in the accuracy and completeness of these tests.
153
+ </p>
154
+ <p>
155
+ <strong>Certification</strong> maturity Test Kits are used as part
156
+ of a formal certification program.
153
157
  </p>
154
158
  <p>
155
159
  <strong>High</strong> maturity Test Kits typically have been
@@ -157,7 +161,7 @@
157
161
  that has limited ambiguity.
158
162
  </p>
159
163
  <p>
160
- <strong>Moderate</strong> and <strong>Low</strong> maturity Test Kits typically have
164
+ <strong>Medium</strong> and <strong>Low</strong> maturity Test Kits typically have
161
165
  not had widespread usage and/or are targeting less mature
162
166
  specifications that contain ambiguous or conflicting information.
163
167
  These Test Kits are still of value to implementers, but they should be
@@ -252,16 +252,18 @@ module Inferno
252
252
 
253
253
  # @private
254
254
  def matching_pattern_codeable_concept_slice?(slice, discriminator)
255
- slice_value = discriminator[:path].present? ? slice.send((discriminator[:path]).to_s)&.coding : slice.coding
256
- slice_value&.any? do |coding|
255
+ codings = resolve_path(slice, discriminator[:path])
256
+ .flat_map { |codeable_concept| Array.wrap(codeable_concept.coding) }
257
+ codings.any? do |coding|
257
258
  coding.code == discriminator[:code] && coding.system == discriminator[:system]
258
259
  end
259
260
  end
260
261
 
261
262
  # @private
262
263
  def matching_pattern_coding_slice?(slice, discriminator)
263
- slice_value = discriminator[:path].present? ? slice.send(discriminator[:path]) : slice
264
- slice_value&.code == discriminator[:code] && slice_value&.system == discriminator[:system]
264
+ resolve_path(slice, discriminator[:path]).any? do |coding|
265
+ coding.code == discriminator[:code] && coding.system == discriminator[:system]
266
+ end
265
267
  end
266
268
 
267
269
  # @private
@@ -323,7 +325,10 @@ module Inferno
323
325
  when String
324
326
  value == coding.code
325
327
  when Hash
326
- value[:system] == coding.system && value[:code] == coding.code
328
+ next false unless value[:system] == coding.system
329
+
330
+ # A value without a code stands for every code in its system
331
+ value[:code].nil? ? coding.code.present? : value[:code] == coding.code
327
332
  end
328
333
  end
329
334
  end
@@ -1,3 +1,5 @@
1
+ require_relative 'must_support_strings'
2
+
1
3
  module Inferno
2
4
  module DSL
3
5
  # The MustSupportAssessment module contains the logic for tests
@@ -23,7 +25,7 @@ module Inferno
23
25
  # test kits can subclass to build these objects, eg from generated YAML, instead of hand-rolling one.
24
26
  # @param requirement_extension [String] Extension URL that implies "required" as an alternative to the MS flag
25
27
  # @yield [MustSupportMetadataExtractor] Customize the metadata before running the test
26
- # @return [Array<String>] List of missing elements
28
+ # @return [Array<String>] List of missing elements, sorted alphabetically
27
29
  def missing_must_support_elements(resources, profile_url, validator_name: :default, metadata: nil,
28
30
  requirement_extension: nil, &)
29
31
  debug_metadata = config.options[:debug_must_support_metadata]
@@ -195,18 +197,17 @@ module Inferno
195
197
  end
196
198
  end
197
199
 
200
+ # See {MustSupportStrings} for how each missing item is represented. Sorted to match
201
+ # {ProfileMetadata#must_support_strings}.
198
202
  def missing_must_support_strings
199
- missing_elements.map { |element_definition| missing_element_string(element_definition) } +
200
- missing_slices.map { |slice_definition| slice_definition[:slice_id] } +
201
- missing_extensions.map { |extension_definition| extension_definition[:id] }
203
+ (missing_elements.map { |element_definition| missing_element_string(element_definition) } +
204
+ missing_slices.map { |slice_definition| MustSupportStrings.slice_string(slice_definition) } +
205
+ missing_extensions.map { |extension_definition| MustSupportStrings.extension_string(extension_definition) })
206
+ .sort
202
207
  end
203
208
 
204
209
  def missing_element_string(element_definition)
205
- if element_definition[:fixed_value].present?
206
- "#{element_definition[:path]}:#{element_definition[:fixed_value]}"
207
- else
208
- element_definition[:path]
209
- end
210
+ MustSupportStrings.element_string(element_definition)
210
211
  end
211
212
 
212
213
  def must_support_extensions
@@ -452,17 +453,6 @@ module Inferno
452
453
  def find_slice_by_values(element, value_definitions)
453
454
  Array.wrap(element).find { |el| verify_slice_by_values(el, value_definitions) }
454
455
  end
455
-
456
- def required_binding_value_match?(coding, values)
457
- values.any? do |value|
458
- case value
459
- when String
460
- value == coding.code
461
- when Hash
462
- value[:system] == coding.system && value[:code] == coding.code
463
- end
464
- end
465
- end
466
456
  end
467
457
  end
468
458
  end
@@ -0,0 +1,62 @@
1
+ module Inferno
2
+ module DSL
3
+ # Builds the strings used to identify Must Support elements, slices, and extensions from their
4
+ # metadata, so that lists of what is expected to be supported (eg {ProfileMetadata#must_support_strings})
5
+ # and lists of what is missing (eg {MustSupportAssessment#missing_must_support_elements}) agree.
6
+ #
7
+ # Each item is represented by its ElementDefinition id (which includes the names of any parent
8
+ # slices) without the leading resource type, eg 'category:us-core' rather than
9
+ # 'Condition.category:us-core'. Elements with a fixed value have it appended, eg 'name.use = old'.
10
+ # @private
11
+ module MustSupportStrings
12
+ module_function
13
+
14
+ # @param element_definition [Hash] Must Support element metadata
15
+ # @return [String]
16
+ def element_string(element_definition)
17
+ path = element_display_path(element_definition)
18
+
19
+ fixed_value = element_definition[:fixed_value]
20
+ if fixed_value.present? || fixed_value == false
21
+ "#{path} = #{fixed_value}"
22
+ else
23
+ path
24
+ end
25
+ end
26
+
27
+ # @param slice_definition [Hash] Must Support slice metadata
28
+ # @return [String]
29
+ def slice_string(slice_definition)
30
+ id_string(slice_definition, :slice_id)
31
+ end
32
+
33
+ # @param extension_definition [Hash] Must Support extension metadata
34
+ # @return [String]
35
+ def extension_string(extension_definition)
36
+ id_string(extension_definition, :id)
37
+ end
38
+
39
+ # Elements under a choice type slice (eg value[x]:valueQuantity.value) have their navigation
40
+ # path rewritten so the slice name is mangled (eg valueQuantity:valueQuantity.value), so report
41
+ # the original path instead. Elements where only some types of a choice are Must Support
42
+ # (eg original path effective[x], path effectiveDateTime) keep their path since it names the type.
43
+ def element_display_path(element_definition)
44
+ original_path = element_definition[:original_path]
45
+ original_path&.include?('[x]:') ? original_path : element_definition[:path]
46
+ end
47
+
48
+ def id_string(definition, id_key)
49
+ id = definition[id_key]
50
+ raise ArgumentError, "Must Support metadata is missing #{id_key.inspect}: #{definition.inspect}" if id.blank?
51
+
52
+ without_resource_type(id)
53
+ end
54
+
55
+ # Resource types are PascalCase while element names are camelCase, so a leading
56
+ # capitalized segment is the resource type prefix of an ElementDefinition id.
57
+ def without_resource_type(id)
58
+ id&.sub(/\A[A-Z][A-Za-z0-9]*\./, '')
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,3 +1,5 @@
1
+ require_relative 'must_support_strings'
2
+
1
3
  module Inferno
2
4
  module DSL
3
5
  # A ProfileMetadata object is a small, inheritable data holder for generated metadata about a
@@ -85,29 +87,18 @@ module Inferno
85
87
  # check, this includes everything that's expected to be supported, not just what's absent
86
88
  # from a particular set of resources.
87
89
  #
88
- # Elements are represented as their `path`, with the matched `fixed_value` appended after a
89
- # colon where present (eg `"code.coding.code:45473-6"`). Slices and extensions are
90
- # represented as their `path`, with their `slice_name` appended after a colon where present
91
- # (eg `"category:us-core"`, `"extension:us-core-race"`).
90
+ # Each item is represented the same way as in the list returned by a missing-elements check:
91
+ # by its ElementDefinition id (including the names of any parent slices) without the leading
92
+ # resource type (eg `"category:us-core"`, `"extension:us-core-race"`), with any matched
93
+ # `fixed_value` appended to elements (eg `"code.coding.code = 45473-6"`). See {MustSupportStrings}.
92
94
  # @return [Array<String>]
93
95
  def must_support_strings
94
- element_strings =
95
- Array.wrap(must_supports[:elements]).map do |element|
96
- must_support_string(element[:path], element[:fixed_value])
97
- end
98
- slice_strings =
99
- Array.wrap(must_supports[:slices]).map { |slice| must_support_string(slice[:path], slice[:slice_name]) }
100
- extension_strings =
101
- Array.wrap(must_supports[:extensions]).map { |ext| must_support_string(ext[:path], ext[:slice_name]) }
96
+ element_strings = Array.wrap(must_supports[:elements]).map { |el| MustSupportStrings.element_string(el) }
97
+ slice_strings = Array.wrap(must_supports[:slices]).map { |slice| MustSupportStrings.slice_string(slice) }
98
+ extension_strings = Array.wrap(must_supports[:extensions]).map { |ex| MustSupportStrings.extension_string(ex) }
102
99
 
103
100
  (element_strings + slice_strings + extension_strings).sort
104
101
  end
105
-
106
- private
107
-
108
- def must_support_string(path, suffix)
109
- suffix.present? ? "#{path}:#{suffix}" : path
110
- end
111
102
  end
112
103
  end
113
104
  end
@@ -75,6 +75,17 @@ module Inferno
75
75
  test_run.test_suite_id || test_run.test_group_id || test.parent&.id || test.id
76
76
  end
77
77
 
78
+ # @private
79
+ def no_test_run_message(test_run_identifier, url)
80
+ base_message = "Inferno could not find an active test run for this request to '#{url}'"
81
+
82
+ if test_run_identifier.blank?
83
+ "#{base_message}: no identifier found."
84
+ else
85
+ "#{base_message}: identifier '#{test_run_identifier}' is not associated with a waiting session."
86
+ end
87
+ end
88
+
78
89
  # @private
79
90
  def handle(req, res)
80
91
  request = Inferno::Entities::Request.from_hanami_request(req)
@@ -84,10 +95,7 @@ module Inferno
84
95
 
85
96
  test_run = find_test_run(test_run_identifier)
86
97
 
87
- if test_run.nil?
88
- halt 500, "Unable to find test run with identifier '#{test_run_identifier}' " \
89
- "on request to '#{request.url}'."
90
- end
98
+ halt 500, no_test_run_message(test_run_identifier, request.url) if test_run.nil?
91
99
 
92
100
  test_runs_repo.mark_as_no_longer_waiting(test_run.id)
93
101
 
@@ -309,14 +309,17 @@ module Inferno
309
309
  !optional?
310
310
  end
311
311
 
312
- # Mark as simulation verification. Tests are not simulation verification by default.
312
+ # Flag as simulation verification. Tests are not simulation verification by default.
313
313
  #
314
314
  # @param simulation_verification [Boolean]
315
315
  # @return [void]
316
- def simulation_verification(simulation_verification = true) # rubocop:disable Style/OptionalBooleanParameter
316
+ def flag_as_simulation_verification(simulation_verification = true) # rubocop:disable Style/OptionalBooleanParameter
317
317
  @is_simulation_verification = simulation_verification
318
318
  end
319
319
 
320
+ # Alias for {#flag_as_simulation_verification}
321
+ alias simulation_verification flag_as_simulation_verification
322
+
320
323
  # The test is simulation verification if true
321
324
  #
322
325
  # @return [Boolean]
@@ -324,14 +327,17 @@ module Inferno
324
327
  !!@is_simulation_verification
325
328
  end
326
329
 
327
- # Mark as attestation. Tests are not attestation by default.
330
+ # Flag as attestation. Tests are not attestation by default.
328
331
  #
329
332
  # @param attestation [Boolean]
330
333
  # @return [void]
331
- def attestation(attestation = true) # rubocop:disable Style/OptionalBooleanParameter
334
+ def flag_as_attestation(attestation = true) # rubocop:disable Style/OptionalBooleanParameter
332
335
  @is_attestation = attestation
333
336
  end
334
337
 
338
+ # Alias for {#flag_as_attestation}
339
+ alias attestation flag_as_attestation
340
+
335
341
  # The test is attestation if true
336
342
  #
337
343
  # @return [Boolean]
@@ -339,6 +345,21 @@ module Inferno
339
345
  !!@is_attestation
340
346
  end
341
347
 
348
+ # Flag as tester-initiated exchange. Tests are not tester-initiated exchanges by default.
349
+ #
350
+ # @param tester_initiated_exchange [Boolean]
351
+ # @return [void]
352
+ def flag_as_tester_initiated_exchange(tester_initiated_exchange = true) # rubocop:disable Style/OptionalBooleanParameter
353
+ @is_tester_initiated_exchange = tester_initiated_exchange
354
+ end
355
+
356
+ # The test is a tester-initiated exchange if true
357
+ #
358
+ # @return [Boolean]
359
+ def tester_initiated_exchange?
360
+ !!@is_tester_initiated_exchange
361
+ end
362
+
342
363
  # @private
343
364
  def default_id
344
365
  to_s
@@ -97,7 +97,9 @@ module Inferno
97
97
 
98
98
  # @private
99
99
  def error_response_format_value
100
- @error_response_format_value ||= :text
100
+ return @error_response_format_value if @error_response_format_value.present?
101
+
102
+ superclass.respond_to?(:error_response_format_value) ? superclass.error_response_format_value : :text
101
103
  end
102
104
  end
103
105
 
@@ -308,7 +310,8 @@ module Inferno
308
310
  log_error(e)
309
311
  render_error_and_halt do
310
312
  error_response(
311
- 'An error occurred while determining the test run identifier for this request.',
313
+ "Inferno could not find an active test run for this request to '#{request.url}': " \
314
+ 'error encountered while determining the test run identifier.',
312
315
  code: 'exception',
313
316
  diagnostics: e.full_message
314
317
  )
@@ -317,7 +320,7 @@ module Inferno
317
320
 
318
321
  # @private
319
322
  def no_session_message
320
- base_message = "Unable to find test run for request to '#{request.url}'"
323
+ base_message = "Inferno could not find an active test run for this request to '#{request.url}'"
321
324
  location = test_run_identifier_location_description
322
325
  identifier = find_test_run_identifier
323
326
 
@@ -369,10 +372,11 @@ module Inferno
369
372
  details: FHIR::CodeableConcept.new(text: message)
370
373
  )
371
374
  issue.diagnostics = diagnostics if diagnostics
375
+ meta = FHIR::Meta.new(source: Application['base_url'])
372
376
 
373
377
  response.status = 500
374
378
  response.content_type = 'application/fhir+json'
375
- response.body = FHIR::OperationOutcome.new(issue: [issue]).to_json
379
+ response.body = FHIR::OperationOutcome.new(meta:, issue: [issue]).to_json
376
380
  end
377
381
 
378
382
  # @private
@@ -427,7 +431,7 @@ module Inferno
427
431
  log_error(e)
428
432
  render_error_and_halt do
429
433
  error_response(
430
- 'An error occurred while processing this request.',
434
+ "Inferno encountered an error while processing this request to '#{request.url}'.",
431
435
  code: 'exception',
432
436
  diagnostics: e.full_message
433
437
  )
@@ -459,7 +463,7 @@ module Inferno
459
463
 
460
464
  response_headers = response_headers.map { |name, value| { name:, value: } }
461
465
 
462
- repo.create(
466
+ persisted_request = repo.create(
463
467
  verb:,
464
468
  url:,
465
469
  direction: 'incoming',
@@ -474,6 +478,15 @@ module Inferno
474
478
  tags: env['inferno.tags']
475
479
  )
476
480
 
481
+ # Display the request with the waiting test's result. Failures are
482
+ # logged rather than raised so that they cannot prevent the test run
483
+ # from resuming.
484
+ begin
485
+ results_repo.add_request(env['inferno.result_id'], persisted_request.id)
486
+ rescue StandardError => e
487
+ log_error(e, url:)
488
+ end
489
+
477
490
  if env['inferno.resume_test_run']
478
491
  test_run_id = env['inferno.test_run_id']
479
492
  Inferno::Repositories::TestRuns.new.mark_as_no_longer_waiting(test_run_id)
@@ -60,9 +60,8 @@ module Inferno
60
60
  def bound_systems_from_valueset_include_element(include_element)
61
61
  if include_element.concept.present?
62
62
  include_element
63
- elsif include_element.system.present? && include_element.filter&.empty?
64
- # Cannot process intensional value set with filters
65
- ig_resources.code_system_by_url(include_element.system)
63
+ elsif whole_code_system_include?(include_element)
64
+ whole_code_system(include_element)
66
65
  elsif include_element.valueSet.present?
67
66
  include_element.valueSet.map do |vs|
68
67
  a_value_set = ig_resources.value_set_by_url(vs)
@@ -71,6 +70,18 @@ module Inferno
71
70
  end
72
71
  end
73
72
 
73
+ def whole_code_system_include?(include_element)
74
+ include_element.system.present? && include_element.concept.blank? &&
75
+ include_element.filter&.empty? && include_element.valueSet.blank?
76
+ end
77
+
78
+ # An include of a whole code system lists no codes. Use the code system's concepts when the IG
79
+ # has them, otherwise keep the include so that any code from its system is accepted.
80
+ def whole_code_system(include_element)
81
+ code_system = ig_resources.code_system_by_url(include_element.system)
82
+ code_system&.concept.present? ? code_system : include_element
83
+ end
84
+
74
85
  def codes_from_value_set_binding(the_element)
75
86
  codes_from_system_code_pair(codings_from_value_set_binding(the_element))
76
87
  end
@@ -99,7 +110,7 @@ module Inferno
99
110
  bound_systems.flat_map do |bound_system|
100
111
  case bound_system
101
112
  when FHIR::ValueSet::Compose::Include
102
- bound_system.concept.map { |concept| { system: bound_system.system, code: concept.code } }
113
+ codings_from_include(bound_system)
103
114
  when FHIR::CodeSystem
104
115
  bound_system.concept.map { |concept| { system: bound_system.url, code: concept.code } }
105
116
  else
@@ -108,6 +119,13 @@ module Inferno
108
119
  end.uniq
109
120
  end
110
121
 
122
+ # A whole code system include becomes a system-only value
123
+ def codings_from_include(include_element)
124
+ return [{ system: include_element.system }] if include_element.concept.blank?
125
+
126
+ include_element.concept.map { |concept| { system: include_element.system, code: concept.code } }
127
+ end
128
+
111
129
  def value_set_expansion_contains(element)
112
130
  value_set(element)&.expansion&.contains
113
131
  end
@@ -92,7 +92,8 @@ module Inferno
92
92
 
93
93
  # Set/get the maturity level for the test kit
94
94
  #
95
- # @param new_maturity [String]
95
+ # @param new_maturity [String] one of 'Low', 'Medium', 'High', or
96
+ # 'Certification'
96
97
  # @return [String]
97
98
  def maturity(new_maturity = nil)
98
99
  return @maturity if new_maturity.nil?