Repository: FHIR/sushi
Pull request #260 (2020-02-26 20:15:38)

Apply ObeysRules to Element.constraint
This PR applies any `ObeysRule`s specified on structure definitions onto `Element.constraint`. Any fields specified on the Invariant are added to the constraint, which is then pushed onto the existing list of constraints.

If someone tries to apply an `ObeysRule` using an invariant that was not defined, it logs an error. If someone applies an `ObeysRule` that had previously logged an error (i.e. if the severity is not error), the invariant is still applied, since we have other places where we log errors and still set the technically incorrect values.

Bonus - removed some code that had expired according to @cmoesel's expiration date.

cmoesel: I haven't reviewed the PR yet (only the comments), but I thought I'd remark on @mint-thompson's comment.  For that second example (with the rules reversed), Mint is right that the snapshot element for `Observation.value[x]:valueString` will technically be incorrect because it won't have the invariant.  But... this is an overarching problem for pretty much *all* our rules.  The same issue exists for cardinality rules, flag rules, only rules, etc.  So... I wouldn't hold up this PR for that reason; as it's probably best to resolve that on the systemic level-- probably by doing one of the following:
* re-ordering rules before they're applied such that all rules related to a sliced element come _before_ the rules related to specific slices of that element
* when applying rules to a sliced element (or its child), also apply those rules to any specific slices of that element (or their children).

The other wrinkle here (which is a bit of a saving grace) is that I think in most cases, the `differential` will actually be fine -- and when run through the IG Publisher, the publisher will correctly build the snapshot such that the rules get correctly applied to all slices.
jafeltra: I hadn't considered @mint-thompson's test case, so I'm glad it was brought up. I think it seems like from @cmoesel's comment I won't add specific handling for that case as part of this PR, since it seems like something we should handle at a broader level.

Patch:
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index 1385567..f40c476 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -1,5 +1,5 @@
 import { StructureDefinition, ElementDefinitionBindingStrength } from '../fhirtypes';
