Repository: FHIR/sushi (ts)

Issue:
ImplementationGuide.definition.resource.name not populated correctly for SUSHI generated ActorDefinition, Requirements, TestPlan and TestScript resource types
The FHIR IG Publisher and HL7 IG Template have been updated to include a new "Testing" tab page on all canonical resource types when TestPlan and/or TestScript resources are linked or reference those canonical resources. When reviewing the rendered "Testing" tab page, the listings of TestPlan and TestScript resource links are displaying the TestPlan.name and TestScript.name values instead of the expected TestPlan.title and TestScript.title values. However, this is only happening when the TestPlan and TestScript resource instances are generated by SUSHI from FSH definitions.

I have reviewed the SUSHI source code and found where this occurs - the **IGExporter.ts** **_addResourcePackage(referenceKey, pkgResource, configResource)_** method that processes these SUSHI generated resources is casting the ActorDefinition, Requirements, TestPlan and TestScript resource types as an **InstanceDefinition** class where the title attribute is not expected to exist. Since the ActorDefinition, Requirements, TestPlan and TestScript resource types implement the CanonicalResource interface, they do have the title attribute defined which needs to be used to populate the corresponding **ImplementationGuide.definition.resource.name** value.

_FYI - The **ImplementationGuide.definition.resource.name** value is used for the link name(s) in the IG pages._

The current logic is:

```
  private addPackageResource(
    referenceKey: string,
    pkgResource: StructureDefinition | ValueSet | CodeSystem | InstanceDefinition,
    configResource?: ConfigurationResource
  ): void {
    const newResource: ImplementationGuideDefinitionResource = {
      reference: { reference: referenceKey }
    };
    if (pkgResource instanceof InstanceDefinition) {
      newResource.name =
        configResource?.name ?? pkgResource._instanceMeta.title ?? pkgResource._instanceMeta.name;
      ...
```

A simple update to this logic to test for the existence of the title attribute will correct this issue as follows:

```
  private addPackageResource(
    referenceKey: string,
    pkgResource: StructureDefinition | ValueSet | CodeSystem | InstanceDefinition,
    configResource?: ConfigurationResource
  ): void {
    const newResource: ImplementationGuideDefinitionResource = {
      reference: { reference: referenceKey }
    };
    if (pkgResource instanceof InstanceDefinition) {
      const title = typeof pkgResource.title === 'string' ? pkgResource.title : null;
      if (title) {
        newResource.name = title;
      } else {
        newResource.name =
          configResource?.name ?? pkgResource._instanceMeta.title ?? pkgResource._instanceMeta.name;
      }
```


Pull request:
Use title and name values in IG resource list
Fixes #1296 and completes task [CIMPL-1138](https://standardhealthrecord.atlassian.net/browse/CIMPL-1138).

Some resource types have title and name elements that are strings or similar datatypes. If present, these can be used to set the name in the IG resource list.

For package resources, only the title element value is used for this purpose. A title set with a keyword will have higher precedence than a title set by assigning to an element.

For predefined resources, the name element can be used for this purpose as well. The title element has a higher precedence than the name element. Note that the name element will only be used for predefined resources, not package resources. This change applies to both example and non-example predefined resources.

As expected, this change affects many IGs using FSH. It can change the order of resources in the IG if they are being sorted by name, which also changes the order of generated link references. The link references themselves are unchanged. It will also change the titles shown on pages in the IG, since many artifacts will now have a name based on something other than their `id`.

Patch:
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index 729783f..93fe039 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -28,7 +28,7 @@ import {
 } from '../fhirtypes';
 import { CONFORMANCE_AND_TERMINOLOGY_RESOURCES } from '../fhirtypes/common';
 import { ConfigurationMenuItem, ConfigurationResource } from '../fshtypes';
-import { logger, Type, getFilesRecursive } from '../utils';
+import { logger, Type, getFilesRecursive, stringOrElse } from '../utils';
 import { FHIRDefinitions } from '../fhirdefs';
 import { Configuration } from '../fshtypes';
 import { parseCodeLexeme } from '../import';
