Repository: FHIR/sushi (ts)

Issue:
SUSHI does not support pattern[x]/fixed[x] w/ inline instances
The following should be supported, but currently is not:
```
Instance: USPostalAddress
InstanceOf: Address
Usage: #inline
* type = #postal
* country = "US"

Profile: USPatient
Parent: Patient
* address = USPostalAddress
```

As written, this should create the following attribute in the `USPatient` profile's `Patient.address` element:
```json
"patternAddress": {
  "type": "postal",
  "country": "US"
}
```

Unless I'm mistaken, supporting something like the FSH above is the _only_ way to get a `patternAddress` into an element without introducing a syntax that allows `{` `}` objects in FSH.

Pull request:
Support fixing inline instances on profiles
This PR fixes #511 and supports fixing inline instances on profiles. The example in the issue is now supported, and the tests show a few other examples of what is not allowed.

I do have a couple questions about the implementation:
- In order to reach the `InstanceDefinition` case in `ElementDefinition.fixValue`, I had to make the change to the `applyMixins` function to not set the constructor name. I'm not entirely sure if this is the right approach, because it looks like the original implementation is the way that the [TypeScript documentation]( https://www.typescriptlang.org/docs/handbook/mixins.html#mixin-sample) describes. However, without this change, instances will be an instance of the `InstanceDefinition` class, but their constructor name is `HasId` because the `applyMixins` overwrites this. If others don't think this change is correct, I can always have `ElementDefinition.fixValue` go into the `InstanceDefinition` case whenever we see a `HasId` constructor name.
- I got rid of the error in the importer that we cannot fix instances to profiles since we are now allowing that. However, I'm not sure if there were other cases that we had in mind when writing that error that we still don't want to support. If so, I can add an error back in for those cases.

Patch:
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index a90bf60..90b21dd 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -7,6 +7,7 @@ import {
 } from '../fhirtypes';
 import { Profile, Extension, Invariant } from '../fshtypes';
 import { FSHTank } from '../import';
