Repository: FHIR/sushi
Pull request #577 (2020-08-20 21:56:35)

Allow SUSHI to run without config.yaml
This adds the ability for SUSHI to run without a config.yaml file. When no config.yaml is present, SUSHI looks for an `ImplementationGuide.json` file in the `../input` folder (assuming SUSHI is running in the IG Pub context), and if one is found, will extract the `canonical` and any `dependencies` from that file, and use that to process the FSH. SUSHI will run in `FSHOnly` mode if it has to extract the configuration in this way. If no `ImplementationGuide.json` is found, SUSHI will just fail for having no configuration as it used to.

ngfreiter: Ok @cmoesel, this should have all the changes you asked for, except for moving `extractConfiguration` into `ensureConfiguration`, see my comment https://github.com/FHIR/sushi/pull/577#discussion_r475620571 for why I did not yet do that. I can still do that, I just wasn't totally sure it was the right choice, and wanted to wait to hear what you thought of my comment before doing anything.
ngfreiter: > I did some additional manual testing on this and found the following results:
> 
> * with `config.yaml` -- each resource has the `version` from `config.yaml` populated in its definition
> * without `config.yaml` -- the `version` is _not_ populated in each definition (even if it is specified in IG JSON)
> 
> To get better parity w/ config, I think that if the IG JSON has a `version`, you should put it in the config so it gets populated into the resources.

I came across this difference when implementing, and found that, at least in my experience, the IG Publisher automatically adds `version`. When someone doesn't have a `config.yaml`, it seemed to me like that is a signal for SUSHI to be as hands off as possible, and since the Publisher adds version anyway, I thought it made sense to leave it out.

Does the Publisher adding version change your opinion? If not, let me know and I can change it to extract `version` as well.
ngfreiter: PS, sorry @cmoesel I should've noted the `version` thing instead of making you have to dig to find it.
cmoesel: > Does the Publisher adding version change your opinion? 

Not really, as I already knew it does that.  I just like having SUSHI generate profiles that are as complete as possible -- for people who want to view them pre-publisher or for other workflows where perhaps they're not using the publisher at all?

I see FSHOnly (and zero config) more as a signal that they don't want SUSHI to touch anything _except_ the definitional files -- but I think the definitional files should still be fully processed the same way.  If we want to consider no longer populating `version` in the definitions, we can -- but I think that if we do change it, we should change it across the board.
ngfreiter: > Not really, as I already knew it does that. I just like having SUSHI generate profiles that are as complete as possible -- for people who want to view them pre-publisher or for other workflows where perhaps they're not using the publisher at all?

Somehow I had a feeling you already knew that 😉 . I will make the change.


ngfreiter: Version, coming to a PR near you in https://github.com/FHIR/sushi/pull/577/commits/67daafac4119532250149f82cf690c141245ea58

