Repository: FHIR/sushi (ts)

Issue:
Fix inline Instances to type constrained elements
See this thread for more details: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/Issue.20creating.20Bundle.20instance.

Currently, an inline instance can be fixed to an element of type Resource. However, users should also be able to fix an instance of a Patient to an element of type Patient, or an instance of Observation to an element of type Observation. Additionally, users shouldn't be able to fix an instance of Observation to an element of type Patient.

Pull request description:
Inline instances on non resources
This fixes #347. 

When setting some inline instance A on another instance B, we were only allowing A to be set on B if the element on B was of type Resource. However, instance B could be an instance of a profile that constrains the type of an element on B from Resource to something more specific, like Patient. If this is the case, then we should still be able to set A as an inline instance to the element on B if A is of type Patient, but we did not allow that. This PR fixes this by allowing inline instances to be set on elements that are sub-types of Resource. The rules for adding are:
* If the element is of type Resource, any instance can be set there, regardless of resourceType
* If the element is of type DomainResource, any instance that is not of resourceType Bundle, Parameters, or Binary can be set. These are the only 3 resources that do not inherit from DomainResource, https://www.hl7.org/fhir/domainresource.html#bnr.
* If the element is of type X, any instance of type X can be set on it.

To test this, you can look at the test cases added in https://github.com/FHIR/sushi/commit/6171c5d94d48a1f474d59a6f3ac99bcd788796a1 and see that they fail, then test them on https://github.com/FHIR/sushi/commit/3e90b7e22f7485d492b70a8911111b1fa91424cd and see that they pass. You can also construct an example set of FSH definitions out of the test case added to InstanceExporter.test.ts, and see that when running SUSHI on that example on master, you will get errors and setting the inline instance won't work, but when running SUSHI on this branch, the errors will go away and setting the inline instance will work correctly.

Patch:
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index fa36720..7675fcf 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -1373,10 +1373,17 @@ export class ElementDefinition {
       throw new NoSingleTypeError('Resource');
     }
     const type = this.type[0].code;
