inferno_core 0.6.1 → 0.6.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/inferno/apps/cli/evaluate.rb +22 -12
- data/lib/inferno/apps/cli/templates/Dockerfile.tt +0 -1
- data/lib/inferno/apps/cli/templates/README.md.tt +10 -0
- data/lib/inferno/apps/cli/templates/docs/Overview.md +7 -0
- data/lib/inferno/apps/cli/templates/docs/README.md.tt +10 -0
- data/lib/inferno/apps/cli/templates/docs/_Footer.md +5 -0
- data/lib/inferno/apps/cli/templates/docs/_Sidebar.md +5 -0
- data/lib/inferno/config/boot/presets.rb +1 -1
- data/lib/inferno/dsl/auth_info.rb +87 -1
- data/lib/inferno/dsl/configurable.rb +14 -1
- data/lib/inferno/dsl/fhir_client.rb +66 -0
- data/lib/inferno/dsl/fhir_evaluation/evaluation_context.rb +4 -2
- data/lib/inferno/dsl/fhir_evaluation/evaluator.rb +8 -3
- data/lib/inferno/dsl/fhir_evaluation/profile_conformance_helper.rb +66 -0
- data/lib/inferno/dsl/fhir_evaluation/reference_extractor.rb +61 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_must_supports_present.rb +379 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_references_resolve.rb +53 -0
- data/lib/inferno/dsl/fhir_evaluation/rules/all_resources_reachable.rb +63 -0
- data/lib/inferno/dsl/fhir_resource_navigation.rb +226 -0
- data/lib/inferno/dsl/input_output_handling.rb +1 -0
- data/lib/inferno/dsl/must_support_metadata_extractor.rb +366 -0
- data/lib/inferno/dsl/primitive_type.rb +9 -0
- data/lib/inferno/dsl/runnable.rb +13 -1
- data/lib/inferno/dsl/value_extractor.rb +136 -0
- data/lib/inferno/dsl.rb +1 -0
- data/lib/inferno/entities/ig.rb +46 -24
- data/lib/inferno/entities/input.rb +63 -3
- data/lib/inferno/public/bundle.js +16 -16
- data/lib/inferno/repositories/session_data.rb +2 -0
- data/lib/inferno/version.rb +1 -1
- data/spec/runnable_context.rb +8 -5
- data/spec/shared/test_kit_examples.rb +23 -1
- metadata +15 -2
@@ -0,0 +1,136 @@
|
|
1
|
+
module Inferno
|
2
|
+
module DSL
|
3
|
+
class ValueExtractor
|
4
|
+
attr_accessor :ig_resources, :resource, :profile_elements
|
5
|
+
|
6
|
+
def initialize(ig_resources, resource, profile_elements)
|
7
|
+
self.ig_resources = ig_resources
|
8
|
+
self.resource = resource
|
9
|
+
self.profile_elements = profile_elements
|
10
|
+
end
|
11
|
+
|
12
|
+
def values_from_fixed_codes(profile_element, type)
|
13
|
+
return [] unless type == 'CodeableConcept'
|
14
|
+
|
15
|
+
elements = profile_elements.select do |element|
|
16
|
+
element.path == "#{profile_element.path}.coding.code" && element.fixedCode.present?
|
17
|
+
end
|
18
|
+
|
19
|
+
elements.map(&:fixedCode)
|
20
|
+
end
|
21
|
+
|
22
|
+
def values_from_pattern_coding(profile_element, type)
|
23
|
+
return [] unless type == 'CodeableConcept'
|
24
|
+
|
25
|
+
elements = profile_elements.select do |element|
|
26
|
+
element.path == "#{profile_element.path}.coding" && element.patternCoding.present?
|
27
|
+
end
|
28
|
+
|
29
|
+
elements.map { |element| element.patternCoding.code }
|
30
|
+
end
|
31
|
+
|
32
|
+
def values_from_pattern_codeable_concept(profile_element, type)
|
33
|
+
return [] unless type == 'CodeableConcept'
|
34
|
+
|
35
|
+
elements = profile_elements.select do |element|
|
36
|
+
element.path == profile_element.path && element.patternCodeableConcept.present? && element.min.positive?
|
37
|
+
end
|
38
|
+
|
39
|
+
elements.map { |element| element.patternCodeableConcept.coding.first.code }
|
40
|
+
end
|
41
|
+
|
42
|
+
def value_set_binding(the_element)
|
43
|
+
the_element&.binding
|
44
|
+
end
|
45
|
+
|
46
|
+
def value_set(the_element)
|
47
|
+
ig_resources.value_set_by_url(value_set_binding(the_element)&.valueSet)
|
48
|
+
end
|
49
|
+
|
50
|
+
def bound_systems(the_element)
|
51
|
+
bound_systems_from_valueset(value_set(the_element))
|
52
|
+
end
|
53
|
+
|
54
|
+
def bound_systems_from_valueset(value_set)
|
55
|
+
value_set&.compose&.include&.map do |include_element|
|
56
|
+
bound_systems_from_valueset_include_element(include_element)
|
57
|
+
end&.flatten&.compact
|
58
|
+
end
|
59
|
+
|
60
|
+
def bound_systems_from_valueset_include_element(include_element)
|
61
|
+
if include_element.concept.present?
|
62
|
+
include_element
|
63
|
+
elsif include_element.system.present? && include_element.filter&.empty?
|
64
|
+
# Cannot process intensional value set with filters
|
65
|
+
ig_resources.code_system_by_url(include_element.system)
|
66
|
+
elsif include_element.valueSet.present?
|
67
|
+
include_element.valueSet.map do |vs|
|
68
|
+
a_value_set = ig_resources.value_set_by_url(vs)
|
69
|
+
bound_systems_from_valueset(a_value_set)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def codes_from_value_set_binding(the_element)
|
75
|
+
codes_from_system_code_pair(codings_from_value_set_binding(the_element))
|
76
|
+
end
|
77
|
+
|
78
|
+
def codes_from_system_code_pair(codings)
|
79
|
+
codings.present? ? codings.map { |coding| coding[:code] }.compact.uniq : []
|
80
|
+
end
|
81
|
+
|
82
|
+
def codings_from_value_set_binding(the_element)
|
83
|
+
return [] if the_element.nil?
|
84
|
+
|
85
|
+
bound_systems = bound_systems(the_element)
|
86
|
+
|
87
|
+
return codings_from_bound_systems(bound_systems) if bound_systems.present?
|
88
|
+
|
89
|
+
expansion_contains = value_set_expansion_contains(the_element)
|
90
|
+
|
91
|
+
return [] if expansion_contains.blank?
|
92
|
+
|
93
|
+
expansion_contains.map { |contains| { system: contains.system, code: contains.code } }.compact.uniq
|
94
|
+
end
|
95
|
+
|
96
|
+
def codings_from_bound_systems(bound_systems)
|
97
|
+
return [] unless bound_systems.present?
|
98
|
+
|
99
|
+
bound_systems.flat_map do |bound_system|
|
100
|
+
case bound_system
|
101
|
+
when FHIR::ValueSet::Compose::Include
|
102
|
+
bound_system.concept.map { |concept| { system: bound_system.system, code: concept.code } }
|
103
|
+
when FHIR::CodeSystem
|
104
|
+
bound_system.concept.map { |concept| { system: bound_system.url, code: concept.code } }
|
105
|
+
else
|
106
|
+
[]
|
107
|
+
end
|
108
|
+
end.uniq
|
109
|
+
end
|
110
|
+
|
111
|
+
def value_set_expansion_contains(element)
|
112
|
+
value_set(element)&.expansion&.contains
|
113
|
+
end
|
114
|
+
|
115
|
+
def fhir_metadata(current_path)
|
116
|
+
FHIR.const_get(resource)::METADATA[current_path]
|
117
|
+
end
|
118
|
+
|
119
|
+
def values_from_resource_metadata(paths)
|
120
|
+
values = []
|
121
|
+
|
122
|
+
paths.each do |current_path|
|
123
|
+
current_metadata = fhir_metadata(current_path)
|
124
|
+
|
125
|
+
next unless current_metadata&.dig('valid_codes').present?
|
126
|
+
|
127
|
+
values += current_metadata['valid_codes'].flat_map do |system, codes|
|
128
|
+
codes.map { |code| { system:, code: } }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
values
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
data/lib/inferno/dsl.rb
CHANGED
data/lib/inferno/entities/ig.rb
CHANGED
@@ -14,10 +14,7 @@ module Inferno
|
|
14
14
|
class IG < Entity
|
15
15
|
ATTRIBUTES = [
|
16
16
|
:id,
|
17
|
-
:
|
18
|
-
:extensions,
|
19
|
-
:value_sets,
|
20
|
-
:search_params,
|
17
|
+
:resources_by_type,
|
21
18
|
:examples
|
22
19
|
].freeze
|
23
20
|
|
@@ -25,12 +22,8 @@ module Inferno
|
|
25
22
|
|
26
23
|
def initialize(**params)
|
27
24
|
super(params, ATTRIBUTES)
|
28
|
-
|
29
|
-
@profiles = []
|
30
|
-
@extensions = []
|
31
|
-
@value_sets = []
|
25
|
+
@resources_by_type ||= Hash.new { |hash, key| hash[key] = [] }
|
32
26
|
@examples = []
|
33
|
-
@search_params = []
|
34
27
|
end
|
35
28
|
|
36
29
|
def self.from_file(ig_path)
|
@@ -71,6 +64,9 @@ module Inferno
|
|
71
64
|
next
|
72
65
|
end
|
73
66
|
end
|
67
|
+
|
68
|
+
ig.id = extract_package_id(ig.ig_resource)
|
69
|
+
|
74
70
|
ig
|
75
71
|
end
|
76
72
|
|
@@ -91,6 +87,9 @@ module Inferno
|
|
91
87
|
next
|
92
88
|
end
|
93
89
|
end
|
90
|
+
|
91
|
+
ig.id = extract_package_id(ig.ig_resource)
|
92
|
+
|
94
93
|
ig
|
95
94
|
end
|
96
95
|
|
@@ -113,28 +112,51 @@ module Inferno
|
|
113
112
|
end
|
114
113
|
|
115
114
|
def handle_resource(resource, relative_path)
|
116
|
-
|
117
|
-
|
118
|
-
if resource.type == 'Extension'
|
119
|
-
extensions.push resource
|
120
|
-
else
|
121
|
-
profiles.push resource
|
122
|
-
end
|
123
|
-
when 'ValueSet'
|
124
|
-
value_sets.push resource
|
125
|
-
when 'SearchParameter'
|
126
|
-
search_params.push resource
|
127
|
-
when 'ImplementationGuide'
|
128
|
-
@id = extract_package_id(resource)
|
115
|
+
if relative_path.start_with? 'package/example'
|
116
|
+
examples << resource
|
129
117
|
else
|
130
|
-
|
118
|
+
resources_by_type[resource.resourceType] << resource
|
131
119
|
end
|
132
120
|
end
|
133
121
|
|
134
|
-
def extract_package_id(ig_resource)
|
122
|
+
def self.extract_package_id(ig_resource)
|
135
123
|
"#{ig_resource.id}##{ig_resource.version || 'current'}"
|
136
124
|
end
|
137
125
|
|
126
|
+
def profiles
|
127
|
+
resources_by_type['StructureDefinition'].filter { |sd| sd.type != 'Extension' }
|
128
|
+
end
|
129
|
+
|
130
|
+
def extensions
|
131
|
+
resources_by_type['StructureDefinition'].filter { |sd| sd.type == 'Extension' }
|
132
|
+
end
|
133
|
+
|
134
|
+
def capability_statement(mode = 'server')
|
135
|
+
resources_by_type['CapabilityStatement'].find do |capability_statement_resource|
|
136
|
+
capability_statement_resource.rest.any? { |r| r.mode == mode }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def ig_resource
|
141
|
+
resources_by_type['ImplementationGuide'].first
|
142
|
+
end
|
143
|
+
|
144
|
+
def profile_by_url(url)
|
145
|
+
profiles.find { |profile| profile.url == url }
|
146
|
+
end
|
147
|
+
|
148
|
+
def resource_for_profile(url)
|
149
|
+
profiles.find { |profile| profile.url == url }.type
|
150
|
+
end
|
151
|
+
|
152
|
+
def value_set_by_url(url)
|
153
|
+
resources_by_type['ValueSet'].find { |profile| profile.url == url }
|
154
|
+
end
|
155
|
+
|
156
|
+
def code_system_by_url(url)
|
157
|
+
resources_by_type['CodeSystem'].find { |system| system.url == url }
|
158
|
+
end
|
159
|
+
|
138
160
|
# @private
|
139
161
|
def add_self_to_repository
|
140
162
|
repository.insert(self)
|
@@ -43,7 +43,7 @@ module Inferno
|
|
43
43
|
|
44
44
|
# These are the attributes that can be directly copied when merging a
|
45
45
|
# runnable's input with an input configuration.
|
46
|
-
MERGEABLE_ATTRIBUTES = (ATTRIBUTES - [:type]).freeze
|
46
|
+
MERGEABLE_ATTRIBUTES = (ATTRIBUTES - [:type, :options]).freeze
|
47
47
|
|
48
48
|
def initialize(**params)
|
49
49
|
bad_params = params.keys - ATTRIBUTES
|
@@ -69,21 +69,27 @@ module Inferno
|
|
69
69
|
|
70
70
|
self.type = child_input.type if child_input.present? && child_input.type != 'text'
|
71
71
|
|
72
|
+
merge_options(primary_source: self, secondary_source: child_input)
|
73
|
+
|
72
74
|
self
|
73
75
|
end
|
74
76
|
|
75
77
|
# @private
|
76
78
|
# Merge this input with an input from a configuration. Fields defined in
|
77
79
|
# the configuration take precedence over those defined on this input.
|
78
|
-
def merge(other_input)
|
80
|
+
def merge(other_input, merge_all: false)
|
79
81
|
return self if other_input.nil?
|
80
82
|
|
81
|
-
|
83
|
+
attributes_to_merge = merge_all ? ATTRIBUTES : MERGEABLE_ATTRIBUTES
|
84
|
+
|
85
|
+
attributes_to_merge.each do |attribute|
|
82
86
|
merge_attribute(attribute, primary_source: other_input, secondary_source: self)
|
83
87
|
end
|
84
88
|
|
85
89
|
self.type = other_input.type if other_input.type.present? && other_input.type != 'text'
|
86
90
|
|
91
|
+
merge_options(primary_source: other_input, secondary_source: self)
|
92
|
+
|
87
93
|
self
|
88
94
|
end
|
89
95
|
|
@@ -103,6 +109,60 @@ module Inferno
|
|
103
109
|
send("#{attribute}=", value)
|
104
110
|
end
|
105
111
|
|
112
|
+
# @private
|
113
|
+
# Merge input options. This performs a normal merge for all options except
|
114
|
+
# for the "components" field, the members of which are individually merged
|
115
|
+
# by `merge_components`
|
116
|
+
# @param primary_source [Input]
|
117
|
+
# @param secondary_source [Input]
|
118
|
+
def merge_options(primary_source:, secondary_source:)
|
119
|
+
primary_options = primary_source.options.dup || {}
|
120
|
+
secondary_options = secondary_source.options.dup || {}
|
121
|
+
|
122
|
+
return if primary_options.blank? && secondary_options.blank?
|
123
|
+
|
124
|
+
primary_components = primary_options.delete(:components) || []
|
125
|
+
secondary_components = secondary_options.delete(:components) || []
|
126
|
+
|
127
|
+
send('options=', secondary_options.merge(primary_options))
|
128
|
+
|
129
|
+
merge_components(primary_components:, secondary_components:)
|
130
|
+
end
|
131
|
+
|
132
|
+
# @private
|
133
|
+
# Merge component hashes.
|
134
|
+
# @param primary_source [Input]
|
135
|
+
# @param secondary_source [Input]
|
136
|
+
def merge_components(primary_components:, secondary_components:) # rubocop:disable Metrics/CyclomaticComplexity
|
137
|
+
primary_components
|
138
|
+
.each { |component| component[:name] = component[:name].to_sym }
|
139
|
+
secondary_components
|
140
|
+
.each { |component| component[:name] = component[:name].to_sym }
|
141
|
+
|
142
|
+
return if primary_components.blank? && secondary_components.blank?
|
143
|
+
|
144
|
+
component_keys =
|
145
|
+
(primary_components + secondary_components)
|
146
|
+
.map { |component| component[:name] }
|
147
|
+
.uniq
|
148
|
+
|
149
|
+
merged_components = component_keys.map do |key|
|
150
|
+
primary_component = primary_components.find { |component| component[:name] == key }
|
151
|
+
secondary_component = secondary_components.find { |component| component[:name] == key }
|
152
|
+
|
153
|
+
next secondary_component if primary_component.blank?
|
154
|
+
|
155
|
+
next primary_component if secondary_component.blank?
|
156
|
+
|
157
|
+
Input.new(**secondary_component).merge(Input.new(**primary_component), merge_all: true).to_hash
|
158
|
+
end
|
159
|
+
|
160
|
+
merged_components.each { |component| component[:name] = component[:name].to_sym }
|
161
|
+
|
162
|
+
self.options ||= {}
|
163
|
+
self.options[:components] = merged_components
|
164
|
+
end
|
165
|
+
|
106
166
|
def to_hash
|
107
167
|
ATTRIBUTES.each_with_object({}) do |attribute, hash|
|
108
168
|
value = send(attribute)
|