Repository: FHIR/sushi
Pull request #1421 (2024-01-29 21:31:55)

Use current extension for specifying IG resource format
According to Zulip [here](https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/How.20do.20I.20get.20SUSHI.20to.20ignore.20a.20binary.20JSON.20logical.20instance.3F/near/418612384), the extension URL we had been using to configure logical model examples has been deprecated. The new extension for the implementationguide-resource-format extension is `http://hl7.org/fhir/tools/StructureDefinition/implementationguide-resource-format`.

This PR updates SUSHI to automatically add the correct extension when creating the `resource` entry of an `Instance` of a `Logical` for the IG JSON. It also adds a warning when a provided configuration uses the deprecated extension URL to help users update to the current URL.

I ran a full regression and no one seems to actually be using this old URL because I didn't see any new warnings logged.

Note that there is already a PR on FSH School to update the extension we document there.

jafeltra: > In reviewing this, I discovered a bug in our general implementation of this for bring-your-own-JSON logical instances. In short, it only works if the resourceType in the JSON is the id of the logical model -- but if the resourceType is the canonical URL of the logical model, it doesn't work and ends up creating two entries for the instance in the IG JSON (and also doesn't flag the deprecated URL). I think that using the canonical URL is actually the right thing, so it's unfortunate we don't handle it correctly. (Note that when we export FSH Instances of logical models, we use the canonical URL).

I had noticed this recently too, so since you ran into it as well, I think it is worth fixing sooner rather than later. I think I handled it correctly, and it wasn't a huge change, so I included it in [c0e6ead](https://github.com/FHIR/sushi/pull/1421/commits/c0e6ead15624ed80b8891ff29a8a7d247f211180). I think it fixes all the cases in the example you provided, but let me know if I missed anything.

Patch:
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index e879258..a0a46a0 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -33,6 +33,16 @@ import { FHIRDefinitions } from '../fhirdefs';
 import { Configuration } from '../fshtypes';
 import { parseCodeLexeme } from '../import';
 
+// Deprecated but still supported in IG Publisher, so we'll support it too.
+const DEPRECATED_RESOURCE_FORMAT_EXTENSION =
+  'http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format';
+const CURRENT_RESOURCE_FORMAT_EXTENSION =
+  'http://hl7.org/fhir/tools/StructureDefinition/implementationguide-resource-format';
+const IG_RESOURCE_FORMAT_EXTENSIONS = [
+  DEPRECATED_RESOURCE_FORMAT_EXTENSION,
+  CURRENT_RESOURCE_FORMAT_EXTENSION
+];
+
 function isR4(fhirVersion: string[]) {
   return fhirVersion.some(v => /^R4B?$/.test(getFHIRVersionInfo(v).name));
 }
@@ -897,18 +907,24 @@ export class IGExporter {
     }
     if (
       pkgResource instanceof InstanceDefinition &&
-      pkgResource._instanceMeta.sdKind === 'logical' &&
-      !configResource?.extension?.some(
-        ext =>
-          ext.url === 'http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format'
-      )
+      pkgResource._instanceMeta.sdKind === 'logical'
     ) {
-      // Logical instances should add a special extension. See: https://fshschool.org/docs/sushi/tips/#instances-of-logical-models
-      newResource.extension = newResource.extension ?? [];
-      newResource.extension.push({
-        url: 'http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format',
-        valueCode: 'application/fhir+json'
-      });
+      if (
+        !configResource?.extension?.some(ext => IG_RESOURCE_FORMAT_EXTENSIONS.includes(ext.url))
+      ) {
+        // Logical instances should add a special extension. See: https://fshschool.org/docs/sushi/tips/#instances-of-logical-models
+        newResource.extension = newResource.extension ?? [];
+        newResource.extension.push({
+          url: CURRENT_RESOURCE_FORMAT_EXTENSION,
+          valueCode: 'application/fhir+json'
+        });
+      } else if (
+        configResource?.extension?.some(ext => ext.url === DEPRECATED_RESOURCE_FORMAT_EXTENSION)
+      ) {
+        logger.warn(
+          `The extension ${DEPRECATED_RESOURCE_FORMAT_EXTENSION} has been deprecated. Update the configuration for ${configResource.reference?.reference ?? newResource.name} to use the current extension, ${CURRENT_RESOURCE_FORMAT_EXTENSION}.`
+        );
+      }
     }
     this.ig.definition.resource.push(newResource);
   }
