Repository: FHIR/sushi (ts)

Issue:
Set properties on instance from pattern[x] on parent element
When we create an instance, we copy over fixed and pattern values from the Structure Definition onto the instance. However, if an element's value is set by a pattern on that element's parent, the element does not get fixed, because we only account for fixed values that are directly on the element, not for pattern values that may be present on parents of the element. As an example, see the below:
```
* identifier[NPI].value = "1265714091"
// workaround for https://github.com/FHIR/sushi/issues/148: "Pattern in StructureDefinition not reflected in Instance" opened 1-25-2020 by MK
* identifier[NPI].system = "http://hl7.org/fhir/sid/us-npi"   // although URI is not a supported type in FSH yet, apparently you can get away with giving the URI as a string, and the json comes out correctly.
```
If we remove the workaround (i.e., delete the line fixing the system) then the system doesn't get set in the example at all:
```
  "identifier": [
    {
      "value": "1265714091"
    }
  ],
```
Although indentifier[NPI] slice is 0..1, identifier[NPI].system is 1..1 -- so according to our basic rules, it should be set since we're going to identifier[NPI] to set the value. The system element needs to not only be checked to see if it has any direct fixed values, it also needs to be checked to see if parents fix it as well. This is similar to https://github.com/FHIR/sushi/issues/84, and it is likely that solving this issue will make that issue easy to solve, but they are not quite duplicates.

Pull request description:
Fix values in instances from parent patterns
This fixes #160 

Values are now fixed on an instance if they are fixed by a pattern[x] element in their parent. One possible way to test this is to build fsh-mcode using SUSHI on master. Then, in Examples.fsh, you should be able to comment out the lines that are labeled as part of a workaround. Then you can build fsh-mcode using SUSHI on this branch. You should get the same result, down to the ordering of the elements in the output. Additionally, you should notice that you do not see the problem that is described in #160

Note that this does not yet add validation to check if an element is fixed by pattern from its parent. This is only adding functionality to set values on an instance according to patterns on parent elements. I am open to adding validation to this PR if the reviewers think I should, but I felt like this could stand on its own.

Whoever reviews, if you can give a good look at the tests, I would appreciate that. I had trouble thinking through different possible test cases. As far as I could think of, my tests cases cover what we need, but I want to make sure this is well tested.

