Repository: FHIR/sushi (ts)

Issue:
Generate URL for inline extensions
If an extension is defined using the Extension keyword, the url is created by SUSHI.  But if the extension is defined inline, the URL is not created by SUSHI, and an error results. Workaround: set the URL if you have inline extensions.

Pull request description:
Auto Fix URL for Inline Extensions
Fixes #178 

When extensions are defined inline, we now automatically fix the `extension.url` element to have a `fixedUri` equal to the sliceName. The previous behavior was to require the user to set `extension.url` manually, and throw an `InvalidExtensionSliceError` if they didn't. We checked `extension.url` in the `validateStructureDefinition` function. Since we are now automatically setting the `extension.url`, this PR also removes the `InvalidExtensionSliceError`, and the `validateStructureDefinition` function, since their sole purpose was checking if `extension.url` had been set for inline extensions.

One way to test this is to modify fsh-mcode to contain an inline extension. In CancerCondition.fsh, you can add these two lines below line 19:
```
* extension[HistologyMorphologyBehavior].value[x] only CodeableConcept
* extension[HistologyMorphologyBehavior].valueCodeableConcept from HistologyMorphologyBehaviorVS (extensible)
```
and comment out the `Extension` definition for `HistologyMorphologyBehavior`, so that the extension is defined inline. When trying to run this on master, you should see errors like:
```
error: The slice HistologyMorphologyBehavior on extension must reference an existing extension, or fix a url if the extension is defined inline.
  File: C:\Users\nfreiter\dev\standardhealth\fsh-mcode\CancerCondition.fsh
```
But when you run on this branch, you should not see those errors, and if you look at the generated StructureDefinition for CancerConditionParent, you should see that the url element for `HistologyMorphologyBehavior` does get a fixedUri set to `HistologyMorphologyBehavior`

Patch:
diff --git a/src/errors/InvalidExtensionSliceError.ts b/src/errors/InvalidExtensionSliceError.ts
deleted file mode 100644
index 9c2e6e3..0000000
--- a/src/errors/InvalidExtensionSliceError.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Annotated } from './Annotated';
-
-export class InvalidExtensionSliceError extends Error implements Annotated {
-  specReferences = ['http://www.hl7.org/fhiR/profiling.html#slicing'];
-  constructor(public sliceName: string) {
-    super(
-      `The slice ${sliceName} on extension must reference an existing extension, or fix a url if the extension is defined inline.`
-    );
-  }
-}
diff --git a/src/errors/index.ts b/src/errors/index.ts
index 0bec755..8820e85 100644
--- a/src/errors/index.ts
+++ b/src/errors/index.ts
@@ -27,7 +27,6 @@ export * from './CannotResolvePathError';
 export * from './InvalidElementAccessError';
 export * from './InvalidSumOfSliceMinsError';
 export * from './InvalidMaxOfSliceError';
-export * from './InvalidExtensionSliceError';
 export * from './ParentNotDefinedError';
 export * from './PackageLoadError';
 export * from './DevPackageLoadError';
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index e869106..cbf7efc 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -1,7 +1,7 @@
 import { StructureDefinition, ElementDefinitionBindingStrength } from '../fhirtypes';
 import { Profile, Extension } from '../fshtypes';
 import { FSHTank } from '../import';
-import { ParentNotDefinedError, InvalidExtensionSliceError } from '../errors';
+import { ParentNotDefinedError } from '../errors';
 import {
   CardRule,
   FixedValueRule,
@@ -138,6 +138,13 @@ export class StructureDefinitionExporter implements Fishable {
                     slice.type[0].profile = [];
                   }
                   slice.type[0].profile.push(extension.url);
+                } else {
+                  // If the extension is inline, fix its url element automatically to the sliceName
+                  const urlElement = structDef.findElementByPath(
+                    `${rule.path}[${slice.sliceName}].url`,
+                    this
+                  );
+                  urlElement.fixValue(slice.sliceName);
                 }
               }
             });
@@ -160,27 +167,6 @@ export class StructureDefinitionExporter implements Fishable {
     }
   }
 