@@ -926,10 +942,6 @@ export class IGExporter {
    * analyzed when making changes to either.
    */
   private addPredefinedResources(): void {
-    const igResourceFormatExtensionUrls = [
-      'http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format',
-      'http://hl7.org/fhir/tools/StructureDefinition/implementationguide-resource-format'
-    ];
     // Similar code for loading custom resources exists in load.ts loadCustomResources()
     const pathEnds = [
       'capabilities',
@@ -958,7 +970,7 @@ export class IGExporter {
     const configuredBinaryResources = (this.config.resources ?? []).filter(
       resource =>
         resource.reference?.reference?.startsWith('Binary/') &&
-        resource.extension?.some(e => igResourceFormatExtensionUrls.includes(e.url))
+        resource.extension?.some(e => IG_RESOURCE_FORMAT_EXTENSIONS.includes(e.url))
     );
     for (const dirPath of predefinedResourcePaths) {
       if (existsSync(dirPath)) {
@@ -976,20 +988,33 @@ export class IGExporter {
             // For predefined examples of Logical Models, the user must provide an entry in config
             // that specifies the reference as Binary/[id], the extension that specifies the resource format,
             // and the exampleCanonical that references the LogicalModel the resource is an example of.
+            // Note: the exampleCanonical should reference the resourceType because the resourceType should
+            // be the absolute URL, but we previously supported using the logical model's id as the example's
+            // resourceType, so support having an exampleCanonical in either form for now.
             // In that case, we do not want to add our own entry for the predefined resource - we just
             // want to use the resource entry from the sushi-config.yaml
             // For predefined examples of Logical Models that do not have a resourceType or id,
             // a Binary resource reference based on the file name can be used, based on Zulip:
             // https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/How.20do.20I.20get.20SUSHI.20to.20ignore.20a.20binary.20JSON.20logical.20instance.3F/near/407861211
-            const hasBinaryReference = configuredBinaryResources.some(
+            const configuredBinaryReference = configuredBinaryResources.find(
               resource =>
                 (resource.reference?.reference === `Binary/${resourceJSON.id}` &&
-                  resource.exampleCanonical ===
-                    `${this.config.canonical}/StructureDefinition/${resourceJSON.resourceType}`) ||
+                  (resource.exampleCanonical ===
+                    `${this.config.canonical}/StructureDefinition/${resourceJSON.resourceType}` ||
+                    resource.exampleCanonical === resourceJSON.resourceType)) ||
                 resource.reference?.reference === `Binary/${path.parse(file).name}`
             );
 
-            if (hasBinaryReference) {
+            if (configuredBinaryReference) {
+              if (
+                configuredBinaryReference.extension?.some(
+                  ext => ext.url === DEPRECATED_RESOURCE_FORMAT_EXTENSION
+                )
+              ) {
+                logger.warn(
+                  `The extension ${DEPRECATED_RESOURCE_FORMAT_EXTENSION} has been deprecated. Update the configuration for ${configuredBinaryReference.reference?.reference ?? configuredBinaryReference.name} to use the current extension, ${CURRENT_RESOURCE_FORMAT_EXTENSION}.`
+                );
+              }
               continue;
             }
 


Test patch:
diff --git a/test/ig/IGExporter.IG.test.ts b/test/ig/IGExporter.IG.test.ts
index 915195a..15722df 100644
--- a/test/ig/IGExporter.IG.test.ts
+++ b/test/ig/IGExporter.IG.test.ts
@@ -1317,7 +1317,7 @@ describe('IGExporter', () => {
           exampleCanonical: 'http://hl7.org/fhir/sushi-test/StructureDefinition/CustomLogicalModel',
           extension: [
             {
-              url: 'http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format',
+              url: 'http://hl7.org/fhir/tools/StructureDefinition/implementationguide-resource-format',
               valueCode: 'application/fhir+json'
             }
           ]
@@ -3044,20 +3044,46 @@ describe('IGExporter', () => {
           },
           extension: [
             {
-              url: 'http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format',
+              url: 'http://hl7.org/fhir/tools/StructureDefinition/implementationguide-resource-format',
               valueCode: 'application/json'
             }
           ],
           name: 'Example of LM JSON',
           exampleCanonical: `${config.canonical}/StructureDefinition/MyLM`
         },
+        {
+          reference: {
+            reference: 'Binary/second-example-logical-model-json'
+          },
+          extension: [
+            {
+              url: 'http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format', // deprecated url still supported
+              valueCode: 'application/json'
+            }
+          ],
+          name: 'Another Example of LM JSON',
+          exampleCanonical: `${config.canonical}/StructureDefinition/MyLM`
+        },
+        {
+          reference: {
+            reference: 'Binary/canonical-url-resourceType-example-logical-model'
+          },
+          extension: [
+            {
+              url: 'http://hl7.org/fhir/tools/StructureDefinition/implementationguide-resource-format',
+              valueCode: 'application/json'
+            }
+          ],
+          name: 'Example of LM JSON with the full canonical URL as the resourceType',
+          exampleCanonical: `${config.canonical}/StructureDefinition/MyLM`
+        },
         {
           reference: {
             reference: 'Binary/example-logical-model-xml'
           },
           extension: [
             {
-              url: 'http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format',
+              url: 'http://hl7.org/fhir/tools/StructureDefinition/implementationguide-resource-format',
               valueCode: 'application/xml'
             }
           ],
@@ -3083,7 +3109,7 @@ describe('IGExporter', () => {
       );
       expect(fs.existsSync(igPath)).toBeTruthy();
       const igContent: ImplementationGuide = fs.readJSONSync(igPath);
-      expect(igContent.definition.resource).toHaveLength(3);
+      expect(igContent.definition.resource).toHaveLength(5);
       expect(igContent.definition.resource).toContainEqual({
         reference: {
           reference: 'StructureDefinition/MyLM'
@@ -3097,26 +3123,57 @@ describe('IGExporter', () => {
         },
         extension: [
           {
-            url: 'http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format',
+            url: 'http://hl7.org/fhir/tools/StructureDefinition/implementationguide-resource-format',
             valueCode: 'application/json'
           }
         ],
         name: 'Example of LM JSON',
         exampleCanonical: `${config.canonical}/StructureDefinition/MyLM`
       });
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'Binary/second-example-logical-model-json'
+        },
+        extension: [
+          {
+            url: 'http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format', // deprecated url still supported
+            valueCode: 'application/json'
+          }
+        ],
+        name: 'Another Example of LM JSON',
+        exampleCanonical: `${config.canonical}/StructureDefinition/MyLM`
+      });
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'Binary/canonical-url-resourceType-example-logical-model'
+        },
+        extension: [
+          {
+            url: 'http://hl7.org/fhir/tools/StructureDefinition/implementationguide-resource-format',
+            valueCode: 'application/json'
+          }
+        ],
+        name: 'Example of LM JSON with the full canonical URL as the resourceType',
+        exampleCanonical: `${config.canonical}/StructureDefinition/MyLM`
+      });
       expect(igContent.definition.resource).toContainEqual({
         reference: {
           reference: 'Binary/example-logical-model-xml'
         },
         extension: [
           {
-            url: 'http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format',
+            url: 'http://hl7.org/fhir/tools/StructureDefinition/implementationguide-resource-format',
             valueCode: 'application/xml'
           }
         ],
         name: 'Example of LM XML',
         exampleCanonical: `${config.canonical}/StructureDefinition/MyLM`
       });
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+      expect(loggerSpy.getAllMessages('warn')).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /extension .* has been deprecated\. Update the configuration for Binary\/second-example-logical-model-json.*/
+      );
     });
   });
 
@@ -3206,7 +3263,10 @@ describe('IGExporter', () => {
         ]
       });
       expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
-      expect(loggerSpy.getAllMessages('warn')).toHaveLength(0);
+      expect(loggerSpy.getAllMessages('warn')).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /extension .* has been deprecated\. Update the configuration for Binary\/MissingResourceTypeAndId.*/
+      );
     });
   });
 
diff --git a/test/ig/fixtures/customized-ig-with-logical-model-example/input/examples/Binary-canonical-url-resourceType-example-logical-model.json b/test/ig/fixtures/customized-ig-with-logical-model-example/input/examples/Binary-canonical-url-resourceType-example-logical-model.json
new file mode 100644
index 0000000..2d86714
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-logical-model-example/input/examples/Binary-canonical-url-resourceType-example-logical-model.json
@@ -0,0 +1,4 @@
+{
+  "resourceType": "http://hl7.org/fhir/us/minimal/StructureDefinition/MyLM",
+  "id": "canonical-url-resourceType-example-logical-model"
+}
\ No newline at end of file
diff --git a/test/ig/fixtures/customized-ig-with-logical-model-example/input/examples/Binary-second-example-logical-model-json.json b/test/ig/fixtures/customized-ig-with-logical-model-example/input/examples/Binary-second-example-logical-model-json.json
new file mode 100644
index 0000000..6b23c36
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-logical-model-example/input/examples/Binary-second-example-logical-model-json.json
@@ -0,0 +1,4 @@
+{
+  "resourceType": "MyLM",
+  "id": "second-example-logical-model-json"
+}
\ No newline at end of file