Repository: FHIR/sushi (ts)

Issue:
Sliced meta.profile with version number forcing instances to have an extra meta.profile repeat without version number.
If there is a profile slice requirement for meta.profile where the canonical requires a version number, and FSH generated instance of the resource will have 2 meta.profile instances one with and one without the version number. If the latest version of the profile changed with breaking changes, this can cause a validation issue.

Occurs with the CARIN BlueButton IG.

FSHOnline example: https://fshschool.org/FSHOnline/#/share/3eM13Rq 
CARIN BB Jira ticket: https://jira.hl7.org/browse/FHIR-32428
CARIN BB Example: https://build.fhir.org/ig/HL7/carin-bb/Organization-Payer1.json.html



Pull request description:
Avoid duplicate entries in Instance's meta.profile due to versioned profiles
Fixes #891 and completes task [CIMPL-800](https://standardhealthrecord.atlassian.net/browse/CIMPL-800).

When creating an Instance of a profiled resource, the profile's URL will be put into the Instance's `meta.profile` array. However, if that URL is already present in the array with a version, remove the unversioned URL from `meta.profile`. It's a little weird to explain, but the example in the issue demonstrates it clearly.

Patch:
diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts
index fa72149..ec9291b 100644
--- a/src/export/InstanceExporter.ts
+++ b/src/export/InstanceExporter.ts
@@ -305,14 +305,35 @@ export class InstanceExporter implements Fishable {
       instanceDef._instanceMeta.sdType = instanceOfStructureDefinition.type;
     }
 
-    // Add the SD we are making an instance of to meta.profile, as long as SD is not a base FHIR resource
-    // If we end up adding more metadata, we should wrap this in a setMetadata function
-    if (isResource && instanceOfStructureDefinition.derivation === 'constraint') {
-      instanceDef.meta = { profile: [instanceOfStructureDefinition.url] };
-    }
-
     // Set Assigned values based on the FSH rules and the Structure Definition
     instanceDef = this.setAssignedValues(fshDefinition, instanceDef, instanceOfStructureDefinition);