-  /**
-   * Does post processing validation on the sd
-   * @param {StructureDefinition} structDef - The sd to validate
-   * @throws {InvalidExtensionSliceError} when the sd contains an extension with invalid slices
-   */
-  private validateStructureDefinition(structDef: StructureDefinition): void {
-    // Need to check that extensions define a URL correctly
-    const extensionSlices = structDef.elements.filter(
-      e => e.path.endsWith('extension') && e.sliceName
-    );
-    extensionSlices.forEach(ext => {
-      const profileUrl =
-        ext.type?.length > 0 && ext.type[0].profile?.length > 0 && ext.type[0].profile[0];
-      const urlChild = ext.children().find(c => c.id === `${ext.id}.url`);
-      // If an element is a slice of extension, it should have a url in type, or a fixedUri
-      if (!profileUrl && !urlChild?.fixedUri) {
-        throw new InvalidExtensionSliceError(ext.sliceName);
-      }
-    });
-  }
-
   fishForFHIR(item: string, ...types: Type[]) {
     let result = this.fisher.fishForFHIR(item, ...types);
     if (
@@ -249,8 +235,6 @@ export class StructureDefinitionExporter implements Fishable {
     }
 
     this.setRules(structDef, fshDefinition);
-    this.validateStructureDefinition(structDef);
-
     structDef.inProgress = false;
 
     return structDef;


Test patch:
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index bba77e5..e1690a0 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -1255,6 +1255,33 @@ describe('StructureDefinitionExporter', () => {
     );
   });
 
+  it('should apply a ContainsRule of an inline extension to an extension element', () => {
+    const profile = new Profile('Foo');
+    profile.parent = 'Observation';
+
+    const containsRule = new ContainsRule('extension');
+    containsRule.items = ['my-inline-extension'];
+    profile.rules.push(containsRule);
+
+    exporter.exportStructDef(profile);
+    const sd = pkg.profiles[0];
+
+    const extension = sd.elements.find(e => e.id === 'Observation.extension');
+    const extensionSlice = sd.elements.find(
+      e => e.id === 'Observation.extension:my-inline-extension'
+    );
+    const extensionSliceUrl = sd.elements.find(
+      e => e.id === 'Observation.extension:my-inline-extension.url'
+    );
+
+    expect(extension.slicing).toBeDefined();
+    expect(extension.slicing.discriminator.length).toBe(1);
+    expect(extension.slicing.discriminator[0]).toEqual({ type: 'value', path: 'url' });
+    expect(extensionSlice).toBeDefined();
+    expect(extensionSliceUrl).toBeDefined();
+    expect(extensionSliceUrl.fixedUri).toBe('my-inline-extension');
+  });
+
   it('should apply multiple ContainsRule on an element with defined slicing', () => {
     const profile = new Profile('Foo');
     profile.parent = 'resprate';
@@ -1366,34 +1393,6 @@ describe('StructureDefinitionExporter', () => {
     expect(loggerSpy.getLastMessage()).toMatch(/File: InvalidValue\.fsh.*Line: 6\D/s);
   });
 
-  // validateStructureDefinition
-  it('should throw InvalidExtensionSliceError when an extension is sliced without providing url', () => {
-    const profile = new Profile('Foo');
-    profile.parent = 'Observation';
-
-    const rule = new ContainsRule('extension');
-    rule.items = ['foo'];
-    profile.rules.push(rule);
-
-    expect(() => {
-      exporter.exportStructDef(profile);
-    }).toThrow(
-      'The slice foo on extension must reference an existing extension, or fix a url if the extension is defined inline.'
-    );
-  });
-
-  it('should log a message when exporting a package containing a sliced extension without a url', () => {
-    const profile = new Profile('Mystery').withFile('NoURL.fsh').withLocation([13, 1, 23, 28]);
-    profile.parent = 'Observation';
-
-    const rule = new ContainsRule('extension');
-    rule.items = ['mysterious'];
-    profile.rules.push(rule);
-    doc.profiles.set('Mystery', profile);
-    exporter.export();
-    expect(loggerSpy.getLastMessage()).toMatch(/File: NoURL\.fsh.*Line: 13 - 23\D/s);
-  });
-
   // toJSON
   it('should correctly generate a diff containing only changed elements', () => {
     // We already have separate tests for the differentials, so this just ensures that the