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.
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)