Repository: FHIR/sushi
Pull request #903 (2021-08-11 16:51:43)

Do not add duplicate concepts from insert rules
Completes task [CIMPL-726](https://standardhealthrecord.atlassian.net/browse/CIMPL-726).

When inserting a rule set into a CodeSystem, check to make sure that none of the rules in the rule set would add concepts that are already part of the CodeSystem. This check is done using a function that is also used when checking concepts that are added from concept rules directly on the CodeSystem.


Patch:
diff --git a/src/fhirtypes/common.ts b/src/fhirtypes/common.ts
index 619e0fa..f37faa7 100644
--- a/src/fhirtypes/common.ts
+++ b/src/fhirtypes/common.ts
@@ -490,7 +490,17 @@ export function applyInsertRules(
             }
             ruleSetRuleClone.path = newPath;
           }
-          expandedRules.push(ruleSetRuleClone);
+          if (ruleSetRuleClone instanceof ConceptRule && fshDefinition instanceof FshCodeSystem) {
+            try {
+              if (fshDefinition.checkConcept(ruleSetRuleClone)) {
+                expandedRules.push(ruleSetRuleClone);
+              }
+            } catch (e) {
+              logger.error(e.message, ruleSetRuleClone.sourceInfo);
+            }
+          } else {
+            expandedRules.push(ruleSetRuleClone);
+          }
           if (firstRule) {
             // Once one rule has been applied, all future rules should inherit the index used on that rule
             // rather than continuing to increment the index with the [+] operator
diff --git a/src/fshtypes/FshCodeSystem.ts b/src/fshtypes/FshCodeSystem.ts
index 2623369..56b78d3 100644
--- a/src/fshtypes/FshCodeSystem.ts
+++ b/src/fshtypes/FshCodeSystem.ts
@@ -27,6 +27,12 @@ export class FshCodeSystem extends FshEntity {
   }
 
   addConcept(newConcept: ConceptRule) {
+    if (this.checkConcept(newConcept)) {
+      this.rules.push(newConcept);
+    }
+  }
+
+  checkConcept(newConcept: ConceptRule): boolean {
     const existingConcept = this.rules.find(
       rule => rule instanceof ConceptRule && rule.code == newConcept.code
     ) as ConceptRule;
@@ -39,7 +45,7 @@ export class FshCodeSystem extends FshEntity {
         newConcept.definition == null &&
         isEqual(existingConcept.hierarchy, newConcept.hierarchy)
       ) {
-        return;
+        return false;
       } else {
         throw new CodeSystemDuplicateCodeError(this.id, newConcept.code);
       }
@@ -62,7 +68,7 @@ export class FshCodeSystem extends FshEntity {
         throw new CodeSystemIncorrectHierarchyError(this.id, newConcept.code);
       }
     });
-    this.rules.push(newConcept);
+    return true;
   }
 
   metadataToFSH(): string {


Test patch:
diff --git a/test/export/CodeSystemExporter.test.ts b/test/export/CodeSystemExporter.test.ts
index 21ff5e2..159712b 100644
--- a/test/export/CodeSystemExporter.test.ts
+++ b/test/export/CodeSystemExporter.test.ts
@@ -767,5 +767,62 @@ describe('CodeSystemExporter', () => {
         /AssignmentRule.*FshCodeSystem.*File: Value\.fsh.*Line: 1 - 3.*Applied in File: Insert\.fsh.*Applied on Line: 5 - 7/s
       );
     });
+
+    it('should maintain concept order when adding concepts from an insert rule', () => {
+      // RuleSet: Bar
+      // * #lion
+      //
+      // CodeSystem: Foo
+      // * #bear
+      // * insert Bar
+      // * #alligator
+      const lionRule = new ConceptRule('lion');
+      ruleSet.rules.push(lionRule);
+
+      const bearRule = new ConceptRule('bear');
+      const insertRule = new InsertRule('');
+      insertRule.ruleSet = 'Bar';
+      const alligatorRule = new ConceptRule('alligator');
+      cs.rules.push(bearRule, insertRule, alligatorRule);
+
+      const exported = exporter.exportCodeSystem(cs);
+      expect(exported.concept[0].code).toBe('bear');
+      expect(exported.concept[1].code).toBe('lion');
+      expect(exported.concept[2].code).toBe('alligator');
+      expect(exported.count).toBe(3);
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
+
+    it('should not add concepts from an insert rule that are duplicates of existing concepts', () => {
+      // RuleSet: Bar
+      // * #lion "Lion"
+      // * #bear "Extra Bear"
+      //
+      // CodeSystem: Foo
+      // * #bear "Regular Bear"
+      // * insert Bar
+      // * #alligator "Alligator"
+      const lionRule = new ConceptRule('lion', 'Lion');
+      const duplicateBear = new ConceptRule('bear', 'Extra Bear')
+        .withFile('RuleSet.fsh')
+        .withLocation([3, 0, 3, 20]);
+      ruleSet.rules.push(lionRule, duplicateBear);
+
+      const bearRule = new ConceptRule('bear', 'Regular Bear');
+      const insertRule = new InsertRule('').withFile('CodeSystem.fsh').withLocation([4, 0, 4, 12]);
+      insertRule.ruleSet = 'Bar';
+      const alligatorRule = new ConceptRule('alligator', 'Alligator');
+      cs.rules.push(bearRule, insertRule, alligatorRule);
+
+      const exported = exporter.exportCodeSystem(cs);
+      expect(exported.concept[0].code).toBe('bear');
+      expect(exported.concept[0].display).toBe('Regular Bear');
+      expect(exported.concept[1].code).toBe('lion');
+      expect(exported.concept[2].code).toBe('alligator');
+      expect(exported.count).toBe(3);
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /CodeSystem Foo already contains code bear.*File: RuleSet\.fsh.*Line: 3\D.*Applied in File: CodeSystem\.fsh.*Applied on Line: 4\D*/s
+      );
+    });
   });
 });