Repository: FHIR/sushi
Pull request #482 (2020-06-08 13:30:23)

Restructure invalid packages to have correct format
This addresses https://standardhealthrecord.atlassian.net/browse/CIMPL-402. 

When reviewing #470, @cmoesel noticed a weird discrepancy between loading US Core 3.1.0 and loading the current version of US Core. The details on that are here https://github.com/FHIR/sushi/pull/470#pullrequestreview-422137685.

A package is supposed to have a certain format in which all of contents are nested within a `package` subfolder, according to https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/dev.20dependencies. However, many older packages did not actually follow this rule, so a package like US Core 3.1.0 may have folders as peers of `package` which should actually be subfolders of package. This PR adds a function to nest all peers that are not within the `package` folder into the `package` folder when downloading a package, so that these incorrectly formatted packages will be fixed by SUSHI to have the correct format.

The FSH implementation of mCODE depends on FHIR Core 4.0.1 and US Core 3.1.0, both of which use the old incorrect format. If you clear your cache, and then run SUSHI on fsh-mcode on master, you will see that those packages are downloaded and saved with certain folders as peers of `package`. If you then clear the cache and run SUSHI on fsh-mcode on this branch, those folders will be nested within `package`, but the output of mCODE will be the same.


Patch:
diff --git a/src/fhirdefs/load.ts b/src/fhirdefs/load.ts
index fc88ee1..fcbf37f 100644
--- a/src/fhirdefs/load.ts
+++ b/src/fhirdefs/load.ts
@@ -127,6 +127,7 @@ export async function loadDependency(
         sync: true,
         strict: true
       });
