Repository: FHIR/sushi
Pull request #273 (2020-03-09 17:16:31)

Export RuleSets
This addresses https://standardhealthrecord.atlassian.net/browse/CIMPL-286 by adding exporting of `RuleSet`s. On an instance, a `RuleSet` can only apply `FixedValueRule`s, and we log an error when the rule is not of that type. To help the user track down errors that come from `RuleSet` rules, the rules have an added `appliedLocation` and `appliedFile` field in addition to the `location` and `file` fields that are on all `FSHEntity`s. Rules are added to the beginning of the `rules` array on the `Profile`, `Extension`, or `Instance` on which the `RuleSet` is applied. The rules are then processed as if they were normal rules.

cmoesel: Also... update your branch!
ngfreiter: Ok @cmoesel updated from your comments and merged master

Patch:
diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts
index fbc4e00..6782f3b 100644
--- a/src/export/InstanceExporter.ts
+++ b/src/export/InstanceExporter.ts
@@ -6,7 +6,8 @@ import {
   setPropertyOnInstance,
   replaceReferences,
   replaceField,
-  splitOnPathPeriods
+  splitOnPathPeriods,
+  applyMixinRules
 } from '../fhirtypes/common';
 import { InstanceOfNotDefinedError } from '../errors/InstanceOfNotDefinedError';
 import { Package } from '.';
