Repository: FHIR/sushi
Pull request #719 (2021-01-11 13:49:00)

Add Extension Description/Title to root element
This addresses [CIMPL-608](https://standardhealthrecord.atlassian.net/browse/CIMPL-608) by adding a configuration option, `applyExtensionMetadataToRoot`, that can be used to control if the `Description` and `Title` of an `Extension` are used to set the `definition` and `short` on the root element of the `Extension`. The flag defaults to true, so if users don't want this behavior, they will have to explicitly set the flag to false.

To test this, you can try running SUSHI with this new flag set to `true`, `false`, and leaving the flag unset. When the flag is `true` or unset, metadata from the `Extension` will get copied onto the root element (the element with path `Extension`). Setting the `short` or `definition` of that element directly with a `^` rule should override the automatic setting. When the flag is explicitly `false`, no metadata should be copied over. It may also be useful to test this all the way through with the IG Publisher, so you can see how the `short` and `definition` elements are used as @cmoesel describes in this [thread](https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/Extension.20short.2C.20definition.2C.20and.20default.20values). 

ngfreiter: Also, if you take a look at this PR, please take a look at the corresponding documentation update: https://github.com/FSHSchool/site/pull/36.

Patch:
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index a1235f3..a7bc13b 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -98,6 +98,12 @@ export class StructureDefinitionExporter implements Fishable {
     structDef.setName(fshDefinition.name, fshDefinition.sourceInfo);
     if (fshDefinition.title) {
       structDef.title = fshDefinition.title;
+      if (
+        fshDefinition instanceof Extension &&
+        !(this.tank.config.applyExtensionMetadataToRoot === false)
+      ) {
+        structDef.elements[0].short = fshDefinition.title;
+      }
     } else {
       delete structDef.title;
     }
@@ -108,6 +114,12 @@ export class StructureDefinitionExporter implements Fishable {
     delete structDef.contact;
     if (fshDefinition.description) {
       structDef.description = fshDefinition.description;
+      if (
+        fshDefinition instanceof Extension &&
+        !(this.tank.config.applyExtensionMetadataToRoot === false)
+      ) {
+        structDef.elements[0].definition = fshDefinition.description;
+      }
     } else {
       delete structDef.description;
     }
diff --git a/src/fshtypes/Configuration.ts b/src/fshtypes/Configuration.ts
index 84f0962..7a31db9 100644
--- a/src/fshtypes/Configuration.ts
+++ b/src/fshtypes/Configuration.ts
@@ -135,6 +135,10 @@ export type Configuration = {
   // When the FSHOnly parameter is set to true, no IG specific content will be generated, SUSHI will
   // only convert FSH definitions to JSON files. When false, IG content is generated.
   FSHOnly?: boolean;
+
+  // When set to true, the "short" and "definition" field on the root element of an Extension will
+  // be set to the "Title" and "Description" of that Extension. Default is true.
+  applyExtensionMetadataToRoot?: boolean;
 };
 
 export type ConfigurationGroup = {
diff --git a/src/import/YAMLConfiguration.ts b/src/import/YAMLConfiguration.ts
index 692bbab..deeccc0 100644
--- a/src/import/YAMLConfiguration.ts
+++ b/src/import/YAMLConfiguration.ts
@@ -177,6 +177,10 @@ export type YAMLConfiguration = {
   // When the FSHOnly parameter is set to true, no IG specific content will be generated, SUSHI will
   // only convert FSH definitions to JSON files. When false or unset, IG content is generated.
   FSHOnly?: boolean;
+
+  // When set to true, the "short" and "definition" field on the root element of an Extension will
+  // be set to the "Title" and "Description" of that Extension. Default is true.
+  applyExtensionMetadataToRoot?: boolean;
 };
 
 export type YAMLConfigurationMeta = {
diff --git a/src/import/importConfiguration.ts b/src/import/importConfiguration.ts
index 498d58d..9e770c9 100644
--- a/src/import/importConfiguration.ts
+++ b/src/import/importConfiguration.ts
@@ -162,7 +162,8 @@ export function importConfiguration(yaml: YAMLConfiguration | string, file: stri
     menu: parseMenu(yaml.menu),
     history: parseHistory(yaml, file),
     indexPageContent: yaml.indexPageContent,
-    FSHOnly: yaml.FSHOnly ?? false
+    FSHOnly: yaml.FSHOnly ?? false,
+    applyExtensionMetadataToRoot: yaml.applyExtensionMetadataToRoot ?? true
   };
 
   // Remove all undefined variables (mainly helpful for test assertions)


Test patch:
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 11998c7..ea20ed2 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -12,7 +12,8 @@ import {
   FshCodeSystem,
   Invariant,
   RuleSet,
-  FshQuantity
+  FshQuantity,
+  Configuration
 } from '../../src/fshtypes';
 import {
   CardRule,
@@ -256,7 +257,39 @@ describe('StructureDefinitionExporter', () => {
     expect(exported.name).toBe('Foo');
     expect(exported.id).toBe('foo');
     expect(exported.title).toBe('Foo Profile');
+    expect(exported.elements[0].short).toBe('Foo Profile');
     expect(exported.description).toBe('foo bar foobar');
+    expect(exported.elements[0].definition).toBe('foo bar foobar');
+    expect(exported.url).toBe('http://hl7.org/fhir/us/minimal/StructureDefinition/foo');
+    expect(exported.version).toBe('1.0.0');
+    expect(exported.type).toBe('Extension');
+    expect(exported.baseDefinition).toBe('http://hl7.org/fhir/StructureDefinition/Extension');
+
+    // Check that Extension.url is correctly assigned
+    expect(exported.elements.find(e => e.id === 'Extension.url').fixedUri).toBe(
+      'http://hl7.org/fhir/us/minimal/StructureDefinition/foo'
+    );
+  });
+
+  it('should not set metadata on the root element when applyExtensionMetadataToRoot is false', () => {
+    const config: Configuration = { ...minimalConfig, applyExtensionMetadataToRoot: false };
+    const tank = new FSHTank([doc], config);
+    const testExporter = new StructureDefinitionExporter(tank, pkg, fisher);
+    const extension = new Extension('Foo');
+    extension.id = 'foo';
+    extension.title = 'Foo Profile';
+    extension.description = 'foo bar foobar';
+    doc.extensions.set(extension.name, extension);
+    testExporter.exportStructDef(extension);
+    const exported = pkg.extensions[0];
+    expect(exported.name).toBe('Foo');
+    expect(exported.id).toBe('foo');
+    expect(exported.title).toBe('Foo Profile');
+    expect(exported.elements[0].short).toBe('Optional Extensions Element');
+    expect(exported.description).toBe('foo bar foobar');
+    expect(exported.elements[0].definition).toBe(
+      'Optional Extension Element - found in all resources.'
+    );
     expect(exported.url).toBe('http://hl7.org/fhir/us/minimal/StructureDefinition/foo');
     expect(exported.version).toBe('1.0.0');
     expect(exported.type).toBe('Extension');
@@ -328,6 +361,7 @@ describe('StructureDefinitionExporter', () => {
     expect(exported.name).toBe('Foo');
     expect(exported.id).toBe('Foo');
     expect(exported.title).toBeUndefined();
+    expect(exported.elements[0].short).toBe('Optional Extensions Element');
     expect(exported.url).toBe('http://hl7.org/fhir/us/minimal/StructureDefinition/Foo');
     expect(exported.elements.find(e => e.id === 'Extension.url').fixedUri).toBe(
       'http://hl7.org/fhir/us/minimal/StructureDefinition/Foo'
diff --git a/test/import/YAMLConfiguration.test.ts b/test/import/YAMLConfiguration.test.ts
index ed5efa7..291daf4 100644
--- a/test/import/YAMLConfiguration.test.ts
+++ b/test/import/YAMLConfiguration.test.ts
@@ -126,6 +126,8 @@ describe('YAMLConfiguration', () => {
           sequence: 'STU 1'
         }
       });
+      expect(config.FSHOnly).toBeFalse();
+      expect(config.applyExtensionMetadataToRoot).toBeTrue();
     });
   });
 });
diff --git a/test/import/fixtures/example-config.yaml b/test/import/fixtures/example-config.yaml
index 6a66246..6e6ebeb 100644
--- a/test/import/fixtures/example-config.yaml
+++ b/test/import/fixtures/example-config.yaml
@@ -225,4 +225,8 @@ indexPageContent: Example Index Page Content
 # The FSHOnly flag indicates if only FSH resources should be exported.
 # If set to true, no IG related content will be generated.
 # The default value for this property is false.
-FSHOnly: false
\ No newline at end of file
+FSHOnly: false
+
+# When set to true, the "short" and "definition" field on the root element of an Extension will
+# be set to the "Title" and "Description" of that Extension. Default is true.
+applyExtensionMetadataToRoot: true
\ No newline at end of file
diff --git a/test/import/importConfiguration.test.ts b/test/import/importConfiguration.test.ts
index 8c00275..b5108ab 100644
--- a/test/import/importConfiguration.test.ts
+++ b/test/import/importConfiguration.test.ts
@@ -40,7 +40,8 @@ describe('importConfiguration', () => {
       ],
       template: 'hl7.fhir.template#0.0.5',
       packageId: 'fhir.us.minimal',
-      FSHOnly: false
+      FSHOnly: false,
+      applyExtensionMetadataToRoot: true
     };
     expect(actual).toEqual(expected);
     expect(loggerSpy.getAllLogs('error')).toHaveLength(0);
@@ -197,7 +198,8 @@ describe('importConfiguration', () => {
         ]
       },
       indexPageContent: 'Example Index Page Content',
-      FSHOnly: false
+      FSHOnly: false,
+      applyExtensionMetadataToRoot: true
     };
     expect(actual).toEqual(expected);
     expect(loggerSpy.getAllLogs('error')).toHaveLength(0);
diff --git a/test/utils/Processing.test.ts b/test/utils/Processing.test.ts
index 43c8876..c261788 100644
--- a/test/utils/Processing.test.ts
+++ b/test/utils/Processing.test.ts
@@ -234,6 +234,7 @@ describe('Processing', () => {
           { packageId: 'hl7.fhir.uv.vhdir', version: 'current' }
         ],
         FSHOnly: false,
+        applyExtensionMetadataToRoot: true,
         parameters: [
           {
             code: 'copyrightyear',