Repository: FHIR/sushi
Pull request #123 (2020-01-22 18:53:18)

Resolve external extension slice names
This addresses #108 and #113.

For #108, the issue was that slices (specifically in extensions) could only be addressed by their sliceName. This PR updates that so an extension slice can be referenced by anything that the MasterFisher can use to resolve the desired extension.

For #113, the issue was that our generated Extensions had the `url` field set, but they did not have the `Extension.url` element fixed to that same url. If you check out the FHIR spec, you will notice that this is common practice in their extensions: http://hl7.org/fhir/r4/extension-patient-proficiency.json.html. Not doing this was causing no `url` to be set on slices of extension on generated instances. This would then cause an IG publisher crash.

To test this further using fsh-mcode, modify the Examples.fsh file to uncomment the lines that have been marked as triggering an IG publisher crash. When you use this branch to build fsh-mcode, and then run the IG publisher, you should no longer see an IG publisher crash. Then if you inspect the output, you should see that the extension slices in the instances do have the `url` correctly set. 


Patch:
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index 209b970..32b3982 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -47,6 +47,9 @@ export class StructureDefinitionExporter implements Fishable {
     if (fshDefinition instanceof Profile) {
       structDef.derivation = 'constraint';
     } else if (fshDefinition instanceof Extension) {
+      // Automatically set url.fixedUri on Extensions
+      const url = structDef.findElement('Extension.url');
+      url.fixedUri = structDef.url;
       structDef.derivation = 'constraint';
       if (structDef.context == null) {
         // NOTE: For now, we always set context to everything, but this will be user-specified
diff --git a/src/fhirtypes/StructureDefinition.ts b/src/fhirtypes/StructureDefinition.ts
index a08604e..4745527 100644
--- a/src/fhirtypes/StructureDefinition.ts
+++ b/src/fhirtypes/StructureDefinition.ts
@@ -218,7 +218,7 @@ export class StructureDefinition {
 
       // After getting matches based on the 'base' part, we now filter according to 'brackets'
       if (pathPart.brackets) {
-        const sliceElement = this.findMatchingSlice(pathPart, matchingElements);
+        const sliceElement = this.findMatchingSlice(pathPart, matchingElements, fisher);
         if (sliceElement) {
           matchingElements = [sliceElement, ...sliceElement.children()];
         } else {
@@ -508,12 +508,24 @@ export class StructureDefinition {
    * @param {ElementDefinition[]} elements - The set of elements to search through
    * @returns {ElementDefinition} - The sliceElement if found, else undefined
    */
-  private findMatchingSlice(pathPart: PathPart, elements: ElementDefinition[]): ElementDefinition {
+  private findMatchingSlice(
+    pathPart: PathPart,
+    elements: ElementDefinition[],
+    fisher: Fishable
+  ): ElementDefinition {
+    let matchingSlice = elements.find(e => e.sliceName === pathPart.brackets.join('/'));
+    if (!matchingSlice && pathPart.brackets?.length === 1) {
+      // If we don't find a match, search predefined extensions for a match
+      const sliceDefinition = fisher.fishForFHIR(pathPart.brackets[0], Type.Extension);
+      if (sliceDefinition?.url) {
+        matchingSlice = elements.find(e => e.type?.[0].profile?.[0] === sliceDefinition.url);
+      }
+    }
     // NOTE: This function will assume the 'brackets' field contains information about slices. Even
     // if you search for foo[sliceName][refName], this will try to find a re-slice
     // sliceName/refName. To find the matching element for foo[sliceName][refName], you must
     // use the findMatchingRef function. Be aware of this ambiguity in the bracket path syntax.
-    return elements.find(e => e.sliceName === pathPart.brackets.join('/'));
+    return matchingSlice;
   }
 
   /**


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index eecc207..9f7e2cc 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -1,8 +1,8 @@
 import { InstanceExporter, Package, StructureDefinitionExporter } from '../../src/export';
 import { FSHTank, FSHDocument } from '../../src/import';
 import { FHIRDefinitions, loadFromPath } from '../../src/fhirdefs';
-import { Instance, Profile, FshCode, FshReference } from '../../src/fshtypes';
-import { FixedValueRule } from '../../src/fshtypes/rules';
+import { Instance, Profile, FshCode, FshReference, Extension } from '../../src/fshtypes';
+import { FixedValueRule, ContainsRule } from '../../src/fshtypes/rules';
 import { loggerSpy, TestFisher } from '../testhelpers';
 import { InstanceDefinition } from '../../src/fhirtypes';
 import path from 'path';
@@ -491,6 +491,37 @@ describe('InstanceExporter', () => {
       ]);
     });
 
+    it('should fix a sliced extension element that is referred to by name', () => {
+      const fooExtension = new Extension('FooExtension');
+      doc.extensions.set(fooExtension.name, fooExtension);
+      const containsRule = new ContainsRule('extension');
+      containsRule.items = ['FooExtension'];
+      patientProf.rules.push(containsRule);
+      const barRule = new FixedValueRule('extension[FooExtension].valueString');
+      barRule.fixedValue = 'bar';
+      patientProfInstance.rules.push(barRule);
+      const exported = exportInstance(patientProfInstance);
+      expect(exported.extension).toEqual([
+        { url: 'http://example.com/StructureDefinition/FooExtension', valueString: 'bar' }
+      ]);
+    });
+
+    it('should fix a sliced extension element that is referred to by aliased url', () => {
+      const fooExtension = new Extension('FooExtension');
+      doc.aliases.set('FooAlias', 'http://example.com/StructureDefinition/FooExtension');
+      doc.extensions.set(fooExtension.name, fooExtension);
+      const containsRule = new ContainsRule('extension');
+      containsRule.items = ['FooExtension'];
+      patientProf.rules.push(containsRule);
+      const barRule = new FixedValueRule('extension[FooAlias].valueString');
+      barRule.fixedValue = 'bar';
+      patientProfInstance.rules.push(barRule);
+      const exported = exportInstance(patientProfInstance);
+      expect(exported.extension).toEqual([
+        { url: 'http://example.com/StructureDefinition/FooExtension', valueString: 'bar' }
+      ]);
+    });
+
     it.skip('should throw when ordered is set in the discriminator but slices arrive out of order', () => {
       const fixedValRule = new FixedValueRule('result[Triglyceride].display');
       fixedValRule.fixedValue = 'foo';
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 4013142..e2cec94 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -119,6 +119,9 @@ describe('StructureDefinitionExporter', () => {
     expect(exported.title).toBe('Foo Profile');
     expect(exported.description).toBe('foo bar foobar');
     expect(exported.url).toBe('http://example.com/StructureDefinition/foo');
+    expect(exported.elements.find(e => e.id === 'Extension.url').fixedUri).toBe(
+      'http://example.com/StructureDefinition/foo'
+    );
     expect(exported.version).toBe('0.0.1');
     // NOTE: For now, we always set context to everything, but this will be user-specified in
     // the future
@@ -145,6 +148,9 @@ describe('StructureDefinitionExporter', () => {
       'Base StructureDefinition for Extension Type: Optional Extension Element - found in all resources.'
     );
     expect(exported.url).toBe('http://example.com/StructureDefinition/Foo');
+    expect(exported.elements.find(e => e.id === 'Extension.url').fixedUri).toBe(
+      'http://example.com/StructureDefinition/Foo'
+    );
     expect(exported.version).toBe('0.0.1');
     // NOTE: For now, we always set context to everything, but this will be user-specified in
     // the future