Skip to content

Commit 18cf131

Browse files
enhance FHIR resource navigation and discriminator path resolution for complex slice types
1 parent ed9d69c commit 18cf131

File tree

6 files changed

+796
-58
lines changed

6 files changed

+796
-58
lines changed

lib/inferno/dsl/fhir_resource_navigation.rb

Lines changed: 143 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def resolve_path(elements, path)
2424
elements = Array.wrap(elements)
2525
return elements if path.blank?
2626

27-
paths = path.split(/(?<!hl7)\./)
27+
paths = path_segments(path)
2828
segment = paths.first
2929
remaining_path = paths.drop(1).join('.')
3030

@@ -49,9 +49,9 @@ def find_a_value_at(given_element, path, include_dar: false, &block)
4949
elements = Array.wrap(given_element)
5050
return find_in_elements(elements, include_dar:, &block) if path.empty?
5151

52-
path_segments = path.split(/(?<!hl7)\./)
52+
path_segments = path_segments(path)
5353

54-
segment = path_segments.shift.delete_suffix('[x]').gsub(/^class$/, 'local_class').gsub('[x]:', ':').to_sym
54+
segment = path_segments.shift
5555

5656
remaining_path = path_segments.join('.')
5757
elements.each do |element|
@@ -78,25 +78,75 @@ def find_in_elements(elements, include_dar: false, &)
7878

7979
# @private
8080
def get_next_value(element, property)
81+
property = property.to_s
8182
extension_url = property[/(?<=where\(url=').*(?='\))/]
82-
if extension_url.present?
83-
element.url == extension_url ? element : nil
84-
elsif property.to_s.include?(':') && !property.to_s.include?('url')
85-
find_slice_via_discriminator(element, property)
83+
return extension_filter_value(element, extension_url) if extension_url.present?
84+
return typed_choice_value(element, property) if explicit_choice_path?(property)
85+
return populated_choice_value(element, property) if implicit_choice_path?(property)
86+
return find_slice_via_discriminator(element, property) if slice_path?(property)
8687

87-
else
88-
local_name = local_field_name(property)
89-
value = element.send(local_name)
90-
primitive_value = get_primitive_type_value(element, property, value)
91-
primitive_value.present? ? primitive_value : value
92-
end
88+
field_value(element, property)
9389
rescue NoMethodError
9490
nil
9591
end
9692

93+
# @private
94+
def extension_filter_value(element, extension_url)
95+
element.url == extension_url ? element : nil
96+
end
97+
98+
# @private
99+
def explicit_choice_path?(property)
100+
property.include?('[x]:')
101+
end
102+
103+
# @private
104+
def implicit_choice_path?(property)
105+
property.end_with?('[x]')
106+
end
107+
108+
# @private
109+
def slice_path?(property)
110+
property.include?(':') && !property.include?('url')
111+
end
112+
113+
# @private
114+
def typed_choice_value(element, property)
115+
_choice_path, typed_field = property.split(':', 2)
116+
field_value(element, typed_field)
117+
end
118+
119+
# @private
120+
def populated_choice_value(element, property)
121+
choice_prefix = property.delete_suffix('[x]')
122+
populated_field =
123+
Array.wrap(element.to_hash&.keys)
124+
.map(&:to_s)
125+
.find do |field_name|
126+
field_name.start_with?(choice_prefix) && value_present?(field_value(element, field_name))
127+
end
128+
129+
return nil if populated_field.blank?
130+
131+
field_value(element, populated_field)
132+
end
133+
134+
# @private
135+
def field_value(element, field_name)
136+
local_name = local_field_name(field_name)
137+
value = element.send(local_name)
138+
primitive_value = get_primitive_type_value(element, field_name, value)
139+
primitive_value.present? ? primitive_value : value
140+
end
141+
97142
# @private
98143
def get_primitive_type_value(element, property, value)
99-
source_value = element.source_hash["_#{property}"]
144+
return nil unless element.respond_to?(:source_hash)
145+
146+
source_hash = element.source_hash
147+
return nil unless source_hash.present?
148+
149+
source_value = source_hash["_#{property}"]
100150

101151
return nil unless source_value.present?
102152

@@ -116,6 +166,47 @@ def local_field_name(field_name)
116166
end
117167
end
118168

