inferno_core 1.4.2 → 1.4.4
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/main.rb +46 -5
- data/lib/inferno/apps/cli/requirements_coverage_checker.rb +1 -1
- data/lib/inferno/apps/cli/templates/Rakefile.tt +0 -13
- data/lib/inferno/apps/cli/templates/config/nginx.background.conf.tt +0 -30
- data/lib/inferno/apps/cli/templates/config/nginx.conf.tt +0 -30
- data/lib/inferno/apps/cli/templates/docker-compose.background.yml.tt +1 -14
- data/lib/inferno/apps/cli/templates/docker-compose.yml.tt +1 -10
- data/lib/inferno/apps/cli/templates/execution_scripts/{README.md.tt → README.md} +3 -3
- data/lib/inferno/apps/cli/templates/lib/%library_name%/requirements/%library_name%_requirements.csv +3 -0
- data/lib/inferno/apps/cli/templates/lib/%library_name%/requirements/Inferno Requirements Template.xlsx +0 -0
- data/lib/inferno/apps/cli/templates/lib/%library_name%/requirements/README.md +12 -0
- data/lib/inferno/apps/cli/templates/lib/%library_name%/requirements/generated/%test_suite_id%_requirements_coverage.csv.tt +1 -0
- data/lib/inferno/dsl/fhir_resource_navigation.rb +12 -2
- data/lib/inferno/dsl/fhir_resource_validation.rb +186 -4
- 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/utils/execution_script_runner.rb +56 -7
- data/lib/inferno/version.rb +1 -1
- data/spec/spec_helper.rb +18 -0
- metadata +7 -4
- data/lib/inferno/apps/cli/templates/lib/%library_name%/suite.rb.tt +0 -59
|
@@ -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
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'hanami/controller'
|
|
2
2
|
require 'rack/request'
|
|
3
|
+
require 'fhir_models'
|
|
3
4
|
require_relative '../ext/rack'
|
|
4
5
|
|
|
5
6
|
module Inferno
|
|
@@ -15,6 +16,8 @@ module Inferno
|
|
|
15
16
|
# request.headers['authorization']&.delete_prefix('Bearer ')
|
|
16
17
|
# end
|
|
17
18
|
#
|
|
19
|
+
# error_response_format :operation_outcome
|
|
20
|
+
#
|
|
18
21
|
# # Return a json FHIR Patient resource
|
|
19
22
|
# def make_response
|
|
20
23
|
# response.status = 200
|
|
@@ -57,9 +60,47 @@ module Inferno
|
|
|
57
60
|
# end
|
|
58
61
|
# end
|
|
59
62
|
# end
|
|
63
|
+
#
|
|
60
64
|
class SuiteEndpoint < Hanami::Action
|
|
61
65
|
attr_reader :req, :res
|
|
62
66
|
|
|
67
|
+
# The built-in options for `error_response_format`
|
|
68
|
+
ERROR_RESPONSE_FORMATS = [:text, :operation_outcome].freeze
|
|
69
|
+
|
|
70
|
+
class << self
|
|
71
|
+
# Select one of Inferno's standard response formats to be returned
|
|
72
|
+
# whenever Inferno has to render an error response of its own due
|
|
73
|
+
# to problems finding the target session or an unhandled exception.
|
|
74
|
+
# You can override {#no_session_response} to customize the response
|
|
75
|
+
# in the no-session case.
|
|
76
|
+
#
|
|
77
|
+
# * `:text` (default): a `500` response with a plain text message
|
|
78
|
+
# * `:operation_outcome`: a `500` response with a FHIR
|
|
79
|
+
# `OperationOutcome` serialized as `application/fhir+json`
|
|
80
|
+
#
|
|
81
|
+
# @param format [Symbol] `:text` or `:operation_outcome`
|
|
82
|
+
# @return [void]
|
|
83
|
+
#
|
|
84
|
+
# @example
|
|
85
|
+
# class MyEndpoint < Inferno::DSL::SuiteEndpoint
|
|
86
|
+
# error_response_format :operation_outcome
|
|
87
|
+
# end
|
|
88
|
+
def error_response_format(format)
|
|
89
|
+
unless ERROR_RESPONSE_FORMATS.include?(format)
|
|
90
|
+
raise ArgumentError,
|
|
91
|
+
"Unknown error_response_format `#{format.inspect}`. " \
|
|
92
|
+
"Must be one of #{ERROR_RESPONSE_FORMATS.join(', ')}."
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
@error_response_format_value = format
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @private
|
|
99
|
+
def error_response_format_value
|
|
100
|
+
@error_response_format_value ||= :text
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
63
104
|
# @!group Overrides These methods should be overridden by subclasses to
|
|
64
105
|
# define the behavior of the endpoint
|
|
65
106
|
|
|
@@ -78,6 +119,22 @@ module Inferno
|
|
|
78
119
|
nil
|
|
79
120
|
end
|
|
80
121
|
|
|
122
|
+
# Override this method to provide a short narrative description of
|
|
123
|
+
# where the test run identifier is expected to be found in an
|
|
124
|
+
# incoming request. When provided, this description is appended to
|
|
125
|
+
# the {#no_session_message} to help implementers debug requests that
|
|
126
|
+
# don't match a waiting test run.
|
|
127
|
+
#
|
|
128
|
+
# @return [String]
|
|
129
|
+
#
|
|
130
|
+
# @example
|
|
131
|
+
# def test_run_identifier_location_description
|
|
132
|
+
# "the 'code' query parameter"
|
|
133
|
+
# end
|
|
134
|
+
def test_run_identifier_location_description
|
|
135
|
+
''
|
|
136
|
+
end
|
|
137
|
+
|
|
81
138
|
# Override this method to build the response.
|
|
82
139
|
#
|
|
83
140
|
# @return [Void]
|
|
@@ -128,6 +185,26 @@ module Inferno
|
|
|
128
185
|
true
|
|
129
186
|
end
|
|
130
187
|
|
|
188
|
+
# Override this method to fully customize the response returned when no
|
|
189
|
+
# waiting test run/session can be found for the incoming request. Set
|
|
190
|
+
# `response.status` and `response.body` (and `response.content_type`, if
|
|
191
|
+
# needed) — Inferno halts the request with those values. By default,
|
|
192
|
+
# this renders one of Inferno's standard responses based on the format
|
|
193
|
+
# selected with `error_response_format` (a plain text `500` response if
|
|
194
|
+
# none was selected).
|
|
195
|
+
#
|
|
196
|
+
# @return [Void]
|
|
197
|
+
#
|
|
198
|
+
# @example
|
|
199
|
+
# def no_session_response
|
|
200
|
+
# response.status = 404
|
|
201
|
+
# response.format = :json
|
|
202
|
+
# response.body = { error: 'no matching session' }.to_json
|
|
203
|
+
# end
|
|
204
|
+
def no_session_response
|
|
205
|
+
error_response(no_session_message, code: 'not-found')
|
|
206
|
+
end
|
|
207
|
+
|
|
131
208
|
# @!endgroup
|
|
132
209
|
|
|
133
210
|
# @private
|
|
@@ -192,7 +269,7 @@ module Inferno
|
|
|
192
269
|
def test_run
|
|
193
270
|
@test_run ||=
|
|
194
271
|
test_runs_repo.find_latest_waiting_by_identifier(find_test_run_identifier).tap do |test_run|
|
|
195
|
-
|
|
272
|
+
render_error_and_halt { no_session_response } if test_run.nil?
|
|
196
273
|
end
|
|
197
274
|
end
|
|
198
275
|
|
|
@@ -216,11 +293,86 @@ module Inferno
|
|
|
216
293
|
@logger ||= Application['logger']
|
|
217
294
|
end
|
|
218
295
|
|
|
296
|
+
# @private
|
|
297
|
+
def log_error(error, url: request.url)
|
|
298
|
+
session_prefix = @test_run ? " session=#{@test_run.test_session_id}" : ''
|
|
299
|
+
logger.error("[#{url}]#{session_prefix} #{error.full_message}")
|
|
300
|
+
end
|
|
301
|
+
|
|
219
302
|
# @private
|
|
220
303
|
def find_test_run_identifier
|
|
221
|
-
@test_run_identifier
|
|
304
|
+
return @test_run_identifier if defined?(@test_run_identifier) # handle memoization in the nil case
|
|
305
|
+
|
|
306
|
+
@test_run_identifier = test_run_identifier
|
|
222
307
|
rescue StandardError => e
|
|
223
|
-
|
|
308
|
+
log_error(e)
|
|
309
|
+
render_error_and_halt do
|
|
310
|
+
error_response(
|
|
311
|
+
'An error occurred while determining the test run identifier for this request.',
|
|
312
|
+
code: 'exception',
|
|
313
|
+
diagnostics: e.full_message
|
|
314
|
+
)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# @private
|
|
319
|
+
def no_session_message
|
|
320
|
+
base_message = "Unable to find test run for request to '#{request.url}'"
|
|
321
|
+
location = test_run_identifier_location_description
|
|
322
|
+
identifier = find_test_run_identifier
|
|
323
|
+
|
|
324
|
+
if identifier.blank?
|
|
325
|
+
detail = location.present? ? " in #{location}" : ''
|
|
326
|
+
"#{base_message}: no identifier found#{detail}."
|
|
327
|
+
else
|
|
328
|
+
detail = location.present? ? ", found in #{location}," : ''
|
|
329
|
+
"#{base_message}: identifier '#{identifier}'#{detail} is not associated with a waiting session."
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# @private
|
|
334
|
+
# Yields to build the response, then halts with whatever ended up in
|
|
335
|
+
# `response.status`/`response.body`. Centralizing the halt here means
|
|
336
|
+
# overrides of the response-building hooks (e.g. #no_session_response)
|
|
337
|
+
# never need to remember to call `halt` themselves.
|
|
338
|
+
def render_error_and_halt
|
|
339
|
+
yield
|
|
340
|
+
halt response.status, response.body.join
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# @private
|
|
344
|
+
# `message` is a short, human-readable summary (goes in the
|
|
345
|
+
# OperationOutcome issue's `details.text`, or stands alone as the whole
|
|
346
|
+
# plain text body). `diagnostics`, if given, is technical detail — e.g.
|
|
347
|
+
# an exception's full backtrace — that goes in the issue's
|
|
348
|
+
# `diagnostics` element, or is appended to the plain text body.
|
|
349
|
+
def error_response(message, code:, diagnostics: nil)
|
|
350
|
+
case self.class.error_response_format_value
|
|
351
|
+
when :operation_outcome
|
|
352
|
+
operation_outcome_error_response(message, code:, diagnostics:)
|
|
353
|
+
else
|
|
354
|
+
text_error_response(message, diagnostics:)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# @private
|
|
359
|
+
def text_error_response(message, diagnostics: nil)
|
|
360
|
+
response.status = 500
|
|
361
|
+
response.body = diagnostics ? "#{message}\n#{diagnostics}" : message
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# @private
|
|
365
|
+
def operation_outcome_error_response(message, code:, diagnostics: nil)
|
|
366
|
+
issue = FHIR::OperationOutcome::Issue.new(
|
|
367
|
+
severity: 'fatal',
|
|
368
|
+
code:,
|
|
369
|
+
details: FHIR::CodeableConcept.new(text: message)
|
|
370
|
+
)
|
|
371
|
+
issue.diagnostics = diagnostics if diagnostics
|
|
372
|
+
|
|
373
|
+
response.status = 500
|
|
374
|
+
response.content_type = 'application/fhir+json'
|
|
375
|
+
response.body = FHIR::OperationOutcome.new(issue: [issue]).to_json
|
|
224
376
|
end
|
|
225
377
|
|
|
226
378
|
# @private
|
|
@@ -272,12 +424,18 @@ module Inferno
|
|
|
272
424
|
|
|
273
425
|
make_response
|
|
274
426
|
rescue StandardError => e
|
|
275
|
-
|
|
427
|
+
log_error(e)
|
|
428
|
+
render_error_and_halt do
|
|
429
|
+
error_response(
|
|
430
|
+
'An error occurred while processing this request.',
|
|
431
|
+
code: 'exception',
|
|
432
|
+
diagnostics: e.full_message
|
|
433
|
+
)
|
|
434
|
+
end
|
|
276
435
|
end
|
|
277
436
|
|
|
278
437
|
# @private
|
|
279
438
|
def add_persistence_callback # rubocop:disable Metrics/CyclomaticComplexity
|
|
280
|
-
logger = Application['logger']
|
|
281
439
|
env = req.env
|
|
282
440
|
env['rack.after_reply'] ||= []
|
|
283
441
|
env['rack.after_reply'] << proc do
|
|
@@ -332,7 +490,7 @@ module Inferno
|
|
|
332
490
|
)
|
|
333
491
|
end
|
|
334
492
|
rescue StandardError => e
|
|
335
|
-
|
|
493
|
+
log_error(e, url:)
|
|
336
494
|
end
|
|
337
495
|
end
|
|
338
496
|
end
|
data/lib/inferno/dsl.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative 'dsl/fhir_resource_navigation'
|
|
|
7
7
|
require_relative 'dsl/fhir_resource_validation'
|
|
8
8
|
require_relative 'dsl/fhirpath_evaluation'
|
|
9
9
|
require_relative 'dsl/http_client'
|
|
10
|
+
require_relative 'dsl/profile_metadata'
|
|
10
11
|
require_relative 'dsl/must_support_assessment'
|
|
11
12
|
require_relative 'dsl/results'
|
|
12
13
|
require_relative 'dsl/runnable'
|
data/lib/inferno/entities/ig.rb
CHANGED
|
@@ -27,7 +27,14 @@ module Inferno
|
|
|
27
27
|
@examples = []
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
# @param ig_path [String] path to either a `.tgz` IG package or a directory containing an
|
|
31
|
+
# unpacked one
|
|
32
|
+
# @param standalone_resources_directory [String] optional path to a directory of loose
|
|
33
|
+
# `*.json` FHIR resource files to merge in alongside the IG, eg author-maintained
|
|
34
|
+
# resources (extra examples under 'package/example', local overrides) that aren't
|
|
35
|
+
# part of the IG package itself. Primarily useful for test kit generator scripts.
|
|
36
|
+
# @return [IG]
|
|
37
|
+
def self.from_file(ig_path, standalone_resources_directory: nil)
|
|
31
38
|
raise "#{ig_path} does not exist" unless File.exist?(ig_path)
|
|
32
39
|
|
|
33
40
|
# fhir_models by default logs the entire content of non-FHIR files
|
|
@@ -35,13 +42,18 @@ module Inferno
|
|
|
35
42
|
original_logger = FHIR.logger
|
|
36
43
|
FHIR.logger = Logger.new('/dev/null')
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
ig =
|
|
46
|
+
if File.directory?(ig_path)
|
|
47
|
+
from_directory(ig_path)
|
|
48
|
+
elsif ig_path.end_with? '.tgz'
|
|
49
|
+
from_tgz(ig_path)
|
|
50
|
+
else
|
|
51
|
+
raise "Unable to load #{ig_path} as it does not appear to be a directory or a .tgz file"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
ig.merge_standalone_resources(standalone_resources_directory) if standalone_resources_directory
|
|
55
|
+
|
|
56
|
+
ig
|
|
45
57
|
ensure
|
|
46
58
|
FHIR.logger = original_logger if defined? original_logger
|
|
47
59
|
end
|
|
@@ -122,6 +134,37 @@ module Inferno
|
|
|
122
134
|
end
|
|
123
135
|
end
|
|
124
136
|
|
|
137
|
+
# Merge in loose FHIR resource files from a directory that isn't part of the IG package
|
|
138
|
+
# itself, eg author-maintained resources (extra examples, local SearchParameter overrides,
|
|
139
|
+
# etc) that a test kit generator keeps alongside a downloaded package. Files that aren't
|
|
140
|
+
# valid FHIR resources are skipped. Does nothing if the directory doesn't exist.
|
|
141
|
+
#
|
|
142
|
+
# Files are classified the same way as within an IG package: those under a `package/example`
|
|
143
|
+
# subdirectory are added to {#examples}, everything else (including files directly in
|
|
144
|
+
# `directory`) is added to {#resources_by_type}.
|
|
145
|
+
# @param directory [String] path to a directory of loose `*.json` FHIR resource files,
|
|
146
|
+
# optionally nested under a `package/example` subdirectory
|
|
147
|
+
# @return [self]
|
|
148
|
+
def merge_standalone_resources(directory)
|
|
149
|
+
return self unless File.directory?(directory)
|
|
150
|
+
|
|
151
|
+
base_path = Pathname.new(directory)
|
|
152
|
+
Dir.glob(File.join(directory, '**', '*.json')).each do |file_path|
|
|
153
|
+
relative_path = Pathname.new(file_path).relative_path_from(base_path).to_s
|
|
154
|
+
|
|
155
|
+
begin
|
|
156
|
+
resource = FHIR::Json.from_json(File.read(file_path))
|
|
157
|
+
next if resource.nil?
|
|
158
|
+
rescue StandardError
|
|
159
|
+
next
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
handle_resource(resource, relative_path)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
self
|
|
166
|
+
end
|
|
167
|
+
|
|
125
168
|
def self.extract_package_id(ig_resource)
|
|
126
169
|
"#{ig_resource.id}##{ig_resource.version || 'current'}"
|
|
127
170
|
end
|