inferno_core 0.6.7 → 0.6.9
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/evaluate.rb +6 -1
- data/lib/inferno/apps/cli/templates/lib/%library_name%/suite.rb.tt +2 -2
- data/lib/inferno/apps/web/controllers/requirements/show.rb +18 -0
- data/lib/inferno/apps/web/controllers/test_suites/requirements/index.rb +29 -0
- data/lib/inferno/apps/web/router.rb +7 -0
- data/lib/inferno/apps/web/serializers/requirement.rb +18 -0
- data/lib/inferno/apps/web/serializers/requirement_set.rb +13 -0
- data/lib/inferno/apps/web/serializers/test_suite.rb +10 -0
- data/lib/inferno/config/boot/requirements.rb +40 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_defined_extensions_have_examples.rb +58 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_extensions_used.rb +76 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_profiles_have_examples.rb +49 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_search_parameters_have_examples.rb +79 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/differential_content_has_examples.rb +124 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/value_sets_demonstrate.rb +233 -0
- data/lib/inferno/dsl/fhir_resource_navigation.rb +11 -2
- data/lib/inferno/dsl/must_support_assessment.rb +15 -3
- data/lib/inferno/dsl/requirement_set.rb +82 -0
- data/lib/inferno/dsl/runnable.rb +22 -0
- data/lib/inferno/dsl/suite_requirements.rb +46 -0
- data/lib/inferno/entities/ig.rb +4 -0
- data/lib/inferno/entities/requirement.rb +63 -0
- data/lib/inferno/entities/test_suite.rb +2 -0
- data/lib/inferno/public/bundle.js +3 -3
- data/lib/inferno/repositories/igs.rb +1 -2
- data/lib/inferno/repositories/requirements.rb +116 -0
- data/lib/inferno/version.rb +1 -1
- metadata +17 -2
@@ -0,0 +1,233 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
require 'uri'
|
6
|
+
require 'base64'
|
7
|
+
|
8
|
+
module Inferno
|
9
|
+
module DSL
|
10
|
+
module FHIREvaluation
|
11
|
+
module Rules
|
12
|
+
# This rule evaluates if an IG defines a new valueset, the examples should
|
13
|
+
# demonstrate reasonable coverage of that valueset.
|
14
|
+
# Note this probably only makes sense for small valuesets
|
15
|
+
# such as status options, not something like disease codes from SNOMED.
|
16
|
+
|
17
|
+
# Algorithm:
|
18
|
+
# 1. Extract pairs of system and code from include in value sets in IG
|
19
|
+
# 2. If valueSet exists in include, retrieve the value sets from UMLS.
|
20
|
+
# Extract pairs of system and code from the result.
|
21
|
+
# 3. For each pair of system and code, check if any resources in the IG have instance of them.
|
22
|
+
# 4. Count total number of existences.
|
23
|
+
|
24
|
+
class ValueSetsDemonstrate < Rule
|
25
|
+
attr_accessor :config, :value_set_unevaluated, :value_set_used, :value_set_unused
|
26
|
+
|
27
|
+
def check(context)
|
28
|
+
@config = context.config
|
29
|
+
@value_set_used = []
|
30
|
+
@value_set_unused = []
|
31
|
+
@value_set_unevaluated = []
|
32
|
+
|
33
|
+
classify_valuesets(context)
|
34
|
+
|
35
|
+
context.add_result create_result_message
|
36
|
+
end
|
37
|
+
|
38
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
39
|
+
def classify_valuesets(context)
|
40
|
+
context.ig.value_sets.each do |valueset|
|
41
|
+
valueset_used_count = 0
|
42
|
+
system_codes = extract_systems_codes_from_valueset(valueset)
|
43
|
+
|
44
|
+
value_set_unevaluated << "#{valueset.url}: unable to find system and code" if system_codes.none?
|
45
|
+
value_set_unevaluated.uniq!
|
46
|
+
|
47
|
+
next if value_set_unevaluated.any? { |element| element.include?(valueset.url) }
|
48
|
+
|
49
|
+
resource_used = []
|
50
|
+
|
51
|
+
context.data.each do |resource|
|
52
|
+
system_codes.each do |system_code|
|
53
|
+
next unless !system_code.nil? && resource_uses_code(resource.to_hash, system_code[:system],
|
54
|
+
system_code[:code])
|
55
|
+
|
56
|
+
valueset_used_count += 1
|
57
|
+
resource_used << resource.id unless resource_used.include?(resource.id)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
if valueset_used_count.positive?
|
62
|
+
# rubocop:disable Layout/LineLength
|
63
|
+
value_set_used << "#{valueset.url} is used #{valueset_used_count} times in #{resource_used.count} resources"
|
64
|
+
# rubocop:enable Layout/LineLength
|
65
|
+
else
|
66
|
+
value_set_unused << valueset.url
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
71
|
+
|
72
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
73
|
+
def create_result_message
|
74
|
+
if value_set_unused.none?
|
75
|
+
message = 'All Value sets are used in Examples:'
|
76
|
+
value_set_used.map { |value_set| message += "\n\t#{value_set}" }
|
77
|
+
|
78
|
+
if value_set_unevaluated.any?
|
79
|
+
message += "\nThe following Value Sets were not able to be evaluated: "
|
80
|
+
value_set_unevaluated.map { |value_set| message += "\n\t#{value_set}" }
|
81
|
+
end
|
82
|
+
|
83
|
+
EvaluationResult.new(message, severity: 'success', rule: self)
|
84
|
+
else
|
85
|
+
message = 'Value sets with all codes used at least once in Examples:'
|
86
|
+
value_set_used.map { |url| message += "\n\t#{url}" }
|
87
|
+
|
88
|
+
message += "\nFound unused Value Sets: "
|
89
|
+
value_set_unused.map { |url| message += "\n\t#{url}" }
|
90
|
+
|
91
|
+
if value_set_unevaluated.any?
|
92
|
+
message += "\nFound unevaluated Value Sets: "
|
93
|
+
value_set_unevaluated.map { |url| message += "\n\t#{url}" }
|
94
|
+
end
|
95
|
+
|
96
|
+
EvaluationResult.new(message, rule: self)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
100
|
+
|
101
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
102
|
+
def extract_systems_codes_from_valueset(valueset)
|
103
|
+
system_codes = []
|
104
|
+
|
105
|
+
if valueset.to_hash['compose']
|
106
|
+
valueset.to_hash['compose']['include'].each do |include|
|
107
|
+
if include['valueSet']
|
108
|
+
include['valueSet'].each do |url|
|
109
|
+
retrieve_valueset_from_api(url)&.each { |system_code| system_codes << system_code }
|
110
|
+
end
|
111
|
+
next
|
112
|
+
end
|
113
|
+
|
114
|
+
system_url = include['system']
|
115
|
+
|
116
|
+
if system_url && include['concept']
|
117
|
+
include['concept'].each do |code|
|
118
|
+
system_codes << { system: system_url, code: code.to_hash['code'] }
|
119
|
+
end
|
120
|
+
next
|
121
|
+
end
|
122
|
+
|
123
|
+
if system_url
|
124
|
+
if system_url['http://hl7.org/fhir']
|
125
|
+
retrieve_valueset_from_api(system_url)&.each { |vs| system_codes << vs }
|
126
|
+
end
|
127
|
+
next
|
128
|
+
end
|
129
|
+
|
130
|
+
value_set_unevaluated << "#{valueset.url}: system url not provided" unless system_url
|
131
|
+
|
132
|
+
# Exclude if system is provided as Uniform Resource Name "urn:"
|
133
|
+
# Exclude filter
|
134
|
+
# Exclude only system is provided (e.g. http://loing.org)
|
135
|
+
exclusions = config.data['Rule']['ValueSetsDemonstrate']['Exclude']
|
136
|
+
if exclusions['URL'] && (system_url['urn'])
|
137
|
+
value_set_unevaluated << "#{valueset.url}: unable to handle Uniform Resource Name"
|
138
|
+
end
|
139
|
+
|
140
|
+
if exclusions['Filter'] && (system_url && include['filter'])
|
141
|
+
value_set_unevaluated << "#{valueset.url}: unable to handle filter"
|
142
|
+
end
|
143
|
+
|
144
|
+
if exclusions['SystemOnly'] && (system_url && !include['concept'] && !include['filter'])
|
145
|
+
value_set_unevaluated << "#{valueset.url}: unabe to handle SystemOnly"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
else
|
149
|
+
value_set_unevaluated << valueset.url
|
150
|
+
end
|
151
|
+
system_codes.flatten.uniq
|
152
|
+
end
|
153
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
154
|
+
|
155
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
156
|
+
def resource_uses_code(resource, system, code)
|
157
|
+
resource.each do |key, value|
|
158
|
+
next unless key == 'code' || ['value', 'valueCodeableConcept', 'valueString',
|
159
|
+
'valueQuantity', 'valueBoolean',
|
160
|
+
'valueInteger', 'valueRange', 'valueRatio',
|
161
|
+
'valueSampleData', 'valueDateTime',
|
162
|
+
'valuePeriod', 'valueTime'].include?(key)
|
163
|
+
next unless value.is_a?(Hash)
|
164
|
+
|
165
|
+
value['coding']&.each do |codeset|
|
166
|
+
return true if codeset.to_hash['system'] == system && codeset.to_hash['code'] == code
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
false
|
171
|
+
end
|
172
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
173
|
+
|
174
|
+
def extract_valueset_from_response(response)
|
175
|
+
value_set = JSON.parse(response.body)
|
176
|
+
|
177
|
+
if value_set['compose'] && value_set['compose']['include']
|
178
|
+
value_set['compose']['include'].map do |include|
|
179
|
+
include['concept']&.map { |concept| { system: include['system'], code: concept['code'] } }
|
180
|
+
end.flatten
|
181
|
+
else
|
182
|
+
puts 'No Value Set found in the response.'
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def retrieve_valueset_from_api(url)
|
187
|
+
url['http:'] = 'https:' if url['http:']
|
188
|
+
uri = URI.parse(url)
|
189
|
+
|
190
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
191
|
+
http.use_ssl = (uri.scheme == 'https')
|
192
|
+
|
193
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
194
|
+
|
195
|
+
username = config.data['Environment']['VSAC']['Username']
|
196
|
+
password = config.data['Environment']['VSAC']['Password']
|
197
|
+
encoded_credentials = Base64.strict_encode64("#{username}:#{password}")
|
198
|
+
request['Authorization'] = "Basic #{encoded_credentials}"
|
199
|
+
|
200
|
+
response = http.request(request)
|
201
|
+
|
202
|
+
content_type = response['content-type']
|
203
|
+
return unless content_type && !content_type.include?('text/html')
|
204
|
+
|
205
|
+
while response.is_a?(Net::HTTPRedirection)
|
206
|
+
redirect_url = response['location']
|
207
|
+
|
208
|
+
redirect_url['xml'] = 'json'
|
209
|
+
uri = URI.parse(redirect_url)
|
210
|
+
|
211
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
212
|
+
http.use_ssl = (uri.scheme == 'https')
|
213
|
+
|
214
|
+
response = http.request(Net::HTTP::Get.new(uri.request_uri))
|
215
|
+
end
|
216
|
+
|
217
|
+
if response.code.to_i == 200
|
218
|
+
extract_valueset_from_response(response)
|
219
|
+
else
|
220
|
+
unless config.data['Rule']['ValueSetsDemonstrate']['IgnoreUnloadableValueset']
|
221
|
+
raise StandardError, "Failed to retrieve external value set: #{url} HTTP Status code: #{response.code}"
|
222
|
+
end
|
223
|
+
|
224
|
+
value_set_unevaluated << "#{url}: Failed to retrieve. HTTP Status code: #{response.code}"
|
225
|
+
nil
|
226
|
+
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
@@ -177,8 +177,17 @@ module Inferno
|
|
177
177
|
end
|
178
178
|
|
179
179
|
def matching_required_binding_slice?(slice, discriminator)
|
180
|
-
discriminator[:path].present? ? slice.send((discriminator[:path]).to_s).coding : slice.coding
|
181
|
-
|
180
|
+
slice_coding = discriminator[:path].present? ? slice.send((discriminator[:path]).to_s).coding : slice.coding
|
181
|
+
slice_coding.any? do |coding|
|
182
|
+
discriminator[:values].any? do |value|
|
183
|
+
case value
|
184
|
+
when String
|
185
|
+
value == coding.code
|
186
|
+
when Hash
|
187
|
+
value[:system] == coding.system && value[:code] == coding.code
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
182
191
|
end
|
183
192
|
|
184
193
|
def verify_slice_by_values(element, value_definitions)
|
@@ -332,18 +332,23 @@ module Inferno
|
|
332
332
|
when 'Date'
|
333
333
|
begin
|
334
334
|
Date.parse(element)
|
335
|
-
rescue ArgumentError
|
335
|
+
rescue ArgumentError, TypeError
|
336
336
|
false
|
337
337
|
end
|
338
338
|
when 'DateTime'
|
339
339
|
begin
|
340
340
|
DateTime.parse(element)
|
341
|
-
rescue ArgumentError
|
341
|
+
rescue ArgumentError, TypeError
|
342
342
|
false
|
343
343
|
end
|
344
344
|
when 'String'
|
345
345
|
element.is_a? String
|
346
346
|
else
|
347
|
+
if element.is_a? FHIR::Bundle::Entry
|
348
|
+
# Special case for type slicing in a Bundle - look at the resource not the entry
|
349
|
+
element = element.resource
|
350
|
+
end
|
351
|
+
|
347
352
|
element.is_a? FHIR.const_get(discriminator[:code])
|
348
353
|
end
|
349
354
|
end
|
@@ -352,7 +357,14 @@ module Inferno
|
|
352
357
|
coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
|
353
358
|
|
354
359
|
find_a_value_at(element, coding_path) do |coding|
|
355
|
-
discriminator[:values].any?
|
360
|
+
discriminator[:values].any? do |value|
|
361
|
+
case value
|
362
|
+
when String
|
363
|
+
value == coding.code
|
364
|
+
when Hash
|
365
|
+
value[:system] == coding.system && value[:code] == coding.code
|
366
|
+
end
|
367
|
+
end
|
356
368
|
end
|
357
369
|
end
|
358
370
|
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Inferno
|
2
|
+
module DSL
|
3
|
+
# A `RequirementSet` represents the set of requirements which are tested by
|
4
|
+
# a TestSuite.
|
5
|
+
#
|
6
|
+
# @!attribute identifier [rw] The unique identifier for the source of
|
7
|
+
# requirements included in this `RequirementSet`
|
8
|
+
# @!attribute title [rw] A human-readable title for this `RequirementSet`
|
9
|
+
# @!attribute actor [rw] The actor whose requirements are included in this
|
10
|
+
# `RequirementSet`
|
11
|
+
# @!attribute requirements [rw] There are three options:
|
12
|
+
# * `"all"` (default) - Include all of the requirements for the specified
|
13
|
+
# actor from the requirement source
|
14
|
+
# * `"referenced"` - Only include requirements from this source if they
|
15
|
+
# are referenced by other included requirements
|
16
|
+
# * `"1,3,5-8"` - Only include the requirements from a comma-delimited
|
17
|
+
# list
|
18
|
+
# @!attribute suite_options [rw] A set of suite options which must be
|
19
|
+
# selected in order for this `RequirementSet` to be included
|
20
|
+
#
|
21
|
+
# @see Inferno::DSL::SuiteRequirements#requirement_sets
|
22
|
+
class RequirementSet
|
23
|
+
ATTRIBUTES = [
|
24
|
+
:identifier,
|
25
|
+
:title,
|
26
|
+
:actor,
|
27
|
+
:requirements,
|
28
|
+
:suite_options
|
29
|
+
].freeze
|
30
|
+
|
31
|
+
include Entities::Attributes
|
32
|
+
|
33
|
+
def initialize(raw_attributes_hash)
|
34
|
+
attributes_hash = raw_attributes_hash.symbolize_keys
|
35
|
+
|
36
|
+
invalid_keys = attributes_hash.keys - ATTRIBUTES
|
37
|
+
|
38
|
+
raise Exceptions::UnknownAttributeException.new(invalid_keys, self.class) if invalid_keys.present?
|
39
|
+
|
40
|
+
attributes_hash.each do |name, value|
|
41
|
+
if name == :suite_options
|
42
|
+
value = value&.map { |option_id, option_value| SuiteOption.new(id: option_id, value: option_value) }
|
43
|
+
end
|
44
|
+
|
45
|
+
instance_variable_set(:"@#{name}", value)
|
46
|
+
end
|
47
|
+
|
48
|
+
self.suite_options ||= []
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns true when the `RequirementSet` includes all of the requirements
|
52
|
+
# from the source for the specified actor
|
53
|
+
#
|
54
|
+
# @return [Boolean]
|
55
|
+
def complete?
|
56
|
+
requirements.blank? || requirements.casecmp?('all')
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns true when the `RequirementSet` only includes requirements
|
60
|
+
# referenced by other `RequirementSet`s
|
61
|
+
#
|
62
|
+
# @return [Boolean]
|
63
|
+
def referenced?
|
64
|
+
requirements&.casecmp? 'referenced'
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns true when the `RequirementSet` only includes requirements
|
68
|
+
# specified in a list
|
69
|
+
#
|
70
|
+
# @return [Boolean]
|
71
|
+
def filtered?
|
72
|
+
!complete? && !referenced?
|
73
|
+
end
|
74
|
+
|
75
|
+
# Expands the compressed comma-separated requirements list into an Array
|
76
|
+
# of full ids
|
77
|
+
def expand_requirement_ids
|
78
|
+
Entities::Requirement.expand_requirement_ids(requirements, identifier)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/inferno/dsl/runnable.rb
CHANGED
@@ -276,6 +276,21 @@ module Inferno
|
|
276
276
|
@input_instructions = format_markdown(new_input_instructions)
|
277
277
|
end
|
278
278
|
|
279
|
+
# Set/Get the IDs of requirements verified by this runnable
|
280
|
+
# Set with [] to clear the list
|
281
|
+
#
|
282
|
+
# @param requirements [Array<String>]
|
283
|
+
# @return [Array<String>] the requirement IDs
|
284
|
+
def verifies_requirements(*requirement_ids)
|
285
|
+
if requirement_ids.empty?
|
286
|
+
@requirement_ids || []
|
287
|
+
elsif requirement_ids == [[]]
|
288
|
+
@requirement_ids = []
|
289
|
+
else
|
290
|
+
@requirement_ids = requirement_ids
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
279
294
|
# Mark as optional. Tests are required by default.
|
280
295
|
#
|
281
296
|
# @param optional [Boolean]
|
@@ -550,6 +565,13 @@ module Inferno
|
|
550
565
|
end
|
551
566
|
end
|
552
567
|
|
568
|
+
# @private
|
569
|
+
def all_requirements(suite_options = [])
|
570
|
+
children(suite_options).flat_map do |child|
|
571
|
+
child.verifies_requirements + child.all_requirements(suite_options)
|
572
|
+
end
|
573
|
+
end
|
574
|
+
|
553
575
|
# @private
|
554
576
|
def inspect
|
555
577
|
non_dynamic_ancestor = ancestors.find { |ancestor| !ancestor.to_s.start_with? '#' }
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative 'requirement_set'
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module DSL
|
5
|
+
module SuiteRequirements
|
6
|
+
# Get/Set the sets of requirments tested by a suite.
|
7
|
+
#
|
8
|
+
# @param sets [Array<Inferno::DSL::RequirementSet>]
|
9
|
+
# @return [Array<Inferno::DSL::RequirementSet>]
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# class Suite < Inferno::TestSuite
|
13
|
+
# requirement_sets(
|
14
|
+
# {
|
15
|
+
# identifier: 'example-regulation-1',
|
16
|
+
# title: 'Example Regulation 1',
|
17
|
+
# actor: 'Provider' # Only include requirements for the 'Provider'
|
18
|
+
# # actor
|
19
|
+
# },
|
20
|
+
# {
|
21
|
+
# identifier: 'example-ig-1',
|
22
|
+
# title: 'Example Implementation Guide 1',
|
23
|
+
# actor: 'Provider',
|
24
|
+
# requirements: '2, 4-5' # Only include these specific requirements
|
25
|
+
# },
|
26
|
+
# {
|
27
|
+
# identifier: 'example-ig-2',
|
28
|
+
# title: 'Example Implementation Guide 2',
|
29
|
+
# requirements: 'Referenced', # Only include requirements from this
|
30
|
+
# # set that are referenced by other
|
31
|
+
# # included requirements
|
32
|
+
# actor: 'Server',
|
33
|
+
# suite_options: { # Only include these requirements if the ig
|
34
|
+
# ig_version: '3.0.0' # version 3.0.0 suite option is selected
|
35
|
+
# }
|
36
|
+
# }
|
37
|
+
# )
|
38
|
+
# end
|
39
|
+
def requirement_sets(*sets)
|
40
|
+
@requirement_sets = sets.map { |set| RequirementSet.new(**set) } if sets.present?
|
41
|
+
|
42
|
+
@requirement_sets || []
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/inferno/entities/ig.rb
CHANGED
@@ -126,6 +126,10 @@ module Inferno
|
|
126
126
|
"#{ig_resource.id}##{ig_resource.version || 'current'}"
|
127
127
|
end
|
128
128
|
|
129
|
+
def value_sets
|
130
|
+
resources_by_type['ValueSet']
|
131
|
+
end
|
132
|
+
|
129
133
|
def profiles
|
130
134
|
resources_by_type['StructureDefinition'].filter { |sd| sd.type != 'Extension' }
|
131
135
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative 'attributes'
|
2
|
+
require_relative 'entity'
|
3
|
+
|
4
|
+
module Inferno
|
5
|
+
module Entities
|
6
|
+
# A `Requirement` represents the specific rule or behavior a runnable is testing.
|
7
|
+
class Requirement < Entity
|
8
|
+
ATTRIBUTES = [
|
9
|
+
:id,
|
10
|
+
:requirement_set,
|
11
|
+
:url,
|
12
|
+
:requirement,
|
13
|
+
:conformance,
|
14
|
+
:actor,
|
15
|
+
:sub_requirements,
|
16
|
+
:conditionality
|
17
|
+
].freeze
|
18
|
+
|
19
|
+
include Inferno::Entities::Attributes
|
20
|
+
|
21
|
+
def initialize(params)
|
22
|
+
super(params, ATTRIBUTES)
|
23
|
+
|
24
|
+
self.requirement_set = id.split('@').first if requirement_set.blank? && id&.include?('@')
|
25
|
+
end
|
26
|
+
|
27
|
+
# Expand a comma-delimited list of requirement id references into an Array
|
28
|
+
# of full requirement ids
|
29
|
+
#
|
30
|
+
# @param requirement_id_string [String] A comma-delimited list of
|
31
|
+
# requirement id references
|
32
|
+
# @param default_set [String] The requirement set identifier which will be
|
33
|
+
# used if none is included in the `requirement_id_string`
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
# expand_requirement_ids('example-ig@1,3,5-7')
|
37
|
+
# # => ['example-ig@1','example-ig@3','example-ig@5','example-ig@6','example-ig@7']
|
38
|
+
# expand_requirement_ids('1,3,5-7', 'example-ig')
|
39
|
+
# # => ['example-ig@1','example-ig@3','example-ig@5','example-ig@6','example-ig@7']
|
40
|
+
def self.expand_requirement_ids(requirement_id_string, default_set = nil)
|
41
|
+
return [] if requirement_id_string.blank?
|
42
|
+
|
43
|
+
current_set = default_set
|
44
|
+
requirement_id_string
|
45
|
+
.split(',')
|
46
|
+
.map(&:strip)
|
47
|
+
.flat_map do |requirement_string|
|
48
|
+
current_set, requirement_string = requirement_string.split('@') if requirement_string.include?('@')
|
49
|
+
|
50
|
+
requirement_ids =
|
51
|
+
if requirement_string.include? '-'
|
52
|
+
start_id, end_id = requirement_string.split('-')
|
53
|
+
(start_id..end_id).to_a
|
54
|
+
else
|
55
|
+
[requirement_string]
|
56
|
+
end
|
57
|
+
|
58
|
+
requirement_ids.map { |id| "#{current_set}@#{id}" }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require_relative 'test_group'
|
2
2
|
require_relative '../dsl/runnable'
|
3
3
|
require_relative '../dsl/suite_option'
|
4
|
+
require_relative '../dsl/suite_requirements'
|
4
5
|
require_relative '../dsl/messages'
|
5
6
|
require_relative '../dsl/links'
|
6
7
|
require_relative '../repositories/test_groups'
|
@@ -16,6 +17,7 @@ module Inferno
|
|
16
17
|
extend DSL::Links
|
17
18
|
extend DSL::FHIRClient::ClassMethods
|
18
19
|
extend DSL::HTTPClient::ClassMethods
|
20
|
+
extend DSL::SuiteRequirements
|
19
21
|
include DSL::FHIRValidation
|
20
22
|
include DSL::FHIRResourceValidation
|
21
23
|
include DSL::FhirpathEvaluation
|