inferno_core 1.4.2 → 1.4.3
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/execute_script.rb +8 -4
- data/lib/inferno/apps/cli/requirements_coverage_checker.rb +1 -1
- data/lib/inferno/dsl/fhir_resource_navigation.rb +12 -2
- data/lib/inferno/dsl/fhir_resource_validation.rb +96 -0
- data/lib/inferno/dsl/must_support_assessment.rb +19 -8
- data/lib/inferno/dsl/must_support_metadata_extractor.rb +44 -1
- data/lib/inferno/dsl/profile_metadata.rb +113 -0
- data/lib/inferno/dsl/resume_test_route.rb +4 -1
- data/lib/inferno/dsl/suite_endpoint.rb +164 -6
- data/lib/inferno/dsl.rb +1 -0
- data/lib/inferno/entities/ig.rb +51 -8
- data/lib/inferno/public/bundle.js +34 -34
- data/lib/inferno/public/bundle.js.LICENSE.txt +0 -2
- data/lib/inferno/test_runner.rb +55 -40
- data/lib/inferno/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 044cfcbc6d3920cfba55d0d63fbf0fb3a1440f4f42a2e45d763beb78f280306a
|
|
4
|
+
data.tar.gz: 243c006acc71fddd3eb165a2c400a1eab4e515f8c3f4915b27cd60fc8e41e734
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a1c934c0c6bb863a66e34b2830807ee274f443323d7c74c10023bf078329abf2bc70d9125f481b388c3e9ac61a647c81a8f6ee6f7c156078a9dc4fc31adcb671
|
|
7
|
+
data.tar.gz: 6a54d11609e88fadd4c4750966e66a1df705ab1a1c79b8c8c0b99f5d3a19467496c46db740dfb2937610c9c78e8a7bf14db7fe8112c7c8399bb43a16bdcd6937
|
|
@@ -138,7 +138,7 @@ module Inferno
|
|
|
138
138
|
|
|
139
139
|
ExecutionStatus = Struct.new(
|
|
140
140
|
:done, :failed, :timed_out, :cancel_pending, :current_session, :current_timeout, :last_log_time,
|
|
141
|
-
:cross_session_status, :last_step_signatures
|
|
141
|
+
:poll_start_time, :cross_session_status, :last_step_signatures
|
|
142
142
|
)
|
|
143
143
|
|
|
144
144
|
attr_accessor :yaml_file, :options, :execution_status
|
|
@@ -155,6 +155,7 @@ module Inferno
|
|
|
155
155
|
cancel_pending: false,
|
|
156
156
|
current_session: sessions.first,
|
|
157
157
|
current_timeout: options[:default_poll_timeout],
|
|
158
|
+
poll_start_time: nil,
|
|
158
159
|
cross_session_status: {},
|
|
159
160
|
last_step_signatures: {}
|
|
160
161
|
)
|
|
@@ -416,6 +417,7 @@ module Inferno
|
|
|
416
417
|
warn ''
|
|
417
418
|
warn "Polling session: #{session.key} (#{session.session_id}) timeout=#{timeout}s"
|
|
418
419
|
deadline = Time.now + timeout
|
|
420
|
+
execution_status.poll_start_time = Time.now
|
|
419
421
|
execution_status.last_log_time = Time.now - LOG_INTERVAL_SECONDS
|
|
420
422
|
|
|
421
423
|
loop do
|
|
@@ -518,12 +520,14 @@ module Inferno
|
|
|
518
520
|
last_completed = last_completed_from_status(status)
|
|
519
521
|
poll_status_last_test =
|
|
520
522
|
last_completed.present? ? " - last test: #{format_last_completed(last_completed, session_key)}" : ''
|
|
521
|
-
|
|
523
|
+
elapsed = (Time.now - execution_status.poll_start_time).round
|
|
524
|
+
warn " [#{session_key}] #{status['status']} (#{test_progress(status, elapsed)})" \
|
|
525
|
+
"#{poll_status_last_test}"
|
|
522
526
|
execution_status.last_log_time = Time.now
|
|
523
527
|
end
|
|
524
528
|
|
|
525
|
-
def test_progress(status)
|
|
526
|
-
"#{status['completed_test_count']}/#{status['test_count']} tests"
|
|
529
|
+
def test_progress(status, elapsed)
|
|
530
|
+
"completed #{status['completed_test_count']}/#{status['test_count']} tests in #{elapsed}s"
|
|
527
531
|
end
|
|
528
532
|
|
|
529
533
|
def fetch_session_status(session_id)
|
|
@@ -119,7 +119,7 @@ module Inferno
|
|
|
119
119
|
suite_runnables.select { |runnable| runnable.verifies_requirements.include? requirement_id }
|
|
120
120
|
|
|
121
121
|
runnables_for_requirement.map do |runnable|
|
|
122
|
-
[requirement_id, runnable.short_id, runnable.id]
|
|
122
|
+
[requirement_id, runnable < Inferno::Entities::TestSuite ? 'suite' : runnable.short_id, runnable.id]
|
|
123
123
|
end
|
|
124
124
|
end
|
|
125
125
|
end
|
|
@@ -42,8 +42,13 @@ module Inferno
|
|
|
42
42
|
# @param given_element [FHIR::Model, Array<FHIR::Model>]
|
|
43
43
|
# @param path [String]
|
|
44
44
|
# @param include_dar [Boolean]
|
|
45
|
+
# @param recursive_segments [Array<String>] names of path segments that are
|
|
46
|
+
# self-referential (eg. `item` in Questionnaire.item.item, via a FHIR
|
|
47
|
+
# `contentReference`). When a segment matches, the search also continues
|
|
48
|
+
# into deeper repeats of that segment (item.item.item...) if the value
|
|
49
|
+
# isn't found at the literal path depth.
|
|
45
50
|
# @return a single matching value (which can include `false`) or `nil` if not found
|
|
46
|
-
def find_a_value_at(given_element, path, include_dar: false, &block)
|
|
51
|
+
def find_a_value_at(given_element, path, include_dar: false, recursive_segments: [], &block)
|
|
47
52
|
return nil if given_element.nil?
|
|
48
53
|
|
|
49
54
|
elements = Array.wrap(given_element)
|
|
@@ -56,8 +61,13 @@ module Inferno
|
|
|
56
61
|
remaining_path = path_segments.join('.')
|
|
57
62
|
elements.each do |element|
|
|
58
63
|
child = get_next_value(element, segment)
|
|
59
|
-
element_found = find_a_value_at(child, remaining_path, include_dar:, &block)
|
|
64
|
+
element_found = find_a_value_at(child, remaining_path, include_dar:, recursive_segments:, &block)
|
|
60
65
|
return element_found if value_not_empty?(element_found)
|
|
66
|
+
|
|
67
|
+
next unless recursive_segments.include?(segment)
|
|
68
|
+
|
|
69
|
+
nested_found = find_a_value_at(child, path, include_dar:, recursive_segments:, &block)
|
|
70
|
+
return nested_found if value_not_empty?(nested_found)
|
|
61
71
|
end
|
|
62
72
|
|
|
63
73
|
nil
|
|
@@ -26,6 +26,7 @@ module Inferno
|
|
|
26
26
|
# allowExampleUrls true
|
|
27
27
|
# txServer nil
|
|
28
28
|
# end
|
|
29
|
+
# expansion_parameters 'path/to/expansion_parameters.json'
|
|
29
30
|
# end
|
|
30
31
|
module FHIRResourceValidation
|
|
31
32
|
def self.included(klass)
|
|
@@ -118,6 +119,99 @@ module Inferno
|
|
|
118
119
|
|
|
119
120
|
alias cli_context validation_context
|
|
120
121
|
|
|
122
|
+
# Environment variable containing the default expansion parameters.
|
|
123
|
+
# Used by {#expansion_parameters} when no value has been set
|
|
124
|
+
# explicitly. May contain either the raw JSON content of a FHIR
|
|
125
|
+
# Parameters resource (if it starts with `{`) or a path to a file
|
|
126
|
+
# containing one.
|
|
127
|
+
EXPANSION_PARAMETERS_ENV_VAR = 'FHIR_RESOURCE_VALIDATOR_EXPANSION_PARAMETERS'.freeze
|
|
128
|
+
|
|
129
|
+
# Set the expansion parameters to be sent with each validation
|
|
130
|
+
# request. This configures how the validator's terminology engine
|
|
131
|
+
# expands value sets during validation (e.g. designation
|
|
132
|
+
# preferences, forcing the use of the latest terminology versions,
|
|
133
|
+
# etc.). The content is sent inline with every validation request
|
|
134
|
+
# made by this validator, since the validator does not have access
|
|
135
|
+
# to Inferno's filesystem.
|
|
136
|
+
#
|
|
137
|
+
# Accepts either a Hash containing the contents of a FHIR Parameters
|
|
138
|
+
# resource, or a String path to a file (JSON or XML) containing one.
|
|
139
|
+
# The file is read once, the first time it's needed.
|
|
140
|
+
#
|
|
141
|
+
# If never set explicitly, this falls back to the
|
|
142
|
+
# `FHIR_RESOURCE_VALIDATOR_EXPANSION_PARAMETERS` environment
|
|
143
|
+
# variable, if present. This allows a shared set of expansion
|
|
144
|
+
# parameters to be configured once for every test kit that uses a
|
|
145
|
+
# given validator instance, while still letting individual test
|
|
146
|
+
# kits opt out or override it by calling this method themselves.
|
|
147
|
+
# The environment variable's content is treated as raw JSON if it
|
|
148
|
+
# starts with `{`, and otherwise as a file path.
|
|
149
|
+
#
|
|
150
|
+
# @example
|
|
151
|
+
# # Passing a Hash
|
|
152
|
+
# fhir_resource_validator do
|
|
153
|
+
# url 'http://example.com/validator'
|
|
154
|
+
# expansion_parameters({
|
|
155
|
+
# resourceType: 'Parameters',
|
|
156
|
+
# parameter: [{ name: 'excludeNested', valueBoolean: true }]
|
|
157
|
+
# })
|
|
158
|
+
# end
|
|
159
|
+
#
|
|
160
|
+
# @example
|
|
161
|
+
# # Passing a file path
|
|
162
|
+
# fhir_resource_validator do
|
|
163
|
+
# url 'http://example.com/validator'
|
|
164
|
+
# expansion_parameters 'path/to/expansion_parameters.json'
|
|
165
|
+
# end
|
|
166
|
+
#
|
|
167
|
+
# @param value [Hash, String, nil] contents of a Parameters resource
|
|
168
|
+
# as a Hash, or a path to a file (JSON or XML) containing one
|
|
169
|
+
def expansion_parameters(value = nil)
|
|
170
|
+
if value
|
|
171
|
+
@expansion_parameters = build_expansion_parameters(value)
|
|
172
|
+
elsif !@expansion_parameters_resolved
|
|
173
|
+
env_value = ENV.fetch(EXPANSION_PARAMETERS_ENV_VAR, nil)
|
|
174
|
+
@expansion_parameters = build_expansion_parameters_from_env(env_value) if env_value
|
|
175
|
+
end
|
|
176
|
+
@expansion_parameters_resolved = true
|
|
177
|
+
|
|
178
|
+
@expansion_parameters
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# @private
|
|
182
|
+
# Determines whether the environment variable's content is raw JSON
|
|
183
|
+
# or a file path based on its first non-whitespace character.
|
|
184
|
+
def build_expansion_parameters_from_env(env_value)
|
|
185
|
+
if env_value.lstrip.start_with?('{')
|
|
186
|
+
build_expansion_parameters_from_json_content(env_value)
|
|
187
|
+
else
|
|
188
|
+
build_expansion_parameters(env_value)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# @private
|
|
193
|
+
def build_expansion_parameters(value)
|
|
194
|
+
case value
|
|
195
|
+
when Hash
|
|
196
|
+
build_expansion_parameters_from_json_content(value.to_json)
|
|
197
|
+
else
|
|
198
|
+
{
|
|
199
|
+
fileName: File.basename(value),
|
|
200
|
+
fileContent: File.read(value),
|
|
201
|
+
fileType: nil
|
|
202
|
+
}
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# @private
|
|
207
|
+
def build_expansion_parameters_from_json_content(json_content)
|
|
208
|
+
{
|
|
209
|
+
fileName: 'expansion_parameters.json',
|
|
210
|
+
fileContent: json_content,
|
|
211
|
+
fileType: nil
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
|
|
121
215
|
# @private
|
|
122
216
|
# Used internally by perform_additional_validation
|
|
123
217
|
def additional_validations
|
|
@@ -622,6 +716,8 @@ module Inferno
|
|
|
622
716
|
],
|
|
623
717
|
sessionId: @session_id
|
|
624
718
|
}
|
|
719
|
+
wrapped_resource[:expansionParameters] = expansion_parameters if expansion_parameters
|
|
720
|
+
|
|
625
721
|
wrapped_resource.to_json
|
|
626
722
|
end
|
|
627
723
|
end
|
|
@@ -15,10 +15,14 @@ module Inferno
|
|
|
15
15
|
# @param resources [Array<FHIR::Resource>]
|
|
16
16
|
# @param profile_url [String]
|
|
17
17
|
# @param validator_name [Symbol] Name of the FHIR Validator that references the IG the profile is in
|
|
18
|
-
# @param metadata [
|
|
19
|
-
# if provided the check will use this instead of re-generating metadata from the profile
|
|
18
|
+
# @param metadata [Inferno::DSL::ProfileMetadata, #must_supports] MustSupport Metadata (optional),
|
|
19
|
+
# if provided the check will use this instead of re-generating metadata from the profile.
|
|
20
|
+
# Must respond to `#must_supports`, returning a Hash with `:elements`, `:extensions`, and
|
|
21
|
+
# `:slices` keys (and optionally `:choices` and `:recursive_elements`) -- the shape produced by
|
|
22
|
+
# {MustSupportMetadataExtractor#must_supports}. {Inferno::DSL::ProfileMetadata} is a base class
|
|
23
|
+
# test kits can subclass to build these objects, eg from generated YAML, instead of hand-rolling one.
|
|
20
24
|
# @param requirement_extension [String] Extension URL that implies "required" as an alternative to the MS flag
|
|
21
|
-
# @yield [
|
|
25
|
+
# @yield [MustSupportMetadataExtractor] Customize the metadata before running the test
|
|
22
26
|
# @return [Array<String>] List of missing elements
|
|
23
27
|
def missing_must_support_elements(resources, profile_url, validator_name: :default, metadata: nil,
|
|
24
28
|
requirement_extension: nil, &)
|
|
@@ -199,6 +203,12 @@ module Inferno
|
|
|
199
203
|
metadata.must_supports[:extensions]
|
|
200
204
|
end
|
|
201
205
|
|
|
206
|
+
# Names of path segments that are self-referential (eg 'item' in Questionnaire.item.item)
|
|
207
|
+
# and so should be searched at any depth of nesting, not just the literal depth of a path.
|
|
208
|
+
def recursive_element_segments
|
|
209
|
+
Array.wrap(metadata.must_supports[:recursive_elements])
|
|
210
|
+
end
|
|
211
|
+
|
|
202
212
|
def missing_extensions(resources = [])
|
|
203
213
|
@missing_extensions ||=
|
|
204
214
|
must_support_extensions.select do |extension_definition|
|
|
@@ -212,7 +222,7 @@ module Inferno
|
|
|
212
222
|
normalized_extension_url(extension.url) == expected_url
|
|
213
223
|
end
|
|
214
224
|
else
|
|
215
|
-
extension = find_a_value_at(resource, path) do |el|
|
|
225
|
+
extension = find_a_value_at(resource, path, recursive_segments: recursive_element_segments) do |el|
|
|
216
226
|
normalized_extension_url(el.url) == expected_url
|
|
217
227
|
end
|
|
218
228
|
|
|
@@ -243,9 +253,10 @@ module Inferno
|
|
|
243
253
|
ms_extension_urls = must_support_extensions.select { |ex| ex[:path] == "#{raw_path}.extension" }
|
|
244
254
|
.map { |ex| ex[:url] }
|
|
245
255
|
|
|
246
|
-
value_found =
|
|
247
|
-
|
|
248
|
-
|
|
256
|
+
value_found =
|
|
257
|
+
find_a_value_at(resource, path, recursive_segments: recursive_element_segments) do |potential_value|
|
|
258
|
+
matching_without_extensions?(potential_value, ms_extension_urls, element_definition[:fixed_value])
|
|
259
|
+
end
|
|
249
260
|
|
|
250
261
|
# Note that false.present? => false, which is why we need to add this extra check
|
|
251
262
|
value_found.present? || value_found == false
|
|
@@ -369,7 +380,7 @@ module Inferno
|
|
|
369
380
|
# TODO: there is a lot of similarity
|
|
370
381
|
# between this and FHIRResourceNavigation.matching_slice?
|
|
371
382
|
# Can these be combined?
|
|
372
|
-
find_a_value_at(resource, path) do |element|
|
|
383
|
+
find_a_value_at(resource, path, recursive_segments: recursive_element_segments) do |element|
|
|
373
384
|
case discriminator[:type]
|
|
374
385
|
when 'patternCodeableConcept'
|
|
375
386
|
find_pattern_codeable_concept_slice(element, discriminator)
|
|
@@ -24,16 +24,49 @@ module Inferno
|
|
|
24
24
|
self.requirement_extension_url = requirement_extension_url
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# @return [String] the canonical URL of the profile
|
|
28
|
+
def profile_url
|
|
29
|
+
profile.url
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [String] the human-readable name of the profile, eg for display purposes.
|
|
33
|
+
# Collapses any runs of repeated whitespace, since profile titles occasionally have them.
|
|
34
|
+
def profile_name
|
|
35
|
+
profile.title&.squeeze(' ')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [String] the version of the profile itself (distinct from the IG's own version)
|
|
39
|
+
def profile_version
|
|
40
|
+
profile.version
|
|
41
|
+
end
|
|
42
|
+
|
|
27
43
|
# Retrieval method for the must support metadata
|
|
28
44
|
# @return [Hash]
|
|
29
45
|
def must_supports
|
|
30
46
|
@must_supports ||= {
|
|
31
47
|
extensions: must_support_extensions,
|
|
32
48
|
slices: must_support_slices,
|
|
33
|
-
elements: must_support_elements
|
|
49
|
+
elements: must_support_elements,
|
|
50
|
+
recursive_elements: recursive_element_segments
|
|
34
51
|
}
|
|
35
52
|
end
|
|
36
53
|
|
|
54
|
+
# Names of path segments that are self-referential, ie, they may repeat at
|
|
55
|
+
# arbitrary depth. FHIR marks this with a `contentReference` back to an
|
|
56
|
+
# ancestor element instead of re-declaring the element's children, eg,
|
|
57
|
+
# Questionnaire.item.item has a contentReference of "#Questionnaire.item".
|
|
58
|
+
# A MustSupport flag on an element under such a segment (eg Questionnaire.item.text)
|
|
59
|
+
# should be considered met if it's populated at any depth of nesting
|
|
60
|
+
# (item.text, item.item.text, item.item.item.text, ...), not just the literal depth
|
|
61
|
+
# at which the flag is declared. NOTE: does not currently handle sliced recursive elements.
|
|
62
|
+
# @return [Array<String>]
|
|
63
|
+
def recursive_element_segments
|
|
64
|
+
profile_elements
|
|
65
|
+
.select { |element| element.contentReference.present? }
|
|
66
|
+
.map { |element| element.id.split('.').last }
|
|
67
|
+
.uniq
|
|
68
|
+
end
|
|
69
|
+
|
|
37
70
|
def by_requirement_extension_only?(element)
|
|
38
71
|
requirement_extension_url && !element.mustSupport &&
|
|
39
72
|
element.extension.any? do |extension|
|
|
@@ -53,6 +86,7 @@ module Inferno
|
|
|
53
86
|
must_support_extension_elements.map do |element|
|
|
54
87
|
{
|
|
55
88
|
id: element.id,
|
|
89
|
+
slice_name: extension_slice_name(element.id),
|
|
56
90
|
path: element.path.gsub("#{resource}.", ''),
|
|
57
91
|
url: canonical_url_without_version(element.type.first.profile.first),
|
|
58
92
|
modifier_extension: element.path.end_with?('modifierExtension')
|
|
@@ -62,6 +96,15 @@ module Inferno
|
|
|
62
96
|
end
|
|
63
97
|
end
|
|
64
98
|
|
|
99
|
+
# The name of the deepest slice in a FHIR ElementDefinition id, eg "us-core-race" for
|
|
100
|
+
# "Patient.extension:us-core-race" or "ombCategory" for
|
|
101
|
+
# "Patient.extension:us-core-race.extension:ombCategory".
|
|
102
|
+
# @param element_id [String]
|
|
103
|
+
# @return [String, nil]
|
|
104
|
+
def extension_slice_name(element_id)
|
|
105
|
+
element_id.split('.').reverse.find { |segment| segment.include?(':') }&.split(':', 2)&.last
|
|
106
|
+
end
|
|
107
|
+
|
|
65
108
|
def canonical_url_without_version(url)
|
|
66
109
|
url&.split('|')&.first
|
|
67
110
|
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module Inferno
|
|
2
|
+
module DSL
|
|
3
|
+
# A ProfileMetadata object is a small, inheritable data holder for generated metadata about a
|
|
4
|
+
# profile or a group of tests -- most notably the Must Support metadata consumed by
|
|
5
|
+
# {MustSupportAssessment#missing_must_support_elements} and
|
|
6
|
+
# {Assertions#assert_must_support_elements_present} via their `metadata:` argument, alongside
|
|
7
|
+
# whatever other generated metadata a test kit needs to carry around (searches, bindings,
|
|
8
|
+
# references, etc).
|
|
9
|
+
#
|
|
10
|
+
# It exists so that test kits don't need to reach for `OpenStruct` (which accepts any attribute
|
|
11
|
+
# silently, making typos and generator/consumer drift hard to catch) or hand-roll their own
|
|
12
|
+
# version of this exact pattern.
|
|
13
|
+
#
|
|
14
|
+
# Test kits define a subclass and declare whatever attributes they need with `.attribute`.
|
|
15
|
+
# `must_supports` is always available, since it's the attribute Inferno's Must Support logic
|
|
16
|
+
# reads:
|
|
17
|
+
#
|
|
18
|
+
# class GroupMetadata < Inferno::DSL::ProfileMetadata
|
|
19
|
+
# attribute :searches
|
|
20
|
+
# attribute :bindings
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# metadata = GroupMetadata.from_file('path/to/generated/metadata.yml')
|
|
24
|
+
# metadata.must_supports #=> { elements: [...], extensions: [...], slices: [...] }
|
|
25
|
+
#
|
|
26
|
+
# @see MustSupportAssessment#missing_must_support_elements
|
|
27
|
+
class ProfileMetadata
|
|
28
|
+
class << self
|
|
29
|
+
# The full list of attribute names declared on this class and its ancestors.
|
|
30
|
+
# @return [Array<Symbol>]
|
|
31
|
+
def attribute_names
|
|
32
|
+
@attribute_names ||= superclass.respond_to?(:attribute_names) ? superclass.attribute_names.dup : []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Declare an attribute that instances of this class (and subclasses) may be initialized with.
|
|
36
|
+
# @param name [Symbol]
|
|
37
|
+
# @return [void]
|
|
38
|
+
def attribute(name)
|
|
39
|
+
name = name.to_sym
|
|
40
|
+
attribute_names << name unless attribute_names.include?(name)
|
|
41
|
+
attr_accessor name
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Build an instance from a YAML file, eg one written by a metadata generator.
|
|
45
|
+
# @param path [String, Pathname]
|
|
46
|
+
# @return [ProfileMetadata]
|
|
47
|
+
def from_file(path)
|
|
48
|
+
new(YAML.load_file(path, aliases: true))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Profile identity, mirroring fields read directly off the FHIR StructureDefinition
|
|
53
|
+
# (`resource` is the resource type the profile constrains, ie `profile.type`).
|
|
54
|
+
attribute :resource
|
|
55
|
+
attribute :profile_url
|
|
56
|
+
attribute :profile_name
|
|
57
|
+
attribute :profile_version
|
|
58
|
+
|
|
59
|
+
attribute :must_supports
|
|
60
|
+
|
|
61
|
+
# @param metadata [Hash] a hash of attribute name/value pairs. Keys not declared with
|
|
62
|
+
# `.attribute` raise an error, so that a mismatch between a metadata generator and this
|
|
63
|
+
# class's declared attributes is caught immediately rather than silently ignored.
|
|
64
|
+
def initialize(metadata = {})
|
|
65
|
+
metadata.each do |key, value|
|
|
66
|
+
key = key.to_sym
|
|
67
|
+
raise "Unknown #{self.class} attribute: #{key}" unless self.class.attribute_names.include?(key)
|
|
68
|
+
|
|
69
|
+
public_send(:"#{key}=", value)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
self.must_supports ||= {}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Hash] the declared attributes and their current values, omitting unset (nil) ones
|
|
76
|
+
def to_hash
|
|
77
|
+
self.class.attribute_names.each_with_object({}) do |name, hash|
|
|
78
|
+
value = public_send(name)
|
|
79
|
+
hash[name] = value unless value.nil?
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Every Must Support element, slice, and extension in `must_supports`, each represented as a
|
|
84
|
+
# single string and sorted alphabetically. Unlike the list returned by a missing-elements
|
|
85
|
+
# check, this includes everything that's expected to be supported, not just what's absent
|
|
86
|
+
# from a particular set of resources.
|
|
87
|
+
#
|
|
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"`).
|
|
92
|
+
# @return [Array<String>]
|
|
93
|
+
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]) }
|
|
102
|
+
|
|
103
|
+
(element_strings + slice_strings + extension_strings).sort
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def must_support_string(path, suffix)
|
|
109
|
+
suffix.present? ? "#{path}:#{suffix}" : path
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -84,7 +84,10 @@ module Inferno
|
|
|
84
84
|
|
|
85
85
|
test_run = find_test_run(test_run_identifier)
|
|
86
86
|
|
|
87
|
-
|
|
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
|
|
88
91
|
|
|
89
92
|
test_runs_repo.mark_as_no_longer_waiting(test_run.id)
|
|
90
93
|
|