Repository: FHIR/sushi (ts)

Issue:
Issue with assigning values on sliced instances
I'm not exactly sure what is going wrong here, but when you make the following instance:
```
Alias: Abrechnung = http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/Abrechnungszeilen

Instance:  TestInstanceSlicing
InstanceOf: Abrechnung
Usage:  #example
* status = #issued
* type.coding.system = "http://fhir.abda.de/eRezeptAbgabedaten/CodeSystem/invoice-typ"
* type.coding.code.value = "abrechnungszeilen"
* lineItem[0].chargeItemCodeableConcept = http://fhir.de/CodeSystem/ifa/pzn#1
```
which is an instance of this this profile: https://simplifier.net/erezeptabgabedaten/abrechnungszeilen-duplicate-2 (which I've also attached as a text file), you get errors like:
```
error Cannot assign [object Object] to this element; a different Coding is already assigned: {"system":"http://TA1.abda.de"}.
  File: /input/fsh/abrechnungszeilen.fsh
  Line: 9
```

At the very least, the [object Object] is no good, but I'm also pretty sure this error should not exist at all. But this does require further investigation.

[Abrechnungszeile.txt](https://github.com/FHIR/sushi/files/5925759/Abrechnungszeile.txt)


Pull request description:
Fix assigned value on sliced children bug
This fixes #748. It is easiest to illustrate with an example. You can use the example cited in the issue, but here is a slightly simpler example I've been using:
```
Profile: MyProfile
Parent: Observation
* category.coding ^slicing.discriminator.type = #value
* category.coding ^slicing.discriminator.path = "coding"
* category.coding ^slicing.rules = #open
* category.coding contains slice1 0..1
* category.coding[slice1] = http://example.com#foo

Instance: MyInstance
InstanceOf: MyProfile
* category[0] = http://example.com#bar
* status = #active
* code = #final
```

In this example, `category.coding` is sliced, and the slice has a value set on it, but the slicing is `#open`, so any other values can also be added to the `category.coding` array. But, if you run this example on `master`, you will get an error like:
```
Cannot assign [object Object] to this element; a different Coding is already assigned: {"code":"foo","system":"http://example.com"}.
```
which is a bug, since the `http://example.com#foo` is assigned to a specific slice, not the entire `coding` array. If you run that example on this branch, you will see no errors, and the first instance rule will be correctly applied.

To further test this PR, change the slicing from `#open` to `#closed`. Now slicing is closed, so every element in the `coding` array must match a slice, and since the `http://example.com#bar` value does not, an error will be logged indicating that this value does not satisfy any slice, and cannot be assigned.

Now try changing `http://example.com#bar` to `http://example.com#foo`. This now does satisfy at least one slice in the closed slicing, so the assignment will be allowed.

Patch:
diff --git a/src/errors/ValueConflictsWithClosedSlicingError.ts b/src/errors/ValueConflictsWithClosedSlicingError.ts
new file mode 100644
index 0000000..4e5f8a3
--- /dev/null
+++ b/src/errors/ValueConflictsWithClosedSlicingError.ts
@@ -0,0 +1,7 @@
+export class ValueConflictsWithClosedSlicingError extends Error {
+  constructor(public requestedValue: boolean | number | string) {
+    super(
+      `Cannot assign ${requestedValue} to this element since it conflicts with all values of the closed slicing.`
+    );
+  }
+}
diff --git a/src/errors/index.ts b/src/errors/index.ts
index 8004f23..f1972e3 100644
--- a/src/errors/index.ts
+++ b/src/errors/index.ts
@@ -44,3 +44,4 @@ export * from './InvalidResourceTypeError';
 export * from './InvalidExtensionParentError';
 export * from './InvalidTypeAccessError';
 export * from './NonAbstractParentOfSpecializationError';
+export * from './ValueConflictsWithClosedSlicingError';
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index 2a0d4ac..1b8cc34 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -38,7 +38,8 @@ import {
   InvalidMappingError,
   InvalidFHIRIdError,
   DuplicateSliceError,
-  NonAbstractParentOfSpecializationError
+  NonAbstractParentOfSpecializationError,
+  ValueConflictsWithClosedSlicingError
 } from '../errors';
 import { setPropertyOnDefinitionInstance, splitOnPathPeriods } from './common';
 import { Fishable, Type, Metadata, logger } from '../utils';
@@ -1360,14 +1361,12 @@ export class ElementDefinition {
     }
 
     // Children of elements with complex types such as Quantity may already have assigned values
-    this.children().forEach(child => this.checkChildAssignedValue(child, fhirValue));
+    this.checkAssignedValueAgainstChildren(this, fhirValue);
 
     // if this is a slice, make sure that nothing on this.slicedElement() is being violated
     const slicedElement = this.slicedElement();
     if (slicedElement) {
-      slicedElement
-        .children()
-        .forEach(child => slicedElement.checkChildAssignedValue(child, fhirValue));
+      slicedElement.checkAssignedValueAgainstChildren(slicedElement, fhirValue);
     }
 
     // If we made it this far, assign the value using fixed[x] or pattern[x] as appropriate
@@ -1382,7 +1381,39 @@ export class ElementDefinition {
     }
   }
 