+import { InstanceExporter } from '../export';
 import {
   ParentNotDefinedError,
   ParentDeclaredAsProfileNameError,
@@ -156,6 +157,17 @@ export class StructureDefinitionExporter implements Fishable {
           if (rule instanceof CardRule) {
             element.constrainCardinality(rule.min, rule.max);
           } else if (rule instanceof FixedValueRule) {
+            if (rule.isInstance) {
+              const instanceExporter = new InstanceExporter(this.tank, this.pkg, this.fisher);
+              const instance = instanceExporter.fishForFHIR(rule.fixedValue as string);
+              if (instance == null) {
+                logger.error(
+                  `Cannot find definition for Instance: ${rule.fixedValue}. Skipping rule.`
+                );
+                continue;
+              }
+              rule.fixedValue = instance;
+            }
             const replacedRule = replaceReferences(rule, this.tank, this);
             element.fixValue(replacedRule.fixedValue, replacedRule.exactly, this);
           } else if (rule instanceof FlagRule) {
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index b575d60..3f137db 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -185,6 +185,10 @@ export class ElementDefinition {
   patternQuantity: Quantity;
   fixedAge: Quantity;
   patternAge: Quantity;
+  fixedAddress: InstanceDefinition;
+  patternAddress: InstanceDefinition;
+  fixedPeriod: InstanceDefinition;
+  patternPeriod: InstanceDefinition;
   fixedRatio: Ratio;
   patternRatio: Ratio;
   fixedReference: Reference;
@@ -1123,8 +1127,8 @@ export class ElementDefinition {
       type = 'Reference';
     } else if (value instanceof FshCanonical) {
       type = 'Canonical';
-    } else if (typeof value === 'object') {
-      type = value.constructor?.name;
+    } else if (value instanceof InstanceDefinition) {
+      type = 'InstanceDefinition';
     } else {
       type = typeof value;
     }
@@ -1219,7 +1223,18 @@ export class ElementDefinition {
         }
         this.fixString(canonicalUrl, exactly);
         break;
+      case 'InstanceDefinition':
+        value = value as InstanceDefinition;
+        const stringVal = JSON.stringify(value);
+        this.fixFHIRValue(
+          stringVal,
+          value.toJSON(),
+          exactly,
+          value._instanceMeta.sdType ?? value.resourceType
+        );
+        break;
       default:
+        type = (typeof value === 'object' && value.constructor?.name) ?? type;
         throw new MismatchedTypeError(type, value, this.type[0].code);
     }
 
diff --git a/src/import/FSHImporter.ts b/src/import/FSHImporter.ts
index 9355a5f..57d11bd 100644
--- a/src/import/FSHImporter.ts
+++ b/src/import/FSHImporter.ts
@@ -852,16 +852,7 @@ export class FSHImporter extends FSHVisitor {
       return [this.visitValueSetRule(ctx.valueSetRule())];
     } else if (ctx.fixedValueRule()) {
       const rule = this.visitFixedValueRule(ctx.fixedValueRule());
-      if (rule.isInstance) {
-        const sourceInfo = { location: this.extractStartStop(ctx), file: this.currentFile };
-        logger.error(
-          'Resources cannot be added inline to a Profile or Extension, skipping rule.',
-          sourceInfo
-        );
-        return [];
-      } else {
-        return [rule];
-      }
+      return [rule];
     } else if (ctx.onlyRule()) {
       return [this.visitOnlyRule(ctx.onlyRule())];
     } else if (ctx.containsRule()) {


Test patch:
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 8af1487..e44020a 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -1666,6 +1666,49 @@ describe('StructureDefinitionExporter', () => {
     );
   });
 
+  it('should apply an instance FixedValueRule and replace the instance', () => {
+    const profile = new Profile('USPatient');
+    profile.parent = 'Patient';
+    const rule = new FixedValueRule('address');
+    rule.fixedValue = 'USPostalAddress';
+    rule.isInstance = true;
+    profile.rules.push(rule); // * address = USPostalAddress
+    doc.profiles.set(profile.name, profile);
+
+    const instance = new Instance('USPostalAddress');
+    instance.instanceOf = 'Address';
+    instance.usage = 'Inline';
+    const fixCountry = new FixedValueRule('country');
+    fixCountry.fixedValue = 'US';
+    instance.rules.push(fixCountry); // * country = "US"
+    doc.instances.set(instance.name, instance);
+
+    exporter.exportStructDef(profile);
+    const sd = pkg.profiles[0];
+    const fixedAddress = sd.findElement('Patient.address');
+    expect(fixedAddress.patternAddress).toEqual({ country: 'US' });
+    expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+  });
+
+  it('should not apply an instance FixedValueRule when the instance cannot be found', () => {
+    const profile = new Profile('USPatient');
+    profile.parent = 'Patient';
+    const rule = new FixedValueRule('address');
+    rule.fixedValue = 'FakeInstance';
+    rule.isInstance = true;
+    profile.rules.push(rule); // * address = FakeInstance
+    doc.profiles.set(profile.name, profile);
+
+    exporter.exportStructDef(profile);
+    const sd = pkg.profiles[0];
+    const fixedAddress = sd.findElement('Patient.address');
+    expect(fixedAddress.patternAddress).toBeUndefined();
+    expect(loggerSpy.getAllMessages('error')).toHaveLength(1);
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Cannot find definition for Instance: FakeInstance. Skipping rule.\D*/s
+    );
+  });
+
   it('should use the url specified in a CaretValueRule when referencing a named code system', () => {
     const profile = new Profile('LightObservation');
     profile.parent = 'Observation';
diff --git a/test/fhirtypes/ElementDefinition.fixInstanceDefinition.test.ts b/test/fhirtypes/ElementDefinition.fixInstanceDefinition.test.ts
new file mode 100644
index 0000000..919b36e
--- /dev/null
+++ b/test/fhirtypes/ElementDefinition.fixInstanceDefinition.test.ts
@@ -0,0 +1,169 @@
+import path from 'path';
+import { loadFromPath } from '../../src/fhirdefs/load';
+import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions';
+import { StructureDefinition, InstanceDefinition } from '../../src/fhirtypes';
+import { TestFisher } from '../testhelpers';
+
+describe('ElementDefinition', () => {
+  let defs: FHIRDefinitions;
+  let patient: StructureDefinition;
+  let observation: StructureDefinition;
+  let fisher: TestFisher;
+
+  beforeAll(() => {
+    defs = new FHIRDefinitions();
+    loadFromPath(
+      path.join(__dirname, '..', 'testhelpers', 'testdefs', 'package'),
+      'testPackage',
+      defs
+    );
+    fisher = new TestFisher().withFHIR(defs);
+  });
+  beforeEach(() => {
+    patient = fisher.fishForStructureDefinition('Patient');
+    observation = fisher.fishForStructureDefinition('Observation');
+  });
+
+  describe('#fixInstance', () => {
+    it('should fix an allowed type of an instance', () => {
+      const addressInstance = new InstanceDefinition();
+      addressInstance._instanceMeta.name = 'USPostalAddress';
+      addressInstance._instanceMeta.sdType = 'Address';
+      addressInstance._instanceMeta.usage = 'Inline';
+      addressInstance.country = 'US';
+      addressInstance.type = 'postal';
+
+      const address = patient.elements.find(e => e.id === 'Patient.address');
+      address.fixValue(addressInstance);
+      expect(address.patternAddress).toEqual({ country: 'US', type: 'postal' });
+      expect(address.fixedAddress).toBeUndefined();
+    });
+
+    it('should fix an allowed type of an instance (exactly)', () => {
+      const addressInstance = new InstanceDefinition();
+      addressInstance._instanceMeta.name = 'USPostalAddress';
+      addressInstance._instanceMeta.sdType = 'Address';
+      addressInstance._instanceMeta.usage = 'Inline';
+      addressInstance.country = 'US';
+      addressInstance.type = 'postal';
+
+      const address = patient.elements.find(e => e.id === 'Patient.address');
+      address.fixValue(addressInstance, true);
+      expect(address.fixedAddress).toEqual({ country: 'US', type: 'postal' });
+      expect(address.patternAddress).toBeUndefined();
+    });
+
+    it('should throw NoSingleTypeError when element has multiple types', () => {
+      const periodInstance = new InstanceDefinition();
+      periodInstance._instanceMeta.name = 'LastYear';
+      periodInstance._instanceMeta.sdType = 'Period';
+      periodInstance._instanceMeta.usage = 'Inline';
+      periodInstance.start = '2019-08-01';
+      periodInstance.end = '2020-08-01';
+
+      const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
+      expect(() => {
+        valueX.fixValue(periodInstance);
+      }).toThrow(
+        'Cannot fix InstanceDefinition value on this element since this element does not have a single type'
+      );
+      expect(() => {
+        valueX.fixValue(periodInstance, true);
+      }).toThrow(
+        'Cannot fix InstanceDefinition value on this element since this element does not have a single type'
+      );
+      expect(valueX.patternPeriod).toBeUndefined();
+      expect(valueX.fixedPeriod).toBeUndefined();
+    });
+
+    it('should throw ValueAlreadyFixedError when the value is fixed to a different value by pattern[x]', () => {
+      const workAddress = new InstanceDefinition();
+      workAddress._instanceMeta.name = 'USPostalAddress';
+      workAddress._instanceMeta.sdType = 'Address';
+      workAddress._instanceMeta.usage = 'Inline';
+      workAddress.country = 'US';
+      workAddress.type = 'postal';
+      workAddress.use = 'work';
+      const homeAddress = new InstanceDefinition();
+      homeAddress._instanceMeta.name = 'USPostalAddress';
+      homeAddress._instanceMeta.sdType = 'Address';
+      homeAddress._instanceMeta.usage = 'Inline';
+      homeAddress.country = 'US';
+      homeAddress.type = 'postal';
+      homeAddress.use = 'home';
+
+      const address = patient.elements.find(e => e.id === 'Patient.address');
+      address.fixValue(workAddress);
+      expect(address.patternAddress).toEqual({ country: 'US', type: 'postal', use: 'work' });
+      address.fixValue(workAddress);
+      expect(address.patternAddress).toEqual({ country: 'US', type: 'postal', use: 'work' });
+      expect(() => {
+        address.fixValue(homeAddress);
+      }).toThrow(
+        'Cannot fix {"country":"US","type":"postal","use":"home"} to this element; a different Address is already fixed: {"country":"US","type":"postal","use":"work"}'
+      );
+    });
+
+    it('should throw ValueAlreadyFixedError when the value is fixed to a different value by fixed[x]', () => {
+      const workAddress = new InstanceDefinition();
+      workAddress._instanceMeta.name = 'USPostalAddress';
+      workAddress._instanceMeta.sdType = 'Address';
+      workAddress._instanceMeta.usage = 'Inline';
+      workAddress.country = 'US';
+      workAddress.type = 'postal';
+      workAddress.use = 'work';
+      const homeAddress = new InstanceDefinition();
+      homeAddress._instanceMeta.name = 'USPostalAddress';
+      homeAddress._instanceMeta.sdType = 'Address';
+      homeAddress._instanceMeta.usage = 'Inline';
+      homeAddress.country = 'US';
+      homeAddress.type = 'postal';
+      homeAddress.use = 'home';
+
+      const address = patient.elements.find(e => e.id === 'Patient.address');
+      address.fixValue(workAddress, true);
+      expect(address.fixedAddress).toEqual({ country: 'US', type: 'postal', use: 'work' });
+      address.fixValue(workAddress, true);
+      expect(address.fixedAddress).toEqual({ country: 'US', type: 'postal', use: 'work' });
+      expect(() => {
+        address.fixValue(homeAddress, true);
+      }).toThrow(
+        'Cannot fix {"country":"US","type":"postal","use":"home"} to this element; a different Address is already fixed: {"country":"US","type":"postal","use":"work"}'
+      );
+    });
+
+    it('should throw FixedToPatternError when trying to change fixed[x] to pattern[x]', () => {
+      const addressInstance = new InstanceDefinition();
+      addressInstance._instanceMeta.name = 'USPostalAddress';
+      addressInstance._instanceMeta.sdType = 'Address';
+      addressInstance._instanceMeta.usage = 'Inline';
+      addressInstance.country = 'US';
+      addressInstance.type = 'postal';
+
+      const address = patient.elements.find(e => e.id === 'Patient.address');
+      address.fixValue(addressInstance, true);
+      expect(address.fixedAddress).toEqual({ country: 'US', type: 'postal' });
+      expect(() => {
+        address.fixValue(addressInstance);
+      }).toThrow(
+        'Cannot fix this element using a pattern; as it is already fixed in the StructureDefinition using fixedAddress'
+      );
+    });
+
+    it('should throw MismatchedTypeError when the value is fixed to an unsupported type', () => {
+      const periodInstance = new InstanceDefinition();
+      periodInstance._instanceMeta.name = 'LastYear';
+      periodInstance._instanceMeta.sdType = 'Period';
+      periodInstance._instanceMeta.usage = 'Inline';
+      periodInstance.start = '2019-08-01';
+      periodInstance.end = '2020-08-01';
+
+      const address = patient.elements.find(e => e.id === 'Patient.address');
+      expect(() => {
+        address.fixValue(periodInstance);
+      }).toThrow(
+        'Cannot fix Period value: {"start":"2019-08-01","end":"2020-08-01"}. Value does not match element type: Address'
+      );
+    });
+  });
+});
diff --git a/test/import/FSHImporter.Profile.test.ts b/test/import/FSHImporter.Profile.test.ts
index 2f39c71..4e60c8c 100644
--- a/test/import/FSHImporter.Profile.test.ts
+++ b/test/import/FSHImporter.Profile.test.ts
@@ -1352,7 +1352,7 @@ describe('FSHImporter', () => {
         assertFixedValueRule(profile.rules[0], 'identifier.system', 'http://example.org');
       });
 
-      it('should log an error and skip the rule when parsing fixed value Resource rule', () => {
+      it('should parse a fixed value Resource rule', () => {
         const input = `
 
         Profile: ObservationProfile
@@ -1362,10 +1362,8 @@ describe('FSHImporter', () => {
 
         const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
-        expect(profile.rules).toHaveLength(0);
-        expect(loggerSpy.getLastMessage('error')).toMatch(
-          /Resources cannot be added inline to a Profile or Extension, skipping rule\..*Line: 5\D*/s
-        );
+        expect(profile.rules).toHaveLength(1);
+        assertFixedValueRule(profile.rules[0], 'contained[0]', 'SomeInstance', false, true);
       });
     });