-import { Profile, Extension } from '../fshtypes';
+import { Profile, Extension, Invariant } from '../fshtypes';
 import { FSHTank } from '../import';
 import { ParentNotDefinedError } from '../errors';
 import {
@@ -9,7 +9,8 @@ import {
   OnlyRule,
   ValueSetRule,
   ContainsRule,
-  CaretValueRule
+  CaretValueRule,
+  ObeysRule
 } from '../fshtypes/rules';
 import { logger, Type, Fishable, Metadata, MasterFisher } from '../utils';
 import { replaceReferences, splitOnPathPeriods } from '../fhirtypes/common';
@@ -154,6 +155,16 @@ export class StructureDefinitionExporter implements Fishable {
             } else {
               structDef.setInstancePropertyByPath(rule.caretPath, rule.value, this);
             }
+          } else if (rule instanceof ObeysRule) {
+            const invariant = this.tank.fish(rule.invariant, Type.Invariant) as Invariant;
+            if (!invariant) {
+              logger.error(
+                `Cannot apply ${rule.invariant} constraint on ${structDef.id} because it was never defined.`,
+                rule.sourceInfo
+              );
+            } else {
+              element.applyConstraint(invariant, structDef.url);
+            }
           }
         } catch (e) {
           logger.error(e.message, rule.sourceInfo);
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index d82d0fc..5f61c3f 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -3,7 +3,7 @@ import sax = require('sax');
 import { minify } from 'html-minifier';
 import { StructureDefinition } from './StructureDefinition';
 import { CodeableConcept, Coding, Quantity, Ratio, Reference } from './dataTypes';
-import { FshCode, FshRatio, FshQuantity, FshReference } from '../fshtypes';
+import { FshCode, FshRatio, FshQuantity, FshReference, Invariant } from '../fshtypes';
 import { FixedValueType, OnlyRule } from '../fshtypes/rules';
 import {
   BindingStrengthError,
@@ -344,6 +344,24 @@ export class ElementDefinition {
       .join('.');
   }
 
+  /**
+   * Apply invariant to the Element.constraint
+   * @see {@link http://hl7.org/fhir/R4/elementdefinition-definitions.html#ElementDefinition.constraint}
+   * @param invariant The invariant to be applied to the constraint
+   * @param source Source URL for the constraint
+   */
+  applyConstraint(invariant: Invariant, source?: string): void {
+    const constraint: ElementDefinitionConstraint = {
+      ...(invariant.name && { key: invariant.name }),
+      ...(invariant.severity && { severity: invariant.severity.code }),
+      ...(invariant.description && { human: invariant.description }),
+      ...(invariant.expression && { expression: invariant.expression }),
+      ...(invariant.xpath && { xpath: invariant.xpath }),
+      ...(source && { source })
+    };
+    this.constraint.push(constraint);
+  }
+
   /**
    * This function sets an instance property of an ED if possible
    * @param {string} path - The path to the ElementDefinition to fix
@@ -1649,98 +1667,6 @@ export class ElementDefinition {
     return slice;
   }
 
-  // NOTE: These are functions we used previously but may not be needed in Sushi.
-  // Commenting out for now, but if we still don't need them by Jan 2020, delete them!
-  //
-  // /**
-  //  * Gets a Map of the slices associated with this element, where the key is the slice name and the value is the
-  //  * ElementDefinition representing the slice.  If there are no slices, it will return an empty Map.
-  //  * @returns {Map<string,ElementDefinition>} the map containing this element's slices
-  //  */
-  // getSliceMap(): Map<string, ElementDefinition> {
-  //   const sliceMap = new Map();
-  //
-  //   // Find all the slice roots, iterate them, and get their children
-  //   let re = new RegExp(`^${escapeRegExp(this.id)}:[^.]+$`);
-  //   // TODO: For now we don't support choices that themselves are in a slice (e.g., assume choice id ends with [x])
-  //   if (this.id.endsWith('[x]')) {
-  //     re = new RegExp(`^${escapeRegExp(this.id.slice(0, -3))}[A-Z][^:.]*:[^.]+$`);
-  //   }
-  //   this.structDef.elements
-  //     .filter(e => re.test(e.id))
-  //     .forEach(e => {
-  //       const name = e.sliceName;
-  //       if (name == null) {
-  //         // TODO: log an error
-  //         return;
-  //       }
-  //       sliceMap.set(name, e);
-  //     });
-  //
-  //   return sliceMap;
-  // }
-  //
-  // /**
-  //  * Replaces the the sliced element with a specific slice, removing all other slices.  If sliceNameToKeep is null or
-  //  * undefined, it removes the discriminator from this element and removes all existing slices.  If this element is
-  //  * not sliced, returns itself.
-  //  * @param {string} sliceNameToKeep - the name of the slice to keep in place of this element
-  //  * @returns {ElementDefinition} the remaining element after unslicing (usually corresponding to sliceNameToKeep)
-  //  */
-  // unSliceIt(sliceNameToKeep: string): ElementDefinition {
-  //   if (!this.slicing) {
-  //     return this;
-  //   }
-  //
-  //   // Remove all slices except the one matching sliceNameToKeep
-  //   const sliceMap = this.getSliceMap();
-  //   for (const name of sliceMap.keys()) {
-  //     if (name !== sliceNameToKeep) {
-  //       sliceMap.get(name).detach();
-  //     }
-  //   }
-  //
-  //   // If sliceNameToKeep was named and exists, detach *this* slice and return kept slice
-  //   if (sliceNameToKeep != null && sliceMap.has(sliceNameToKeep)) {
-  //     this.detach();
-  //     const keeper = sliceMap.get(sliceNameToKeep);
-  //     const oldKeeperID = keeper.id;
-  //     const keeperChildren = keeper.children();
-  //     keeper.id = keeper.id.slice(0, keeper.id.lastIndexOf(':'));
-  //     keeperChildren.forEach(c => (c.id = c.id.replace(oldKeeperID, keeper.id)));
-  //     keeper.sliceName = undefined;
-  //     return keeper;
-  //   }
-  //
-  //   // No slice to keep, so keep and return this instead
-  //   this.slicing = undefined;
-  //   return this;
-  // }
-  //
-  // /**
-  //  * Removes this element, and optionally its children, from its StructureDefinition so it is no longer recognized as an
-  //  * element of the StructureDefinition.
-  //  * @param {boolean} [detachChildren=true] - indicates if this element's children should also be detached from the
-  //  *   StructureDefinition
-  //  * @returns {ElementDefinition[]} the array of ElementDefinitions that were detached from the StructureDefinition
-  //  */
-  // detach(detachChildren: boolean = true): ElementDefinition[] {
-  //   const detached = [];
-  //   const toDetach = [this];
-  //   if (detachChildren) {
-  //     // @ts-ignore
-  //     toDetach.push(...this.children());
-  //   }
-  //   for (const el of toDetach) {
-  //     const i = this.structDef.elements.findIndex(e => e === el);
-  //     if (i !== -1) {
-  //       detached.push(el);
-  //       this.structDef.elements.splice(i, 1);
-  //     }
-  //   }
-  //   return detached;
-  // }
-
   /**
    * Clones the current ElementDefinition, optionally clearing the stored "original" (clears it by default)
    * @param {boolean} [clearOriginal=true] - indicates if the stored "original" should be cleared
diff --git a/src/fshtypes/Invariant.ts b/src/fshtypes/Invariant.ts
index 68dc392..167b40c 100644
--- a/src/fshtypes/Invariant.ts
+++ b/src/fshtypes/Invariant.ts
@@ -16,4 +16,12 @@ export class Invariant extends FshEntity {
   constructor(public name: string) {
     super();
   }
+
+  /**
+   * Read only property for id that just returns the name of the invariant
+   * 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 fb81945..2848ade 100644
--- a/src/import/FSHTank.ts
+++ b/src/import/FSHTank.ts
@@ -1,5 +1,5 @@
 import { FSHDocument } from './FSHDocument';
-import { Profile, Extension, Instance, FshValueSet, FshCodeSystem } from '../fshtypes';
+import { Profile, Extension, Instance, FshValueSet, FshCodeSystem, Invariant } from '../fshtypes';
 import flatMap from 'lodash/flatMap';
 import { Config } from '../fshtypes/Config';
 import { Type, Metadata, Fishable } from '../utils/Fishable';
@@ -59,6 +59,14 @@ export class FSHTank implements Fishable {
     return flatMap(this.docs, doc => Array.from(doc.codeSystems.values()));
   }
 
+  /**
+   * Gets all invariants in the tank
+   * @returns {Invariant[]}
+   */
+  public getAllInvariants(): Invariant[] {
+    return flatMap(this.docs, doc => Array.from(doc.invariants.values()));
+  }
+
   /**
    * Finds the alias in the tank, if it exists
    * @param {string} name - The name of the alias we're looking for
@@ -75,13 +83,20 @@ export class FSHTank implements Fishable {
   fish(
     item: string,
     ...types: Type[]
-  ): Profile | Extension | FshValueSet | FshCodeSystem | Instance | undefined {
+  ): Profile | Extension | FshValueSet | FshCodeSystem | Instance | Invariant | undefined {
     // Resolve alias if necessary
     item = this.resolveAlias(item) ?? item;
 
     // No types passed in means to search ALL supported types
     if (types.length === 0) {
-      types = [Type.Profile, Type.Extension, Type.ValueSet, Type.CodeSystem, Type.Instance];
+      types = [
+        Type.Profile,
+        Type.Extension,
+        Type.ValueSet,
+        Type.CodeSystem,
+        Type.Instance,
+        Type.Invariant
+      ];
     }
 
     for (const t of types) {
@@ -122,6 +137,9 @@ export class FSHTank implements Fishable {
         case Type.Instance:
           result = this.getAllInstances().find(i => i.name === item || i.id === item);
           break;
+        case Type.Invariant:
+          result = this.getAllInvariants().find(i => i.name === item);
+          break;
         case Type.Resource:
         case Type.Type:
         default:
diff --git a/src/utils/Fishable.ts b/src/utils/Fishable.ts
index e765879..0620143 100644
--- a/src/utils/Fishable.ts
+++ b/src/utils/Fishable.ts
@@ -4,6 +4,7 @@ export enum Type {
   ValueSet,
   CodeSystem,
   Instance,
+  Invariant,
   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/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 096106e..20f399e 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -8,7 +8,8 @@ import {
   FshReference,
   Instance,
   FshValueSet,
-  FshCodeSystem
+  FshCodeSystem,
+  Invariant
 } from '../../src/fshtypes';
 import {
   CardRule,
@@ -17,7 +18,8 @@ import {
   ValueSetRule,
   FixedValueRule,
   ContainsRule,
-  CaretValueRule
+  CaretValueRule,
+  ObeysRule
 } from '../../src/fshtypes/rules';
 import { loggerSpy, TestFisher } from '../testhelpers';
 import { ElementDefinitionType } from '../../src/fhirtypes';
@@ -1519,6 +1521,89 @@ describe('StructureDefinitionExporter', () => {
     expect(loggerSpy.getLastMessage()).toMatch(/File: InvalidValue\.fsh.*Line: 6\D*/s);
   });
 
