Repository: FHIR/sushi (ts)

Issue:
Bug: error: Cannot read property 'length' of undefined
The following FSH file generates the error:
error: Cannot read property 'length' of undefined

But the same input with the extension on `compose.include.extension` instead works just fine.

```fsh
Profile: BoostedValueSet
Parent: ValueSet
Id: boostedvalueset
Title: "Boosted ValueSet"
Description: "Any ValueSet that uses the ValueSetBoost extension"
* compose.exclude.extension contains ValueSetBoost 0..* 

Extension: ValueSetBoost
Id: boost
Title: "ValueSet boosting"
Description: "boosting extension"
* value[x] only decimal
```

Pull request:
Unfold Content Reference Elements
Fixes #184.

When we access the children of an element that has a contentReference field on it, we need to unfold from the element on the StructureDefinition that the contentReference refers to. A contentReference is used to refer to an element defined in that same StructureDefinition. Documentation on contentReferences is here https://www.hl7.org/fhir/elementdefinition-definitions.html#ElementDefinition.contentReference. 

This adds the capability to access children of an element that is a contentReference. Accessing a child will unfold all the children of the referenced element onto the element with the contentReference. The contentReference element will also have the contentReference field deleted, since it is no longer a contentReference. If you want to test on a real example, try adding the Profile on ValueSet described in #184 to a FSH file. You should see that the "exclude" element gets unfolded with the children of the "include" element, and constraints are correctly applied to it.

Patch:
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index dd4498f..a94d58a 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -1408,16 +1408,41 @@ export class ElementDefinition {
 
   /**
    * If the element has a single type, graft the type's elements into this StructureDefinition as child elements.
+   * If the element is sliced, unfold from the sliced element on the StructureDefinition
+   * If the elemnet is a content reference, unfold from the referenced element on the StructureDefintion
    * @param {Fishable} fisher - A fishable implementation for finding definitions and metadata
    * @returns {ElementDefinition[]} the unfolded elements or an empty array if the type is multi-value or type can't
    *   be resolved.
    */
   unfold(fisher: Fishable): ElementDefinition[] {
     if (
-      this.type.length === 1 &&
-      (this.type[0].profile == null || this.type[0].profile.length <= 1)
+      (this.type?.length === 1 && (this.type[0].profile ?? []).length <= 1) ||
+      this.contentReference
     ) {
       let newElements: ElementDefinition[] = [];
+      if (this.contentReference) {
+        // If the element is a content reference, we need to unfold from the referenced element
+        // Get the original resource JSON so we unfold unconstrained reference
+        const type = this.structDef.type;
+        const json = fisher.fishForFHIR(type, Type.Resource);
+        if (json) {
+          const def = StructureDefinition.fromJSON(json);
+          // Content references start with #, slice that off to id of referenced element
+          const referencedElement = def.findElement(this.contentReference.slice(1));
+          newElements = referencedElement?.children().map(e => {
+            const eClone = e.clone();
+            eClone.id = eClone.id.replace(referencedElement.id, this.id);
+            eClone.structDef = this.structDef;
+            eClone.captureOriginal();
+            return eClone;
+          });
+          if (newElements.length > 0) {
+            // If we successfully unfolded, this element is no longer a content reference
+            this.type = referencedElement.type;
+            delete this.contentReference;
+          }
+        }
+      }
       if (this.sliceName) {
         // If the element is sliced, we first try to unfold from the SD itself
         const slicedElement = this.slicedElement();


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 030342a..b98317b 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -124,6 +124,7 @@ describe('InstanceExporter', () => {
     let instance: Instance;
     let patientProfInstance: Instance;
     let lipidInstance: Instance;
+    let valueSetInstance: Instance;
     beforeEach(() => {
       patient = new Profile('TestPatient');
       patient.parent = 'Patient';
@@ -140,6 +141,9 @@ describe('InstanceExporter', () => {
       lipidInstance = new Instance('Bam');
       lipidInstance.instanceOf = 'lipidprofile';
       doc.instances.set(lipidInstance.name, lipidInstance);
+      valueSetInstance = new Instance('Boom');
+      valueSetInstance.instanceOf = 'ValueSet';
+      doc.instances.set(valueSetInstance.name, valueSetInstance);
     });
 
     // Setting Metadata
@@ -878,6 +882,21 @@ describe('InstanceExporter', () => {
     it.skip('should fix sliced elements on a sliced primitive', () => {
       /* Need example of sliced primitive */
     });
+
+    // Content Reference
+    it('should fix a child of a contentReference element', () => {
+      const barRule = new FixedValueRule('compose.exclude.version');
+      barRule.fixedValue = 'bar';
+      valueSetInstance.rules.push(barRule);
+      const exported = exportInstance(valueSetInstance);
+      expect(exported.compose).toEqual({
+        exclude: [
+          {
+            version: 'bar'
+          }
+        ]
+      });
+    });
   });
 
   describe('#export', () => {
diff --git a/test/fhirtypes/StructureDefinition.test.ts b/test/fhirtypes/StructureDefinition.test.ts
index a20b2c8..ecd995c 100644
--- a/test/fhirtypes/StructureDefinition.test.ts
+++ b/test/fhirtypes/StructureDefinition.test.ts
@@ -355,10 +355,12 @@ describe('StructureDefinition', () => {
     let respRate: StructureDefinition;
     let lipidProfile: StructureDefinition;
     let clinicalDocument: StructureDefinition;
+    let valueSet: StructureDefinition;
     beforeEach(() => {
       respRate = fisher.fishForStructureDefinition('resprate');
       lipidProfile = fisher.fishForStructureDefinition('lipidprofile');
       clinicalDocument = fisher.fishForStructureDefinition('clinicaldocument');
+      valueSet = fisher.fishForStructureDefinition('ValueSet');
     });
 
     // Simple paths (no brackets)
@@ -543,6 +545,33 @@ describe('StructureDefinition', () => {
       expect(resultDisplay.id).toBe('DiagnosticReport.result.display');
       expect(lipidProfile.elements.length).toBe(originalLength + 6);
     });
+
+    // Content Reference Paths
+    it('should find a child of a content reference element by path', () => {
+      const originalLength = valueSet.elements.length;
+      // Modify system on the current ValueSet to test that we are copying from original SD
+      // not the profiled SD
+      const include = valueSet.elements.find(e => e.id === 'ValueSet.compose.include');
+      const includeSystem = valueSet.elements.find(e => e.id === 'ValueSet.compose.include.system');
+      includeSystem.short = 'This should not get copied over!';
+      const exclude = valueSet.elements.find(e => e.id === 'ValueSet.compose.exclude');
+      const excludeSystem = valueSet.findElementByPath('compose.exclude.system', fisher);
+      expect(excludeSystem).toBeDefined();
+      expect(excludeSystem.id).toBe('ValueSet.compose.exclude.system');
+      expect(excludeSystem.short).toBe('The system the codes come from');
+      expect(exclude.contentReference).toBeUndefined();
+      expect(exclude.type).toEqual(include.type);
+      expect(valueSet.elements.length).toBe(originalLength + 26);
+    });
+
+    it('should find but not unfold a content reference element by path', () => {
+      const originalLength = valueSet.elements.length;
+      const exclude = valueSet.findElementByPath('compose.exclude', fisher);
+      expect(exclude).toBeDefined();
+      expect(exclude.id).toBe('ValueSet.compose.exclude');
+      expect(exclude.contentReference).toBe('#ValueSet.compose.include');
+      expect(valueSet.elements.length).toBe(originalLength);
+    });
   });
 
   describe('#setInstancePropertyByPath', () => {