Repository: FHIR/sushi (ts)

Issue:
Automatically infer canonical URI of dependency from package.json
SUSHI needs a URI for each dependency IG so that it may populate `ImplementationGuide.dependsOn`. However, certain packages do not have ImplementationGuide resources, so we cannot get a canonical URI. In this case, we require that the user manually specify the canonical in the `dependencies` object in `sushi-config.yaml`. We could instead try to infer the URI from the `package.json` which should be present in the package. This may present its own problems, and we should investigate and test some different packages before adding support.

See this thread for details: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/ImplementationGuide.3A.20ImplementationGuide.20instance.20misses.20url.

Pull request description:
Load package.json to provide canonical
Fixes #948 and completes [CIMPL-845](https://standardhealthrecord.atlassian.net/browse/CIMPL-845).

When loading a dependency package, store the package.json in the FHIRDefinitions. When exporting an IG, if the dependency package does not contain its own ImplementationGuide, check the package.json to try to get the canonical URL for the package.

As a side note: the package `de.gkvsv.eRezeptAbrechnungsdaten#1.1.0-rc`, which was mentioned in the thread that led to this issue, has a package.json and no ImplementationGuide. But, the package.json doesn't have a canonical field, so SUSHI will still (correctly) produce the same error as before.

Patch:
diff --git a/src/fhirdefs/FHIRDefinitions.ts b/src/fhirdefs/FHIRDefinitions.ts
index 367ac6b..28a0760 100644
--- a/src/fhirdefs/FHIRDefinitions.ts
+++ b/src/fhirdefs/FHIRDefinitions.ts
@@ -14,6 +14,7 @@ export class FHIRDefinitions implements Fishable {
   private implementationGuides: Map<string, any>;
   private predefinedResources: Map<string, any>;
   private supplementalFHIRDefinitions: Map<string, FHIRDefinitions>;
+  private packageJsons: Map<string, any>;
   packages: string[];
 
   constructor(public readonly isSupplementalFHIRDefinitions = false) {
@@ -28,6 +29,7 @@ export class FHIRDefinitions implements Fishable {
     this.implementationGuides = new Map();
     this.predefinedResources = new Map();
     this.supplementalFHIRDefinitions = new Map();
+    this.packageJsons = new Map();
     // FHIR R4 does not have a StructureDefinition that defines "Base" but FHIR R5 does.
     // We have defined a "placeholder" StructureDefinition for "Base" for R4.
     // Inject the R4 "Base" placeholder StructureDefinition
@@ -185,6 +187,14 @@ export class FHIRDefinitions implements Fishable {
     }
   }
 
+  addPackageJson(id: string, definition: any): void {
+    this.packageJsons.set(id, definition);
+  }
+
+  getPackageJson(id: string): any {
+    return this.packageJsons.get(id);
+  }
+
   fishForFHIR(item: string, ...types: Type[]): any | undefined {
     // No types passed in means to search ALL supported types
     if (types.length === 0) {
diff --git a/src/fhirdefs/load.ts b/src/fhirdefs/load.ts
index 3eef5b7..b061146 100644
--- a/src/fhirdefs/load.ts
+++ b/src/fhirdefs/load.ts
@@ -294,6 +294,9 @@ export function loadFromPath(
             fs.readFileSync(path.join(cachePath, cachedPackage, 'package', file), 'utf-8').trim()
           );
           FHIRDefs.add(def);
+          if (file === 'package.json') {
+            FHIRDefs.addPackageJson(targetPackage, def);
+          }
         }
       }
     }
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index c84ab41..f9a4afb 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -238,6 +238,13 @@ export class IGExporter {
             'dev' === dependsOn.version)
       );
       dependsOn.uri = dependencyIG?.url;
