Repository: FHIR/sushi (ts)

Issue:
Constraining Canonicals does not support canonicals w/ versions
You can now do:
```
* action.definition[x] only Canonical(Questionnaire)
```
and
```
* action.definition[x] only Canonical(http://hl7.org/fhir/StructureDefinition/Questionnaire)
```
BUT you cannot do:
```
// broken
* action.definition[x] only Canonical(Questionnaire|4.0.1)
```
or
```
// broken
* action.definition[x] only Canonical(http://hl7.org/fhir/StructureDefinition/Questionnaire|4.0.1)
```

We need to support this too.

Pull request:
Elements can be constrained with canonicals that contain versions
Fixes #863 and completes task [CIMPL-791](https://standardhealthrecord.atlassian.net/browse/CIMPL-791).

The version provided with the canonical type on the type constraint is appended to the `targetProfile` on the element. When values are assigned to the element, the version is _not_ checked. For example, if an element is constrained to `Canonical(Patient|4.0.1)`, then any `Patient` instance can have its canonical assigned to the element, regardless of the version of the `Patient` profile it is an instance of.

Basically: authors are allowed to specify a version in the type constraint, and that version will appear in the FHIR resource, but the version does _not_ affect SUSHI's type checks.

Patch:
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index f9c02af..4d478b2 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -816,6 +816,10 @@ export class ElementDefinition {
     // Loop through the input types and associate them to the element types in the map
     for (const type of types) {
       const typeMatch = this.findTypeMatch(type, targetTypes, fisher);
+      // if the type is Canonical, it may have a version. preserve it in the match's metadata.
+      if (type.isCanonical && type.type.indexOf('|') > -1) {
+        typeMatch.metadata.url = `${typeMatch.metadata.url}|${type.type.split('|', 2)[1]}`;
+      }
       typeMatches.get(typeMatch.code).push(typeMatch);
     }
 
@@ -958,9 +962,10 @@ export class ElementDefinition {
     fisher: Fishable
   ): ElementTypeMatchInfo {
     let matchedType: ElementDefinitionType;
+    const typeName = type.isCanonical ? type.type.split('|', 2)[0] : type.type;
 
     // Get the lineage (type hierarchy) so we can walk up it when attempting to match
-    const lineage = this.getTypeLineage(type.type, fisher);
+    const lineage = this.getTypeLineage(typeName, fisher);
     if (isEmpty(lineage)) {
       throw new TypeNotFoundError(type.type);
     }
@@ -1652,7 +1657,9 @@ export class ElementDefinition {
     if (sdType && this.type[0].targetProfile) {
       const validTypes: string[] = [];
       this.type[0].targetProfile.forEach(tp => {
-        const tpType = fisher.fishForMetadata(tp)?.sdType;
+        // target profile may have a version after a | character. don't include it when fishing.
+        const unversionedProfile = tp.split('|', 2)[0];
+        const tpType = fisher.fishForMetadata(unversionedProfile)?.sdType;
         if (tpType) {
           validTypes.push(tpType);
         }


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 981c1f9..c0746ad 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -1990,6 +1990,39 @@ describe('InstanceExporter', () => {
       expect(exported.instantiatesCanonical).toEqual(['http://example.org/PlanDefition/1']); // instantiatesCanonical is set
     });
 
+    it('should assign a Canonical that is one of the valid types (without checking the version) when the type is versioned', () => {
+      // Profile: SpecialQuestionnaire
+      // Parent: Questionnaire
+      // derivedFrom only Canonical(TestQuestionnaire|3.1)
+      const specialQuestionnaire = new Profile('SpecialQuestionnaire');
+      specialQuestionnaire.parent = 'Questionnaire';
+      const onlyRule = new OnlyRule('derivedFrom');
+      onlyRule.types = [{ type: 'TestQuestionnaire|3.1', isCanonical: true }];
+      specialQuestionnaire.rules.push(onlyRule);
+      doc.profiles.set(specialQuestionnaire.name, specialQuestionnaire);
+      // Instance: TQInstance
+      // InstanceOf: TestQuestionnaire
+      // url = "http://example.org/TQ/1"
+      const tqInstance = new Instance('TQInstance');
+      tqInstance.instanceOf = 'TestQuestionnaire';
+      const tqUrl = new AssignmentRule('url');
+      tqUrl.value = 'http://example.org/TQ/1';
+      tqInstance.rules.push(tqUrl);
+      doc.instances.set(tqInstance.name, tqInstance);
+      // Instance: SQInstance
+      // InstanceOf: SpecialQuestionnaire
+      // derivedFrom = Canonical(TQInstance)
+      const sqInstance = new Instance('SQInstance');
+      sqInstance.instanceOf = 'SpecialQuestionnaire';
+      const sqDerivedFrom = new AssignmentRule('derivedFrom');
+      sqDerivedFrom.value = new FshCanonical('TQInstance');
+      sqInstance.rules.push(sqDerivedFrom);
+
+      const exported = exportInstance(sqInstance);
+      expect(exported.derivedFrom).toBeDefined();
+      expect(exported.derivedFrom).toEqual(['http://example.org/TQ/1']);
+    });
+
     it('should assign a Canonical that is a child of the valid types', () => {
       const assignedRefRule = new AssignmentRule('instantiatesCanonical');
       const pdProfile = new Profile('PlanDefProfile');
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 1800392..5a4de8e 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -2712,6 +2712,39 @@ describe('StructureDefinitionExporter R4', () => {
       );
     });
 
+    it('should apply a correct OnlyRule with a version on a canonical', () => {
+      const profile = new Profile('Foo');
+      profile.parent = 'PlanDefinition';
+
+      const rule = new OnlyRule('action.definition[x]');
+      rule.types = [{ type: 'Questionnaire|4.0.1', isCanonical: true }];
+      profile.rules.push(rule);
+
+      exporter.exportStructDef(profile);
+      const sd = pkg.profiles[0];
+      const baseStructDef = fisher.fishForStructureDefinition('PlanDefinition');
+
+      const baseActionDef = baseStructDef.findElement('PlanDefinition.action.definition[x]');
+      const constrainedActionDef = sd.findElement('PlanDefinition.action.definition[x]');
+
+      expect(baseActionDef.type).toHaveLength(2);
+      expect(baseActionDef.type[0]).toEqual(
+        new ElementDefinitionType('canonical').withTargetProfiles(
+          'http://hl7.org/fhir/StructureDefinition/ActivityDefinition',
+          'http://hl7.org/fhir/StructureDefinition/PlanDefinition',
+          'http://hl7.org/fhir/StructureDefinition/Questionnaire'
+        )
+      );
+      expect(baseActionDef.type[1]).toEqual(new ElementDefinitionType('uri'));
+
+      expect(constrainedActionDef.type).toHaveLength(1);
+      expect(constrainedActionDef.type[0]).toEqual(
+        new ElementDefinitionType('canonical').withTargetProfiles(
+          'http://hl7.org/fhir/StructureDefinition/Questionnaire|4.0.1'
+        )
+      );
+    });
+
     it('should apply a correct OnlyRule on a canonical to Any', () => {
       const extension = new Extension('Foo');
 
diff --git a/test/fhirtypes/ElementDefinition.constrainType.test.ts b/test/fhirtypes/ElementDefinition.constrainType.test.ts
index 45ca9c4..81d7233 100644
--- a/test/fhirtypes/ElementDefinition.constrainType.test.ts
+++ b/test/fhirtypes/ElementDefinition.constrainType.test.ts
@@ -466,6 +466,25 @@ describe('ElementDefinition', () => {
       );
     });
 
+    it('should allow a canonical to multiple resource types to be constrained with a versioned canonical', () => {
+      const actionDef = planDefinition.elements.find(
+        e => e.id === 'PlanDefinition.action.definition[x]'
+      );
+      const performerConstraint = new OnlyRule('action.definition[x]');
+      performerConstraint.types = [
+        { type: 'ActivityDefinition', isCanonical: true },
+        { type: 'Questionnaire|4.0.1', isCanonical: true }
+      ];
+      actionDef.constrainType(performerConstraint, fisher);
+      expect(actionDef.type).toHaveLength(1);
+      expect(actionDef.type[0]).toEqual(
+        new ElementDefinitionType('canonical').withTargetProfiles(
+          'http://hl7.org/fhir/StructureDefinition/ActivityDefinition',
+          'http://hl7.org/fhir/StructureDefinition/Questionnaire|4.0.1'
+        )
+      );
+    });
+
     it('should allow a canonical to multiple resource types to be constrained to a canonical to a single type', () => {
       const actionDef = planDefinition.elements.find(
         e => e.id === 'PlanDefinition.action.definition[x]'
@@ -596,6 +615,38 @@ describe('ElementDefinition', () => {
       );
     });
 
+    it('should allow a canonical to Any to be constrained to a canonical with a version', () => {
+      const value = extension.elements.find(e => e.id === 'Extension.value[x]');
+      const valueConstraint = new OnlyRule('value[x]');
+      valueConstraint.types = [
+        { type: 'http://hl7.org/fhir/StructureDefinition/Practitioner|4.0.1', isCanonical: true }
+      ];
+      value.constrainType(valueConstraint, fisher);
+      expect(value.type).toHaveLength(1);
+      expect(value.type[0]).toEqual(
+        new ElementDefinitionType('canonical').withTargetProfiles(
+          'http://hl7.org/fhir/StructureDefinition/Practitioner|4.0.1'
+        )
+      );
+    });
+
+    it('should allow a canonical to Any to be constrained to a profile with a version', () => {
+      const value = extension.elements.find(e => e.id === 'Extension.value[x]');
+      const valueConstraint = new OnlyRule('value[x]');
+      valueConstraint.types = [
+        { type: 'http://hl7.org/fhir/StructureDefinition/bp|4.0.1', isCanonical: true }
+      ];
+      value.constrainType(valueConstraint, fisher);
+      expect(value.type).toHaveLength(1);
+      expect(value.type[0]).toEqual(
+        new ElementDefinitionType('canonical').withTargetProfiles(
+          'http://hl7.org/fhir/StructureDefinition/bp|4.0.1'
+        )
+      );
+    });
+
+    it('should output the canonical with the version preceding the fragment when the url has both a version and a fragment', () => {});
+
     it('should allow a canonical to multiple resource types to be constrained such that only the target reference is constrained and others remain as-is', () => {
       const actionDef = planDefinition.elements.find(
         e => e.id === 'PlanDefinition.action.definition[x]'