@@ -278,6 +279,7 @@ export class InstanceExporter {
     if (instanceOfStructureDefinition.derivation === 'constraint') {
       instanceDef.meta = { profile: [instanceOfStructureDefinition.url] };
     }
+    applyMixinRules(fshDefinition, this.tank);
     // Set Fixed values based on the FSH rules and the Structure Definition
     instanceDef = this.setFixedValues(fshDefinition, instanceDef, instanceOfStructureDefinition);
     instanceDef.validateId(fshDefinition.sourceInfo);
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index 995dfbc..4aa8e76 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -18,7 +18,7 @@ import {
   ObeysRule
 } from '../fshtypes/rules';
 import { logger, Type, Fishable, Metadata, MasterFisher } from '../utils';
-import { replaceReferences, splitOnPathPeriods } from '../fhirtypes/common';
+import { replaceReferences, splitOnPathPeriods, applyMixinRules } from '../fhirtypes/common';
 import { Package } from './Package';
 
 /**
@@ -430,6 +430,7 @@ export class StructureDefinitionExporter implements Fishable {
       this.pkg.profiles.push(structDef);
     }
 
+    applyMixinRules(fshDefinition, this.tank);
     this.preprocessStructureDefinition(fshDefinition, structDef.type === 'Extension');
 
     this.setRules(structDef, fshDefinition);
diff --git a/src/fhirtypes/common.ts b/src/fhirtypes/common.ts
index 9ea66da..077028d 100644
--- a/src/fhirtypes/common.ts
+++ b/src/fhirtypes/common.ts
@@ -6,8 +6,16 @@ import {
   ValueSet,
   CodeSystem
 } from '.';
-import { FixedValueRule } from '../fshtypes/rules';
-import { FshReference, Instance, SourceInfo, FshCode } from '../fshtypes';
+import { FixedValueRule, Rule } from '../fshtypes/rules';
+import {
+  FshReference,
+  Instance,
+  SourceInfo,
+  FshCode,
+  Profile,
+  Extension,
+  RuleSet
+} from '../fshtypes';
 import { FSHTank } from '../import';
 import { Type, Fishable } from '../utils/Fishable';
 import cloneDeep = require('lodash/cloneDeep');
@@ -205,6 +213,43 @@ export function replaceField(
   }
 }
 
+/**
+ * Adds Mixin rules onto a Profile, Extension, or Instance
+ * @param {Profile | Extension | Instance} fshDefinition - The definition to apply mixin rules on
+ * @param {FSHTank} tank - The FSHTank containing the fshDefinition
+ */
+export function applyMixinRules(
+  fshDefinition: Profile | Extension | Instance,
+  tank: FSHTank
+): void {
+  // Rules are added to beginning of rules array, so add the last mixin rules first
+  const mixedInRules: Rule[] = [];
+  fshDefinition.mixins.forEach(mixinName => {
+    const ruleSet = tank.fish(mixinName, Type.RuleSet) as RuleSet;
+    if (ruleSet) {
+      ruleSet.rules.forEach(r => {
+        // Record source information of Profile/Extension/Instance on which Mixin is applied
+        r.sourceInfo.appliedFile = fshDefinition.sourceInfo.file;
+        r.sourceInfo.appliedLocation = fshDefinition.sourceInfo.location;
+      });
+      const rules = ruleSet.rules.filter(r => {
+        if (fshDefinition instanceof Instance && !(r instanceof FixedValueRule)) {
+          logger.error(
+            'Rules applied by mixins to an instance must fix a value. Other rules are ignored.',
+            r.sourceInfo
+          );
+          return false;
+        }
+        return true;
+      });
+      mixedInRules.push(...rules);
+    } else {
+      logger.error(`Unable to find definition for RuleSet ${mixinName}.`, fshDefinition.sourceInfo);
+    }
+  });
+  fshDefinition.rules = [...mixedInRules, ...fshDefinition.rules];
+}
+
 const nameRegex = /^[A-Z]([A-Za-z0-9_]){0,254}$/;
 
 export class HasName {
diff --git a/src/fshtypes/FshEntity.ts b/src/fshtypes/FshEntity.ts
index 16801ad..3b89cc0 100644
--- a/src/fshtypes/FshEntity.ts
+++ b/src/fshtypes/FshEntity.ts
@@ -22,11 +22,32 @@ export abstract class FshEntity {
     this.sourceInfo.file = file;
     return this;
   }
+
+  public withAppliedLocation(location: TextLocation | [number, number, number, number]): this {
+    if (Array.isArray(location)) {
+      this.sourceInfo.appliedLocation = {
+        startLine: location[0],
+        startColumn: location[1],
+        endLine: location[2],
+        endColumn: location[3]
+      };
+    } else {
+      this.sourceInfo.appliedLocation = location;
+    }
+    return this;
+  }
+
+  public withAppliedFile(file: string): this {
+    this.sourceInfo.appliedFile = file;
+    return this;
+  }
 }
 
 export type SourceInfo = {
   file?: string;
   location?: TextLocation;
+  appliedFile?: string;
+  appliedLocation?: TextLocation;
 };
 
 export type TextLocation = {
diff --git a/src/fshtypes/RuleSet.ts b/src/fshtypes/RuleSet.ts
index d80366a..402dbf8 100644
--- a/src/fshtypes/RuleSet.ts
+++ b/src/fshtypes/RuleSet.ts
@@ -11,4 +11,12 @@ export class RuleSet extends FshEntity {
     super();
     this.rules = [];
   }
+
+  /**
+   * Read only property for id that just returns the name of the mixin
+   * This was added so that all types that are returned by FSHTank.fish have an id that can be accessed
+   */
+  get id() {
+    return this.name;
+  }
 }
diff --git a/src/import/FSHTank.ts b/src/import/FSHTank.ts
index 2848ade..b269059 100644
--- a/src/import/FSHTank.ts
+++ b/src/import/FSHTank.ts
@@ -1,5 +1,13 @@
 import { FSHDocument } from './FSHDocument';
-import { Profile, Extension, Instance, FshValueSet, FshCodeSystem, Invariant } from '../fshtypes';
+import {
+  Profile,
+  Extension,
+  Instance,
+  FshValueSet,
+  FshCodeSystem,
+  Invariant,
+  RuleSet
+} from '../fshtypes';
 import flatMap from 'lodash/flatMap';
 import { Config } from '../fshtypes/Config';
 import { Type, Metadata, Fishable } from '../utils/Fishable';
@@ -67,6 +75,14 @@ export class FSHTank implements Fishable {
     return flatMap(this.docs, doc => Array.from(doc.invariants.values()));
   }
 
+  /**
+   * Gets all ruleSets in the tank
+   * @returns {RuleSet[]}
+   */
+  public getAllRuleSets(): RuleSet[] {
+    return flatMap(this.docs, doc => Array.from(doc.ruleSets.values()));
+  }
+
   /**
    * Finds the alias in the tank, if it exists
    * @param {string} name - The name of the alias we're looking for
@@ -83,7 +99,15 @@ export class FSHTank implements Fishable {
   fish(
     item: string,
     ...types: Type[]
-  ): Profile | Extension | FshValueSet | FshCodeSystem | Instance | Invariant | undefined {
+  ):
+    | Profile
+    | Extension
+    | FshValueSet
+    | FshCodeSystem
+    | Instance
+    | Invariant
+    | RuleSet
+    | undefined {
     // Resolve alias if necessary
     item = this.resolveAlias(item) ?? item;
 
@@ -95,7 +119,8 @@ export class FSHTank implements Fishable {
         Type.ValueSet,
         Type.CodeSystem,
         Type.Instance,
-        Type.Invariant
+        Type.Invariant,
+        Type.RuleSet
       ];
     }
 
@@ -140,6 +165,9 @@ export class FSHTank implements Fishable {
         case Type.Invariant:
           result = this.getAllInvariants().find(i => i.name === item);
           break;
+        case Type.RuleSet:
+          result = this.getAllRuleSets().find(r => r.name === item);
+          break;
         case Type.Resource:
         case Type.Type:
         default:
diff --git a/src/utils/FSHLogger.ts b/src/utils/FSHLogger.ts
index a92492f..f6fccd8 100644
--- a/src/utils/FSHLogger.ts
+++ b/src/utils/FSHLogger.ts
@@ -15,6 +15,17 @@ const withLocation = format(info => {
     }
     delete info.location;
   }
+  if (info.appliedFile) {
+    info.message += `\n  Applied in File: ${info.appliedFile}`;
+    delete info.appliedFile;
+  }
+  if (info.appliedLocation) {
+    info.message += `\n  Applied on Line: ${info.appliedLocation.startLine}`;
+    if (info.appliedLocation.endLine !== info.appliedLocation.startLine) {
+      info.message += ` - ${info.appliedLocation.endLine}`;
+    }
+    delete info.appliedLocation;
+  }
   return info;
 });
 
diff --git a/src/utils/Fishable.ts b/src/utils/Fishable.ts
index 0620143..a005751 100644
--- a/src/utils/Fishable.ts
+++ b/src/utils/Fishable.ts
@@ -4,7 +4,8 @@ export enum Type {
   ValueSet,
   CodeSystem,
   Instance,
-  Invariant,
+  Invariant, // NOTE: only defined in FSHTanks, not FHIR defs
+  RuleSet, // NOTE: only defined in FSHTanks, not FHIR defs
   Resource, // NOTE: only defined in FHIR defs, not FSHTanks
   Type // NOTE: only defined in FHIR defs, not FSHTanks
 }


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index a37d3ae..bb8f935 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -7,7 +7,8 @@ import {
   FshCode,
   FshReference,
   Extension,
-  FshCodeSystem
+  FshCodeSystem,
+  RuleSet
 } from '../../src/fshtypes';
 import { FixedValueRule, ContainsRule, CardRule, OnlyRule } from '../../src/fshtypes/rules';
 import { loggerSpy, TestFisher } from '../testhelpers';
@@ -1136,4 +1137,95 @@ describe('InstanceExporter', () => {
       expect(loggerSpy.getLastMessage('error')).toMatch(/File: Unmeasurable\.fsh.*Line: 3\D*/s);
     });
   });
