Repository: FHIR/sushi (ts)

Issue:
Fixing the parent of an already fixed value is not working as expected
When fixing the parent of a fixed value in a conflicting way, SUSHI does not log an error and fixes both the parent and the child, even though the child is fixed to a different value by each rule. An example of this is below:

An example of a profile that works as expected:
```
Profile: MyObs
Parent: Observation
* valueQuantity = 10 'mm'
* valueQuantity.value = 20
```
With the above profile, SUSHI will log an error that it "cannot fix 20 to this element; a different decimal is already fixed: 10" and the differential only has the 10 mm value fixed.

However, the following profile does not work as expected (switching the order of the two rules above):
```
Profile: MyObs
Parent: Observation
* valueQuantity.value = 20
* valueQuantity = 10 'mm'
```
With this profile, SUSHI will not log any errors and the conflicting values are both set in the differential as follows: 
```
{
  "id": "Observation.valueQuantity",
  "path": "Observation.valueQuantity",
  ...
  "patternQuantity": {
    "value": 10,
    "code": "mm",
    "system": "http://unitsofmeasure.org"
  }
},
{
  "id": "Observation.valueQuantity.value",
  "path": "Observation.valueQuantity.value",
  "patternDecimal": 20
}
```

Another example of this can be seen when fixing values using inline instances. The following example works similarly to the second example and does not log any errors and produces a conflicting differential:

```
Instance: USPostalAddress
InstanceOf: Address
Usage: #inline
* type = #postal
* country = "US"

Profile: USPatient
Parent: Patient
* address.country = "not-US"
* address = USPostalAddress
```

Pull request:
Fixing complex objects checks for fixed children
Fixes #560 and completes task [CIMPL-496](https://standardhealthrecord.atlassian.net/browse/CIMPL-496).

When fixing a complex object to an element, children of that element or of that element's slicedElement may already have fixed values. When this happens, make sure that the two values do not conflict with one another.

The case of "an instance is assigned to a slice (such as `address[RecentAddress]`), and a later fixed value on a non-slice (such as `address.period.start`) would violate that slice" is out of scope for this task.

Patch:
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index 949e400..9cdd029 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -1,4 +1,12 @@
-import { isEmpty, isEqual, isMatch, cloneDeep, upperFirst, intersectionWith } from 'lodash';
+import {
+  isEmpty,
+  isEqual,
+  isMatch,
+  cloneDeep,
+  upperFirst,
+  intersectionWith,
+  flatten
+} from 'lodash';
 import sax = require('sax');
 import { minify } from 'html-minifier';
 import { isUri } from 'valid-url';
@@ -1292,6 +1300,17 @@ export class ElementDefinition {
       throw new ValueAlreadyFixedError(fshValue, type, JSON.stringify(currentElementValue));
     }
 
+    // Children of elements with complex types such as Quantity may already have fixed values
+    this.children().forEach(child => this.checkChildFixedValue(child, fhirValue));
+
+    // if this is a slice, make sure that nothing on this.slicedElement() is being violated
+    const slicedElement = this.slicedElement();
+    if (slicedElement) {
+      slicedElement
+        .children()
+        .forEach(child => slicedElement.checkChildFixedValue(child, fhirValue));
+    }
+
     // If we made it this far, fix the value using fixed[x] or pattern[x] as appropriate
     if (exactly) {
       // @ts-ignore: Type 'any' is not assignable to type 'never'
@@ -1304,6 +1323,35 @@ export class ElementDefinition {
     }
   }
 