169+
# @private
170+
def path_segments(path)
171+
state = { current_segment: +'', segments: [], parentheses_depth: 0, in_quotes: false }
172+
path.each_char { |char| update_path_segment_state(state, char) }
173+
state[:segments] << state[:current_segment] unless state[:current_segment].empty?
174+
state[:segments]
175+
end
176+
177+
# @private
178+
def update_path_segment_state(state, char)
179+
case char
180+
when "'"
181+
state[:current_segment] << char
182+
state[:in_quotes] = !state[:in_quotes]
183+
when '('
184+
append_path_character(state, char, depth_change: 1)
185+
when ')'
186+
append_path_character(state, char, depth_change: -1)
187+
when '.'
188+
split_path_segment_or_append(state, char)
189+
else
190+
state[:current_segment] << char
191+
end
192+
end
193+
194+
# @private
195+
def append_path_character(state, char, depth_change:)
196+
state[:current_segment] << char
197+
state[:parentheses_depth] += depth_change unless state[:in_quotes]
198+
end
199+
200+
# @private
201+
def split_path_segment_or_append(state, char)
202+
if state[:parentheses_depth].zero? && !state[:in_quotes]
203+
state[:segments] << state[:current_segment].dup
204+
state[:current_segment].clear
205+
else
206+
state[:current_segment] << char
207+
end
208+
end
209+
119210
# @private
120211
def find_slice_via_discriminator(element, property)
121212
return unless metadata.present?
@@ -170,7 +261,7 @@ def matching_pattern_identifier_slice?(slice, discriminator)
170261

171262
# @private
172263
def matching_value_slice?(slice, discriminator)
173-
values = discriminator[:values].map { |value| value.merge(path: value[:path].split('.')) }
264+
values = discriminator[:values].map { |value| value.merge(path: path_segments(value[:path])) }
174265
verify_slice_by_values(slice, values)
175266
end
176267

@@ -200,15 +291,31 @@ def matching_type_slice?(slice, discriminator)
200291

201292
# @private
202293
def matching_required_binding_slice?(slice, discriminator)
203-
slice_coding = discriminator[:path].present? ? slice.send((discriminator[:path]).to_s).coding : slice.coding
204-
slice_coding.any? do |coding|
205-
discriminator[:values].any? do |value|
206-
case value
207-
when String
208-
value == coding.code
209-
when Hash
210-
value[:system] == coding.system && value[:code] == coding.code
211-
end
294+
slice_coding = required_binding_codings(slice, discriminator)
295+
return slice_coding.present? if discriminator[:values].blank?
296+
297+
slice_coding.any? { |coding| required_binding_value_match?(coding, discriminator[:values]) }
298+
end
299+
300+
# @private
301+
def required_binding_codings(slice, discriminator)
302+
if discriminator[:path].present?
303+
Array.wrap(resolve_path(slice, discriminator[:path])).flat_map { |value| Array.wrap(value&.coding) }
304+
elsif slice.is_a?(FHIR::Coding)
305+
[slice]
306+
else
307+
Array.wrap(slice.coding)
308+
end
309+
end
310+
311+
# @private
312+
def required_binding_value_match?(coding, values)
313+
values.any? do |value|
314+
case value
315+
when String
316+
value == coding.code
317+
when Hash
318+
value[:system] == coding.system && value[:code] == coding.code
212319
end
213320
end
214321
end
@@ -221,7 +328,7 @@ def verify_slice_by_values(element, value_definitions)
221328
value_definitions
222329
.select { |value_definition| value_definition[:path].first == path_prefix }
223330
.each { |value_definition| value_definition[:path].shift }
224-
find_a_value_at(element, path_prefix) do |el_found|
331+
value_at_path_matches?(element, path_prefix) do |el_found|
225332
current_and_child_values_match?(el_found, value_definitions_for_path)
226333
end
227334
end
@@ -245,6 +352,17 @@ def current_and_child_values_match?(el_found, value_definitions_for_path)
245352
current_element_values_match && child_element_values_match
246353
end
247354

355+
# @private
356+
def value_at_path_matches?(element, path, include_dar: false, &)
357+
value_found = find_a_value_at(element, path, include_dar:, &)
358+
value_present?(value_found)
359+
end
360+
361+
# @private
362+
def value_present?(value)
363+
value.present? || value == false
364+
end
365+
248366
# @private
249367
def flatten_bundles(resources)
250368
resources.flat_map do |resource|

lib/inferno/dsl/must_support_assessment.rb

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -237,12 +237,10 @@ def find_missing_elements(resources, must_support_elements)
237237
end
238238