+  // ObeysRule
+  it('should apply an ObeysRule at the specified path', () => {
+    const profile = new Profile('Foo');
+    profile.parent = 'Observation';
+    doc.profiles.set(profile.name, profile);
+
+    const invariant = new Invariant('MyInvariant');
+    invariant.description = 'My important invariant';
+    invariant.severity = new FshCode('error');
+    doc.invariants.set(invariant.name, invariant);
+
+    const rule = new ObeysRule('value[x]');
+    rule.invariant = 'MyInvariant';
+    profile.rules.push(rule); // * value[x] obeys MyInvariant
+
+    exporter.exportStructDef(profile);
+    const sd = pkg.profiles[0];
+    const baseStructDef = fisher.fishForStructureDefinition('Observation');
+
+    const baseValueX = baseStructDef.findElement('Observation.value[x]');
+    const changedValueX = sd.findElement('Observation.value[x]');
+
+    expect(baseValueX.constraint).toHaveLength(1);
+    expect(changedValueX.constraint).toHaveLength(2);
+    expect(changedValueX.constraint[1].key).toEqual(invariant.name);
+  });
+
+  it('should apply an ObeysRule to the base element when not path specified', () => {
+    const profile = new Profile('Foo');
+    profile.parent = 'Observation';
+    doc.profiles.set(profile.name, profile);
+
+    const invariant = new Invariant('MyInvariant');
+    invariant.description = 'My important invariant';
+    invariant.severity = new FshCode('error');
+    doc.invariants.set(invariant.name, invariant);
+
+    const rule = new ObeysRule('');
+    rule.invariant = 'MyInvariant';
+    profile.rules.push(rule); // * obeys MyInvariant
+
+    exporter.exportStructDef(profile);
+    const sd = pkg.profiles[0];
+    const baseStructDef = fisher.fishForStructureDefinition('Observation');
+
+    const baseElement = baseStructDef.findElement('Observation');
+    const changedElement = sd.findElement('Observation');
+
+    expect(baseElement.constraint).toHaveLength(7);
+    expect(changedElement.constraint).toHaveLength(8);
+    expect(changedElement.constraint[7].key).toEqual(invariant.name);
+  });
+
+  it('should not apply an ObeysRule on an invariant that does not exist', () => {
+    const profile = new Profile('Foo');
+    profile.parent = 'Observation';
+    doc.profiles.set(profile.name, profile);
+
+    const invariant = new Invariant('MyRealInvariant');
+    invariant.description = 'A very real invariant';
+    invariant.severity = new FshCode('error');
+    doc.invariants.set(invariant.name, invariant);
+
+    const rule = new ObeysRule('value[x]').withFile('FooProfile.fsh').withLocation([4, 7, 4, 15]);
+    rule.invariant = 'MyFakeInvariant';
+    profile.rules.push(rule);
+
+    exporter.exportStructDef(profile);
+    const sd = pkg.profiles[0];
+    const baseStructDef = fisher.fishForStructureDefinition('Observation');
+
+    const baseValueX = baseStructDef.findElement('Observation.value[x]');
+    const changedValueX = sd.findElement('Observation.value[x]');
+
+    expect(baseValueX.constraint).toHaveLength(1);
+    expect(changedValueX.constraint).toHaveLength(1);
+    expect(loggerSpy.getLastMessage()).toMatch(
+      /Cannot apply MyFakeInvariant constraint on Foo because it was never defined./s
+    );
+    expect(loggerSpy.getLastMessage()).toMatch(/File: FooProfile\.fsh.*Line: 4\D*/s);
+    expect(loggerSpy.getAllLogs('error')).toHaveLength(1);
+  });
+
   // Extension preprocessing
   it('should zero out Extension.value[x] when Extension.extension is used', () => {
     const extension = new Extension('MyInferredComplexExtension');
diff --git a/test/fhirtypes/ElementDefinition.test.ts b/test/fhirtypes/ElementDefinition.test.ts
index f945ca8..62874e9 100644
--- a/test/fhirtypes/ElementDefinition.test.ts
+++ b/test/fhirtypes/ElementDefinition.test.ts
@@ -4,6 +4,7 @@ import { ElementDefinition, ElementDefinitionType } from '../../src/fhirtypes/El
 import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition';
 import { TestFisher } from '../testhelpers';
 import { Type } from '../../src/utils/Fishable';
+import { Invariant, FshCode } from '../../src/fshtypes';
 import path from 'path';
 
 describe('ElementDefinition', () => {
@@ -157,6 +158,44 @@ describe('ElementDefinition', () => {
     });
   });
 
+  describe('#applyConstraints', () => {
+    it('should apply a constraint based on an invariant with every field', () => {
+      const invariant = new Invariant('MyInvariant');
+      invariant.description = 'An invariant with all metadata specified.';
+      invariant.expression = 'metadata.exists()';
+      invariant.xpath = 'exists(f:metadata)';
+      invariant.severity = new FshCode('error');
+
+      valueX.applyConstraint(invariant, 'http://example.org/fhir/StructureDefinition/SomeProfile');
+
+      expect(valueX.constraint).toHaveLength(2); // Adds an additional constraint
+      expect(valueX.constraint[1].key).toEqual(invariant.name);
+      expect(valueX.constraint[1]).toStrictEqual({
+        key: invariant.name,
+        severity: invariant.severity.code,
+        human: invariant.description,
+        expression: invariant.expression,
+        xpath: invariant.xpath,
+        source: 'http://example.org/fhir/StructureDefinition/SomeProfile'
+      });
+    });
+
+    it('should apply a constraint with only fields specified on the the invariant', () => {
+      const invariant = new Invariant('SimpleInvariant');
+      invariant.description = 'The simplest invariant';
+      invariant.severity = new FshCode('error');
+      valueX.applyConstraint(invariant);
+
+      expect(valueX.constraint).toHaveLength(2); // Adds an additional constraint
+      expect(valueX.constraint[1].key).toEqual(invariant.name);
+      expect(valueX.constraint[1]).toStrictEqual({
+        key: invariant.name,
+        human: invariant.description,
+        severity: invariant.severity.code
+      });
+    });
+  });
+
   describe('#hasDiff', () => {
     it('should always show a diff for brand new elements w/ no original captured', () => {
       const newElement = new ElementDefinition('newElement');
diff --git a/test/import/FSHTank.test.ts b/test/import/FSHTank.test.ts
index c76e12b..0428475 100644
--- a/test/import/FSHTank.test.ts
+++ b/test/import/FSHTank.test.ts
@@ -1,5 +1,13 @@
 import { FSHTank, FSHDocument } from '../../src/import';
-import { Profile, Extension, FshValueSet, Instance, FshCodeSystem } from '../../src/fshtypes';
+import {
+  Profile,
+  Extension,
+  FshValueSet,
+  Instance,
+  FshCodeSystem,
+  Invariant,
+  FshCode
+} from '../../src/fshtypes';
 import { Type, Metadata } from '../../src/utils/Fishable';
 
 describe('FSHTank', () => {
@@ -32,6 +40,9 @@ describe('FSHTank', () => {
     doc3.instances.set('Instance1', new Instance('Instance1'));
     doc3.instances.get('Instance1').id = 'inst1';
     doc3.instances.get('Instance1').instanceOf = 'Condition';
+    doc3.invariants.set('Invariant1', new Invariant('Invariant1'));
+    doc3.invariants.get('Invariant1').description = 'first invariant';
+    doc3.invariants.get('Invariant1').severity = new FshCode('error');
 
     tank = new FSHTank([doc1, doc2, doc3], {
       name: 'test',
@@ -47,6 +58,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
     });
 
     it('should find valid fish when fishing by name for all types', () => {
@@ -55,6 +67,7 @@ describe('FSHTank', () => {
       expect(tank.fish('ValueSet2').id).toBe('vs2');
       expect(tank.fish('CodeSystem2').id).toBe('cs2');
       expect(tank.fish('Instance1').id).toBe('inst1');
+      expect(tank.fish('Invariant1').name).toBe('Invariant1');
     });
 
     it('should find valid fish when fishing by url for all types', () => {
@@ -62,7 +75,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
+      // not applicable for Instance or Invariant
     });
 
     it('should not find fish when fishing by invalid name/id/url', () => {
@@ -78,6 +91,7 @@ describe('FSHTank', () => {
           Type.ValueSet,
           Type.CodeSystem,
           Type.Instance,
+          Type.Invariant,
           Type.Resource,
           Type.Type
         )
@@ -93,6 +107,7 @@ describe('FSHTank', () => {
           Type.ValueSet,
           Type.CodeSystem,
           Type.Instance,
+          Type.Invariant,
           Type.Resource,
           Type.Type
         )
@@ -108,6 +123,7 @@ describe('FSHTank', () => {
           Type.Extension,
           Type.CodeSystem,
           Type.Instance,
+          Type.Invariant,
           Type.Resource,
           Type.Type
         )
@@ -123,6 +139,7 @@ describe('FSHTank', () => {
           Type.Extension,
           Type.ValueSet,
           Type.Instance,
+          Type.Invariant,
           Type.Resource,
           Type.Type
         )
@@ -138,6 +155,23 @@ describe('FSHTank', () => {
           Type.Extension,
           Type.ValueSet,
           Type.CodeSystem,
+          Type.Invariant,
+          Type.Resource,
+          Type.Type
+        )
+      ).toBeUndefined();
+    });
+
+    it('should only find invariants when invariants are requested', () => {
+      expect(tank.fish('Invariant1', Type.Invariant).name).toBe('Invariant1');
+      expect(
+        tank.fish(
+          'Invariant1',
+          Type.Profile,
+          Type.Extension,
+          Type.ValueSet,
+          Type.CodeSystem,
+          Type.Instance,
           Type.Resource,
           Type.Type
         )
@@ -172,12 +206,17 @@ describe('FSHTank', () => {
       id: 'inst1',
       name: 'Instance1'
     };
+    const inv1MD: Metadata = {
+      id: 'Invariant1', // id will always be name on Invariants
+      name: 'Invariant1'
+    };
     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
     });
 
     it('should find valid fish when fishing by name for all types', () => {
@@ -186,6 +225,7 @@ describe('FSHTank', () => {
       expect(tank.fishForMetadata('ValueSet1')).toEqual(vs1MD);
       expect(tank.fishForMetadata('CodeSystem1')).toEqual(cs1MD);
       expect(tank.fishForMetadata('Instance1')).toEqual(inst1MD);
+      expect(tank.fishForMetadata('Invariant1')).toEqual(inv1MD);
     });
 
     it('should find valid fish when fishing by url for all types', () => {
@@ -193,7 +233,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
+      // not applicable for Instance or Invariant
     });
 
     it('should not find fish when fishing by invalid name/id/url', () => {
@@ -209,6 +249,7 @@ describe('FSHTank', () => {
           Type.ValueSet,
           Type.CodeSystem,
           Type.Instance,
+          Type.Invariant,
           Type.Resource,
           Type.Type
         )
@@ -224,6 +265,7 @@ describe('FSHTank', () => {
           Type.ValueSet,
           Type.CodeSystem,
           Type.Instance,
+          Type.Invariant,
           Type.Resource,
           Type.Type
         )
@@ -239,6 +281,7 @@ describe('FSHTank', () => {
           Type.Extension,
           Type.CodeSystem,
           Type.Instance,
+          Type.Invariant,
           Type.Resource,
           Type.Type
         )
@@ -254,6 +297,7 @@ describe('FSHTank', () => {
           Type.Extension,
           Type.ValueSet,
           Type.Instance,
+          Type.Invariant,
           Type.Resource,
           Type.Type
         )
@@ -269,6 +313,23 @@ describe('FSHTank', () => {
           Type.Extension,
           Type.ValueSet,
           Type.CodeSystem,
+          Type.Invariant,
+          Type.Resource,
+          Type.Type
+        )
+      ).toBeUndefined();
+    });
+
+    it('should only find invariants when invariants are requested', () => {
+      expect(tank.fishForMetadata('Invariant1', Type.Invariant)).toEqual(inv1MD);
+      expect(
+        tank.fishForMetadata(
+          'Invariant1',
+          Type.Profile,
+          Type.Extension,
+          Type.ValueSet,
+          Type.CodeSystem,
+          Type.Instance,
           Type.Resource,
           Type.Type
         )
@@ -297,6 +358,8 @@ describe('FSHTank', () => {
       expect(tank.fishForFHIR('inst1')).toBeUndefined();
       expect(tank.fishForFHIR('inst1', Type.Instance)).toBeUndefined();
       expect(tank.fishForFHIR('Instance1')).toBeUndefined();
+      expect(tank.fishForFHIR('Invariant1')).toBeUndefined();
+      expect(tank.fishForFHIR('Invariant1', Type.Invariant)).toBeUndefined();
     });
   });
 });