@@ -837,9 +837,14 @@ export class IGExporter {
     };
     if (pkgResource instanceof InstanceDefinition) {
       newResource.name =
-        configResource?.name ?? pkgResource._instanceMeta.title ?? pkgResource._instanceMeta.name;
+        configResource?.name ??
+        pkgResource._instanceMeta.title ??
+        stringOrElse(pkgResource.title) ??
+        pkgResource._instanceMeta.name;
       newResource.description =
-        configResource?.description ?? pkgResource._instanceMeta.description;
+        configResource?.description ??
+        pkgResource._instanceMeta.description ??
+        stringOrElse(pkgResource.description);
       newResource._linkRef = pkgResource.id;
     } else {
       newResource.name =
@@ -1002,14 +1007,12 @@ export class IGExporter {
 
               const metaExtensionDescription = this.getMetaExtensionDescription(resourceJSON);
               const metaExtensionName = this.getMetaExtensionName(resourceJSON);
-
+              // On some resources (Patient for example) title, name, and description can be objects, avoid using them when this is true
               newResource.description =
                 configResource?.description ??
                 metaExtensionDescription ??
                 existingDescription ??
-                (CONFORMANCE_AND_TERMINOLOGY_RESOURCES.has(resourceJSON.resourceType)
-                  ? resourceJSON.description
-                  : undefined);
+                stringOrElse(resourceJSON.description);
               if (configResource?.fhirVersion) {
                 newResource.fhirVersion = configResource.fhirVersion;
               }
@@ -1019,7 +1022,12 @@ export class IGExporter {
               }
               if (path.basename(dirPath) === 'examples') {
                 newResource.name =
-                  configResource?.name ?? metaExtensionName ?? existingName ?? resourceJSON.id;
+                  configResource?.name ??
+                  metaExtensionName ??
+                  existingName ??
+                  stringOrElse(resourceJSON.title) ??
+                  stringOrElse(resourceJSON.name) ??
+                  resourceJSON.id;
                 newResource._linkRef = resourceJSON.id;
                 // set exampleCanonical or exampleBoolean, preferring configured values
                 if (configResource?.exampleCanonical) {
@@ -1051,17 +1059,14 @@ export class IGExporter {
                 } else {
                   newResource.exampleBoolean = false;
                 }
-                // On some resources (Patient for example) these fields can be objects, avoid using them when this is true
-                const title = typeof resourceJSON.title === 'string' ? resourceJSON.title : null;
-                const name = typeof resourceJSON.name === 'string' ? resourceJSON.name : null;
                 newResource.name =
                   configResource?.name ??
                   metaExtensionName ??
                   existingResource?.name ??
-                  title ??
-                  name ??
+                  stringOrElse(resourceJSON.title) ??
+                  stringOrElse(resourceJSON.name) ??
                   resourceJSON.id;
-                newResource._linkRef = name ?? resourceJSON.id;
+                newResource._linkRef = stringOrElse(resourceJSON.name) ?? resourceJSON.id;
               }
               if (configResource?.extension?.length) {
                 newResource.extension = configResource.extension;
diff --git a/src/utils/TypeUtils.ts b/src/utils/TypeUtils.ts
new file mode 100644
index 0000000..ea16b96
--- /dev/null
+++ b/src/utils/TypeUtils.ts
@@ -0,0 +1,3 @@
+export function stringOrElse(value: any): string | undefined {
+  return typeof value === 'string' ? value : undefined;
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 524cfb1..a3e6374 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -6,3 +6,4 @@ export * from './Mixin';
 export * from './Processing';
 export * from './PathUtils';
 export * from './puns';
+export * from './TypeUtils';


Test patch:
diff --git a/test/ig/IGExporter.IG.test.ts b/test/ig/IGExporter.IG.test.ts
index 2c698a3..2c6ab9a 100644
--- a/test/ig/IGExporter.IG.test.ts
+++ b/test/ig/IGExporter.IG.test.ts
@@ -253,6 +253,7 @@ describe('IGExporter', () => {
                 reference: 'CapabilityStatement/capability-statement-example'
               },
               name: 'capability-statement-example',
+              description: 'Test description',
               exampleBoolean: false // Not 'Example' Usages will set this to false
             },
             {
@@ -614,6 +615,7 @@ describe('IGExporter', () => {
             reference: 'CapabilityStatement/capability-statement-example'
           },
           name: 'capability-statement-example',
+          description: 'Test description',
           exampleBoolean: false // Not 'Example' Usages will set this to false
         },
         {
@@ -890,6 +892,7 @@ describe('IGExporter', () => {
             reference: 'CapabilityStatement/capability-statement-example'
           },
           name: 'capability-statement-example',
+          description: 'Test description',
           exampleBoolean: false, // Not 'Example' Usages will set this to false
           groupingId: 'CatawumpusGroup'
         },
@@ -1823,6 +1826,7 @@ describe('IGExporter', () => {
     let fixtures: string;
     let config: Configuration;
     let defs: FHIRDefinitions;
+    let testScriptInstance: InstanceDefinition;
 
     beforeAll(() => {
       defs = new FHIRDefinitions();
@@ -1863,6 +1867,13 @@ describe('IGExporter', () => {
       patientInstance._instanceMeta.usage = 'Example';
       pkg.instances.push(patientInstance);
 
+      testScriptInstance = new InstanceDefinition();
+      testScriptInstance.resourceType = 'TestScript';
+      testScriptInstance.id = 'AnotherTestScript';
+      testScriptInstance._instanceMeta.name = 'AnotherTestScript';
+      testScriptInstance._instanceMeta.usage = 'Example';
+      pkg.instances.push(testScriptInstance);
+
       exporter = new IGExporter(pkg, defs, fixtures);
     });
 
@@ -1915,6 +1926,13 @@ describe('IGExporter', () => {
         name: 'Populate Questionnaire', // Use name over ID
         exampleBoolean: false
       });
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'OperationDefinition/AnotherOD'
+        },
+        name: 'Title for Populate Another Questionnaire', // Use title over name
+        exampleBoolean: false
+      });
       expect(igContent.definition.resource).toContainEqual({
         reference: {
           reference: 'Patient/BazPatient'
@@ -2013,7 +2031,7 @@ describe('IGExporter', () => {
           reference: 'Goal/GoalWithDescription'
         },
         name: 'GoalWithDescription',
-        // NOTE: no description since Goal is not a conformance resource (and Goal.description is a CodeableConcept)
+        // NOTE: no description since Goal.description is a CodeableConcept
         exampleBoolean: true
       });
     });
@@ -2051,6 +2069,64 @@ describe('IGExporter', () => {
         name: 'GoalWithDescription',
         exampleBoolean: true
       });
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'TestScript/MyTestScript'
+        },
+        name: 'Test script title',
+        description: 'This is the description of my nice test script.',
+        exampleBoolean: true
+      });
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'TestScript/AnotherTestScript'
+        },
+        name: 'AnotherTestScript',
+        exampleBoolean: true
+      });
+    });
+
+    it('should add an example reference with a title attribute to the ImplementationGuide resource', () => {
+      testScriptInstance.title = 'Another good title';
+
+      exporter.export(tempOut);
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent: ImplementationGuide = fs.readJSONSync(igPath);
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'TestScript/AnotherTestScript'
+        },
+        name: 'Another good title',
+        exampleBoolean: true
+      });
+    });
+
+    it('should add an example reference with a description attribute to the ImplementationGuide resource', () => {
+      testScriptInstance.description = 'This is the description for another test script.';
+
+      exporter.export(tempOut);
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent: ImplementationGuide = fs.readJSONSync(igPath);
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'TestScript/AnotherTestScript'
+        },
+        name: 'AnotherTestScript',
+        description: 'This is the description for another test script.',
+        exampleBoolean: true
+      });
     });
 
     it('should add resource references with a title to the ImplementationGuide resource', () => {
@@ -2218,6 +2294,9 @@ describe('IGExporter', () => {
         {
           reference: { reference: 'OperationDefinition/MyOD' }
         },
+        {
+          reference: { reference: 'OperationDefinition/AnotherOD' }
+        },
         {
           reference: { reference: 'StructureDefinition/MyTitlePatient' }
         },
@@ -2236,6 +2315,12 @@ describe('IGExporter', () => {
         {
           reference: { reference: 'Patient/BarPatient' }
         },
+        {
+          reference: { reference: 'TestScript/MyTestScript' }
+        },
+        {
+          reference: { reference: 'TestScript/AnotherTestScript' }
+        },
         {
           reference: { reference: 'Goal/GoalWithDescription' }
         }
@@ -2301,6 +2386,13 @@ describe('IGExporter', () => {
           name: 'Populate Questionnaire',
           exampleBoolean: false
         },
+        {
+          reference: {
+            reference: 'OperationDefinition/AnotherOD'
+          },
+          name: 'Title for Populate Another Questionnaire',
+          exampleBoolean: false
+        },
         {
           reference: {
             reference: 'StructureDefinition/MyTitlePatient'
@@ -2345,6 +2437,21 @@ describe('IGExporter', () => {
           name: 'BarPatient',
           exampleCanonical: 'http://hl7.org/fhir/sushi-test/StructureDefinition/MyPatient'
         },
+        {
+          reference: {
+            reference: 'TestScript/MyTestScript'
+          },
+          name: 'Test script title',
+          description: 'This is the description of my nice test script.',
+          exampleBoolean: true
+        },
+        {
+          reference: {
+            reference: 'TestScript/AnotherTestScript'
+          },
+          name: 'AnotherTestScript',
+          exampleBoolean: true
+        },
         {
           reference: {
             reference: 'Goal/GoalWithDescription'
@@ -2364,6 +2471,7 @@ describe('IGExporter', () => {
           resources: [
             'CapabilityStatement/MyCS',
             'OperationDefinition/MyOD',
+            'OperationDefinition/AnotherOD',
             'Goal/GoalWithDescription'
           ]
         },
@@ -2387,7 +2495,12 @@ describe('IGExporter', () => {
           id: 'OtherThings',
           name: 'Other Things',
           description: 'This group has other things',
-          resources: ['StructureDefinition/MyLM', 'ValueSet/MyVS']
+          resources: [
+            'StructureDefinition/MyLM',
+            'ValueSet/MyVS',
+            'TestScript/MyTestScript',
+            'TestScript/AnotherTestScript'
+          ]
         }
       ];
       exporter.export(tempOut);
@@ -2417,6 +2530,14 @@ describe('IGExporter', () => {
           exampleBoolean: false,
           groupingId: 'FirstGroup'
         },
+        {
+          reference: {
+            reference: 'OperationDefinition/AnotherOD'
+          },
+          name: 'Title for Populate Another Questionnaire',
+          exampleBoolean: false,
+          groupingId: 'FirstGroup'
+        },
         {
           reference: {
             reference: 'Goal/GoalWithDescription'
@@ -2521,6 +2642,23 @@ describe('IGExporter', () => {
           name: "Yes/No/Don't Know",
           exampleBoolean: false,
           groupingId: 'OtherThings'
+        },
+        {
+          reference: {
+            reference: 'TestScript/MyTestScript'
+          },
+          name: 'Test script title',
+          description: 'This is the description of my nice test script.',
+          exampleBoolean: true,
+          groupingId: 'OtherThings'
+        },
+        {
+          reference: {
+            reference: 'TestScript/AnotherTestScript'
+          },
+          name: 'AnotherTestScript',
+          exampleBoolean: true,
+          groupingId: 'OtherThings'
         }
       ]);
     });
@@ -2544,6 +2682,13 @@ describe('IGExporter', () => {
       expect(fs.existsSync(igPath)).toBeTruthy();
       const igContent: ImplementationGuide = fs.readJSONSync(igPath);
       expect(igContent.definition?.resource).toEqual([
+        {
+          reference: {
+            reference: 'TestScript/AnotherTestScript'
+          },
+          name: 'AnotherTestScript',
+          exampleBoolean: true
+        },
         {
           reference: {
             reference: 'Patient/BarPatient'
@@ -2644,6 +2789,14 @@ describe('IGExporter', () => {
           name: 'StayName',
           exampleBoolean: true
         },
+        {
+          reference: {
+            reference: 'TestScript/MyTestScript'
+          },
+          name: 'Test script title',
+          description: 'This is the description of my nice test script.',
+          exampleBoolean: true
+        },
         {
           reference: {
             reference: 'StructureDefinition/MyTitlePatient'
@@ -2651,6 +2804,13 @@ describe('IGExporter', () => {
           name: 'This patient has a title',
           exampleBoolean: false
         },
+        {
+          reference: {
+            reference: 'OperationDefinition/AnotherOD'
+          },
+          name: 'Title for Populate Another Questionnaire',
+          exampleBoolean: false
+        },
         {
           reference: {
             reference: 'ValueSet/MyVS'
diff --git a/test/ig/fixtures/customized-ig-with-resources/input/examples/TestScript-MyTestScript.json b/test/ig/fixtures/customized-ig-with-resources/input/examples/TestScript-MyTestScript.json
new file mode 100644
index 0000000..2b954bc
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/input/examples/TestScript-MyTestScript.json
@@ -0,0 +1,9 @@
+{
+  "resourceType": "TestScript",
+  "id": "MyTestScript",
+  "title": "Test script title",
+  "status": "draft",
+  "url": "http://example.org/MyTestScript",
+  "name": "My nice test script",
+  "description": "This is the description of my nice test script."
+}
diff --git a/test/ig/fixtures/customized-ig-with-resources/input/operations/OperationDefinition-AnotherOD.json b/test/ig/fixtures/customized-ig-with-resources/input/operations/OperationDefinition-AnotherOD.json
new file mode 100644
index 0000000..3f6f026
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/input/operations/OperationDefinition-AnotherOD.json
@@ -0,0 +1,98 @@
+{
+  "resourceType": "OperationDefinition",
+  "id": "AnotherOD",
+  "text": {
+    "status": "generated",
+    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative with Details</b></p><p><b>id</b>: example</p><p><b>url</b>: <b>http://h7.org/fhir/OperationDefinition/example</b></p><p><b>version</b>: B</p><p><b>name</b>: Populate Questionnaire</p><p><b>status</b>: draft</p><p><b>kind</b>: operation</p><p><b>date</b>: 04/08/2015</p><p><b>publisher</b>: Acme Healthcare Services</p><p><b>contact</b>: </p><p><b>description</b>: Limited implementation of the Populate Questionnaire implementation</p><p><b>useContext</b>: </p><p><b>jurisdiction</b>: United Kingdom of Great Britain and Northern Ireland (the) <span>(Details : {urn:iso:std:iso:3166 code 'GB' = 'United Kingdom of Great Britain and Northern Ireland', given as 'United Kingdom of Great Britain and Northern Ireland (the)'})</span></p><p><b>code</b>: populate</p><p><b>comment</b>: Only implemented for Labs and Medications so far</p><p><b>base</b>: <a>OperationDefinition/Questionnaire-populate</a></p><p><b>resource</b>: Questionnaire</p><p><b>system</b>: false</p><p><b>type</b>: false</p><p><b>instance</b>: true</p><blockquote><p><b>parameter</b></p><p><b>name</b>: subject</p><p><b>use</b>: in</p><p><b>min</b>: 1</p><p><b>max</b>: 1</p><p><b>documentation</b>: The resource that is to be the *QuestionnaireResponse.subject*. The [[[QuestionnaireResponse]]]      instance will reference the provided subject.  In addition, if the *local* parameter is      set to true, server information about the specified subject will be used to populate the      instance.</p><p><b>type</b>: Reference</p></blockquote><blockquote><p><b>parameter</b></p><p><b>name</b>: local</p><p><b>use</b>: in</p><p><b>min</b>: 0</p><p><b>max</b>: 1</p><p><b>documentation</b>: If the *local* parameter is set to true, server information about the specified subject will be used to populate the instance.</p><p><b>type</b>: Reference</p></blockquote><blockquote><p><b>parameter</b></p><p><b>name</b>: return</p><p><b>use</b>: out</p><p><b>min</b>: 1</p><p><b>max</b>: 1</p><p><b>documentation</b>: The partially (or fully)-populated set of answers for the specified Questionnaire</p><p><b>type</b>: QuestionnaireResponse</p></blockquote><blockquote><p><b>overload</b></p><p><b>parameterName</b>: subject, local</p></blockquote><blockquote><p><b>overload</b></p><p><b>parameterName</b>: subject</p><p><b>comment</b>: local defaults to false when not passed as a parameter</p></blockquote></div>"
+  },
+  "url": "http://h7.org/fhir/OperationDefinition/AnotherOD",
+  "version": "B",
+  "name": "Name of Populate Another Questionnaire",
+  "title": "Title for Populate Another Questionnaire",
+  "status": "draft",
+  "kind": "operation",
+  "date": "2015-08-04",
+  "publisher": "Acme Healthcare Services",
+  "contact": [
+    {
+      "name": "System Administrator",
+      "telecom": [
+        {
+          "system": "email",
+          "value": "beep@coyote.acme.com"
+        }
+      ]
+    }
+  ],
+  "useContext": [
+    {
+      "code": {
+        "system": "http://build.fhir.org/codesystem-usage-context-type",
+        "code": "venue",
+        "display": "Clinical Venue"
+      },
+      "valueCodeableConcept": {
+        "coding": [
+          {
+            "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
+            "code": "IMP",
+            "display": "inpatient encounter"
+          }
+        ]
+      }
+    }
+  ],
+  "jurisdiction": [
+    {
+      "coding": [
+        {
+          "system": "urn:iso:std:iso:3166",
+          "code": "GB",
+          "display": "United Kingdom of Great Britain and Northern Ireland (the)"
+        }
+      ]
+    }
+  ],
+  "code": "populate",
+  "comment": "Only implemented for Labs and Medications so far",
+  "base": "OperationDefinition/Questionnaire-populate",
+  "resource": ["Questionnaire"],
+  "system": false,
+  "type": false,
+  "instance": true,
+  "parameter": [
+    {
+      "name": "subject",
+      "use": "in",
+      "min": 1,
+      "max": "1",
+      "documentation": "The resource that is to be the *QuestionnaireResponse.subject*. The [[[QuestionnaireResponse]]]      instance will reference the provided subject.  In addition, if the *local* parameter is      set to true, server information about the specified subject will be used to populate the      instance.",
+      "type": "Reference"
+    },
+    {
+      "name": "local",
+      "use": "in",
+      "min": 0,
+      "max": "1",
+      "documentation": "If the *local* parameter is set to true, server information about the specified subject will be used to populate the instance.",
+      "type": "Reference"
+    },
+    {
+      "name": "return",
+      "use": "out",
+      "min": 1,
+      "max": "1",
+      "documentation": "The partially (or fully)-populated set of answers for the specified Questionnaire",
+      "type": "QuestionnaireResponse"
+    }
+  ],
+  "overload": [
+    {
+      "parameterName": ["subject", "local"]
+    },
+    {
+      "parameterName": ["subject"],
+      "comment": "local defaults to false when not passed as a parameter"
+    }
+  ]
+}