Repository: FHIR/sushi
Pull request #66 (2019-12-23 23:36:38)

Initial Instance Export
This PR adds initial support for exporting instances.

Supported features include:
- Fixing values on instances
- Fixing values on extensions by accessing the extension directly through an index (not by using a slice name)
- Fixing values on an attribute that has been constrained from a `1..*` cardinality to a `1..1` cardinality will correctly set the value within an array
- Instances are available within the Table of Contents in an IG at the same level as other profiles and extensions

Unsupported features that will come in later tasks:
- Implying fixed values that are fixed in a profile or extension
- Fixing values using slices
- Adding extensions to primitives

jafeltra: I think I've addressed all of the comments. If I missed any comments or there are any further suggestions, just let me know!

@mint-thompson hopefully making InstanceDefinition into a class will help with some of the confusion you had when first following along the export. If you feel like there is anything else I can do to make it less confusing, let me know!
ngfreiter: The changes addressed my concerns, so I'm happy with it. To my eye it also seemed like you hit on all of @cmoesel 's comments, but @mint-thompson when you check out the changes you requested, if you could also verify that the comments from Chris have been addressed, that would make me feel more safe and secure. Or if we don't merge until after the holiday Chris can verify Chris's changes.
jafeltra: I just rebased after the two updates to master and updated one test file to reflect the new way we load packages.

