Repository: FHIR/sushi (ts)

Issue:
SUSHI does not export extensions correctly when setting extension on primitive using caret rule
Easier to explain by example ([FSH Online](https://fshschool.org/FSHOnline/#/share/36L5EiC)):
```
Profile: MyProfile
Parent: Patient
// This serializes correctly:
* ^extension[MyBooleanExtension].valueBoolean = true
// But this does not serialize correctly:
* ^status.extension[MyBooleanExtension].valueBoolean = true

Extension: MyBooleanExtension
* value[x] only boolean
```

The first extension gets serialized correctly:
```json
{
  "url": "http://example.org/StructureDefinition/MyBooleanExtension",
  "valueBoolean": true
}
```

The second does not:
```json
{
  "_sliceName": "MyBooleanExtension",
  "url": {
    "assignedValue": "http://example.org/StructureDefinition/MyBooleanExtension",
    "_primitive": true
  },
  "valueBoolean": {
    "assignedValue": true,
    "_primitive": true
  }
}
```

Pull request description:
Clean up extensions on primitives
Fixes #752 and completes task [CIMPL-668](https://standardhealthrecord.atlassian.net/browse/CIMPL-668).

When cleaning up properties on a StructureDefinition, properties are recursively traversed to find and remove those that were added by SUSHI to help in the export process. Properties that were added by SUSHI start with an underscore character. Properties that contain extra attributes of primitives also start with an underscore character, but will have a
sibling property with the same name without the underscore. Therefore, if such a sibling property is present, the property being examined is a true member of the StructureDefinition, and should be included in the cleanup traversal.

Basically, I'm _pretty_ sure that doing this won't ever get us into trouble, but I might be wrong.

Patch:
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index cfdaea4..6b0b754 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -564,7 +564,11 @@ export class StructureDefinitionExporter implements Fishable {
     this.preprocessStructureDefinition(fshDefinition, structDef.type === 'Extension');
 
     this.setRules(structDef, fshDefinition);
-    cleanResource(structDef, (prop: string) => prop == 'elements' || prop.indexOf('_') == 0);
+    // The elements list does not need to be cleaned up.
+    // And, the _sliceName and _primitive properties added by SUSHI should be skipped.
+    cleanResource(structDef, (prop: string) =>
+      ['elements', '_sliceName', '_primitive'].includes(prop)
+    );
     structDef.inProgress = false;
 
     // check for another structure definition with the same id
diff --git a/src/fhirtypes/common.ts b/src/fhirtypes/common.ts
index b3e997b..7bb2d9f 100644
--- a/src/fhirtypes/common.ts
+++ b/src/fhirtypes/common.ts
@@ -309,7 +309,7 @@ export function getSliceName(pathPart: PathPart): string {
  * @param { {[key: string]: any} } object - The object to replace fields on
  * @param {(object: { [key: string]: any }, prop: string) => boolean} matchFn - The function to match with
  * @param {(object: { [key: string]: any }, prop: string) => void} replaceFn - The function to replace with
- * @param {string => boolean} skipFn - A function that returns true if a property should not be traversed
+ * @param {(object: { [key: string]: any }, prop: string) => boolean} skipFn - A function that returns true if a property should not be traversed
  */
 export function replaceField(
   object: { [key: string]: any },


Test patch:
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 23fb9af..2ed7dcc 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -3087,6 +3087,36 @@ describe('StructureDefinitionExporter', () => {
     expect(sd).toHaveProperty('_url', { id: 'my-id' });
   });
 
+  it('should apply a CaretValueRule on an extension of a primitive element without a path', () => {
+    // Extension: MyBooleanExtension
+    // * value[x] only boolean
+    const extension = new Extension('MyBooleanExtension');
+    const onlyBoolean = new OnlyRule('value[x]');
+    onlyBoolean.types.push({ type: 'boolean' });
+    extension.rules.push(onlyBoolean);
+    doc.extensions.set(extension.name, extension);
+    // Profile: ExtensionOnPublisher
+    // Parent: Patient
+    // * ^publisher.extension[MyBooleanExtension].valueBoolean = true
+    const profile = new Profile('ExtensionOnPublisher');
+    profile.parent = 'Patient';
+    const rule = new CaretValueRule('');
+    rule.caretPath = 'publisher.extension[MyBooleanExtension].valueBoolean';
+    rule.value = true;
+    profile.rules.push(rule);
+
+    exporter.exportStructDef(profile);
+    const sd = pkg.profiles[0];
+    expect(sd).toHaveProperty('_publisher', {
+      extension: [
+        {
+          url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/MyBooleanExtension',
+          valueBoolean: true
+        }
+      ]
+    });
+  });
+
   it('should not apply an invalid CaretValueRule on an element without a path', () => {
     const profile = new Profile('Foo');
     profile.parent = 'Observation';