Repository: FHIR/sushi
Pull request #1150 (2022-09-15 17:25:47)

Support for R5 ImplementationGuide JSON
The R5 ballot version is coming soon, and with it come changes to the `ImplementationGuide` resource. The version this branch targets is http://hl7.org/fhir/2022Sep/implementationguide.html. This PR adds support for converting any properties that changed and adds support for new properties introduced in the new version. The differences are summarized in the "R4 diff" table on that page.

All new properties are supported in the `sushi-config.yaml`. Any choice elements are supported by specifying the choice you are using. For example, `source[x]` can be provided as `sourceString`, `sourceUrl`, or `sourceMarkdown` in the `sushi-config.yaml`. Any property that changed from R4 to R5 that can be inferred based on an old R4 property are converted automatically. For example, if `IG.definition.resource.exampleBoolean` or `IG.definition.resource.exampleCanonical`  were set in the R4 resource, SUSHI will now remove those properties and set `IG.definition.resource.isExample` as appropriate.

The draft release notes describe the changes made, and the commits do a fairly good job of describing each change. I'm also happy to provide more details as anyone reviews, because I'm currently creating an update to FSH School as well.

jafeltra: [eb76ad8](https://github.com/FHIR/sushi/pull/1150/commits/eb76ad8ba9c8c9c14f1b73c864a8553d5cb69c1e) should fix the crash you were seeing. It also renames the functions to indicate they are for R5 IGs and simplifies the R4 regex.

Patch:
diff --git a/src/fhirtypes/ImplementationGuide.ts b/src/fhirtypes/ImplementationGuide.ts
index 78b6630..c23eceb 100644
--- a/src/fhirtypes/ImplementationGuide.ts
+++ b/src/fhirtypes/ImplementationGuide.ts
@@ -2,6 +2,7 @@ import { ContactDetail, UsageContext } from './metaDataTypes';
 import {
   BackboneElement,
   CodeableConcept,
+  Coding,
   DomainResource,
   Extension,
   Reference
@@ -22,6 +23,9 @@ export type ImplementationGuide = DomainResource & {
   useContext?: UsageContext[];
   jurisdiction?: CodeableConcept[];
   copyright?: string;
+  copyrightLabel?: string; // Added in R5 IG resource
+  versionAlgorithmString?: string; // Added in R5 IG resource
+  versionAlgorithmCoding?: Coding; // Added in R5 IG resource
   packageId: string;
   license?: string;
   fhirVersion: string[];
@@ -37,6 +41,7 @@ export type ImplementationGuideDependsOn = BackboneElement & {
   uri?: string; // optional for Configuration usecase where packageId is used instead
   packageId?: string;
   version?: string;
+  reason?: string; // Added in R5 IG resource
 };
 
 export type ImplementationGuideGlobal = {
@@ -65,6 +70,8 @@ export type ImplementationGuideDefinitionResource = {
   description?: string;
   exampleBoolean?: boolean;
   exampleCanonical?: string;
+  isExample?: boolean; // R5 added this property to replace exampleBoolean and exampleCanonical; you can only have one of the three
+  profile?: string[]; // R5 added this property to replace exampleCanonical
   groupingId?: string;
   extension?: Extension[];
 };
@@ -72,17 +79,21 @@ export type ImplementationGuideDefinitionResource = {
 export type ImplementationGuideDefinitionPage = {
   nameUrl?: string;
   nameReference?: Reference;
+  name?: string; // R5 added this property to replace nameUrl NOTE: it is 1..1
   title?: string; // optional to support Configuration use case where title has a default
   generation?: ImplementationGuideDefinitionPageGeneration; // optional to support Configuration...
   extension?: Extension[];
   modifierExtension?: Extension[];
+  sourceUrl?: string; // Added in R5 IG resource
+  sourceString?: string; // Added in R5 IG resource
+  sourceMarkdown?: string; // Added in R5 IG resource
   page?: ImplementationGuideDefinitionPage[];
 };
 
 export type ImplementationGuideDefinitionPageGeneration = 'html' | 'markdown' | 'xml' | 'generated';
 
 export type ImplementationGuideDefinitionParameter = {
-  code: string;
+  code: string | Coding; // code changed from a code to a Coding type in R5
   value: string;
 };
 
diff --git a/src/fshtypes/Configuration.ts b/src/fshtypes/Configuration.ts
index debbef2..c2e47b8 100644
--- a/src/fshtypes/Configuration.ts
+++ b/src/fshtypes/Configuration.ts
@@ -11,7 +11,8 @@ import {
   ImplementationGuideDefinitionPage,
   ImplementationGuideDefinitionParameter,
   ImplementationGuideStatus,
-  CodeableConcept
+  CodeableConcept,
+  Coding
 } from '../fhirtypes';
 
 /**
@@ -59,6 +60,9 @@ export type Configuration = {
   useContext?: UsageContext[];
   jurisdiction?: CodeableConcept[];
   copyright?: string;
+  copyrightLabel?: string; // Added in R5 IG resource
+  versionAlgorithmString?: string; // Added in R5 IG resource
+  versionAlgorithmCoding?: Coding; // Added in R5 IG resource
 
   // SUSHI will use id as both id and packageId in the IG unless a specific packageId is specified
   packageId?: string;
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index 3c297fa..0902aee 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -21,6 +21,7 @@ import {
   StructureDefinition,
   ValueSet,
   CodeSystem,
+  CodeSystemConcept,
   InstanceDefinition,
   ImplementationGuideDefinitionPage,
   ImplementationGuideDependsOn
@@ -29,6 +30,17 @@ import { ConfigurationMenuItem } from '../fshtypes';
 import { logger, Type, getFilesRecursive } from '../utils';
 import { FHIRDefinitions } from '../fhirdefs';
 import { Configuration } from '../fshtypes';
+import { parseCodeLexeme } from '../import';
+
+function isR4(fhirVersion: string[]) {
+  let containsR4Version = false;
+  fhirVersion.forEach(v => {
+    if (/^4\.[013]\./.test(v)) {
+      containsR4Version = true;
+    }
+  });
+  return containsR4Version;
+}
 
 // List of Conformance and Terminology resources from http://hl7.org/fhir/R4/resourcelist.html
 // and http://hl7.org/fhir/5.0.0-snapshot1/resourcelist.html
@@ -97,6 +109,7 @@ export class IGExporter {
     this.addMenuXML(outPath);
     this.checkIgIni();
     this.checkPackageList();
+    this.updateForR5();
     this.addImplementationGuide(outPath);
   }
 
@@ -122,6 +135,13 @@ export class IGExporter {
         r.exampleCanonical = this.normalizeResourceReference(r.exampleCanonical, false);
       }
     });
+    this.config.parameters?.forEach(p => {
+      const parsedCode = parseCodeLexeme(p.code as string); // parameter.code will always be a string coming from the config
+      if (parsedCode.system) {
+        // If a system and code is provided, normalize the system
+        p.code = `${this.normalizeResourceReference(parsedCode.system, false)}#${parsedCode.code}`;
+      }
+    });
   }
 
   /**
@@ -248,6 +268,9 @@ export class IGExporter {
     // Clone it so we don't mutate the original
     const dependsOn = cloneDeep(dependency);
 
+    // By default, dependsOn.reason should not be supported because it is an R5 element
+    delete dependsOn.reason;
+
     if (dependsOn.version == null) {
       // No need for the detailed log message since we already logged one in the package loader.
       logger.error(
@@ -1294,6 +1317,148 @@ export class IGExporter {
       );
     }
   }
+
+  updateForR5(): void {
+    // If it isn't R4, we will update it. SUSHI only supports R4 or later, and as of now, we will just target R5.
+    if (!isR4(this.config.fhirVersion)) {
+      // Update IG.definition.resource
+      this.ig.definition.resource.forEach(resource => {
+        // Use IG.definition.resource.isExample
+        if (resource.exampleBoolean || resource.exampleCanonical) {
+          resource.isExample = true;
+          if (resource.exampleCanonical) {
+            resource.profile = [resource.exampleCanonical];
+          }
+          delete resource.exampleBoolean;
+          delete resource.exampleCanonical;
+        } else if (resource.exampleBoolean === false) {
+          resource.isExample = false;
+          delete resource.exampleBoolean;
+        }
+
+        // Assign IG.definition.resource.profile if provided
+        const configEntry = this.config.resources?.find(
+          r => r.reference?.reference === resource.reference?.reference
+        );
+        if (configEntry?.profile != null) {
+          resource.profile = configEntry.profile;
+        }
+        // Assign Ig.definition.resource.isExample if provided
+        if (configEntry?.isExample != null) {
+          resource.isExample = configEntry.isExample;
+        }
+      });
+
+      // Update IG.definition.page.name
+      this.updatePageNameForR5(this.ig.definition.page);
+
+      // Add new IG.definition.page.source[x] property
+      this.ig.definition.page.page.forEach(page => {
+        // this.ig.definition.page is the toc.html page we create
+        // All configured pages are at the next level, so start at that level
+        this.addPageSourceForR5(page, this.config.pages);
+      });
+      // Default IG.definition.page.source[x] on every page if not set
+      this.defaultPageSourceUrlForR5(this.ig.definition.page);
+
+      // Update IG.definition.parameter
+      this.ig.definition.parameter.forEach(parameter => {
+        const guideParameterCodes: string[] =
+          this.fhirDefs
+            .fishForFHIR('http://hl7.org/fhir/guide-parameter-code', Type.CodeSystem)
+            ?.concept.map((concept: CodeSystemConcept) => concept.code) ?? [];
+
+        const code = parameter.code as string;
+        parameter.code = { code };
+        const parsedCode = parseCodeLexeme(code);
+
+        if (parsedCode.code && parsedCode.system) {
+          // If we can parse the code and we have a system and a code provided, we should use that.
+          parameter.code.code = parsedCode.code;
+          parameter.code.system = parsedCode.system;
+        } else if (guideParameterCodes.some(c => c === code)) {
+          // Otherwise, only a code was provided, so we check if it is in the bound VS
+          parameter.code.system = 'http://hl7.org/fhir/guide-parameter-code';
+        } else {
+          // If the code is not in the VS in the R5 IG resource, we default the system
+          // based on https://chat.fhir.org/#narrow/stream/179252-IG-creation/topic/Unknown.20FHIRVersion.20code.20'5.2E0.2E0-ballot'/near/298697304
+          parameter.code.system = 'http://hl7.org/fhir/tools/CodeSystem/ig-parameters';
+        }
+      });
+
+      // Add new copyrightLabel property
+      if (this.config.copyrightLabel) {
+        this.ig.copyrightLabel = this.config.copyrightLabel;
+      }
+
+      // Add new versionAlgorithm property
+      if (this.config.versionAlgorithmString) {
+        this.ig.versionAlgorithmString = this.config.versionAlgorithmString;
+      } else if (this.config.versionAlgorithmCoding) {
+        this.ig.versionAlgorithmCoding = this.config.versionAlgorithmCoding;
+      }
+
+      // Add new dependsOn.reason property
+      this.ig.dependsOn?.forEach(dependency => {
+        const configDependency = this.config.dependencies?.find(
+          d => d.packageId === dependency.packageId
+        );
+        if (configDependency.reason) {
+          dependency.reason = configDependency.reason;
+        }
+      });
+    }
+  }
+
+  updatePageNameForR5(page: ImplementationGuideDefinitionPage): void {
+    if (page?.nameUrl) {
+      page.name = page.nameUrl;
+      delete page.nameUrl;
+    }
+    if (page.page?.length) {
+      for (const subPage of page?.page) {
+        this.updatePageNameForR5(subPage);
+      }
+    }
+  }
+
+  addPageSourceForR5(
+    page: ImplementationGuideDefinitionPage,
+    configPages: ImplementationGuideDefinitionPage[]
+  ): void {
+    const configPage = configPages?.find(
+      p =>
+        p.nameUrl.substring(0, p.nameUrl.lastIndexOf('.')) ===
+        page.name.substring(0, page.name.lastIndexOf('.'))
+    );
+    if (configPage) {
+      if (configPage.sourceUrl) {
+        page.sourceUrl = configPage.sourceUrl;
+      } else if (configPage.sourceString) {
+        page.sourceString = configPage.sourceString;
+      } else if (configPage.sourceMarkdown) {
+        page.sourceMarkdown = configPage.sourceMarkdown;
+      }
+
+      if (page.page?.length) {
+        for (const subPage of page?.page) {
+          this.addPageSourceForR5(subPage, configPage.page);
+        }
+      }
+    }
+  }
+
+  defaultPageSourceUrlForR5(page: ImplementationGuideDefinitionPage): void {
+    if (page.sourceUrl == null && page.sourceString == null && page.sourceMarkdown == null) {
+      page.sourceUrl = page.name;
+    }
+
+    if (page.page?.length) {
+      for (const subPage of page?.page) {
+        this.defaultPageSourceUrlForR5(subPage);
+      }
+    }
+  }
 }
 
 type PageMetadata = {
diff --git a/src/import/YAMLConfiguration.ts b/src/import/YAMLConfiguration.ts
index 8ce0513..7010099 100644
--- a/src/import/YAMLConfiguration.ts
+++ b/src/import/YAMLConfiguration.ts
@@ -72,6 +72,10 @@ export type YAMLConfiguration = {
   jurisdiction?: YAMLConfigurationJurisdiction | YAMLConfigurationJurisdiction[];
 
   copyright?: ImplementationGuide['copyright']; // string
+  copyrightLabel?: ImplementationGuide['copyrightLabel']; // string; only supported in R5
+
+  versionAlgorithmString?: ImplementationGuide['versionAlgorithmString']; // string; only supported in R5
+  versionAlgorithmCoding?: ImplementationGuide['versionAlgorithmCoding'] | string; // Coding | string; only supported in R5
 
   // SUSHI will use id as both id and packageId in the IG unless a specific packageId is specified
   packageId?: ImplementationGuide['packageId']; // string
@@ -316,6 +320,7 @@ export type YAMLConfigurationDependencyDetails = {
   version:
     | ImplementationGuideDependsOn['version'] // string
     | number; // YAML will parse some versions as numbers (e.g., 1.2)
+  reason?: ImplementationGuideDependsOn['reason']; // string; only supported in R5
 };
 
 export type YAMLConfigurationGlobalMap = {
@@ -345,6 +350,10 @@ export type YAMLConfigurationResource = {
   description?: ImplementationGuideDefinitionResource['description']; // string
   exampleBoolean?: ImplementationGuideDefinitionResource['exampleBoolean']; // boolean
   exampleCanonical?: ImplementationGuideDefinitionResource['exampleCanonical']; // canonical
+  isExample?: ImplementationGuideDefinitionResource['isExample']; // boolean; only supported in R5
+  profile?:
+    | ImplementationGuideDefinitionResource['profile'] // string[]; only supported in R5
+    | ImplementationGuideDefinitionResource['profile'][0]; // string; only supported in R5
   groupingId?: ImplementationGuideDefinitionResource['groupingId']; // string
 };
 
@@ -362,6 +371,9 @@ export type YAMLConfigurationPage = null | {
     | '#generated';
   extension?: Extension[];
   modifierExtension?: Extension[];
+  sourceUrl?: string; // only supported in R5
+  sourceString?: string; // only supported in R5
+  sourceMarkdown?: string; // only supported in R5
   [key: string]: YAMLConfigurationPage | string | Extension[]; // other types allowed so title/generation/extension/modifierExtension work
 };
 
diff --git a/src/import/importConfiguration.ts b/src/import/importConfiguration.ts
index 7780cce..fa191c4 100644
--- a/src/import/importConfiguration.ts
+++ b/src/import/importConfiguration.ts
@@ -142,6 +142,13 @@ export function importConfiguration(yaml: YAMLConfiguration | string, file: stri
     useContext: parseUsageContext(yaml.useContext, file),
     jurisdiction: parseJurisdiction(yaml.jurisdiction, file),
     copyright: yaml.copyright,
+    copyrightLabel: yaml.copyrightLabel,
+    versionAlgorithmString: yaml.versionAlgorithmString,
+    versionAlgorithmCoding: parseCoding(
+      yaml.versionAlgorithmCoding,
+      'versionAlgorithmCoding',
+      file
+    ),
     packageId: yaml.packageId ?? yaml.id,
     license: parseSimpleCode(yaml.license, 'license', file),
     fhirVersion: normalizeToArray(yaml.fhirVersion)?.map(v =>
@@ -573,7 +580,8 @@ function parseDependencies(
       version:
         typeof versionOrDetails.version === 'string' || typeof versionOrDetails.version === 'number'
           ? `${versionOrDetails.version}`
-          : undefined
+          : undefined,
+      reason: versionOrDetails.reason
     });
   });
 }
@@ -620,6 +628,7 @@ function parseResources(
     return {
       reference: { reference },
       ...details,
+      profile: normalizeToArray(details.profile),
       fhirVersion: normalizeToArray(details.fhirVersion)?.map(v =>
         parseSimpleCode(v, `resource[${reference}].fhirVersion`, file)
       )
@@ -657,6 +666,15 @@ function parsePage(
       file
     );
   }
+  if (details?.sourceUrl) {
+    page.sourceUrl = details.sourceUrl;
+  }
+  if (details?.sourceString) {
+    page.sourceString = details.sourceString;
+  }
+  if (details?.sourceMarkdown) {
+    page.sourceMarkdown = details.sourceMarkdown;
+  }
   if (details?.extension) {
     page.extension = details.extension;
   }
@@ -668,6 +686,9 @@ function parsePage(
       if (
         key == 'title' ||
         key == 'generation' ||
+        key == 'sourceUrl' ||
+        key == 'sourceString' ||
+        key == 'sourceMarkdown' ||
         key == 'extension' ||
         key == 'modifierExtension'
       ) {


Test patch:
diff --git a/test/ig/IGExporter.IG.test.ts b/test/ig/IGExporter.IG.test.ts
index 7db36f7..715fcf4 100644
--- a/test/ig/IGExporter.IG.test.ts
+++ b/test/ig/IGExporter.IG.test.ts
@@ -2657,4 +2657,466 @@ describe('IGExporter', () => {
       expect(fs.existsSync(angelicPath)).toBeTruthy();
     });
   });
+
+  describe('#r5-ig-format', () => {
+    let tempOut: string;
+
+    beforeEach(() => {
+      loggerSpy.reset();
+      tempOut = temp.mkdirSync('sushi-test');
+      const fixtures = path.join(__dirname, 'fixtures', 'simple-ig');
+      const defs = new FHIRDefinitions();
+      // r5-definitions contains the guide-parameter-code CodeSystem, which was originally included in 5.0.0-ballot
+      loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r5-definitions', defs);
+
+      const r5config = cloneDeep(minimalConfig);
+      r5config.fhirVersion = ['5.0.0-ballot'];
+      r5config.copyrightLabel = 'Shorty Fsh 2022+';
+      r5config.dependencies = [
+        {
+          packageId: 'hl7.fhir.us.ocean',
+          uri: 'http://example.org/ocean',
+          id: 'ocean',
+          version: '1.0.0',
+          reason: 'The ocean is essential because it has fsh'
+        }
+      ];
+      r5config.resources = [
+        {
+          reference: { reference: 'Patient/patient-example-two' },
+          exampleCanonical: 'http://hl7.org/fhir/sushi-test/StructureDefinition/sample-patient'
+        },
+        {
+          reference: { reference: 'Patient/patient-example-three' },
+          profile: [
+            'http://example.org/StructureDefiniton/first',
+            'http://example.org/StructureDefiniton/second'
+          ]
+        },
+        {
+          reference: { reference: 'Patient/patient-example-four' },
+          isExample: false
+        }
+      ];
+      r5config.pages = [
+        {
+          nameUrl: 'index.md',
+          title: 'Home',
+          generation: 'markdown',
+          sourceMarkdown: 'source markdown for index'
+        },
+        {
+          nameUrl: 'other-page.md',
+          title: 'Some Other Page',
+          generation: 'markdown',
+          sourceUrl: 'http://example.org',
+          page: [
+            {
+              nameUrl: 'nested-page.md',
+              sourceString: 'source string for nested page'
+            },
+            {
+              nameUrl: 'second-nested-page.md'
+            }
+          ]
+        }
+      ];
+      r5config.parameters = [
+        {
+          code: 'copyrightyear',
+          value: '2020+'
+        },
+        {
+          code: 'generate-xml',
+          value: 'true'
+        },
+        {
+          code: 'http://example.org/parameters#special-code',
+          value: 'sparkles'
+        },
+        {
+          code: 'http://example.org/CodeSystem/sample-code-system#url-code',
+          value: 'true'
+        },
+        {
+          code: 'SampleCodeSystem#name-code',
+          value: 'true'
+        },
+        {
+          code: 'sample-code-system#id-code',
+          value: 'true'
+        }
+      ];
+
+      const pkg = new Package(r5config);
+
+      // Profile: StructureDefinition/sample-patient
+      pkg.profiles.push(
+        StructureDefinition.fromJSON(
+          fs.readJSONSync(
+            path.join(fixtures, 'profiles', 'StructureDefinition-sample-patient.json')
+          )
+        )
+      );
+
+      // Example: Patient/patient-example
+      const instanceDef = InstanceDefinition.fromJSON(
+        fs.readJSONSync(path.join(fixtures, 'examples', 'Patient-example.json'))
+      );
+      instanceDef._instanceMeta.usage = 'Example';
+      pkg.instances.push(instanceDef);
+
+      // Example: Patient/patient-example-two
+      const instanceDef2 = InstanceDefinition.fromJSON(
+        fs.readJSONSync(path.join(fixtures, 'examples', 'Patient-example-two.json'))
+      );
+      instanceDef2._instanceMeta.usage = 'Example';
+      pkg.instances.push(instanceDef2);
+
+      // Example: Patient/patient-example-three
+      const instanceDef3 = InstanceDefinition.fromJSON(
+        fs.readJSONSync(path.join(fixtures, 'examples', 'Patient-example-three.json'))
+      );
+      instanceDef3._instanceMeta.usage = 'Example';
+      pkg.instances.push(instanceDef3);
+
+      // Example: Patient/patient-example-four
+      const instanceDef4 = InstanceDefinition.fromJSON(
+        fs.readJSONSync(path.join(fixtures, 'examples', 'Patient-example-three.json'))
+      );
+      instanceDef4.id = 'patient-example-four';
+      instanceDef4._instanceMeta.name = 'patient-example-four';
+      instanceDef4._instanceMeta.usage = 'Example';
+      pkg.instances.push(instanceDef4);
+
+      // CodeSystem: CodeSystem/sample-code-system
+      const codeSystemDef = new CodeSystem();
+      codeSystemDef.id = 'sample-code-system';
+      codeSystemDef.name = 'SampleCodeSystem';
+      codeSystemDef.url = 'http://example.org/CodeSystem/sample-code-system';
+      pkg.codeSystems.push(codeSystemDef);
+
+      const exporter = new IGExporter(pkg, defs, fixtures);
+      // No need to regenerate the IG on every test -- generate it once and inspect what you
+      // need to in the tests
+      exporter.export(tempOut);
+    });
+
+    afterEach(() => {
+      temp.cleanupSync();
+    });
+
+    it('should replace a definition.resource.exampleBoolean set to true with isExample', () => {
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'Patient/patient-example'
+        },
+        name: 'patient-example',
+        isExample: true // Replaces exampleBoolean with isExample
+      });
+    });
+
+    it('should replace an definition.resource.exampleBoolean set to false with isExample', () => {
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'StructureDefinition/sample-patient'
+        },
+        name: 'SamplePatient',
+        description:
+          'Demographics and other administrative information about an individual or animal receiving care or other health-related services.',
+        isExample: false // Replaces exampleBoolean with isExample
+      });
+    });
+
+    it('should replace an definition.resource.exampleCanonical with isExample and profile', () => {
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'Patient/patient-example-two'
+        },
+        name: 'patient-example-two',
+        isExample: true, // Replaces exampleBoolean with isExample
+        profile: ['http://hl7.org/fhir/sushi-test/StructureDefinition/sample-patient'] // Includes profile
+      });
+    });
+
+    it('should use the definition.resource.isExample boolean in configuration if provided', () => {
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'Patient/patient-example-four'
+        },
+        name: 'patient-example-four',
+        isExample: false // Provided by configuration
+      });
+    });
+
+    it('should use the definition.resource.profile array in configuration if provided', () => {
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.resource).toContainEqual({
+        reference: {
+          reference: 'Patient/patient-example-three'
+        },
+        name: 'patient-example-three',
+        isExample: true,
+        // Profile is added if it is provided
+        profile: [
+          'http://example.org/StructureDefiniton/first',
+          'http://example.org/StructureDefiniton/second'
+        ]
+      });
+    });
+
+    it('should replace definition.page.nameUrl and any nested pages nameUrls with name and support source[x]', () => {
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.page).toEqual({
+        title: 'Table of Contents',
+        generation: 'html',
+        name: 'toc.html',
+        sourceUrl: 'toc.html', // Defaults sourceUrl if no configuration provided
+        page: [
+          {
+            name: 'index.html', // Replaces nameUrl with name
+            title: 'Home',
+            generation: 'markdown',
+            sourceMarkdown: 'source markdown for index' // Supports source[x]
+          },
+          {
+            name: 'other-page.html',
+            title: 'Some Other Page',
+            generation: 'markdown',
+            sourceUrl: 'http://example.org', // Supports source[x]
+            page: [
+              {
+                name: 'nested-page.html',
+                title: 'Nested Page',
+                generation: 'markdown',
+                sourceString: 'source string for nested page' // Supports source[x]
+              },
+              {
+                name: 'second-nested-page.html',
+                title: 'Second Nested Page',
+                generation: 'markdown',
+                sourceUrl: 'second-nested-page.html'
+              }
+            ]
+          }
+        ]
+      });
+    });
+
+    it('should replace definition.parameter code with a Coding and include the system if the value is in the bound VS', () => {
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.parameter).toContainEqual({
+        code: {
+          code: 'generate-xml', // Replaces simple code with a Coding
+          system: 'http://hl7.org/fhir/guide-parameter-code' // Defaults to the ValueSet including in the binding
+        },
+        value: 'true'
+      });
+    });
+
+    it('should replace definition.parameter code with a Coding and set the default system if the value is not in the bound VS', () => {
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.parameter).toContainEqual({
+        code: {
+          code: 'copyrightyear', // Replaces simple code with a Coding
+          system: 'http://hl7.org/fhir/tools/CodeSystem/ig-parameters' // Default system is set
+        },
+        value: '2020+'
+      });
+    });
+
+    it('should set the definition.parameter system and code if provided', () => {
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.parameter).toContainEqual({
+        code: {
+          code: 'special-code', // Replaces simple code with a Coding
+          system: 'http://example.org/parameters' // Uses the provided system
+        },
+        value: 'sparkles'
+      });
+    });
+
+    it('should support referencing system by name, id, or full url when setting a definition.parameter code with a system', () => {
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.parameter).toContainEqual({
+        code: {
+          code: 'url-code',
+          system: 'http://example.org/CodeSystem/sample-code-system' // Normalizes system
+        },
+        value: 'true'
+      });
+      expect(igContent.definition.parameter).toContainEqual({
+        code: {
+          code: 'name-code',
+          system: 'http://example.org/CodeSystem/sample-code-system' // Normalizes system
+        },
+        value: 'true'
+      });
+      expect(igContent.definition.parameter).toContainEqual({
+        code: {
+          code: 'id-code',
+          system: 'http://example.org/CodeSystem/sample-code-system' // Normalizes system
+        },
+        value: 'true'
+      });
+    });
+
+    it('should set copyrightLabel when provided in configuration', () => {
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.copyrightLabel).toEqual('Shorty Fsh 2022+');
+    });
+
+    it('should set versionAlgorithmString when provided in configuration', () => {
+      // Export IG in this test so can test all variations of versionAlgorithm[x]
+      const configWithVersionAlgorithm = cloneDeep(minimalConfig);
+      configWithVersionAlgorithm.fhirVersion = ['5.0.0-ballot'];
+      configWithVersionAlgorithm.versionAlgorithmString = 'date';
+
+      const fixtures = path.join(__dirname, 'fixtures', 'simple-ig');
+      const defs = new FHIRDefinitions();
+      const pkg = new Package(configWithVersionAlgorithm);
+      const exporter = new IGExporter(pkg, defs, fixtures);
+      exporter.export(tempOut);
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+
+      expect(igContent.versionAlgorithmString).toEqual('date');
+    });
+
+    it('should set versionAlgorithmCoding when provided as FSH Code in configuration', () => {
+      // Export IG in this test so can test all variations of versionAlgorithm[x]
+      const configWithVersionAlgorithm = cloneDeep(minimalConfig);
+      configWithVersionAlgorithm.fhirVersion = ['5.0.0-ballot'];
+      configWithVersionAlgorithm.versionAlgorithmCoding = {
+        system: 'http://example.org',
+        code: 'semver'
+      };
+
+      const fixtures = path.join(__dirname, 'fixtures', 'simple-ig');
+      const defs = new FHIRDefinitions();
+      const pkg = new Package(configWithVersionAlgorithm);
+      const exporter = new IGExporter(pkg, defs, fixtures);
+      exporter.export(tempOut);
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+
+      expect(igContent.versionAlgorithmCoding).toEqual({
+        system: 'http://example.org',
+        code: 'semver'
+      });
+    });
+
+    it('should support dependsOn.reason when provided in configuration', () => {
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.dependsOn).toEqual([
+        {
+          packageId: 'hl7.fhir.us.ocean',
+          uri: 'http://example.org/ocean',
+          id: 'ocean',
+          version: '1.0.0',
+          reason: 'The ocean is essential because it has fsh'
+        }
+      ]);
+    });
+  });
 });
diff --git a/test/import/importConfiguration.test.ts b/test/import/importConfiguration.test.ts
index 6a8e7dd..d928497 100644
--- a/test/import/importConfiguration.test.ts
+++ b/test/import/importConfiguration.test.ts
@@ -1302,6 +1302,45 @@ describe('importConfiguration', () => {
     });
   });
 
+  describe('#copyrightLabel', () => {
+    it('should copy copyrightLabel as-is', () => {
+      minYAML.copyrightLabel = 'Copyright Scaly Productions 2022';
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(config.copyrightLabel).toBe('Copyright Scaly Productions 2022');
+    });
+  });
+
+  describe('#versionAlgorithm', () => {
+    it('should copy versionAlgorithmString as-is', () => {
+      minYAML.versionAlgorithmString = 'date';
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(config.versionAlgorithmString).toBe('date');
+    });
+
+    it('should support versionAlgorithmCoding as a FSH code', () => {
+      minYAML.versionAlgorithmCoding = 'http://example.org#semver';
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(config.versionAlgorithmCoding).toEqual({
+        code: 'semver',
+        system: 'http://example.org'
+      });
+    });
+
+    it('should support versionAlgorithmCoding as a coding object', () => {
+      minYAML.versionAlgorithmCoding = {
+        system: 'http://example.org',
+        code: 'semver',
+        version: '1.0.0'
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(config.versionAlgorithmCoding).toEqual({
+        code: 'semver',
+        system: 'http://example.org',
+        version: '1.0.0'
+      });
+    });
+  });
+
   describe('#packageId', () => {
     it('should use the id as packageId when packageId is not provided', () => {
       const config = importConfiguration(minYAML, 'test-config.yaml');
@@ -1433,6 +1472,27 @@ describe('importConfiguration', () => {
           )
       ).toBeTruthy();
     });
+
+    it('should convert dependencies to a list with reason and uri when provided', () => {
+      minYAML.dependencies = {
+        foo: {
+          id: 'foo',
+          uri: 'http://example.org',
+          version: '1.0.0',
+          reason: 'Foo is always necessary'
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(config.dependencies).toEqual([
+        {
+          packageId: 'foo',
+          id: 'foo',
+          version: '1.0.0',
+          uri: 'http://example.org',
+          reason: 'Foo is always necessary'
+        }
+      ]);
+    });
   });
 
   describe('#global', () => {
@@ -1589,6 +1649,34 @@ describe('importConfiguration', () => {
         }
       ]);
     });
+    it('should support resources.[name].profile as an array', () => {
+      minYAML.resources = {
+        'Patient/my-example-patient': {
+          profile: ['http://example.org/patient-profile']
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(config.resources).toEqual([
+        {
+          reference: { reference: 'Patient/my-example-patient' },
+          profile: ['http://example.org/patient-profile']
+        }
+      ]);
+    });
+    it('should convert single-item resources.[name].profile to an array', () => {
+      minYAML.resources = {
+        'Patient/my-example-patient': {
+          profile: 'http://example.org/patient-profile'
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(config.resources).toEqual([
+        {
+          reference: { reference: 'Patient/my-example-patient' },
+          profile: ['http://example.org/patient-profile']
+        }
+      ]);
+    });
     it('should convert omitted resources correctly', () => {
       minYAML.resources = {
         'Patient/my-bad-example-patient': 'omit',
@@ -1701,6 +1789,33 @@ describe('importConfiguration', () => {
         }
       ]);
     });
+
+    it('should support pages with source[x]', () => {
+      minYAML.pages = {
+        'index.md': {
+          title: 'Example Home',
+          sourceMarkdown: 'source markdown for index'
+        },
+        'examples.xml': {
+          title: 'Examples',
+          sourceUrl: 'http://example.org',
+          'simpleExamples.xml': {
+            sourceString: 'source of simple examples'
+          }
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(config.pages).toEqual([
+        { nameUrl: 'index.md', title: 'Example Home', sourceMarkdown: 'source markdown for index' },
+        {
+          nameUrl: 'examples.xml',
+          title: 'Examples',
+          sourceUrl: 'http://example.org',
+          page: [{ nameUrl: 'simpleExamples.xml', sourceString: 'source of simple examples' }]
+        }
+      ]);
+    });
+
     it('should report invalid generation codes', () => {
       minYAML.pages = {
         'index.md': {
diff --git a/test/testhelpers/testdefs/r5-definitions/package/CodeSystem-guide-parameter-code.json b/test/testhelpers/testdefs/r5-definitions/package/CodeSystem-guide-parameter-code.json
new file mode 100644
index 0000000..651b1ea
--- /dev/null
+++ b/test/testhelpers/testdefs/r5-definitions/package/CodeSystem-guide-parameter-code.json
@@ -0,0 +1,85 @@
+{
+  "resourceType": "CodeSystem",
+  "id": "guide-parameter-code",
+  "meta": {
+    "lastUpdated": "2022-09-10T04:52:37.223+10:00",
+    "profile": [
+      "http://hl7.org/fhir/StructureDefinition/shareablecodesystem"
+    ]
+  },
+  "text": {
+    "status": "generated",
+    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n            <p>This code system \n              <code>http://hl7.org/fhir/guide-parameter-code</code> defines the following codes:\n            </p>\n            <table class=\"codes\">\n              <tr>\n                <td style=\"white-space:nowrap\">\n                  <b>Code</b>\n                </td>\n                <td>\n                  <b>Display</b>\n                </td>\n                <td>\n                  <b>Definition</b>\n                </td>\n              </tr>\n              <tr>\n                <td style=\"white-space:nowrap\">apply\n                  <a name=\"guide-parameter-code-apply\"> </a>\n                </td>\n                <td>Apply Metadata Value</td>\n                <td>If the value of this string 0..* parameter is one of the metadata fields then all conformance resources will have any specified [Resource].[field] overwritten with the ImplementationGuide.[field], where field is one of: version, date, status, publisher, contact, copyright, experimental, jurisdiction, useContext.</td>\n              </tr>\n              <tr>\n                <td style=\"white-space:nowrap\">path-resource\n                  <a name=\"guide-parameter-code-path-resource\"> </a>\n                </td>\n                <td>Resource Path</td>\n                <td>The value of this string 0..* parameter is a subfolder of the build context's location that is to be scanned to load resources. Scope is (if present) a particular resource type.</td>\n              </tr>\n              <tr>\n                <td style=\"white-space:nowrap\">path-pages\n                  <a name=\"guide-parameter-code-path-pages\"> </a>\n                </td>\n                <td>Pages Path</td>\n                <td>The value of this string 0..1 parameter is a subfolder of the build context's location that contains files that are part of the html content processed by the builder.</td>\n              </tr>\n              <tr>\n                <td style=\"white-space:nowrap\">path-tx-cache\n                  <a name=\"guide-parameter-code-path-tx-cache\"> </a>\n                </td>\n                <td>Terminology Cache Path</td>\n                <td>The value of this string 0..1 parameter is a subfolder of the build context's location that is used as the terminology cache. If this is not present, the terminology cache is on the local system, not under version control.</td>\n              </tr>\n              <tr>\n                <td style=\"white-space:nowrap\">expansion-parameter\n                  <a name=\"guide-parameter-code-expansion-parameter\"> </a>\n                </td>\n                <td>Expansion Profile</td>\n                <td>The value of this string 0..* parameter is a parameter (name=value) when expanding value sets for this implementation guide. This is particularly used to specify the versions of published terminologies such as SNOMED CT.</td>\n              </tr>\n              <tr>\n                <td style=\"white-space:nowrap\">rule-broken-links\n                  <a name=\"guide-parameter-code-rule-broken-links\"> </a>\n                </td>\n                <td>Broken Links Rule</td>\n                <td>The value of this string 0..1 parameter is either &quot;warning&quot; or &quot;error&quot; (default = &quot;error&quot;). If the value is &quot;warning&quot; then IG build tools allow the IG to be considered successfully build even when there is no internal broken links.</td>\n              </tr>\n              <tr>\n                <td style=\"white-space:nowrap\">generate-xml\n                  <a name=\"guide-parameter-code-generate-xml\"> </a>\n                </td>\n                <td>Generate XML</td>\n                <td>The value of this boolean 0..1 parameter specifies whether the IG publisher creates examples in XML format. If not present, the Publication Tool decides whether to generate XML.</td>\n              </tr>\n              <tr>\n                <td style=\"white-space:nowrap\">generate-json\n                  <a name=\"guide-parameter-code-generate-json\"> </a>\n                </td>\n                <td>Generate JSON</td>\n                <td>The value of this boolean 0..1 parameter specifies whether the IG publisher creates examples in JSON format. If not present, the Publication Tool decides whether to generate JSON.</td>\n              </tr>\n              <tr>\n                <td style=\"white-space:nowrap\">generate-turtle\n                  <a name=\"guide-parameter-code-generate-turtle\"> </a>\n                </td>\n                <td>Generate Turtle</td>\n                <td>The value of this boolean 0..1 parameter specifies whether the IG publisher creates examples in Turtle format. If not present, the Publication Tool decides whether to generate Turtle.</td>\n              </tr>\n              <tr>\n                <td style=\"white-space:nowrap\">html-template\n                  <a name=\"guide-parameter-code-html-template\"> </a>\n                </td>\n                <td>HTML Template</td>\n                <td>The value of this string singleton parameter is the name of the file to use as the builder template for each generated page (see templating).</td>\n              </tr>\n            </table>\n          </div>"
+  },
+  "extension": [
+    {
+      "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg",
+      "valueCode": "fhir"
+    }
+  ],
+  "url": "http://hl7.org/fhir/guide-parameter-code",
+  "identifier": [
+    {
+      "system": "urn:ietf:rfc:3986",
+      "value": "urn:oid:2.16.840.1.113883.4.642.4.997"
+    }
+  ],
+  "version": "5.0.0-ballot",
+  "name": "GuideParameterCode",
+  "status": "draft",
+  "experimental": false,
+  "caseSensitive": true,
+  "content": "complete",
+  "concept": [
+    {
+      "code": "apply",
+      "display": "Apply Metadata Value",
+      "definition": "If the value of this string 0..* parameter is one of the metadata fields then all conformance resources will have any specified [Resource].[field] overwritten with the ImplementationGuide.[field], where field is one of: version, date, status, publisher, contact, copyright, experimental, jurisdiction, useContext."
+    },
+    {
+      "code": "path-resource",
+      "display": "Resource Path",
+      "definition": "The value of this string 0..* parameter is a subfolder of the build context's location that is to be scanned to load resources. Scope is (if present) a particular resource type."
+    },
+    {
+      "code": "path-pages",
+      "display": "Pages Path",
+      "definition": "The value of this string 0..1 parameter is a subfolder of the build context's location that contains files that are part of the html content processed by the builder."
+    },
+    {
+      "code": "path-tx-cache",
+      "display": "Terminology Cache Path",
+      "definition": "The value of this string 0..1 parameter is a subfolder of the build context's location that is used as the terminology cache. If this is not present, the terminology cache is on the local system, not under version control."
+    },
+    {
+      "code": "expansion-parameter",
+      "display": "Expansion Profile",
+      "definition": "The value of this string 0..* parameter is a parameter (name=value) when expanding value sets for this implementation guide. This is particularly used to specify the versions of published terminologies such as SNOMED CT."
+    },
+    {
+      "code": "rule-broken-links",
+      "display": "Broken Links Rule",
+      "definition": "The value of this string 0..1 parameter is either \"warning\" or \"error\" (default = \"error\"). If the value is \"warning\" then IG build tools allow the IG to be considered successfully build even when there is no internal broken links."
+    },
+    {
+      "code": "generate-xml",
+      "display": "Generate XML",
+      "definition": "The value of this boolean 0..1 parameter specifies whether the IG publisher creates examples in XML format. If not present, the Publication Tool decides whether to generate XML."
+    },
+    {
+      "code": "generate-json",
+      "display": "Generate JSON",
+      "definition": "The value of this boolean 0..1 parameter specifies whether the IG publisher creates examples in JSON format. If not present, the Publication Tool decides whether to generate JSON."
+    },
+    {
+      "code": "generate-turtle",
+      "display": "Generate Turtle",
+      "definition": "The value of this boolean 0..1 parameter specifies whether the IG publisher creates examples in Turtle format. If not present, the Publication Tool decides whether to generate Turtle."
+    },
+    {
+      "code": "html-template",
+      "display": "HTML Template",
+      "definition": "The value of this string singleton parameter is the name of the file to use as the builder template for each generated page (see templating)."
+    }
+  ]
+}
\ No newline at end of file