+  private checkChildFixedValue(child: ElementDefinition, fhirValue: any) {
+    const childType = child.type[0].code;
+    const fixedX = `fixed${upperFirst(childType)}` as keyof ElementDefinition;
+    const patternX = `pattern${upperFirst(childType)}` as keyof ElementDefinition;
+    const currentChildValue = child[fixedX] ?? child[patternX];
+    if (currentChildValue != null) {
+      // find the element on fhirValue that would get assigned to the child.
+      // sometimes, there may be arrays inside complex object types, such as CodeableConcept.
+      // therefore, flatten those arrays as the path is traversed so that all possible new values are found.
+      const childPath = child.path.replace(`${this.path}.`, '').split('.');
+      let newChildValue = [fhirValue];
+      for (const pathPart of childPath) {
+        newChildValue = flatten(
+          newChildValue.map(value => {
+            return value?.[pathPart];
+          })
+        );
+      }
+      newChildValue.forEach(value => {
+        if (value != null) {
+          const childCompareFn = typeof value === 'object' ? isMatch : isEqual;
+          if (!childCompareFn(value, currentChildValue)) {
+            throw new ValueAlreadyFixedError(value, childType, JSON.stringify(currentChildValue));
+          }
+        }
+      });
+    }
+  }
+
   /**
    * Checks if an element is fixed by a fixed[x] or pattern[x] on its direct parent
    * @returns {any} the value the element is fixed to by its parent, undefined if value is not fixed


Test patch:
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 956b69c..47e3328 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -11,7 +11,8 @@ import {
   FshValueSet,
   FshCodeSystem,
   Invariant,
-  RuleSet
+  RuleSet,
+  FshQuantity
 } from '../../src/fshtypes';
 import {
   CardRule,
@@ -1865,6 +1866,220 @@ describe('StructureDefinitionExporter', () => {
     expect(loggerSpy.getLastMessage()).toMatch(/File: Fixed\.fsh.*Line: 4\D*/s);
   });
 