-    if (type === 'Resource') {
+    if (
+      type === 'Resource' ||
+      (type === 'DomainResource' &&
+        // These are the only 3 resources not inherited from DomainResource
+        // https://www.hl7.org/fhir/domainresource.html#bnr
+        !['Bundle', 'Parameters', 'Binary'].includes(value.resourceType)) ||
+      type === value.resourceType
+    ) {
       return value;
     } else {
-      throw new MismatchedTypeError('Resource', value.id, type);
+      throw new MismatchedTypeError(value.resourceType, value.id, type);
     }
   }
 


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index ea4b48e..16a77e2 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -129,11 +129,13 @@ describe('InstanceExporter', () => {
     let patient: Profile;
     let respRate: Profile;
     let patientProf: Profile;
+    let bundle: Profile;
     let patientInstance: Instance;
     let patientProfInstance: Instance;
     let lipidInstance: Instance;
     let valueSetInstance: Instance;
     let respRateInstance: Instance;
+    let bundleInstance: Instance;
     beforeEach(() => {
       patient = new Profile('TestPatient');
       patient.parent = 'Patient';
@@ -144,6 +146,9 @@ describe('InstanceExporter', () => {
       respRate = new Profile('TestRespRate');
       respRate.parent = 'resprate';
       doc.profiles.set(respRate.name, respRate);
+      bundle = new Profile('TestBundle');
+      bundle.parent = 'Bundle';
+      doc.profiles.set(bundle.name, bundle);
       patientInstance = new Instance('Bar')
         .withFile('PatientInstance.fsh')
         .withLocation([10, 1, 20, 30]);
@@ -163,6 +168,9 @@ describe('InstanceExporter', () => {
       respRateInstance = new Instance('Bang');
       respRateInstance.instanceOf = 'TestRespRate';
       doc.instances.set(respRateInstance.name, respRateInstance);
+      bundleInstance = new Instance('Pow');
+      bundleInstance.instanceOf = 'TestBundle';
+      doc.instances.set(bundleInstance.name, bundleInstance);
     });
 
     // Setting Metadata
@@ -1399,6 +1407,42 @@ describe('InstanceExporter', () => {
       ]);
     });
 
+    it('should fix an inline resource to an instance element with a specific type', () => {
+      const inlineInstance = new Instance('MyInlinePatient');
+      inlineInstance.instanceOf = 'Patient';
+      const fixedValRule = new FixedValueRule('active');
+      fixedValRule.fixedValue = true;
+      inlineInstance.rules.push(fixedValRule);
+      // * active = true
+      doc.instances.set(inlineInstance.name, inlineInstance);
+
+      const caretRule = new CaretValueRule('entry');
+      caretRule.caretPath = 'slicing.discriminator.type';
+      caretRule.value = new FshCode('value');
+      const containsRule = new ContainsRule('entry');
+      containsRule.items = [{ name: 'PatientsOnly' }];
+      const cardRule = new CardRule('entry[PatientsOnly]');
+      cardRule.min = 0;
+      cardRule.max = '1';
+      const typeRule = new OnlyRule('entry[PatientsOnly].resource');
+      typeRule.types = [{ type: 'Patient' }];
+      // * entry ^slicing.discriminator.type = #value
+      // * entry contains Patient 0..1
+      // * entry[PatientsOnly].resource only Patient
+      bundle.rules.push(caretRule, containsRule, cardRule, typeRule);
+
+      const bundleValRule = new FixedValueRule('entry[PatientsOnly].resource');
+      bundleValRule.fixedValue = 'MyInlinePatient';
+      bundleValRule.isResource = true;
+      // * entry[PatientsOnly].resource = MyInlinePatient
+      bundleInstance.rules.push(bundleValRule);
+
+      const exported = exportInstance(bundleInstance);
+      expect(exported.entry[0]).toEqual({
+        resource: { resourceType: 'Patient', id: 'MyInlinePatient', active: true }
+      });
+    });
+
     it('should only export an instance once', () => {
       const bundleInstance = new Instance('MyBundle');
       bundleInstance.instanceOf = 'Bundle';
diff --git a/test/fhirtypes/ElementDefinition.checkFixResource.test.ts b/test/fhirtypes/ElementDefinition.checkFixResource.test.ts
index c651196..72705cf 100644
--- a/test/fhirtypes/ElementDefinition.checkFixResource.test.ts
+++ b/test/fhirtypes/ElementDefinition.checkFixResource.test.ts
@@ -48,7 +48,7 @@ describe('ElementDefinition', () => {
       expect(() => {
         status.checkFixResource(inlineInstance);
       }).toThrow(
-        'Cannot fix Resource value: MyInlineInstance. Value does not match element type: code'
+        'Cannot fix Patient value: MyInlineInstance. Value does not match element type: code'
       );
     });
   });
diff --git a/test/fhirtypes/StructureDefinition.test.ts b/test/fhirtypes/StructureDefinition.test.ts
index 604e788..67d55d5 100644
--- a/test/fhirtypes/StructureDefinition.test.ts
+++ b/test/fhirtypes/StructureDefinition.test.ts
@@ -1083,11 +1083,77 @@ describe('StructureDefinition', () => {
       }).toThrow('Cannot resolve element from path: extension[fake-extension].value[x]');
     });
 
-    it('should allow fixing an InstanceDefinition to a resource element', () => {
-      const instanceDef = new InstanceDefinition();
-      instanceDef.resourceType = 'Patient';
-      const { fixedValue } = respRate.validateValueAtPath('contained[0]', instanceDef, fisher);
-      expect(fixedValue.resourceType).toBe('Patient');
+    describe('#Inline Instances', () => {
+      beforeEach(() => {
+        const contained = respRate.findElementByPath('contained', fisher);
+        contained.sliceIt('type', '$this', false, 'open');
+        contained.addSlice('PatientsOnly');
+        const containedPatients = respRate.findElementByPath('contained[PatientsOnly]', fisher);
+        const patientRule = new OnlyRule('contained[PatientsOnly]');
+        patientRule.types = [{ type: 'Patient' }];
+        containedPatients.constrainType(patientRule, fisher);
+
+        contained.addSlice('DomainsOnly');
+        const containedDomains = respRate.findElementByPath('contained[DomainsOnly]', fisher);
+        const domainRule = new OnlyRule('contained[DomainsOnly]');
+        domainRule.types = [{ type: 'DomainResource' }];
+        containedDomains.constrainType(domainRule, fisher);
+      });
+
+      it('should allow fixing a Patient type InstanceDefinition to a Resource element', () => {
+        const instanceDef = new InstanceDefinition();
+        instanceDef.resourceType = 'Patient';
+        const { fixedValue } = respRate.validateValueAtPath('contained[0]', instanceDef, fisher);
+        expect(fixedValue.resourceType).toBe('Patient');
+      });
+
+      it('should allow fixing a Patient type InstanceDefinition to a DomainResource element', () => {
+        const instanceDef = new InstanceDefinition();
+        instanceDef.resourceType = 'Patient';
+        const { fixedValue } = respRate.validateValueAtPath(
+          'contained[DomainsOnly][0]',
+          instanceDef,
+          fisher
+        );
+        expect(fixedValue.resourceType).toBe('Patient');
+      });
+
+      it('should allow fixing a Patient type InstanceDefinition to a Patient element', () => {
+        const instanceDef = new InstanceDefinition();
+        instanceDef.resourceType = 'Patient';
+        const { fixedValue } = respRate.validateValueAtPath(
+          'contained[PatientsOnly][0]',
+          instanceDef,
+          fisher
+        );
+        expect(fixedValue.resourceType).toBe('Patient');
+      });
+
+      it('should allow fixing a Bundle type InstanceDefinition to a Resource element', () => {
+        const instanceDef = new InstanceDefinition();
+        instanceDef.id = 'OfJoy';
+        instanceDef.resourceType = 'Bundle';
+        const { fixedValue } = respRate.validateValueAtPath('contained[0]', instanceDef, fisher);
+        expect(fixedValue.resourceType).toBe('Bundle');
+      });
+
+      it('should not allow fixing a Bundle type InstanceDefinition to a DomainResource element', () => {
+        const instanceDef = new InstanceDefinition();
+        instanceDef.id = 'OfJoy';
+        instanceDef.resourceType = 'Bundle';
+        expect(() =>
+          respRate.validateValueAtPath('contained[DomainsOnly][0]', instanceDef, fisher)
+        ).toThrow(/Bundle.*OfJoy.*DomainResource/);
+      });
+
+      it('should not allow fixing a Bundle type InstanceDefinition to a Patient element', () => {
+        const instanceDef = new InstanceDefinition();
+        instanceDef.id = 'OfJoy';
+        instanceDef.resourceType = 'Bundle';
+        expect(() =>
+          respRate.validateValueAtPath('contained[PatientsOnly][0]', instanceDef, fisher)
+        ).toThrow(/Bundle.*OfJoy.*Patient/);
+      });
     });
   });