Skip to content

Commit 212ec4e

Browse files
improve must support assessment by normalizing extension URLs and correctly handling discriminator paths for Bundle entries.
1 parent 109a340 commit 212ec4e

8 files changed

+234
-20
lines changed

lib/inferno/dsl/fhir_resource_navigation.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ def matching_type_slice?(slice, discriminator)
194194
when 'String'
195195
slice_value.is_a? String
196196
else
197-
slice_value = slice_value.resource if slice_value.is_a?(FHIR::Bundle::Entry)
198197
slice_value.is_a? FHIR.const_get(discriminator[:code])
199198
end
200199
end

lib/inferno/dsl/must_support_assessment.rb

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,18 @@ def must_support_extensions
202202
def missing_extensions(resources = [])
203203
@missing_extensions ||=
204204
must_support_extensions.select do |extension_definition|
205+
expected_url = normalized_extension_url(extension_definition[:url])
206+
205207
resources.none? do |resource|
206208
path = extension_definition[:path]
207209

208210
if path == 'extension'
209-
resource.extension.any? { |extension| extension.url == extension_definition[:url] }
211+
Array.wrap(resource.extension).any? do |extension|
212+
normalized_extension_url(extension.url) == expected_url
213+
end
210214
else
211215
extension = find_a_value_at(resource, path) do |el|
212-
el.url == extension_definition[:url]
216+
normalized_extension_url(el.url) == expected_url
213217
end
214218

215219
extension.present?
@@ -240,8 +244,9 @@ def resource_populates_element?(resource, element_definition)
240244

241245
ms_extension_urls = must_support_extensions.select { |ex| ex[:path] == "#{path}.extension" }
242246
.map { |ex| ex[:url] }
247+
include_dar = normalized_extension_urls(ms_extension_urls).include?(FHIRResourceNavigation::DAR_EXTENSION_URL)
243248

244-
value_found = find_a_value_at(resource, path) do |potential_value|
249+
value_found = find_a_value_at(resource, path, include_dar:) do |potential_value|
245250
matching_without_extensions?(potential_value, ms_extension_urls, element_definition[:fixed_value])
246251
end
247252

@@ -257,8 +262,11 @@ def process_must_support_element_in_extension(resource, path)
257262
extension_name = extension_split.first
258263
extension_path = extension_split.last
259264

260-
found_extension_url = must_support_extensions.find { |ex| ex[:id].include?(extension_name) }[:url]
261-
ms_element_extension = resource.extension.find { |ex| ex.url == found_extension_url }
265+
found_extension_url =
266+
normalized_extension_url(must_support_extensions.find { |ex| ex[:id].include?(extension_name) }[:url])
267+
ms_element_extension = resource.extension.find do |extension|
268+
normalized_extension_url(extension.url) == found_extension_url
269+
end
262270

263271
if ms_element_extension.present?
264272
resource = ms_element_extension
@@ -269,17 +277,33 @@ def process_must_support_element_in_extension(resource, path)
269277
end
270278

271279
def matching_without_extensions?(value, ms_extension_urls, fixed_value)
272-
if value.instance_of?(Inferno::DSL::PrimitiveType)
273-
urls = value.extension&.map(&:url)
274-
has_ms_extension = (urls & ms_extension_urls).present?
275-
value = value.value
276-
end
280+
has_ms_extension = must_support_extension_present?(value, ms_extension_urls)
281+
282+
value = value.value if value.instance_of?(Inferno::DSL::PrimitiveType)
277283

278284
return false unless has_ms_extension || value_without_extensions?(value)
279285

280286
matches_fixed_value?(value, fixed_value)
281287
end
282288

289+
def must_support_extension_present?(value, ms_extension_urls)
290+
return false unless value.respond_to?(:extension)
291+
292+
(extension_urls(value) & normalized_extension_urls(ms_extension_urls)).present?
293+
end
294+
295+
def extension_urls(value)
296+
Array.wrap(value.extension).map { |extension| normalized_extension_url(extension.url) }
297+
end
298+
299+
def normalized_extension_urls(urls)
300+
Array.wrap(urls).map { |url| normalized_extension_url(url) }
301+
end
302+
303+
def normalized_extension_url(url)
304+
url&.split('|')&.first
305+
end
306+
283307
def matches_fixed_value?(value, fixed_value)
284308
fixed_value.blank? || value == fixed_value
285309
end

lib/inferno/dsl/must_support_metadata_extractor.rb

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,17 @@ def must_support_extensions
5454
{
5555
id: element.id,
5656
path: element.path.gsub("#{resource}.", ''),
57-
url: element.type.first.profile.first
57+
url: canonical_url_without_version(element.type.first.profile.first)
5858
}.tap do |metadata|
5959
metadata[:by_requirement_extension_only] = true if by_requirement_extension_only?(element)
6060
end
6161
end
6262
end
6363