+  it('should not apply a FixedValueRule to a parent element when it would conflict with a child element', () => {
+    // Profile: MyObs
+    // Parent: Observation
+    // * valueQuantity.value = 20
+    // * valueQuantity = 10 'mm'
+    const profile = new Profile('MyObs');
+    profile.parent = 'Observation';
+    const valueRule = new FixedValueRule('valueQuantity.value')
+      .withFile('Fixed.fsh')
+      .withLocation([3, 8, 3, 29]);
+    valueRule.fixedValue = 20;
+    const quantityRule = new FixedValueRule('valueQuantity')
+      .withFile('Fixed.fsh')
+      .withLocation([4, 8, 4, 27]);
+    quantityRule.fixedValue = new FshQuantity(10, new FshCode('mm', 'http://unitsofmeasure.org'));
+    profile.rules.push(valueRule, quantityRule);
+
+    exporter.exportStructDef(profile);
+    const sd = pkg.profiles[0];
+    const childElement = sd.findElement('Observation.value[x]:valueQuantity.value');
+    const parentElement = sd.findElement('Observation.value[x]:valueQuantity');
+
+    expect(childElement.patternDecimal).toBe(20);
+    expect(parentElement.patternQuantity).toBeUndefined();
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Cannot fix 10 to this element.*File: Fixed\.fsh.*Line: 4\D*/s
+    );
+  });
+
+  it('should not apply a FixedValueRule to a complex typed element when it would conflict with a child element present in an array in the type', () => {
+    // Profile: MyObs
+    // Parent: Observation
+    // * valueCodeableConcept.coding.code = #pancake
+    // * valueCodeableConcept = #waffle // this rule should not be applied
+    const profile = new Profile('MyObs');
+    profile.parent = 'Observation';
+    const innerRule = new FixedValueRule('valueCodeableConcept.coding.code');
+    innerRule.fixedValue = new FshCode('pancake');
+    const outerRule = new FixedValueRule('valueCodeableConcept')
+      .withFile('Fixed.fsh')
+      .withLocation([4, 9, 4, 33]);
+    outerRule.fixedValue = new FshCode('waffle');
+    profile.rules.push(innerRule, outerRule);
+
+    exporter.exportStructDef(profile);
+    const sd = pkg.profiles[0];
+    const innerElement = sd.findElement('Observation.value[x]:valueCodeableConcept.coding.code');
+    const outerElement = sd.findElement('Observation.value[x]:valueCodeableConcept');
+    expect(innerElement.patternCode).toBe('pancake');
+    expect(outerElement.patternCodeableConcept).toBeUndefined();
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Cannot fix waffle to this element.*File: Fixed\.fsh.*Line: 4\D*/s
+    );
+  });
+
+  it('should not apply a FixedValueRule to a slice when it would conflict with a child of the list element', () => {
+    // Instance: CustomPostalAddress
+    // InstanceOf: Address
+    // Usage: #inline
+    // * period.start = "2020-04-01"
+    const customPostalAddress = new Instance('CustomPostalAddress');
+    customPostalAddress.instanceOf = 'Address';
+    customPostalAddress.usage = 'Inline';
+    const customStart = new FixedValueRule('period.start');
+    customStart.fixedValue = '2020-04-01';
+
+    customPostalAddress.rules.push(customStart);
+    doc.instances.set(customPostalAddress.name, customPostalAddress);
+
+    // Profile: MovingPatient
+    // Parent: Patient
+    // * address.period.start = "1998-07-04"
+    // * address ^slicing.discriminator[0].type = #pattern
+    // * address ^slicing.discriminator[0].path = "$this"
+    // * address ^slicing.rules = #open
+    // * address contains RecentAddress 1..1
+    // * address[RecentAddress] = CustomPostalAddress // this rule should not be applied
+    const movingPatient = new Profile('MovingPatient');
+    movingPatient.parent = 'Patient';
+    const movingStart = new FixedValueRule('address.period.start');
+    movingStart.fixedValue = '1998-07-04';
+    const slicingType = new CaretValueRule('address');
+    slicingType.caretPath = 'slicing.discriminator[0].type';
+    slicingType.value = new FshCode('pattern');
+    const slicingPath = new CaretValueRule('address');
+    slicingPath.caretPath = 'slicing.discriminator[0].path';
+    slicingPath.value = 'code';
+    const slicingRules = new CaretValueRule('address');
+    slicingRules.caretPath = 'slicing.rules';
+    slicingRules.value = new FshCode('open');
+    const recentAddress = new ContainsRule('address');
+    recentAddress.items.push({ name: 'RecentAddress' });
+    const recentCard = new CardRule('address[RecentAddress]');
+    recentCard.min = 1;
+    recentCard.max = '1';
+    const recentInstance = new FixedValueRule('address[RecentAddress]')
+      .withFile('Fixed.fsh')
+      .withLocation([8, 9, 8, 54]);
+    recentInstance.fixedValue = 'CustomPostalAddress';
+    recentInstance.isInstance = true;
+
+    movingPatient.rules.push(
+      movingStart,
+      slicingType,
+      slicingPath,
+      slicingRules,
+      recentAddress,
+      recentCard,
+      recentInstance
+    );
+
+    exporter.exportStructDef(movingPatient);
+    const sd = pkg.profiles[0];
+    const periodStartElement = sd.findElement('Patient.address.period.start');
+    const addressSliceElement = sd.findElement('Patient.address:RecentAddress');
+
+    expect(periodStartElement.patternDateTime).toBe('1998-07-04');
+    expect(addressSliceElement.patternAddress).toBeUndefined();
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Cannot fix 2020-04-01 to this element.*File: Fixed\.fsh.*Line: 8\D*/s
+    );
+  });
+
+  it('should not apply a FixedValueRule to a slice when it would conflict with a child slice of the list element', () => {
+    // Instance: CustomPostalAddress
+    // InstanceOf: Address
+    // Usage: #inline
+    // * line[0] = "First part of address"
+    const customPostalAddress = new Instance('CustomPostalAddress');
+    customPostalAddress.instanceOf = 'Address';
+    customPostalAddress.usage = 'Inline';
+    const customLine = new FixedValueRule('line[0]');
+    customLine.fixedValue = 'First part of address';
+    customPostalAddress.rules.push(customLine);
+    doc.instances.set(customPostalAddress.name, customPostalAddress);
+
+    // Profile: CustomPatient
+    // Parent: Patient
+    // * address.line ^slicing.discriminator[0].type = #pattern
+    // * address.line ^slicing.discriminator[0].path = "$this"
+    // * address.line ^slicing.rules = #open
+    // * address.line contains SpecificLine 1..1
+    // * address.line[SpecificLine] = "Specific part of address"
+    // * address ^slicing.discriminator[0].type = #pattern
+    // * address ^slicing.discriminator[0].path = "$this"
+    // * address ^slicing.rules = #open
+    // * address contains RecentAddress 1..1
+    // * address[RecentAddress] = CustomPostalAddress // this rule should not be applied
+    const customPatient = new Profile('CustomPatient');
+    customPatient.parent = 'Patient';
+
+    const slicingTypeLine = new CaretValueRule('address.line');
+    slicingTypeLine.caretPath = 'slicing.discriminator[0].type';
+    slicingTypeLine.value = new FshCode('pattern');
+    const slicingPathLine = new CaretValueRule('address.line');
+    slicingPathLine.caretPath = 'slicing.discriminator[0].path';
+    slicingPathLine.value = 'code';
+    const slicingRulesLine = new CaretValueRule('address.line');
+    slicingRulesLine.caretPath = 'slicing.rules';
+    slicingRulesLine.value = new FshCode('open');
+    const containsSpecificLine = new ContainsRule('address.line');
+    containsSpecificLine.items.push({ name: 'SpecificLine' });
+    const specificLineCard = new CardRule('address.line[SpecificLine]');
+    specificLineCard.min = 1;
+    specificLineCard.max = '1';
+    const specificLine = new FixedValueRule('address.line[SpecificLine]');
+    specificLine.fixedValue = 'Specific part of address';
+
+    const slicingType = new CaretValueRule('address');
+    slicingType.caretPath = 'slicing.discriminator[0].type';
+    slicingType.value = new FshCode('pattern');
+    const slicingPath = new CaretValueRule('address');
+    slicingPath.caretPath = 'slicing.discriminator[0].path';
+    slicingPath.value = 'code';
+    const slicingRules = new CaretValueRule('address');
+    slicingRules.caretPath = 'slicing.rules';
+    slicingRules.value = new FshCode('open');
+    const recentAddress = new ContainsRule('address');
+    recentAddress.items.push({ name: 'RecentAddress' });
+    const recentCard = new CardRule('address[RecentAddress]');
+    recentCard.min = 1;
+    recentCard.max = '1';
+    const recentInstance = new FixedValueRule('address[RecentAddress]')
+      .withFile('Fixed.fsh')
+      .withLocation([12, 9, 12, 54]);
+    recentInstance.fixedValue = 'CustomPostalAddress';
+    recentInstance.isInstance = true;
+
+    customPatient.rules.push(
+      slicingTypeLine,
+      slicingPathLine,
+      slicingRulesLine,
+      containsSpecificLine,
+      specificLineCard,
+      specificLine,
+      slicingType,
+      slicingPath,
+      slicingRules,
+      recentAddress,
+      recentCard,
+      recentInstance
+    );
+
+    exporter.exportStructDef(customPatient);
+    const sd = pkg.profiles[0];
+    const addressLineElement = sd.findElement('Patient.address.line:SpecificLine');
+    const addressSliceElement = sd.findElement('Patient.address:RecentAddress');
+    expect(addressLineElement.patternString).toBeDefined();
+    expect(addressSliceElement.patternAddress).toBeUndefined();
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Cannot fix First part of address to this element.*File: Fixed\.fsh.*Line: 12\D*/s
+    );
+  });
+
   // Contains Rule
   it('should apply a ContainsRule on an element with defined slicing', () => {
     const profile = new Profile('Foo');