Repository: FHIR/sushi (ts)

Issue:
Fixed codes don't account for parent elements w/ pattern[x]
Currently, when the user tries to fix a value, we check the Structure Definition to see if that value has already been set on the element by a pattern or fixed field. However, the pattern field can be used to specify which values children of the element must have. We do not account for this. For example, if Patient.maritalStatus has a field like:

```
"patternCodeableConcept": {
  "coding": [
    { "code": "foo" }
  ]
}
```

We currently still allow the user to re-fix the value of Patient.maritalStatus.coding[0].code. This behavior is incorrect, and should be fixed to not allow this value to be fixed to something other than "foo"

Pull request description:
Account for pattern on parent
Fixes #84.

Previously when fixing values, we did not account for values being fixed via their parents by a pattern. This PR adds support for checking if a value is fixed by pattern from its parent, and we now throw an error if we try to re-set a value that is fixed by pattern.

If you'd like to test it out, fixing `CodeableConcept` elements works with pattern, so they provide a good test case. For example, the following will now generate an error, which was not the case with master:
```
Profile: myPatient
Parent: Patient
* maritalStatus = http://wedding.com#married
* maritalStatus.coding.code = #plentyoffish
```

Patch:
diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts
index c08b977..3fa867c 100644
--- a/src/export/InstanceExporter.ts
+++ b/src/export/InstanceExporter.ts
@@ -105,7 +105,8 @@ export class InstanceExporter {
       );
       // 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();
