Repository: FHIR/sushi (ts)

Issue:
SUSHI should support nested /input/resources folders when added to path-resource config
SUSHI 2.3.0 added support for using the `path-resource` parameter to specify additional paths that can contain resources.  While this works w/ most paths, it does not work for paths that are nested subfolders under existing supported folders (like `input/resources/nested`).  If you try to do that, SUSHI emits a warning saying it will not add the resource to the IG JSON.

If the user has specifically added the folder via the `path-resource` parameter, then SUSHI should not warn on this case.

See: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/nested.20folders.20in.20.2Finput.2Fresources.2Fabc/near/274334692

Pull request description:
path-resource parameter support fully extended to ig exporter
Fixes #1037, ensuring that predefined resources imported using the `path-resource` parameter in the IG are present in the `definition.resource` section of the exported IG. Also, warnings on deeply nested predefined resources are no longer logged when they're covered by the `path-resource` parameter.

Patch:
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index 62c3305..c43a665 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -825,13 +825,28 @@ export class IGExporter {
       'vocabulary',
       'examples'
     ];
+    const predefinedResourcePaths = pathEnds.map(pathEnd =>
+      path.join(this.inputPath, 'input', pathEnd)
+    );
+    let pathResourceDirectories: string[];
+    const pathResources = this.config.parameters
+      ?.filter(parameter => parameter.value && parameter.code === 'path-resource')
+      .map(parameter => parameter.value);
+    if (pathResources) {
+      pathResourceDirectories = pathResources
+        .map(directoryPath => path.join(this.inputPath, directoryPath))
+        .filter(directoryPath => existsSync(directoryPath));
+      if (pathResourceDirectories) predefinedResourcePaths.push(...pathResourceDirectories);
+    }
     const deeplyNestedFiles: string[] = [];
-    for (const pathEnd of pathEnds) {
-      const dirPath = path.resolve(this.inputPath, 'input', pathEnd);
+    for (const dirPath of predefinedResourcePaths) {
       if (existsSync(dirPath)) {
         const files = getFilesRecursive(dirPath);
         for (const file of files) {
-          if (path.dirname(file) !== dirPath) {
+          if (
+            path.dirname(file) !== dirPath &&
+            !pathResourceDirectories?.includes(path.dirname(file))
+          ) {
             deeplyNestedFiles.push(file);
             continue;
           }
@@ -899,7 +914,7 @@ export class IGExporter {
                 newResource.groupingId = configResource.groupingId;
                 this.addGroup(newResource.groupingId);
               }
-              if (pathEnd === 'examples') {
+              if (path.basename(dirPath) === 'examples') {
                 newResource.name =
                   configResource?.name ?? metaExtensionName ?? existingName ?? resourceJSON.id;
                 // set exampleCanonical or exampleBoolean, preferring configured values


Test patch:
diff --git a/test/ig/IGExporter.IG.test.ts b/test/ig/IGExporter.IG.test.ts
index f604d9d..337c500 100644
--- a/test/ig/IGExporter.IG.test.ts
+++ b/test/ig/IGExporter.IG.test.ts
@@ -1600,6 +1600,7 @@ describe('IGExporter', () => {
     let tempOut: string;
     let fixtures: string;
     let defs: FHIRDefinitions;
+    let config: Configuration;
 
     beforeAll(() => {
       defs = new FHIRDefinitions();
@@ -1611,7 +1612,12 @@ describe('IGExporter', () => {
     beforeEach(() => {
       loggerSpy.reset();
       tempOut = temp.mkdirSync('sushi-test');
-      const config = cloneDeep(minimalConfig);
+      config = cloneDeep(minimalConfig);
+      config.parameters = [];
+      config.parameters.push({
+        code: 'path-resource',
+        value: path.join('input', 'resources', 'path-resource-nest')
+      });
       const pkg = new Package(config);
       exporter = new IGExporter(pkg, defs, path.resolve(fixtures));
     });
@@ -1630,7 +1636,7 @@ describe('IGExporter', () => {
       );
       expect(fs.existsSync(igPath)).toBeTruthy();
       const igContent: ImplementationGuide = fs.readJSONSync(igPath);
-      expect(igContent.definition.resource).toHaveLength(2);
+      expect(igContent.definition.resource).toHaveLength(3);
       expect(igContent.definition.resource).toContainEqual({
         reference: {
           reference: 'Patient/BarPatient'
@@ -1645,6 +1651,13 @@ describe('IGExporter', () => {
         name: 'MyPatient',
         exampleBoolean: false
       });
+      expect(igContent.definition.resource).toContainEqual({
+        exampleBoolean: false,
+        name: 'MyCorrectlyNestedPatient',
+        reference: {
+          reference: 'StructureDefinition/MyCorrectlyNestedPatient'
+        }
+      });
     });
 
     it('should warn on deeply nested resources', () => {
@@ -1658,6 +1671,24 @@ describe('IGExporter', () => {
       expect(warning).not.toInclude('Patient-BarPatient.json');
       expect(warning).not.toInclude('StructureDefinition-MyPatient.json');
     });
+
+    it('should not warn on deeply nested resources when implicated by the path-resource parameter', () => {
+      defs = new FHIRDefinitions();
+      loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs);
+      fixtures = path.join(__dirname, 'fixtures', 'customized-ig-with-nested-resources');
+      loadCustomResources(path.join(fixtures, 'input'), fixtures, config.parameters, defs);
+
+      exporter.export(tempOut);
+      const warning = loggerSpy.getFirstMessage('warn');
+      expect(warning).toInclude(
+        'The following files were not added to the ImplementationGuide JSON'
+      );
+      expect(warning).toInclude(path.join('nested1', 'StructureDefinition-MyTitlePatient.json'));
+      expect(warning).toInclude(path.join('nested2', 'ValueSet-MyVS.json'));
+      expect(warning).not.toInclude(
+        path.join('path-resource-nest', 'StructureDefinition-MyCorrectlyNestedPatient.json')
+      );
+    });
   });
 
   describe('#customized-ig-with-logical-model-example', () => {
diff --git a/test/ig/fixtures/customized-ig-with-nested-resources/input/resources/path-resource-nest/StructureDefinition-MyCorrectlyNestedPatient.json b/test/ig/fixtures/customized-ig-with-nested-resources/input/resources/path-resource-nest/StructureDefinition-MyCorrectlyNestedPatient.json
new file mode 100644
index 0000000..e803ac2
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-nested-resources/input/resources/path-resource-nest/StructureDefinition-MyCorrectlyNestedPatient.json
@@ -0,0 +1,25 @@
+{
+  "resourceType": "StructureDefinition",
+  "id": "MyCorrectlyNestedPatient",
+  "url": "http://hl7.org/fhir/sushi-test/StructureDefinition/MyCorrectlyNestedPatient",
+  "name": "MyCorrectlyNestedPatient",
+  "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"
+          }
+        ]
+      }
+    ]
+  }
+}
\ No newline at end of file