inferno_core 0.6.0 → 0.6.2
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 +1 -0
- data/lib/inferno/config/boot/presets.rb +1 -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/fhir_resource_validation.rb +5 -3
- data/lib/inferno/dsl/fhir_validation.rb +11 -5
- data/lib/inferno/dsl/must_support_metadata_extractor.rb +366 -0
- data/lib/inferno/dsl/primitive_type.rb +9 -0
- data/lib/inferno/dsl/value_extractor.rb +136 -0
- data/lib/inferno/entities/ig.rb +46 -24
- data/lib/inferno/public/bundle.js +16 -16
- data/lib/inferno/version.rb +1 -1
- data/spec/shared/test_kit_examples.rb +23 -1
- metadata +25 -2
@@ -0,0 +1,226 @@
|
|
1
|
+
require_relative 'primitive_type'
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module DSL
|
5
|
+
# The FHIRResourceNavigation module is used to pick values from a FHIR resource, based on a profile.
|
6
|
+
# Originally intended for use for verifying the presence of Must Support elements on a resource.
|
7
|
+
# This module expects pre-processed metadata defining the elements of the profile
|
8
|
+
# to be present in the attribute `metadata` in the including class.
|
9
|
+
# @see Inferno::DSL::MustSupportMetadataExtractor
|
10
|
+
module FHIRResourceNavigation
|
11
|
+
DAR_EXTENSION_URL = 'http://hl7.org/fhir/StructureDefinition/data-absent-reason'.freeze
|
12
|
+
PRIMITIVE_DATA_TYPES = FHIR::PRIMITIVES.keys
|
13
|
+
|
14
|
+
# Get a value from the given FHIR element(s) by walking the given path through the element.
|
15
|
+
# @param elements [FHIR::Model, Array<FHIR::Model>]
|
16
|
+
# @param path [String]
|
17
|
+
# @return [Array<FHIR::Model>]
|
18
|
+
def resolve_path(elements, path)
|
19
|
+
elements = Array.wrap(elements)
|
20
|
+
return elements if path.blank?
|
21
|
+
|
22
|
+
paths = path.split(/(?<!hl7)\./)
|
23
|
+
segment = paths.first
|
24
|
+
remaining_path = paths.drop(1).join('.')
|
25
|
+
|
26
|
+
elements.flat_map do |element|
|
27
|
+
child = get_next_value(element, segment)
|
28
|
+
resolve_path(child, remaining_path)
|
29
|
+
end.compact
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get a value from the given FHIR element(s), by navigating through the resource to the given path.
|
33
|
+
# Fields with a DataAbsentReason extension present may be selected if include_dar is true.
|
34
|
+
# To filter the resulting elements, a block may be passed in.
|
35
|
+
# @param given_element [FHIR::Model, Array<FHIR::Model>]
|
36
|
+
# @param path [String]
|
37
|
+
# @param include_dar [Boolean]
|
38
|
+
# @return [Array<FHIR::Model>]
|
39
|
+
def find_a_value_at(given_element, path, include_dar: false, &block)
|
40
|
+
return nil if given_element.nil?
|
41
|
+
|
42
|
+
elements = Array.wrap(given_element)
|
43
|
+
return find_in_elements(elements, include_dar:, &block) if path.empty?
|
44
|
+
|
45
|
+
path_segments = path.split(/(?<!hl7)\./)
|
46
|
+
|
47
|
+
segment = path_segments.shift.delete_suffix('[x]').gsub(/^class$/, 'local_class').gsub('[x]:', ':').to_sym
|
48
|
+
|
49
|
+
remaining_path = path_segments.join('.')
|
50
|
+
elements.each do |element|
|
51
|
+
child = get_next_value(element, segment)
|
52
|
+
element_found = find_a_value_at(child, remaining_path, include_dar:, &block)
|
53
|
+
return element_found if element_found.present? || element_found == false
|
54
|
+
end
|
55
|
+
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def find_in_elements(elements, include_dar: false, &)
|
60
|
+
unless include_dar
|
61
|
+
elements = elements.reject do |el|
|
62
|
+
el.respond_to?(:extension) && el.extension.any? { |ext| ext.url == DAR_EXTENSION_URL }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
return elements.find(&) if block_given?
|
67
|
+
|
68
|
+
elements.first
|
69
|
+
end
|
70
|
+
|
71
|
+
def get_next_value(element, property)
|
72
|
+
extension_url = property[/(?<=where\(url=').*(?='\))/]
|
73
|
+
if extension_url.present?
|
74
|
+
element.url == extension_url ? element : nil
|
75
|
+
elsif property.to_s.include?(':') && !property.to_s.include?('url')
|
76
|
+
find_slice_via_discriminator(element, property)
|
77
|
+
|
78
|
+
else
|
79
|
+
local_name = local_field_name(property)
|
80
|
+
value = element.send(local_name)
|
81
|
+
primitive_value = get_primitive_type_value(element, property, value)
|
82
|
+
primitive_value.present? ? primitive_value : value
|
83
|
+
end
|
84
|
+
rescue NoMethodError
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_primitive_type_value(element, property, value)
|
89
|
+
source_value = element.source_hash["_#{property}"]
|
90
|
+
|
91
|
+
return nil unless source_value.present?
|
92
|
+
|
93
|
+
primitive_value = PrimitiveType.new(source_value)
|
94
|
+
primitive_value.value = value
|
95
|
+
primitive_value
|
96
|
+
end
|
97
|
+
|
98
|
+
def local_field_name(field_name)
|
99
|
+
# fhir_models prepends fields whose names are reserved in ruby with "local_"
|
100
|
+
# This should be used before `x.send(field_name)`
|
101
|
+
if ['method', 'class'].include?(field_name.to_s)
|
102
|
+
"local_#{field_name}"
|
103
|
+
else
|
104
|
+
field_name
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def find_slice_via_discriminator(element, property)
|
109
|
+
return unless metadata.present?
|
110
|
+
|
111
|
+
element_name = local_field_name(property.to_s.split(':')[0])
|
112
|
+
slice_name = local_field_name(property.to_s.split(':')[1])
|
113
|
+
|
114
|
+
slice_by_name = metadata.must_supports[:slices].find { |slice| slice[:slice_name] == slice_name }
|
115
|
+
discriminator = slice_by_name[:discriminator]
|
116
|
+
slices = Array.wrap(element.send(element_name))
|
117
|
+
slices.find { |slice| matching_slice?(slice, discriminator) }
|
118
|
+
end
|
119
|
+
|
120
|
+
def matching_slice?(slice, discriminator)
|
121
|
+
case discriminator[:type]
|
122
|
+
when 'patternCodeableConcept'
|
123
|
+
matching_pattern_codeable_concept_slice?(slice, discriminator)
|
124
|
+
when 'patternCoding'
|
125
|
+
matching_pattern_coding_slice?(slice, discriminator)
|
126
|
+
when 'patternIdentifier'
|
127
|
+
matching_pattern_identifier_slice?(slice, discriminator)
|
128
|
+
when 'value'
|
129
|
+
matching_value_slice?(slice, discriminator)
|
130
|
+
when 'type'
|
131
|
+
matching_type_slice?(slice, discriminator)
|
132
|
+
when 'requiredBinding'
|
133
|
+
matching_required_binding_slice?(slice, discriminator)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def matching_pattern_codeable_concept_slice?(slice, discriminator)
|
138
|
+
slice_value = discriminator[:path].present? ? slice.send((discriminator[:path]).to_s)&.coding : slice.coding
|
139
|
+
slice_value&.any? do |coding|
|
140
|
+
coding.code == discriminator[:code] && coding.system == discriminator[:system]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def matching_pattern_coding_slice?(slice, discriminator)
|
145
|
+
slice_value = discriminator[:path].present? ? slice.send(discriminator[:path]) : slice
|
146
|
+
slice_value&.code == discriminator[:code] && slice_value&.system == discriminator[:system]
|
147
|
+
end
|
148
|
+
|
149
|
+
def matching_pattern_identifier_slice?(slice, discriminator)
|
150
|
+
slice.identifier.system == discriminator[:system]
|
151
|
+
end
|
152
|
+
|
153
|
+
def matching_value_slice?(slice, discriminator)
|
154
|
+
values = discriminator[:values].map { |value| value.merge(path: value[:path].split('.')) }
|
155
|
+
verify_slice_by_values(slice, values)
|
156
|
+
end
|
157
|
+
|
158
|
+
def matching_type_slice?(slice, discriminator)
|
159
|
+
case discriminator[:code]
|
160
|
+
when 'Date'
|
161
|
+
begin
|
162
|
+
Date.parse(slice)
|
163
|
+
rescue ArgumentError
|
164
|
+
false
|
165
|
+
end
|
166
|
+
when 'DateTime'
|
167
|
+
begin
|
168
|
+
DateTime.parse(slice)
|
169
|
+
rescue ArgumentError
|
170
|
+
false
|
171
|
+
end
|
172
|
+
when 'String'
|
173
|
+
slice.is_a? String
|
174
|
+
else
|
175
|
+
slice.is_a? FHIR.const_get(discriminator[:code])
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def matching_required_binding_slice?(slice, discriminator)
|
180
|
+
discriminator[:path].present? ? slice.send((discriminator[:path]).to_s).coding : slice.coding
|
181
|
+
slice_value { |coding| discriminator[:values].include?(coding.code) }
|
182
|
+
end
|
183
|
+
|
184
|
+
def verify_slice_by_values(element, value_definitions)
|
185
|
+
path_prefixes = value_definitions.map { |value_definition| value_definition[:path].first }.uniq
|
186
|
+
path_prefixes.all? do |path_prefix|
|
187
|
+
value_definitions_for_path =
|
188
|
+
value_definitions
|
189
|
+
.select { |value_definition| value_definition[:path].first == path_prefix }
|
190
|
+
.each { |value_definition| value_definition[:path].shift }
|
191
|
+
find_a_value_at(element, path_prefix) do |el_found|
|
192
|
+
current_and_child_values_match?(el_found, value_definitions_for_path)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def current_and_child_values_match?(el_found, value_definitions_for_path)
|
198
|
+
child_element_value_definitions, current_element_value_definitions =
|
199
|
+
value_definitions_for_path.partition { |value_definition| value_definition[:path].present? }
|
200
|
+
|
201
|
+
current_element_values_match =
|
202
|
+
current_element_value_definitions
|
203
|
+
.all? { |value_definition| value_definition[:value] == el_found }
|
204
|
+
|
205
|
+
child_element_values_match =
|
206
|
+
if child_element_value_definitions.present?
|
207
|
+
verify_slice_by_values(el_found, child_element_value_definitions)
|
208
|
+
else
|
209
|
+
true
|
210
|
+
end
|
211
|
+
current_element_values_match && child_element_values_match
|
212
|
+
end
|
213
|
+
|
214
|
+
def flatten_bundles(resources)
|
215
|
+
resources.flat_map do |resource|
|
216
|
+
if resource&.resourceType == 'Bundle'
|
217
|
+
# Recursive to consider that Bundles may contain Bundles
|
218
|
+
flatten_bundles(resource.entry.map(&:resource))
|
219
|
+
else
|
220
|
+
resource
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
@@ -157,7 +157,7 @@ module Inferno
|
|
157
157
|
end
|
158
158
|
|
159
159
|
# @see Inferno::DSL::FHIRResourceValidation#resource_is_valid?
|
160
|
-
def resource_is_valid?(resource, profile_url, runnable)
|
160
|
+
def resource_is_valid?(resource, profile_url, runnable, add_messages_to_runnable: true) # rubocop:disable Metrics/CyclomaticComplexity
|
161
161
|
profile_url ||= FHIR::Definitions.resource_definition(resource.resourceType).url
|
162
162
|
|
163
163
|
begin
|
@@ -173,8 +173,10 @@ module Inferno
|
|
173
173
|
|
174
174
|
message_hashes = message_hashes_from_outcome(outcome, resource, profile_url)
|
175
175
|
|
176
|
-
|
177
|
-
|
176
|
+
if add_messages_to_runnable
|
177
|
+
message_hashes
|
178
|
+
.each { |message_hash| runnable.add_message(message_hash[:type], message_hash[:message]) }
|
179
|
+
end
|
178
180
|
|
179
181
|
unless response.status == 200
|
180
182
|
raise Inferno::Exceptions::ErrorInValidatorException,
|
@@ -30,9 +30,13 @@ module Inferno
|
|
30
30
|
# @param resource [FHIR::Model]
|
31
31
|
# @param profile_url [String]
|
32
32
|
# @param validator [Symbol] the name of the validator to use
|
33
|
+
# @param add_messages_to_runnable [Boolean] whether to add validation messages to runnable or not
|
33
34
|
# @return [Boolean] whether the resource is valid
|
34
|
-
def resource_is_valid?(
|
35
|
-
|
35
|
+
def resource_is_valid?(
|
36
|
+
resource: self.resource, profile_url: nil,
|
37
|
+
validator: :default, add_messages_to_runnable: true
|
38
|
+
)
|
39
|
+
find_validator(validator).resource_is_valid?(resource, profile_url, self, add_messages_to_runnable:)
|
36
40
|
end
|
37
41
|
|
38
42
|
# Find a particular validator. Looks through a runnable's parents up to
|
@@ -113,7 +117,7 @@ module Inferno
|
|
113
117
|
end
|
114
118
|
|
115
119
|
# @see Inferno::DSL::FHIRValidation#resource_is_valid?
|
116
|
-
def resource_is_valid?(resource, profile_url, runnable)
|
120
|
+
def resource_is_valid?(resource, profile_url, runnable, add_messages_to_runnable: true) # rubocop:disable Metrics/CyclomaticComplexity
|
117
121
|
profile_url ||= FHIR::Definitions.resource_definition(resource.resourceType).url
|
118
122
|
|
119
123
|
begin
|
@@ -128,8 +132,10 @@ module Inferno
|
|
128
132
|
|
129
133
|
message_hashes = message_hashes_from_outcome(outcome, resource, profile_url)
|
130
134
|
|
131
|
-
|
132
|
-
|
135
|
+
if add_messages_to_runnable
|
136
|
+
message_hashes
|
137
|
+
.each { |message_hash| runnable.add_message(message_hash[:type], message_hash[:message]) }
|
138
|
+
end
|
133
139
|
|
134
140
|
unless response.status == 200
|
135
141
|
raise Inferno::Exceptions::ErrorInValidatorException,
|
@@ -0,0 +1,366 @@
|
|
1
|
+
require_relative 'value_extractor'
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module DSL
|
5
|
+
# The MustSupportMetadataExtractor takes a StructureDefinition and parses it into a hash-based metadata
|
6
|
+
# that simplifies checking for MustSupport elements.
|
7
|
+
# MustSupport elements may be either plain elements, extensions, or slices.
|
8
|
+
# This logic was originally developed for the US Core Test Kit and has been migrated into Inferno core.
|
9
|
+
class MustSupportMetadataExtractor
|
10
|
+
attr_accessor :profile_elements, :profile, :resource, :ig_resources, :requirement_extension_url
|
11
|
+
|
12
|
+
# Construct a new extractor
|
13
|
+
# @param profile_elements [Array<FHIR::ElementDefinition>] the elements of the profile to consider,
|
14
|
+
# ie, profile.snapshot.element
|
15
|
+
# @param profile [FHIR::StructureDefinition] the profile to parse
|
16
|
+
# @param resource [String] the resourceType that the profile applies to, ie, profile.type
|
17
|
+
# @param ig_resources [Inferno::Entities::IG]
|
18
|
+
# @param requirement_extension_url [String] the URL of an extension to flag elements as required even if not MS
|
19
|
+
def initialize(profile_elements, profile, resource, ig_resources, requirement_extension_url = nil)
|
20
|
+
self.profile_elements = profile_elements
|
21
|
+
self.profile = profile
|
22
|
+
self.resource = resource
|
23
|
+
self.ig_resources = ig_resources
|
24
|
+
self.requirement_extension_url = requirement_extension_url
|
25
|
+
end
|
26
|
+
|
27
|
+
# Retrieval method for the must support metadata
|
28
|
+
# @return [Hash]
|
29
|
+
def must_supports
|
30
|
+
@must_supports ||= {
|
31
|
+
extensions: must_support_extensions,
|
32
|
+
slices: must_support_slices,
|
33
|
+
elements: must_support_elements
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def by_requirement_extension_only?(element)
|
38
|
+
requirement_extension_url && !element.mustSupport &&
|
39
|
+
element.extension.any? do |extension|
|
40
|
+
extension.url == requirement_extension_url && extension.valueBoolean
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def all_must_support_elements
|
45
|
+
profile_elements.select { |element| element.mustSupport || by_requirement_extension_only?(element) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def must_support_extension_elements
|
49
|
+
all_must_support_elements.select { |element| element.path.end_with? 'extension' }
|
50
|
+
end
|
51
|
+
|
52
|
+
def must_support_extensions
|
53
|
+
must_support_extension_elements.map do |element|
|
54
|
+
{
|
55
|
+
id: element.id,
|
56
|
+
path: element.path.gsub("#{resource}.", ''),
|
57
|
+
url: element.type.first.profile.first
|
58
|
+
}.tap do |metadata|
|
59
|
+
metadata[:by_requirement_extension_only] = true if by_requirement_extension_only?(element)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def must_support_slice_elements
|
65
|
+
all_must_support_elements.select do |element|
|
66
|
+
!element.path.end_with?('extension') && element.sliceName.present?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def sliced_element(slice)
|
71
|
+
profile_elements.find do |element|
|
72
|
+
element.id == slice.path || element.id == slice.id.sub(":#{slice.sliceName}", '')
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def discriminators(slice)
|
77
|
+
slice&.slicing&.discriminator
|
78
|
+
end
|
79
|
+
|
80
|
+
def find_element_by_discriminator_path(current_element, discriminator_path)
|
81
|
+
if discriminator_path.present?
|
82
|
+
profile_elements.find { |element| element.id == "#{current_element.id}.#{discriminator_path}" } ||
|
83
|
+
profile_elements.find { |element| element.id == "#{current_element.path}.#{discriminator_path}" }
|
84
|
+
else
|
85
|
+
current_element
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def save_pattern_slice(pattern_element, discriminator_path, metadata)
|
90
|
+
if pattern_element.patternCodeableConcept
|
91
|
+
{
|
92
|
+
type: 'patternCodeableConcept',
|
93
|
+
path: discriminator_path,
|
94
|
+
code: pattern_element.patternCodeableConcept.coding.first.code,
|
95
|
+
system: pattern_element.patternCodeableConcept.coding.first.system
|
96
|
+
}
|
97
|
+
elsif pattern_element.patternCoding
|
98
|
+
{
|
99
|
+
type: 'patternCoding',
|
100
|
+
path: discriminator_path,
|
101
|
+
code: pattern_element.patternCoding.code,
|
102
|
+
system: pattern_element.patternCoding.system
|
103
|
+
}
|
104
|
+
elsif pattern_element.patternIdentifier
|
105
|
+
{
|
106
|
+
type: 'patternIdentifier',
|
107
|
+
path: discriminator_path,
|
108
|
+
system: pattern_element.patternIdentifier.system
|
109
|
+
}
|
110
|
+
elsif required_binding_pattern?(pattern_element)
|
111
|
+
{
|
112
|
+
type: 'requiredBinding',
|
113
|
+
path: discriminator_path,
|
114
|
+
values: extract_required_binding_values(pattern_element, metadata)
|
115
|
+
}
|
116
|
+
else
|
117
|
+
# prevent errors in case an IG does something different
|
118
|
+
{
|
119
|
+
type: 'unsupported',
|
120
|
+
path: discriminator_path
|
121
|
+
}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def required_binding_pattern?(pattern_element)
|
126
|
+
pattern_element.binding&.strength == 'required' && pattern_element.binding&.valueSet
|
127
|
+
end
|
128
|
+
|
129
|
+
def extract_required_binding_values(pattern_element, metadata)
|
130
|
+
value_extractor = ValueExtractor.new(ig_resources, resource, profile_elements)
|
131
|
+
|
132
|
+
value_extractor.codings_from_value_set_binding(pattern_element).presence ||
|
133
|
+
value_extractor.values_from_resource_metadata([metadata[:path]]).presence || []
|
134
|
+
end
|
135
|
+
|
136
|
+
def must_support_type_slice_elements
|
137
|
+
must_support_slice_elements.select do |element|
|
138
|
+
discriminators(sliced_element(element))&.first&.type == 'type'
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def discriminator_path(discriminator)
|
143
|
+
if discriminator.path == '$this'
|
144
|
+
''
|
145
|
+
elsif discriminator.path.start_with?('$this.')
|
146
|
+
discriminator.path[6..]
|
147
|
+
else
|
148
|
+
discriminator.path
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def type_slices
|
153
|
+
must_support_type_slice_elements.map do |current_element|
|
154
|
+
discriminator = discriminators(sliced_element(current_element)).first
|
155
|
+
type_path = discriminator_path(discriminator)
|
156
|
+
type_element = find_element_by_discriminator_path(current_element, type_path)
|
157
|
+
|
158
|
+
type_code = type_element.type.first.code
|
159
|
+
|
160
|
+
{
|
161
|
+
slice_id: current_element.id,
|
162
|
+
slice_name: current_element.sliceName,
|
163
|
+
path: current_element.path.gsub("#{resource}.", ''),
|
164
|
+
discriminator: {
|
165
|
+
type: 'type',
|
166
|
+
code: type_code.upcase_first
|
167
|
+
}
|
168
|
+
}.tap do |metadata|
|
169
|
+
metadata[:by_requirement_extension_only] = true if by_requirement_extension_only?(current_element)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def must_support_value_slice_elements
|
175
|
+
must_support_slice_elements.select do |element|
|
176
|
+
# discriminator type 'pattern' is deprecated in FHIR R5 and made equivalent to 'value'
|
177
|
+
['value', 'pattern'].include?(discriminators(sliced_element(element))&.first&.type)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def value_slices # rubocop:disable Metrics/CyclomaticComplexity
|
182
|
+
must_support_value_slice_elements.map do |current_element|
|
183
|
+
{
|
184
|
+
slice_id: current_element.id,
|
185
|
+
slice_name: current_element.sliceName,
|
186
|
+
path: current_element.path.gsub("#{resource}.", '')
|
187
|
+
}.tap do |metadata|
|
188
|
+
fixed_values = []
|
189
|
+
pattern_value = {}
|
190
|
+
|
191
|
+
element_discriminators = discriminators(sliced_element(current_element))
|
192
|
+
|
193
|
+
element_discriminators.each do |discriminator|
|
194
|
+
discriminator_path = discriminator_path(discriminator)
|
195
|
+
pattern_element = find_element_by_discriminator_path(current_element, discriminator_path)
|
196
|
+
|
197
|
+
# This is a workaround for a known version of a profile that has a bad discriminator:
|
198
|
+
# the discriminator refers to a nested field within a CodeableConcept,
|
199
|
+
# but the profile doesn't contain an element definition for it, so there's no way to
|
200
|
+
# define a fixed value on the element to define the slice.
|
201
|
+
# In this instance the element has a second (good) discriminator on the CodeableConcept field itself,
|
202
|
+
# and in subsequent versions of the profile, the bad discriminator was removed.
|
203
|
+
next if pattern_element.nil? && element_discriminators.length > 1
|
204
|
+
|
205
|
+
if pattern_element.fixed.present?
|
206
|
+
fixed_values << {
|
207
|
+
path: discriminator_path,
|
208
|
+
value: pattern_element.fixed
|
209
|
+
}
|
210
|
+
elsif pattern_value.present?
|
211
|
+
raise StandardError, "Found more than one pattern slices for the same element #{pattern_element}."
|
212
|
+
else
|
213
|
+
pattern_value = save_pattern_slice(pattern_element, discriminator_path, metadata)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
if fixed_values.present?
|
218
|
+
metadata[:discriminator] = {
|
219
|
+
type: 'value',
|
220
|
+
values: fixed_values
|
221
|
+
}
|
222
|
+
elsif pattern_value.present?
|
223
|
+
metadata[:discriminator] = pattern_value
|
224
|
+
end
|
225
|
+
|
226
|
+
metadata[:by_requirement_extension_only] = true if by_requirement_extension_only?(current_element)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def must_support_slices
|
232
|
+
type_slices + value_slices
|
233
|
+
end
|
234
|
+
|
235
|
+
def plain_must_support_elements
|
236
|
+
all_must_support_elements - must_support_extension_elements - must_support_slice_elements
|
237
|
+
end
|
238
|
+
|
239
|
+
def element_part_of_slice_discrimination?(element)
|
240
|
+
must_support_slice_elements.any? { |ms_slice| element.id.include?(ms_slice.id) }
|
241
|
+
end
|
242
|
+
|
243
|
+
def handle_fixed_values(metadata, element)
|
244
|
+
if element.fixed.present?
|
245
|
+
metadata[:fixed_value] = element.fixed
|
246
|
+
elsif element.patternCodeableConcept.present? && !element_part_of_slice_discrimination?(element)
|
247
|
+
metadata[:fixed_value] = element.patternCodeableConcept.coding.first.code
|
248
|
+
metadata[:path] += '.coding.code'
|
249
|
+
elsif element.fixedCode.present?
|
250
|
+
metadata[:fixed_value] = element.fixedCode
|
251
|
+
elsif element.patternIdentifier.present? && !element_part_of_slice_discrimination?(element)
|
252
|
+
metadata[:fixed_value] = element.patternIdentifier.system
|
253
|
+
metadata[:path] += '.system'
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def type_must_support_extension?(extensions)
|
258
|
+
extensions&.any? do |extension|
|
259
|
+
extension.url == 'http://hl7.org/fhir/StructureDefinition/elementdefinition-type-must-support' &&
|
260
|
+
extension.valueBoolean
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def save_type_code?(type)
|
265
|
+
type.code == 'Reference'
|
266
|
+
end
|
267
|
+
|
268
|
+
def get_type_must_support_metadata(current_metadata, current_element)
|
269
|
+
current_element.type.map do |type|
|
270
|
+
next unless type_must_support_extension?(type.extension)
|
271
|
+
|
272
|
+
metadata =
|
273
|
+
{
|
274
|
+
path: "#{current_metadata[:path].delete_suffix('[x]')}#{type.code.upcase_first}",
|
275
|
+
original_path: current_metadata[:path]
|
276
|
+
}
|
277
|
+
metadata[:types] = [type.code] if save_type_code?(type)
|
278
|
+
handle_type_must_support_target_profiles(type, metadata) if type.code == 'Reference'
|
279
|
+
|
280
|
+
metadata
|
281
|
+
end.compact
|
282
|
+
end
|
283
|
+
|
284
|
+
def handle_type_must_support_target_profiles(type, metadata)
|
285
|
+
target_profiles = extract_target_profiles(type)
|
286
|
+
|
287
|
+
# remove target_profile for FHIR Base resource type.
|
288
|
+
target_profiles.delete_if { |reference| reference.start_with?('http://hl7.org/fhir/StructureDefinition') }
|
289
|
+
metadata[:target_profiles] = target_profiles if target_profiles.present?
|
290
|
+
end
|
291
|
+
|
292
|
+
def extract_target_profiles(type)
|
293
|
+
target_profiles = []
|
294
|
+
|
295
|
+
if type.targetProfile&.length == 1
|
296
|
+
target_profiles << type.targetProfile.first
|
297
|
+
else
|
298
|
+
type.source_hash['_targetProfile']&.each_with_index do |hash, index|
|
299
|
+
if hash.present?
|
300
|
+
element = FHIR::Element.new(hash)
|
301
|
+
target_profiles << type.targetProfile[index] if type_must_support_extension?(element.extension)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
target_profiles
|
307
|
+
end
|
308
|
+
|
309
|
+
def handle_choice_type_in_sliced_element(current_metadata, must_support_elements_metadata)
|
310
|
+
choice_element_metadata = must_support_elements_metadata.find do |metadata|
|
311
|
+
metadata[:original_path].present? &&
|
312
|
+
current_metadata[:path].include?(metadata[:original_path])
|
313
|
+
end
|
314
|
+
|
315
|
+
return unless choice_element_metadata.present?
|
316
|
+
|
317
|
+
current_metadata[:original_path] = current_metadata[:path]
|
318
|
+
current_metadata[:path] =
|
319
|
+
current_metadata[:path].sub(choice_element_metadata[:original_path], choice_element_metadata[:path])
|
320
|
+
end
|
321
|
+
|
322
|
+
def must_support_elements
|
323
|
+
must_support_elements_metadata = []
|
324
|
+
plain_must_support_elements.each do |current_element|
|
325
|
+
current_metadata = {
|
326
|
+
path: current_element.id.gsub("#{resource}.", '')
|
327
|
+
}
|
328
|
+
current_metadata[:by_requirement_extension_only] = true if by_requirement_extension_only?(current_element)
|
329
|
+
|
330
|
+
type_must_support_metadata = get_type_must_support_metadata(current_metadata, current_element)
|
331
|
+
|
332
|
+
if type_must_support_metadata.any?
|
333
|
+
must_support_elements_metadata.concat(type_must_support_metadata)
|
334
|
+
else
|
335
|
+
handle_choice_type_in_sliced_element(current_metadata, must_support_elements_metadata)
|
336
|
+
|
337
|
+
supported_types = extract_supported_types(current_element)
|
338
|
+
current_metadata[:types] = supported_types if supported_types.present?
|
339
|
+
|
340
|
+
if current_element.type.first&.code == 'Reference'
|
341
|
+
handle_type_must_support_target_profiles(current_element.type.first,
|
342
|
+
current_metadata)
|
343
|
+
end
|
344
|
+
|
345
|
+
handle_fixed_values(current_metadata, current_element)
|
346
|
+
|
347
|
+
remove_conflicting_metadata_without_fixed_value(must_support_elements_metadata, current_metadata)
|
348
|
+
|
349
|
+
must_support_elements_metadata << current_metadata
|
350
|
+
end
|
351
|
+
end
|
352
|
+
must_support_elements_metadata.uniq
|
353
|
+
end
|
354
|
+
|
355
|
+
def extract_supported_types(current_element)
|
356
|
+
current_element.type.select { |type| save_type_code?(type) }.map(&:code)
|
357
|
+
end
|
358
|
+
|
359
|
+
def remove_conflicting_metadata_without_fixed_value(must_support_elements_metadata, current_metadata)
|
360
|
+
must_support_elements_metadata.delete_if do |metadata|
|
361
|
+
metadata[:path] == current_metadata[:path] && metadata[:fixed_value].blank?
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|