Repository: FHIR/sushi (ts)

Issue:
Can't add arbitrary extensions to example instances
An instance ( example) should be able to have any number of extensions even if they are not defined inline.   Right now an extension in an instance that not defined as part of the SD is causing the the instance not be generated.

Pull request description:
Allow arbitrary extensions to be added to instances
Fixes #188 and also https://standardhealthrecord.atlassian.net/browse/CIMPL-267.

This makes it so arbitrary extensions can be added to the `extension` array on an instance. The extension must be known to SUSHI, so if you want to add an extension from US Core, you must specify US Core as a dependency. As an example:
```
Alias: USCoreBirthsex = http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex

Instance: MyPatientInstance
InstanceOf: Patient
Title: "The title of my patient"
* extension[USCoreBirthsex].valueCode = #F
```
This is how this feature would be used. The USCoreBirthSex extension is not on the SD of Patient, but we are able to reference it by alias when creating an instance to create the following instance:
```
{
  "resourceType": "Patient",
  "id": "MyPatientInstance",
  "extension": [
    {
      "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex",
      "valueCode": "F"
    }
  ]
}
```
Note that the url is set automatically, so the user does not have to set it.

Patch:
diff --git a/src/fhirtypes/StructureDefinition.ts b/src/fhirtypes/StructureDefinition.ts
index 41b9153..d805d2e 100644
--- a/src/fhirtypes/StructureDefinition.ts
+++ b/src/fhirtypes/StructureDefinition.ts
@@ -412,9 +412,11 @@ export class StructureDefinition {
   ): { fixedValue: any; pathParts: PathPart[] } {
     const pathParts = this.parseFSHPath(path);
     let currentPath = '';
+    let previousPath = '';
     let currentElement: ElementDefinition;
     for (const pathPart of pathParts) {
       // Construct the path up to this point
+      previousPath = currentPath;
       currentPath += `${currentPath ? '.' : ''}${pathPart.base}`;
       // If we are indexing into an array, the last bracket should be numeric
       const arrayIndex = getArrayIndex(pathPart);
@@ -426,6 +428,28 @@ export class StructureDefinition {
         pathPart.brackets?.forEach(p => (currentPath += `[${p}]`));
       }
       currentElement = this.findElementByPath(currentPath, fisher);
+      // Allow for adding extension elements to the instance that are not on the SD
+      if (!currentElement && pathPart.base === 'extension') {
+        // Get extension element (if currentPath is A.B.extension[C], get A.B.extension)
+        const extensionPath = `${previousPath ? `${previousPath}'.'` : ''}${pathPart.base}`;
+        const extensionElement = this.findElementByPath(extensionPath, fisher);
+        // Get the extension being referred to
+        const extension = fisher.fishForMetadata(pathPart.brackets[0]);
+        if (extension && extensionElement) {
+          // If the extension exists, add it as a slice to the SD so that we can fix it
+          // This function is only called by InstanceExporter on copies of SDs, not those being exported
+          if (!extensionElement.slicing) {
+            extensionElement.sliceIt('value', 'url');
+          }
+          const slice = extensionElement.addSlice(pathPart.brackets[0]);
+          if (!slice.type[0].profile) {
+            slice.type[0].profile = [];
+          }
+          slice.type[0].profile.push(extension.url);
+          // Search again for the desired element now that the extension is added
+          currentElement = this.findElementByPath(currentPath, fisher);
+        }
+      }
 
       // If the element has a base.max that is greater than 1, but the element has been constrained, still set properties in an array
       const nonArrayElementIsBasedOnArray =


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 3a1ecf2..446d440 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -864,6 +864,27 @@ describe('InstanceExporter', () => {
       ]);
     });
 
+    it('should fix an extension that is defined but not present on the SD', () => {
+      const fooExtension = new Extension('FooExtension');
+      doc.aliases.set('FooAlias', 'http://example.com/StructureDefinition/FooExtension');
+      doc.extensions.set(fooExtension.name, fooExtension);
+      const barRule = new FixedValueRule('extension[FooAlias].valueString');
+      barRule.fixedValue = 'bar';
+      patientInstance.rules.push(barRule);
+      const exported = exportInstance(patientInstance);
+      expect(exported.extension).toEqual([
+        { url: 'http://example.com/StructureDefinition/FooExtension', valueString: 'bar' }
+      ]);
+    });
+
+    it('should not fix an extension that is not defined and not present on the SD', () => {
+      const barRule = new FixedValueRule('extension[FooAlias].valueString');
+      barRule.fixedValue = 'bar';
+      patientInstance.rules.push(barRule);
+      const exported = exportInstance(patientInstance);
+      expect(exported.extension).toBeUndefined();
+    });
+
     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/fhirtypes/StructureDefinition.test.ts b/test/fhirtypes/StructureDefinition.test.ts
index ecd995c..3ef143d 100644
--- a/test/fhirtypes/StructureDefinition.test.ts
+++ b/test/fhirtypes/StructureDefinition.test.ts
@@ -871,6 +871,28 @@ describe('StructureDefinition', () => {
       expect(pathParts[0]).toEqual({ base: 'extension', brackets: ['2'] });
       expect(pathParts[1]).toEqual({ primitive: true, base: 'url' });
     });
+
+    it('should allow setting arbitrary defined extensions', () => {
+      const originalLength = respRate.elements.length;
+      const { fixedValue, pathParts } = respRate.validateValueAtPath(
+        'extension[patient-mothersMaidenName].value[x]',
+        'foo',
+        fisher
+      );
+      expect(fixedValue).toBe('foo');
+      expect(pathParts.length).toBe(2);
+      expect(pathParts[0]).toEqual({
+        base: 'extension',
+        brackets: ['patient-mothersMaidenName', '0']
+      });
+      expect(respRate.elements.length).toBe(originalLength + 5);
+    });
+
+    it('should not allow setting arbitrary undefined extensions', () => {
+      expect(() => {
+        respRate.validateValueAtPath('extension[fake-extension].value[x]', 'foo', fisher);
+      }).toThrow('Cannot resolve element from path: extension[fake-extension].value[x]');
+    });
   });
 
   describe('#captureOriginalElements()', () => {