Patch:
diff --git a/src/app.ts b/src/app.ts
index c4d5982..5014221 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -87,7 +87,7 @@ async function app() {
     const igExporter = new IGExporter(outPackage, igDataPath);
     igExporter.export(program.out);
   } else {
-    for (const sd of [...outPackage.profiles, ...outPackage.extensions]) {
+    for (const sd of [...outPackage.profiles, ...outPackage.extensions, ...outPackage.instances]) {
       fs.writeFileSync(
         path.join(program.out, sd.getFileName()),
         JSON.stringify(sd.toJSON(), null, 2),
@@ -99,6 +99,7 @@ async function app() {
   logger.info(`
   Profiles:    ${outPackage.profiles.length}
   Extensions:  ${outPackage.extensions.length}
+  Instances:   ${outPackage.instances.length}
   Errors:      ${stats.numError}
   Warnings:    ${stats.numWarn}`);
 }
diff --git a/src/errors/InstanceOfNotDefinedError.ts b/src/errors/InstanceOfNotDefinedError.ts
new file mode 100644
index 0000000..f5f8fb7
--- /dev/null
+++ b/src/errors/InstanceOfNotDefinedError.ts
@@ -0,0 +1,12 @@
+import { WithSource } from './WithSource';
+import { SourceInfo } from '../fshtypes';
+
+export class InstanceOfNotDefinedError extends Error implements WithSource {
+  constructor(
+    public instanceName: string,
+    public instanceOf: string,
+    public sourceInfo: SourceInfo
+  ) {
+    super(`InstanceOf ${instanceOf} not found for ${instanceName}`);
+  }
+}
diff --git a/src/export/FHIRExporter.ts b/src/export/FHIRExporter.ts
index e0e1369..fc0a0c0 100644
--- a/src/export/FHIRExporter.ts
+++ b/src/export/FHIRExporter.ts
@@ -3,6 +3,7 @@ import { Package } from './Package';
 import { ProfileExporter } from './ProfileExporter';
 import { ExtensionExporter } from './ExtensionExporter';
 import { FHIRDefinitions } from '../fhirdefs';
+import { InstanceExporter } from './InstanceExporter';
 /**
  * FHIRExporter handles the processing of FSH documents, storing the FSH types within them as FHIR types.
  * FHIRExporter takes the Profiles and Extensions within the FSHDocuments of a FSHTank and returns them
@@ -12,6 +13,7 @@ export class FHIRExporter {
   private readonly FHIRDefs: FHIRDefinitions;
   private profileExporter: ProfileExporter;
   private extensionExporter: ExtensionExporter;
+  private instanceExporter: InstanceExporter;
 
   constructor(FHIRDefs: FHIRDefinitions) {
     this.FHIRDefs = FHIRDefs;
@@ -20,8 +22,14 @@ export class FHIRExporter {
   export(tank: FSHTank): Package {
     this.profileExporter = new ProfileExporter(this.FHIRDefs, tank);
     this.extensionExporter = new ExtensionExporter(this.FHIRDefs, tank);
+    this.instanceExporter = new InstanceExporter(
+      this.FHIRDefs,
+      tank,
+      this.profileExporter.resolve.bind(this.profileExporter)
+    );
     const profileDefs = this.profileExporter.export();
     const extensionDefs = this.extensionExporter.export();
+    const instanceDefs = this.instanceExporter.export();
     // TODO: There is currently a bug in how we do exports that causes some duplicates in the
     // Package.  More specifically, if a FSH Extension is resolved while exporting a Profile,
     // then the resolved Extension will be put in the Profiles array.  The reverse is also
@@ -29,6 +37,11 @@ export class FHIRExporter {
     // just remove the duplicates here so the downstream processes don't have to deal with it.
     const deduplicatedProfileDefs = profileDefs.filter(sd => tank.findProfile(sd.name));
     const deduplicatedExtensionDefs = extensionDefs.filter(sd => tank.findExtension(sd.name));
-    return new Package(deduplicatedProfileDefs, deduplicatedExtensionDefs, tank.config);
+    return new Package(
+      deduplicatedProfileDefs,
+      deduplicatedExtensionDefs,
+      instanceDefs,
+      tank.config
+    );
   }
 }
diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts
new file mode 100644
index 0000000..4f550ea
--- /dev/null
+++ b/src/export/InstanceExporter.ts
@@ -0,0 +1,77 @@
+import { FSHTank } from '../import/FSHTank';
+import { StructureDefinition, InstanceDefinition, ResolveFn } from '../fhirtypes';
+import { Instance } from '../fshtypes';
+import { FHIRDefinitions } from '../fhirdefs';
+import { logger } from '../utils/FSHLogger';
+import { setPropertyOnInstance } from '../fhirtypes/common';
+import { InstanceOfNotDefinedError } from '../errors/InstanceOfNotDefinedError';
+
+export class InstanceExporter {
+  constructor(
+    public readonly FHIRDefs: FHIRDefinitions,
+    public readonly tank: FSHTank,
+    public readonly resolve: ResolveFn
+  ) {}
+
+  private setFixedValues(
+    fshInstanceDef: Instance,
+    instanceDef: InstanceDefinition,
+    instanceOfStructureDefinition: StructureDefinition
+  ): InstanceDefinition {
+    // All rules will be FixValueRule
+    fshInstanceDef.rules.forEach(rule => {
+      const { fixedValue, pathParts } = instanceOfStructureDefinition.validateValueAtPath(
+        rule.path,
+        rule.fixedValue,
+        this.resolve
+      );
+
+      setPropertyOnInstance(instanceDef, pathParts, fixedValue);
+    });
+
+    return instanceDef;
+  }
+
+  exportInstance(fshDefinition: Instance): InstanceDefinition {
+    const instanceOfStructureDefinition = this.resolve(fshDefinition.instanceOf);
+
+    if (!instanceOfStructureDefinition) {
+      throw new InstanceOfNotDefinedError(
+        fshDefinition.name,
+        fshDefinition.instanceOf,
+        fshDefinition.sourceInfo
+      );
+    }
+
+    let instanceDef = new InstanceDefinition();
+    instanceDef.resourceType = instanceOfStructureDefinition.type; // ResourceType is determined by the StructureDefinition of the type
+    instanceDef.instanceName = fshDefinition.id; // This is name of the instance in the FSH
+
+    // All other values of the instance will be fixedValues explicitly on the FSH Instance
+    instanceDef = this.setFixedValues(fshDefinition, instanceDef, instanceOfStructureDefinition);
+
+    // Add any set values that were set on the StructDef but not explicitly on the FSH Instance
+
+    return instanceDef;
+  }
+
+  /**
+   * Exports Instances
+   * @param {FSHTank} tank - The FSH tank we are exporting
+   * @returns {InstanceDefinition[]} - The Instances exported
+   */
+  export(): InstanceDefinition[] {
+    const instanceDefs: InstanceDefinition[] = [];
+    for (const doc of this.tank.docs) {
+      for (const instance of doc.instances.values()) {
+        try {
+          const instanceDef = this.exportInstance(instance);
+          instanceDefs.push(instanceDef);
+        } catch (e) {
+          logger.error(e.message, e.sourceInfo);
+        }
+      }
+    }
+    return instanceDefs;
+  }
+}
diff --git a/src/export/Package.ts b/src/export/Package.ts
index 0d84312..80dbd37 100644
--- a/src/export/Package.ts
+++ b/src/export/Package.ts
@@ -1,10 +1,11 @@
-import { StructureDefinition } from '../fhirtypes';
+import { StructureDefinition, InstanceDefinition } from '../fhirtypes';
 import { Config } from '../fshtypes';
 
 export class Package {
   constructor(
     public readonly profiles: StructureDefinition[],
     public readonly extensions: StructureDefinition[],
+    public readonly instances: InstanceDefinition[],
     public readonly config: Config
   ) {}
 }
diff --git a/src/export/index.ts b/src/export/index.ts
index ab999e3..3f99516 100644
--- a/src/export/index.ts
+++ b/src/export/index.ts
@@ -2,5 +2,6 @@ export * from './FHIRExporter';
 export * from './StructureDefinitionExporter';
 export * from './ProfileExporter';
 export * from './ExtensionExporter';
+export * from './InstanceExporter';
 export * from './exportFHIR';
 export * from './Package';
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index 1ae0004..332cefe 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -20,7 +20,7 @@ import {
   InvalidSumOfSliceMinsError,
   InvalidMaxOfSliceError
 } from '../errors';
-import { setPropertyOnInstance } from './common';
+import { setPropertyOnDefinitionInstance } from './common';
 
 export class ElementDefinitionType {
   private _code: string;
@@ -328,7 +328,7 @@ export class ElementDefinition {
    * @param {ResolveFn} resolve - A function that can resolve a type to a StructureDefinition instance
    */
   setInstancePropertyByPath(path: string, value: any, resolve: ResolveFn = () => undefined): void {
-    setPropertyOnInstance(this, path, value, resolve);
+    setPropertyOnDefinitionInstance(this, path, value, resolve);
   }
 
   /**
diff --git a/src/fhirtypes/InstanceDefinition.ts b/src/fhirtypes/InstanceDefinition.ts
new file mode 100644
index 0000000..c783a9d
--- /dev/null
+++ b/src/fhirtypes/InstanceDefinition.ts
@@ -0,0 +1,33 @@
+/**
+ * A class representing a FHIR Instance.
+ *
+ * The defined properties on InstanceDefinition are relatively simple due to the fact that each FHIR resource will
+ * be setting different properties based on their own definitions.
+ */
+export class InstanceDefinition {
+  resourceType: string;
+  instanceName: string;
+  id?: string;
+  [key: string]: any; // Allow any key value pair on InstanceDefinition due to the high number of potential properties that can be set on a FHIR instance
+
+  /**
+   * Get the file name for serializing to disk.
+   * @returns {string} the filename
+   */
+  getFileName(): string {
+    return `${this.resourceType}-${this.id ?? this.instanceName}.json`;
+  }
+
+  toJSON(): any {
+    delete this.instanceName; // Only needed for the file name - not a FHIR property
+    return this;
+  }
+
+  static fromJSON(json: { [key: string]: any }): InstanceDefinition {
+    const instanceDefinition = new InstanceDefinition();
+    Object.keys(json).forEach(key => {
+      instanceDefinition[key] = json[key];
+    });
+    return instanceDefinition;
+  }
+}
diff --git a/src/fhirtypes/StructureDefinition.ts b/src/fhirtypes/StructureDefinition.ts
index 942339e..63ab681 100644
--- a/src/fhirtypes/StructureDefinition.ts
+++ b/src/fhirtypes/StructureDefinition.ts
@@ -5,7 +5,7 @@ import { Meta } from './specialTypes';
 import { Identifier, CodeableConcept, Coding, Narrative, Resource, Extension } from './dataTypes';
 import { ContactDetail, UsageContext } from './metaDataTypes';
 import { CannotResolvePathError, InvalidElementAccessError } from '../errors';
-import { getArrayIndex, setPropertyOnInstance } from './common';
+import { getArrayIndex, setPropertyOnDefinitionInstance } from './common';
 
 /**
  * A class representing a FHIR R4 StructureDefinition.  For the most part, each allowable property in a StructureDefinition
@@ -145,7 +145,7 @@ export class StructureDefinition {
     // If the path already exists, get it and return the match
     // If !path just return the base parent element
     const fullPath = path ? `${this.type}.${path}` : this.type;
-    const match = this.elements.find(e => e.path === fullPath);
+    const match = this.elements.find(e => e.path === fullPath && !e.id.includes(':'));
     if (match != null) {
       return match;
     }
@@ -215,6 +215,15 @@ export class StructureDefinition {
           }
         }
       }
+
+      // If there are no brackets, remove any slices that don't match exactly
+      if (!pathPart.brackets) {
+        matchingElements = matchingElements.filter(
+          e =>
+            e.id.includes(`${fhirPathString}:${pathPart.base}`) ||
+            !e.id.includes(`${fhirPathString}:`)
+        );
+      }
     }
 
     // We could still have child elements that are matching, if so filter them out now
@@ -233,7 +242,7 @@ export class StructureDefinition {
     if (path.startsWith('snapshot') || path.startsWith('differential')) {
       throw new InvalidElementAccessError(path);
     }
-    setPropertyOnInstance(this, path, value, resolve);
+    setPropertyOnDefinitionInstance(this, path, value, resolve);
   }
 
   /**
@@ -347,6 +356,12 @@ export class StructureDefinition {
         pathPart.brackets?.forEach(p => (currentPath += `[${p}]`));
       }
       currentElement = this.findElementByPath(currentPath, resolve);
+
+      // If the element has a base.max that is greater than 1, but the element has been constrained, still set properties in an array
+      const nonArrayElementIsBasedOnArray =
+        currentElement?.base?.max !== '0' &&
+        currentElement?.base?.max !== '1' &&
+        (currentElement?.max === '0' || currentElement?.max === '1');
       if (
         !currentElement ||
         currentElement.max === '0' ||
@@ -358,10 +373,11 @@ export class StructureDefinition {
         // or is being incorrectly accessed as an array
         throw new CannotResolvePathError(path);
       } else if (
-        arrayIndex == null &&
-        currentElement.max != null &&
-        currentElement.max !== '0' &&
-        currentElement.max !== '1'
+        (arrayIndex == null &&
+          currentElement.max != null &&
+          currentElement.max !== '0' &&
+          currentElement.max !== '1') ||
+        nonArrayElementIsBasedOnArray
       ) {
         // Modify the path to have 0 indices
         if (!pathPart.brackets) pathPart.brackets = [];
diff --git a/src/fhirtypes/common.ts b/src/fhirtypes/common.ts
index 13d28be..67562a2 100644
--- a/src/fhirtypes/common.ts
+++ b/src/fhirtypes/common.ts
@@ -1,4 +1,4 @@
-import { ResolveFn, StructureDefinition, PathPart, ElementDefinition } from '.';
+import { ResolveFn, StructureDefinition, PathPart, ElementDefinition, InstanceDefinition } from '.';
 
 /**
  * This function sets an instance property of an SD or ED if possible
@@ -7,7 +7,7 @@ import { ResolveFn, StructureDefinition, PathPart, ElementDefinition } from '.';
  * @param {any} value - The value to fix
  * @param {ResolveFn} resolve - A function that can resolve a type to a StructureDefinition instance
  */
-export function setPropertyOnInstance(
+export function setPropertyOnDefinitionInstance(
   instance: StructureDefinition | ElementDefinition,
   path: string,
   value: any,
@@ -19,6 +19,14 @@ export function setPropertyOnInstance(
     value,
     resolve
   );
+  setPropertyOnInstance(instance, pathParts, fixedValue);
+}
+
+export function setPropertyOnInstance(
+  instance: StructureDefinition | ElementDefinition | InstanceDefinition,
+  pathParts: PathPart[],
+  fixedValue: any
+): void {
   if (fixedValue != null) {
     // If we can fix the value on the StructureDefinition StructureDefinition, then we can set the
     // instance property here
diff --git a/src/fhirtypes/index.ts b/src/fhirtypes/index.ts
index d055122..ad09501 100644
--- a/src/fhirtypes/index.ts
+++ b/src/fhirtypes/index.ts
@@ -4,4 +4,5 @@ export * from './primitiveTypes';
 export * from './specialTypes';
 export * from './ElementDefinition';
 export * from './ImplementationGuide';
+export * from './InstanceDefinition';
 export * from './StructureDefinition';
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index b63fd77..20173a8 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -155,6 +155,17 @@ export class IGExporter {
         exampleBoolean: false
       });
     });
+    this.pkg.instances.forEach(instance => {
+      const instancePath = path.join(igPath, 'input', 'resources', instance.getFileName());
+      outputJSONSync(instancePath, instance.toJSON(), { spaces: 2 });
+      this.ig.definition.resource.push({
+        reference: {
+          reference: `${instance.resourceType}/${instance.id ?? instance.instanceName}`
+        },
+        name: instance.getFileName().slice(0, -5), // Slice off the .json of the file name
+        exampleBoolean: true
+      });
+    });
   }
 
   /**


Test patch:
diff --git a/test/export/FHIRExporter.test.ts b/test/export/FHIRExporter.test.ts
index 66f0858..26af096 100644
--- a/test/export/FHIRExporter.test.ts
+++ b/test/export/FHIRExporter.test.ts
@@ -11,7 +11,7 @@ describe('FHIRExporter', () => {
     });
     const result = exportFHIR(input, new FHIRDefinitions());
     expect(result).toEqual(
-      new Package([], [], { name: 'test', version: '0.0.1', canonical: 'http://example.com' })
+      new Package([], [], [], { name: 'test', version: '0.0.1', canonical: 'http://example.com' })
     );
   });
 });
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
new file mode 100644
index 0000000..39f7e4c
--- /dev/null
+++ b/test/export/InstanceExporter.test.ts
@@ -0,0 +1,107 @@
+import { InstanceExporter, ProfileExporter } from '../../src/export';
+import { FSHTank, FSHDocument } from '../../src/import';
+import { FHIRDefinitions, loadFromPath } from '../../src/fhirdefs';
+import { ResolveFn } from '../../src/fhirtypes';
+import { Instance, Profile, FshCode } from '../../src/fshtypes';
+import { FixedValueRule } from '../../src/fshtypes/rules';
+import { loggerSpy } from '../testhelpers/loggerSpy';
+import { getResolver } from '../testhelpers/getResolver';
+import { spyResolve } from '../testhelpers/spyResolve';
+import path from 'path';
+
+describe('InstanceExporter', () => {
+  let defs: FHIRDefinitions;
+  let resolve: ResolveFn;
+  let doc: FSHDocument;
+  let input: FSHTank;
+  let exporter: InstanceExporter;
+  let profileExporter: ProfileExporter;
+
+  beforeAll(() => {
+    defs = new FHIRDefinitions();
+    loadFromPath(
+      path.join(__dirname, '..', 'testhelpers', 'testdefs', 'package'),
+      'testPackage',
+      defs
+    );
+    resolve = getResolver(defs);
+  });
+
+  beforeEach(() => {
+    doc = new FSHDocument('fileName');
+    input = new FSHTank([doc], { name: 'test', version: '0.0.1', canonical: 'http://example.com' });
+    profileExporter = new ProfileExporter(defs, input);
+    spyResolve(profileExporter, resolve);
+    exporter = new InstanceExporter(defs, input, profileExporter.resolve.bind(profileExporter));
+  });
+
+  it('should output empty results with empty input', () => {
+    const exported = exporter.export();
+    expect(exported).toEqual([]);
+  });
+
+  it('should export a single instance', () => {
+    const instance = new Instance('MyInstance');
+    instance.instanceOf = 'Patient';
+    doc.instances.set(instance.name, instance);
+    const exported = exporter.export();
+    expect(exported.length).toBe(1);
+  });
+
+  it('should export multiple instances', () => {
+    const instanceFoo = new Instance('Foo');
+    instanceFoo.instanceOf = 'Patient';
+    const instanceBar = new Instance('Bar');
+    instanceBar.instanceOf = 'Patient';
+    doc.instances.set(instanceFoo.name, instanceFoo);
+    doc.instances.set(instanceBar.name, instanceBar);
+    const exported = exporter.export();
+    expect(exported.length).toBe(2);
+  });
+
+  it('should still export instance if one fails', () => {
+    const instanceFoo = new Instance('Foo');
+    instanceFoo.instanceOf = 'Baz';
+    const instanceBar = new Instance('Bar');
+    instanceBar.instanceOf = 'Patient';
+    doc.instances.set(instanceFoo.name, instanceFoo);
+    doc.instances.set(instanceBar.name, instanceBar);
+    const exported = exporter.export();
+    expect(exported.length).toBe(1);
+    expect(exported[0].instanceName).toBe('Bar');
+  });
+
+  it('should log a message with source information when the parent is not found', () => {
+    const instance = new Instance('Bogus').withFile('Bogus.fsh').withLocation([2, 9, 4, 23]);
+    instance.instanceOf = 'BogusParent';
+    doc.instances.set(instance.name, instance);
+    exporter.export();
+    expect(loggerSpy.getLastMessage()).toMatch(/File: Bogus\.fsh.*Line: 2 - 4\D/s);
+  });
+
+  it('should export instances with InstanceOf FSHy profile', () => {
+    const profileFoo = new Profile('Foo');
+    profileFoo.parent = 'Patient';
+    const instanceBar = new Instance('Bar');
+    instanceBar.instanceOf = 'Foo';
+    doc.profiles.set(profileFoo.name, profileFoo);
+    doc.instances.set(instanceBar.name, instanceBar);
+    const exported = exporter.export();
+    expect(exported.length).toBe(1); // One instance is successfully exported because profile is defined
+    expect(exported[0].instanceName).toBe('Bar');
+    expect(exported[0].resourceType).toBe('Patient');
+  });
+
+  it('should fix values on an instance', () => {
+    const instance = new Instance('Bar');
+    instance.instanceOf = 'Patient';
+    const fixedValRule = new FixedValueRule('gender');
+    const fixedFshCode = new FshCode('foo', 'http://foo.com');
+    fixedValRule.fixedValue = fixedFshCode;
+    instance.rules.push(fixedValRule);
+    doc.instances.set(instance.name, instance);
+    const exported = exporter.export();
+    expect(exported.length).toBe(1);
+    expect(exported[0].gender).toBe('foo');
+  });
+});
diff --git a/test/fhirtypes/InstanceDefinition.test.ts b/test/fhirtypes/InstanceDefinition.test.ts
new file mode 100644
index 0000000..307c61f
--- /dev/null
+++ b/test/fhirtypes/InstanceDefinition.test.ts
@@ -0,0 +1,39 @@
+import { InstanceDefinition } from '../../src/fhirtypes';
+
+describe('InstanceDefinition', () => {
+  const patientInstanceJSON = {
+    resourceType: 'Patient',
+    id: 'example',
+    name: [
+      {
+        family: 'Shaw',
+        given: ['Amy', 'V.']
+      }
+    ]
+  };
+  let patientInstance: InstanceDefinition;
+  beforeEach(() => {
+    patientInstance = InstanceDefinition.fromJSON(patientInstanceJSON);
+  });
+
+  describe('#fromJSON', () => {
+    it('should load an instance properly', () => {
+      expect(patientInstance.resourceType).toBe('Patient');
+      expect(patientInstance.id).toBe('example');
+      expect(patientInstance.name).toEqual([
+        {
+          family: 'Shaw',
+          given: ['Amy', 'V.']
+        }
+      ]);
+    });
+  });
+
+  describe('#toJSON', () => {
+    it('should round trip back to the original JSON', () => {
+      patientInstance.instanceName = 'TestName'; // If imported from FSH file, will have an instance name that is removed in toJSON
+      const newJSON = patientInstance.toJSON();
+      expect(newJSON).toEqual(patientInstanceJSON);
+    });
+  });
+});
diff --git a/test/fhirtypes/StructureDefinition.test.ts b/test/fhirtypes/StructureDefinition.test.ts
index ce91121..563b0b2 100644
--- a/test/fhirtypes/StructureDefinition.test.ts
+++ b/test/fhirtypes/StructureDefinition.test.ts
@@ -488,6 +488,20 @@ describe('StructureDefinition', () => {
       expect(pathParts[1]).toEqual({ base: 'value' });
     });
 
+    it('should allow fixing an instance value to an element in an array if the element was constrained from an array', () => {
+      // code.coding[RespRateCode] has been constrained from 1..* to 1..1
+      const { fixedValue, pathParts } = respRate.validateValueAtPath(
+        'code.coding[RespRateCode].id',
+        'foo',
+        getResolver(defs)
+      );
+      expect(fixedValue).toBe('foo');
+      expect(pathParts.length).toBe(3);
+      expect(pathParts[0]).toEqual({ base: 'code' });
+      expect(pathParts[1]).toEqual({ base: 'coding', brackets: ['RespRateCode', '0'] }); // 0 in path parts means value will be set in an array
+      expect(pathParts[2]).toEqual({ base: 'id' });
+    });
+
     it('should not allow using array brackets when an element is not an array', () => {
       expect(() => {
         structureDefinition.validateValueAtPath('version[0]', 'foo', resolve);
@@ -519,7 +533,7 @@ describe('StructureDefinition', () => {
       );
       expect(fixedValue).toBe('foo');
       expect(pathParts.length).toBe(3);
-      expect(pathParts[0]).toEqual({ base: 'category', brackets: ['VSCat'] });
+      expect(pathParts[0]).toEqual({ base: 'category', brackets: ['VSCat', '0'] });
       expect(pathParts[1]).toEqual({ base: 'coding', brackets: ['0'] });
       expect(pathParts[2]).toEqual({ base: 'version' });
     });
@@ -535,6 +549,21 @@ describe('StructureDefinition', () => {
       expect(pathParts[0]).toEqual({ base: 'extension', brackets: ['required', '3'] });
       expect(pathParts[1]).toEqual({ base: 'value[x]' });
     });
+
+    it('should allow setting values directly on extensions by accessing indexes', () => {
+      // This test also tests that we can access later indexes of an array that has been unfolded at other indexes
+      // For example, extension[0].url has already been unfolded an has values fixed.
+      // validateValueAtPath correctly validates values for extension[2]
+      const { fixedValue, pathParts } = CSSPC.validateValueAtPath(
+        'extension[2].url',
+        'foo',
+        getResolver(defs)
+      );
+      expect(fixedValue).toBe('foo');
+      expect(pathParts.length).toBe(2);
+      expect(pathParts[0]).toEqual({ base: 'extension', brackets: ['2'] });
+      expect(pathParts[1]).toEqual({ base: 'url' });
+    });
   });
 
   describe('#captureOriginalElements()', () => {
diff --git a/test/ig/IGExporter.test.ts b/test/ig/IGExporter.test.ts
index 5626ec7..2be4956 100644
--- a/test/ig/IGExporter.test.ts
+++ b/test/ig/IGExporter.test.ts
@@ -3,7 +3,7 @@ import path from 'path';
 import temp from 'temp';
 import ini from 'ini';
 import { IGExporter } from '../../src/ig';
-import { StructureDefinition } from '../../src/fhirtypes';
+import { StructureDefinition, InstanceDefinition } from '../../src/fhirtypes';
 import { Package } from '../../src/export';
 import { Config } from '../../src/fshtypes';
 import { loggerSpy } from '../testhelpers/loggerSpy';
@@ -20,8 +20,9 @@ describe('IGExporter', () => {
     beforeAll(() => {
       const fixtures = path.join(__dirname, 'fixtures', 'simple-ig');
       const config: Config = fs.readJSONSync(path.join(fixtures, 'package.json'));
-      pkg = new Package([], [], config);
+      pkg = new Package([], [], [], config);
       const resources = path.join(fixtures, 'resources');
+      const instances = path.join(fixtures, 'instances');
       fs.readdirSync(resources).forEach(f => {
         if (f.endsWith('.json')) {
           const sd = StructureDefinition.fromJSON(fs.readJSONSync(path.join(resources, f)));
@@ -32,6 +33,12 @@ describe('IGExporter', () => {
           }
         }
       });
+      fs.readdirSync(instances).forEach(f => {
+        if (f.endsWith('.json')) {
+          const instanceDef = InstanceDefinition.fromJSON(fs.readJSONSync(path.join(instances, f)));
+          pkg.instances.push(instanceDef);
+        }
+      });
       exporter = new IGExporter(pkg, path.resolve(fixtures, 'ig-data'));
       tempOut = temp.mkdirSync('sushi-test');
       // No need to regenerate the IG on every test -- generate it once and inspect what you
@@ -54,18 +61,25 @@ describe('IGExporter', () => {
 
     it('should copy over the resource files', () => {
       const resourcesPath = path.join(tempOut, 'input', 'resources');
-      expect(fs.readdirSync(resourcesPath)).toHaveLength(4);
+      expect(fs.readdirSync(resourcesPath)).toHaveLength(5);
       const ids = [
         'sample-observation',
         'sample-patient',
         'sample-value-extension',
         'sample-complex-extension'
       ];
+
+      // StructureDefinitions copied
       ids.forEach(id => {
         const resourcePath = path.join(resourcesPath, `StructureDefinition-${id}.json`);
         expect(fs.existsSync(resourcePath)).toBeTruthy();
         expect(fs.readJSONSync(resourcePath).id).toEqual(id);
       });
+
+      // Instances copied
+      const instancePath = path.join(resourcesPath, 'Patient-example.json');
+      expect(fs.existsSync(instancePath)).toBeTruthy();
+      expect(fs.readJSONSync(instancePath).id).toEqual('example');
     });
 
     it('should generate an ig.ini with the correct values based on the package.json', () => {
@@ -148,6 +162,13 @@ describe('IGExporter', () => {
               description:
                 'Base StructureDefinition for Extension Type: Optional Extension Element - found in all resources.',
               exampleBoolean: false
+            },
+            {
+              reference: {
+                reference: 'Patient/example'
+              },
+              name: 'Patient-example',
+              exampleBoolean: true
             }
           ],
           page: {
@@ -212,7 +233,7 @@ describe('IGExporter', () => {
     beforeAll(() => {
       const fixtures = path.join(__dirname, 'fixtures', 'customized-ig');
       const config: Config = fs.readJSONSync(path.join(fixtures, 'package.json'));
-      pkg = new Package([], [], config);
+      pkg = new Package([], [], [], config);
       exporter = new IGExporter(pkg, path.resolve(fixtures, 'ig-data'));
       tempOut = temp.mkdirSync('sushi-test');
       // No need to regenerate the IG on every test -- generate it once and inspect what you
@@ -298,7 +319,7 @@ describe('IGExporter', () => {
     beforeAll(() => {
       const fixtures = path.join(__dirname, 'fixtures', 'invalid-data-ig');
       const config: Config = fs.readJSONSync(path.join(fixtures, 'package.json'));
-      pkg = new Package([], [], config);
+      pkg = new Package([], [], [], config);
       exporter = new IGExporter(pkg, path.resolve(fixtures, 'ig-data'));
       tempOut = temp.mkdirSync('sushi-test');
       // No need to regenerate the IG on every test -- generate it once and inspect what you
diff --git a/test/ig/fixtures/simple-ig/instances/Patient-example.json b/test/ig/fixtures/simple-ig/instances/Patient-example.json
new file mode 100644
index 0000000..0ed041f
--- /dev/null
+++ b/test/ig/fixtures/simple-ig/instances/Patient-example.json
@@ -0,0 +1,29 @@
+{
+  "resourceType": "Patient",
+  "id": "example",
+  "extension": [
+    {
+      "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race",
+      "extension": [
+        {
+          "url": "ombCategory",
+          "valueCoding": {
+            "code": "2160-3",
+            "system": "urn:oid:2.16.840.1.113883.6.238"
+          }
+        }
+      ]
+    }
+  ],
+  "identifier": [
+    {
+      "value": "1032702"
+    }
+  ],
+  "name": [
+    {
+      "family": "Shaw",
+      "given": ["Amy", "V."]
+    }
+  ]
+}