inferno_core 1.4.1 → 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 +27 -5
- data/lib/inferno/apps/cli/requirements_coverage_checker.rb +1 -1
- data/lib/inferno/apps/cli/session/session_compare.rb +27 -3
- data/lib/inferno/apps/cli/session/session_status.rb +20 -4
- data/lib/inferno/apps/web/controllers/test_runs/create.rb +8 -1
- data/lib/inferno/apps/web/controllers/test_runs/destroy.rb +10 -1
- data/lib/inferno/config/boot/db.rb +45 -0
- data/lib/inferno/config/boot/executor.rb +4 -0
- data/lib/inferno/config/boot/web.rb +4 -0
- data/lib/inferno/dsl/fhir_resource_navigation.rb +12 -2
- data/lib/inferno/dsl/fhir_resource_validation.rb +96 -0
- data/lib/inferno/dsl/input_output_handling.rb +7 -5
- 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 +14 -2
- data/lib/inferno/dsl/suite_endpoint.rb +176 -7
- data/lib/inferno/dsl.rb +1 -0
- data/lib/inferno/entities/ig.rb +51 -8
- data/lib/inferno/jobs.rb +5 -1
- data/lib/inferno/public/bundle.js +34 -34
- data/lib/inferno/public/bundle.js.LICENSE.txt +0 -2
- data/lib/inferno/test_runner.rb +61 -28
- data/lib/inferno/utils/db_concurrency_check.rb +166 -0
- data/lib/inferno/version.rb +1 -1
- metadata +4 -2
|
@@ -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
|
|
|
@@ -94,7 +97,16 @@ module Inferno
|
|
|
94
97
|
update_result(waiting_result, result_message)
|
|
95
98
|
persist_request(request, test_run, waiting_result, test)
|
|
96
99
|
|
|
97
|
-
Jobs.perform(
|
|
100
|
+
Jobs.perform(
|
|
101
|
+
Jobs::ResumeTestRun,
|
|
102
|
+
test_run.id,
|
|
103
|
+
tags: [
|
|
104
|
+
'source:route',
|
|
105
|
+
"session:#{test_run.test_session_id}",
|
|
106
|
+
"run:#{test_run.test_suite_id || test_run.test_group_id || test_run.test_id}",
|
|
107
|
+
"test:#{test.id}"
|
|
108
|
+
]
|
|
109
|
+
)
|
|
98
110
|
|
|
99
111
|
res.redirect_to redirect_route(test_run, test)
|
|
100
112
|
end
|
|
@@ -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
|
|
@@ -254,6 +406,8 @@ module Inferno
|
|
|
254
406
|
def resume
|
|
255
407
|
req.env['inferno.resume_test_run'] = true
|
|
256
408
|
req.env['inferno.test_run_id'] = test_run.id
|
|
409
|
+
req.env['inferno.run_identifier'] = test_run.test_suite_id || test_run.test_group_id || test_run.test_id
|
|
410
|
+
req.env['inferno.waiting_test_id'] = test.id
|
|
257
411
|
end
|
|
258
412
|
|
|
259
413
|
# @private
|
|
@@ -270,12 +424,18 @@ module Inferno
|
|
|
270
424
|
|
|
271
425
|
make_response
|
|
272
426
|
rescue StandardError => e
|
|
273
|
-
|
|
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
|
|
274
435
|
end
|
|
275
436
|
|
|
276
437
|
# @private
|
|
277
438
|
def add_persistence_callback # rubocop:disable Metrics/CyclomaticComplexity
|
|
278
|
-
logger = Application['logger']
|
|
279
439
|
env = req.env
|
|
280
440
|
env['rack.after_reply'] ||= []
|
|
281
441
|
env['rack.after_reply'] << proc do
|
|
@@ -318,10 +478,19 @@ module Inferno
|
|
|
318
478
|
test_run_id = env['inferno.test_run_id']
|
|
319
479
|
Inferno::Repositories::TestRuns.new.mark_as_no_longer_waiting(test_run_id)
|
|
320
480
|
|
|
321
|
-
Inferno::Jobs.perform(
|
|
481
|
+
Inferno::Jobs.perform(
|
|
482
|
+
Jobs::ResumeTestRun,
|
|
483
|
+
test_run_id,
|
|
484
|
+
tags: [
|
|
485
|
+
'source:suite_endpoint',
|
|
486
|
+
"session:#{env['inferno.test_session_id']}",
|
|
487
|
+
"run:#{env['inferno.run_identifier']}",
|
|
488
|
+
"test:#{env['inferno.waiting_test_id']}"
|
|
489
|
+
]
|
|
490
|
+
)
|
|
322
491
|
end
|
|
323
492
|
rescue StandardError => e
|
|
324
|
-
|
|
493
|
+
log_error(e, url:)
|
|
325
494
|
end
|
|
326
495
|
end
|
|
327
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
|
data/lib/inferno/jobs.rb
CHANGED
|
@@ -6,9 +6,13 @@ require_relative 'jobs/invoke_validator_session'
|
|
|
6
6
|
|
|
7
7
|
module Inferno
|
|
8
8
|
module Jobs
|
|
9
|
-
def self.perform(job_klass, *params, force_synchronous: false)
|
|
9
|
+
def self.perform(job_klass, *params, force_synchronous: false, tags: nil)
|
|
10
|
+
tags = Array(tags).compact
|
|
11
|
+
|
|
10
12
|
if force_synchronous || (Application['async_jobs'] == false)
|
|
11
13
|
job_klass.new.perform(*params)
|
|
14
|
+
elsif tags.any?
|
|
15
|
+
job_klass.set(tags:).perform_async(*params)
|
|
12
16
|
else
|
|
13
17
|
job_klass.perform_async(*params)
|
|
14
18
|
end
|