Repository: FHIR/sushi
Pull request #1446 (2024-03-22 15:41:50)

Find value for choice slice when choice element has one type
Completes task [CIMPL-1248](https://standardhealthrecord.atlassian.net/browse/CIMPL-1248).

When a slice has a descendent that is a choice element, and that choice element has only one type, a choice slice is not created on the StructureDefinition to represent that choice slice. This is because the choice element itself has all the necessary information when it has only one type. When validating required elements on an instance, check for the presence of such an element. If it is found, use its type to determine the name of the property to look for on the instance being validated.


Patch:
diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts
index a029d30..f2a9bb6 100644
--- a/src/export/InstanceExporter.ts
+++ b/src/export/InstanceExporter.ts
@@ -435,10 +435,16 @@ export class InstanceExporter implements Fishable {
         element
           .findConnectedElements()
           .forEach(ce => possibleChoiceSlices.push(...ce.children(true)));
-        const choiceSlices = possibleChoiceSlices.filter(c => c.path === child.path && c.sliceName);
+        const choiceSlices = possibleChoiceSlices.filter(
+          c => c.path === child.path && (c.sliceName || c.type.length === 1)
+        );
         for (const choiceSlice of choiceSlices) {
+          // for each of these elements, we either have a sliceName, or we build a sliceName from the type
+          const sliceName =
+            choiceSlice.sliceName ??
+            childPathEnd.replace('[x]', upperFirst(choiceSlice.type[0].code));
           // as above, we use the _ prefixed element if it exists
-          instanceChild = instance[`_${choiceSlice.sliceName}`] ?? instance[choiceSlice.sliceName];
+          instanceChild = instance[`_${sliceName}`] ?? instance[sliceName];
           const splitChoicePath = splitOnPathPeriods(choiceSlice.id);
           // If the element we're assigning to has a sliceName, use it to ensure that we're validating
           // against the correct choice slice


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 04f23e8..3002d29 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -8823,6 +8823,77 @@ describe('InstanceExporter', () => {
       );
     });
 
+    it('should not log a warning when a choice element with one type has its cardinality satisfied by a rule that includes the name of an ancestor slice', () => {
+      // Profile: TestQuestionnaire
+      // Parent: Questionnaire
+      // * item ^slicing.discriminator[0].path = "type"
+      // * item ^slicing.discriminator[0].type = #value
+      // * item ^slicing.rules = #open
+      // * item contains boo 0..1
+      // * item[boo].answerOption.value[x] only string
+      const slicingPath = new CaretValueRule('item');
+      slicingPath.caretPath = 'slicing.discriminator.path';
+      slicingPath.value = 'type';
+      const slicingType = new CaretValueRule('item');
+      slicingType.caretPath = 'slicing.discriminator.type';
+      slicingType.value = new FshCode('value');
+      const slicingRules = new CaretValueRule('item');
+      slicingRules.caretPath = 'slicing.rules';
+      slicingRules.value = new FshCode('open');
+      const itemContains = new ContainsRule('item');
+      itemContains.items.push({ name: 'boo' });
+      const booCard = new CardRule('item[boo]');
+      booCard.min = 0;
+      booCard.max = '1';
+      const booOnly = new OnlyRule('item[boo].answerOption.value[x]');
+      booOnly.types = [{ type: 'string' }];
+      questionnaire.rules.push(
+        slicingPath,
+        slicingType,
+        slicingRules,
+        itemContains,
+        booCard,
+        booOnly
+      );
+      // Instance: SurpriseQuestionnaire
+      // InstanceOf: TestQuestionnaire
+      // * status = #active
+      // * item[0].answerOption[0].valueString = "regular"
+      // * item[0].linkId = "regularLink"
+      // * item[0].type = #group
+      // * item[boo].answerOption[0].valueString = "boo!"
+      // * item[boo].linkId = "booLink"
+      // * item[boo].type = #group
+      const questionnaireInstance = new Instance('SurpriseQuestionnaire');
+      questionnaireInstance.instanceOf = 'TestQuestionnaire';
+      const status = new AssignmentRule('status');
+      status.value = new FshCode('active');
+      const regularValue = new AssignmentRule('item[0].answerOption[0].valueString');
+      regularValue.value = 'regular';
+      const regularLink = new AssignmentRule('item[0].linkId');
+      regularLink.value = 'regularLink';
+      const regularType = new AssignmentRule('item[0].type');
+      regularType.value = new FshCode('group');
+      const surpriseValue = new AssignmentRule('item[boo].answerOption[0].valueString');
+      surpriseValue.value = 'boo!';
+      const surpriseLink = new AssignmentRule('item[boo].linkId');
+      surpriseLink.value = 'booLink';
+      const surpriseType = new AssignmentRule('item[boo].type');
+      surpriseType.value = new FshCode('group');
+      questionnaireInstance.rules.push(
+        status,
+        regularValue,
+        regularLink,
+        regularType,
+        surpriseValue,
+        surpriseLink,
+        surpriseType
+      );
+      exportInstance(questionnaireInstance);
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+      expect(loggerSpy.getAllMessages('warn')).toHaveLength(0);
+    });
+
     it('should not log an error when a reslice element fulfills a cardinality constraint', () => {
       // Profile: TestPatient
       // Parent: Patient