+
+  describe('#Mixins', () => {
+    let instance: Instance;
+    let mixin: RuleSet;
+
+    beforeEach(() => {
+      instance = new Instance('Foo').withFile('Instance.fsh').withLocation([5, 6, 7, 16]);
+      instance.instanceOf = 'Patient';
+      doc.instances.set(instance.name, instance);
+
+      mixin = new RuleSet('Bar');
+      doc.ruleSets.set(mixin.name, mixin);
+      instance.mixins.push('Bar');
+    });
+
+    it('should apply rules from a single mixin', () => {
+      const rule = new FixedValueRule('active');
+      rule.fixedValue = true;
+      mixin.rules.push(rule);
+
+      const exported = exporter.exportInstance(instance);
+      expect(exported.active).toBe(true);
+    });
+
+    it('should apply rules from multiple mixins in the correct order', () => {
+      const rule1 = new FixedValueRule('active');
+      rule1.fixedValue = true;
+      mixin.rules.push(rule1);
+
+      const mixin2 = new RuleSet('Baz');
+      doc.ruleSets.set(mixin2.name, mixin2);
+      const rule2 = new FixedValueRule('active');
+      rule2.fixedValue = false;
+      mixin2.rules.push(rule2);
+      instance.mixins.push('Baz');
+
+      const exported = exporter.exportInstance(instance);
+      expect(exported.active).toBe(false);
+    });
+
+    it('should emit an error when the path is not found on a mixin rule', () => {
+      const rule = new FixedValueRule('activez').withFile('Mixin.fsh').withLocation([1, 2, 3, 12]);
+      rule.fixedValue = true;
+      mixin.rules.push(rule);
+
+      exporter.exportInstance(instance);
+      expect(loggerSpy.getLastMessage('error')).toMatch(/activez/);
+      expect(loggerSpy.getLastMessage()).toMatch(/File: Mixin\.fsh.*Line: 1 - 3\D*/s);
+      expect(loggerSpy.getLastMessage()).toMatch(
+        /Applied in File: Instance\.fsh.*Applied on Line: 5 - 7\D*/s
+      );
+    });
+
+    it('should emit an error when applying an invalid mixin rule', () => {
+      const rule = new FixedValueRule('active').withFile('Mixin.fsh').withLocation([1, 2, 3, 12]);
+      rule.fixedValue = 'some string';
+      mixin.rules.push(rule);
+
+      exporter.exportInstance(instance);
+      expect(loggerSpy.getLastMessage('error')).toMatch(/some string/);
+      expect(loggerSpy.getLastMessage()).toMatch(/File: Mixin\.fsh.*Line: 1 - 3\D*/s);
+      expect(loggerSpy.getLastMessage()).toMatch(
+        /Applied in File: Instance\.fsh.*Applied on Line: 5 - 7\D*/s
+      );
+    });
+
+    it('should emit an error when a mixin cannot be found', () => {
+      instance.mixins.push('Barz');
+
+      exporter.exportInstance(instance);
+
+      expect(loggerSpy.getLastMessage('error')).toMatch(/Barz/);
+      expect(loggerSpy.getLastMessage('error')).toMatch(/File: Instance\.fsh.*Line: 5 - 7\D*/s);
+    });
+
+    it('should emit an error when a mixin applies a non-fixed value rule', () => {
+      const rule = new CardRule('active').withFile('Mixin.fsh').withLocation([1, 2, 3, 12]);
+      rule.min = 0;
+      rule.max = '1';
+      mixin.rules.push(rule);
+
+      exporter.exportInstance(instance);
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Rules applied by mixins to an instance must fix a value. Other rules are ignored/
+      );
+      expect(loggerSpy.getLastMessage()).toMatch(/File: Mixin\.fsh.*Line: 1 - 3\D*/s);
+      expect(loggerSpy.getLastMessage()).toMatch(
+        /Applied in File: Instance\.fsh.*Applied on Line: 5 - 7\D*/s
+      );
+    });
+  });
 });
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index d538235..faae72f 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -9,7 +9,8 @@ import {
   Instance,
   FshValueSet,
   FshCodeSystem,
-  Invariant
+  Invariant,
+  RuleSet
 } from '../../src/fshtypes';
 import {
   CardRule,
@@ -2506,4 +2507,92 @@ describe('StructureDefinitionExporter', () => {
     expect(pkg.profiles.length).toBe(1);
     expect(pkg.extensions.length).toBe(1);
   });
+
+  describe('#Mixins', () => {
+    let profile: Profile;
+    let mixin: RuleSet;
+
+    beforeEach(() => {
+      profile = new Profile('Foo').withFile('Profile.fsh').withLocation([5, 6, 7, 16]);
+      profile.parent = 'Patient';
+      doc.profiles.set(profile.name, profile);
+
+      mixin = new RuleSet('Bar');
+      doc.ruleSets.set(mixin.name, mixin);
+      profile.mixins.push('Bar');
+    });
+
+    it('should apply rules from a single mixin', () => {
+      const nameRule = new CaretValueRule('');
+      nameRule.caretPath = 'title';
+      nameRule.value = 'Wow fancy';
+      mixin.rules.push(nameRule);
+
+      exporter.exportStructDef(profile);
+      const sd = pkg.profiles[0];
+
+      expect(sd.title).toBe('Wow fancy');
+    });
+
+    it('should apply rules from multiple mixins in the correct order', () => {
+      const nameRule1 = new CaretValueRule('');
+      nameRule1.caretPath = 'title';
+      nameRule1.value = 'Wow fancy';
+      mixin.rules.push(nameRule1);
+
+      const mixin2 = new RuleSet('Baz');
+      doc.ruleSets.set(mixin2.name, mixin2);
+      const nameRule2 = new CaretValueRule('');
+      nameRule2.caretPath = 'title';
+      nameRule2.value = 'Wow fancier title';
+      mixin2.rules.push(nameRule2);
+      profile.mixins.push('Baz');
+
+      exporter.exportStructDef(profile);
+      const sd = pkg.profiles[0];
+
+      expect(sd.title).toBe('Wow fancier title');
+    });
+
+    it('should emit an error when the path is not found on a mixin rule', () => {
+      const nameRule = new CaretValueRule('fakepath')
+        .withFile('Mixin.fsh')
+        .withLocation([1, 2, 3, 12]);
+      nameRule.caretPath = 'title';
+      nameRule.value = 'Wow fancy';
+      mixin.rules.push(nameRule);
+
+      exporter.exportStructDef(profile);
+
+      expect(loggerSpy.getLastMessage('error')).toMatch(/fakepath/);
+      expect(loggerSpy.getLastMessage()).toMatch(/File: Mixin\.fsh.*Line: 1 - 3\D*/s);
+      expect(loggerSpy.getLastMessage()).toMatch(
+        /Applied in File: Profile\.fsh.*Applied on Line: 5 - 7\D*/s
+      );
+    });
+
+    it('should emit an error when applying an invalid mixin rule', () => {
+      const cardRule = new CardRule('active').withFile('Mixin.fsh').withLocation([1, 2, 3, 12]);
+      cardRule.max = '2';
+      cardRule.min = 0;
+      mixin.rules.push(cardRule);
+
+      exporter.exportStructDef(profile);
+
+      expect(loggerSpy.getLastMessage('error')).toMatch(/0\.\.2.* 0\.\.1/);
+      expect(loggerSpy.getLastMessage()).toMatch(/File: Mixin\.fsh.*Line: 1 - 3\D*/s);
+      expect(loggerSpy.getLastMessage()).toMatch(
+        /Applied in File: Profile\.fsh.*Applied on Line: 5 - 7\D*/s
+      );
+    });
+
+    it('should emit an error when a mixin cannot be found', () => {
+      profile.mixins.push('Barz');
+
+      exporter.exportStructDef(profile);
+
+      expect(loggerSpy.getLastMessage('error')).toMatch(/Barz/);
+      expect(loggerSpy.getLastMessage('error')).toMatch(/File: Profile\.fsh.*Line: 5 - 7\D*/s);
+    });
+  });
 });
