inferno_core 0.6.8 → 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/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_extensions_used.rb +76 -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/repositories/igs.rb +1 -2
- data/lib/inferno/repositories/requirements.rb +116 -0
- data/lib/inferno/version.rb +1 -1
- metadata +14 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5740a0c7e8d54767851b4f7e9ee7c5c6ca9ac175211d63b4990f99bbbe9266ca
|
4
|
+
data.tar.gz: 4fd1b9c5cb4f37c441a668ae58c8b6cd49aeea18ad393b2d7bbb9699d86a0060
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f6adc91f5db1ba2a6535c847c0fe7b691cd85455fb0f34a44bd525428dee3ddcd4c414d7989ea4e13fdd50b9c39778a19d26e97473f1617b85691b4683776f49
|
7
|
+
data.tar.gz: 463d68cf3e187f992bd134bbc177072bfaa9ec625a2006ca1bb23c49591a4bc9521ca027232c68c5d1ebcf499dcca84f3f722ca2afd9bd1b6038bbb19daf4835
|
@@ -13,13 +13,13 @@ module <%= module_name %>
|
|
13
13
|
|
14
14
|
input :credentials,
|
15
15
|
title: 'OAuth Credentials',
|
16
|
-
type: :
|
16
|
+
type: :auth_info,
|
17
17
|
optional: true
|
18
18
|
|
19
19
|
# All FHIR requests in this suite will use this FHIR client
|
20
20
|
fhir_client do
|
21
21
|
url :url
|
22
|
-
|
22
|
+
auth_info :credentials
|
23
23
|
end
|
24
24
|
|
25
25
|
# All FHIR validation requests will use this FHIR validator
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative '../../serializers/requirement'
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module Web
|
5
|
+
module Controllers
|
6
|
+
module Requirements
|
7
|
+
class Show < Controller
|
8
|
+
def handle(req, res)
|
9
|
+
requirement = repo.find(req.params[:id])
|
10
|
+
halt 404 if requirement.nil?
|
11
|
+
|
12
|
+
res.body = serialize(requirement)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Inferno
|
2
|
+
module Web
|
3
|
+
module Controllers
|
4
|
+
module TestSuites
|
5
|
+
module Requirements
|
6
|
+
class Index < Controller
|
7
|
+
include Import[test_suites_repo: 'inferno.repositories.test_suites']
|
8
|
+
include Import[test_sessions_repo: 'inferno.repositories.test_sessions']
|
9
|
+
|
10
|
+
def handle(req, res)
|
11
|
+
test_suite = test_suites_repo.find(req.params[:id])
|
12
|
+
halt 404, "Test Suite `#{req.params[:id]}` not found" if test_suite.nil?
|
13
|
+
|
14
|
+
test_session = nil
|
15
|
+
if req.params[:session_id]
|
16
|
+
test_session = test_sessions_repo.find(req.params[:session_id])
|
17
|
+
halt 404, "Test session `#{req.params[:session_id]}` not found" if test_session.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
requirements = repo.requirements_for_suite(test_suite.id, test_session&.id)
|
21
|
+
|
22
|
+
res.body = serialize(requirements)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -47,6 +47,13 @@ module Inferno
|
|
47
47
|
put '/:id/check_configuration',
|
48
48
|
to: Inferno::Web::Controllers::TestSuites::CheckConfiguration,
|
49
49
|
as: :check_configuration
|
50
|
+
get ':id/requirements',
|
51
|
+
to: Inferno::Web::Controllers::TestSuites::Requirements::Index,
|
52
|
+
as: :requirements
|
53
|
+
end
|
54
|
+
|
55
|
+
scope 'requirements' do
|
56
|
+
get '/:id', to: Inferno::Web::Controllers::Requirements::Show, as: :show
|
50
57
|
end
|
51
58
|
|
52
59
|
get '/requests/:id', to: Inferno::Web::Controllers::Requests::Show, as: :requests_show
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'serializer'
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module Web
|
5
|
+
module Serializers
|
6
|
+
class Requirement < Serializer
|
7
|
+
identifier :id
|
8
|
+
|
9
|
+
field :requirement
|
10
|
+
field :conformance
|
11
|
+
field :actor
|
12
|
+
field :sub_requirements
|
13
|
+
field :conditionality
|
14
|
+
field :url, if: :field_present?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative 'preset'
|
2
|
+
require_relative 'requirement_set'
|
2
3
|
require_relative 'suite_option'
|
3
4
|
require_relative 'test_group'
|
4
5
|
|
@@ -27,6 +28,7 @@ module Inferno
|
|
27
28
|
|
28
29
|
view :full do
|
29
30
|
include_view :summary
|
31
|
+
|
30
32
|
field :test_groups do |suite, options|
|
31
33
|
suite_options = options[:suite_options]
|
32
34
|
TestGroup.render_as_hash(suite.groups(suite_options), suite_options:)
|
@@ -36,6 +38,14 @@ module Inferno
|
|
36
38
|
suite_options = options[:suite_options]
|
37
39
|
Input.render_as_hash(suite.available_inputs(suite_options).values)
|
38
40
|
end
|
41
|
+
field :requirement_sets, if: :field_present? do |suite, options|
|
42
|
+
selected_options = options[:suite_options] || []
|
43
|
+
requirement_sets = suite.requirement_sets.select do |requirement_set|
|
44
|
+
requirement_set.suite_options.all? { |suite_option| selected_options.include? suite_option }
|
45
|
+
end
|
46
|
+
|
47
|
+
RequirementSet.render_as_hash(requirement_sets)
|
48
|
+
end
|
39
49
|
end
|
40
50
|
end
|
41
51
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative '../../repositories/requirements'
|
2
|
+
|
3
|
+
Inferno::Application.register_provider(:requirements) do
|
4
|
+
prepare do
|
5
|
+
target_container.start :suites
|
6
|
+
|
7
|
+
requirements_repo = Inferno::Repositories::Requirements.new
|
8
|
+
|
9
|
+
test_kit_gems =
|
10
|
+
Bundler
|
11
|
+
.definition
|
12
|
+
.specs
|
13
|
+
.select { |spec| spec.metadata.fetch('inferno_test_kit', 'false').casecmp? 'true' }
|
14
|
+
|
15
|
+
files_to_load = Dir.glob(['lib/*test_kit/requirements/*.csv'])
|
16
|
+
|
17
|
+
if ENV['LOAD_DEV_SUITES'].present?
|
18
|
+
ENV['LOAD_DEV_SUITES'].split(',').map(&:strip).reject(&:empty?).each do |suite|
|
19
|
+
files_to_load.concat Dir.glob(File.join(Inferno::Application.root, 'dev_suites', suite, 'requirements',
|
20
|
+
'*.csv'))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
files_to_load +=
|
25
|
+
test_kit_gems.flat_map do |gem|
|
26
|
+
[
|
27
|
+
Dir.glob([File.join(gem.full_gem_path, 'lib', '*test_kit', 'requirements', '*.csv')])
|
28
|
+
].flatten
|
29
|
+
end
|
30
|
+
|
31
|
+
files_to_load.compact!
|
32
|
+
files_to_load.reject! { |file| file.include?('out_of_scope') }
|
33
|
+
files_to_load.map! { |path| File.realpath(path) }
|
34
|
+
files_to_load.uniq!
|
35
|
+
|
36
|
+
files_to_load.each do |path|
|
37
|
+
requirements_repo.insert_from_file(path)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module DSL
|
5
|
+
module FHIREvaluation
|
6
|
+
module Rules
|
7
|
+
class AllExtensionsUsed < Rule
|
8
|
+
def check(context)
|
9
|
+
all_extensions = collect_profile_extensions(context.ig.profiles)
|
10
|
+
unused_extensions = remove_found_resource_extensions(all_extensions, context.data)
|
11
|
+
if unused_extensions.any? { |_profile, extensions| !extensions.empty? }
|
12
|
+
message = get_fail_message(unused_extensions)
|
13
|
+
result = EvaluationResult.new(message, rule: self)
|
14
|
+
else
|
15
|
+
message = 'All extensions specified in profiles are represented in instances.'
|
16
|
+
result = EvaluationResult.new(message, severity: 'success', rule: self)
|
17
|
+
end
|
18
|
+
|
19
|
+
context.add_result result
|
20
|
+
end
|
21
|
+
|
22
|
+
def collect_profile_extensions(profiles)
|
23
|
+
extensions = Hash.new { |extension, profile| extension[profile] = Set.new }
|
24
|
+
profiles.each do |profile|
|
25
|
+
profile.each_element do |value, metadata|
|
26
|
+
next unless metadata['type'] == 'ElementDefinition'
|
27
|
+
|
28
|
+
path_end = value.id.split('.')[-1]
|
29
|
+
next unless path_end.include?('extension')
|
30
|
+
|
31
|
+
value.type.each do |element_definition|
|
32
|
+
element_definition.profile.each do |extension_url|
|
33
|
+
extensions[profile.url].add(extension_url)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
extensions
|
39
|
+
end
|
40
|
+
|
41
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
42
|
+
def remove_found_resource_extensions(extensions, examples)
|
43
|
+
unused_extensions = extensions.dup
|
44
|
+
examples.each do |resource|
|
45
|
+
resource.each_element do |value, _metadata, path|
|
46
|
+
path_elements = path.split('.')
|
47
|
+
next unless path_elements.length > 1
|
48
|
+
|
49
|
+
next unless path_elements[-2].include?('extension') && path_elements[-1] == 'url'
|
50
|
+
|
51
|
+
profiles = resource&.meta&.profile || []
|
52
|
+
update_unused_extensions(profiles, value, unused_extensions, extensions)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
unused_extensions
|
56
|
+
end
|
57
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
58
|
+
|
59
|
+
def update_unused_extensions(profiles, value, unused_extensions, extensions)
|
60
|
+
profiles.each do |profile|
|
61
|
+
unused_extensions[profile].delete(value) if extensions.key?(profile)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_fail_message(extensions)
|
66
|
+
message = 'Found extensions specified in profiles, but not used in instances:'
|
67
|
+
extensions.each do |profile, extension|
|
68
|
+
message += "\n Profile: #{profile}, \n\tExtensions: #{extension.join(', ')}" unless extension.empty?
|
69
|
+
end
|
70
|
+
message
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module DSL
|
5
|
+
module FHIREvaluation
|
6
|
+
module Rules
|
7
|
+
class DifferentialContentHasExamples < Rule
|
8
|
+
def check(context)
|
9
|
+
unused_differential = Hash.new { |field, url| field[url] = Set.new }
|
10
|
+
collect_profile_differential_content(unused_differential, context.ig.profiles)
|
11
|
+
collect_profile_differential_content(unused_differential, context.ig.extensions)
|
12
|
+
remove_found_differential_content(unused_differential, context.data)
|
13
|
+
|
14
|
+
if unused_differential.any? { |_url, diff| !diff.empty? }
|
15
|
+
message = gen_differential_fail_message(unused_differential)
|
16
|
+
result = EvaluationResult.new(message, rule: self)
|
17
|
+
else
|
18
|
+
message = 'All differential fields are represented in instances'
|
19
|
+
result = EvaluationResult.new(message, severity: 'success', rule: self)
|
20
|
+
end
|
21
|
+
|
22
|
+
context.add_result result
|
23
|
+
end
|
24
|
+
|
25
|
+
def collect_profile_differential_content(unused_differential, profiles)
|
26
|
+
profiles.each do |profile|
|
27
|
+
profile.each_element do |value, _metadata, path|
|
28
|
+
next unless path.start_with? 'differential'
|
29
|
+
|
30
|
+
next unless value.is_a? FHIR::ElementDefinition
|
31
|
+
next unless value.id.include? '.'
|
32
|
+
|
33
|
+
# Skip fields that are disallowed by the profile (cardinality 0..0)
|
34
|
+
# Note that max is a string to allow for "*", not an int
|
35
|
+
next if value.max == '0'
|
36
|
+
|
37
|
+
# TODO: discriminate between extensions
|
38
|
+
# if you have field.extension:A and field.extension:B
|
39
|
+
# only field.extension is recorded and checked for
|
40
|
+
# if A and B are not defined in the IG,they may be missed
|
41
|
+
clean_val = clean_value(value)
|
42
|
+
|
43
|
+
unused_differential[profile.url].add(clean_val)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
49
|
+
def remove_found_differential_content(unused_differential, examples)
|
50
|
+
examples.each do |resource|
|
51
|
+
extension_base_path = ''
|
52
|
+
extension_url = ''
|
53
|
+
resource.each_element do |value, _metadata, path|
|
54
|
+
profiles = resource&.meta&.profile || []
|
55
|
+
profiles.each do |profile|
|
56
|
+
processed_path = plain_value(path)
|
57
|
+
processed_path = rm_brackets(processed_path)
|
58
|
+
|
59
|
+
if path.match?('extension\[\d+\]\.url$')
|
60
|
+
extension_base_path = path.rpartition('.').first
|
61
|
+
extension_url = value
|
62
|
+
unused_differential[extension_url].delete('url') if unused_differential.key?(extension_url)
|
63
|
+
unused_differential[extension_url].delete('extension') if unused_differential.key?(extension_url)
|
64
|
+
unused_differential.delete(extension_url) if unused_differential[extension_url].empty?
|
65
|
+
elsif path.start_with?(extension_base_path) && !extension_base_path.empty?
|
66
|
+
if unused_differential.key?(extension_url)
|
67
|
+
unused_differential[extension_url].delete(processed_path.rpartition('.').last)
|
68
|
+
end
|
69
|
+
unused_differential.delete(extension_url) if unused_differential[extension_url].empty?
|
70
|
+
else
|
71
|
+
unused_differential[profile].delete(processed_path) if unused_differential.key?(profile)
|
72
|
+
unused_differential.delete(profile) if unused_differential[profile].empty?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
79
|
+
|
80
|
+
def clean_value(value)
|
81
|
+
stripped_val = value.id.partition('.').last
|
82
|
+
stripped_val = stripped_val.partition('[').first if stripped_val.end_with? ']'
|
83
|
+
stripped_val.split('.').map do |field|
|
84
|
+
field = field.partition(':').first if field.include?(':')
|
85
|
+
field = field.partition('[').first if field.include?('[')
|
86
|
+
field
|
87
|
+
end.join('.')
|
88
|
+
end
|
89
|
+
|
90
|
+
def plain_value(path)
|
91
|
+
if path.include? '.'
|
92
|
+
path_array = path.split('.').map! do |field|
|
93
|
+
field.start_with?('value') ? 'value' : field
|
94
|
+
end
|
95
|
+
path_array.join('.')
|
96
|
+
elsif path.start_with?('value')
|
97
|
+
'value'
|
98
|
+
elsif path.end_with?(']')
|
99
|
+
path.rpartition('[').first
|
100
|
+
else
|
101
|
+
path
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def rm_brackets(path)
|
106
|
+
path_array = path.split('.').map! do |field|
|
107
|
+
field.include?('[') ? field.partition('[').first : field
|
108
|
+
end
|
109
|
+
path_array.join('.')
|
110
|
+
end
|
111
|
+
|
112
|
+
def gen_differential_fail_message(unused_differential)
|
113
|
+
"Found fields highlighted in the differential view, but not used in instances: #{
|
114
|
+
unused_differential.map do |url, field|
|
115
|
+
next if field.empty?
|
116
|
+
|
117
|
+
"\n Profile/Extension: #{url} \n\tFields: #{field.join(', ')}"
|
118
|
+
end.compact.join}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -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
|
@@ -18,8 +18,7 @@ module Inferno
|
|
18
18
|
def find_or_load(id_or_path)
|
19
19
|
return find(id_or_path) if exists?(id_or_path)
|
20
20
|
|
21
|
-
ig_by_path = find_by_path(id_or_path)
|
22
|
-
|
21
|
+
ig_by_path = find_by_path(id_or_path) || find_by_path(find_local_file(id_or_path))
|
23
22
|
return ig_by_path if ig_by_path
|
24
23
|
|
25
24
|
load(id_or_path)
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require_relative 'in_memory_repository'
|
3
|
+
require_relative '../entities/requirement'
|
4
|
+
|
5
|
+
module Inferno
|
6
|
+
module Repositories
|
7
|
+
# Repository that deals with persistence for the `Requirement` entity.
|
8
|
+
class Requirements < InMemoryRepository
|
9
|
+
def insert_from_file(path)
|
10
|
+
result = []
|
11
|
+
|
12
|
+
CSV.foreach(path, headers: true, header_converters: :symbol) do |row|
|
13
|
+
req_set = row[:req_set]
|
14
|
+
id = row[:id]
|
15
|
+
sub_requirements_field = row[:subrequirements]
|
16
|
+
|
17
|
+
combined_id = "#{req_set}@#{id}"
|
18
|
+
|
19
|
+
# Processing sub requirements: e.g. "170.315(g)(31)_hti-2-proposal@5,17,23,26,27,32,35,38-41"
|
20
|
+
sub_requirements = Inferno::Entities::Requirement.expand_requirement_ids(sub_requirements_field)
|
21
|
+
|
22
|
+
result << {
|
23
|
+
requirement_set: req_set,
|
24
|
+
id: combined_id,
|
25
|
+
url: row[:url],
|
26
|
+
requirement: row[:requirement],
|
27
|
+
conformance: row[:conformance],
|
28
|
+
actor: row[:actor],
|
29
|
+
sub_requirements: sub_requirements,
|
30
|
+
conditionality: row[:conditionality]&.downcase
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
result.each do |raw_req|
|
35
|
+
requirement = Entities::Requirement.new(raw_req)
|
36
|
+
|
37
|
+
insert(requirement)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def filter_requirements_by_ids(ids)
|
42
|
+
all.select { |requirement| ids.include?(requirement.id) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def requirements_for_suite(test_suite_id, test_session_id = nil)
|
46
|
+
test_suite = Inferno::Repositories::TestSuites.new.find(test_suite_id)
|
47
|
+
selected_suite_options =
|
48
|
+
if test_session_id.present?
|
49
|
+
Inferno::Repositories::TestSessions.new.find(test_session_id).suite_options
|
50
|
+
else
|
51
|
+
[]
|
52
|
+
end
|
53
|
+
|
54
|
+
requirement_sets =
|
55
|
+
test_suite
|
56
|
+
.requirement_sets
|
57
|
+
.select do |set|
|
58
|
+
set.suite_options.all? { |set_option| selected_suite_options.include? set_option }
|
59
|
+
end
|
60
|
+
|
61
|
+
requirements =
|
62
|
+
complete_requirement_set_requirements(requirement_sets) +
|
63
|
+
filtered_requirement_set_requirements(requirement_sets)
|
64
|
+
|
65
|
+
add_referenced_requirement_set_requirements(requirements, requirement_sets).uniq
|
66
|
+
end
|
67
|
+
|
68
|
+
def complete_requirement_set_requirements(requirement_sets)
|
69
|
+
requirement_sets.select(&:complete?)
|
70
|
+
.flat_map do |requirement_set|
|
71
|
+
all.select do |requirement|
|
72
|
+
requirement.requirement_set == requirement_set.identifier && requirement.actor == requirement_set.actor
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def filtered_requirement_set_requirements(requirement_sets)
|
78
|
+
requirement_sets.select(&:filtered?)
|
79
|
+
.flat_map do |requirement_set|
|
80
|
+
requirement_set
|
81
|
+
.expand_requirement_ids
|
82
|
+
.map { |requirement_id| find(requirement_id) }
|
83
|
+
.select { |requirement| requirement.actor == requirement_set.actor }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def add_referenced_requirement_set_requirements( # rubocop:disable Metrics/CyclomaticComplexity
|
88
|
+
requirements_to_process,
|
89
|
+
requirement_sets,
|
90
|
+
processed_requirements = []
|
91
|
+
)
|
92
|
+
return processed_requirements if requirements_to_process.blank?
|
93
|
+
|
94
|
+
referenced_requirement_sets = requirement_sets.select(&:referenced?)
|
95
|
+
|
96
|
+
referenced_requirement_ids =
|
97
|
+
requirements_to_process
|
98
|
+
.flat_map(&:sub_requirements)
|
99
|
+
.select do |requirement_id|
|
100
|
+
referenced_requirement_sets.any? do |set|
|
101
|
+
requirement_id.start_with?("#{set.identifier}@") && (find(requirement_id).actor == set.actor)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
new_requirements =
|
106
|
+
referenced_requirement_ids.map { |id| find(id) } - requirements_to_process - processed_requirements
|
107
|
+
|
108
|
+
add_referenced_requirement_set_requirements(
|
109
|
+
new_requirements,
|
110
|
+
referenced_requirement_sets,
|
111
|
+
(processed_requirements + requirements_to_process).uniq
|
112
|
+
)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/inferno/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: inferno_core
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen MacVicar
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2025-
|
13
|
+
date: 2025-04-18 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activesupport
|
@@ -465,6 +465,7 @@ files:
|
|
465
465
|
- lib/inferno/apps/web/application.rb
|
466
466
|
- lib/inferno/apps/web/controllers/controller.rb
|
467
467
|
- lib/inferno/apps/web/controllers/requests/show.rb
|
468
|
+
- lib/inferno/apps/web/controllers/requirements/show.rb
|
468
469
|
- lib/inferno/apps/web/controllers/test_runs/create.rb
|
469
470
|
- lib/inferno/apps/web/controllers/test_runs/destroy.rb
|
470
471
|
- lib/inferno/apps/web/controllers/test_runs/results/index.rb
|
@@ -479,6 +480,7 @@ files:
|
|
479
480
|
- lib/inferno/apps/web/controllers/test_sessions/show.rb
|
480
481
|
- lib/inferno/apps/web/controllers/test_suites/check_configuration.rb
|
481
482
|
- lib/inferno/apps/web/controllers/test_suites/index.rb
|
483
|
+
- lib/inferno/apps/web/controllers/test_suites/requirements/index.rb
|
482
484
|
- lib/inferno/apps/web/controllers/test_suites/show.rb
|
483
485
|
- lib/inferno/apps/web/index.html.erb
|
484
486
|
- lib/inferno/apps/web/router.rb
|
@@ -489,6 +491,8 @@ files:
|
|
489
491
|
- lib/inferno/apps/web/serializers/message.rb
|
490
492
|
- lib/inferno/apps/web/serializers/preset.rb
|
491
493
|
- lib/inferno/apps/web/serializers/request.rb
|
494
|
+
- lib/inferno/apps/web/serializers/requirement.rb
|
495
|
+
- lib/inferno/apps/web/serializers/requirement_set.rb
|
492
496
|
- lib/inferno/apps/web/serializers/result.rb
|
493
497
|
- lib/inferno/apps/web/serializers/serializer.rb
|
494
498
|
- lib/inferno/apps/web/serializers/session_data.rb
|
@@ -505,6 +509,7 @@ files:
|
|
505
509
|
- lib/inferno/config/boot/ig_files.rb
|
506
510
|
- lib/inferno/config/boot/logging.rb
|
507
511
|
- lib/inferno/config/boot/presets.rb
|
512
|
+
- lib/inferno/config/boot/requirements.rb
|
508
513
|
- lib/inferno/config/boot/sidekiq.rb
|
509
514
|
- lib/inferno/config/boot/suites.rb
|
510
515
|
- lib/inferno/config/boot/validator.rb
|
@@ -535,11 +540,14 @@ files:
|
|
535
540
|
- lib/inferno/dsl/fhir_evaluation/reference_extractor.rb
|
536
541
|
- lib/inferno/dsl/fhir_evaluation/rule.rb
|
537
542
|
- lib/inferno/dsl/fhir_evaluation/rules/all_defined_extensions_have_examples.rb
|
543
|
+
- lib/inferno/dsl/fhir_evaluation/rules/all_extensions_used.rb
|
538
544
|
- lib/inferno/dsl/fhir_evaluation/rules/all_must_supports_present.rb
|
539
545
|
- lib/inferno/dsl/fhir_evaluation/rules/all_profiles_have_examples.rb
|
540
546
|
- lib/inferno/dsl/fhir_evaluation/rules/all_references_resolve.rb
|
541
547
|
- lib/inferno/dsl/fhir_evaluation/rules/all_resources_reachable.rb
|
542
548
|
- lib/inferno/dsl/fhir_evaluation/rules/all_search_parameters_have_examples.rb
|
549
|
+
- lib/inferno/dsl/fhir_evaluation/rules/differential_content_has_examples.rb
|
550
|
+
- lib/inferno/dsl/fhir_evaluation/rules/value_sets_demonstrate.rb
|
543
551
|
- lib/inferno/dsl/fhir_resource_navigation.rb
|
544
552
|
- lib/inferno/dsl/fhir_resource_validation.rb
|
545
553
|
- lib/inferno/dsl/fhir_validation.rb
|
@@ -556,11 +564,13 @@ files:
|
|
556
564
|
- lib/inferno/dsl/oauth_credentials.rb
|
557
565
|
- lib/inferno/dsl/primitive_type.rb
|
558
566
|
- lib/inferno/dsl/request_storage.rb
|
567
|
+
- lib/inferno/dsl/requirement_set.rb
|
559
568
|
- lib/inferno/dsl/results.rb
|
560
569
|
- lib/inferno/dsl/resume_test_route.rb
|
561
570
|
- lib/inferno/dsl/runnable.rb
|
562
571
|
- lib/inferno/dsl/suite_endpoint.rb
|
563
572
|
- lib/inferno/dsl/suite_option.rb
|
573
|
+
- lib/inferno/dsl/suite_requirements.rb
|
564
574
|
- lib/inferno/dsl/tcp_exception_handler.rb
|
565
575
|
- lib/inferno/dsl/value_extractor.rb
|
566
576
|
- lib/inferno/entities.rb
|
@@ -573,6 +583,7 @@ files:
|
|
573
583
|
- lib/inferno/entities/message.rb
|
574
584
|
- lib/inferno/entities/preset.rb
|
575
585
|
- lib/inferno/entities/request.rb
|
586
|
+
- lib/inferno/entities/requirement.rb
|
576
587
|
- lib/inferno/entities/result.rb
|
577
588
|
- lib/inferno/entities/session_data.rb
|
578
589
|
- lib/inferno/entities/test.rb
|
@@ -607,6 +618,7 @@ files:
|
|
607
618
|
- lib/inferno/repositories/presets.rb
|
608
619
|
- lib/inferno/repositories/repository.rb
|
609
620
|
- lib/inferno/repositories/requests.rb
|
621
|
+
- lib/inferno/repositories/requirements.rb
|
610
622
|
- lib/inferno/repositories/results.rb
|
611
623
|
- lib/inferno/repositories/session_data.rb
|
612
624
|
- lib/inferno/repositories/tags.rb
|