Repository: FHIR/sushi (ts)

Issue:
Shouldn't ig-data/examples be copied over an installation
I put some files in ig-data/examples, but they weren't copied as expected.

Also, I'd love it if the copying of examples happened AFTER the generation of examples in input so that I could easily override FSH generated examples by stuffing a customized one in there.

Pull request:
Copy over custom files when exporting IG
Fixes #257 and also implements https://standardhealthrecord.atlassian.net/browse/CIMPL-256. 

Now when the user places valid JSON files in any of the following folders within the `ig-data` folder:
* capabilities (for CapabilityStatements/Conformance resources)
* examples
* extensions
* models (for logical models)
* operations
* profiles
* resources (for anything/everything or whatever doesn't fit one of the other folders)
* vocabulary (for code systems & value sets)

We add those files to the ImplementationGuide resource, and copy the file over into the corresponding folder in `build/input`. This happens after the FSH has been written to `build/input`, so custom files will overwrite FSH files. 

Patch:
diff --git a/src/app.ts b/src/app.ts
index 0da3900..450db26 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -7,7 +7,7 @@ import { importText, FSHTank, RawFSH } from './import';
 import { exportFHIR } from './export';
 import { IGExporter } from './ig';
 import { logger, stats } from './utils';
-import { loadDependency } from './fhirdefs';
+import { loadDependency, loadCustomResources } from './fhirdefs';
 import { FHIRDefinitions } from './fhirdefs';
 
 app();
@@ -77,6 +77,9 @@ async function app() {
     );
   }
 
