Repository: FHIR/sushi
Pull request #70 (2020-01-03 14:17:31)

Fix value on Instance from Structure Definition
This addresses https://standardhealthrecord.atlassian.net/browse/CIMPL-189: Automatically set fixed values on instance from profile.

The desired behavior is that fixed values are set on an instance for one of two reasons:
*  If a top level (aka the element is not nested within some other element) element in the SD that we are making an instance of has a fixed value.
* If the direct parent of a nested element is created by the user within the instance, and the nested element has a fixed value.

For example, let's say we made a profile of Patient, https://www.hl7.org/fhir/patient.html, and `active` was fixed to `true` on this profile. Then when making an instance of Patient, `active` is auto set to true since it is top level. If on the profile of patient we set `communication.preferred` to `true`, then `communication.preferred` is only auto set to `true` on the instance if the user writes a rule on the instance that creates the `communication` object. So this would be any rule that fixes a value on `communication` or a child of `communication`.

ngfreiter: Ok, so the latest commit adds some tests to verify that we are checking that values aren't being re-fixed to a value different than that on the Structure Definition. However, we did realize that there is an issue with how values are being fixed. If an element has a parent element that is fixed via a pattern, the user can still set the child elements to a value different than that required by the pattern. This is really an issue with the `fixValue` function, which is used as a part of this PR, but we decided this is a separate issue and it will not be addressed here. I added a test that is currently skipped that should pass when we fix this issue. I'll also add this issue to our backlog.
ngfreiter: Here is the new issue https://standardhealthrecord.atlassian.net/browse/CIMPL-200 mentioned above.

Patch:
diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts
index 4f550ea..b13b865 100644
--- a/src/export/InstanceExporter.ts
+++ b/src/export/InstanceExporter.ts
@@ -1,5 +1,11 @@
 import { FSHTank } from '../import/FSHTank';
-import { StructureDefinition, InstanceDefinition, ResolveFn } from '../fhirtypes';
+import {
+  StructureDefinition,
+  InstanceDefinition,
+  ResolveFn,
+  ElementDefinition,
+  PathPart
+} from '../fhirtypes';
 import { Instance } from '../fshtypes';
 import { FHIRDefinitions } from '../fhirdefs';
 import { logger } from '../utils/FSHLogger';
@@ -27,11 +33,61 @@ export class InstanceExporter {
       );
 
       setPropertyOnInstance(instanceDef, pathParts, fixedValue);
+
+      // For each part of that path, we add fixed values from the SD
+      let path = '';
+      for (const [i, pathPart] of pathParts.entries()) {
+        path += `${path ? '.' : ''}${pathPart.base}`;
+        const element = instanceOfStructureDefinition.findElementByPath(path, this.resolve);
+        this.setFixedValuesForDirectChildren(element, pathParts.slice(0, i + 1), instanceDef);
+      }
     });
 
+    // Fix values from the SD for all elements at the top level of the SD
+    this.setFixedValuesForDirectChildren(
+      instanceOfStructureDefinition.findElement(instanceDef.resourceType),
+      [],
+      instanceDef
+    );
+
     return instanceDef;
   }
 
+  /**
+   * Given an ElementDefinition, set fixed values for the direct children of that element
+   * according to the ElementDefinitions of the children
+   * @param {ElementDefinition} element - The element whose children we will fix
+   * @param {PathPart[]} existingPath - The path to the element whose children we will fix
+   * @param {InstanceDefinition} instanceDef - The InstanceDefinition to fix values on
+   */
+  private setFixedValuesForDirectChildren(
+    element: ElementDefinition,
+    existingPath: PathPart[],
+    instanceDef: InstanceDefinition
+  ) {
+    const directChildren = element.children(true);
+    for (const child of directChildren) {
+      // Fixed values may be specified by the fixed[x] or pattern[x] fields
+      const fixedValueKey = Object.keys(child).find(
+        k => k.startsWith('fixed') || k.startsWith('pattern')
+      );
+      if (fixedValueKey) {
+        // Get the end of the child path, this is the part that differs from existingPath
+        const childPathPart = {
+          base: child
+            .diffId()
+            .split('.')
+            .slice(-1)[0]
+        };
+        setPropertyOnInstance(
+          instanceDef,
+          [...existingPath, childPathPart],
+          child[fixedValueKey as keyof ElementDefinition]
+        );
+      }
+    }
+  }
+
   exportInstance(fshDefinition: Instance): InstanceDefinition {
     const instanceOfStructureDefinition = this.resolve(fshDefinition.instanceOf);
 
@@ -47,11 +103,9 @@ export class InstanceExporter {
     instanceDef.resourceType = instanceOfStructureDefinition.type; // ResourceType is determined by the StructureDefinition of the type
     instanceDef.instanceName = fshDefinition.id; // This is name of the instance in the FSH
 
-    // All other values of the instance will be fixedValues explicitly on the FSH Instance
+    // Set Fixed values based on the FSH rules and the Structure Definition
     instanceDef = this.setFixedValues(fshDefinition, instanceDef, instanceOfStructureDefinition);
 
-    // Add any set values that were set on the StructDef but not explicitly on the FSH Instance
-
     return instanceDef;
   }
 
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index 332cefe..b0a37b4 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -308,17 +308,24 @@ export class ElementDefinition {
         diff[prop] = cloneDeep(this[prop]);
       }
     }
