Repository: FHIR/sushi (ts)

Issue:
SUSHI incorrectly handles cardinality inference in sliced arrays
It seems like SUHSI incorrectly infers the cardinality of a sliced array when values are assigned within that sliced array. See this example below:
```
Alias: $loinc = http://loinc.org
Alias: $snomed = http://snomed.info/sct

Profile: RespRate
Parent: Observation
* code.coding ^slicing.discriminator[0].type = #pattern
* code.coding ^slicing.discriminator[0].path = "$this"
* code.coding ^slicing.rules = #open
* code.coding contains
    loinc 1..* and
    snomed 0..*
* code.coding[loinc] = $loinc#9279-1
* code.coding[loinc].system 1..
* code.coding[loinc].code 1..
* code.coding[snomed] = $snomed#86290005
* code.coding[snomed].system 1..
* code.coding[snomed].code 1..
```
SUSHI incorrectly makes the `code.coding[snomed]` slice have cardinality `1..*` because of this line:
```
* code.coding[snomed] = $snomed#86290005
```
If you break that line into multiple pieces:
```
* code.coding[snomed].system = $snomed
* code.coding[snomed].code = #86290005
```
The `code.coding[snomed]` slice stays optional. I'm not quite sure what is going on here, but we need to investigate this and figure out what the correct behavior is.

Pull request description:
Don't change min to 1 when a slice is assigned a value
We previously would bump min 0 to min 1 if you assigned a value directly to a slice path (and that path was also a discriminator for the slice).  We don't do that anymore because we should never make an optional slice required.

The funny thing is that we had a test that actually checked for this faulty behavior and failed when I fixed it.

Fixes #810



Patch:
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index 3644278..f9c02af 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -1500,14 +1500,17 @@ export class ElementDefinition {
     }
 
     // If the element is found in a discriminator.path, then we enforce that it has minimum cardinality 1
