Repository: FHIR/sushi
Pull request #1423 (2024-02-02 21:33:32)

Fall back to package.json when resolving "latest" version in IG
When a dependency uses the "latest" version in sushi-config.yaml, SUSHI must populate the generated IG JSON with the real version number (not "latest"). PR #1409 fixes this for dependency packages that contain an ImplementationGuide resource, but it does not address the case were a dependency package does not have an ImplementationGuide resource. In this case, we should fall back to the package's package.json file.

This was raised on Zulip here: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/automatically.20get.20latest.20dependencies/near/419233182


Patch:
diff --git a/src/fhirdefs/FHIRDefinitions.ts b/src/fhirdefs/FHIRDefinitions.ts
index 2a6e331..0b33e32 100644
--- a/src/fhirdefs/FHIRDefinitions.ts
+++ b/src/fhirdefs/FHIRDefinitions.ts
@@ -25,6 +25,11 @@ export class FHIRDefinitions extends BaseFHIRDefinitions implements Fishable {
     }
   }
 
+  // Expose the package.json files to support extracting the version when "latest" is used
+  allPackageJsons(): any[] {
+    return Array.from(this.packageJsons?.values() ?? []);
+  }
+
   // This getter is only used in tests to verify what supplemental packages are loaded
   get supplementalFHIRPackages(): string[] {
     return flatten(Array.from(this.supplementalFHIRDefinitions.keys()));
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index a0a46a0..c25bf41 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -271,8 +271,15 @@ export class IGExporter {
 
     if (dependsOn.version === 'latest') {
       const dependencyIG = igs.find(ig => ig.packageId === dependsOn.packageId);
-      if (dependencyIG != null) {
+      if (dependencyIG?.version != null) {
         dependsOn.version = dependencyIG.version;
+      } else {
+        const packageJSON = this.fhirDefs
+          .allPackageJsons()
+          .find(p => p.name === dependsOn.packageId);
+        if (packageJSON?.version != null) {
+          dependsOn.version = packageJSON.version;
+        }
       }
     }
 


Test patch:
diff --git a/test/fhirdefs/FHIRDefinitions.test.ts b/test/fhirdefs/FHIRDefinitions.test.ts
index 7e1eaba..2083285 100644
--- a/test/fhirdefs/FHIRDefinitions.test.ts
+++ b/test/fhirdefs/FHIRDefinitions.test.ts
@@ -1231,4 +1231,16 @@ describe('FHIRDefinitions', () => {
       ]);
     });
   });
+
+  describe('#allPackageJSONs', () => {
+    it('should return all package jsons', () => {
+      const testDefs = new FHIRDefinitions();
+      testDefs.addPackageJson('sushi.test.1', { name: 'sushi.test.1', version: '0.0.1' });
+      testDefs.addPackageJson('sushi.test.2', { name: 'sushi.test.2', version: '0.0.2' });
+      expect(testDefs.allPackageJsons()).toEqual([
+        { name: 'sushi.test.1', version: '0.0.1' },
+        { name: 'sushi.test.2', version: '0.0.2' }
+      ]);
+    });
+  });
 });
diff --git a/test/ig/IGExporter.IG.test.ts b/test/ig/IGExporter.IG.test.ts
index 15722df..7a7572e 100644
--- a/test/ig/IGExporter.IG.test.ts
+++ b/test/ig/IGExporter.IG.test.ts
@@ -467,30 +467,6 @@ describe('IGExporter', () => {
       ]);
     });
 
-    it('should use the resolved version of a package when a dependency version is "latest"', () => {
-      config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: 'latest' }];
-      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 US Core is exported with the resolved version
-      expect(dependencies).toEqual([
-        {
-          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 version is not provided', () => {
       config.dependencies = [
         // NOTE: version is intentionally missing
@@ -5330,4 +5306,75 @@ describe('IGExporter', () => {
       ]);
     });
   });
+
+  describe('#resolve-latest', () => {
+    let ig: any;
+
+    beforeAll(() => {
+      loggerSpy.reset();
+      const tempOut = temp.mkdirSync('sushi-test');
+      const fixtures = path.join(__dirname, 'fixtures', 'simple-ig');
+      const defs = new FHIRDefinitions();
+      // r4-definitions contains ImplementationGuide-hl7.fhir.us.core.json used for resolution
+      loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs);
+      // add de.medizininformatikinitiative.kerndatensatz.consent package.json used for resolution
+      defs.addPackageJson('de.medizininformatikinitiative.kerndatensatz.consent', {
+        name: 'de.medizininformatikinitiative.kerndatensatz.consent',
+        version: '1.0.6',
+        description: 'Put a description here',
+        author: 'sebastianstubert',
+        fhirVersions: ['4.0.1'],
+        dependencies: {
+          'de.einwilligungsmanagement': '1.0.1'
+        }
+      });
+      const config = cloneDeep(minimalConfig);
+      config.dependencies = [
+        { packageId: 'hl7.fhir.us.core', version: 'latest' },
+        { packageId: 'de.medizininformatikinitiative.kerndatensatz.consent', version: 'latest' }
+      ];
+      const pkg = new Package(config);
+      const exporter = new IGExporter(pkg, defs, fixtures);
+      // No need to regenerate the IG on every test -- generate it once and inspect what you
+      // need to in the tests
+      exporter.export(tempOut);
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      ig = fs.readJSONSync(igPath);
+    });
+
+    afterAll(() => {
+      temp.cleanupSync();
+    });
+
+    it('should use the resolved version of a package from IG JSON when a dependency version is "latest"', () => {
+      const dependencies: ImplementationGuideDependsOn[] = ig.dependsOn;
+      expect(loggerSpy.getAllLogs('error')).toHaveLength(0);
+      // Ensure US Core is exported with the resolved version
+      expect(dependencies).toContainEqual({
+        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 use the resolved version of a package from package.json when a dependency version is "latest"', () => {
+      // See: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/automatically.20get.20latest.20dependencies/near/419245939
+      const dependencies: ImplementationGuideDependsOn[] = ig.dependsOn;
+      expect(loggerSpy.getAllLogs('error')).toHaveLength(0);
+      // Ensure consent is exported with the resolved version
+      expect(dependencies).toContainEqual({
+        id: 'de_medizininformatikinitiative_kerndatensatz_consent',
+        uri: 'http://fhir.org/packages/de.medizininformatikinitiative.kerndatensatz.consent/ImplementationGuide/de.medizininformatikinitiative.kerndatensatz.consent',
+        packageId: 'de.medizininformatikinitiative.kerndatensatz.consent',
+        version: '1.0.6'
+      });
+    });
+  });
 });