Repository: FHIR/sushi (ts)

Issue:
SUSHI does not correctly extract config from IG XML/JSON
If you run SUSHI without a `sushi-config.yaml`, it *should* extract information from the IG XML or JSON if it can find it.  This is not working correctly.

See: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/How.20I.20use.20Sushi.201.2E0.2E0.20to.20manage.20an.20IG/near/213361487

Pull request description:
Fix extraction of config info from IG resource in new publisher config
Also logs the extracted config so the user can see what values are being used.

Fixes #635

Patch:
diff --git a/src/app.ts b/src/app.ts
index 8103f32..07078f4 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -111,7 +111,7 @@ async function app() {
       logger.info('No FSH files present.');
       process.exit(0);
     }
-    config = readConfig(isIgPubContext ? originalInput : input);
+    config = readConfig(isIgPubContext ? originalInput : input, isLegacyIgPubContext);
     tank = fillTank(rawFSH, config);
   } catch {
     program.outputHelp();
diff --git a/src/import/loadConfigurationFromIgResource.ts b/src/import/loadConfigurationFromIgResource.ts
index ba44e6b..0b0e4e5 100644
--- a/src/import/loadConfigurationFromIgResource.ts
+++ b/src/import/loadConfigurationFromIgResource.ts
@@ -8,22 +8,24 @@ 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
+ * @param igRoot - path to the root of the IG project
  * @returns {Configuration} the extracted configuration
  */
-export function loadConfigurationFromIgResource(input: string): Configuration | null {
+export function loadConfigurationFromIgResource(igRoot: 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');
+  const igIniPath = path.join(igRoot, 'ig.ini');
   if (fs.existsSync(igIniPath)) {
     try {
       const igIni = ini.parse(fs.readFileSync(igIniPath, 'utf-8'));
-      igPath = path.join(input, '..', igIni.ig);
+      if (igIni.IG?.ig) {
+        igPath = path.join(igRoot, igIni.IG.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 igInputPath = path.join(igRoot, 'input');
   const possibleIgPaths: string[] = [];
   if (fs.existsSync(igPath)) {
     possibleIgPaths.push(igPath);
@@ -56,14 +58,16 @@ export function loadConfigurationFromIgResource(input: string): Configuration |
   });
   // Extract the configuration from the resource
   if (igResource && igResource.url && !multipleIgs) {
-    logger.info(`Extracting FSHOnly configuration from ${igPath}.`);
-    return {
+    const config = {
       canonical: igResource.url.replace(/\/ImplementationGuide.*/, ''),
-      FSHOnly: true,
-      dependencies: igResource.dependsOn?.filter(dep => dep.packageId && dep.version),
+      version: igResource.version,
       fhirVersion: igResource.fhirVersion ?? [],
-      version: igResource.version
+      dependencies: igResource.dependsOn?.filter(dep => dep.packageId && dep.version),
+      FSHOnly: true
     };
+    logger.info(`Extracting FSHOnly configuration from ${igPath}:`);
+    Object.entries(config).forEach(e => logger.info(`  ${e[0]}: ${JSON.stringify(e[1])}`));
+    return config;
   }
   return null;
 }
diff --git a/src/utils/Processing.ts b/src/utils/Processing.ts
index 1d05484..7b8b9ff 100644
--- a/src/utils/Processing.ts
+++ b/src/utils/Processing.ts
@@ -119,11 +119,11 @@ export function ensureOutputDir(
   return outDir;
 }
 
-export function readConfig(input: string): Configuration {
+export function readConfig(input: string, isLegacyIgPubContext: boolean): Configuration {
   const configPath = ensureConfiguration(input);
   let config: Configuration;
   if (configPath == null || !fs.existsSync(configPath)) {
-    config = loadConfigurationFromIgResource(input);
+    config = loadConfigurationFromIgResource(isLegacyIgPubContext ? path.dirname(input) : input);
   } else {
     const configYaml = fs.readFileSync(configPath, 'utf8');
     config = importConfiguration(configYaml, configPath);


Test patch:
diff --git a/test/import/fixtures/ig-JSON-no-ig-JSON/fsh/not-config.yaml b/test/import/fixtures/ig-JSON-no-ig-JSON/not-config.yaml
similarity index 100%
rename from test/import/fixtures/ig-JSON-no-ig-JSON/fsh/not-config.yaml
rename to test/import/fixtures/ig-JSON-no-ig-JSON/not-config.yaml
diff --git a/test/import/fixtures/ig-JSON-no-url/fsh/not-config.yaml b/test/import/fixtures/ig-JSON-no-url/not-config.yaml
similarity index 100%
rename from test/import/fixtures/ig-JSON-no-url/fsh/not-config.yaml
rename to test/import/fixtures/ig-JSON-no-url/not-config.yaml
diff --git a/test/import/fixtures/ig-JSON-only-url/fsh/not-config.yaml b/test/import/fixtures/ig-JSON-only-url/not-config.yaml
similarity index 100%
rename from test/import/fixtures/ig-JSON-only-url/fsh/not-config.yaml
rename to test/import/fixtures/ig-JSON-only-url/not-config.yaml
diff --git a/test/import/fixtures/ig-JSON-other-files/fsh/not-config.yaml b/test/import/fixtures/ig-JSON-other-files/not-config.yaml
similarity index 100%
rename from test/import/fixtures/ig-JSON-other-files/fsh/not-config.yaml
rename to test/import/fixtures/ig-JSON-other-files/not-config.yaml
diff --git a/test/import/fixtures/ig-JSON-with-ini/ig.ini b/test/import/fixtures/ig-JSON-with-ini/ig.ini
index 252253a..1a32658 100644
--- a/test/import/fixtures/ig-JSON-with-ini/ig.ini
+++ b/test/import/fixtures/ig-JSON-with-ini/ig.ini
@@ -1,2 +1,3 @@
+[IG]
 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/fsh/not-config.yaml b/test/import/fixtures/ig-JSON-with-ini/not-config.yaml
similarity index 100%
rename from test/import/fixtures/ig-JSON-with-ini/fsh/not-config.yaml
rename to test/import/fixtures/ig-JSON-with-ini/not-config.yaml
diff --git a/test/import/fixtures/ig-JSON/fsh/not-config.yaml b/test/import/fixtures/ig-JSON/not-config.yaml
similarity index 100%
rename from test/import/fixtures/ig-JSON/fsh/not-config.yaml
rename to test/import/fixtures/ig-JSON/not-config.yaml
diff --git a/test/import/fixtures/ig-XML-and-ig-JSON/fsh/not-config.yaml b/test/import/fixtures/ig-XML-and-ig-JSON/not-config.yaml
similarity index 100%
rename from test/import/fixtures/ig-XML-and-ig-JSON/fsh/not-config.yaml
rename to test/import/fixtures/ig-XML-and-ig-JSON/not-config.yaml
diff --git a/test/import/fixtures/ig-XML/fsh/not-config.yaml b/test/import/fixtures/ig-XML/not-config.yaml
similarity index 100%
rename from test/import/fixtures/ig-XML/fsh/not-config.yaml
rename to test/import/fixtures/ig-XML/not-config.yaml
diff --git a/test/import/loadConfigurationFromIgResource.test.ts b/test/import/loadConfigurationFromIgResource.test.ts
index 36e1b46..a9f8d15 100644
--- a/test/import/loadConfigurationFromIgResource.test.ts
+++ b/test/import/loadConfigurationFromIgResource.test.ts
@@ -11,31 +11,27 @@ describe('loadConfigurationFromIgResource', () => {
   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 inputPath = path.join(__dirname, 'fixtures', 'ig-JSON-with-ini');
     const config = loadConfigurationFromIgResource(inputPath);
     expect(config).toEqual({ canonical: 'http://example.org', FSHOnly: true, fhirVersion: [] });
-    expect(loggerSpy.getLastMessage('info')).toMatch(
+    expect(loggerSpy.getFirstMessage('info')).toMatch(
       new RegExp(
-        `from ${escapeRegExp(
-          path.join(inputPath, '..', 'sneaky-input', 'ImplementationGuide.json')
-        )}`
+        `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 inputPath = path.join(__dirname, 'fixtures', 'ig-JSON-only-url');
     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'))}`
-      )
+    expect(loggerSpy.getFirstMessage('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 inputPath = path.join(__dirname, 'fixtures', 'ig-JSON');
     const config = loadConfigurationFromIgResource(inputPath);
     expect(config).toEqual({
       canonical: 'http://example.org',
@@ -47,15 +43,20 @@ describe('loadConfigurationFromIgResource', () => {
       fhirVersion: ['4.0.1'],
       version: '1.0.0'
     });
-    expect(loggerSpy.getLastMessage('info')).toMatch(
-      new RegExp(
-        `from ${escapeRegExp(path.join(inputPath, '..', 'input', 'ImplementationGuide.json'))}`
-      )
+    expect(loggerSpy.getFirstMessage('info')).toMatch(
+      new RegExp(`from ${escapeRegExp(path.join(inputPath, 'input', 'ImplementationGuide.json'))}`)
+    );
+    expect(loggerSpy.getMessageAtIndex(1, 'info')).toEqual('  canonical: "http://example.org"');
+    expect(loggerSpy.getMessageAtIndex(2, 'info')).toEqual('  version: "1.0.0"');
+    expect(loggerSpy.getMessageAtIndex(3, 'info')).toEqual('  fhirVersion: ["4.0.1"]');
+    expect(loggerSpy.getMessageAtIndex(4, 'info')).toEqual(
+      '  dependencies: [{"packageId":"foo.bar","version":"1.2.3"},{"packageId":"bar.foo","version":"current"}]'
     );
+    expect(loggerSpy.getMessageAtIndex(5, 'info')).toEqual('  FSHOnly: true');
   });
 
   it('should extract an XML configuration with a url and dependencies', () => {
-    const inputPath = path.join(__dirname, 'fixtures', 'ig-XML', 'fsh');
+    const inputPath = path.join(__dirname, 'fixtures', 'ig-XML');
     const config = loadConfigurationFromIgResource(inputPath);
     expect(config).toEqual({
       canonical: 'http://example.org',
@@ -66,26 +67,22 @@ describe('loadConfigurationFromIgResource', () => {
       ],
       fhirVersion: ['4.0.1']
     });
-    expect(loggerSpy.getLastMessage('info')).toMatch(
-      new RegExp(
-        `from ${escapeRegExp(path.join(inputPath, '..', 'input', 'ImplementationGuide.xml'))}`
-      )
+    expect(loggerSpy.getFirstMessage('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 inputPath = path.join(__dirname, 'fixtures', 'ig-JSON-other-files');
     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'))}`
-      )
+    expect(loggerSpy.getFirstMessage('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');
+    const inputPath = path.join(__dirname, 'fixtures', 'ig-XML-and-ig-JSON');
     expect(loadConfigurationFromIgResource(inputPath)).toBeNull();
     expect(loggerSpy.getLastMessage('error')).toMatch(
       /Multiple possible ImplementationGuide resources/
@@ -93,12 +90,12 @@ describe('loadConfigurationFromIgResource', () => {
   });
 
   it('should return null when there is no ImplementationGuide JSON with a url', () => {
-    const inputPath = path.join(__dirname, 'fixtures', 'ig-JSON-no-url', 'fsh');
+    const inputPath = path.join(__dirname, 'fixtures', 'ig-JSON-no-url');
     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');
+    const inputPath = path.join(__dirname, 'fixtures', 'ig-JSON-no-ig-JSON');
     expect(loadConfigurationFromIgResource(inputPath)).toBeNull();
   });
 
diff --git a/test/utils/Processing.test.ts b/test/utils/Processing.test.ts
index d53eb72..927612b 100644
--- a/test/utils/Processing.test.ts
+++ b/test/utils/Processing.test.ts
@@ -164,7 +164,7 @@ describe('Processing', () => {
 
     it('should return the contents of sushi-config.yaml from the input directory', () => {
       const input = path.join(__dirname, 'fixtures', 'valid-yaml');
-      const config = readConfig(input);
+      const config = readConfig(input, false);
       expect(config).toEqual({
         filePath: path.join(__dirname, 'fixtures', 'valid-yaml', 'sushi-config.yaml'),
         id: 'sushi-test',
@@ -206,8 +206,18 @@ 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);
+      const input = path.join(__dirname, 'fixtures', 'ig-JSON-only');
+      const config = readConfig(input, false);
+      expect(config).toEqual({
+        FSHOnly: true,
+        canonical: 'http://example.org',
+        fhirVersion: ['4.0.1']
+      });
+    });
+
+    it('should extract a configuration from an ImplementationGuide JSON when config.yaml is absent (legacy)', () => {
+      const input = path.join(__dirname, 'fixtures', 'ig-JSON-only-legacy', 'fsh');
+      const config = readConfig(input, true);
       expect(config).toEqual({
         FSHOnly: true,
         canonical: 'http://example.org',
@@ -218,7 +228,7 @@ describe('Processing', () => {
     it('should log and throw an error when sushi-config.yaml is not found in the input directory', () => {
       const input = path.join(__dirname, 'fixtures', 'no-package');
       expect(() => {
-        readConfig(input);
+        readConfig(input, false);
       }).toThrow();
       expect(loggerSpy.getLastMessage('error')).toMatch(/No sushi-config\.yaml/s);
     });
@@ -226,7 +236,7 @@ describe('Processing', () => {
     it('should log and throw an error when the contents of sushi-config.yaml are not valid yaml', () => {
       const input = path.join(__dirname, 'fixtures', 'invalid-yaml');
       expect(() => {
-        readConfig(input);
+        readConfig(input, false);
       }).toThrow();
       expect(loggerSpy.getLastMessage('error')).toMatch(/not a valid YAML object/s);
     });
@@ -234,7 +244,7 @@ describe('Processing', () => {
     it('should log and throw an error when the configuration does not include a FHIR R4 dependency', () => {
       const input = path.join(__dirname, 'fixtures', 'fhir-dstu2');
       expect(() => {
-        readConfig(input);
+        readConfig(input, false);
       }).toThrow();
       expect(loggerSpy.getLastMessage('error')).toMatch(/must specify FHIR R4 as a fhirVersion/s);
     });
@@ -334,7 +344,7 @@ describe('Processing', () => {
       tempRoot = temp.mkdirSync('output-dir');
       tempIGPubRoot = temp.mkdirSync('output-ig-dir');
       const input = path.join(__dirname, 'fixtures', 'valid-yaml');
-      const config = readConfig(input);
+      const config = readConfig(input, false);
       outPackage = new Package(config);
 
       const myProfile = new StructureDefinition();
diff --git a/test/utils/fixtures/ig-JSON-only/fsh/not-config.yaml b/test/utils/fixtures/ig-JSON-only-legacy/fsh/not-config.yaml
similarity index 100%
rename from test/utils/fixtures/ig-JSON-only/fsh/not-config.yaml
rename to test/utils/fixtures/ig-JSON-only-legacy/fsh/not-config.yaml
diff --git a/test/utils/fixtures/ig-JSON-only-legacy/input/ImplementationGuide.json b/test/utils/fixtures/ig-JSON-only-legacy/input/ImplementationGuide.json
new file mode 100644
index 0000000..2233912
--- /dev/null
+++ b/test/utils/fixtures/ig-JSON-only-legacy/input/ImplementationGuide.json
@@ -0,0 +1,7 @@
+{
+  "resourceType": "ImplementationGuide",
+  "url": "http://example.org",
+  "fhirVersion": [
+    "4.0.1"
+  ]
+}
\ No newline at end of file
diff --git a/test/utils/fixtures/ig-JSON-only/not-config.yaml b/test/utils/fixtures/ig-JSON-only/not-config.yaml
new file mode 100644
index 0000000..23e34e0
--- /dev/null
+++ b/test/utils/fixtures/ig-JSON-only/not-config.yaml
@@ -0,0 +1,1 @@
+# This is not a config file
\ No newline at end of file