Repository: FHIR/sushi (ts)

Issue:
SUSHI should set fixed values that are used in discriminators to 1..1
See: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/Creating.20Examples.20with.20slicing/near/191818082

Pull request description:
Set discriminator value cardinality
This fixes #301 

Now when a part of a slice gets a value fixed on it, and that value is used by the `path` in the relevant slice discriminator which discriminates based on `value` or `pattern`, if the slice has a minimum cardinality of 0, we change that minimum cardinality to be 1.

Patch:
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index 10a9fa9..1361bc2 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -1166,6 +1166,23 @@ export class ElementDefinition {
         throw new MismatchedTypeError(type, value, this.type[0].code);
     }
 
+    // If the element is found in a discriminator.path, then we enforce that it has minimum cardinality 1
+    // since its value is fixed
+    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 &&
+            ['value', 'pattern'].includes(d.type)
+        ) &&
+        this.min === 0
+      ) {
+        this.constrainCardinality(1, '');
+      }
+    });
+
     // Units error should not stop fixing value, but must still be logged
     const types = this.findTypesByCode('Quantity');
     if (units && types.length === 0) {


Test patch:
diff --git a/test/fhirtypes/ElementDefinition.fixValue.test.ts b/test/fhirtypes/ElementDefinition.fixValue.test.ts
index ae7a7fb..7924a53 100644
--- a/test/fhirtypes/ElementDefinition.fixValue.test.ts
+++ b/test/fhirtypes/ElementDefinition.fixValue.test.ts
@@ -82,6 +82,90 @@ describe('ElementDefinition', () => {
         coding: [{ code: 'mycode', system: 'https://code.com' }]
       });
     });
+
+    it('should ensure that minimum cardinality is 1 when fixing a value mentioned in a parent slice discriminator', () => {
+      const cat = medicationRequest.elements.find(e => e.id === 'MedicationRequest.category');
+      cat.slicing = { discriminator: [{ type: 'value', path: 'coding.code' }], rules: 'open' };
+      cat.addSlice('mouse');
+      const catMouseCodingCode = medicationRequest.findElementByPath(
+        'category[mouse].coding.code',
+        fisher
+      );
+      expect(catMouseCodingCode.min).toBe(0);
+      catMouseCodingCode.fixValue(new FshCode('cheese'));
+      expect(catMouseCodingCode.patternCode).toBe('cheese');
+      expect(catMouseCodingCode.min).toBe(1);
+    });
+
+    it('should ensure that minimum cardinality is 1 when fixing 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.fixValue('http://mice.cheese');
+      expect(inUriMouse.patternUri).toBe('http://mice.cheese');
+      expect(inUriMouse.min).toBe(1);
+    });
+
+    it('should ensure that minimum cardinality is 1 when fixing 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' };
+      cat.addSlice('mouse');
+      const catCoding = medicationRequest.findElementByPath('category.coding', fisher);
+      catCoding.slicing = { discriminator: [{ type: 'value', path: 'coding' }], rules: 'open' };
+      catCoding.addSlice('rat');
+      const catMouseCodingRatCode = medicationRequest.findElementByPath(
+        'category[mouse].coding[rat].code',
+        fisher
+      );
+      expect(catMouseCodingRatCode.min).toBe(0);
+      catMouseCodingRatCode.fixValue(new FshCode('cheese'));
+      expect(catMouseCodingRatCode.patternCode).toBe('cheese');
+      expect(catMouseCodingRatCode.min).toBe(1);
+    });
+
+    it('should not ensure that minimum cardinality is 1 when fixing 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' };
+      cat.addSlice('mouse');
+      const catMouseCodingSystem = medicationRequest.findElementByPath(
+        'category[mouse].coding.system',
+        fisher
+      );
+      expect(catMouseCodingSystem.min).toBe(0);
+      catMouseCodingSystem.fixValue('http://mice.cheese');
+      expect(catMouseCodingSystem.patternUri).toBe('http://mice.cheese');
+      expect(catMouseCodingSystem.min).toBe(0);
+    });
+
+    it('should not ensure that minimum cardinality is 1 when fixing a value mentioned in a non value/pattern discriminator', () => {
+      const cat = medicationRequest.elements.find(e => e.id === 'MedicationRequest.category');
+      cat.slicing = { discriminator: [{ type: 'exists', path: 'coding.code' }], rules: 'open' };
+      cat.addSlice('mouse');
+      const catMouseCodingCode = medicationRequest.findElementByPath(
+        'category[mouse].coding.code',
+        fisher
+      );
+      expect(catMouseCodingCode.min).toBe(0);
+      catMouseCodingCode.fixValue(new FshCode('cheese'));
+      expect(catMouseCodingCode.patternCode).toBe('cheese');
+      expect(catMouseCodingCode.min).toBe(0);
+    });
+
+    it('should not ensure that minimum cardinality is 1 when fixing a value with min card > 1 mentioned in a parent slice discriminator', () => {
+      const cat = medicationRequest.elements.find(e => e.id === 'MedicationRequest.category');
+      cat.slicing = { discriminator: [{ type: 'value', path: 'coding' }], rules: 'open' };
+      cat.addSlice('mouse');
+      const catMouseCoding = medicationRequest.findElementByPath('category[mouse].coding', fisher);
+      catMouseCoding.min = 2;
+      expect(catMouseCoding.min).toBe(2);
+      catMouseCoding.fixValue(new FshCode('cheese'));
+      expect(catMouseCoding.patternCoding).toEqual({ code: 'cheese' });
+      expect(catMouseCoding.min).toBe(2); // We do not try to decrease min to 1
+    });
   });
 
   describe('#fixedByDirectParent', () => {