Repository: FHIR/sushi (ts)

Issue:
Support Canonical keyword w/ package id and name
In some cases (e.g., writing a `CapabilityStatement`), the author needs to refer to the canonical URL for the IG itself.  Currently, that would have to be hard-coded, but the `Canonical` keyword should support passing in the package id or name (specified in sushi-config.yaml) to resolve it to the Canonical URL.

See: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/Canonical.20of.20the.20IG.20itself.3F

Pull request description:
Canonical keyword support for Implementation Guide url
Fixes #963, allowing the canonical of the `ImplementationGuide` itself to be generated by the `Canonical()` shortcut when the user supplied the `packageId`, `name`, or `id` of the package. If the configuration in the package does not include a `url` property, it is generated with the pattern `<package-canonical>/ImplementationGuide/<package-id>`.

Patch:
diff --git a/src/export/Package.ts b/src/export/Package.ts
index 7a84556..bb7d5e7 100644
--- a/src/export/Package.ts
+++ b/src/export/Package.ts
@@ -140,6 +140,21 @@ export class Package implements Fishable {
         metadata.parent = result.baseDefinition;
       }
       return metadata;
+    } else if (
+      // If nothing is returned, perhaps the Package itself is being referenced
+      item === this.config.packageId ||
+      item === this.config.name ||
+      item === this.config.id
+    ) {
+      const metadata: Metadata = {
+        id: this.config.packageId || this.config.id,
+        name: this.config.name,
+        url:
+          this.config.url ||
+          `${this.config.canonical}/ImplementationGuide/${this.config.packageId || this.config.id}`,
+        resourceType: 'ImplementationGuide'
+      };
+      return metadata;
     }
   }
 }
diff --git a/src/import/loadConfigurationFromIgResource.ts b/src/import/loadConfigurationFromIgResource.ts
index 4b88028..fdd46f8 100644
--- a/src/import/loadConfigurationFromIgResource.ts
+++ b/src/import/loadConfigurationFromIgResource.ts
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
 import ini from 'ini';
 import { Fhir as FHIRConverter } from 'fhir/fhir';
 import { Configuration } from '../fshtypes';
-import { ImplementationGuide } from '../fhirtypes';
+import { ImplementationGuide, ImplementationGuideDependsOn } from '../fhirtypes';
 import { logger } from '../utils';
 
 /**
@@ -59,15 +59,22 @@ export function loadConfigurationFromIgResource(igRoot: string): Configuration |
   // Extract the configuration from the resource
   if (igResource && igResource.url && !multipleIgs) {
     logger.info(`Extracting FSHOnly configuration from ${igPath}...`);
-    const config = {
+    const config: Configuration = {
+      id: igResource.id,
       canonical: igResource.url.replace(/\/ImplementationGuide.*/, ''),
+      url: igResource.url,
+      name: igResource.name,
+      packageId: igResource.packageId,
       version: igResource.version,
       fhirVersion: igResource.fhirVersion ?? [],
       dependencies: igResource.dependsOn?.filter(dep => dep.packageId && dep.version),
       parameters: igResource.definition?.parameter ?? [],
       FSHOnly: true
     };