-    // If the id is a choice slice, we change the id/path in the differential to use a shortcut syntax
-    // described here https://blog.fire.ly/2019/09/13/type-slicing-in-fhir-r4/.
     // Path gets set automatically when setting id
-    diff.id = diff.id
+    diff.id = diff.diffId();
+    return diff;
+  }
+
+  /**
+   * Gets the id of an element on the differential using the shortcut syntax described here
+   * https://blog.fire.ly/2019/09/13/type-slicing-in-fhir-r4/
+   * @returns {string} the id for the differential
+   */
+  diffId(): string {
+    return this.id
       .split('.')
       .map(p => {
         const i = p.indexOf('[x]:');
         return i > -1 ? p.slice(i + 4) : p;
       })
       .join('.');
-    return diff;
   }
 
   /**
@@ -1291,11 +1298,16 @@ export class ElementDefinition {
    * Finds and returns all child elements of this element.  For example, the children of `Foo.bar` might be the
    * elements `Foo.bar.one`, `Foo.bar.two`, and `Foo.bar.two.a`.  This will not "expand" or "unroll" elements; it
    * only returns those child elements that already exist in the structure definition.
+   * @param {boolean} directOnly - If true, only direct children of the element are returned
    * @returns {ElementDefinition[]} the child elements of this element
    */
