Repository: FHIR/sushi (ts)

Issue:
Improve handling of complex id, and assignment of primitive instances generally
If an entity’s id is an object and not just a string, it can produce errors and undesirable results. As long as that object has a “value” attribute, we should be able to use that attribute for the actual string. This only happens when assigning an inline instance of string to “id”.

The right thing to do is to make it so that the value ends up on the actual “id” and the other parts end up on “_id”.

While we discovered this with id, this is applicable when assigning any inline instance of a primitive.

Pull request description:
Allow assigning an inline instance of a primitive
**Description:**

This PR makes it so you can assign an inline instance to an element that is a primitive data type. The `value` property of the inline instance is assigned directly to the primitive element. If the instance defines any other properties, they get added at the `_` property of the primitive instance.

**Testing Instructions:**

I added two tests to test out two slightly different scenarios for this task, but if you wanted to try to FSH for these, this is what I've been using to test:

The following FSH should create a file called Questionnaire-Id3193360.json and assign the id as `Id3193360`:

```
Instance: Id3193360
InstanceOf: id
Usage: #inline
* value = "3193360"

Instance: MyQuestionnaire
InstanceOf: Questionnaire
Usage: #example
* id = Id3193360
* status = #final
```

The following FSH should add "Important Observation" to the `valueString` and add the extension to `_valueString`:

```
Instance: ValueString
InstanceOf: string
Usage: #inline
* value = "Important Observation"
* extension.url = "http://example.com/fake"
* extension.valueBoolean = true

Instance: MyObservation
InstanceOf: Observation
* valueString = ValueString
* status = #final
* code = #123
```

I ran a regression of all the repos changed in the last year, and no changes were reported.

**Related Issue:** Fixes #1526