-    // since its value is assigned
+    // since its value is assigned. It has to be there, otherwise it's not that slice. The one exception
+    // is if you are fixing the value of the sliced element itself (e.g., discriminator path is $this),
+    // in which case we don't want to force the whole slice to be required.
     const parentSlices = [this, ...this.getAllParents().reverse()].filter(el => el.sliceName);
     parentSlices.forEach(parentSlice => {
       const slicedElement = parentSlice.slicedElement();
       if (
         slicedElement.slicing.discriminator?.some(
           d =>
-            `${slicedElement.path}${d.path === '$this' ? '' : `.${d.path}`}` === this.path &&
+            d.path !== '$this' &&
+            `${slicedElement.path}.${d.path}` === this.path &&
             ['value', 'pattern'].includes(d.type)
         ) &&
         this.min === 0


Test patch:
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 3a016c6..a6670ea 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -3963,6 +3963,51 @@ describe('StructureDefinitionExporter R4', () => {
       expect(addressLineElement.slicing.discriminator[0].path).toEqual('code');
     });
 
+    it('should not change slice cardinality when an AssignmentRule is applied directly on the slice', () => {
+      // See: https://github.com/FHIR/sushi/issues/810
+      //
+      // Profile: RespRateFragment
+      // Parent: Observation
+      // * code.coding ^slicing.discriminator[0].type = #pattern
+      // * code.coding ^slicing.discriminator[0].path = "$this"
+      // * code.coding ^slicing.rules = #open
+      // * code.coding contains snomed 0..*
+      // * code.coding[snomed] = http://snomed.info/sct#86290005
+      const profile = new Profile('RespRateFragment');
+      profile.parent = 'Observation';
+      const slicingTypeLine = new CaretValueRule('code.coding');
+      slicingTypeLine.caretPath = 'slicing.discriminator[0].type';
+      slicingTypeLine.value = new FshCode('pattern');
+      const slicingPathLine = new CaretValueRule('code.coding');
+      slicingPathLine.caretPath = 'slicing.discriminator[0].path';
+      slicingPathLine.value = '$this';
+      const slicingRulesLine = new CaretValueRule('code.coding');
+      slicingRulesLine.caretPath = 'slicing.rules';
+      slicingRulesLine.value = new FshCode('open');
+      const containsSnomed = new ContainsRule('code.coding');
+      containsSnomed.items.push({ name: 'snomed' });
+      const snomedCard = new CardRule('code.coding[snomed]');
+      snomedCard.min = 0;
+      snomedCard.max = '*';
+      const snomedAssignment = new AssignmentRule('code.coding[snomed]');
+      snomedAssignment.value = new FshCode('86290005', 'http://snomed.info/sct');
+
+      profile.rules.push(
+        slicingTypeLine,
+        slicingPathLine,
+        slicingRulesLine,
+        containsSnomed,
+        snomedCard,
+        snomedAssignment
+      );
+
+      exporter.exportStructDef(profile);
+      const sd = pkg.profiles[0];
+      const snomedElement = sd.findElement('Observation.code.coding:snomed');
+      expect(snomedElement.min).toBe(0);
+      expect(snomedElement.max).toBe('*');
+    });
+
     it('should not apply a AssignmentRule to a slice when it would conflict with a child slice of the list element', () => {
       // Instance: CustomPostalAddress
       // InstanceOf: Address
diff --git a/test/fhirtypes/ElementDefinition.assignValue.test.ts b/test/fhirtypes/ElementDefinition.assignValue.test.ts
index fb033ee..e041e21 100644
--- a/test/fhirtypes/ElementDefinition.assignValue.test.ts
+++ b/test/fhirtypes/ElementDefinition.assignValue.test.ts
@@ -70,19 +70,6 @@ describe('ElementDefinition', () => {
       expect(catMouseCodingCode.min).toBe(1);
     });
 
-    it('should ensure that minimum cardinality is 1 when assigning a value mentioned in a slice discriminator', () => {
-      const inUri = medicationRequest.elements.find(
-        e => e.id === 'MedicationRequest.instantiatesUri'
-      );
-      inUri.slicing = { discriminator: [{ type: 'value', path: '$this' }], rules: 'open' };
-      inUri.addSlice('mouse');
-      const inUriMouse = medicationRequest.findElementByPath('instantiatesUri[mouse]', fisher);
-      expect(inUriMouse.min).toBe(0);
-      inUriMouse.assignValue('http://mice.cheese');
-      expect(inUriMouse.patternUri).toBe('http://mice.cheese');
-      expect(inUriMouse.min).toBe(1);
-    });
-
     it('should ensure that minimum cardinality is 1 when assigning a value mentioned in the discriminator of a grandparent slice', () => {
       const cat = medicationRequest.elements.find(e => e.id === 'MedicationRequest.category');
       cat.slicing = { discriminator: [{ type: 'value', path: 'coding.code' }], rules: 'open' };
@@ -100,6 +87,19 @@ describe('ElementDefinition', () => {
       expect(catMouseCodingRatCode.min).toBe(1);
     });
 
+    it('should not ensure that minimum cardinality is 1 when assigning a value mentioned in a slice discriminator via $this', () => {
+      const inUri = medicationRequest.elements.find(
+        e => e.id === 'MedicationRequest.instantiatesUri'
+      );
+      inUri.slicing = { discriminator: [{ type: 'value', path: '$this' }], rules: 'open' };
+      inUri.addSlice('mouse');
+      const inUriMouse = medicationRequest.findElementByPath('instantiatesUri[mouse]', fisher);
+      expect(inUriMouse.min).toBe(0);
+      inUriMouse.assignValue('http://mice.cheese');
+      expect(inUriMouse.patternUri).toBe('http://mice.cheese');
+      expect(inUriMouse.min).toBe(0);
+    });
+
     it('should not ensure that minimum cardinality is 1 when assigning a value not mentioned in a slice discriminator', () => {
       const cat = medicationRequest.elements.find(e => e.id === 'MedicationRequest.category');
       cat.slicing = { discriminator: [{ type: 'value', path: 'coding.code' }], rules: 'open' };