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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/inferno/apps/cli/evaluate.rb +22 -12
  3. data/lib/inferno/apps/cli/templates/Dockerfile.tt +0 -1
  4. data/lib/inferno/apps/cli/templates/README.md.tt +10 -0
  5. data/lib/inferno/apps/cli/templates/docs/Overview.md +7 -0
  6. data/lib/inferno/apps/cli/templates/docs/README.md.tt +10 -0
  7. data/lib/inferno/apps/cli/templates/docs/_Footer.md +5 -0
  8. data/lib/inferno/apps/cli/templates/docs/_Sidebar.md +5 -0
  9. data/lib/inferno/config/boot/presets.rb +1 -1
  10. data/lib/inferno/dsl/auth_info.rb +87 -1
  11. data/lib/inferno/dsl/configurable.rb +14 -1
  12. data/lib/inferno/dsl/fhir_client.rb +66 -0
  13. data/lib/inferno/dsl/fhir_evaluation/evaluation_context.rb +4 -2
  14. data/lib/inferno/dsl/fhir_evaluation/evaluator.rb +8 -3
  15. data/lib/inferno/dsl/fhir_evaluation/profile_conformance_helper.rb +66 -0
  16. data/lib/inferno/dsl/fhir_evaluation/reference_extractor.rb +61 -0
  17. data/lib/inferno/dsl/fhir_evaluation/rules/all_must_supports_present.rb +379 -0
  18. data/lib/inferno/dsl/fhir_evaluation/rules/all_references_resolve.rb +53 -0
  19. data/lib/inferno/dsl/fhir_evaluation/rules/all_resources_reachable.rb +63 -0
  20. data/lib/inferno/dsl/fhir_resource_navigation.rb +226 -0
  21. data/lib/inferno/dsl/input_output_handling.rb +1 -0
  22. data/lib/inferno/dsl/must_support_metadata_extractor.rb +366 -0
  23. data/lib/inferno/dsl/primitive_type.rb +9 -0
  24. data/lib/inferno/dsl/runnable.rb +13 -1
  25. data/lib/inferno/dsl/value_extractor.rb +136 -0
  26. data/lib/inferno/dsl.rb +1 -0
  27. data/lib/inferno/entities/ig.rb +46 -24
  28. data/lib/inferno/entities/input.rb +63 -3
  29. data/lib/inferno/public/bundle.js +16 -16
  30. data/lib/inferno/repositories/session_data.rb +2 -0
  31. data/lib/inferno/version.rb +1 -1
  32. data/spec/runnable_context.rb +8 -5
  33. data/spec/shared/test_kit_examples.rb +23 -1
  34. 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
@@ -1,4 +1,5 @@
1
1
  require_relative 'dsl/assertions'
2
+ require_relative 'dsl/auth_info'
2
3
  require_relative 'dsl/fhir_client'
3
4
  require_relative 'dsl/fhir_validation'
4
5
  require_relative 'dsl/fhir_evaluation/evaluator'
@@ -14,10 +14,7 @@ module Inferno
14
14
  class IG < Entity
15
15
  ATTRIBUTES = [
16
16
  :id,
17
- :profiles,
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
- case resource.resourceType
117
- when 'StructureDefinition'
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
- examples.push(resource) if relative_path.start_with? 'package/example'
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
- MERGEABLE_ATTRIBUTES.each do |attribute|
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)