Repository: FHIR/sushi
Pull request #89 (2020-01-13 20:38:00)

Unfold children of slices directly from SD
This addresses https://standardhealthrecord.atlassian.net/browse/CIMPL-206, unable to find inline children of slices.

When an element is sliced, and that element has children that are on the SD, we should unfold those children onto slices of the element. Previously we were unfolding the element from the FHIR definition of the type of the element. Now we will first check if the sliced element has children on the SD, and if it does, we unfold with those children. If the sliced element does not have children, then we revert to the old behavior where we unfold from the FHIR definition of the type.

If you want to test this further, checkout master on fsh-mcode https://github.com/standardhealth/fsh-mcode, but go into CancerGenomics.fsh and uncomment lines 45 through 80, which deal with setting slices on `component`. When you run SUSHI on master against fsh-mcode, you should see a bunch of errors coming from these lines. When you run SUSHI on this branch against fsh-mcode, these errors should go away, and you should see these lines accurately reflected in the output StructureDefinition-CancerGeneticVariant.json file.

cmoesel: @ngfreiter -- unless you're planning any further changes, I'm fine with merging this now.
ngfreiter: Nope I wasn't thinking of adding anything else, merge away!

Patch:
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index 10155db..b71bb51 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -387,15 +387,13 @@ export class ElementDefinition {
     }
 
     // If element is a slice
