inferno_core 0.6.1 → 0.6.2
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/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/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 +11 -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
|
@@ -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
|