-    config.dependencies?.forEach(dep => {
+    if (!igResource.id) {
+      delete config.id;
+    }
+    config.dependencies?.forEach((dep: ImplementationGuideDependsOn) => {
       if (/[A-Z]/.test(dep.packageId)) {
         const lowercasePackageId = dep.packageId.toLowerCase();
         logger.warn(


Test patch:
diff --git a/test/export/Package.test.ts b/test/export/Package.test.ts
index 537d6ba..749756c 100644
--- a/test/export/Package.test.ts
+++ b/test/export/Package.test.ts
@@ -5,6 +5,7 @@ import { minimalConfig } from '../utils/minimalConfig';
 
 describe('Package', () => {
   const pkg: Package = new Package(minimalConfig);
+  pkg.config.url = 'http://not-an-auto-generated-url.org';
   beforeAll(() => {
     // Profile[0]: Funny / fun-ny / Condition
     const profile0 = new StructureDefinition();
@@ -810,5 +811,35 @@ describe('Package', () => {
 
       expect(pkg.fishForMetadata('DrSue')).toEqual(drSueInstanceByID);
     });
+
+    it('should return package metadata when fishing with the package id', () => {
+      const packageMetadata = pkg.fishForMetadata('fhir.us.minimal');
+
+      expect(packageMetadata.id).toEqual(minimalConfig.id);
+      expect(packageMetadata.name).toEqual(minimalConfig.name);
+      expect(packageMetadata.url).toEqual('http://not-an-auto-generated-url.org');
+      expect(packageMetadata.resourceType).toEqual('ImplementationGuide');
+    });
+
+    it('should return package metadata when fishing with the package name', () => {
+      const packageMetadata = pkg.fishForMetadata('MinimalIG');
+
+      expect(packageMetadata.id).toEqual(minimalConfig.id);
+      expect(packageMetadata.name).toEqual(minimalConfig.name);
+      expect(packageMetadata.url).toEqual('http://not-an-auto-generated-url.org');
+      expect(packageMetadata.resourceType).toEqual('ImplementationGuide');
+    });
+
+    it('should return package metadata with an auto-generated url when url is missing', () => {
+      delete pkg.config.url;
+      const packageMetadata = pkg.fishForMetadata('MinimalIG');
+
+      expect(packageMetadata.id).toEqual(minimalConfig.id);
+      expect(packageMetadata.name).toEqual(minimalConfig.name);
+      expect(packageMetadata.url).toEqual(
+        `${minimalConfig.canonical}/ImplementationGuide/${minimalConfig.id}`
+      );
+      expect(packageMetadata.resourceType).toEqual('ImplementationGuide');
+    });
   });
 });
diff --git a/test/import/fixtures/ig-JSON-uppercase-deps/input/ImplementationGuide.json b/test/import/fixtures/ig-JSON-uppercase-deps/input/ImplementationGuide.json
index c9d07dd..bc6c1e0 100644
--- a/test/import/fixtures/ig-JSON-uppercase-deps/input/ImplementationGuide.json
+++ b/test/import/fixtures/ig-JSON-uppercase-deps/input/ImplementationGuide.json
@@ -1,13 +1,24 @@
 {
   "resourceType": "ImplementationGuide",
   "url": "http://example.org/ImplementationGuide",
+  "name": "TestIG",
+  "packageId": "fhir.test.ig",
   "dependsOn": [
-    { "packageId": "FOO.BAR", "version": "1.2.3" },
-    { "packageId": "bAr.fOo", "version": "current" },
-    { "packageId": "boo.far", "version": "0.0.1" }
+    {
+      "packageId": "FOO.BAR",
+      "version": "1.2.3"
+    },
+    {
+      "packageId": "bAr.fOo",
+      "version": "current"
+    },
+    {
+      "packageId": "boo.far",
+      "version": "0.0.1"
+    }
   ],
   "fhirVersion": [
     "4.0.1"
   ],
   "version": "1.0.0"
-}
+}
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON/input/ImplementationGuide.json b/test/import/fixtures/ig-JSON/input/ImplementationGuide.json
index 2275d22..6dd1d4f 100644
--- a/test/import/fixtures/ig-JSON/input/ImplementationGuide.json
+++ b/test/import/fixtures/ig-JSON/input/ImplementationGuide.json
@@ -1,6 +1,8 @@
 {
   "resourceType": "ImplementationGuide",
   "url": "http://example.org/ImplementationGuide",
+  "name": "TestIG",
+  "packageId": "fhir.test.ig",
   "dependsOn": [
     {
       "packageId": "foo.bar",
diff --git a/test/import/fixtures/ig-XML-uppercase-deps/input/ImplementationGuide.xml b/test/import/fixtures/ig-XML-uppercase-deps/input/ImplementationGuide.xml
index fa2b5fb..6247a0b 100644
--- a/test/import/fixtures/ig-XML-uppercase-deps/input/ImplementationGuide.xml
+++ b/test/import/fixtures/ig-XML-uppercase-deps/input/ImplementationGuide.xml
@@ -1,6 +1,8 @@
 
 <ImplementationGuide>
   <url value="http://example.org/ImplementationGuide"/>
+  <name value="TestIG"/>
+  <packageId value="fhir.test.ig"/>
   <dependsOn>
     <packageId value="FOO.BAR"/>
     <version value="1.2.3"/>
diff --git a/test/import/fixtures/ig-XML/input/ImplementationGuide.xml b/test/import/fixtures/ig-XML/input/ImplementationGuide.xml
index 7ff041d..1188b9d 100644
--- a/test/import/fixtures/ig-XML/input/ImplementationGuide.xml
+++ b/test/import/fixtures/ig-XML/input/ImplementationGuide.xml
@@ -1,6 +1,8 @@
 
 <ImplementationGuide>
   <url value="http://example.org/ImplementationGuide"/>
+  <name value="TestIG"/>
+  <packageId value="fhir.test.ig"/>
   <dependsOn>
     <packageId value="foo.bar"/>
     <version value="1.2.3"/>
diff --git a/test/import/loadConfigurationFromIgResource.test.ts b/test/import/loadConfigurationFromIgResource.test.ts
index 7707a40..3d52913 100644
--- a/test/import/loadConfigurationFromIgResource.test.ts
+++ b/test/import/loadConfigurationFromIgResource.test.ts
@@ -15,6 +15,11 @@ describe('loadConfigurationFromIgResource', () => {
     const config = loadConfigurationFromIgResource(inputPath);
     expect(config).toEqual({
       canonical: 'http://example.org',
+      name: undefined,
+      dependencies: undefined,
+      packageId: undefined,
+      url: 'http://example.org/ImplementationGuide',
+      version: undefined,
       FSHOnly: true,
       fhirVersion: [],
       parameters: []
@@ -31,9 +36,13 @@ describe('loadConfigurationFromIgResource', () => {
     const config = loadConfigurationFromIgResource(inputPath);
     expect(config).toEqual({
       canonical: 'http://example.org',
+      url: 'http://example.org/ImplementationGuide',
       FSHOnly: true,
       fhirVersion: [],
-      parameters: []
+      parameters: [],
+      dependencies: undefined,
+      packageId: undefined,
+      version: undefined
     });
     expect(loggerSpy.getFirstMessage('info')).toMatch(
       new RegExp(`from ${escapeRegExp(path.join(inputPath, 'input', 'ImplementationGuide.json'))}`)
@@ -45,6 +54,9 @@ describe('loadConfigurationFromIgResource', () => {
     const config = loadConfigurationFromIgResource(inputPath);
     expect(config).toEqual({
       canonical: 'http://example.org',
+      url: 'http://example.org/ImplementationGuide',
+      name: 'TestIG',
+      packageId: 'fhir.test.ig',
       FSHOnly: true,
       dependencies: [
         { packageId: 'foo.bar', version: '1.2.3' },
@@ -68,21 +80,26 @@ describe('loadConfigurationFromIgResource', () => {
     );
     expect(loggerSpy.getMessageAtIndex(1, 'info')).toEqual('Extracted configuration:');
     expect(loggerSpy.getMessageAtIndex(2, 'info')).toEqual('  canonical: "http://example.org"');
-    expect(loggerSpy.getMessageAtIndex(3, 'info')).toEqual('  version: "1.0.0"');
-    expect(loggerSpy.getMessageAtIndex(4, 'info')).toEqual('  fhirVersion[0]: "4.0.1"');
-    expect(loggerSpy.getMessageAtIndex(5, 'info')).toEqual(
+    expect(loggerSpy.getMessageAtIndex(3, 'info')).toEqual(
+      '  url: "http://example.org/ImplementationGuide"'
+    );
+    expect(loggerSpy.getMessageAtIndex(4, 'info')).toEqual('  name: "TestIG"');
+    expect(loggerSpy.getMessageAtIndex(5, 'info')).toEqual('  packageId: "fhir.test.ig"');
+    expect(loggerSpy.getMessageAtIndex(6, 'info')).toEqual('  version: "1.0.0"');
+    expect(loggerSpy.getMessageAtIndex(7, 'info')).toEqual('  fhirVersion[0]: "4.0.1"');
+    expect(loggerSpy.getMessageAtIndex(8, 'info')).toEqual(
       '  dependencies[0]: {"packageId":"foo.bar","version":"1.2.3"}'
     );
-    expect(loggerSpy.getMessageAtIndex(6, 'info')).toEqual(
+    expect(loggerSpy.getMessageAtIndex(9, 'info')).toEqual(
       '  dependencies[1]: {"packageId":"bar.foo","version":"current"}'
     );
-    expect(loggerSpy.getMessageAtIndex(7, 'info')).toEqual(
+    expect(loggerSpy.getMessageAtIndex(10, 'info')).toEqual(
       '  parameters[0]: {"code":"path-resource","value":"input/resources"}'
     );
-    expect(loggerSpy.getMessageAtIndex(8, 'info')).toEqual(
+    expect(loggerSpy.getMessageAtIndex(11, 'info')).toEqual(
       '  parameters[1]: {"code":"path-resource","value":"input/second-resource"}'
     );
-    expect(loggerSpy.getMessageAtIndex(9, 'info')).toEqual('  FSHOnly: true');
+    expect(loggerSpy.getMessageAtIndex(12, 'info')).toEqual('  FSHOnly: true');
   });
 
   it('should convert uppercase package Ids to lowercase', () => {
@@ -90,6 +107,9 @@ describe('loadConfigurationFromIgResource', () => {
     const config = loadConfigurationFromIgResource(inputPath);
     expect(config).toEqual({
       canonical: 'http://example.org',
+      url: 'http://example.org/ImplementationGuide',
+      name: 'TestIG',
+      packageId: 'fhir.test.ig',
       FSHOnly: true,
       dependencies: [
         { packageId: 'foo.bar', version: '1.2.3' },
@@ -105,18 +125,23 @@ describe('loadConfigurationFromIgResource', () => {
     );
     expect(loggerSpy.getMessageAtIndex(1, 'info')).toEqual('Extracted configuration:');
     expect(loggerSpy.getMessageAtIndex(2, 'info')).toEqual('  canonical: "http://example.org"');
-    expect(loggerSpy.getMessageAtIndex(3, 'info')).toEqual('  version: "1.0.0"');
-    expect(loggerSpy.getMessageAtIndex(4, 'info')).toEqual('  fhirVersion[0]: "4.0.1"');
-    expect(loggerSpy.getMessageAtIndex(5, 'info')).toEqual(
+    expect(loggerSpy.getMessageAtIndex(3, 'info')).toEqual(
+      '  url: "http://example.org/ImplementationGuide"'
+    );
+    expect(loggerSpy.getMessageAtIndex(4, 'info')).toEqual('  name: "TestIG"');
+    expect(loggerSpy.getMessageAtIndex(5, 'info')).toEqual('  packageId: "fhir.test.ig"');
+    expect(loggerSpy.getMessageAtIndex(6, 'info')).toEqual('  version: "1.0.0"');
+    expect(loggerSpy.getMessageAtIndex(7, 'info')).toEqual('  fhirVersion[0]: "4.0.1"');
+    expect(loggerSpy.getMessageAtIndex(8, 'info')).toEqual(
       '  dependencies[0]: {"packageId":"foo.bar","version":"1.2.3"}'
     );
-    expect(loggerSpy.getMessageAtIndex(6, 'info')).toEqual(
+    expect(loggerSpy.getMessageAtIndex(9, 'info')).toEqual(
       '  dependencies[1]: {"packageId":"bar.foo","version":"current"}'
     );
-    expect(loggerSpy.getMessageAtIndex(7, 'info')).toEqual(
+    expect(loggerSpy.getMessageAtIndex(10, 'info')).toEqual(
       '  dependencies[2]: {"packageId":"boo.far","version":"0.0.1"}'
     );
-    expect(loggerSpy.getMessageAtIndex(8, 'info')).toEqual('  FSHOnly: true');
+    expect(loggerSpy.getMessageAtIndex(11, 'info')).toEqual('  FSHOnly: true');
   });
 
   it('should extract an XML configuration with a url and dependencies', () => {
@@ -124,6 +149,9 @@ describe('loadConfigurationFromIgResource', () => {
     const config = loadConfigurationFromIgResource(inputPath);
     expect(config).toEqual({
       canonical: 'http://example.org',
+      url: 'http://example.org/ImplementationGuide',
+      name: 'TestIG',
+      packageId: 'fhir.test.ig',
       FSHOnly: true,
       dependencies: [
         { packageId: 'foo.bar', version: '1.2.3' },
@@ -137,15 +165,20 @@ describe('loadConfigurationFromIgResource', () => {
     );
     expect(loggerSpy.getMessageAtIndex(1, 'info')).toEqual('Extracted configuration:');
     expect(loggerSpy.getMessageAtIndex(2, 'info')).toEqual('  canonical: "http://example.org"');
-    expect(loggerSpy.getMessageAtIndex(3, 'info')).toEqual('  version: undefined');
-    expect(loggerSpy.getMessageAtIndex(4, 'info')).toEqual('  fhirVersion[0]: "4.0.1"');
-    expect(loggerSpy.getMessageAtIndex(5, 'info')).toEqual(
+    expect(loggerSpy.getMessageAtIndex(3, 'info')).toEqual(
+      '  url: "http://example.org/ImplementationGuide"'
+    );
+    expect(loggerSpy.getMessageAtIndex(4, 'info')).toEqual('  name: "TestIG"');
+    expect(loggerSpy.getMessageAtIndex(5, 'info')).toEqual('  packageId: "fhir.test.ig"');
+    expect(loggerSpy.getMessageAtIndex(6, 'info')).toEqual('  version: undefined');
+    expect(loggerSpy.getMessageAtIndex(7, 'info')).toEqual('  fhirVersion[0]: "4.0.1"');
+    expect(loggerSpy.getMessageAtIndex(8, 'info')).toEqual(
       '  dependencies[0]: {"packageId":"foo.bar","version":"1.2.3"}'
     );
-    expect(loggerSpy.getMessageAtIndex(6, 'info')).toEqual(
+    expect(loggerSpy.getMessageAtIndex(9, 'info')).toEqual(
       '  dependencies[1]: {"packageId":"bar.foo","version":"current"}'
     );
-    expect(loggerSpy.getMessageAtIndex(7, 'info')).toEqual('  FSHOnly: true');
+    expect(loggerSpy.getMessageAtIndex(10, 'info')).toEqual('  FSHOnly: true');
   });
 
   it('should convert uppercase package Ids to lowercase when extracting from XML configuration', () => {
@@ -153,6 +186,9 @@ describe('loadConfigurationFromIgResource', () => {
     const config = loadConfigurationFromIgResource(inputPath);
     expect(config).toEqual({
       canonical: 'http://example.org',
+      url: 'http://example.org/ImplementationGuide',
+      name: 'TestIG',
+      packageId: 'fhir.test.ig',
       FSHOnly: true,
       dependencies: [
         { packageId: 'foo.bar', version: '1.2.3' },
@@ -167,18 +203,23 @@ describe('loadConfigurationFromIgResource', () => {
     );
     expect(loggerSpy.getMessageAtIndex(1, 'info')).toEqual('Extracted configuration:');
     expect(loggerSpy.getMessageAtIndex(2, 'info')).toEqual('  canonical: "http://example.org"');
-    expect(loggerSpy.getMessageAtIndex(3, 'info')).toEqual('  version: undefined');
-    expect(loggerSpy.getMessageAtIndex(4, 'info')).toEqual('  fhirVersion[0]: "4.0.1"');
-    expect(loggerSpy.getMessageAtIndex(5, 'info')).toEqual(
+    expect(loggerSpy.getMessageAtIndex(3, 'info')).toEqual(
+      '  url: "http://example.org/ImplementationGuide"'
+    );
+    expect(loggerSpy.getMessageAtIndex(4, 'info')).toEqual('  name: "TestIG"');
+    expect(loggerSpy.getMessageAtIndex(5, 'info')).toEqual('  packageId: "fhir.test.ig"');
+    expect(loggerSpy.getMessageAtIndex(6, 'info')).toEqual('  version: undefined');
+    expect(loggerSpy.getMessageAtIndex(7, 'info')).toEqual('  fhirVersion[0]: "4.0.1"');
+    expect(loggerSpy.getMessageAtIndex(8, 'info')).toEqual(
       '  dependencies[0]: {"packageId":"foo.bar","version":"1.2.3"}'
     );
-    expect(loggerSpy.getMessageAtIndex(6, 'info')).toEqual(
+    expect(loggerSpy.getMessageAtIndex(9, 'info')).toEqual(
       '  dependencies[1]: {"packageId":"bar.foo","version":"current"}'
     );
-    expect(loggerSpy.getMessageAtIndex(7, 'info')).toEqual(
+    expect(loggerSpy.getMessageAtIndex(10, 'info')).toEqual(
       '  dependencies[2]: {"packageId":"boo.far","version":"0.0.1"}'
     );
-    expect(loggerSpy.getMessageAtIndex(8, 'info')).toEqual('  FSHOnly: true');
+    expect(loggerSpy.getMessageAtIndex(11, 'info')).toEqual('  FSHOnly: true');
   });
 
   it('should find the ImplementationGuide JSON file even when other files are present', () => {
@@ -186,6 +227,7 @@ describe('loadConfigurationFromIgResource', () => {
     const config = loadConfigurationFromIgResource(inputPath);
     expect(config).toEqual({
       canonical: 'http://example.org',
+      url: 'http://example.org/ImplementationGuide',
       FSHOnly: true,
       fhirVersion: [],
       parameters: []
diff --git a/test/utils/Processing.test.ts b/test/utils/Processing.test.ts
index 7f7ecff..2e03936 100644
--- a/test/utils/Processing.test.ts
+++ b/test/utils/Processing.test.ts
@@ -270,8 +270,13 @@ describe('Processing', () => {
       expect(config).toEqual({
         FSHOnly: true,
         canonical: 'http://example.org',
+        dependencies: undefined,
         fhirVersion: ['4.0.1'],
-        parameters: []
+        name: undefined,
+        packageId: undefined,
+        parameters: [],
+        url: 'http://example.org',
+        version: undefined
       });
     });
 
@@ -891,7 +896,7 @@ describe('Processing', () => {
 
         // Make sure we didn't create the devious path
         const deviousPath = path.join(tempDeviousIGPubRoot, 'fsh-generated', 'devious');
-        expect(fs.existsSync(deviousPath)).toBeFalse();
+        expect(fs.existsSync(deviousPath)).toBe(false);
 
         // Make sure we do have all the good file names
         const angelicPath = path.join(tempDeviousIGPubRoot, 'fsh-generated', 'resources');