+    // should we add the instanceOf to meta.profile?
+    // if the exact url is not in there, and a versioned url is also not in there, add it to the front.
+    // otherwise, add it at the front.
+    if (isResource && instanceOfStructureDefinition.derivation === 'constraint') {
+      // elements of instanceDef.meta.profile may be objects if they are provided by slices,
+      // since they have to keep track of the _sliceName property.
+      // this is technically not a match for the defined type of instanceDef.meta.profile,
+      // so give the parameter a union type to handle both cases.
+      if (
+        !instanceDef.meta?.profile?.some((profile: string | { assignedValue: string }) => {
+          const profileUrl = typeof profile === 'object' ? profile.assignedValue : profile;
+          return (
+            profileUrl === instanceOfStructureDefinition.url ||
+            profileUrl.startsWith(`${instanceOfStructureDefinition.url}|`)
+          );
+        })
+      ) {
+        // we might have to create meta or meta.profile first, if no rules already created those
+        if (instanceDef.meta == null) {
+          instanceDef.meta = { profile: [instanceOfStructureDefinition.url] };
+        } else if (instanceDef.meta.profile == null) {
+          instanceDef.meta.profile = [instanceOfStructureDefinition.url];
+        } else {
+          instanceDef.meta.profile.unshift(instanceOfStructureDefinition.url);
+        }
+      }
+    }
     instanceDef.validateId(fshDefinition.sourceInfo);
     this.validateRequiredElements(
       instanceDef,


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 4b51fb2..981c1f9 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -209,6 +209,28 @@ describe('InstanceExporter', () => {
       expect(exported.meta).toBeUndefined();
     });
 
+    it('should set meta.profile with the InstanceOf profile before checking for required elements', () => {
+      /*
+       * meta 1..1 MS
+       * meta.profile 1..* MS
+       */
+      const metaMS = new FlagRule('meta');
+      metaMS.mustSupport = true;
+      const metaCard = new CardRule('meta');
+      metaCard.min = 1;
+      metaCard.max = '1';
+      const metaProfileMS = new FlagRule('meta.profile');
+      metaProfileMS.mustSupport = true;
+      const metaProfileCard = new CardRule('meta.profile');
+      metaProfileCard.min = 1;
+      metaProfileCard.max = '*';
+      patient.rules.push(metaMS, metaCard, metaProfileMS, metaProfileCard);
+
+      const exported = exportInstance(patientInstance);
+      expect(exported.meta.profile).toHaveLength(1);
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
+
     it('should only set meta.profile with one profile when profile is set on the InstanceOf profile', () => {
       const patientAbstractProfile = new Profile('MyPatientAbstract');
       patientAbstractProfile.parent = 'Patient';
@@ -228,13 +250,13 @@ describe('InstanceExporter', () => {
       metaProfileMS.mustSupport = true;
       const metaProfileCard = new CardRule('meta.profile');
       metaProfileCard.min = 1;
-      metaProfileCard.max = '1';
+      metaProfileCard.max = '*';
       const typeCaretRule = new CaretValueRule('meta.profile');
       typeCaretRule.caretPath = 'slicing.discriminator.type';
       typeCaretRule.value = new FshCode('pattern');
-      const pathCaretRule = new CaretValueRule('item');
+      const pathCaretRule = new CaretValueRule('meta.profile');
       pathCaretRule.caretPath = 'slicing.discriminator.path';
-      pathCaretRule.value = 'type';
+      pathCaretRule.value = '$this';
       const containsRule = new ContainsRule('meta.profile');
       containsRule.items = [{ name: 'supportedPatientProfile' }];
       const cardRule = new CardRule('meta.profile[supportedPatientProfile]');
@@ -274,6 +296,247 @@ describe('InstanceExporter', () => {
       ]);
     });
 
+    it('should add the InstanceOf profile as the first meta.profile if it is not added by any rules', () => {
+      /*
+       * meta 1..1 MS
+       * meta.profile 1..* MS
+       * meta.profile ^slicing.discriminator.type = #pattern
+       * meta.profile ^slicing.discriminator.path = "$this"
+       */
+      const metaMS = new FlagRule('meta');
+      metaMS.mustSupport = true;
+      const metaCard = new CardRule('meta');
+      metaCard.min = 1;
+      metaCard.max = '1';
+      const metaProfileMS = new FlagRule('meta.profile');
+      metaProfileMS.mustSupport = true;
+      const metaProfileCard = new CardRule('meta.profile');
+      metaProfileCard.min = 1;
+      metaProfileCard.max = '*';
+      const typeCaretRule = new CaretValueRule('meta.profile');
+      typeCaretRule.caretPath = 'slicing.discriminator.type';
+      typeCaretRule.value = new FshCode('pattern');
+      const pathCaretRule = new CaretValueRule('meta.profile');
+      pathCaretRule.caretPath = 'slicing.discriminator.path';
+      pathCaretRule.value = '$this';
+      patient.rules.push(
+        metaMS,
+        metaCard,
+        metaProfileMS,
+        metaProfileCard,
+        typeCaretRule,
+        pathCaretRule
+      );
+
+      // * name[0].family = "LastName"
+      // * meta.profile[+] = "http://example.org/fhir/StructureDefinition/OtherPatient"
+      const patientAssignmentRule = new AssignmentRule('name[0].family');
+      patientAssignmentRule.value = 'LastName';
+      const otherProfileRule = new AssignmentRule('meta.profile[+]');
+      otherProfileRule.value = 'http://example.org/fhir/StructureDefinition/OtherPatient';
+      patientInstance.rules.push(patientAssignmentRule, otherProfileRule);
+
+      const exported = exportInstance(patientInstance);
+      expect(exported.meta.profile).toHaveLength(2);
+      expect(exported.meta.profile).toEqual([
+        'http://hl7.org/fhir/us/minimal/StructureDefinition/TestPatient',
+        'http://example.org/fhir/StructureDefinition/OtherPatient'
+      ]);
+    });
+
+    it('should set meta.profile without the unversioned InstanceOf profile if a versioned InstanceOf profile is present', () => {
+      /*
+       * meta 1..1 MS
+       * meta.profile 1..* MS
+       * meta.profile ^slicing.discriminator.type = #pattern
+       * meta.profile ^slicing.discriminator.path = "$this"
+       * meta.profile contains supportedProfile 1..1
+       * meta.profile[supportedProfile] = Canonical(TestPatient|0.1.0)
+       */
+      const metaMS = new FlagRule('meta');
+      metaMS.mustSupport = true;
+      const metaCard = new CardRule('meta');
+      metaCard.min = 1;
+      metaCard.max = '1';
+      const metaProfileMS = new FlagRule('meta.profile');
+      metaProfileMS.mustSupport = true;
+      const metaProfileCard = new CardRule('meta.profile');
+      metaProfileCard.min = 1;
+      metaProfileCard.max = '*';
+      const typeCaretRule = new CaretValueRule('meta.profile');
+      typeCaretRule.caretPath = 'slicing.discriminator.type';
+      typeCaretRule.value = new FshCode('pattern');
+      const pathCaretRule = new CaretValueRule('meta.profile');
+      pathCaretRule.caretPath = 'slicing.discriminator.path';
+      pathCaretRule.value = '$this';
+      const containsRule = new ContainsRule('meta.profile');
+      containsRule.items = [{ name: 'supportedProfile' }];
+      const cardRule = new CardRule('meta.profile[supportedProfile]');
+      cardRule.min = 1;
+      cardRule.max = '1';
+      const supportedProfileCanonical = new AssignmentRule('meta.profile[supportedProfile]');
+      supportedProfileCanonical.value = new FshCanonical('TestPatient');
+      supportedProfileCanonical.value.version = '0.1.0';
+      patient.rules.push(
+        metaMS,
+        metaCard,
+        metaProfileMS,
+        metaProfileCard,
+        typeCaretRule,
+        pathCaretRule,
+        containsRule,
+        cardRule,
+        supportedProfileCanonical
+      );
+
+      // * name[0].family = "LastName"
+      const patientAssignmentRule = new AssignmentRule('name[0].family');
+      patientAssignmentRule.value = 'LastName';
+      patientInstance.rules.push(patientAssignmentRule);
+
+      const exported = exportInstance(patientInstance);
+      expect(exported.meta.profile).toHaveLength(1);
+      expect(exported.meta.profile).toEqual([
+        'http://hl7.org/fhir/us/minimal/StructureDefinition/TestPatient|0.1.0'
+      ]);
+    });
+
+    it('should keep the unversioned InstanceOf in meta.profile if it is also added by a rule on the profile', () => {
+      /*
+       * meta 1..1 MS
+       * meta.profile 2..* MS
+       * meta.profile ^slicing.discriminator.type = #pattern
+       * meta.profile ^slicing.discriminator.path = "$this"
+       * meta.profile contains supportedProfile 1..1 and unversionedProfile 1..1
+       * meta.profile[supportedProfile] = Canonical(TestPatient|0.1.0)
+       * meta.profile[unversionedProfile] = Canonical(TestPatient)
+       */
+      const metaMS = new FlagRule('meta');
+      metaMS.mustSupport = true;
+      const metaCard = new CardRule('meta');
+      metaCard.min = 1;
+      metaCard.max = '1';
+      const metaProfileMS = new FlagRule('meta.profile');
+      metaProfileMS.mustSupport = true;
+      const metaProfileCard = new CardRule('meta.profile');
+      metaProfileCard.min = 2;
+      metaProfileCard.max = '*';
+      const typeCaretRule = new CaretValueRule('meta.profile');
+      typeCaretRule.caretPath = 'slicing.discriminator.type';
+      typeCaretRule.value = new FshCode('pattern');
+      const pathCaretRule = new CaretValueRule('meta.profile');
+      pathCaretRule.caretPath = 'slicing.discriminator.path';
+      pathCaretRule.value = '$this';
+      const containsRule = new ContainsRule('meta.profile');
+      containsRule.items = [{ name: 'supportedProfile' }, { name: 'unversionedProfile' }];
+      const supportedCardRule = new CardRule('meta.profile[supportedProfile]');
+      supportedCardRule.min = 1;
+      supportedCardRule.max = '1';
+      const supportedProfileCanonical = new AssignmentRule('meta.profile[supportedProfile]');
+      supportedProfileCanonical.value = new FshCanonical('TestPatient');
+      supportedProfileCanonical.value.version = '0.1.0';
+      const unversionedCardRule = new CardRule('meta.profile[unversionedProfile]');
+      unversionedCardRule.min = 1;
+      unversionedCardRule.max = '1';
+      const unversionedProfileCanonical = new AssignmentRule('meta.profile[unversionedProfile]');
+      unversionedProfileCanonical.value = new FshCanonical('TestPatient');
+      patient.rules.push(
+        metaMS,
+        metaCard,
+        metaProfileMS,
+        metaProfileCard,
+        typeCaretRule,
+        pathCaretRule,
+        containsRule,
+        supportedCardRule,
+        supportedProfileCanonical,
+        unversionedCardRule,
+        unversionedProfileCanonical
+      );
+
+      // * name[0].family = "LastName"
+      const patientAssignmentRule = new AssignmentRule('name[0].family');
+      patientAssignmentRule.value = 'LastName';
+      patientInstance.rules.push(patientAssignmentRule);
+
+      const exported = exportInstance(patientInstance);
+      expect(exported.meta.profile).toHaveLength(2);
+      expect(exported.meta.profile).toEqual([
+        'http://hl7.org/fhir/us/minimal/StructureDefinition/TestPatient|0.1.0',
+        'http://hl7.org/fhir/us/minimal/StructureDefinition/TestPatient'
+      ]);
+    });
+
+    it('should keep the unversioned InstanceOf in meta.profile if it is also added by a rule on the instance', () => {
+      /*
+       * meta 1..1 MS
+       * meta.profile 1..* MS
+       * meta.profile ^slicing.discriminator.type = #pattern
+       * meta.profile ^slicing.discriminator.path = "$this"
+       * meta.profile contains supportedProfile 1..1
+       * meta.profile[supportedProfile] = Canonical(TestPatient|0.1.0)
+       */
+      const metaMS = new FlagRule('meta');
+      metaMS.mustSupport = true;
+      const metaCard = new CardRule('meta');
+      metaCard.min = 1;
+      metaCard.max = '1';
+      const metaProfileMS = new FlagRule('meta.profile');
+      metaProfileMS.mustSupport = true;
+      const metaProfileCard = new CardRule('meta.profile');
+      metaProfileCard.min = 1;
+      metaProfileCard.max = '*';
+      const typeCaretRule = new CaretValueRule('meta.profile');
+      typeCaretRule.caretPath = 'slicing.discriminator.type';
+      typeCaretRule.value = new FshCode('pattern');
+      const pathCaretRule = new CaretValueRule('meta.profile');
+      pathCaretRule.caretPath = 'slicing.discriminator.path';
+      pathCaretRule.value = '$this';
+      const containsRule = new ContainsRule('meta.profile');
+      containsRule.items = [{ name: 'supportedProfile' }];
+      const cardRule = new CardRule('meta.profile[supportedProfile]');
+      cardRule.min = 1;
+      cardRule.max = '1';
+      const supportedProfileCanonical = new AssignmentRule('meta.profile[supportedProfile]');
+      supportedProfileCanonical.value = new FshCanonical('TestPatient');
+      supportedProfileCanonical.value.version = '0.1.0';
+      patient.rules.push(
+        metaMS,
+        metaCard,
+        metaProfileMS,
+        metaProfileCard,
+        typeCaretRule,
+        pathCaretRule,
+        containsRule,
+        cardRule,
+        supportedProfileCanonical
+      );
+
+      // * name[0].family = "LastName"
+      // * meta.profile[+] = Canonical(TestPatient|0.1.0)
+      // * meta.profile[+] = Canonical(TestPatient)
+      const patientAssignmentRule = new AssignmentRule('name[0].family');
+      patientAssignmentRule.value = 'LastName';
+      const versionedProfileRule = new AssignmentRule('meta.profile[+]');
+      versionedProfileRule.value = new FshCanonical('TestPatient');
+      versionedProfileRule.value.version = '0.1.0';
+      const unversionedProfileRule = new AssignmentRule('meta.profile[+]');
+      unversionedProfileRule.value = new FshCanonical('TestPatient');
+
+      patientInstance.rules.push(
+        patientAssignmentRule,
+        versionedProfileRule,
+        unversionedProfileRule
+      );
+
+      const exported = exportInstance(patientInstance);
+      expect(exported.meta.profile).toHaveLength(2);
+      expect(exported.meta.profile).toEqual([
+        'http://hl7.org/fhir/us/minimal/StructureDefinition/TestPatient|0.1.0',
+        'http://hl7.org/fhir/us/minimal/StructureDefinition/TestPatient'
+      ]);
+    });
+
     // Setting instance id
     it('should set id to instance name by default', () => {
       const myExamplePatient = new Instance('MyExample');