diff --git a/test/import/FSHTank.test.ts b/test/import/FSHTank.test.ts
index 0428475..600165a 100644
--- a/test/import/FSHTank.test.ts
+++ b/test/import/FSHTank.test.ts
@@ -6,7 +6,8 @@ import {
   Instance,
   FshCodeSystem,
   Invariant,
-  FshCode
+  FshCode,
+  RuleSet
 } from '../../src/fshtypes';
 import { Type, Metadata } from '../../src/utils/Fishable';
 
@@ -43,6 +44,7 @@ describe('FSHTank', () => {
     doc3.invariants.set('Invariant1', new Invariant('Invariant1'));
     doc3.invariants.get('Invariant1').description = 'first invariant';
     doc3.invariants.get('Invariant1').severity = new FshCode('error');
+    doc3.ruleSets.set('RuleSet1', new RuleSet('RuleSet1'));
 
     tank = new FSHTank([doc1, doc2, doc3], {
       name: 'test',
@@ -58,7 +60,7 @@ describe('FSHTank', () => {
       expect(tank.fish('vs1').name).toBe('ValueSet1');
       expect(tank.fish('cs1').name).toBe('CodeSystem1');
       expect(tank.fish('inst1').name).toBe('Instance1');
-      // not applicable for Invariant
+      // not applicable for Invariant or RuleSet
     });
 
     it('should find valid fish when fishing by name for all types', () => {
@@ -68,6 +70,7 @@ describe('FSHTank', () => {
       expect(tank.fish('CodeSystem2').id).toBe('cs2');
       expect(tank.fish('Instance1').id).toBe('inst1');
       expect(tank.fish('Invariant1').name).toBe('Invariant1');
+      expect(tank.fish('RuleSet1').name).toBe('RuleSet1');
     });
 
     it('should find valid fish when fishing by url for all types', () => {
@@ -75,7 +78,7 @@ describe('FSHTank', () => {
       expect(tank.fish('http://example.org/StructureDefinition/ext1').name).toBe('Extension1');
       expect(tank.fish('http://example.org/ValueSet/vs1').name).toBe('ValueSet1');
       expect(tank.fish('http://example.org/CodeSystem/cs1').name).toBe('CodeSystem1');
-      // not applicable for Instance or Invariant
+      // not applicable for Instance or Invariant or RuleSet
     });
 
     it('should not find fish when fishing by invalid name/id/url', () => {
@@ -92,6 +95,7 @@ describe('FSHTank', () => {
           Type.CodeSystem,
           Type.Instance,
           Type.Invariant,
+          Type.RuleSet,
           Type.Resource,
           Type.Type
         )
@@ -108,6 +112,7 @@ describe('FSHTank', () => {
           Type.CodeSystem,
           Type.Instance,
           Type.Invariant,
+          Type.RuleSet,
           Type.Resource,
           Type.Type
         )
@@ -124,6 +129,7 @@ describe('FSHTank', () => {
           Type.CodeSystem,
           Type.Instance,
           Type.Invariant,
+          Type.RuleSet,
           Type.Resource,
           Type.Type
         )
@@ -140,6 +146,7 @@ describe('FSHTank', () => {
           Type.ValueSet,
           Type.Instance,
           Type.Invariant,
+          Type.RuleSet,
           Type.Resource,
           Type.Type
         )
@@ -156,6 +163,7 @@ describe('FSHTank', () => {
           Type.ValueSet,
           Type.CodeSystem,
           Type.Invariant,
+          Type.RuleSet,
           Type.Resource,
           Type.Type
         )
@@ -172,6 +180,24 @@ describe('FSHTank', () => {
           Type.ValueSet,
           Type.CodeSystem,
           Type.Instance,
+          Type.RuleSet,
+          Type.Resource,
+          Type.Type
+        )
+      ).toBeUndefined();
+    });
+
+    it('should only find ruleSets when ruleSets are requested', () => {
+      expect(tank.fish('RuleSet1', Type.RuleSet).name).toBe('RuleSet1');
+      expect(
+        tank.fish(
+          'RuleSet1',
+          Type.Profile,
+          Type.Extension,
+          Type.ValueSet,
+          Type.CodeSystem,
+          Type.Instance,
+          Type.Invariant,
           Type.Resource,
           Type.Type
         )
@@ -210,13 +236,17 @@ describe('FSHTank', () => {
       id: 'Invariant1', // id will always be name on Invariants
       name: 'Invariant1'
     };
+    const rul1MD: Metadata = {
+      id: 'RuleSet1', // id will always be name for Mixins
+      name: 'RuleSet1'
+    };
     it('should find valid fish metadata when fishing by id for all types', () => {
       expect(tank.fishForMetadata('prf1')).toEqual(prf1MD);
       expect(tank.fishForMetadata('ext1')).toEqual(ext1MD);
       expect(tank.fishForMetadata('vs1')).toEqual(vs1MD);
       expect(tank.fishForMetadata('cs1')).toEqual(cs1MD);
       expect(tank.fishForMetadata('inst1')).toEqual(inst1MD);
-      // not applicable for Invariant
+      // not applicable for Invariant or RuleSet
     });
 
     it('should find valid fish when fishing by name for all types', () => {
@@ -226,6 +256,7 @@ describe('FSHTank', () => {
       expect(tank.fishForMetadata('CodeSystem1')).toEqual(cs1MD);
       expect(tank.fishForMetadata('Instance1')).toEqual(inst1MD);
       expect(tank.fishForMetadata('Invariant1')).toEqual(inv1MD);
+      expect(tank.fishForMetadata('RuleSet1')).toEqual(rul1MD);
     });
 
     it('should find valid fish when fishing by url for all types', () => {
@@ -233,7 +264,7 @@ describe('FSHTank', () => {
       expect(tank.fishForMetadata('http://example.org/StructureDefinition/ext1')).toEqual(ext1MD);
       expect(tank.fishForMetadata('http://example.org/ValueSet/vs1')).toEqual(vs1MD);
       expect(tank.fishForMetadata('http://example.org/CodeSystem/cs1')).toEqual(cs1MD);
-      // not applicable for Instance or Invariant
+      // not applicable for Instance or Invariant or RuleSet
     });
 
     it('should not find fish when fishing by invalid name/id/url', () => {
@@ -250,6 +281,7 @@ describe('FSHTank', () => {
           Type.CodeSystem,
           Type.Instance,
           Type.Invariant,
+          Type.RuleSet,
           Type.Resource,
           Type.Type
         )
@@ -266,6 +298,7 @@ describe('FSHTank', () => {
           Type.CodeSystem,
           Type.Instance,
           Type.Invariant,
+          Type.RuleSet,
           Type.Resource,
           Type.Type
         )
@@ -282,6 +315,7 @@ describe('FSHTank', () => {
           Type.CodeSystem,
           Type.Instance,
           Type.Invariant,
+          Type.RuleSet,
           Type.Resource,
           Type.Type
         )
@@ -298,6 +332,7 @@ describe('FSHTank', () => {
           Type.ValueSet,
           Type.Instance,
           Type.Invariant,
+          Type.RuleSet,
           Type.Resource,
           Type.Type
         )
@@ -314,6 +349,7 @@ describe('FSHTank', () => {
           Type.ValueSet,
           Type.CodeSystem,
           Type.Invariant,
+          Type.RuleSet,
           Type.Resource,
           Type.Type
         )
@@ -330,6 +366,24 @@ describe('FSHTank', () => {
           Type.ValueSet,
           Type.CodeSystem,
           Type.Instance,
+          Type.RuleSet,
+          Type.Resource,
+          Type.Type
+        )
+      ).toBeUndefined();
+    });
+
+    it('should only find ruleSets when ruleSets are requested', () => {
+      expect(tank.fishForMetadata('RuleSet1', Type.RuleSet)).toEqual(rul1MD);
+      expect(
+        tank.fishForMetadata(
+          'Mixin1',
+          Type.Profile,
+          Type.Extension,
+          Type.ValueSet,
+          Type.CodeSystem,
+          Type.Instance,
+          Type.Invariant,
           Type.Resource,
           Type.Type
         )
@@ -360,6 +414,8 @@ describe('FSHTank', () => {
       expect(tank.fishForFHIR('Instance1')).toBeUndefined();
       expect(tank.fishForFHIR('Invariant1')).toBeUndefined();
       expect(tank.fishForFHIR('Invariant1', Type.Invariant)).toBeUndefined();
+      expect(tank.fishForFHIR('RuleSet1')).toBeUndefined();
+      expect(tank.fishForFHIR('RuleSet1', Type.RuleSet)).toBeUndefined();
     });
   });
 });