Repository: FHIR/sushi
Pull request #709 (2020-12-31 19:00:58)

Support canonical fragments
Fixes https://github.com/FHIR/sushi/issues/688.

This supports using fragments of the form `#{id}` when the `Canonical` keyword is used for an inline instance that does not have an absolute URL. The issue description provides a good reference to test this on, but the first instance in that example must be set to `Usage: #inline` for this to work. I wasn't sure if this should only work on inline instances, or all instances, so let me know if you think I approached that wrong here.

ngfreiter: @jafeltra, I've added that test here: https://github.com/FHIR/sushi/pull/709/commits/00bf538b776e65f279f574e064414eb3543bc3ec.

Patch:
diff --git a/src/export/Package.ts b/src/export/Package.ts
index f97a41a..aa5bf23 100644
--- a/src/export/Package.ts
+++ b/src/export/Package.ts
@@ -59,6 +59,8 @@ export class Package implements Fishable {
       const metadata: Metadata = {
         id: result.id,
         name: result instanceof InstanceDefinition ? result._instanceMeta.name : result.name,
+        instanceUsage:
+          result instanceof InstanceDefinition ? result._instanceMeta.usage : undefined,
         url: result.url
       };
       if (result instanceof StructureDefinition) {
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index e364f07..3056816 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -1223,7 +1223,7 @@ export class ElementDefinition {
       case 'Canonical':
         value = value as FshCanonical;
         // Get the canonical url of the entity
-        let canonicalUrl = fisher.fishForMetadata(
+        const canonicalDefinition = fisher.fishForMetadata(
           value.entityName,
           Type.Resource,
           Type.Type,
@@ -1232,8 +1232,13 @@ export class ElementDefinition {
           Type.ValueSet,
           Type.CodeSystem,
           Type.Instance
-        )?.url;
-        if (!canonicalUrl) {
+        );
+        let canonicalUrl: string;
+        if (canonicalDefinition?.url) {
+          canonicalUrl = canonicalDefinition.url;
+        } else if (canonicalDefinition?.id && canonicalDefinition.instanceUsage === 'Inline') {
+          canonicalUrl = `#${canonicalDefinition.id}`;
+        } else {
           throw new InvalidCanonicalUrlError(value.entityName);
         }
         if (value.version) {
diff --git a/src/import/FSHTank.ts b/src/import/FSHTank.ts
index 097452c..a7482b4 100644
--- a/src/import/FSHTank.ts
+++ b/src/import/FSHTank.ts
@@ -214,6 +214,8 @@ export class FSHTank implements Fishable {
         meta.parent = result.parent;
       } else if (result instanceof FshValueSet || result instanceof FshCodeSystem) {
         meta.url = getUrlFromFshDefinition(result, this.config.canonical);
+      } else if (result instanceof Instance) {
+        meta.instanceUsage = result.usage;
       }
       return meta;
     }
diff --git a/src/utils/Fishable.ts b/src/utils/Fishable.ts
index 5973bfa..2e255c1 100644
--- a/src/utils/Fishable.ts
+++ b/src/utils/Fishable.ts
@@ -1,3 +1,5 @@
+import { Instance } from '../fshtypes';
+
 export enum Type {
   Profile,
   Extension,
@@ -18,6 +20,7 @@ export interface Metadata {
   url?: string;
   parent?: string;
   abstract?: boolean;
+  instanceUsage?: Instance['usage'];
 }
 
 export interface Fishable {


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index aacb030..1bf909f 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -1229,6 +1229,25 @@ describe('InstanceExporter', () => {
       });
     });
 
+    it('should apply an Assignment rule with Canonical of an inline instance', () => {
+      const observationInstance = new Instance('MyObservation');
+      observationInstance.instanceOf = 'Observation';
+      doc.instances.set(observationInstance.name, observationInstance);
+
+      const inlineInstance = new Instance('MyMedRequest');
+      inlineInstance.usage = 'Inline';
+      doc.instances.set(inlineInstance.name, inlineInstance);
+
+      const assignedValueRule = new AssignmentRule('code.coding.system');
+      assignedValueRule.value = new FshCanonical('MyMedRequest');
+      observationInstance.rules.push(assignedValueRule);
+
+      const exported = exportInstance(observationInstance);
+      expect(exported.code).toEqual({
+        coding: [{ system: '#MyMedRequest' }]
+      });
+    });
+
     it('should not apply an Assignment rule with an invalid Canonical entity and log an error', () => {
       const observationInstance = new Instance('MyObservation');
       observationInstance.instanceOf = 'Observation';
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 3d22ea9..890711d 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -1655,6 +1655,24 @@ describe('StructureDefinitionExporter', () => {
     expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
   });
 
+  it('should apply an Assignment rule with Canonical of an inline instance', () => {
+    const profile = new Profile('MyObservation');
+    profile.parent = 'Observation';
+    const rule = new AssignmentRule('code.coding.system');
+    rule.value = new FshCanonical('MyCodeSystem');
+    profile.rules.push(rule);
+
+    const inlineInstance = new Instance('MyCodeSystem');
+    inlineInstance.usage = 'Inline';
+    doc.instances.set(inlineInstance.name, inlineInstance);
+
+    exporter.exportStructDef(profile);
+    const sd = pkg.profiles[0];
+    const assignedSystem = sd.findElement('Observation.code.coding.system');
+    expect(assignedSystem.patternUri).toEqual('#MyCodeSystem');
+    expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+  });
+
   it('should apply an AssignmentRule with Canonical of a FHIR entity', () => {
     const profile = new Profile('MyObservation');
     profile.parent = 'Observation';
diff --git a/test/import/FSHTank.test.ts b/test/import/FSHTank.test.ts
index 5b422ab..4cf22ba 100644
--- a/test/import/FSHTank.test.ts
+++ b/test/import/FSHTank.test.ts
@@ -260,7 +260,8 @@ describe('FSHTank', () => {
     };
     const inst1MD: Metadata = {
       id: 'inst1',
-      name: 'Instance1'
+      name: 'Instance1',
+      instanceUsage: 'Example'
     };
     const inv1MD: Metadata = {
       id: 'Invariant1', // id will always be name on Invariants