+  // Load custom resources specified in ig-data folder
+  loadCustomResources(input, defs);
+
   const rawFSHes = files
     .filter(file => file.endsWith('.fsh'))
     .map(file => {
diff --git a/src/fhirdefs/load.ts b/src/fhirdefs/load.ts
index eb9fe69..da6fd9c 100644
--- a/src/fhirdefs/load.ts
+++ b/src/fhirdefs/load.ts
@@ -113,6 +113,55 @@ export async function loadDependency(
   return FHIRDefs;
 }
 
+/**
+ * Loads custom resources defined in ig-data into FHIRDefs
+ * @param {string} input - The input path to the cli
+ * @param {FHIRDefinitions} defs - The FHIRDefinitions object to load definitions into
+ */
+export function loadCustomResources(input: string, defs: FHIRDefinitions): void {
+  // Similar code for loading custom resources exists in IGExporter.ts addPredefinedResources()
+  const pathEnds = [
+    'capabilities',
+    'extensions',
+    'models',
+    'operations',
+    'profiles',
+    'resources',
+    'vocabulary',
+    'examples'
+  ];
+  for (const pathEnd of pathEnds) {
+    let xmlFile = false;
+    let invalidFile = false;
+    const dirPath = path.join(input, 'ig-data', 'input', pathEnd);
+    if (fs.existsSync(dirPath)) {
+      const files = fs.readdirSync(dirPath);
+      for (const file of files) {
+        let resourceJSON: any;
+        if (file.endsWith('.json')) {
+          resourceJSON = fs.readJSONSync(path.join(dirPath, file));
+        } else {
+          xmlFile = xmlFile || file.endsWith('.xml');
+          invalidFile = true;
+          continue;
+        }
+        if (pathEnd !== 'examples') {
+          // add() will only add resources of resourceType:
+          // StructureDefinition, ValueSet, CodeSystem, or ImplementationGuide
+          defs.add(resourceJSON);
+        }
+      }
+    }
+    if (invalidFile) {
+      let message = `Invalid file detected in directory ${dirPath}. Input FHIR definitions must be JSON.`;
+      if (xmlFile) {
+        message += ' XML format not supported.';
+      }
+      logger.error(message);
+    }
+  }
+}
+
 /**
  * Loads a set of JSON files at targetPath into FHIRDefs
  * @param {string} targetPath - The path to the directory containing the JSON definitions
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index b6bc6f3..3f00c92 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -13,7 +13,8 @@ import {
   ImplementationGuideDefinitionPageGeneration,
   StructureDefinition,
   ValueSet,
-  CodeSystem
+  CodeSystem,
+  InstanceDefinition
 } from '../fhirtypes';
 import { logger, Type } from '../utils';
 import { FHIRDefinitions } from '../fhirdefs';
@@ -43,6 +44,7 @@ export class IGExporter {
     ensureDirSync(outPath);
     this.initIG();
     this.addResources();
+    this.addPredefinedResources(outPath);
     this.addStaticFiles(outPath);
     this.addIndex(outPath);
     this.addOtherPageContent(outPath);
@@ -378,7 +380,7 @@ export class IGExporter {
     resources.forEach(r => {
       this.ig.definition.resource.push({
         reference: { reference: `${r.resourceType}/${r.id}` },
-        name: r.title ?? r.name,
+        name: r.title ?? r.name ?? r.id,
         description: r.description,
         exampleBoolean: false
       });
@@ -405,6 +407,104 @@ export class IGExporter {
     });
   }
 
+  /**
+   * Adds any user provided resource files
+   * This includes definitions in:
+   * capabilities, extensions, models, operations, profiles, resources, vocabulary, examples
+   * Based on: https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#root.input
+   *
+   * @param {string} igPath - the path where the IG is exported to
+   */
+  private addPredefinedResources(igPath: string): void {
+    // Similar code for loading custom resources exists in load.ts loadCustomResources()
+    const pathEnds = [
+      'capabilities',
+      'extensions',
+      'models',
+      'operations',
+      'profiles',
+      'resources',
+      'vocabulary',
+      'examples'
+    ];
+    for (const pathEnd of pathEnds) {
+      const dirPath = path.join(this.igDataPath, 'input', pathEnd);
+      if (fs.existsSync(dirPath)) {
+        const files = fs.readdirSync(dirPath);
+        for (const file of files) {
+          let resourceJSON: InstanceDefinition;
+          if (file.endsWith('.json')) {
+            resourceJSON = fs.readJSONSync(path.join(dirPath, file));
+
+            if (resourceJSON.resourceType == null || resourceJSON.id == null) {
+              logger.error(
+                `Resource at ${path.join(dirPath, file)} must define resourceType and id.`
+              );
+              continue;
+            }
+
+            const resource: ImplementationGuideDefinitionResource = {
+              reference: {
+                reference: `${resourceJSON.resourceType}/${resourceJSON.id}`
+              },
+              name: resourceJSON.id, // will be overwritten w/ title or name where applicable
+              description: resourceJSON.description
+            };
+
+            if (pathEnd === 'examples') {
+              const exampleUrl = resourceJSON.meta?.profile?.find(
+                url =>
+                  this.pkg.fish(url, Type.Profile) ?? this.fhirDefs.fishForFHIR(url, Type.Profile)
+              );
+              if (exampleUrl) {
+                resource.exampleCanonical = exampleUrl;
+              } else {
+                resource.exampleBoolean = true;
+              }
+            } else {
+              resource.exampleBoolean = false;
+              // On some resources (Patient for example) these fields can be objects, avoid using them when this is true
+              const title = typeof resourceJSON.title === 'string' ? resourceJSON.title : null;
+              const name = typeof resourceJSON.name === 'string' ? resourceJSON.name : null;
+              if (title || name) {
+                resource.name = title ?? name;
+              }
+            }
+
+            const existingIndex = this.ig.definition.resource.findIndex(
+              r => r.reference.reference === resource.reference.reference
+            );
+            if (existingIndex >= 0) {
+              if (
+                this.ig.definition.resource[existingIndex].exampleBoolean ||
+                this.ig.definition.resource[existingIndex].exampleCanonical
+              ) {
+                // If it is replacing an existing example, preserve description and name from SUSHI
+                // Allows user method for setting description/name on external example
+                const oldDescription = this.ig.definition.resource[existingIndex].description;
+                const oldName = this.ig.definition.resource[existingIndex].name;
+                if (oldDescription) resource.description = oldDescription;
+                if (oldName) resource.name = this.ig.definition.resource[existingIndex].name;
+              }
+              this.ig.definition.resource[existingIndex] = resource;
+            } else {
+              this.ig.definition.resource.push(resource);
+            }
+            fs.copySync(
+              path.join(dirPath, file),
+              path.join(
+                igPath,
+                'input',
+                pathEnd,
+                `${resourceJSON.resourceType}-${resourceJSON.id}.json`
+              )
+            );
+          }
+        }
+      }
+    }
+  }
+
   /**
    * Writes the in-memory ImplementationGuide JSON to the IG output folder.
    *


Test patch:
diff --git a/test/fhirdefs/load.test.ts b/test/fhirdefs/load.test.ts
index ac98ebf..2a5fe38 100644
--- a/test/fhirdefs/load.test.ts
+++ b/test/fhirdefs/load.test.ts
@@ -1,7 +1,7 @@
-import { loadFromPath, loadDependency } from '../../src/fhirdefs/load';
+import { loadFromPath, loadDependency, loadCustomResources } from '../../src/fhirdefs/load';
 import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions';
 import { Type } from '../../src/utils';
-import { TestFisher } from '../testhelpers';
+import { TestFisher, loggerSpy } from '../testhelpers';
 import path from 'path';
 import fs from 'fs-extra';
 import tar from 'tar';
@@ -187,3 +187,27 @@ describe('#loadDependency()', () => {
     );
   });
 });
+
+describe('#loadCustomResources', () => {
+  let defs: FHIRDefinitions;
+  beforeAll(() => {
+    defs = new FHIRDefinitions();
+    const fixtures = path.join(__dirname, '..', 'ig', 'fixtures', 'customized-ig-with-resources');
+    loadCustomResources(fixtures, defs);
+  });
+
+  it('should load custom resources', () => {
+    // Only StructureDefinitions, ValueSets, and CodeSystems are loaded in
+    expect(defs.size()).toBe(10);
+    expect(defs.allExtensions().some(e => e.id === 'patient-birthPlace')).toBeTruthy();
+    expect(defs.allProfiles().some(e => e.id === 'MyPatient')).toBeTruthy();
+    expect(defs.allProfiles().some(e => e.id === 'MyTitlePatient')).toBeTruthy();
+    expect(defs.allValueSets().some(e => e.id === 'MyVS')).toBeTruthy();
+  });
+
+  it('should log an error for invalid input files', () => {
+    expect(loggerSpy.getMessageAtIndex(-1, 'error')).toMatch(
+      /Invalid file.*resources.*XML format not supported/
+    );
+  });
+});
diff --git a/test/ig/IGExporter.test.ts b/test/ig/IGExporter.test.ts
index 8a15aca..70d7a0d 100644
--- a/test/ig/IGExporter.test.ts
+++ b/test/ig/IGExporter.test.ts
@@ -7,7 +7,8 @@ import { StructureDefinition, InstanceDefinition, CodeSystem } from '../../src/f
 import { Package } from '../../src/export';
 import { Config } from '../../src/fshtypes';
 import { loggerSpy } from '../testhelpers/loggerSpy';
-import { FHIRDefinitions, loadFromPath } from '../../src/fhirdefs';
+import { FHIRDefinitions, loadFromPath, loadCustomResources } from '../../src/fhirdefs';
+import { TestFisher } from '../testhelpers';
 
 describe('IGExporter', () => {
   // Track temp files/folders for cleanup
@@ -486,6 +487,197 @@ describe('IGExporter', () => {
     });
   });
 
+  describe('#customized-ig-with-resources', () => {
+    let pkg: Package;
+    let exporter: IGExporter;
+    let tempOut: string;
+
+    beforeAll(() => {
+      const defs = new FHIRDefinitions();
+      loadFromPath(
+        path.join(__dirname, '..', 'testhelpers', 'testdefs', 'package'),
+        'testPackage',
+        defs
+      );
+      const fixtures = path.join(__dirname, 'fixtures', 'customized-ig-with-resources');
+      const config: Config = fs.readJSONSync(path.join(fixtures, 'package.json'));
+      pkg = new Package(config);
+      loadCustomResources(fixtures, defs);
+
+      // Add a patient to the package that will be overwritten
+      const fisher = new TestFisher(null, defs, pkg);
+      const patient = fisher.fishForStructureDefinition('Patient');
+      patient.id = 'MyPatient';
+      patient.description = 'This should go away';
+      pkg.profiles.push(patient);
+
+      const patientInstance = new InstanceDefinition();
+      patientInstance.resourceType = 'Patient';
+      patientInstance.id = 'FooPatient';
+      patientInstance._instanceMeta.description = 'This should stay';
+      patientInstance._instanceMeta.name = 'StayName';
+      pkg.instances.push(patientInstance);
+
+      exporter = new IGExporter(pkg, defs, path.resolve(fixtures, 'ig-data'));
+      tempOut = temp.mkdirSync('sushi-test');
+      exporter.export(tempOut);
+    });
+
+    afterAll(() => {
+      temp.cleanupSync();
+    });
+
+    it('should copy over resource files', () => {
+      const directoryContents = new Map<string, string[]>();
+      const dirNames = [
+        'capabilities',
+        'extensions',
+        'models',
+        'operations',
+        'profiles',
+        'resources',
+        'vocabulary',
+        'examples'
+      ];
+      for (const dirName of dirNames) {
+        directoryContents.set(dirName, fs.readdirSync(path.join(tempOut, 'input', dirName)));
+      }
+      expect(directoryContents.get('capabilities')).toEqual(['CapabilityStatement-MyCS.json']);
+      expect(directoryContents.get('models')).toEqual(['StructureDefinition-MyLM.json']);
+      expect(directoryContents.get('extensions')).toEqual([
+        'StructureDefinition-patient-birthPlace.json'
+      ]);
+      expect(directoryContents.get('operations')).toEqual(['OperationDefinition-MyOD.json']);
+      expect(directoryContents.get('profiles')).toEqual([
+        'StructureDefinition-MyPatient.json',
+        'StructureDefinition-MyTitlePatient.json'
+      ]);
+      expect(directoryContents.get('resources')).toEqual(['Patient-BazPatient.json']);
+      expect(directoryContents.get('vocabulary')).toEqual(['ValueSet-MyVS.json']);
+      expect(directoryContents.get('examples')).toEqual([
+        'Patient-BarPatient.json',
+        'Patient-FooPatient.json' // Renamed from "PoorlyNamedPatient.json"
+      ]);
+    });
+
+    it('should add basic resource references to the ImplementationGuide resource', () => {
+      const igPath = path.join(tempOut, 'input', 'ImplementationGuide-sushi-test.json');
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'StructureDefinition/MyLM'
+        },
+        name: 'MyLM',
+        exampleBoolean: false
+      });
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'OperationDefinition/MyOD'
+        },
+        name: 'Populate Questionnaire', // Use name over ID
+        exampleBoolean: false
+      });
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'Patient/BazPatient'
+        },
+        name: 'BazPatient',
+        exampleBoolean: false
+      });
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'ValueSet/MyVS'
+        },
+        // eslint-disable-next-line
+        name: "Yes/No/Don't Know", // Use name over ID
+        exampleBoolean: false
+      });
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'StructureDefinition/patient-birthPlace'
+        },
+        name: 'birthPlace', // Use name over ID
+        exampleBoolean: false
+      });
+    });
+
+    it('should overwrite existing resource references in the ImplementationGuide resource', () => {
+      const igPath = path.join(tempOut, 'input', 'ImplementationGuide-sushi-test.json');
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      // Should only have one copy of MyPatient
+      expect(
+        igContent.definition.resource.filter(
+          (r: any) => r.reference.reference === 'StructureDefinition/MyPatient'
+        )
+      ).toHaveLength(1);
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'StructureDefinition/MyPatient'
+        },
+        name: 'MyPatient',
+        exampleBoolean: false
+        // Description is overwritten to be null
+      });
+    });
+
+    it('should add resource references with a description to the ImplementationGuide resource', () => {
+      const igPath = path.join(tempOut, 'input', 'ImplementationGuide-sushi-test.json');
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'CapabilityStatement/MyCS'
+        },
+        name: 'Base FHIR Capability Statement (Empty)', // Use name over ID
+        description: 'Test description',
+        exampleBoolean: false
+      });
+    });
+
+    it('should add example references to the ImplementationGuide resource', () => {
+      const igPath = path.join(tempOut, 'input', 'ImplementationGuide-sushi-test.json');
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'Patient/FooPatient'
+        },
+        // Preserve description and name from SUSHI defined Instance
+        description: 'This should stay',
+        name: 'StayName',
+        exampleBoolean: true
+      });
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'Patient/BarPatient'
+        },
+        name: 'BarPatient',
+        exampleCanonical: 'http://hl7.org/fhir/sushi-test/StructureDefinition/MyPatient'
+      });
+    });
+
+    it('should add resource references with a title to the ImplementationGuide resource', () => {
+      const igPath = path.join(tempOut, 'input', 'ImplementationGuide-sushi-test.json');
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'StructureDefinition/MyTitlePatient'
+        },
+        name: 'This patient has a title',
+        exampleBoolean: false
+      });
+    });
+
+    it('should log an error for input files missing resourceType or id', () => {
+      expect(loggerSpy.getMessageAtIndex(-1, 'error')).toMatch(
+        /.*InvalidPatient.json must define resourceType and id/
+      );
+    });
+  });
+
   describe('#invalid-data-ig', () => {
     let pkg: Package;
     let exporter: IGExporter;
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/ig.ini b/test/ig/fixtures/customized-ig-with-resources/ig-data/ig.ini
new file mode 100644
index 0000000..ba55e21
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/ig.ini
@@ -0,0 +1,13 @@
+[IG]
+ig = input/ImplementationGuide-sushi-test.json
+template = fhir.base.template
+usage-stats-opt-out = true
+copyrightyear = 2018+
+license = CC0-1.0
+version = 0.1.0
+ballotstatus = STU1
+fhirspec = http://hl7.org/fhir/R4/
+excludexml = Yes
+excludejson = Yes
+excludettl = Yes
+excludeMaps = Yes
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/input/capabilities/CapabilityStatement-MyCS.json b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/capabilities/CapabilityStatement-MyCS.json
new file mode 100644
index 0000000..f8cc04c
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/capabilities/CapabilityStatement-MyCS.json
@@ -0,0 +1,52 @@
+{
+  "resourceType" : "CapabilityStatement",
+  "id" : "MyCS",
+  "text" : {
+    "status" : "generated",
+    "div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><h2>Base FHIR Capability Statement (Empty)</h2><div><p>This is the base Capability Statement for FHIR. It represents a server that provides the none of the functionality defined by FHIR. It is provided to use as a template for system designers to build their own Capability Statements from. A capability statement has to contain something, so this contains a read of a Capability Statement</p>\n</div><table><tr><td>Mode</td><td>SERVER</td></tr><tr><td>Description</td><td>An empty Capability Statement</td></tr><tr><td>Transaction</td><td></td></tr><tr><td>System History</td><td></td></tr><tr><td>System Search</td><td></td></tr></table><table><tr><th><b>Resource Type</b></th><th><b>Profile</b></th><th><b title=\"GET a resource (read interaction)\">Read</b></th><th><b title=\"GET all set of resources of the type (search interaction)\">Search</b></th><th><b title=\"PUT a new resource version (update interaction)\">Update</b></th><th><b title=\"POST a new resource (create interaction)\">Create</b></th></tr><tr><td>CapabilityStatement</td><td>y</td><td></td><td></td><td></td></tr></table></div>"
+  },
+  "url" : "http://hl7.org/fhir/CapabilityStatement/MyCS",
+  "version" : "4.0.1",
+  "name" : "Base FHIR Capability Statement (Empty)",
+  "status" : "draft",
+  "experimental" : true,
+  "date" : "2019-11-01T09:29:23+11:00",
+  "publisher" : "FHIR Project Team",
+  "contact" : [{
+    "telecom" : [{
+      "system" : "url",
+      "value" : "http://hl7.org/fhir"
+    }]
+  }],
+  "description" : "Test description",
+  "kind" : "capability",
+  "software" : {
+    "name" : "Insert your software name here..."
+  },
+  "fhirVersion" : "4.0.1",
+  "format" : ["xml",
+  "json"],
+  "rest" : [{
+    "mode" : "server",
+    "documentation" : "An empty Capability Statement",
+    "security" : {
+      "cors" : true,
+      "service" : [{
+        "coding" : [{
+          "system" : "http://terminology.hl7.org/CodeSystem/restful-security-service",
+          "code" : "SMART-on-FHIR",
+          "display" : "SMART-on-FHIR"
+        }],
+        "text" : "See http://docs.smarthealthit.org/"
+      }],
+      "description" : "This is the Capability Statement to declare that the server supports SMART-on-FHIR. See the SMART-on-FHIR docs for the extension that would go with such a server"
+    },
+    "resource" : [{
+      "type" : "CapabilityStatement",
+      "interaction" : [{
+        "code" : "read",
+        "documentation" : "Read CapabilityStatement Resource"
+      }]
+    }]
+  }]
+}
\ No newline at end of file
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/input/examples/Patient-BarPatient.json b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/examples/Patient-BarPatient.json
new file mode 100644
index 0000000..05174a8
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/examples/Patient-BarPatient.json
@@ -0,0 +1,8 @@
+{
+  "resourceType": "Patient",
+  "id": "BarPatient",
+  "meta": {
+    "profile": ["http://hl7.org/fhir/sushi-test/StructureDefinition/MyPatient"]
+  },
+  "name": [{ "family": "Bar" }]
+}
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/input/examples/PoorlyNamedPatient.json b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/examples/PoorlyNamedPatient.json
new file mode 100644
index 0000000..990823f
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/examples/PoorlyNamedPatient.json
@@ -0,0 +1,5 @@
+{
+  "resourceType": "Patient",
+  "id": "FooPatient",
+  "name": [{ "family": "Foo" }]
+}
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/input/extensions/StructureDefinition-patient-birthPlace.json b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/extensions/StructureDefinition-patient-birthPlace.json
new file mode 100644
index 0000000..a6c06ea
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/extensions/StructureDefinition-patient-birthPlace.json
@@ -0,0 +1,46 @@
+{
+  "resourceType" : "StructureDefinition",
+  "id" : "patient-birthPlace",
+  "url" : "http://hl7.org/fhir/StructureDefinition/patient-birthPlace",
+  "version" : "4.0.1",
+  "name" : "birthPlace",
+  "status" : "draft",
+  "fhirVersion" : "4.0.1",
+  "kind" : "complex-type",
+  "abstract" : false,
+  "context" : [{
+    "type" : "element",
+    "expression" : "Patient"
+  }],
+  "type" : "Extension",
+  "baseDefinition" : "http://hl7.org/fhir/StructureDefinition/Extension",
+  "derivation" : "constraint",
+  "differential" : {
+    "element" : [{
+      "id" : "Extension",
+      "path" : "Extension",
+      "short" : "Place of Birth for patient",
+      "definition" : "The registered place of birth of the patient. A sytem may use the address.text if they don't store the birthPlace address in discrete elements.",
+      "min" : 0,
+      "max" : "1"
+    },
+    {
+      "id" : "Extension.extension",
+      "path" : "Extension.extension",
+      "max" : "0"
+    },
+    {
+      "id" : "Extension.url",
+      "path" : "Extension.url",
+      "fixedUri" : "http://hl7.org/fhir/StructureDefinition/patient-birthPlace"
+    },
+    {
+      "id" : "Extension.value[x]",
+      "path" : "Extension.value[x]",
+      "min" : 1,
+      "type" : [{
+        "code" : "Address"
+      }]
+    }]
+  }
+}
\ No newline at end of file
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/input/models/StructureDefinition-MyLM.json b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/models/StructureDefinition-MyLM.json
new file mode 100644
index 0000000..ea55556
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/models/StructureDefinition-MyLM.json
@@ -0,0 +1,25 @@
+{
+  "resourceType": "StructureDefinition",
+  "id": "MyLM",
+  "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/MyLM",
+  "name": "MyLM",
+  "status": "active",
+  "fhirVersion": "4.0.1",
+  "type": "MyLM",
+  "baseDefinition" : "http://hl7.org/fhir/StructureDefinition/Element",
+  "kind": "logical",
+  "derivation": "specialization",
+  "snapshot": {
+    "element": [
+      {
+        "id": "modeling-woo",
+        "path": "modeling-woo",
+        "type": [
+          {
+            "code": "boolean"
+          }
+        ]
+      }
+    ]
+  }
+}
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/input/operations/OperationDefinition-MyOD.json b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/operations/OperationDefinition-MyOD.json
new file mode 100644
index 0000000..16f2e2c
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/operations/OperationDefinition-MyOD.json
@@ -0,0 +1,104 @@
+{
+  "resourceType": "OperationDefinition",
+  "id": "MyOD",
+  "text": {
+    "status": "generated",
+    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative with Details</b></p><p><b>id</b>: example</p><p><b>url</b>: <b>http://h7.org/fhir/OperationDefinition/example</b></p><p><b>version</b>: B</p><p><b>name</b>: Populate Questionnaire</p><p><b>status</b>: draft</p><p><b>kind</b>: operation</p><p><b>date</b>: 04/08/2015</p><p><b>publisher</b>: Acme Healthcare Services</p><p><b>contact</b>: </p><p><b>description</b>: Limited implementation of the Populate Questionnaire implementation</p><p><b>useContext</b>: </p><p><b>jurisdiction</b>: United Kingdom of Great Britain and Northern Ireland (the) <span>(Details : {urn:iso:std:iso:3166 code 'GB' = 'United Kingdom of Great Britain and Northern Ireland', given as 'United Kingdom of Great Britain and Northern Ireland (the)'})</span></p><p><b>code</b>: populate</p><p><b>comment</b>: Only implemented for Labs and Medications so far</p><p><b>base</b>: <a>OperationDefinition/Questionnaire-populate</a></p><p><b>resource</b>: Questionnaire</p><p><b>system</b>: false</p><p><b>type</b>: false</p><p><b>instance</b>: true</p><blockquote><p><b>parameter</b></p><p><b>name</b>: subject</p><p><b>use</b>: in</p><p><b>min</b>: 1</p><p><b>max</b>: 1</p><p><b>documentation</b>: The resource that is to be the *QuestionnaireResponse.subject*. The [[[QuestionnaireResponse]]]      instance will reference the provided subject.  In addition, if the *local* parameter is      set to true, server information about the specified subject will be used to populate the      instance.</p><p><b>type</b>: Reference</p></blockquote><blockquote><p><b>parameter</b></p><p><b>name</b>: local</p><p><b>use</b>: in</p><p><b>min</b>: 0</p><p><b>max</b>: 1</p><p><b>documentation</b>: If the *local* parameter is set to true, server information about the specified subject will be used to populate the instance.</p><p><b>type</b>: Reference</p></blockquote><blockquote><p><b>parameter</b></p><p><b>name</b>: return</p><p><b>use</b>: out</p><p><b>min</b>: 1</p><p><b>max</b>: 1</p><p><b>documentation</b>: The partially (or fully)-populated set of answers for the specified Questionnaire</p><p><b>type</b>: QuestionnaireResponse</p></blockquote><blockquote><p><b>overload</b></p><p><b>parameterName</b>: subject, local</p></blockquote><blockquote><p><b>overload</b></p><p><b>parameterName</b>: subject</p><p><b>comment</b>: local defaults to false when not passed as a parameter</p></blockquote></div>"
+  },
+  "url": "http://h7.org/fhir/OperationDefinition/MyOD",
+  "version": "B",
+  "name": "Populate Questionnaire",
+  "status": "draft",
+  "kind": "operation",
+  "date": "2015-08-04",
+  "publisher": "Acme Healthcare Services",
+  "contact": [
+    {
+      "name": "System Administrator",
+      "telecom": [
+        {
+          "system": "email",
+          "value": "beep@coyote.acme.com"
+        }
+      ]
+    }
+  ],
+  "useContext": [
+    {
+      "code": {
+        "system": "http://build.fhir.org/codesystem-usage-context-type",
+        "code": "venue",
+        "display": "Clinical Venue"
+      },
+      "valueCodeableConcept": {
+        "coding": [
+          {
+            "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
+            "code": "IMP",
+            "display": "inpatient encounter"
+          }
+        ]
+      }
+    }
+  ],
+  "jurisdiction": [
+    {
+      "coding": [
+        {
+          "system": "urn:iso:std:iso:3166",
+          "code": "GB",
+          "display": "United Kingdom of Great Britain and Northern Ireland (the)"
+        }
+      ]
+    }
+  ],
+  "code": "populate",
+  "comment": "Only implemented for Labs and Medications so far",
+  "base": "OperationDefinition/Questionnaire-populate",
+  "resource": [
+    "Questionnaire"
+  ],
+  "system": false,
+  "type": false,
+  "instance": true,
+  "parameter": [
+    {
+      "name": "subject",
+      "use": "in",
+      "min": 1,
+      "max": "1",
+      "documentation": "The resource that is to be the *QuestionnaireResponse.subject*. The [[[QuestionnaireResponse]]]      instance will reference the provided subject.  In addition, if the *local* parameter is      set to true, server information about the specified subject will be used to populate the      instance.",
+      "type": "Reference"
+    },
+    {
+      "name": "local",
+      "use": "in",
+      "min": 0,
+      "max": "1",
+      "documentation": "If the *local* parameter is set to true, server information about the specified subject will be used to populate the instance.",
+      "type": "Reference"
+    },
+    {
+      "name": "return",
+      "use": "out",
+      "min": 1,
+      "max": "1",
+      "documentation": "The partially (or fully)-populated set of answers for the specified Questionnaire",
+      "type": "QuestionnaireResponse"
+    }
+  ],
+  "overload": [
+    {
+      "parameterName": [
+        "subject",
+        "local"
+      ]
+    },
+    {
+      "parameterName": [
+        "subject"
+      ],
+      "comment": "local defaults to false when not passed as a parameter"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/input/profiles/StructureDefinition-MyPatient.json b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/profiles/StructureDefinition-MyPatient.json
new file mode 100644
index 0000000..4f65cc4
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/profiles/StructureDefinition-MyPatient.json
@@ -0,0 +1,25 @@
+{
+  "resourceType": "StructureDefinition",
+  "id": "MyPatient",
+  "url": "http://hl7.org/fhir/sushi-test/StructureDefinition/MyPatient",
+  "name": "MyPatient",
+  "status": "active",
+  "fhirVersion": "4.0.1",
+  "kind": "resource",
+  "type": "Patient",
+  "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient",
+  "derivation": "constraint",
+  "differential": {
+    "element": [
+      {
+        "id": "Patient.deceased[x]",
+        "path": "Patient.deceased[x]",
+        "type": [
+          {
+            "code": "boolean"
+          }
+        ]
+      }
+    ]
+  }
+}
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/input/profiles/StructureDefinition-MyTitlePatient.json b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/profiles/StructureDefinition-MyTitlePatient.json
new file mode 100644
index 0000000..a1dec3d
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/profiles/StructureDefinition-MyTitlePatient.json
@@ -0,0 +1,26 @@
+{
+  "resourceType": "StructureDefinition",
+  "id": "MyTitlePatient",
+  "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/MyTitlePatient",
+  "title": "This patient has a title",
+  "name": "MyTitlePatient",
+  "status": "active",
+  "fhirVersion": "4.0.1",
+  "kind": "resource",
+  "type": "Patient",
+  "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient",
+  "derivation": "constraint",
+  "differential": {
+    "element": [
+      {
+        "id": "Patient.deceased[x]",
+        "path": "Patient.deceased[x]",
+        "type": [
+          {
+            "code": "boolean"
+          }
+        ]
+      }
+    ]
+  }
+}
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/input/resources/InvalidFile.txt b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/resources/InvalidFile.txt
new file mode 100644
index 0000000..fdcf641
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/resources/InvalidFile.txt
@@ -0,0 +1,1 @@
+Invalid file
\ No newline at end of file
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/input/resources/InvalidFile.xml b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/resources/InvalidFile.xml
new file mode 100644
index 0000000..19b16cc
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/resources/InvalidFile.xml
@@ -0,0 +1,1 @@
+<xml></xml>
\ No newline at end of file
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/input/resources/InvalidPatient.json b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/resources/InvalidPatient.json
new file mode 100644
index 0000000..99712a6
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/resources/InvalidPatient.json
@@ -0,0 +1,4 @@
+{
+  "resourceType": "Patient",
+  "name": [{ "family": "Invalid" }]
+}
\ No newline at end of file
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/input/resources/Patient-BazPatient.json b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/resources/Patient-BazPatient.json
new file mode 100644
index 0000000..61f1635
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/resources/Patient-BazPatient.json
@@ -0,0 +1,5 @@
+{
+  "resourceType": "Patient",
+  "id": "BazPatient",
+  "name": [{ "family": "Baz" }]
+}
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/input/vocabulary/ValueSet-MyVS.json b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/vocabulary/ValueSet-MyVS.json
new file mode 100644
index 0000000..205adb3
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/input/vocabulary/ValueSet-MyVS.json
@@ -0,0 +1,47 @@
+{
+  "resourceType": "ValueSet",
+  "id": "MyVS",
+  "url": "http://hl7.org/fhir/ValueSet/MyVS",
+  "version": "4.0.1",
+  "name": "Yes/No/Don't Know",
+  "status": "draft",
+  "compose": {
+    "include": [
+      {
+        "valueSet": [
+          "http://terminology.hl7.org/ValueSet/v2-0136"
+        ]
+      },
+      {
+        "system": "http://terminology.hl7.org/CodeSystem/data-absent-reason",
+        "concept": [
+          {
+            "code": "asked-unknown",
+            "display": "Don't know"
+          }
+        ]
+      }
+    ]
+  },
+  "expansion": {
+    "identifier": "urn:uuid:bf99fe50-2c2b-41ad-bd63-bee6919810b4",
+    "timestamp": "2015-07-14T10:00:00Z",
+    "contains": [
+      {
+        "system": "http://terminology.hl7.org/CodeSystem/v2-0136",
+        "code": "Y",
+        "display": "Yes"
+      },
+      {
+        "system": "http://terminology.hl7.org/CodeSystem/v2-0136",
+        "code": "N",
+        "display": "No"
+      },
+      {
+        "system": "http://terminology.hl7.org/CodeSystem/data-absent-reason",
+        "code": "asked-unknown",
+        "display": "Don't know"
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/test/ig/fixtures/customized-ig-with-resources/ig-data/package-list.json b/test/ig/fixtures/customized-ig-with-resources/ig-data/package-list.json
new file mode 100644
index 0000000..ab47abd
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/ig-data/package-list.json
@@ -0,0 +1,33 @@
+{
+  "package-id": "sushi-test",
+  "title": "FSH Test IG",
+  "canonical": "http://hl7.org/fhir/sushi-test",
+  "introduction": "Custom intro",
+  "list": [
+    {
+      "version": "current",
+      "desc": "Continuous Integration Build (latest in version control)",
+      "path": "http://build.fhir.org/ig/fhir/sushi-test",
+      "status": "ci-build",
+      "current": true
+    },
+    {
+      "version": "0.2.0",
+      "fhirversion": "4.0.1",
+      "date": "2019-09-01",
+      "desc": "Second STU ballot (Sep 2019 Ballot)",
+      "path": "http://hl7.org/fhir/sushi-test/stu2",
+      "status": "ballot",
+      "sequence": "STU 2"
+    },
+    {
+      "version": "0.1.0",
+      "fhirversion": "4.0.1",
+      "date": "2019-05-01",
+      "desc": "Initial STU ballot (May 2019 Ballot)",
+      "path": "http://hl7.org/fhir/sushi-test/stu1",
+      "status": "ballot",
+      "sequence": "STU 1"
+    }
+  ]
+}
diff --git a/test/ig/fixtures/customized-ig-with-resources/package.json b/test/ig/fixtures/customized-ig-with-resources/package.json
new file mode 100644
index 0000000..34f4cb5
--- /dev/null
+++ b/test/ig/fixtures/customized-ig-with-resources/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "sushi-test",
+  "version" : "0.1.0",
+  "canonical" : "http://hl7.org/fhir/sushi-test",
+  "url" : "http://hl7.org/fhir/sushi-test",
+  "title" : "FSH Test IG",
+  "description": "Provides a simple example of how FSH can be used to create an IG",
+  "dependencies": {
+     "hl7.fhir.r4.core": "4.0.1"
+  },
+  "language": "en",
+  "author": "James Tuna",
+  "maintainers": [
+    {
+      "name": "Bill Cod",
+      "email": "cod@reef.gov"
+    }
+  ],
+  "license": "CC0-1.0"
+ }
\ No newline at end of file