239239
def resource_populates_element?(resource, element_definition)
240-
path = element_definition[:path]
240+
raw_path = element_definition[:path]
241+
path = navigation_compatible_must_support_path(raw_path)
241242

242-
# handle MustSupport element under extension: Ex: extension:supporting-info.value[x]
243-
resource, path = process_must_support_element_in_extension(resource, path) if path.start_with?('extension:')
244-
245-
ms_extension_urls = must_support_extensions.select { |ex| ex[:path] == "#{path}.extension" }
243+
ms_extension_urls = must_support_extensions.select { |ex| ex[:path] == "#{raw_path}.extension" }
246244
.map { |ex| ex[:url] }
247245
include_dar = normalized_extension_urls(ms_extension_urls).include?(FHIRResourceNavigation::DAR_EXTENSION_URL)
248246

@@ -254,6 +252,47 @@ def resource_populates_element?(resource, element_definition)
254252
value_found.present? || value_found == false
255253
end
256254

255+
def navigation_compatible_must_support_path(path)
256+
logical_segments = []
257+
258+
path_segments(path).map do |segment|
259+
normalized_segment = normalized_must_support_path_segment(segment, logical_segments)
260+
logical_segments << segment.split(':').first
261+
normalized_segment
262+
end.join('.')
263+
end
264+
265+
def normalized_must_support_path_segment(segment, logical_segments)
266+
extension_type, extension_name = segment.match(/\A(modifierExtension|extension):(.+)\z/)&.captures
267+
return segment if extension_type.blank?
268+
269+
extension_path = [logical_segments.join('.'), extension_type].reject(&:blank?).join('.')
270+
extension_definition = must_support_extension_definition(extension_path, extension_type, extension_name)
271+
return segment if extension_definition.blank?
272+
273+
"#{extension_type}.where(url='#{normalized_extension_url(extension_definition[:url])}')"
274+
end
275+
276+
def must_support_extension_definition(extension_path, extension_type, extension_name)
277+
suffix = "#{extension_type}:#{extension_name}"
278+
path_matching_extensions = must_support_extensions.select { |definition| definition[:path] == extension_path }
279+
280+
extension_definition_candidates(path_matching_extensions).each do |definitions, matcher|
281+
match = definitions.find { |definition| definition[:id].public_send(matcher, suffix) }
282+
return match if match.present?
283+
end
284+
285+
nil
286+
end
287+
288+
def extension_definition_candidates(path_matching_extensions)
289+
[
290+
[path_matching_extensions, :end_with?],
291+
[path_matching_extensions, :include?],
292+
[must_support_extensions, :end_with?]
293+
]
294+
end
295+
257296
def process_must_support_element_in_extension(resource, path)
258297
return [resource, path] unless path.start_with?('extension:')
259298

@@ -370,7 +409,7 @@ def find_pattern_identifier_slice(element, discriminator)
370409
end
371410

372411
def find_value_slice(element, discriminator)
373-
values = discriminator[:values].map { |value| value.merge(path: value[:path].split('.')) }
412+
values = discriminator[:values].map { |value| value.merge(path: path_segments(value[:path])) }
374413
find_slice_by_values(element, values)
375414
end
376415

@@ -401,23 +440,33 @@ def find_type_slice(element, discriminator)
401440
end
402441

403442
def find_required_binding_slice(element, discriminator)
443+
if element.is_a?(FHIR::Coding) && required_binding_value_match?(element, discriminator[:values])
444+
return element
445+
end
446+
404447
coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
405448

406449
find_a_value_at(element, coding_path) do |coding|
407-
discriminator[:values].any? do |value|
408-
case value
409-
when String
410-
value == coding.code
411-
when Hash
412-
value[:system] == coding.system && value[:code] == coding.code
413-
end
414-
end
450+
required_binding_value_match?(coding, discriminator[:values])
415451
end
416452
end
417453

418454
def find_slice_by_values(element, value_definitions)
419455
Array.wrap(element).find { |el| verify_slice_by_values(el, value_definitions) }
420456
end
457+
458+
def required_binding_value_match?(coding, values)
459+
return true if values.blank?
460+
461+
values.any? do |value|
462+
case value
463+
when String
464+
value == coding.code
465+
when Hash
466+
value[:system] == coding.system && value[:code] == coding.code
467+
end
468+
end
469+
end
421470
end
422471
end
423472
end

0 commit comments

Comments
 (0)