inferno_core 0.6.1 → 0.6.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/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)
|