-    if (this.sliceName) {
-      const slicedElement = this.structDef.elements.find(e => e.path === this.path && e.slicing);
-      if (slicedElement) {
-        // Check that slicedElement max >= new sum of mins
-        const sumOfMins = slicedElement.checkSumOfSliceMins(slicedElement.max, min - this.min);
-        // If new sum of mins > slicedElement min, increase slicedElement min
-        if (sumOfMins > slicedElement.min) {
-          slicedElement.min = sumOfMins;
-        }
+    const slicedElement = this.slicedElement();
+    if (slicedElement) {
+      // Check that slicedElement max >= new sum of mins
+      const sumOfMins = slicedElement.checkSumOfSliceMins(slicedElement.max, min - this.min);
+      // If new sum of mins > slicedElement min, increase slicedElement min
+      if (sumOfMins > slicedElement.min) {
+        slicedElement.min = sumOfMins;
       }
     }
 
@@ -411,9 +409,9 @@ export class ElementDefinition {
    * @throws {InvalidSumOfSliceMinsError} when the sum of mins of the slices exceeds max of sliced element
    */
   private checkSumOfSliceMins(newSlicedElementMax: string, sliceMinIncrease = 0) {
-    const slices = this.structDef.elements.filter(
-      e => e.id !== this.id && e.path === this.path && !e.slicing
-    );
+    const slices = this.parent()
+      .children()
+      .filter(e => e.id !== this.id && e.path === this.path && !e.slicing);
     const sumOfMins = sliceMinIncrease + slices.reduce((prev, curr) => (prev += curr.min), 0);
     if (newSlicedElementMax !== '*' && sumOfMins > parseInt(newSlicedElementMax)) {
       throw new InvalidSumOfSliceMinsError(sumOfMins, newSlicedElementMax, this.id);
@@ -1353,6 +1351,16 @@ export class ElementDefinition {
     });
   }
 
+  /**
+   * Finds and returns the elemnent being sliced
+   * @returns {ElementDefinition | undefined} the sliced element or undefined if the element is not a slice
+   */
+  slicedElement(): ElementDefinition | undefined {
+    if (this.sliceName) {
+      return this.structDef.elements.find(e => e.id === this.id.slice(0, this.id.lastIndexOf(':')));
+    }
+  }
+
   /**
    * If the element has a single type, graft the type's elements into this StructureDefinition as child elements.
    * @param {ResolveFn} resolve - a function that can resolve a type to a StructureDefinition instance
@@ -1364,11 +1372,23 @@ export class ElementDefinition {
       this.type.length === 1 &&
       (this.type[0].profile == null || this.type[0].profile.length <= 1)
     ) {
-      // If it has a profile, use that, otherwise use the code
-      const type = this.type[0].profile?.[0] ?? this.type[0].code;
-      const def = resolve(type);
-      if (def) {
-        const newElements = def.elements.slice(1).map(e => {
+      let newElements: ElementDefinition[] = [];
+      if (this.sliceName) {
+        // If the element is sliced, we first try to unfold from the SD itself
+        const slicedElement = this.slicedElement();
+        newElements = slicedElement.children().map(e => {
+          const eClone = e.clone();
+          eClone.id = eClone.id.replace(slicedElement.id, this.id);
+          eClone.structDef = this.structDef;
+          eClone.captureOriginal();
+          return eClone;
+        });
+      }
+      if (newElements.length === 0) {
+        // If it has a profile, use that, otherwise use the code
+        const type = this.type[0].profile?.[0] ?? this.type[0].code;
+        const def = resolve(type);
+        newElements = def?.elements.slice(1).map(e => {
           const eClone = e.clone();
           eClone.id = eClone.id.replace(def.type, `${this.id}`);
           eClone.structDef = this.structDef;
@@ -1376,6 +1396,8 @@ export class ElementDefinition {
           eClone.captureOriginal();
           return eClone;
         });
+      }
+      if (newElements.length > 0) {
         this.structDef.addElements(newElements);
         return newElements;
       }


Test patch:
diff --git a/test/fhirtypes/ElementDefinition.test.ts b/test/fhirtypes/ElementDefinition.test.ts
index fb836ab..9733ce4 100644
--- a/test/fhirtypes/ElementDefinition.test.ts
+++ b/test/fhirtypes/ElementDefinition.test.ts
@@ -319,6 +319,53 @@ describe('ElementDefinition', () => {
       expect(observation.elements[codeIdx + 5].id).toBe('Observation.subject');
     });
 
+    it('should add children from the Structure Definition when they exist', () => {
+      const numOriginalElements = observation.elements.length;
+      const component = observation.elements.find(e => e.path === 'Observation.component');
+      component.slicing = {
+        ordered: false,
+        rules: 'open',
+        discriminator: [{ type: 'value', path: 'code' }]
+      };
+      const componentSlice = component.addSlice('FooSlice');
+      const componentSliceIdx = observation.elements.findIndex(e => e.id === componentSlice.id);
+      const newElements = componentSlice.unfold(resolve);
+      expect(newElements).toHaveLength(8);
+      expect(newElements[0].id).toBe('Observation.component:FooSlice.id');
+      expect(newElements[1].id).toBe('Observation.component:FooSlice.extension');
+      expect(newElements[2].id).toBe('Observation.component:FooSlice.modifierExtension');
+      expect(newElements[3].id).toBe('Observation.component:FooSlice.code');
+      expect(newElements[4].id).toBe('Observation.component:FooSlice.value[x]');
+      expect(newElements[5].id).toBe('Observation.component:FooSlice.dataAbsentReason');
+      expect(newElements[6].id).toBe('Observation.component:FooSlice.interpretation');
+      expect(newElements[7].id).toBe('Observation.component:FooSlice.referenceRange');
+      expect(observation.elements).toHaveLength(numOriginalElements + 9);
+      expect(observation.elements[componentSliceIdx + 1].id).toBe(
+        'Observation.component:FooSlice.id'
+      );
+      expect(observation.elements[componentSliceIdx + 2].id).toBe(
+        'Observation.component:FooSlice.extension'
+      );
+      expect(observation.elements[componentSliceIdx + 3].id).toBe(
+        'Observation.component:FooSlice.modifierExtension'
+      );
+      expect(observation.elements[componentSliceIdx + 4].id).toBe(
+        'Observation.component:FooSlice.code'
+      );
+      expect(observation.elements[componentSliceIdx + 5].id).toBe(
+        'Observation.component:FooSlice.value[x]'
+      );
+      expect(observation.elements[componentSliceIdx + 6].id).toBe(
+        'Observation.component:FooSlice.dataAbsentReason'
+      );
+      expect(observation.elements[componentSliceIdx + 7].id).toBe(
+        'Observation.component:FooSlice.interpretation'
+      );
+      expect(observation.elements[componentSliceIdx + 8].id).toBe(
+        'Observation.component:FooSlice.referenceRange'
+      );
+    });
+
     it('should not add any children when an element has multiple types', () => {
       const numOriginalElements = observation.elements.length;
       const valueIdx = observation.elements.findIndex(e => e.path === 'Observation.value[x]');
diff --git a/test/fhirtypes/StructureDefinition.test.ts b/test/fhirtypes/StructureDefinition.test.ts
index 4a40ace..07fb968 100644
--- a/test/fhirtypes/StructureDefinition.test.ts
+++ b/test/fhirtypes/StructureDefinition.test.ts
@@ -307,6 +307,22 @@ describe('StructureDefinition', () => {
       expect(codeText.short).toBe('Plain text representation of the concept');
       expect(observation.elements.length).toBe(originalLength + 4);
     });
+
+    it('should find an element that must be unfolded from the Structure Definition by path', () => {
+      const originalLength = observation.elements.length;
+      const component = observation.elements.find(e => e.id === 'Observation.component');
+      component.slicing = {
+        ordered: false,
+        rules: 'open',
+        discriminator: [{ type: 'value', path: 'code' }]
+      };
+      component.addSlice('FooSlice');
+      const componentCode = observation.findElementByPath('component[FooSlice].code', resolve);
+      expect(componentCode).toBeDefined();
+      expect(componentCode.id).toBe('Observation.component:FooSlice.code');
+      expect(componentCode.path).toBe('Observation.component.code');
+      expect(observation.elements.length).toBe(originalLength + 9);
+    });
   });
 
   describe('#setInstancePropertyByPath', () => {