Patch:
diff --git a/src/fshtypes/Configuration.ts b/src/fshtypes/Configuration.ts
index d94897c..91a4848 100644
--- a/src/fshtypes/Configuration.ts
+++ b/src/fshtypes/Configuration.ts
@@ -27,9 +27,9 @@ import {
  * @see {@link https://github.com/FHIR/sample-ig/blob/master/input/includes/menu.xml}
  */
 export type Configuration = {
-  filePath: string;
+  filePath?: string;
   canonical: string;
-  id: string;
+  id?: string;
   meta?: Meta;
   implicitRules?: string;
   language?: string;
@@ -38,10 +38,10 @@ export type Configuration = {
   extension?: Extension[];
   modifierExtension?: Extension[];
   url?: string;
-  version: string;
-  name: string;
+  version?: string;
+  name?: string;
   title?: string;
-  status: ImplementationGuideStatus;
+  status?: ImplementationGuideStatus;
   experimental?: boolean;
   date?: string;
 
diff --git a/src/import/ensureConfiguration.ts b/src/import/ensureConfigurationFile.ts
similarity index 99%
rename from src/import/ensureConfiguration.ts
rename to src/import/ensureConfigurationFile.ts
index 65b0e9f..66045dd 100644
--- a/src/import/ensureConfiguration.ts
+++ b/src/import/ensureConfigurationFile.ts
@@ -56,7 +56,7 @@ class CommentPair extends YAMLPair {
  * @param allowFromScratch - create a config file from scratch even if package.json isn't found
  * @returns {string|undefined} path to the config file or undefined if it couldn't find or create one
  */
-export function ensureConfiguration(root: string, allowFromScratch = false): string {
+export function ensureConfigurationFile(root: string, allowFromScratch = false): string {
   const configPath = [path.join(root, 'config.yaml'), path.join(root, 'config.yml')].find(
     fs.existsSync
   );
diff --git a/src/import/index.ts b/src/import/index.ts
index 4777e2c..e15db14 100644
--- a/src/import/index.ts
+++ b/src/import/index.ts
@@ -7,4 +7,5 @@ export * from './RawFSH';
 export * from './importConfiguration';
 export * from './YAMLConfiguration';
 export * from './parseCodeLexeme';
-export * from './ensureConfiguration';
+export * from './ensureConfigurationFile';
+export * from './loadConfigurationFromIgResource';
diff --git a/src/import/loadConfigurationFromIgResource.ts b/src/import/loadConfigurationFromIgResource.ts
new file mode 100644
index 0000000..ba44e6b
--- /dev/null
+++ b/src/import/loadConfigurationFromIgResource.ts
@@ -0,0 +1,69 @@
+import path from 'path';
+import fs from 'fs-extra';
+import ini from 'ini';
+import { Fhir as FHIRConverter } from 'fhir/fhir';
+import { Configuration } from '../fshtypes';
+import { ImplementationGuide } from '../fhirtypes';
+import { logger } from '../utils';
+
+/**
+ * Attempts to find an ig resource file and extract the required configuration properties from it
+ * @param input - path to the input directory
+ * @returns {Configuration} the extracted configuration
+ */
+export function loadConfigurationFromIgResource(input: string): Configuration | null {
+  // First, look in the ig.ini file for a path to the IG resource
+  let igPath: string;
+  const igIniPath = path.join(input, '..', 'ig.ini');
+  if (fs.existsSync(igIniPath)) {
+    try {
+      const igIni = ini.parse(fs.readFileSync(igIniPath, 'utf-8'));
+      igPath = path.join(input, '..', igIni.ig);
+    } catch {}
+  }
+  // Make a list of possible path, if ig.ini exists and points to a ig file, add just that
+  // otherwise consider all files in the input folder of the ig
+  const igInputPath = path.join(input, '..', 'input');
+  const possibleIgPaths: string[] = [];
+  if (fs.existsSync(igPath)) {
+    possibleIgPaths.push(igPath);
+  } else if (fs.existsSync(igInputPath)) {
+    possibleIgPaths.push(...fs.readdirSync(igInputPath).map(file => path.join(igInputPath, file)));
+  }
+  // Go through each possible path, and check each xml/json file to see if they are an IG resource
+  const fhirConverter = new FHIRConverter();
+  let igResource: ImplementationGuide;
+  let multipleIgs = false;
+  possibleIgPaths.forEach(filePath => {
+    let fileContent;
+    if (path.extname(filePath) === '.json') {
+      fileContent = fs.readJSONSync(filePath);
+    } else if (path.extname(filePath) === '.xml') {
+      fileContent = fhirConverter.xmlToObj(fs.readFileSync(filePath).toString());
+    }
+    if (fileContent?.resourceType === 'ImplementationGuide') {
+      // If 2 possible IG resources are found, we cannot tell which to use, so return
+      if (igResource != null) {
+        multipleIgs = true;
+        logger.error(
+          'Multiple possible ImplementationGuide resources in "input" folder, so no configuration can be extracted.' +
+            ' Ensure only one ImplementationGuide resource is in the "input" folder, or give the path to the desired resource in ig.ini.'
+        );
+      }
+      igResource = fileContent;
+      igPath = filePath;
+    }
+  });
+  // Extract the configuration from the resource
+  if (igResource && igResource.url && !multipleIgs) {
+    logger.info(`Extracting FSHOnly configuration from ${igPath}.`);
+    return {
+      canonical: igResource.url.replace(/\/ImplementationGuide.*/, ''),
+      FSHOnly: true,
+      dependencies: igResource.dependsOn?.filter(dep => dep.packageId && dep.version),
+      fhirVersion: igResource.fhirVersion ?? [],
+      version: igResource.version
+    };
+  }
+  return null;
+}
diff --git a/src/utils/Processing.ts b/src/utils/Processing.ts
index 83fc43a..1cc881c 100644
--- a/src/utils/Processing.ts
+++ b/src/utils/Processing.ts
@@ -4,7 +4,13 @@ import readlineSync from 'readline-sync';
 import { logger } from './FSHLogger';
 import { loadDependency } from '../fhirdefs/load';
 import { FHIRDefinitions } from '../fhirdefs';
-import { FSHTank, RawFSH, importText, ensureConfiguration, importConfiguration } from '../import';
+import {
+  FSHTank,
+  RawFSH,
+  importText,
+  ensureConfigurationFile,
+  importConfiguration
+} from '../import';
 import { cloneDeep, padEnd } from 'lodash';
 import YAML from 'yaml';
 import { Package } from '../export';
@@ -19,6 +25,7 @@ import {
   filterProfileInstances
 } from './InstanceDefinitionUtils';
 import { Configuration } from '../fshtypes';
+import { loadConfigurationFromIgResource } from '../import/loadConfigurationFromIgResource';
 
 export function findInputDir(input: string): string {
   // If no input folder is specified, set default to current directory
@@ -57,13 +64,21 @@ export function ensureOutputDir(input: string, output: string, isIgPubContext: b
 }
 
 export function readConfig(input: string): Configuration {
-  const configPath = ensureConfiguration(input);
+  const configPath = ensureConfigurationFile(input);
+  let config: Configuration;
   if (configPath == null || !fs.existsSync(configPath)) {
-    logger.error('No config.yaml in FSH definition folder.');
+    config = loadConfigurationFromIgResource(input);
+  } else {
+    const configYaml = fs.readFileSync(configPath, 'utf8');
+    config = importConfiguration(configYaml, configPath);
+  }
+  if (!config) {
+    logger.error(
+      'No config.yaml in FSH definition folder, and no configuration could' +
+        ' be extracted from an ImplementationGuide resource.'
+    );
     throw Error;
   }
-  const configYaml = fs.readFileSync(configPath, 'utf8');
-  const config = importConfiguration(configYaml, configPath);
   if (!config.fhirVersion.includes('4.0.1')) {
     logger.error(
       'The config.yaml must specify FHIR R4 as a fhirVersion. Be sure to' +


Test patch:
diff --git a/test/import/ensureConfiguration.test.ts b/test/import/ensureConfigurationFile.test.ts
similarity index 96%
rename from test/import/ensureConfiguration.test.ts
rename to test/import/ensureConfigurationFile.test.ts
index 4d42f1a..8ae9a60 100644
--- a/test/import/ensureConfiguration.test.ts
+++ b/test/import/ensureConfigurationFile.test.ts
@@ -3,9 +3,9 @@ import fs from 'fs-extra';
 import temp from 'temp';
 import YAML from 'yaml';
 import { loggerSpy } from '../testhelpers/loggerSpy';
-import { ensureConfiguration } from '../../src/import';
+import { ensureConfigurationFile } from '../../src/import';
 
-describe('ensureConfiguration', () => {
+describe('ensureConfigurationFile', () => {
   // Track temp files/folders for cleanup
   temp.track();
 
@@ -19,7 +19,7 @@ describe('ensureConfiguration', () => {
 
   it('should return the path to a pre-existing config.yaml', () => {
     const tank = path.join(__dirname, 'fixtures', 'existing-config-yaml');
-    const configPath = ensureConfiguration(tank);
+    const configPath = ensureConfigurationFile(tank);
     expect(configPath).toBe(path.join(tank, 'config.yaml'));
     // Generating a config causes a warning, so there should be NO warnings in this case
     expect(loggerSpy.getAllLogs('warn')).toHaveLength(0);
@@ -27,7 +27,7 @@ describe('ensureConfiguration', () => {
 
   it('should return the path to a pre-existing config.yml', () => {
     const tank = path.join(__dirname, 'fixtures', 'existing-config-yml');
-    const configPath = ensureConfiguration(tank);
+    const configPath = ensureConfigurationFile(tank);
     expect(configPath).toBe(path.join(tank, 'config.yml'));
     // Generating a config causes a warning, so there should be NO warnings in this case
     expect(loggerSpy.getAllLogs('warn')).toHaveLength(0);
@@ -35,7 +35,7 @@ describe('ensureConfiguration', () => {
 
   it('should return undefined on an empty folder when fromScratch is false', () => {
     const tank = temp.mkdirSync('sushi-test');
-    const configPath = ensureConfiguration(tank, false);
+    const configPath = ensureConfigurationFile(tank, false);
     expect(configPath).toBeUndefined();
     // Generating a config causes a warning, so there should be NO warnings in this case
     expect(loggerSpy.getAllLogs('warn')).toHaveLength(0);
@@ -43,7 +43,7 @@ describe('ensureConfiguration', () => {
 
   it('should generate a default config on an empty folder when fromScratch is true', () => {
     const tank = temp.mkdirSync('sushi-test');
-    const configPath = ensureConfiguration(tank, true);
+    const configPath = ensureConfigurationFile(tank, true);
     expect(configPath).toBe(path.join(tank, 'config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
@@ -115,7 +115,7 @@ describe('ensureConfiguration', () => {
     // Copy the fixture to a temp folder since we actually create files in the tank
     const tank = temp.mkdirSync('sushi-test');
     fs.copySync(path.join(__dirname, 'fixtures', 'package-json-only'), tank);
-    const configPath = ensureConfiguration(tank);
+    const configPath = ensureConfigurationFile(tank);
     expect(configPath).toBe(path.join(tank, 'config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
@@ -197,8 +197,8 @@ describe('ensureConfiguration', () => {
     delete packageJSON.license;
     fs.writeJsonSync(path.join(tank, 'package.json'), packageJSON);
 
-    // ensureConfiguration
-    const configPath = ensureConfiguration(tank);
+    // ensureConfigurationFile
+    const configPath = ensureConfigurationFile(tank);
     expect(configPath).toBe(path.join(tank, 'config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
@@ -264,8 +264,8 @@ describe('ensureConfiguration', () => {
     ];
     fs.writeJsonSync(path.join(tank, 'package.json'), packageJSON);
 
-    // ensureConfiguration
-    const configPath = ensureConfiguration(tank);
+    // ensureConfigurationFile
+    const configPath = ensureConfigurationFile(tank);
     expect(configPath).toBe(path.join(tank, 'config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
@@ -346,7 +346,7 @@ describe('ensureConfiguration', () => {
     // Copy the fixture to a temp folder since we actually create files in the tank
     const tank = temp.mkdirSync('sushi-test');
     fs.copySync(path.join(__dirname, 'fixtures', 'package-json-and-ig-ini'), tank);
-    const configPath = ensureConfiguration(tank);
+    const configPath = ensureConfigurationFile(tank);
     expect(configPath).toBe(path.join(tank, 'config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
@@ -422,8 +422,8 @@ describe('ensureConfiguration', () => {
     const newIgIni = igIni.split('\n').slice(0, 4).join('\n');
     fs.writeFileSync(path.join(tank, 'ig-data', 'ig.ini'), newIgIni, 'utf8');
 
-    // ensureConfiguration
-    const configPath = ensureConfiguration(tank);
+    // ensureConfigurationFile
+    const configPath = ensureConfigurationFile(tank);
     expect(configPath).toBe(path.join(tank, 'config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
@@ -480,7 +480,7 @@ describe('ensureConfiguration', () => {
     // Copy the fixture to a temp folder since we actually create files in the tank
     const tank = temp.mkdirSync('sushi-test');
     fs.copySync(path.join(__dirname, 'fixtures', 'package-json-and-package-list'), tank);
-    const configPath = ensureConfiguration(tank);
+    const configPath = ensureConfigurationFile(tank);
     expect(configPath).toBe(path.join(tank, 'config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
@@ -581,8 +581,8 @@ describe('ensureConfiguration', () => {
     packageList.list[0].desc = 'Continuous Integration Build (latest in version control)';
     fs.writeJsonSync(path.join(tank, 'ig-data', 'package-list.json'), packageList);
 
-    // ensureConfiguration
-    const configPath = ensureConfiguration(tank);
+    // ensureConfigurationFile
+    const configPath = ensureConfigurationFile(tank);
     expect(configPath).toBe(path.join(tank, 'config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
@@ -657,7 +657,7 @@ describe('ensureConfiguration', () => {
     // Copy the fixture to a temp folder since we actually create files in the tank
     const tank = temp.mkdirSync('sushi-test');
     fs.copySync(path.join(__dirname, 'fixtures', 'package-json-and-menu-xml'), tank);
-    const configPath = ensureConfiguration(tank);
+    const configPath = ensureConfigurationFile(tank);
     expect(configPath).toBe(path.join(tank, 'config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
@@ -745,7 +745,7 @@ describe('ensureConfiguration', () => {
     // Copy the fixture to a temp folder since we actually create files in the tank
     const tank = temp.mkdirSync('sushi-test');
     fs.copySync(path.join(__dirname, 'fixtures', 'package-json-and-index-md'), tank);
-    const configPath = ensureConfiguration(tank);
+    const configPath = ensureConfigurationFile(tank);
     expect(configPath).toBe(path.join(tank, 'config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
diff --git a/test/import/fixtures/ig-JSON-no-ig-JSON/fsh/not-config.yaml b/test/import/fixtures/ig-JSON-no-ig-JSON/fsh/not-config.yaml
new file mode 100644
index 0000000..23e34e0
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-no-ig-JSON/fsh/not-config.yaml
@@ -0,0 +1,1 @@
+# This is not a config file
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON-no-ig-JSON/input/NotImplementationGuide.json b/test/import/fixtures/ig-JSON-no-ig-JSON/input/NotImplementationGuide.json
new file mode 100644
index 0000000..7c09e08
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-no-ig-JSON/input/NotImplementationGuide.json
@@ -0,0 +1,4 @@
+{
+  "resourceType": "NotImplementationGuide",
+  "url": "http://example.org"
+}
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON-no-ig-JSON/input/NotImplementationGuide.txt b/test/import/fixtures/ig-JSON-no-ig-JSON/input/NotImplementationGuide.txt
new file mode 100644
index 0000000..8e1fc94
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-no-ig-JSON/input/NotImplementationGuide.txt
@@ -0,0 +1,1 @@
+Not an ImplementationGuide JSON
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON-no-url/fsh/not-config.yaml b/test/import/fixtures/ig-JSON-no-url/fsh/not-config.yaml
new file mode 100644
index 0000000..23e34e0
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-no-url/fsh/not-config.yaml
@@ -0,0 +1,1 @@
+# This is not a config file
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON-no-url/input/ImplementationGuide.json b/test/import/fixtures/ig-JSON-no-url/input/ImplementationGuide.json
new file mode 100644
index 0000000..aeabb2e
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-no-url/input/ImplementationGuide.json
@@ -0,0 +1,3 @@
+{
+  "resourceType": "ImplementationGuide"
+}
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON-only-url/fsh/not-config.yaml b/test/import/fixtures/ig-JSON-only-url/fsh/not-config.yaml
new file mode 100644
index 0000000..23e34e0
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-only-url/fsh/not-config.yaml
@@ -0,0 +1,1 @@
+# This is not a config file
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON-only-url/input/ImplementationGuide.json b/test/import/fixtures/ig-JSON-only-url/input/ImplementationGuide.json
new file mode 100644
index 0000000..2d78bea
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-only-url/input/ImplementationGuide.json
@@ -0,0 +1,4 @@
+{
+  "resourceType": "ImplementationGuide",
+  "url": "http://example.org/ImplementationGuide"
+}
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON-other-files/fsh/not-config.yaml b/test/import/fixtures/ig-JSON-other-files/fsh/not-config.yaml
new file mode 100644
index 0000000..23e34e0
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-other-files/fsh/not-config.yaml
@@ -0,0 +1,1 @@
+# This is not a config file
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON-other-files/input/ImplementationGuide.json b/test/import/fixtures/ig-JSON-other-files/input/ImplementationGuide.json
new file mode 100644
index 0000000..2d78bea
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-other-files/input/ImplementationGuide.json
@@ -0,0 +1,4 @@
+{
+  "resourceType": "ImplementationGuide",
+  "url": "http://example.org/ImplementationGuide"
+}
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON-other-files/input/NotImplementationGuide.json b/test/import/fixtures/ig-JSON-other-files/input/NotImplementationGuide.json
new file mode 100644
index 0000000..7c09e08
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-other-files/input/NotImplementationGuide.json
@@ -0,0 +1,4 @@
+{
+  "resourceType": "NotImplementationGuide",
+  "url": "http://example.org"
+}
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON-other-files/input/NotImplementationGuide.txt b/test/import/fixtures/ig-JSON-other-files/input/NotImplementationGuide.txt
new file mode 100644
index 0000000..8e1fc94
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-other-files/input/NotImplementationGuide.txt
@@ -0,0 +1,1 @@
+Not an ImplementationGuide JSON
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON-with-ini/fsh/not-config.yaml b/test/import/fixtures/ig-JSON-with-ini/fsh/not-config.yaml
new file mode 100644
index 0000000..23e34e0
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-with-ini/fsh/not-config.yaml
@@ -0,0 +1,1 @@
+# This is not a config file
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON-with-ini/ig.ini b/test/import/fixtures/ig-JSON-with-ini/ig.ini
new file mode 100644
index 0000000..252253a
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-with-ini/ig.ini
@@ -0,0 +1,2 @@
+ig = sneaky-input/ImplementationGuide.json
+template = fhir.base.template
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON-with-ini/input/ImplementationGuide.json b/test/import/fixtures/ig-JSON-with-ini/input/ImplementationGuide.json
new file mode 100644
index 0000000..2d78bea
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-with-ini/input/ImplementationGuide.json
@@ -0,0 +1,4 @@
+{
+  "resourceType": "ImplementationGuide",
+  "url": "http://example.org/ImplementationGuide"
+}
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON-with-ini/sneaky-input/ImplementationGuide.json b/test/import/fixtures/ig-JSON-with-ini/sneaky-input/ImplementationGuide.json
new file mode 100644
index 0000000..2d78bea
--- /dev/null
+++ b/test/import/fixtures/ig-JSON-with-ini/sneaky-input/ImplementationGuide.json
@@ -0,0 +1,4 @@
+{
+  "resourceType": "ImplementationGuide",
+  "url": "http://example.org/ImplementationGuide"
+}
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON/fsh/not-config.yaml b/test/import/fixtures/ig-JSON/fsh/not-config.yaml
new file mode 100644
index 0000000..23e34e0
--- /dev/null
+++ b/test/import/fixtures/ig-JSON/fsh/not-config.yaml
@@ -0,0 +1,1 @@
+# This is not a config file
\ No newline at end of file
diff --git a/test/import/fixtures/ig-JSON/input/ImplementationGuide.json b/test/import/fixtures/ig-JSON/input/ImplementationGuide.json
new file mode 100644
index 0000000..28db46d
--- /dev/null
+++ b/test/import/fixtures/ig-JSON/input/ImplementationGuide.json
@@ -0,0 +1,14 @@
+{
+  "resourceType": "ImplementationGuide",
+  "url": "http://example.org/ImplementationGuide",
+  "dependsOn": [
+    { "packageId": "foo.bar", "version": "1.2.3" },
+    { "packageId": "bar.foo", "version": "current" },
+    {"packageId": "just.packageId"},
+    {"version": "just.version"}
+  ],
+  "fhirVersion": [
+    "4.0.1"
+  ],
+  "version": "1.0.0"
+}
diff --git a/test/import/fixtures/ig-XML-and-ig-JSON/fsh/not-config.yaml b/test/import/fixtures/ig-XML-and-ig-JSON/fsh/not-config.yaml
new file mode 100644
index 0000000..23e34e0
--- /dev/null
+++ b/test/import/fixtures/ig-XML-and-ig-JSON/fsh/not-config.yaml
@@ -0,0 +1,1 @@
+# This is not a config file
\ No newline at end of file
diff --git a/test/import/fixtures/ig-XML-and-ig-JSON/input/ImplementationGuide.json b/test/import/fixtures/ig-XML-and-ig-JSON/input/ImplementationGuide.json
new file mode 100644
index 0000000..b5dae51
--- /dev/null
+++ b/test/import/fixtures/ig-XML-and-ig-JSON/input/ImplementationGuide.json
@@ -0,0 +1,10 @@
+{
+  "resourceType": "ImplementationGuide",
+  "url": "http://example.org/ImplementationGuide",
+  "dependsOn": [
+    { "packageId": "foo.bar", "version": "1.2.3" },
+    { "packageId": "bar.foo", "version": "current" },
+    {"packageId": "just.packageId"},
+    {"version": "just.version"}
+  ]
+}
diff --git a/test/import/fixtures/ig-XML-and-ig-JSON/input/ImplementationGuide.xml b/test/import/fixtures/ig-XML-and-ig-JSON/input/ImplementationGuide.xml
new file mode 100644
index 0000000..b7331fc
--- /dev/null
+++ b/test/import/fixtures/ig-XML-and-ig-JSON/input/ImplementationGuide.xml
@@ -0,0 +1,12 @@
+
+<ImplementationGuide>
+  <url value="http://example.org/ImplementationGuide"/>
+  <dependsOn>
+    <packageId value="foo.bar"/>
+    <version value="1.2.3"/>
+  </dependsOn>
+  <dependsOn>
+    <packageId value="bar.foo"/>
+    <version value="current"/>
+  </dependsOn>
+</ImplementationGuide>
\ No newline at end of file
diff --git a/test/import/fixtures/ig-XML/fsh/not-config.yaml b/test/import/fixtures/ig-XML/fsh/not-config.yaml
new file mode 100644
index 0000000..23e34e0
--- /dev/null
+++ b/test/import/fixtures/ig-XML/fsh/not-config.yaml
@@ -0,0 +1,1 @@
+# This is not a config file
\ No newline at end of file
diff --git a/test/import/fixtures/ig-XML/input/ImplementationGuide.xml b/test/import/fixtures/ig-XML/input/ImplementationGuide.xml
new file mode 100644
index 0000000..7ff041d
--- /dev/null
+++ b/test/import/fixtures/ig-XML/input/ImplementationGuide.xml
@@ -0,0 +1,13 @@
+
+<ImplementationGuide>
+  <url value="http://example.org/ImplementationGuide"/>
+  <dependsOn>
+    <packageId value="foo.bar"/>
+    <version value="1.2.3"/>
+  </dependsOn>
+  <dependsOn>
+    <packageId value="bar.foo"/>
+    <version value="current"/>
+  </dependsOn>
+  <fhirVersion value="4.0.1"/>
+</ImplementationGuide>
\ No newline at end of file
diff --git a/test/import/loadConfigurationFromIgResource.test.ts b/test/import/loadConfigurationFromIgResource.test.ts
new file mode 100644
index 0000000..36e1b46
--- /dev/null
+++ b/test/import/loadConfigurationFromIgResource.test.ts
@@ -0,0 +1,109 @@
+import path from 'path';
+import { escapeRegExp } from 'lodash';
+import { loggerSpy } from '../testhelpers/loggerSpy';
+import { loadConfigurationFromIgResource } from '../../src/import';
+
+describe('loadConfigurationFromIgResource', () => {
+  beforeEach(() => {
+    loggerSpy.reset();
+  });
+
+  it('should extract a configuration pointed to by an ig.ini file', () => {
+    // Note that the potentially valid IG at ig-JSON-with-ini/input/ImplementationGuide.json
+    // is ignored in favor of the one pointed to by ig.ini
+    const inputPath = path.join(__dirname, 'fixtures', 'ig-JSON-with-ini', 'fsh');
+    const config = loadConfigurationFromIgResource(inputPath);
+    expect(config).toEqual({ canonical: 'http://example.org', FSHOnly: true, fhirVersion: [] });
+    expect(loggerSpy.getLastMessage('info')).toMatch(
+      new RegExp(
+        `from ${escapeRegExp(
+          path.join(inputPath, '..', 'sneaky-input', 'ImplementationGuide.json')
+        )}`
+      )
+    );
+  });
+
+  it('should extract a configuration with only a url', () => {
+    const inputPath = path.join(__dirname, 'fixtures', 'ig-JSON-only-url', 'fsh');
+    const config = loadConfigurationFromIgResource(inputPath);
+    expect(config).toEqual({ canonical: 'http://example.org', FSHOnly: true, fhirVersion: [] });
+    expect(loggerSpy.getLastMessage('info')).toMatch(
+      new RegExp(
+        `from ${escapeRegExp(path.join(inputPath, '..', 'input', 'ImplementationGuide.json'))}`
+      )
+    );
+  });
+
+  it('should extract a configuration with a url and other data', () => {
+    const inputPath = path.join(__dirname, 'fixtures', 'ig-JSON', 'fsh');
+    const config = loadConfigurationFromIgResource(inputPath);
+    expect(config).toEqual({
+      canonical: 'http://example.org',
+      FSHOnly: true,
+      dependencies: [
+        { packageId: 'foo.bar', version: '1.2.3' },
+        { packageId: 'bar.foo', version: 'current' }
+      ],
+      fhirVersion: ['4.0.1'],
+      version: '1.0.0'
+    });
+    expect(loggerSpy.getLastMessage('info')).toMatch(
+      new RegExp(
+        `from ${escapeRegExp(path.join(inputPath, '..', 'input', 'ImplementationGuide.json'))}`
+      )
+    );
+  });
+
+  it('should extract an XML configuration with a url and dependencies', () => {
+    const inputPath = path.join(__dirname, 'fixtures', 'ig-XML', 'fsh');
+    const config = loadConfigurationFromIgResource(inputPath);
+    expect(config).toEqual({
+      canonical: 'http://example.org',
+      FSHOnly: true,
+      dependencies: [
+        { packageId: 'foo.bar', version: '1.2.3' },
+        { packageId: 'bar.foo', version: 'current' }
+      ],
+      fhirVersion: ['4.0.1']
+    });
+    expect(loggerSpy.getLastMessage('info')).toMatch(
+      new RegExp(
+        `from ${escapeRegExp(path.join(inputPath, '..', 'input', 'ImplementationGuide.xml'))}`
+      )
+    );
+  });
+
+  it('should find the ImplementationGuide JSON file even when other files are present', () => {
+    const inputPath = path.join(__dirname, 'fixtures', 'ig-JSON-other-files', 'fsh');
+    const config = loadConfigurationFromIgResource(inputPath);
+    expect(config).toEqual({ canonical: 'http://example.org', FSHOnly: true, fhirVersion: [] });
+    expect(loggerSpy.getLastMessage('info')).toMatch(
+      new RegExp(
+        `from ${escapeRegExp(path.join(inputPath, '..', 'input', 'ImplementationGuide.json'))}`
+      )
+    );
+  });
+
+  it('should return null when there are two potentially valid IG files', () => {
+    const inputPath = path.join(__dirname, 'fixtures', 'ig-XML-and-ig-JSON', 'fsh');
+    expect(loadConfigurationFromIgResource(inputPath)).toBeNull();
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Multiple possible ImplementationGuide resources/
+    );
+  });
+
+  it('should return null when there is no ImplementationGuide JSON with a url', () => {
+    const inputPath = path.join(__dirname, 'fixtures', 'ig-JSON-no-url', 'fsh');
+    expect(loadConfigurationFromIgResource(inputPath)).toBeNull();
+  });
+
+  it('should return null when there is no ImplementationGuide JSON', () => {
+    const inputPath = path.join(__dirname, 'fixtures', 'ig-JSON-no-ig-JSON', 'fsh');
+    expect(loadConfigurationFromIgResource(inputPath)).toBeNull();
+  });
+
+  it('should return null when the input path does not exist', () => {
+    const inputPath = path.join(__dirname, 'fixtures', 'fake-path', 'fsh');
+    expect(loadConfigurationFromIgResource(inputPath)).toBeNull();
+  });
+});
diff --git a/test/utils/Processing.test.ts b/test/utils/Processing.test.ts
index fa2fa7b..3a51da3 100644
--- a/test/utils/Processing.test.ts
+++ b/test/utils/Processing.test.ts
@@ -138,6 +138,16 @@ describe('Processing', () => {
       });
     });
 
+    it('should extract a configuration from an ImplementationGuide JSON when config.yaml is absent', () => {
+      const input = path.join(__dirname, 'fixtures', 'ig-JSON-only', 'fsh');
+      const config = readConfig(input);
+      expect(config).toEqual({
+        FSHOnly: true,
+        canonical: 'http://example.org',
+        fhirVersion: ['4.0.1']
+      });
+    });
+
     it('should log and throw an error when config.yaml is not found in the input directory', () => {
       const input = path.join(__dirname, 'fixtures', 'no-package');
       expect(() => {
diff --git a/test/utils/fixtures/ig-JSON-only/fsh/not-config.yaml b/test/utils/fixtures/ig-JSON-only/fsh/not-config.yaml
new file mode 100644
index 0000000..23e34e0
--- /dev/null
+++ b/test/utils/fixtures/ig-JSON-only/fsh/not-config.yaml
@@ -0,0 +1,1 @@
+# This is not a config file
\ No newline at end of file
diff --git a/test/utils/fixtures/ig-JSON-only/input/ImplementationGuide.json b/test/utils/fixtures/ig-JSON-only/input/ImplementationGuide.json
new file mode 100644
index 0000000..2233912
--- /dev/null
+++ b/test/utils/fixtures/ig-JSON-only/input/ImplementationGuide.json
@@ -0,0 +1,7 @@
+{
+  "resourceType": "ImplementationGuide",
+  "url": "http://example.org",
+  "fhirVersion": [
+    "4.0.1"
+  ]
+}
\ No newline at end of file