+        fixableElement[fixedValueKey as keyof ElementDefinition] ??
+        fixableElement.fixedByDirectParent();
       // 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
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index ea5c177..3901e72 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -843,6 +843,9 @@ export class ElementDefinition {
     currentElementValue: boolean | string | number,
     elementType: string
   ): true | undefined {
+    if (currentElementValue == null) {
+      currentElementValue = this.fixedByAnyParent();
+    }
     if (currentElementValue != null && currentElementValue !== value) {
       throw new ValueAlreadyFixedError(value, elementType, currentElementValue);
     }
@@ -853,7 +856,7 @@ export class ElementDefinition {
    * 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 {
+  fixedByDirectParent(): any {
     const parent = this.parent();
     const patternKey = parent ? Object.keys(parent).find(k => k.startsWith('pattern')) : null;
     if (patternKey) {
@@ -862,6 +865,39 @@ export class ElementDefinition {
     }
   }
 
+  /**
+   * Checks if an element is fixed by a pattern[x] on any of its parents
+   * @returns {any} the value the element is fixed to by its parent, undefined if value is not fixed
+   */
+  fixedByAnyParent(): any {
+    const parent = this.parent();
+    if (parent == null) {
+      return;
+    } else {
+      let fixedValue = this.fixedByDirectParent();
+      if (fixedValue == null) {
+        // Get the value from the parent, and index into that value
+        const parentValue = parent.fixedByAnyParent();
+        const childIndex = this.path.replace(`${parent.path}.`, '');
+        if (Array.isArray(parentValue)) {
+          // If the value is an array, there are two cases
+          // 1 - All the fixed values in the array match => return the value
+          // 2 - The fixed values in the array don't match => return [val1, val2]
+          // since values do exist, but they conflict so no value should be allowed to be set
+          // and for any value [val1, val2] != value
+          fixedValue =
+            parentValue.every(pv => pv[childIndex] === parentValue[0][childIndex]) &&
+            parentValue.length > 0
+              ? parentValue[0][childIndex]
+              : parentValue.map(pv => pv[childIndex]);
+        } else {
+          fixedValue = parentValue?.[childIndex];
+        }
+      }
+      return fixedValue;
+    }
+  }
+
   /**
    * Fixes a boolean to this element.
    * @see {@link fixValue}
@@ -1220,14 +1256,15 @@ export class ElementDefinition {
    */
   private fixFshCodeToCodeableConcept(code: FshCode): void {
     // Check if this is already fixed to something else
-    if (this.patternCodeableConcept) {
+    const alreadyFixedValue = this.patternCodeableConcept ?? this.fixedByAnyParent();
+    if (alreadyFixedValue) {
       const fixedToSame =
-        this.patternCodeableConcept.coding &&
-        this.patternCodeableConcept.coding.some(c => {
+        alreadyFixedValue.coding &&
+        alreadyFixedValue.coding.some((c: any) => {
           return c.code == code.code && c.system == code.system;
         });
       if (!fixedToSame) {
-        const found = this.patternCodeableConcept.coding[0];
+        const found = alreadyFixedValue.coding[0];
         throw new CodeAlreadyFixedError(new FshCode(found.code, found.system), code);
       }
       // It's already fixed, so there is nothing to do
@@ -1256,10 +1293,13 @@ export class ElementDefinition {
    */
   private fixFshCodeToCoding(code: FshCode): void {
     // Check if this is already fixed to something else
-    if (this.patternCoding) {
-      if (this.patternCoding.code != code.code || this.patternCoding.system != code.system) {
-        const found = this.patternCoding;
-        throw new CodeAlreadyFixedError(new FshCode(found.code, found.system), code);
+    const alreadyFixedValue = this.patternCoding ?? this.fixedByAnyParent();
+    if (alreadyFixedValue) {
+      if (alreadyFixedValue.code != code.code || alreadyFixedValue.system != code.system) {
+        throw new CodeAlreadyFixedError(
+          new FshCode(alreadyFixedValue.code, alreadyFixedValue.system),
+          code
+        );
       }
       // It's already fixed, so there is nothing to do
       return;
@@ -1284,10 +1324,13 @@ export class ElementDefinition {
    */
   private fixFshCodeToQuantityUnitCode(code: FshCode): void {
     // Check if this is already fixed to something else
-    if (this.patternQuantity) {
-      if (this.patternQuantity.code != code.code || this.patternQuantity.system != code.system) {
-        const found = this.patternQuantity;
-        throw new CodeAlreadyFixedError(new FshCode(found.code, found.system), code);
+    const alreadyFixedValue = this.patternQuantity ?? this.fixedByAnyParent();
+    if (alreadyFixedValue) {
+      if (alreadyFixedValue.code != code.code || alreadyFixedValue.system != code.system) {
+        throw new CodeAlreadyFixedError(
+          new FshCode(alreadyFixedValue.code, alreadyFixedValue.system),
+          code
+        );
       }
       // It's already fixed, so there is nothing to do
       return;
@@ -1311,9 +1354,10 @@ export class ElementDefinition {
    */
   private fixFshCodeToCode(code: FshCode): void {
     // Check if this is already fixed to something else
-    if (this.fixedCode) {
-      if (this.fixedCode != code.code) {
-        throw new CodeAlreadyFixedError(new FshCode(this.fixedCode), code);
+    const alreadyFixedValue = this.fixedCode ?? this.fixedByAnyParent();
+    if (alreadyFixedValue) {
+      if (alreadyFixedValue != code.code) {
+        throw new CodeAlreadyFixedError(new FshCode(alreadyFixedValue), code);
       }
       // It's already fixed, so there is nothing to do
       return;
@@ -1331,9 +1375,10 @@ export class ElementDefinition {
    */
   private fixFshCodeToString(code: FshCode): void {
     // Check if this is already fixed to something else
-    if (this.fixedString) {
-      if (this.fixedString != code.code) {
-        throw new CodeAlreadyFixedError(new FshCode(this.fixedString), code);
+    const alreadyFixedValue = this.fixedString ?? this.fixedByAnyParent();
+    if (alreadyFixedValue) {
+      if (alreadyFixedValue != code.code) {
+        throw new CodeAlreadyFixedError(new FshCode(alreadyFixedValue), code);
       }
       // It's already fixed, so there is nothing to do
       return;
@@ -1351,9 +1396,10 @@ export class ElementDefinition {
    */
   private fixFshCodeToUri(code: FshCode): void {
     // Check if this is already fixed to something else
-    if (this.fixedUri) {
-      if (this.fixedUri != code.code) {
-        throw new CodeAlreadyFixedError(new FshCode(this.fixedUri), code);
+    const alreadyFixedValue = this.fixedUri ?? this.fixedByAnyParent();
+    if (alreadyFixedValue) {
+      if (alreadyFixedValue != code.code) {
+        throw new CodeAlreadyFixedError(new FshCode(alreadyFixedValue), code);
       }
       // It's already fixed, so there is nothing to do
       return;


Test patch:
diff --git a/test/fhirtypes/ElementDefinition.fixFshCode.test.ts b/test/fhirtypes/ElementDefinition.fixFshCode.test.ts
index fc8e0fd..740ffcb 100644
--- a/test/fhirtypes/ElementDefinition.fixFshCode.test.ts
+++ b/test/fhirtypes/ElementDefinition.fixFshCode.test.ts
@@ -47,6 +47,18 @@ describe('ElementDefinition', () => {
       expect(clone).toEqual(concept);
     });
 
+    it('should throw CodeAlreadyFixedError when fixing a code to a CodeableConcept fixed to a different code by a parent', () => {
+      const rr = observation.elements.find(e => e.id === 'Observation.referenceRange');
+      // @ts-ignore
+      rr.patternBackboneElement = { type: { coding: [{ system: 'http://foo.com', code: 'bar' }] } };
+      const rrType = observation.elements.find(e => e.id === 'Observation.referenceRange.type');
+      const clone = cloneDeep(rrType);
+      expect(() => {
+        rrType.fixFshCode(barFooCode);
+      }).toThrow(/http:\/\/bar.com#foo.*http:\/\/foo.com#bar/);
+      expect(clone).toEqual(rrType);
+    });
+
     it('should fix a code to a Coding', () => {
       const concept = observation.elements.find(e => e.id === 'Observation.code');
       concept.unfold(fisher);
@@ -68,6 +80,22 @@ describe('ElementDefinition', () => {
       expect(clone).toEqual(coding);
     });
 
+    it('should throw CodeAlreadyFixedError when fixing a code to a Coding fixed to a different code by a parent', () => {
+      const rr = observation.elements.find(e => e.id === 'Observation.referenceRange');
+      // @ts-ignore
+      rr.patternBackboneElement = { type: { coding: { system: 'http://foo.com', code: 'bar' } } };
+      const rrType = observation.elements.find(e => e.id === 'Observation.referenceRange.type');
+      rrType.unfold(fisher);
+      const rrTypeCoding = observation.elements.find(
+        e => e.id === 'Observation.referenceRange.type.coding'
+      );
+      const clone = cloneDeep(rrTypeCoding);
+      expect(() => {
+        clone.fixFshCode(barFooCode);
+      }).toThrow(/http:\/\/bar.com#foo.*http:\/\/foo.com#bar/);
+      expect(clone).toEqual(rrTypeCoding);
+    });
+
     it('should fix a code to a code', () => {
       const code = observation.elements.find(e => e.id === 'Observation.status');
       code.fixFshCode(fooBarCode);
@@ -85,6 +113,23 @@ describe('ElementDefinition', () => {
       expect(clone).toEqual(code);
     });
 
+    it('should throw CodeAlreadyFixedError when fixing a code to a code fixed to a different code by a parent', () => {
+      const code = observation.elements.find(e => e.id === 'Observation.code');
+      code.unfold(fisher);
+      const coding = observation.elements.find(e => e.id === 'Observation.code.coding');
+      coding.unfold(fisher);
+      const codeCodingCode = observation.elements.find(
+        e => e.id === 'Observation.code.coding.code'
+      );
+      // Setup original fixed code
+      code.fixFshCode(fooBarCode);
+      const clone = cloneDeep(codeCodingCode);
+      expect(() => {
+        clone.fixFshCode(barFooCode);
+      }).toThrow(/http:\/\/bar.com#foo.*#bar/);
+      expect(clone).toEqual(codeCodingCode);
+    });
+
     it('should fix a code to a Quantity', () => {
       const quantity = observation.elements.find(e => e.id === 'Observation.referenceRange.low');
       quantity.fixFshCode(fooBarCode);
@@ -102,6 +147,18 @@ describe('ElementDefinition', () => {
       expect(clone).toEqual(quantity);
     });
 
+    it('should throw CodeAlreadyFixedError when fixing a code to a Quantity fixed to a different code by a parent', () => {
+      const rr = observation.elements.find(e => e.id === 'Observation.referenceRange');
+      // @ts-ignore
+      rr.patternBackboneElement = { low: { system: 'http://foo.com', code: 'bar' } };
+      const rrLow = observation.elements.find(e => e.id === 'Observation.referenceRange.low');
+      const clone = cloneDeep(rrLow);
+      expect(() => {
+        clone.fixFshCode(barFooCode);
+      }).toThrow(/http:\/\/bar.com#foo.*http:\/\/foo.com#bar/);
+      expect(clone).toEqual(rrLow);
+    });
+
     it('should fix a code to a string', () => {
       const string = observation.elements.find(e => e.id === 'Observation.referenceRange.text');
       string.fixFshCode(fooBarCode);
@@ -119,6 +176,19 @@ describe('ElementDefinition', () => {
       expect(clone).toEqual(string);
     });
 
+    it('should throw CodeAlreadyFixedError when fixing a code to a string fixed to a different string by a parent', () => {
+      const code = observation.elements.find(e => e.id === 'Observation.code');
+      code.patternCodeableConcept = { text: 'http://foo.com#bar' };
+      code.unfold(fisher);
+      const string = observation.elements.find(e => e.id === 'Observation.code.text');
+      // Setup original fixed code
+      const clone = cloneDeep(string);
+      expect(() => {
+        clone.fixFshCode(barFooCode);
+      }).toThrow(/http:\/\/bar.com#foo.*#bar/);
+      expect(clone).toEqual(string);
+    });
+
     it('should fix a code to a uri', () => {
       const uri = observation.elements.find(e => e.id === 'Observation.implicitRules');
       uri.fixFshCode(fooBarCode);
@@ -136,6 +206,18 @@ describe('ElementDefinition', () => {
       expect(clone).toEqual(uri);
     });
 
+    it('should throw CodeAlreadyFixedError when fixing a code to a uri fixed to a different uri by a parent', () => {
+      const rrLow = observation.elements.find(e => e.id === 'Observation.referenceRange.low');
+      rrLow.patternQuantity = { system: 'http://foo.com#bar' };
+      rrLow.unfold(fisher);
+      const uri = observation.elements.find(e => e.id === 'Observation.referenceRange.low.system');
+      const clone = cloneDeep(uri);
+      expect(() => {
+        clone.fixFshCode(barFooCode);
+      }).toThrow(/http:\/\/bar.com#foo.*#bar/);
+      expect(clone).toEqual(uri);
+    });
+
     it('should throw CodedTypeNotFoundError when binding to an unsupported type', () => {
       const instant = observation.elements.find(e => e.id === 'Observation.issued');
       const clone = cloneDeep(instant);
diff --git a/test/fhirtypes/ElementDefinition.fixValue.test.ts b/test/fhirtypes/ElementDefinition.fixValue.test.ts
index 21c95ab..79b8e45 100644
--- a/test/fhirtypes/ElementDefinition.fixValue.test.ts
+++ b/test/fhirtypes/ElementDefinition.fixValue.test.ts
@@ -100,27 +100,114 @@ describe('ElementDefinition', () => {
         display: 'bar'
       });
     });
+
+    it('should throw ValueAlreadyFixedError when fixing a value fixed via parent pattern', () => {
+      const medicationForm = medication.elements.find(e => e.id === 'Medication.form');
+      medicationForm.fixValue(new FshCode('foo', 'http://thankYouForSettingMe.com'));
+      const medicationFormCodingSystem = medication.findElementByPath('form.coding.system', fisher);
+      expect(() => {
+        medicationFormCodingSystem.fixValue('http://ohManIWillNeverGetSet.sad');
+      }).toThrow(
+        'Cannot fix http://ohManIWillNeverGetSet.sad to this element; a different uri is already fixed: http://thankYouForSettingMe.com'
+      );
+    });
+
+    it('should throw ValueAlreadyFixedError when fixing a value fixed via parent pattern to a conflicting array', () => {
+      const medicationForm = medication.elements.find(e => e.id === 'Medication.form');
+      medicationForm.patternCodeableConcept = { coding: [{ system: 'foo' }, { system: 'bar' }] };
+      const medicationFormCodingSystem = medication.findElementByPath('form.coding.system', fisher);
+      expect(() => {
+        medicationFormCodingSystem.fixValue('baz');
+      }).toThrow('Cannot fix baz to this element; a different uri is already fixed: foo,bar');
+    });
   });
-  describe('#fixedByParent', () => {
+
+  describe('#fixedByDirectParent', () => {
     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();
+      const patternValue = statusReasonCoding.fixedByDirectParent();
       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();
+      const patternValue = statusReasonCoding.fixedByDirectParent();
+      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.fixedByDirectParent();
+      expect(patternValue).toBeUndefined();
+    });
+  });
+
+  describe('#fixedByAnyParent', () => {
+    it('should find a pattern value from a direct 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.fixedByAnyParent();
+      expect(patternValue).toEqual([{ code: 'foo' }]);
+    });
+
+    it('should find a pattern value from a grandparent when it exists', () => {
+      const identifier = medicationRequest.elements.find(
+        e => e.id === 'MedicationRequest.identifier'
+      );
+      identifier.max = '1';
+      // @ts-ignore
+      identifier.patternIdentifier = { period: { start: '2011-11-11' } };
+      const identifierPeriodStart = medicationRequest.findElementByPath(
+        'identifier.period.start',
+        fisher
+      );
+      const patternValue = identifierPeriodStart.fixedByAnyParent();
+      expect(patternValue).toBe('2011-11-11');
+    });
+
+    it('should find an array pattern value from a grandparent when it exists', () => {
+      const statusReason = medicationRequest.elements.find(
+        e => e.id === 'MedicationRequest.statusReason'
+      );
+      statusReason.fixValue(new FshCode('foo', 'bar'));
+      const statusReasonCodingSystem = medicationRequest.findElementByPath(
+        'statusReason.coding.system',
+        fisher
+      );
+      // Single element in array
+      let patternValue = statusReasonCodingSystem.fixedByAnyParent();
+      expect(patternValue).toBe('bar');
+
+      // Multiple not matching array elements
+      statusReason.patternCodeableConcept = { coding: [{ system: 'foo' }, { system: 'bar' }] };
+      patternValue = statusReasonCodingSystem.fixedByAnyParent();
+      expect(patternValue).toEqual(['foo', 'bar']);
+
+      // Multiple matching array elements
+      statusReason.patternCodeableConcept = { coding: [{ system: 'foo' }, { system: 'foo' }] };
+      patternValue = statusReasonCodingSystem.fixedByAnyParent();
+      expect(patternValue).toBe('foo');
+    });
+
+    it('should not find a pattern value from the parent when none is present', () => {
+      const statusReasonCoding = medicationRequest.findElementByPath(
+        'statusReason.coding.version',
+        fisher
+      );
+      const patternValue = statusReasonCoding.fixedByAnyParent();
       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();
+      const patternValue = root.fixedByAnyParent();
       expect(patternValue).toBeUndefined();
     });
   });