Patch:
diff --git a/src/fhirtypes/common.ts b/src/fhirtypes/common.ts
index 9ecae80..8413eca 100644
--- a/src/fhirtypes/common.ts
+++ b/src/fhirtypes/common.ts
@@ -749,7 +749,19 @@ export function setPropertyOnInstance(
             // We have to be a little careful when assigning, in case array values are contained in the object
             assignComplexValue(current[key], assignedValue);
           } else {
-            current[key] = assignedValue;
+            if (pathPart.primitive && typeof assignedValue === 'object') {
+              // If setting a primitive value that is an object (an inline instance), set
+              // the value directly and any other values on the _ property
+              if (assignedValue.value != null) {
+                current[key] = assignedValue.value;
+                delete assignedValue.value;
+              }
+              if (!isEmpty(assignedValue)) {
+                current[`_${key}`] = assignedValue;
+              }
+            } else {
+              current[key] = assignedValue;
+            }
           }
         }
       }


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 1bac89e..0f8bb77 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -266,6 +266,13 @@ describe('InstanceExporter', () => {
       carePlanInstance = new Instance('C');
       carePlanInstance.instanceOf = 'CarePlan';
       doc.instances.set(carePlanInstance.name, carePlanInstance);
+      const carePlanStatus = new AssignmentRule('status');
+      carePlanStatus.value = new FshCode('draft');
+      const carePlanIntent = new AssignmentRule('intent');
+      carePlanIntent.value = new FshCode('plan');
+      const carePlanSubject = new AssignmentRule('subject');
+      carePlanSubject.value = new FshReference('Patient/fake');
+      carePlanInstance.rules.push(carePlanStatus, carePlanIntent, carePlanSubject);
       lipidInstance = new Instance('Bam')
         .withFile('LipidInstance.fsh')
         .withLocation([10, 1, 20, 30]);
@@ -10529,9 +10536,15 @@ describe('InstanceExporter', () => {
         // * name = "Everyone"
         doc.instances.set(inlineOrganization.name, inlineOrganization);
 
-        const caretRule = new CaretValueRule('entry');
-        caretRule.caretPath = 'slicing.discriminator.type';
-        caretRule.value = new FshCode('value');
+        const slicingType = new CaretValueRule('entry');
+        slicingType.caretPath = 'slicing.discriminator.type';
+        slicingType.value = new FshCode('value');
+        const slicingPath = new CaretValueRule('entry');
+        slicingPath.caretPath = 'slicing.discriminator.path';
+        slicingPath.value = 'code';
+        const slicingRules = new CaretValueRule('entry');
+        slicingRules.caretPath = 'slicing.rules';
+        slicingRules.value = new FshCode('open');
         const containsRule = new ContainsRule('entry');
         containsRule.items = [{ name: 'PatientsOnly' }];
         const cardRule = new CardRule('entry[PatientsOnly]');
@@ -10548,12 +10561,16 @@ describe('InstanceExporter', () => {
         const choiceTypeRule = new OnlyRule('entry[PatientOrOrganization].resource');
         choiceTypeRule.types = [{ type: 'Patient' }, { type: 'Organization' }];
         // * entry ^slicing.discriminator.type = #value
+        // * entry ^slicing.discriminator.path = "code"
+        // * entry ^slicing.rules = #open
         // * entry contains Patient 0..1
         // * entry[PatientsOnly].resource only Patient
         // * entry contains PatientOrOrganization 0..1
         // * entry[PatientOrOrganization] only Patient or Organization
         bundle.rules.push(
-          caretRule,
+          slicingType,
+          slicingPath,
+          slicingRules,
           containsRule,
           cardRule,
           typeRule,
@@ -11140,7 +11157,7 @@ describe('InstanceExporter', () => {
         patientInstance.rules.push(nameRule);
 
         exportInstance(patientInstance);
-        expect(loggerSpy.getMessageAtIndex(2, 'error')).toMatch(
+        expect(loggerSpy.getLastMessage('error')).toMatch(
           /Cannot assign number value: 990\. Value does not match element type: HumanName.*File: Instances\.fsh.*Line: 5\D*/s
         );
       });
@@ -11166,7 +11183,7 @@ describe('InstanceExporter', () => {
         patientInstance.rules.push(nameRule);
 
         exportInstance(patientInstance);
-        expect(loggerSpy.getMessageAtIndex(2, 'error')).toMatch(
+        expect(loggerSpy.getLastMessage('error')).toMatch(
           /Cannot assign boolean value: false\. Value does not match element type: HumanName.*File: Instances\.fsh.*Line: 6\D*/s
         );
       });
@@ -11354,6 +11371,63 @@ describe('InstanceExporter', () => {
           ]
         });
       });
+
+      it('should assign an inline instance of a primitive to a primitive element', () => {
+        const primitiveInstannce = new Instance('MyFancyId')
+          .withFile('Primitive.fsh')
+          .withLocation([1, 2, 3, 4]);
+        primitiveInstannce.instanceOf = 'id';
+        primitiveInstannce.usage = 'Inline';
+        doc.instances.set(primitiveInstannce.name, primitiveInstannce);
+        // * value = "fancy-id"
+        const valueRule = new AssignmentRule('value');
+        valueRule.value = 'fancy-id';
+        primitiveInstannce.rules.push(valueRule);
+
+        // * id = MyFancyId
+        const inlineRule = new AssignmentRule('id');
+        inlineRule.value = 'MyFancyId';
+        inlineRule.isInstance = true;
+        patientInstance.rules.push(inlineRule);
+
+        const exported = exportInstance(patientInstance);
+        expect(exported.id).toEqual('fancy-id');
+        expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+        expect(loggerSpy.getAllMessages('warn')).toHaveLength(0);
+      });
+
+      it('should assign an inline instance of a primitive with additional properties to a primitive element', () => {
+        const primitiveInstance = new Instance('MyFancyString')
+          .withFile('Primitive.fsh')
+          .withLocation([1, 2, 3, 4]);
+        primitiveInstance.instanceOf = 'string';
+        primitiveInstance.usage = 'Inline';
+        doc.instances.set(primitiveInstance.name, primitiveInstance);
+        // * value = "fancy title string"
+        const valueRule = new AssignmentRule('value');
+        valueRule.value = 'fancy title string';
+        // * extension.url = "http://example.org/fake"
+        const extensionUrl = new AssignmentRule('extension.url');
+        extensionUrl.value = 'http://example.org/fake';
+        // * extension.valueBoolean = true
+        const extensionValue = new AssignmentRule('extension.valueBoolean');
+        extensionValue.value = true;
+        primitiveInstance.rules.push(valueRule, extensionUrl, extensionValue);
+
+        // * title = MyFancyString
+        const inlineRule = new AssignmentRule('title');
+        inlineRule.value = 'MyFancyString';
+        inlineRule.isInstance = true;
+        carePlanInstance.rules.push(inlineRule);
+
+        const exported = exportInstance(carePlanInstance);
+        expect(exported.title).toEqual('fancy title string');
+        expect(exported._title).toEqual({
+          extension: [{ url: 'http://example.org/fake', valueBoolean: true }]
+        });
+        expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+        expect(loggerSpy.getAllMessages('warn')).toHaveLength(0);
+      });
     });
   });