64+
def canonical_url_without_version(url)
65+
url&.split('|')&.first
66+
end
67+
6468
def must_support_slice_elements
6569
all_must_support_elements.select do |element|
6670
!element.path.end_with?('extension') && element.sliceName.present?
@@ -153,24 +157,49 @@ def type_slices
153157
must_support_type_slice_elements.map do |current_element|
154158
discriminator = discriminators(sliced_element(current_element)).first
155159
type_path = discriminator_path(discriminator)
156-
type_element = find_element_by_discriminator_path(current_element, type_path)
160+
type_element, type_path = find_type_slice_target(current_element, type_path)
157161

158162
type_code = type_element.type.first.code
163+
discriminator_metadata = {
164+
type: 'type',
165+
code: type_code.upcase_first
166+
}
167+
discriminator_metadata[:path] = type_path if type_path.present?
159168

160169
{
161170
slice_id: current_element.id,
162171
slice_name: current_element.sliceName,
163172
path: current_element.path.gsub("#{resource}.", ''),
164-
discriminator: {
165-
type: 'type',
166-
code: type_code.upcase_first
167-
}
173+
discriminator: discriminator_metadata
168174
}.tap do |metadata|
169175
metadata[:by_requirement_extension_only] = true if by_requirement_extension_only?(current_element)
170176
end
171177
end
172178
end
173179

180+
def find_type_slice_target(current_element, type_path)
181+
type_element = find_element_by_discriminator_path(current_element, type_path)
182+
183+
return [type_element, type_path] unless type_slice_requires_resource_path?(type_path, type_element)
184+
185+
resource_element = type_slice_resource_element(current_element)
186+
187+
return [type_element, type_path] unless resource_element.present?
188+
189+
[resource_element, resource_element.path.delete_prefix("#{current_element.path}.")]
190+
end
191+
192+
def type_slice_requires_resource_path?(type_path, type_element)
193+
type_path.blank? &&
194+
type_element&.type&.all? { |type| type.code == 'BackboneElement' }
195+
end
196+
197+
def type_slice_resource_element(current_element)
198+
profile_elements.find do |element|
199+
element.id == "#{current_element.id}.resource" && element.type.present?
200+
end
201+
end
202+
174203
def must_support_value_slice_elements
175204
must_support_slice_elements.select do |element|
176205
# discriminator type 'pattern' is deprecated in FHIR R5 and made equivalent to 'value'

spec/fixtures/metadata/pas_inquiry_request_bundle_v220.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
:discriminator:
2323
:type: type
2424
:code: Claim
25+
:path: resource
2526
:elements:
2627
- :path: identifier
2728
- :path: timestamp

spec/fixtures/metadata/pas_request_bundle_v201.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
:discriminator:
2323
:type: type
2424
:code: Claim
25+
:path: resource
2526
:elements:
2627
- :path: identifier
2728
- :path: timestamp

spec/inferno/dsl/fhir_resource_navigation_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,15 +173,15 @@ def metadata_fixture(filename)
173173
expect(matcher).to_not be_matching_type_slice(slice, discriminator)
174174
end
175175

176-
it 'matches a Bundle::Entry by unwrapping resource when no discriminator path' do
176+
it 'does not match a Bundle::Entry without a discriminator path' do
177177
slice = FHIR::Bundle::Entry.new(
178178
resource: FHIR::Claim.new
179179
)
180180
discriminator = { code: 'Claim' }
181-
expect(matcher).to be_matching_type_slice(slice, discriminator)
181+
expect(matcher).to_not be_matching_type_slice(slice, discriminator)
182182
end
183183

184-
it 'does not match a Bundle::Entry with wrong resource type when no discriminator path' do
184+
it 'does not match a Bundle::Entry with the wrong resource type when no discriminator path' do
185185
slice = FHIR::Bundle::Entry.new(
186186
resource: FHIR::Patient.new
187187
)

spec/inferno/dsl/must_support_assessment_spec.rb

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,92 @@ def run_with_metadata(resources, metadata)
210210
end
211211
end
212212