-  private checkChildAssignedValue(child: ElementDefinition, fhirValue: any) {
+  private checkAssignedValueAgainstChildren(currentChild: ElementDefinition, fhirValue: any) {
+    const directChildren = currentChild.children(true);
+
+    let i = 0;
+    while (i < directChildren.length) {
+      const child = directChildren[i];
+
+      this.checkAssignedValueAgainstChild(child, fhirValue);
+      this.checkAssignedValueAgainstChildren(child, fhirValue);
+
+      // If the slicing is closed, only throw an error if all slices conflict
+      if (child.slicing?.rules === 'closed') {
+        let numInvalid = 0;
+        child.getSlices().forEach(slice => {
+          try {
+            this.checkAssignedValueAgainstChild(slice, fhirValue);
+            this.checkAssignedValueAgainstChildren(slice, fhirValue);
+          } catch (e) {
+            numInvalid += 1;
+          }
+        });
+        if (numInvalid >= child.getSlices().length) {
+          throw new ValueConflictsWithClosedSlicingError(JSON.stringify(fhirValue));
+        }
+      }
+
+      // If there is any slicing, skip past the slices. Closed slices are handled above
+      // And open slices need not be validated against
+      i += child.slicing ? child.getSlices().length + 1 : 1;
+    }
+  }
+
+  private checkAssignedValueAgainstChild(child: ElementDefinition, fhirValue: any) {
     const childType = child.type[0].code;
     const fixedX = `fixed${upperFirst(childType)}` as keyof ElementDefinition;
     const patternX = `pattern${upperFirst(childType)}` as keyof ElementDefinition;


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 0854431..2268b7d 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -2085,6 +2085,156 @@ describe('InstanceExporter', () => {
       expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
     });
 
+    it('should not assign a value which violates a closed child slicing', () => {
+      // Profile: MyProfile
+      // Parent: Observation
+      // * category.coding ^slicing.discriminator.type = #value
+      // * category.coding ^slicing.discriminator.path = "coding"
+      // * category.coding ^slicing.rules = #closed
+      // * category.coding contains slice1 0..1
+      // * category.coding[slice1] = http://example.com#foo
+      const profile = new Profile('MyProfile');
+      profile.parent = 'Observation';
+      const typeRule = new CaretValueRule('category.coding');
+      typeRule.caretPath = 'slicing.discriminator[0].type';
+      typeRule.value = new FshCode('pattern');
+      const pathRule = new CaretValueRule('category.coding');
+      pathRule.caretPath = 'slicing.discriminator[0].path';
+      pathRule.value = 'code';
+      const rulesRule = new CaretValueRule('category.coding');
+      rulesRule.caretPath = 'slicing.rules';
+      rulesRule.value = new FshCode('closed');
+      const containsRule = new ContainsRule('category.coding');
+      containsRule.items.push({ name: 'slice1' });
+      const assignmentRule = new AssignmentRule('category.coding[slice1]');
+      assignmentRule.value = new FshCode('foo', 'http://example.com');
+      profile.rules.push(typeRule, pathRule, rulesRule, containsRule, assignmentRule);
+      doc.profiles.set(profile.name, profile);
+
+      // Instance: MyInstance
+      // InstanceOf: MyProfile
+      // * category[0] = http://not-example.org#bar
+      const instance = new Instance('MyInstance');
+      instance.instanceOf = 'MyProfile';
+      const categoryRule = new AssignmentRule('category[0]');
+      categoryRule.value = new FshCode('bar', 'http://not-example.com');
+      instance.rules.push(categoryRule);
+      doc.instances.set(instance.name, instance);
+
+      sdExporter.export();
+      const exported = exporter.export().instances;
+      const exportedInstance = exported.find(i => i._instanceMeta.name === 'MyInstance');
+      expect(exportedInstance.category).toBeUndefined();
+      expect(loggerSpy.getAllMessages('error')).toContainEqual(
+        'Cannot assign {"coding":[{"code":"bar","system":"http://not-example.com"}]} to this element since it conflicts with all values of the closed slicing.'
+      );
+    });
+
+    it('should assign a value which does not violate all elements of a closed child slicing', () => {
+      // Profile: MyProfile
+      // Parent: Observation
+      // * category.coding ^slicing.discriminator.type = #value
+      // * category.coding ^slicing.discriminator.path = "coding"
+      // * category.coding ^slicing.rules = #closed
+      // * category.coding contains slice1 0..1 and slice2 0..1
+      // * category.coding[slice1] = http://example.com#foo
+      // * category.coding[slice2] = http://example.com#bar
+      const profile = new Profile('MyProfile');
+      profile.parent = 'Observation';
+      const typeRule = new CaretValueRule('category.coding');
+      typeRule.caretPath = 'slicing.discriminator[0].type';
+      typeRule.value = new FshCode('pattern');
+      const pathRule = new CaretValueRule('category.coding');
+      pathRule.caretPath = 'slicing.discriminator[0].path';
+      pathRule.value = 'code';
+      const rulesRule = new CaretValueRule('category.coding');
+      rulesRule.caretPath = 'slicing.rules';
+      rulesRule.value = new FshCode('closed');
+      const containsRule = new ContainsRule('category.coding');
+      containsRule.items.push({ name: 'slice1' });
+      containsRule.items.push({ name: 'slice2' });
+      const assignmentRule = new AssignmentRule('category.coding[slice1]');
+      assignmentRule.value = new FshCode('foo', 'http://example.com');
+      const assignmentRule2 = new AssignmentRule('category.coding[slice2]');
+      assignmentRule2.value = new FshCode('bar', 'http://example.com');
+      profile.rules.push(
+        typeRule,
+        pathRule,
+        rulesRule,
+        containsRule,
+        assignmentRule,
+        assignmentRule2
+      );
+      doc.profiles.set(profile.name, profile);
+
+      // Instance: MyInstance
+      // InstanceOf: MyProfile
+      // * category[0] = http://example#bar
+      const instance = new Instance('MyInstance');
+      instance.instanceOf = 'MyProfile';
+      const categoryRule = new AssignmentRule('category[0]');
+      categoryRule.value = new FshCode('bar', 'http://example.com');
+      instance.rules.push(categoryRule);
+      doc.instances.set(instance.name, instance);
+
+      sdExporter.export();
+      const exported = exporter.export().instances;
+      const exportedInstance = exported.find(i => i._instanceMeta.name === 'MyInstance');
+      expect(exportedInstance.category).toEqual([
+        { coding: [{ code: 'bar', system: 'http://example.com' }] }
+      ]);
+      expect(loggerSpy.getAllMessages('error')).not.toContainEqual(
+        'Cannot assign {"coding":[{"code":"bar","system":"http://not-example.com"}]} to this element since it conflicts with all values of the closed slicing.'
+      );
+    });
+
+    it('should assign a value which violates an open child slicing', () => {
+      // Profile: MyProfile
+      // Parent: Observation
+      // * category.coding ^slicing.discriminator.type = #value
+      // * category.coding ^slicing.discriminator.path = "coding"
+      // * category.coding ^slicing.rules = #open
+      // * category.coding contains slice1 0..1
+      // * category.coding[slice1] = http://example.com#foo
+      const profile = new Profile('MyProfile');
+      profile.parent = 'Observation';
+      const typeRule = new CaretValueRule('category.coding');
+      typeRule.caretPath = 'slicing.discriminator[0].type';
+      typeRule.value = new FshCode('pattern');
+      const pathRule = new CaretValueRule('category.coding');
+      pathRule.caretPath = 'slicing.discriminator[0].path';
+      pathRule.value = 'code';
+      const rulesRule = new CaretValueRule('category.coding');
+      rulesRule.caretPath = 'slicing.rules';
+      rulesRule.value = new FshCode('open');
+      const containsRule = new ContainsRule('category.coding');
+      containsRule.items.push({ name: 'slice1' });
+      const assignmentRule = new AssignmentRule('category.coding[slice1]');
+      assignmentRule.value = new FshCode('foo', 'http://example.com');
+      profile.rules.push(typeRule, pathRule, rulesRule, containsRule, assignmentRule);
+      doc.profiles.set(profile.name, profile);
+
+      // Instance: MyInstance
+      // InstanceOf: MyProfile
+      // * category[0] = http://not-example.org#bar
+      const instance = new Instance('MyInstance');
+      instance.instanceOf = 'MyProfile';
+      const categoryRule = new AssignmentRule('category[0]');
+      categoryRule.value = new FshCode('bar', 'http://not-example.com');
+      instance.rules.push(categoryRule);
+      doc.instances.set(instance.name, instance);
+
+      sdExporter.export();
+      const exported = exporter.export().instances;
+      const exportedInstance = exported.find(i => i._instanceMeta.name === 'MyInstance');
+      expect(exportedInstance.category).toEqual([
+        { coding: [{ code: 'bar', system: 'http://not-example.com' }] }
+      ]);
+      expect(loggerSpy.getAllMessages('error')).not.toContainEqual(
+        'Cannot assign {"coding":[{"code":"bar","system":"http://not-example.com"}]} to this element since it conflicts with all values of the closed slicing.'
+      );
+    });
+
     it('should only export an instance once', () => {
       const bundleInstance = new Instance('MyBundle');
       bundleInstance.instanceOf = 'Bundle';
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 86e03cc..23fb9af 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -2125,7 +2125,7 @@ describe('StructureDefinitionExporter', () => {
     // Parent: Patient
     // * address.line ^slicing.discriminator[0].type = #pattern
     // * address.line ^slicing.discriminator[0].path = "$this"
-    // * address.line ^slicing.rules = #open
+    // * address.line ^slicing.rules = #closed
     // * address.line contains SpecificLine 1..1
     // * address.line[SpecificLine] = "Specific part of address"
     // * address ^slicing.discriminator[0].type = #pattern
@@ -2144,7 +2144,7 @@ describe('StructureDefinitionExporter', () => {
     slicingPathLine.value = 'code';
     const slicingRulesLine = new CaretValueRule('address.line');
     slicingRulesLine.caretPath = 'slicing.rules';
-    slicingRulesLine.value = new FshCode('open');
+    slicingRulesLine.value = new FshCode('closed');
     const containsSpecificLine = new ContainsRule('address.line');
     containsSpecificLine.items.push({ name: 'SpecificLine' });
     const specificLineCard = new CardRule('address.line[SpecificLine]');
@@ -2195,7 +2195,7 @@ describe('StructureDefinitionExporter', () => {
     expect(addressLineElement.patternString).toBeDefined();
     expect(addressSliceElement.patternAddress).toBeUndefined();
     expect(loggerSpy.getLastMessage('error')).toMatch(
-      /Cannot assign First part of address to this element.*File: Assigned\.fsh.*Line: 12\D*/s
+      /Cannot assign.*First part of address.*to this element.*File: Assigned\.fsh.*Line: 12\D*/s
     );
   });