Patch:
diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts
index 5b850c3..2228a3a 100644
--- a/src/export/InstanceExporter.ts
+++ b/src/export/InstanceExporter.ts
@@ -20,18 +20,19 @@ export class InstanceExporter {
     instanceOfStructureDefinition: StructureDefinition
   ): InstanceDefinition {
     // Fix values from the SD for all elements at the top level of the SD
-    this.setFixedValuesForDirectChildren(
+    this.setFixedValuesFromStructureDefinition(
       instanceOfStructureDefinition.findElement(instanceDef.resourceType),
       '',
       instanceDef,
       instanceOfStructureDefinition
     );
 
-    // All rules will be FixValueRule
-    fshInstanceDef.rules.forEach(rule => {
-      rule = replaceReferences(rule, this.tank, this.fisher);
+    const rules = fshInstanceDef.rules.map(r => replaceReferences(r, this.tank, this.fisher));
+
+    // Fix all values from the SD that are exposed when processing the instance rules
+    rules.forEach(rule => {
       try {
-        const { fixedValue, pathParts } = instanceOfStructureDefinition.validateValueAtPath(
+        const validated = instanceOfStructureDefinition.validateValueAtPath(
           rule.path,
           rule.fixedValue,
           this.fisher
@@ -39,7 +40,7 @@ export class InstanceExporter {
 
         // For each part of that path, we add fixed values from the SD
         let path = '';
-        for (const [i, pathPart] of pathParts.entries()) {
+        for (const [i, pathPart] of validated.pathParts.entries()) {
           path += `${path ? '.' : ''}${pathPart.base}`;
           // Add back non-numeric (slice) brackets
           pathPart.brackets?.forEach(b => (path += /^[-+]?\d+$/.test(b) ? '' : `[${b}]`));
@@ -51,14 +52,26 @@ export class InstanceExporter {
             .join('.');
           rulePathPart += '.';
 
-          this.setFixedValuesForDirectChildren(
+          this.setFixedValuesFromStructureDefinition(
             element,
             rulePathPart,
             instanceDef,
             instanceOfStructureDefinition
           );
         }
+      } catch (e) {
+        logger.error(e.message, rule.sourceInfo);
+      }
+    });
 
+    // Fix all values explicitly set in the instance rules (all rules will be FixValueRule)
+    rules.forEach(rule => {
+      try {
+        const { fixedValue, pathParts } = instanceOfStructureDefinition.validateValueAtPath(
+          rule.path,
+          rule.fixedValue,
+          this.fisher
+        );
         // Fix value fom the rule
         setPropertyOnInstance(instanceDef, pathParts, fixedValue);
       } catch (e) {
@@ -90,45 +103,53 @@ export class InstanceExporter {
    * @param {InstanceDefinition} instanceDef - The InstanceDefinition to fix values on
    * @param {StructureDefinition} instanceOfStructureDefinition - The structure definition the instance instantiates
    */
-  private setFixedValuesForDirectChildren(
+  private setFixedValuesFromStructureDefinition(
     element: ElementDefinition,
     existingPath: string,
     instanceDef: InstanceDefinition,
     instanceOfStructureDefinition: StructureDefinition
   ) {
-    const directChildren = element.children(true);
-    for (const child of directChildren) {
+    // We will fix values on the element, or direct children
+    const fixableElements = [element, ...element.children(true)];
+    for (const fixableElement of fixableElements) {
       // Fixed values may be specified by the fixed[x] or pattern[x] fields
-      const fixedValueKey = Object.keys(child).find(
+      const fixedValueKey = Object.keys(fixableElement).find(
         k => k.startsWith('fixed') || k.startsWith('pattern')
       );
-      if (fixedValueKey && child.min > 0) {
-        // Get the end of the child path, this is the part that differs from existingPath
-        const childPath = child
-          .diffId()
-          .split('.')
-          .slice(-1)[0];
+      // Fixed value can come from fixed[x] or pattern[x] directly on element, or via pattern[x] on parent
+      const foundFixedValue =
+        fixableElement[fixedValueKey as keyof ElementDefinition] ?? fixableElement.fixedByParent();
+      // We only fix the value if the element is the original element, or it is a direct child with card 1..n
+      if (foundFixedValue && (fixableElement.id === element.id || fixableElement.min > 0)) {
+        // Get the end of the path, this is the part that differs from existingPath
+        let fixablePath = fixableElement.diffId().replace(`${element.diffId()}`, '');
+        // If the fixableElement is a child of the original element, we must remove prefixed '.' from path
+        if (fixablePath.startsWith('.')) fixablePath = fixablePath.slice(1);
+
         // Turn FHIR slicing (element:slicName/resliceName) into FSH slicing (element[sliceName][resliceName])
-        const colonSplitPath = childPath.split(':');
-        let fshChildPath = colonSplitPath[0];
+        const colonSplitPath = fixablePath.split(':');
+        let fshElementPath = colonSplitPath[0];
         const slicePathSection = colonSplitPath[1];
         const sliceNames = slicePathSection?.split('/');
         sliceNames?.forEach(s => {
-          fshChildPath += `[${s}]`;
+          fshElementPath += `[${s}]`;
         });
+        // Fix the value if we validly can
         try {
           const { fixedValue, pathParts } = instanceOfStructureDefinition.validateValueAtPath(
-            existingPath + fshChildPath,
-            child[fixedValueKey as keyof ElementDefinition],
+            // fshElementPath is '' when the fixableElement is the original element, trailing '.' on this path must be removed
+            // Otherwise add the child path to existing path
+            fshElementPath === '' ? existingPath.slice(0, -1) : existingPath + fshElementPath,
+            foundFixedValue,
             this.fisher
           );
           setPropertyOnInstance(instanceDef, pathParts, fixedValue);
         } catch (e) {
           logger.error(e.message);
         }
-      } else if (fixedValueKey) {
+      } else if (foundFixedValue) {
         logger.debug(
-          `Element ${child.id} is optional with min cardinality 0, so fixed value for optional element is not set on instance ${instanceDef.instanceName}`
+          `Element ${fixableElement.id} is optional with min cardinality 0, so fixed value for optional element is not set on instance ${instanceDef.instanceName}`
         );
       }
     }
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index 67d5bd2..135e493 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -846,6 +846,19 @@ export class ElementDefinition {
     return true;
   }
 
+  /**
+   * Checks if an element is fixed by a pattern[x] on its direct parent
+   * @returns {any} the value the element is fixed to by its parent, undefined if value is not fixed
+   */
+  fixedByParent(): any {
+    const parent = this.parent();
+    const patternKey = parent ? Object.keys(parent).find(k => k.startsWith('pattern')) : null;
+    if (patternKey) {
+      const patternValue: any = parent[patternKey as keyof ElementDefinition];
+      return patternValue[this.path.replace(`${parent.path}.`, '')];
+    }
+  }
+
   /**
    * Fixes a boolean to this element.
    * @see {@link fixValue}


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 87cced9..030342a 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -470,6 +470,115 @@ describe('InstanceExporter', () => {
       ]);
     });
 
+    // Fixing with pattern[x]
+    it('should fix a nested element that is fixed by pattern[x] from a parent on the SD', () => {
+      const fixedValRule = new FixedValueRule('maritalStatus.coding');
+      fixedValRule.fixedValue = new FshCode('foo', 'http://foo.com');
+      patient.rules.push(fixedValRule);
+      const instanceFixedValRule = new FixedValueRule('maritalStatus.coding[0].version');
+      instanceFixedValRule.fixedValue = '1.2.3';
+      instance.rules.push(instanceFixedValRule);
+      const exported = exportInstance(instance);
+      expect(exported.maritalStatus).toEqual({
+        coding: [
+          {
+            code: 'foo',
+            system: 'http://foo.com',
+            version: '1.2.3'
+          }
+        ]
+      });
+    });
+
+    it('should fix multiple nested elements that are fixed by pattern[x] from a parent on the SD', () => {
+      const fixedValRule = new FixedValueRule('maritalStatus.coding');
+      fixedValRule.fixedValue = new FshCode('foo', 'http://foo.com');
+      patient.rules.push(fixedValRule);
+      const instanceFixedValRule = new FixedValueRule('maritalStatus.coding[0].version');
+      instanceFixedValRule.fixedValue = '1.2.3';
+      instance.rules.push(instanceFixedValRule);
+      const instanceFixedValRule2 = new FixedValueRule('maritalStatus.coding[1].version');
+      instanceFixedValRule2.fixedValue = '3.2.1';
+      instance.rules.push(instanceFixedValRule2);
+      const exported = exportInstance(instance);
+      expect(exported.maritalStatus).toEqual({
+        coding: [
+          {
+            code: 'foo',
+            system: 'http://foo.com',
+            version: '1.2.3'
+          },
+          {
+            code: 'foo',
+            system: 'http://foo.com',
+            version: '3.2.1'
+          }
+        ]
+      });
+    });
+
+    it('should fix a nested element that is fixed by array pattern[x] from a parent on the SD', () => {
+      const fixedValRule = new FixedValueRule('maritalStatus');
+      fixedValRule.fixedValue = new FshCode('foo', 'http://foo.com');
+      patient.rules.push(fixedValRule);
+      const instanceFixedValRule = new FixedValueRule('maritalStatus.coding[0].version');
+      instanceFixedValRule.fixedValue = '1.2.3';
+      instance.rules.push(instanceFixedValRule);
+      const exported = exportInstance(instance);
+      expect(exported.maritalStatus).toEqual({
+        coding: [
+          {
+            code: 'foo',
+            system: 'http://foo.com',
+            version: '1.2.3'
+          }
+        ]
+      });
+    });
+
+    it('should fix multiple nested elements that are fixed by array pattern[x] from a parent on the SD', () => {
+      const fixedValRule = new FixedValueRule('maritalStatus');
+      fixedValRule.fixedValue = new FshCode('foo', 'http://foo.com');
+      patient.rules.push(fixedValRule);
+      const instanceFixedValRule1 = new FixedValueRule('maritalStatus.coding[0].version');
+      instanceFixedValRule1.fixedValue = '1.2.3';
+      instance.rules.push(instanceFixedValRule1);
+      const instanceFixedValRule2 = new FixedValueRule('maritalStatus.coding[1].version');
+      instanceFixedValRule2.fixedValue = '3.2.1';
+      instance.rules.push(instanceFixedValRule2);
+      const exported = exportInstance(instance);
+      expect(exported.maritalStatus).toEqual({
+        coding: [
+          {
+            code: 'foo',
+            system: 'http://foo.com',
+            version: '1.2.3'
+          },
+          {
+            version: '3.2.1'
+          }
+        ]
+      });
+    });
+
+    it('should fix cardinality 1..n elements that are fixed by array pattern[x] from a parent on the SD', () => {
+      const fixedValRule = new FixedValueRule('maritalStatus');
+      fixedValRule.fixedValue = new FshCode('foo', 'http://foo.com');
+      patient.rules.push(fixedValRule);
+      const cardRule = new CardRule('maritalStatus');
+      cardRule.min = 1;
+      patient.rules.push(cardRule);
+      const exported = exportInstance(instance);
+      expect(exported.maritalStatus).toEqual({
+        coding: [
+          {
+            code: 'foo',
+            system: 'http://foo.com'
+          }
+        ]
+      });
+    });
+
     // TODO: The fixValue functions should be updated to not fix a value when a parent element sets
     // the value to something different using a pattern
     it.skip('should not fix an element to a value different than a parent pattern value on the Structure Definition', () => {
diff --git a/test/fhirtypes/ElementDefinition.fixValue.test.ts b/test/fhirtypes/ElementDefinition.fixValue.test.ts
index a361b3c..21c95ab 100644
--- a/test/fhirtypes/ElementDefinition.fixValue.test.ts
+++ b/test/fhirtypes/ElementDefinition.fixValue.test.ts
@@ -101,4 +101,27 @@ describe('ElementDefinition', () => {
       });
     });
   });
+  describe('#fixedByParent', () => {
+    it('should find a pattern value from the parent when it exists', () => {
+      const statusReason = medicationRequest.elements.find(
+        e => e.id === 'MedicationRequest.statusReason'
+      );
+      statusReason.fixValue(new FshCode('foo'));
+      const statusReasonCoding = medicationRequest.findElementByPath('statusReason.coding', fisher);
+      const patternValue = statusReasonCoding.fixedByParent();
+      expect(patternValue).toEqual([{ code: 'foo' }]);
+    });
+
+    it('should not find a pattern value from the parent when none is present', () => {
+      const statusReasonCoding = medicationRequest.findElementByPath('statusReason.coding', fisher);
+      const patternValue = statusReasonCoding.fixedByParent();
+      expect(patternValue).toBeUndefined();
+    });
+
+    it('should return undefined when being run on the root element', () => {
+      const root = medicationRequest.elements.find(e => e.id === 'MedicationRequest');
+      const patternValue = root.fixedByParent();
+      expect(patternValue).toBeUndefined();
+    });
+  });
 });