213+
describe 'must support test for versioned extension canonicals' do
214+
let(:communication_request_metadata) do
215+
OpenStruct.new(
216+
must_supports: {
217+
extensions: [
218+
{
219+
id: 'CommunicationRequest.extension:serviceLineNumber',
220+
path: 'extension',
221+
url: 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-serviceLineNumber|2.2.0'
222+
},
223+
{
224+
id: 'CommunicationRequest.payload.extension:contentModifier',
225+
path: 'payload.extension',
226+
url: 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-contentModifier|2.2.0'
227+
}
228+
],
229+
slices: [],
230+
elements: []
231+
}
232+
)
233+
end
234+
235+
let(:communication_request) do
236+
FHIR::CommunicationRequest.new(
237+
extension: [
238+
{
239+
url: 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-serviceLineNumber',
240+
valuePositiveInt: 1
241+
}
242+
],
243+
payload: [
244+
{
245+
extension: [
246+
{
247+
url: 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-contentModifier',
248+
valueCodeableConcept: { text: 'modifier' }
249+
}
250+
],
251+
contentString: 'Need additional documentation'
252+
}
253+
]
254+
)
255+
end
256+
257+
it 'matches resource extensions when metadata canonical urls include a version' do
258+
result = run_with_metadata([communication_request], communication_request_metadata)
259+
expect(result).to be_empty
260+
end
261+
end
262+
263+
describe 'must support test for non-primitive elements with must support extensions' do
264+
let(:claim_response_metadata) do
265+
OpenStruct.new(
266+
must_supports: {
267+
extensions: [
268+
{
269+
id: 'ClaimResponse.request.extension:DataAbsentReason',
270+
path: 'request.extension',
271+
url: 'http://hl7.org/fhir/StructureDefinition/data-absent-reason|5.2.0'
272+
}
273+
],
274+
slices: [],
275+
elements: [{ path: 'request' }]
276+
}
277+
)
278+
end
279+
280+
let(:claim_response) do
281+
FHIR::ClaimResponse.new(
282+
request: {
283+
extension: [
284+
{
285+
url: 'http://hl7.org/fhir/StructureDefinition/data-absent-reason',
286+
valueCode: 'unknown'
287+
}
288+
]
289+
}
290+
)
291+
end
292+
293+
it 'treats a DAR-only reference as populated when the extension is must support' do
294+
result = run_with_metadata([claim_response], claim_response_metadata)
295+
expect(result).to be_empty
296+
end
297+
end
298+
213299
describe 'must support test for slices' do
214300
context 'with patternCodeableConcept slicing' do
215301
let(:careplan_profile) { fixture('StructureDefinition-us-core-careplan.json') }

spec/inferno/dsl/must_support_metadata_extractor_spec.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
require_relative '../../extract_tgz_helper'
12
require_relative '../../../lib/inferno/dsl/must_support_metadata_extractor'
23

34
RSpec.describe Inferno::DSL::MustSupportMetadataExtractor do
@@ -31,6 +32,20 @@
3132
allow(profile_element).to receive_messages(mustSupport: true, path: 'foo.extension', id: 'id', type: [type])
3233
end
3334

35+
describe '#must_support_extensions' do
36+
let(:type) do
37+
type = double
38+
allow(type).to receive(:profile).and_return(['http://example.org/StructureDefinition/example-extension|1.2.3'])
39+
type
40+
end
41+
42+
it 'removes canonical version suffixes from extension urls' do
43+
expect(extractor.must_support_extensions).to eq(
44+
[{ id: 'id', path: 'foo.extension', url: 'http://example.org/StructureDefinition/example-extension' }]
45+
)
46+
end
47+
end
48+
3449
describe '#get_type_must_support_metadata' do
3550
let(:metadata) do
3651
{ path: 'path' }
@@ -87,6 +102,65 @@
87102
expect(slices[0][:discriminator][:type]).to eq('type')
88103
expect(slices[0][:discriminator][:code]).to eq('Date')
89104
end
105+
106+
it 'extracts the discriminator path when the sliced type is on Bundle.entry.resource' do
107+
discriminator = FHIR::ElementDefinition::Slicing::Discriminator.new(type: 'type', path: '$this')
108+
slicing = FHIR::ElementDefinition::Slicing.new(discriminator: [discriminator])
109+
110+
backbone_type = instance_double(FHIR::ElementDefinition::Type, code: 'BackboneElement')
111+
claim_type = instance_double(FHIR::ElementDefinition::Type, code: 'Claim')
112+
bundle_profile = instance_double(
113+
FHIR::StructureDefinition,
114+
baseDefinition: 'baseDefinition',
115+
name: 'bundle',
116+
type: 'Bundle',
117+
version: '2.2.0'
118+
)
119+
profile_elements = [
120+
instance_double(
121+
FHIR::ElementDefinition,
122+
id: 'Bundle.entry',
123+
path: 'Bundle.entry',
124+
sliceName: nil,
125+
mustSupport: false,
126+
slicing:,
127+
type: [backbone_type]
128+
),
129+
instance_double(
130+
FHIR::ElementDefinition,
131+
id: 'Bundle.entry:Claim',
132+
path: 'Bundle.entry',
133+
sliceName: 'Claim',
134+
mustSupport: true,
135+
slicing: nil,
136+
type: [backbone_type]
137+
),
138+
instance_double(
139+
FHIR::ElementDefinition,
140+
id: 'Bundle.entry:Claim.resource',
141+
path: 'Bundle.entry.resource',
142+
sliceName: nil,
143+
mustSupport: false,
144+
slicing: nil,
145+
type: [claim_type]
146+
)
147+
]
148+
149+
slices = described_class.new(profile_elements, bundle_profile, 'Bundle', ig_resources).type_slices
150+
151+
expect(slices).to contain_exactly(
152+
{
153+
slice_id: 'Bundle.entry:Claim',
154+
slice_name: 'Claim',
155+
path: 'entry',
156+
discriminator: {
157+
type: 'type',
158+
code: 'Claim',
159+
path: 'resource'
160+
}
161+
}
162+
)
163+
end
90164
end
91165

92166
describe '#value_slices' do

0 commit comments

Comments
 (0)