+      cleanCachedPackage(targetDirectory);
       // Now try to load again from the path
       loadedPackage = loadFromPath(loadPath, fullPackageName, FHIRDefs);
     } else {
@@ -142,6 +143,30 @@ export async function loadDependency(
   return FHIRDefs;
 }
 
+/**
+ * This function takes a package which contains contents at the same level as the "package" folder, and nests
+ * all that content within the "package" folder.
+ *
+ * A package should have the format described here https://confluence.hl7.org/pages/viewpage.action?pageId=35718629#NPMPackageSpecification-Format
+ * in which all contents are within the "package" folder. Some packages (ex US Core 3.1.0) have an incorrect format in which folders
+ * are not sub-folders of "package", but are instead at the same level. The IG Publisher fixes these packages as described
+ * https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/dev.20dependencies, so SUSHI should as well.
+ *
+ * @param {string} packageDirectory - The directory containing the package
+ */
+export function cleanCachedPackage(packageDirectory: string): void {
+  if (fs.existsSync(path.join(packageDirectory, 'package'))) {
+    fs.readdirSync(packageDirectory)
+      .filter(file => file !== 'package')
+      .forEach(file => {
+        fs.renameSync(
+          path.join(packageDirectory, file),
+          path.join(packageDirectory, 'package', file)
+        );
+      });
+  }
+}
+
 /**
  * Loads custom resources defined in ig-data into FHIRDefs
  * @param {string} input - The input path to the cli


Test patch:
diff --git a/test/fhirdefs/fixtures/sushi-test-no-package#current/StructureDefinition-MyPatient.json b/test/fhirdefs/fixtures/sushi-test-no-package#current/StructureDefinition-MyPatient.json
new file mode 100644
index 0000000..4f65cc4
--- /dev/null
+++ b/test/fhirdefs/fixtures/sushi-test-no-package#current/StructureDefinition-MyPatient.json
@@ -0,0 +1,25 @@
+{
+  "resourceType": "StructureDefinition",
+  "id": "MyPatient",
+  "url": "http://hl7.org/fhir/sushi-test/StructureDefinition/MyPatient",
+  "name": "MyPatient",
+  "status": "active",
+  "fhirVersion": "4.0.1",
+  "kind": "resource",
+  "type": "Patient",
+  "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient",
+  "derivation": "constraint",
+  "differential": {
+    "element": [
+      {
+        "id": "Patient.deceased[x]",
+        "path": "Patient.deceased[x]",
+        "type": [
+          {
+            "code": "boolean"
+          }
+        ]
+      }
+    ]
+  }
+}
diff --git a/test/fhirdefs/fixtures/sushi-test-no-package#current/other/filler.txt b/test/fhirdefs/fixtures/sushi-test-no-package#current/other/filler.txt
new file mode 100644
index 0000000..6dee2e8
--- /dev/null
+++ b/test/fhirdefs/fixtures/sushi-test-no-package#current/other/filler.txt
@@ -0,0 +1,1 @@
+Hello I'm filler
\ No newline at end of file
diff --git a/test/fhirdefs/fixtures/sushi-test-wrong-format#current/StructureDefinition-MyPatient.json b/test/fhirdefs/fixtures/sushi-test-wrong-format#current/StructureDefinition-MyPatient.json
new file mode 100644
index 0000000..4f65cc4
--- /dev/null
+++ b/test/fhirdefs/fixtures/sushi-test-wrong-format#current/StructureDefinition-MyPatient.json
@@ -0,0 +1,25 @@
+{
+  "resourceType": "StructureDefinition",
+  "id": "MyPatient",
+  "url": "http://hl7.org/fhir/sushi-test/StructureDefinition/MyPatient",
+  "name": "MyPatient",
+  "status": "active",
+  "fhirVersion": "4.0.1",
+  "kind": "resource",
+  "type": "Patient",
+  "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient",
+  "derivation": "constraint",
+  "differential": {
+    "element": [
+      {
+        "id": "Patient.deceased[x]",
+        "path": "Patient.deceased[x]",
+        "type": [
+          {
+            "code": "boolean"
+          }
+        ]
+      }
+    ]
+  }
+}
diff --git a/test/fhirdefs/fixtures/sushi-test-wrong-format#current/other/filler.txt b/test/fhirdefs/fixtures/sushi-test-wrong-format#current/other/filler.txt
new file mode 100644
index 0000000..6c985d0
--- /dev/null
+++ b/test/fhirdefs/fixtures/sushi-test-wrong-format#current/other/filler.txt
@@ -0,0 +1,1 @@
+Hi I'm filler
\ No newline at end of file
diff --git a/test/fhirdefs/fixtures/sushi-test-wrong-format#current/package/package.json b/test/fhirdefs/fixtures/sushi-test-wrong-format#current/package/package.json
new file mode 100644
index 0000000..99f065f
--- /dev/null
+++ b/test/fhirdefs/fixtures/sushi-test-wrong-format#current/package/package.json
@@ -0,0 +1,24 @@
+{
+  "name": "sushi-test",
+  "version" : "0.1.0",
+  "canonical" : "http://hl7.org/fhir/sushi-test",
+  "url" : "http://hl7.org/fhir/sushi-test",
+  "title" : "FSH Test IG",
+  "description": "Provides a simple example of how FSH can be used to create an IG",
+  "dependencies": {
+     "hl7.fhir.r4.core": "4.0.1",
+     "hl7.fhir.us.core": "3.1.0",
+     "hl7.fhir.uv.vhdir":"current"
+  },
+  "date": "20200413230227",
+  "language": "en",
+  "author": "James Tuna",
+  "maintainers": [
+    {
+      "name": "Bill Cod",
+      "email": "cod@reef.gov",
+      "url": "https://capecodfishermen.org/"
+    }
+  ],
+  "license": "CC0-1.0"
+ }
\ No newline at end of file
diff --git a/test/fhirdefs/load.test.ts b/test/fhirdefs/load.test.ts
index 50ade5e..fe999ab 100644
--- a/test/fhirdefs/load.test.ts
+++ b/test/fhirdefs/load.test.ts
@@ -1,4 +1,9 @@
-import { loadFromPath, loadDependency, loadCustomResources } from '../../src/fhirdefs/load';
+import {
+  loadFromPath,
+  loadDependency,
+  loadCustomResources,
+  cleanCachedPackage
+} from '../../src/fhirdefs/load';
 import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions';
 import { Type } from '../../src/utils';
 import { TestFisher, loggerSpy } from '../testhelpers';
@@ -331,3 +336,43 @@ describe('#loadCustomResources', () => {
     );
   });
 });
+
+describe('#cleanCachedPackage', () => {
+  let renameSpy: jest.SpyInstance;
+  let cachePath: string;
+
+  beforeAll(() => {
+    renameSpy = jest.spyOn(fs, 'renameSync').mockImplementation(() => {});
+    cachePath = path.join(__dirname, 'fixtures');
+  });
+
+  beforeEach(() => {
+    renameSpy.mockClear();
+  });
+
+  it('should move all contents of a package into the "package" folder', () => {
+    const packagePath = path.join(cachePath, 'sushi-test-wrong-format#current');
+    cleanCachedPackage(packagePath);
+    expect(renameSpy.mock.calls.length).toBe(2);
+    expect(renameSpy.mock.calls).toContainEqual([
+      path.join(packagePath, 'other'),
+      path.join(packagePath, 'package', 'other')
+    ]);
+    expect(renameSpy.mock.calls).toContainEqual([
+      path.join(packagePath, 'StructureDefinition-MyPatient.json'),
+      path.join(packagePath, 'package', 'StructureDefinition-MyPatient.json')
+    ]);
+  });
+
+  it('should do nothing if the package does not have a "package" folder', () => {
+    const packagePath = path.join(cachePath, 'sushi-test-no-package#current');
+    cleanCachedPackage(packagePath);
+    expect(renameSpy.mock.calls.length).toBe(0);
+  });
+
+  it('should do nothing if the package is correctly structured', () => {
+    const packagePath = path.join(cachePath, 'sushi-test#current');
+    cleanCachedPackage(packagePath);
+    expect(renameSpy.mock.calls.length).toBe(0);
+  });
+});