Repository: FHIR/sushi (ts)

Issue:
Improve instance support for elements with multiple profiles
If an element specifies multiple profiles, SUSHI seems to not allow you to assign any values to subpaths of that element.  Instead, SUSHI should just use the base type and emit a warning.

See: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/sushi.3A.20problem.20with.20external.20profiles.20containing.20profiles

Pull request:
Warn when unfolding non-choice element with multiple profiles
Fixed #794 and completes task [CIMPL-716](https://standardhealthrecord.atlassian.net/browse/CIMPL-716).

When assigning a value on an Instance's element, if that element is nested under a non-choice element with multiple profiles, it cannot be determined which of the profiles should apply when unfolding the profiled element. Therefore, ignore all profiles and use the base type. Warn the user that this is being done.

Previously in this scenario, the unfolding would not occur, which resulted in an error being logged indicating that the path to the nested element did not exist. Such an error message was misleading and unhelpful.

You may be wondering why choice and non-choice elements need to be handled differently. Existing logic in `StructureDefinition.findElementByPath` expects choice elements with multiple profiles to get an empty array back from `ElementDefinition.unfold` so it can proceed with [separate logic for choice elements](https://github.com/FHIR/sushi/blob/master/src/fhirtypes/StructureDefinition.ts#L219-L226). In order to limit the code changes for this PR to the `ElementDefinition.unfold` method, the behavior for choice elements is preserved. A [test case for StructureDefinitionExporter](https://github.com/FHIR/sushi/blob/master/test/export/StructureDefinitionExporter.test.ts#L3450) demonstrates a scenario of multiple profiles on a choice element.


Patch:
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index e92bd65..c75bf9a 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -1962,10 +1962,23 @@ export class ElementDefinition {
    *   be resolved.
    */
   unfold(fisher: Fishable): ElementDefinition[] {
-    if (
-      (this.type?.length === 1 && (this.type[0].profile ?? []).length <= 1) ||
-      this.contentReference
-    ) {
+    // if we have a choice element where id ends with [x], proceed if there is at most one profile, since it expects to get [] back in that case
+    // if we don't have a choice element, proceed with any number of profiles
+    const isChoiceElement = this.id.endsWith('[x]');
+    const proceedToUnfold =
+      (this.type?.length === 1 && (!isChoiceElement || (this.type[0].profile ?? []).length <= 1)) ||
+      this.contentReference;
+    if (proceedToUnfold) {
+      let profileToUse: string;
+      const availableProfiles = this.type?.length === 1 ? this.type[0].profile : [];
+      // if more than one profile is available, none of them can be used, since there's no way to decide.
+      if (availableProfiles?.length > 1) {
+        logger.warn(
+          `Multiple profiles present on element ${this.id}. Base element type will be used instead of any profiles.`
+        );
+      } else if (availableProfiles?.length === 1) {
+        profileToUse = availableProfiles[0];
+      }
       let newElements: ElementDefinition[] = [];
       if (this.contentReference) {
         // Get the original resource JSON so we unfold unconstrained reference
@@ -2003,8 +2016,8 @@ export class ElementDefinition {
         newElements = this.cloneChildren(slicedElement);
       }
       if (newElements.length === 0) {
-        // If it has a profile, use that, otherwise use the code
-        const type = this.type[0].profile?.[0] ?? this.type[0].code;
+        // If we have exactly one profile to use, use that, otherwise use the code
+        const type = profileToUse ?? this.type[0].code;
         const json = fisher.fishForFHIR(
           type,
           Type.Resource,


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index f7c6ecf..e9a0497 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -1173,6 +1173,43 @@ describe('InstanceExporter', () => {
       });
     });
 
+    it('should log a warning when assigning a value to an element nested within an element with multiple profiles', () => {
+      // Consider two Identifier profiles with mutually incompatible assigned values
+      const regularIdentifier = new Profile('RegularIdentifier');
+      regularIdentifier.parent = 'Identifier';
+      const regularSystem = new AssignmentRule('system');
+      regularSystem.value = 'http://example.org/regular';
+      regularIdentifier.rules.push(regularSystem);
+      doc.profiles.set(regularIdentifier.name, regularIdentifier);
+      const unusualIdentifier = new Profile('UnusualIdentifier');
+      unusualIdentifier.parent = 'Identifier';
+      const unusualSystem = new AssignmentRule('system');
+      unusualSystem.value = 'http://example.org/unusual';
+      unusualIdentifier.rules.push(unusualSystem);
+      doc.profiles.set(unusualIdentifier.name, unusualIdentifier);
+      // In TestPatient, give the generalPractitioner.identifier element multiple profiles
+      const gpOnly = new OnlyRule('generalPractitioner.identifier');
+      gpOnly.types.push({ type: 'RegularIdentifier' }, { type: 'UnusualIdentifier' });
+      patient.rules.push(gpOnly);
+      // Assign a value to generalPractitioner.identifier.value, which is nested within the profiled element
+      const gpValue = new AssignmentRule('generalPractitioner.identifier.value');
+      gpValue.value = '12345';
+      // Assign a value to generalPractitioner.identifier.system. This value is invalid on both profiles, but those profiles are being ignored.
+      const gpSystem = new AssignmentRule('generalPractitioner.identifier.system');
+      gpSystem.value = 'http://example.org/something-else';
+      patientInstance.rules.push(gpValue, gpSystem);
+      const exported = exportInstance(patientInstance);
+      // The assigned values should be present
+      expect(exported.generalPractitioner[0].identifier.value).toBe('12345');
+      expect(exported.generalPractitioner[0].identifier.system).toBe(
+        'http://example.org/something-else'
+      );
+      // We should receive a warning that the profiles are being ignored
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        'Multiple profiles present on element Patient.generalPractitioner.identifier. Base element type will be used instead of any profiles.'
+      );
+    });
+
     // Assigning with pattern[x]
     it('should assign a nested element that is assigned by pattern[x] from a parent on the SD', () => {
       const assignedValRule = new AssignmentRule('maritalStatus.coding');