-  children(): ElementDefinition[] {
+  children(directOnly = false): ElementDefinition[] {
     return this.structDef.elements.filter(e => {
-      return e !== this && e.id.startsWith(`${this.id}.`);
+      return (
+        e !== this &&
+        e.id.startsWith(`${this.id}.`) &&
+        (!directOnly || e.path.split('.').length === this.path.split('.').length + 1)
+      );
     });
   }
 


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 39f7e4c..cbc9b3b 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -104,4 +104,148 @@ describe('InstanceExporter', () => {
     expect(exported.length).toBe(1);
     expect(exported[0].gender).toBe('foo');
   });
+
+  describe('#exportInstance', () => {
+    let patient: Profile;
+    let instance: Instance;
+    beforeEach(() => {
+      patient = new Profile('TestPatient');
+      patient.parent = 'Patient';
+      doc.profiles.set(patient.name, patient);
+      instance = new Instance('Bar');
+      instance.instanceOf = 'TestPatient';
+      doc.instances.set(instance.name, instance);
+    });
+
+    // Fixing top level elements
+    it('should fix top level elements that are fixed on the Structure Definition', () => {
+      const fixedValRule = new FixedValueRule('active');
+      fixedValRule.fixedValue = true;
+      patient.rules.push(fixedValRule);
+      const exported = exporter.exportInstance(instance);
+      expect(exported.active).toEqual(true);
+    });
+
+    it('should fix top level elements that are fixed by a pattern on the Structure Definition', () => {
+      const fixedValRule = new FixedValueRule('maritalStatus');
+      const fixedFshCode = new FshCode('foo', 'http://foo.com');
+      fixedValRule.fixedValue = fixedFshCode;
+      patient.rules.push(fixedValRule);
+      const exported = exporter.exportInstance(instance);
+      expect(exported.maritalStatus).toEqual({
+        coding: [{ code: 'foo', system: 'http://foo.com' }]
+      });
+    });
+
+    it('should fix top level choice elements that are fixed on the Structure Definition', () => {
+      const fixedValRule = new FixedValueRule('deceasedBoolean');
+      fixedValRule.fixedValue = true;
+      patient.rules.push(fixedValRule);
+      const exported = exporter.exportInstance(instance);
+      expect(exported.deceasedBoolean).toBe(true);
+    });
+
+    it('should fix an element to a value the same as the fixed value on the Structure Definition', () => {
+      const fixedValRule = new FixedValueRule('active');
+      fixedValRule.fixedValue = true;
+      patient.rules.push(fixedValRule);
+      const instanceFixedValRule = new FixedValueRule('active');
+      instanceFixedValRule.fixedValue = true;
+      instance.rules.push(instanceFixedValRule);
+      const exported = exporter.exportInstance(instance);
+      expect(exported.active).toEqual(true);
+    });
+
+    it('should fix an element to a value the same as the fixed pattern on the Structure Definition', () => {
+      const fixedValRule = new FixedValueRule('maritalStatus');
+      const fixedFshCode = new FshCode('foo', 'http://foo.com');
+      fixedValRule.fixedValue = fixedFshCode;
+      patient.rules.push(fixedValRule);
+      const instanceFixedValRule = new FixedValueRule('maritalStatus');
+      const instanceFixedFshCode = new FshCode('foo', 'http://foo.com');
+      instanceFixedValRule.fixedValue = instanceFixedFshCode;
+      instance.rules.push(instanceFixedValRule);
+      const exported = exporter.exportInstance(instance);
+      expect(exported.maritalStatus).toEqual({
+        coding: [{ code: 'foo', system: 'http://foo.com' }]
+      });
+    });
+
+    it('should not fix an element to a value different than the fixed value on the Structure Definition', () => {
+      const fixedValRule = new FixedValueRule('active');
+      fixedValRule.fixedValue = true;
+      patient.rules.push(fixedValRule);
+      const instanceFixedValRule = new FixedValueRule('active');
+      instanceFixedValRule.fixedValue = false;
+      instance.rules.push(instanceFixedValRule);
+      expect(() => exporter.exportInstance(instance)).toThrow(
+        'Cannot fix false to this element; a different boolean is already fixed: true'
+      );
+    });
+
+    it('should not fix an element to a value different than the pattern value on the Structure Definition', () => {
+      const fixedValRule = new FixedValueRule('maritalStatus');
+      const fixedFshCode = new FshCode('foo', 'http://foo.com');
+      fixedValRule.fixedValue = fixedFshCode;
+      patient.rules.push(fixedValRule);
+      const instanceFixedValRule = new FixedValueRule('maritalStatus');
+      const instanceFixedFshCode = new FshCode('bar', 'http://bar.com');
+      instanceFixedValRule.fixedValue = instanceFixedFshCode;
+      instance.rules.push(instanceFixedValRule);
+      expect(() => exporter.exportInstance(instance)).toThrow(
+        'Cannot fix http://bar.com#bar to this element; a different code is already fixed: http://foo.com#foo.'
+      );
+    });
+
+    // Nested elements
+    it('should fix a nested element that has parents defined in the instance and is fixed on the Structure Definition', () => {
+      const fixedValRule = new FixedValueRule('communication.preferred');
+      fixedValRule.fixedValue = true;
+      patient.rules.push(fixedValRule);
+      const instanceFixedValRule = new FixedValueRule('communication[0].language');
+      instanceFixedValRule.fixedValue = new FshCode('foo');
+      instance.rules.push(instanceFixedValRule);
+      const exported = exporter.exportInstance(instance);
+      expect(exported.communication[0]).toEqual({
+        preferred: true,
+        language: { coding: [{ code: 'foo' }] }
+      });
+    });
+
+    it('should fix a nested element that has parents and children defined in the instance and is fixed on the Structure Definition', () => {
+      const fixedValRule = new FixedValueRule('communication.language.text');
+      fixedValRule.fixedValue = 'foo';
+      patient.rules.push(fixedValRule);
+      const instanceFixedValRule = new FixedValueRule(
+        'communication[0].language.coding[0].version'
+      );
+      instanceFixedValRule.fixedValue = 'bar';
+      instance.rules.push(instanceFixedValRule);
+      const exported = exporter.exportInstance(instance);
+      expect(exported.communication[0]).toEqual({
+        language: { text: 'foo', coding: [{ version: 'bar' }] }
+      });
+    });
+
+    it('should not fix a nested element that does not have parents defined in the instance', () => {
+      const fixedValRule = new FixedValueRule('communication.preferred');
+      fixedValRule.fixedValue = true;
+      patient.rules.push(fixedValRule);
+      const exported = exporter.exportInstance(instance);
+      expect(exported.communication).toBeUndefined();
+    });
+
+    // 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', () => {
+      const fixedValRule = new FixedValueRule('maritalStatus');
+      const fixedFshCode = new FshCode('foo', 'http://foo.com');
+      fixedValRule.fixedValue = fixedFshCode;
+      patient.rules.push(fixedValRule);
+      const instanceFixedValRule = new FixedValueRule('maritalStatus.coding[0].system');
+      instanceFixedValRule.fixedValue = 'http://bar.com';
+      instance.rules.push(instanceFixedValRule);
+      expect(() => exporter.exportInstance(instance)).toThrow();
+    });
+  });
 });