+      if (dependsOn.uri == null) {
+        // there may be a package.json that can help us here
+        const dependencyPackageJson = this.fhirDefs.getPackageJson(
+          `${dependsOn.packageId}#${dependsOn.version}`
+        );
+        dependsOn.uri = dependencyPackageJson?.canonical;
+      }
 
       if (dependsOn.uri == null) {
         logger.error(


Test patch:
diff --git a/test/fhirdefs/load.test.ts b/test/fhirdefs/load.test.ts
index ceeca69..c8e4061 100644
--- a/test/fhirdefs/load.test.ts
+++ b/test/fhirdefs/load.test.ts
@@ -58,6 +58,10 @@ describe('#loadFromPath()', () => {
   it('should load base FHIR code sytems', () => {
     expect(defs.allCodeSystems().some(r => r.id === 'allergyintolerance-clinical')).toBeTruthy();
   });
+
+  it('should load the package.json file', () => {
+    expect(defs.getPackageJson('r4-definitions')).toBeDefined();
+  });
 });
 
 describe('#loadDependency()', () => {
diff --git a/test/ig/IGExporter.IG.test.ts b/test/ig/IGExporter.IG.test.ts
index 401d66a..e5e5a69 100644
--- a/test/ig/IGExporter.IG.test.ts
+++ b/test/ig/IGExporter.IG.test.ts
@@ -35,6 +35,11 @@ describe('IGExporter', () => {
 
     beforeAll(() => {
       defs = new FHIRDefinitions();
+      loadFromPath(
+        path.join(__dirname, '..', 'testhelpers', 'testdefs'),
+        'fhir.no.ig.package#1.0.1',
+        defs
+      );
       loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs);
       fixtures = path.join(__dirname, 'fixtures', 'simple-ig');
 
@@ -327,6 +332,39 @@ describe('IGExporter', () => {
       ]);
     });
 
+    it('should get a canonical from package.json when a dependency has no implementation guide', () => {
+      config.dependencies = [
+        { packageId: 'fhir.no.ig.package', version: '1.0.1' },
+        { packageId: 'hl7.fhir.us.core', version: '3.1.0' }
+      ];
+      exporter.export(tempOut);
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-sushi-test.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const content = fs.readJSONSync(igPath);
+      const dependencies: ImplementationGuideDependsOn[] = content.dependsOn;
+      expect(loggerSpy.getAllLogs('error')).toHaveLength(0);
+      // ensure both packages are in the dependencies
+      expect(dependencies).toEqual([
+        {
+          id: 'fhir_no_ig_package',
+          uri: 'http://example.org/fhir/no/ig/package',
+          packageId: 'fhir.no.ig.package',
+          version: '1.0.1'
+        },
+        {
+          id: 'hl7_fhir_us_core',
+          uri: 'http://hl7.org/fhir/us/core/ImplementationGuide/hl7.fhir.us.core',
+          packageId: 'hl7.fhir.us.core',
+          version: '3.1.0'
+        }
+      ]);
+    });
+
     it('should issue an error when a dependency url cannot be inferred', () => {
       config.dependencies = [
         // NOTE: Will not find mCODE IG URL because we didn't load the mcode IG
@@ -1576,11 +1614,6 @@ describe('IGExporter', () => {
 
     beforeAll(() => {
       defs = new FHIRDefinitions();
-      loadFromPath(
-        path.join(__dirname, '..', 'testhelpers', 'testdefs', 'package'),
-        'testPackage',
-        defs
-      );
       fixtures = path.join(__dirname, 'fixtures', 'customized-ig-with-logical-model-example');
       loadCustomResources(path.join(fixtures, 'input'), defs);
     });
diff --git a/test/testhelpers/testdefs/fhir.no.ig.package#1.0.1/package/package.json b/test/testhelpers/testdefs/fhir.no.ig.package#1.0.1/package/package.json
new file mode 100644
index 0000000..cc04956
--- /dev/null
+++ b/test/testhelpers/testdefs/fhir.no.ig.package#1.0.1/package/package.json
@@ -0,0 +1,25 @@
+{
+  "name": "fhir.no.ig.package",
+  "tools-version": 3,
+  "type": "fhir.ig",
+  "date": "20200212130435",
+  "license": "CC0-1.0",
+  "canonical": "http://example.org/fhir/no/ig/package",
+  "url": "http://example.org/fhir/no/ig/package/1.0.1",
+  "title": "FHIR No IG package",
+  "description": "This is a longer description of the No IG package",
+  "fhirVersions": [
+    "4.0.1"
+  ],
+  "dependencies": {
+    "hl7.fhir.r4.core": "4.0.1"
+  },
+  "author": "Example author",
+  "maintainers": [
+    {
+      "name": "Example maintainer",
+      "email": "example.maintainer@example.org"
+    }
+  ],
+  "version": "1.0.1"
+}
\ No newline at end of file
diff --git a/test/testhelpers/testdefs/r4-definitions/package/package.json b/test/testhelpers/testdefs/r4-definitions/package/package.json
new file mode 100644
index 0000000..fce2a1d
--- /dev/null
+++ b/test/testhelpers/testdefs/r4-definitions/package/package.json
@@ -0,0 +1,16 @@
+{
+  "name": "hl7.fhir.r4.core",
+  "version": "4.0.1",
+  "fhir-version-list": [
+    "4.0.1"
+  ],
+  "type": "fhir.core",
+  "license": "CC0-1.0",
+  "title": "FHIR R4 package : Core",
+  "description": "Definitions (API, structures and terminologies) for the R4 version of the FHIR standard",
+  "author": "HL7 Inc",
+  "url": "http://hl7.org/fhir/R4",
+  "tools-version": 3,
+  "canonical": "http://hl7.org/fhir",
+  "homepage": "http://hl7.org/fhir"
+}
\ No newline at end of file