Repository: FHIR/sushi (ts)

Issue:
Can't set age value in instance using quantity syntax
Attempting this (on instance of `FamilyMemberHistory`):
```
* condition.onsetAge = 30.0 'a'
```
Results in this:
```
error Cannot fix number value: 30. Value does not match element type: Age
```

See: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/Instance.20age

Pull request description:
Allow quantities to be fixed to specializations
Allow quantities to be fixed to specializations like Age and Duration. Note that we do not do any extra validation (e.g., we don't ensure that Age uses valid units for Age, or that a Count uses the '1' unit) -- we leave that for the IG Publisher to validate.

To test this, you can first run the tests at commit 16a827e without the fix.  Four tests will fail.  Then run them again at commit ed136db (the fix) and they will succeed.

You can also test them via a profile like this:

```
Profile: ConditionWithFixedOnsetAge
Parent: Condition
* onset[x] only Age
* onset[x] = 42 'a'
```

Or an instance like this:
```
Instance: ConditionAcquiredAt42
InstanceOf: Condition
* subject = Reference(Patient/abc)
* onsetAge = 42 'a'
```

Fixes #515 

Patch:
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index 2b8540e..b575d60 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -183,6 +183,8 @@ export class ElementDefinition {
   patternCoding: Coding;
   fixedQuantity: Quantity;
   patternQuantity: Quantity;
+  fixedAge: Quantity;
+  patternAge: Quantity;
   fixedRatio: Ratio;
   patternRatio: Ratio;
   fixedReference: Reference;
@@ -1157,7 +1159,21 @@ export class ElementDefinition {
         break;
       case 'Quantity':
         value = value as FshQuantity;
-        this.fixFHIRValue(value.toString(), value.toFHIRQuantity(), exactly, 'Quantity');
+        // Special case quantity to support compatible specializations (like Age), but try to do it
+        // in a flexible way (without hard-coding every specialization here).
+        let providedType = 'Quantity';
+        const actualType = this.type[0].code;
+        if (actualType !== 'Quantity') {
+          const actualTypeSD = fisher?.fishForFHIR(actualType, Type.Type);
+          if (actualTypeSD?.baseDefinition === 'http://hl7.org/fhir/StructureDefinition/Quantity') {
+            // We treat every quantity instance as compatible w/ every specialization. This is not
+            // strictly true, but in order to validate it, we'd need to either support/process the
+            // FHIRPath rules or hard-code a bunch of special case logic here. Instead, let the IG
+            // Publisher deal with this extra validation and just pass it through here.
+            providedType = actualType;
+          }
+        }
+        this.fixFHIRValue(value.toString(), value.toFHIRQuantity(), exactly, providedType);
         break;
       case 'Ratio':
         value = value as FshRatio;


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 5ab6db1..cfddec7 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -9,7 +9,8 @@ import {
   FshReference,
   Extension,
   FshCodeSystem,
-  RuleSet
+  RuleSet,
+  FshQuantity
 } from '../../src/fshtypes';
 import {
   FixedValueRule,
@@ -1279,6 +1280,26 @@ describe('InstanceExporter', () => {
       ]);
     });
 
+    // Fixing Quantities to Quantity specializations (e.g., Age)
+    it('should fix a Quantity to a Quantity specialization', () => {
+      const conditionInstance = new Instance('SomeCondition');
+      conditionInstance.instanceOf = 'Condition';
+      const fixedAgeRule = new FixedValueRule('onsetAge');
+      fixedAgeRule.fixedValue = new FshQuantity(
+        42.0,
+        new FshCode('a', 'http://unitsofmeasure.org', 'years')
+      );
+      conditionInstance.rules.push(fixedAgeRule);
+      doc.instances.set(conditionInstance.name, conditionInstance);
+      const exported = exportInstance(conditionInstance);
+      expect(exported.onsetAge).toEqual({
+        value: 42.0,
+        code: 'a',
+        system: 'http://unitsofmeasure.org',
+        unit: 'years'
+      });
+    });
+
     // Sliced elements
     it('should fix a single sliced element to a value', () => {
       const fixedValRule = new FixedValueRule('extension[level].valueCoding.system');
diff --git a/test/fhirtypes/ElementDefinition.fixFshQuantity.test.ts b/test/fhirtypes/ElementDefinition.fixFshQuantity.test.ts
index 75ef843..9b79d26 100644
--- a/test/fhirtypes/ElementDefinition.fixFshQuantity.test.ts
+++ b/test/fhirtypes/ElementDefinition.fixFshQuantity.test.ts
@@ -5,12 +5,15 @@ import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions';
 import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition';
 import { FshQuantity, FshCode } from '../../src/fshtypes';
 import { TestFisher } from '../testhelpers';
+import { ElementDefinitionType } from '../../src/fhirtypes';
 
 describe('ElementDefinition', () => {
   let defs: FHIRDefinitions;
+  let condition: StructureDefinition;
   let observation: StructureDefinition;
   let fshQuantity1: FshQuantity;
   let fshQuantity2: FshQuantity;
+  let fshQuantityAge: FshQuantity;
   let fisher: TestFisher;
 
   beforeAll(() => {
@@ -26,6 +29,8 @@ describe('ElementDefinition', () => {
     observation = fisher.fishForStructureDefinition('Observation');
     fshQuantity1 = new FshQuantity(1.23, new FshCode('mm', 'http://unitsofmeasure.org'));
     fshQuantity2 = new FshQuantity(1.24, new FshCode('mm', 'http://unitsofmeasure.org'));
+    condition = fisher.fishForStructureDefinition('Condition');
+    fshQuantityAge = new FshQuantity(42.0, new FshCode('a', 'http://unitsofmeasure.org'));
   });
 
   describe('#fixFshQuantity', () => {
@@ -55,6 +60,30 @@ describe('ElementDefinition', () => {
       expect(referenceRangeLow.patternQuantity).toBeUndefined();
     });
 
+    it('should fix a FshQuantity to a quantity specialization', () => {
+      const onsetX = condition.elements.find(e => e.id === 'Condition.onset[x]');
+      onsetX.type = [new ElementDefinitionType('Age')];
+      onsetX.fixValue(fshQuantityAge, false, fisher);
+      expect(onsetX.patternAge).toEqual({
+        value: 42.0,
+        code: 'a',
+        system: 'http://unitsofmeasure.org'
+      });
+      expect(onsetX.fixedAge).toBeUndefined();
+    });
+
+    it('should fix a FshQuantity to a quantity specialization (exactly)', () => {
+      const onsetX = condition.elements.find(e => e.id === 'Condition.onset[x]');
+      onsetX.type = [new ElementDefinitionType('Age')];
+      onsetX.fixValue(fshQuantityAge, true, fisher);
+      expect(onsetX.fixedAge).toEqual({
+        value: 42.0,
+        code: 'a',
+        system: 'http://unitsofmeasure.org'
+      });
+      expect(onsetX.patternAge).toBeUndefined();
+    });
+
     it('should throw NoSingleTypeError when element has multiple types', () => {
       const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
       expect(() => {
diff --git a/test/fhirtypes/ElementDefinition.setInstancePropertyByPath.test.ts b/test/fhirtypes/ElementDefinition.setInstancePropertyByPath.test.ts
index 91f519f..b3ad218 100644
--- a/test/fhirtypes/ElementDefinition.setInstancePropertyByPath.test.ts
+++ b/test/fhirtypes/ElementDefinition.setInstancePropertyByPath.test.ts
@@ -3,7 +3,7 @@ import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions';
 import { ElementDefinition, ElementDefinitionType } from '../../src/fhirtypes/ElementDefinition';
 import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition';
 import { TestFisher } from '../testhelpers';
-import { FshCode } from '../../src/fshtypes';
+import { FshCode, FshQuantity } from '../../src/fshtypes';
 import path from 'path';
 
 describe('ElementDefinition', () => {
@@ -45,8 +45,25 @@ describe('ElementDefinition', () => {
     });
 
     it('should set a nested instance property which must be created', () => {
-      status.setInstancePropertyByPath('patternQuantity.value', 1.2, fisher);
-      expect(status.patternQuantity.value).toBe(1.2);
+      // This isn't how you would normally do this, but it's useful for testing this part of the logic
+      const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
+      valueX.setInstancePropertyByPath('patternQuantity.value', 1.2, fisher);
+      expect(valueX.patternQuantity.value).toBe(1.2);
+    });
+
+    it('should set a Quantity on a Quantity specialization instance property (e.g., Age)', () => {
+      // This isn't how you would normally do this, but it's useful for testing this part of the logic
+      const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
+      valueX.setInstancePropertyByPath(
+        'patternAge',
+        new FshQuantity(42.0, new FshCode('a', 'http://unitsofmeasure.org')),
+        fisher
+      );
+      expect(valueX.patternAge).toEqual({
+        value: 42.0,
+        code: 'a',
+        system: 'http://unitsofmeasure.org'
+      });
     });
 
     it('should not set an instance property which is being fixed incorrectly', () => {