Repository: FHIR/sushi (ts)

Issue:
SUSHI should not include instances of custom resources in the IG
The IG Publisher does not support custom resources.  SUSHI already suppresses custom resources from the generated IG JSON.  If an author creates an example of a custom resource, however, SUSHI _does_ still include that instance in the IG JSON.  This then causes problems in the IG Publisher (since it's referencing a custom resource).  SUSHI should probably suppress instances of custom resources too.

See: https://chat.fhir.org/#narrow/stream/179252-IG-creation/topic/bad.20resource.20identity/near/282797466

Example FSH from issue reported on Zulip:
```
Resource: FormTemplate
Parent: Resource
Id: formtemplate
Title: "FHIR2 Form Template"
Description: "FHIR2 Form Template"
* template 1..1 BackboneElement "The root of the doc"
  * templateStatus 1..1 string "the template status"


Instance: FHIR2FormTemplateExample
InstanceOf: FormTemplate
Title: "Example : Questionnaire"
Description: "Example : Questionnaire"
Usage: #example
* template.templateStatus = "draft"
```

Pull request:
Fixes SUSHI adding instances of custom resources in the IG + outputs warning
Fixes #1090 

The IG Publisher does not support instances of custom resources. This PR fixes that issue and issues a warning. 

To test:
Test with this FSH code and see if the FormTemplate Instance displays under `resources` in the IG. If it does, then the code is not working.

```
Resource: FormTemplate
Parent: Resource
Id: formtemplate
Title: "FHIR2 Form Template"
Description: "FHIR2 Form Template"
* template 1..1 BackboneElement "The root of the doc"
  * templateStatus 1..1 string "the template status"

Instance: FHIR2FormTemplateExample
InstanceOf: FormTemplate
Title: "Example : Questionnaire"
Description: "Example : Questionnaire"
Usage: #example
* template.templateStatus = "draft"
```
Also try to test if a normal Instance (like a Patient Instance) still displays under `resources` in IG.

Thanks for reviewing! 💯 


Patch:
diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts
index b59d496..1c9e35f 100644
--- a/src/export/InstanceExporter.ts
+++ b/src/export/InstanceExporter.ts
@@ -16,8 +16,9 @@ import {
 import { InstanceOfNotDefinedError } from '../errors/InstanceOfNotDefinedError';
 import { InstanceOfLogicalProfileError } from '../errors/InstanceOfLogicalProfileError';
 import { Package } from '.';
-import { cloneDeep, merge, uniq } from 'lodash';
+import { cloneDeep, merge, padEnd, uniq } from 'lodash';
 import { AssignmentRule } from '../fshtypes/rules';
+import chalk from 'chalk';
 
 export class InstanceExporter implements Fishable {
   constructor(
@@ -26,6 +27,38 @@ export class InstanceExporter implements Fishable {
     private readonly fisher: Fishable
   ) {}
 
+  /**
+   * Checks if there exist Instances of Custom Resources, which are not supported
+   * by the IG Publisher and are therefore not included in the IG.
+   */
+  private warnOnInstanceOfCustomResource(): void {
+    const instancesOfCustomResource = this.pkg.instances.filter(instance =>
+      this.pkg.resources.some(r => r.type === instance.resourceType)
+    );
+    if (instancesOfCustomResource.length) {
+      // logger.warn color
+      const clr = chalk.rgb(179, 98, 0);
+      const maxLength = 61;
+      const instancesOfCustomResourceLogs = instancesOfCustomResource.map(r => {
+        const printableName = r.id.length > maxLength ? r.id.slice(0, maxLength - 3) + '...' : r.id;
+        return clr('│') + padEnd(` - ${printableName}`, 65) + clr('│');
+      });
+
+      // prettier-ignore
+      const message = [
+          clr('\n╭─────────────────────────────────────────────────────────────────') + '' + clr('╮'),
+            clr('│') + ' Detected the following instance(s) of custom resources:         ' + clr('│'),
+            ...instancesOfCustomResourceLogs,
+            clr('│') + '                                                                 ' + clr('│'),
+            clr('│') + " Since non-conformant resources and their instances aren't       " + clr('│'),
+            clr('│') + ' supported by standard FHIR tooling, these instances will        ' + clr('│'),
+            clr('│') + ' not be included in the generated IG resource.                   ' + clr('│'),
+            clr('╰─────────────────────────────────────────────────────────────────') + '' + clr('╯\n')
+        ];
+      logger.warn(message.join('\n'));
+    }
+  }
+
   private setAssignedValues(
     fshInstanceDef: Instance,
     instanceDef: InstanceDefinition,
@@ -543,6 +576,7 @@ export class InstanceExporter implements Fishable {
         logger.error(e.message, e.sourceInfo);
       }
     }
+    this.warnOnInstanceOfCustomResource();
     if (instances.length > 0) {
       logger.info(`Converted ${instances.length} FHIR instances.`);
     }
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index 93c76a1..3c297fa 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -782,6 +782,11 @@ export class IGExporter {
     );
     instances
       .filter(instance => instance._instanceMeta.usage !== 'Inline')
+      .filter(
+        instance =>
+          // Filter out instances that have a type that has the same type as a custom resource defined in the package
+          !this.pkg.resources.some(r => r.type === instance.resourceType)
+      )
       .forEach(instance => {
         const referenceKey = `${instance.resourceType}/${
           instance.id ?? instance._instanceMeta.name


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 8c1d7ac..eea483b 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -10,7 +10,8 @@ import {
   Extension,
   FshCodeSystem,
   RuleSet,
-  FshQuantity
+  FshQuantity,
+  Resource
 } from '../../src/fshtypes';
 import {
   AssignmentRule,
@@ -4902,6 +4903,41 @@ describe('InstanceExporter', () => {
       expect(exported.length).toBe(1);
       expect(loggerSpy.getLastMessage('error')).toMatch(/File: Unmeasurable\.fsh.*Line: 3\D*/s);
     });
+
+    it('should log a warning when exporting an instance of a custom resource', () => {
+      const resource = new Resource('Foo');
+      doc.resources.set(resource.name, resource);
+      const instance = new Instance('FooInstance');
+      instance.instanceOf = 'Foo';
+      doc.instances.set(instance.name, instance);
+      sdExporter.export();
+      const exported = exporter.export().instances;
+      expect(exported.length).toBe(1);
+      expect(loggerSpy.getAllMessages('warn').length).toBe(2);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /following instance\(s\) of custom resources:.*- FooInstance/s
+      );
+    });
+
+    it('should log a warning when exporting multiple instances of custom resources', () => {
+      const resource1 = new Resource('Foo');
+      const resource2 = new Resource('Bar');
+      doc.resources.set(resource1.name, resource1);
+      doc.resources.set(resource2.name, resource2);
+      const instance1 = new Instance('FooInstance');
+      instance1.instanceOf = 'Foo';
+      doc.instances.set(instance1.name, instance1);
+      const instance2 = new Instance('BarInstance');
+      instance2.instanceOf = 'Bar';
+      doc.instances.set(instance2.name, instance2);
+      sdExporter.export();
+      const exported = exporter.export().instances;
+      expect(exported.length).toBe(2);
+      expect(loggerSpy.getAllMessages('warn').length).toBe(2);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /following instance\(s\) of custom resources:.*- FooInstance.*- BarInstance/s
+      );
+    });
   });
 
   describe('#insertRules', () => {
diff --git a/test/ig/IGExporter.IG.test.ts b/test/ig/IGExporter.IG.test.ts
index 4b0240c..7db36f7 100644
--- a/test/ig/IGExporter.IG.test.ts
+++ b/test/ig/IGExporter.IG.test.ts
@@ -804,11 +804,26 @@ describe('IGExporter', () => {
         ],
         history: {} // to suppress warning for HL7 IGs
       };
+      const customResourceInstance = new InstanceDefinition();
+      customResourceInstance.resourceType = 'CustomResource';
+      customResourceInstance.id = 'FooResource';
+      customResourceInstance._instanceMeta.description = 'CustomResource Description';
+      customResourceInstance._instanceMeta.name = 'CustomResource name';
+      customResourceInstance._instanceMeta.usage = 'Example';
+
+      const patientInstance = new InstanceDefinition();
+      patientInstance.resourceType = 'Patient';
+      patientInstance.id = 'FooPatient';
+      patientInstance._instanceMeta.description = 'Sample description';
+      patientInstance._instanceMeta.name = 'Sample name';
+      patientInstance._instanceMeta.usage = 'Example';
+
       pkg = new Package(config);
       pkg.profiles.push(...pkgProfiles);
       pkg.extensions.push(...pkgExtensions);
       pkg.logicals.push(...pkgLogicals);
       pkg.resources.push(...pkgResources);
+      pkg.instances.push(customResourceInstance, patientInstance);
       exporter = new IGExporter(pkg, defs, fixtures);
     });
 
@@ -816,7 +831,7 @@ describe('IGExporter', () => {
       temp.cleanupSync();
     });
 
-    it('should generate an implementation guide for simple-ig with package containing logical model and custom resource', () => {
+    it('should generate an implementation guide for simple-ig with package containing logical model and custom resource and also ignore custom resource and its instance(s)', () => {
       exporter.export(tempOut);
       const igPath = path.join(
         tempOut,
@@ -913,7 +928,16 @@ describe('IGExporter', () => {
               description:
                 'This is an example of a custom logical model defined using FSH with parent of Element',
               exampleBoolean: false
+            },
+            {
+              reference: {
+                reference: 'Patient/FooPatient'
+              },
+              name: 'Sample name',
+              description: 'Sample description',
+              exampleBoolean: true
             }
+            // CustomResource and CustomResourceInstance are excluded because they are Custom Resources
           ],
           page: {
             nameUrl: 'toc.html',
@@ -1687,7 +1711,6 @@ describe('IGExporter', () => {
       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(