Repository: FHIR/sushi
Pull request #74 (2020-01-07 14:20:48)

Unify Profile and Extension Exporters
This addresses CIMPL-192: https://standardhealthrecord.atlassian.net/browse/CIMPL-192

This PR eliminates the separate profile and extension exporters as all functionality was the same between the two exporters. Additionally, having the single StructureDefinitionExporter helped to solve the issue where profiles and extensions were exported in the opposite exporter and were therefore exported multiple times. In the exporter now, we have separate arrays for profile definitions and extension definitions. When the export is finished for a single definition, we push it onto the appropriate array.

A few tests had to be changed since I removed the general `this.structDefs` array for the two separate arrays. I also added a test to check that we no longer export duplicate definitions.

jafeltra: @kjmahalingam the changes should be made to the two places you pointed out!

Patch:
diff --git a/src/export/ExtensionExporter.ts b/src/export/ExtensionExporter.ts
deleted file mode 100644
index 9de7d76..0000000
--- a/src/export/ExtensionExporter.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { StructureDefinitionExporter } from './StructureDefinitionExporter';
-import { FSHTank } from '../import/FSHTank';
-import { StructureDefinition } from '../fhirtypes';
-import { FHIRDefinitions } from '../fhirdefs';
-import { logger } from '../utils/FSHLogger';
-
-export class ExtensionExporter extends StructureDefinitionExporter {
-  constructor(FHIRDefs: FHIRDefinitions, tank: FSHTank) {
-    super(FHIRDefs, tank);
-  }
-
-  /**
-   * Exports Extensions to StructureDefinitions
-   * @returns {StructureDefinition[]}
-   */
-  export(): StructureDefinition[] {
-    for (const extension of this.tank.getAllExtensions()) {
-      try {
-        this.exportStructDef(extension);
-      } catch (e) {
-        logger.error(e.message, e.sourceInfo);
-      }
-    }
-    return this.structDefs;
-  }
-}
diff --git a/src/export/FHIRExporter.ts b/src/export/FHIRExporter.ts
index fc0a0c0..91b337f 100644
--- a/src/export/FHIRExporter.ts
+++ b/src/export/FHIRExporter.ts
@@ -1,7 +1,6 @@
 import { FSHTank } from '../import/FSHTank';
 import { Package } from './Package';
-import { ProfileExporter } from './ProfileExporter';
-import { ExtensionExporter } from './ExtensionExporter';
+import { StructureDefinitionExporter } from './StructureDefinitionExporter';
 import { FHIRDefinitions } from '../fhirdefs';
 import { InstanceExporter } from './InstanceExporter';
 /**
@@ -11,8 +10,7 @@ import { InstanceExporter } from './InstanceExporter';
  */
 export class FHIRExporter {
   private readonly FHIRDefs: FHIRDefinitions;
-  private profileExporter: ProfileExporter;
-  private extensionExporter: ExtensionExporter;
+  private structureDefinitionExporter: StructureDefinitionExporter;
   private instanceExporter: InstanceExporter;
 
   constructor(FHIRDefs: FHIRDefinitions) {
@@ -20,28 +18,16 @@ export class FHIRExporter {
   }
 
   export(tank: FSHTank): Package {
-    this.profileExporter = new ProfileExporter(this.FHIRDefs, tank);
-    this.extensionExporter = new ExtensionExporter(this.FHIRDefs, tank);
+    this.structureDefinitionExporter = new StructureDefinitionExporter(this.FHIRDefs, tank);
     this.instanceExporter = new InstanceExporter(
       this.FHIRDefs,
       tank,
-      this.profileExporter.resolve.bind(this.profileExporter)
+      this.structureDefinitionExporter.resolve.bind(this.structureDefinitionExporter)
     );
-    const profileDefs = this.profileExporter.export();
-    const extensionDefs = this.extensionExporter.export();
+
+    const { profileDefs, extensionDefs } = this.structureDefinitionExporter.export();
     const instanceDefs = this.instanceExporter.export();
-    // TODO: There is currently a bug in how we do exports that causes some duplicates in the
-    // Package.  More specifically, if a FSH Extension is resolved while exporting a Profile,
-    // then the resolved Extension will be put in the Profiles array.  The reverse is also
-    // true.  We need to determine how best to fix that bug, but in the meantime, we will
-    // just remove the duplicates here so the downstream processes don't have to deal with it.
-    const deduplicatedProfileDefs = profileDefs.filter(sd => tank.findProfile(sd.name));
-    const deduplicatedExtensionDefs = extensionDefs.filter(sd => tank.findExtension(sd.name));
-    return new Package(
-      deduplicatedProfileDefs,
-      deduplicatedExtensionDefs,
-      instanceDefs,
-      tank.config
-    );
+
+    return new Package(profileDefs, extensionDefs, instanceDefs, tank.config);
   }
 }
diff --git a/src/export/ProfileExporter.ts b/src/export/ProfileExporter.ts
deleted file mode 100644
index 6218944..0000000
--- a/src/export/ProfileExporter.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { StructureDefinitionExporter } from './StructureDefinitionExporter';
-import { FSHTank } from '../import/FSHTank';
-import { StructureDefinition } from '../fhirtypes';
-import { FHIRDefinitions } from '../fhirdefs';
-import { logger } from '../utils/FSHLogger';
-
-export class ProfileExporter extends StructureDefinitionExporter {
-  constructor(FHIRDefs: FHIRDefinitions, tank: FSHTank) {
-    super(FHIRDefs, tank);
-  }
-
-  /**
-   * Exports Profiles to StructureDefinitions
-   * @returns {StructureDefinition[]}
-   */
-  export(): StructureDefinition[] {
-    for (const profile of this.tank.getAllProfiles()) {
-      try {
-        this.exportStructDef(profile);
-      } catch (e) {
-        logger.error(e.message, e.sourceInfo);
-      }
-    }
-    return this.structDefs;
-  }
-}
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index 8a72084..aab9771 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -16,15 +16,19 @@ import { logger } from '../utils/FSHLogger';
 import cloneDeep from 'lodash/cloneDeep';
 
 /**
- * The StructureDefinitionExporter is a parent class for ProfileExporter and ExtensionExporter.
- * The operations and structure of both exporters are very similar, so any shared functionality
- * between the two should be included in this class.
+ * The StructureDefinitionExporter is the class for exporting Profiles and Extensions.
+ * The operations and structure of both exporters are very similar, so they currently share an exporter.
  */
 export class StructureDefinitionExporter {
-  public readonly structDefs: StructureDefinition[] = [];
+  public readonly profileDefs: StructureDefinition[] = [];
+  public readonly extensionDefs: StructureDefinition[] = [];
 
   constructor(public readonly FHIRDefs: FHIRDefinitions, public readonly tank: FSHTank) {}
 
+  private get structDefs(): StructureDefinition[] {
+    return [...this.profileDefs, ...this.extensionDefs];
+  }
+
   /**
    * Sets the metadata for the StructureDefinition
    * @param {StructureDefinition} structDef - The StructureDefinition to set metadata on
@@ -162,13 +166,6 @@ export class StructureDefinitionExporter {
       );
       if (!structDef) {
         // If we find a FSH definition, then we can export and resolve for its type again
-        // TODO: This causes a problem because this instance of StructureDefinitionExporter is
-        // either a ProfileExporter or ExtensionExporter -- but we are resolving profiles and
-        // extensions here.  So the end result is that an Extension definition might end up in
-        // the ProfileExporter's structDefs array and/or a Profile definition might end up in the
-        // ExtensionExporter's array.  This causes duplicates in the final Package.  We need to
-        // fix this, but until we decide the best solution, we are just deduplicating in the
-        // FHIRExporter.
         const fshDefinition = this.tank.findProfile(type) ?? this.tank.findExtension(type);
         if (fshDefinition) {
           this.exportStructDef(fshDefinition);
@@ -202,6 +199,34 @@ export class StructureDefinitionExporter {
     this.setRules(structDef, fshDefinition);
     this.validateStructureDefinition(structDef);
 
-    this.structDefs.push(structDef);
+    if (structDef.type === 'Extension') {
+      this.extensionDefs.push(structDef);
+    } else {
+      this.profileDefs.push(structDef);
+    }
+  }
+
+  /**
+   * Exports Profiles and Extensions to StructureDefinitions
+   * @returns {AllStructureDefinitions}
+   */
+  export(): AllStructureDefinitions {
+    this.tank.getAllStructureDefinitions().forEach(sd => {
+      try {
+        this.exportStructDef(sd);
+      } catch (e) {
+        logger.error(e.message, e.sourceInfo);
+      }
+    });
+    return {
+      profileDefs: this.profileDefs,
+      extensionDefs: this.extensionDefs
+    };
   }
 }
+
+// Type to hold both profile and extension structure definitions
+export type AllStructureDefinitions = {
+  profileDefs: StructureDefinition[];
+  extensionDefs: StructureDefinition[];
+};
diff --git a/src/export/index.ts b/src/export/index.ts
index 3f99516..9429937 100644
--- a/src/export/index.ts
+++ b/src/export/index.ts
@@ -1,7 +1,5 @@
 export * from './FHIRExporter';
 export * from './StructureDefinitionExporter';
-export * from './ProfileExporter';
-export * from './ExtensionExporter';
 export * from './InstanceExporter';
 export * from './exportFHIR';
 export * from './Package';
diff --git a/src/import/FSHTank.ts b/src/import/FSHTank.ts
index 5573c5e..1291934 100644
--- a/src/import/FSHTank.ts
+++ b/src/import/FSHTank.ts
@@ -26,6 +26,14 @@ export class FSHTank {
     return flatMap(this.docs, doc => Array.from(doc.extensions.values()));
   }
 
+  /**
+   * Gets all structure definitions (profiles and extensions) in the tank
+   * @returns {(Profile | Extension)[]}
+   */
+  public getAllStructureDefinitions(): (Profile | Extension)[] {
+    return [...this.getAllProfiles(), ...this.getAllExtensions()];
+  }
+
   /**
    * Finds the profile in the tank by name, id, or alias, if it exists
    * @param {string} key - The name or id of the profile we're looking for


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index cbc9b3b..94975b3 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -1,4 +1,4 @@
-import { InstanceExporter, ProfileExporter } from '../../src/export';
+import { InstanceExporter, StructureDefinitionExporter } from '../../src/export';
 import { FSHTank, FSHDocument } from '../../src/import';
 import { FHIRDefinitions, loadFromPath } from '../../src/fhirdefs';
 import { ResolveFn } from '../../src/fhirtypes';
@@ -15,7 +15,7 @@ describe('InstanceExporter', () => {
   let doc: FSHDocument;
   let input: FSHTank;
   let exporter: InstanceExporter;
-  let profileExporter: ProfileExporter;
+  let structureDefinitionExporter: StructureDefinitionExporter;
 
   beforeAll(() => {
     defs = new FHIRDefinitions();
@@ -30,9 +30,13 @@ describe('InstanceExporter', () => {
   beforeEach(() => {
     doc = new FSHDocument('fileName');
     input = new FSHTank([doc], { name: 'test', version: '0.0.1', canonical: 'http://example.com' });
-    profileExporter = new ProfileExporter(defs, input);
-    spyResolve(profileExporter, resolve);
-    exporter = new InstanceExporter(defs, input, profileExporter.resolve.bind(profileExporter));
+    structureDefinitionExporter = new StructureDefinitionExporter(defs, input);
+    spyResolve(structureDefinitionExporter, resolve);
+    exporter = new InstanceExporter(
+      defs,
+      input,
+      structureDefinitionExporter.resolve.bind(structureDefinitionExporter)
+    );
   });
 
   it('should output empty results with empty input', () => {
diff --git a/test/export/ExtensionExporter.test.ts b/test/export/StructureDefinition.ExtensionExporter.test.ts
similarity index 89%
rename from test/export/ExtensionExporter.test.ts
rename to test/export/StructureDefinition.ExtensionExporter.test.ts
index 4583a81..01f26f2 100644
--- a/test/export/ExtensionExporter.test.ts
+++ b/test/export/StructureDefinition.ExtensionExporter.test.ts
@@ -1,4 +1,4 @@
-import { ExtensionExporter } from '../../src/export';
+import { StructureDefinitionExporter } from '../../src/export';
 import { FSHTank, FSHDocument } from '../../src/import';
 import { FHIRDefinitions, loadFromPath } from '../../src/fhirdefs';
 import { Extension } from '../../src/fshtypes';
@@ -12,7 +12,7 @@ describe('ExtensionExporter', () => {
   let defs: FHIRDefinitions;
   let doc: FSHDocument;
   let input: FSHTank;
-  let exporter: ExtensionExporter;
+  let exporter: StructureDefinitionExporter;
   let resolve: ResolveFn;
 
   beforeAll(() => {
@@ -28,19 +28,19 @@ describe('ExtensionExporter', () => {
   beforeEach(() => {
     doc = new FSHDocument('fileName');
     input = new FSHTank([doc], { name: 'test', version: '0.0.1', canonical: 'http://example.com' });
-    exporter = new ExtensionExporter(defs, input);
+    exporter = new StructureDefinitionExporter(defs, input);
     spyResolve(exporter, resolve);
   });
 
   it('should output empty results with empty input', () => {
-    const exported = exporter.export();
+    const exported = exporter.export().extensionDefs;
     expect(exported).toEqual([]);
   });
 
   it('should export a single extension', () => {
     const extension = new Extension('Foo');
     doc.extensions.set(extension.name, extension);
-    const exported = exporter.export();
+    const exported = exporter.export().extensionDefs;
     expect(exported.length).toBe(1);
   });
 
@@ -49,7 +49,7 @@ describe('ExtensionExporter', () => {
     const extensionBar = new Extension('Bar');
     doc.extensions.set(extensionFoo.name, extensionFoo);
     doc.extensions.set(extensionBar.name, extensionBar);
-    const exported = exporter.export();
+    const exported = exporter.export().extensionDefs;
     expect(exported.length).toBe(2);
   });
 
@@ -59,7 +59,7 @@ describe('ExtensionExporter', () => {
     const extensionBar = new Extension('Bar');
     doc.extensions.set(extensionFoo.name, extensionFoo);
     doc.extensions.set(extensionBar.name, extensionBar);
-    const exported = exporter.export();
+    const exported = exporter.export().extensionDefs;
     expect(exported.length).toBe(1);
     expect(exported[0].name).toBe('Bar');
   });
@@ -78,7 +78,7 @@ describe('ExtensionExporter', () => {
     extensionBar.parent = 'Foo';
     doc.extensions.set(extensionFoo.name, extensionFoo);
     doc.extensions.set(extensionBar.name, extensionBar);
-    const exported = exporter.export();
+    const exported = exporter.export().extensionDefs;
     expect(exported.length).toBe(2);
     expect(exported[0].name).toBe('Foo');
     expect(exported[1].name).toBe('Bar');
@@ -94,7 +94,7 @@ describe('ExtensionExporter', () => {
     doc.extensions.set(extensionFoo.name, extensionFoo);
     doc.extensions.set(extensionBar.name, extensionBar);
     doc.extensions.set(extensionBaz.name, extensionBaz);
-    const exported = exporter.export();
+    const exported = exporter.export().extensionDefs;
     expect(exported.length).toBe(3);
     expect(exported[0].name).toBe('Foo');
     expect(exported[1].name).toBe('Bar');
@@ -112,7 +112,7 @@ describe('ExtensionExporter', () => {
     doc.extensions.set(extensionFoo.name, extensionFoo);
     doc.extensions.set(extensionBar.name, extensionBar);
     doc.extensions.set(extensionBaz.name, extensionBaz);
-    const exported = exporter.export();
+    const exported = exporter.export().extensionDefs;
     expect(exported.length).toBe(3);
     expect(exported[0].name).toBe('Foo');
     expect(exported[1].name).toBe('Bar');
@@ -130,7 +130,7 @@ describe('ExtensionExporter', () => {
     doc.extensions.set(extensionFoo.name, extensionFoo);
     doc.extensions.set(extensionBar.name, extensionBar);
     doc.extensions.set(extensionBaz.name, extensionBaz);
-    const exported = exporter.export();
+    const exported = exporter.export().extensionDefs;
     expect(exported.length).toBe(3);
     expect(exported[0].name).toBe('Baz');
     expect(exported[1].name).toBe('Bar');
diff --git a/test/export/ProfileExporter.test.ts b/test/export/StructureDefinition.ProfileExporter.test.ts
similarity index 89%
rename from test/export/ProfileExporter.test.ts
rename to test/export/StructureDefinition.ProfileExporter.test.ts
index 50f910d..39e6fb6 100644
--- a/test/export/ProfileExporter.test.ts
+++ b/test/export/StructureDefinition.ProfileExporter.test.ts
@@ -1,4 +1,4 @@
-import { ProfileExporter } from '../../src/export';
+import { StructureDefinitionExporter } from '../../src/export';
 import { FSHTank, FSHDocument } from '../../src/import';
 import { FHIRDefinitions, loadFromPath } from '../../src/fhirdefs';
 import { Profile } from '../../src/fshtypes';
@@ -12,7 +12,7 @@ describe('ProfileExporter', () => {
   let defs: FHIRDefinitions;
   let doc: FSHDocument;
   let input: FSHTank;
-  let exporter: ProfileExporter;
+  let exporter: StructureDefinitionExporter;
   let resolve: ResolveFn;
 
   beforeAll(() => {
@@ -28,19 +28,19 @@ describe('ProfileExporter', () => {
   beforeEach(() => {
     doc = new FSHDocument('fileName');
     input = new FSHTank([doc], { name: 'test', version: '0.0.1', canonical: 'http://example.com' });
-    exporter = new ProfileExporter(defs, input);
+    exporter = new StructureDefinitionExporter(defs, input);
     spyResolve(exporter, resolve);
   });
 
   it('should output empty results with empty input', () => {
-    const exported = exporter.export();
+    const exported = exporter.export().profileDefs;
     expect(exported).toEqual([]);
   });
 
   it('should export a single profile', () => {
     const profile = new Profile('Foo');
     doc.profiles.set(profile.name, profile);
-    const exported = exporter.export();
+    const exported = exporter.export().profileDefs;
     expect(exported.length).toBe(1);
   });
 
@@ -49,7 +49,7 @@ describe('ProfileExporter', () => {
     const profileBar = new Profile('Bar');
     doc.profiles.set(profileFoo.name, profileFoo);
     doc.profiles.set(profileBar.name, profileBar);
-    const exported = exporter.export();
+    const exported = exporter.export().profileDefs;
     expect(exported.length).toBe(2);
   });
 
@@ -59,7 +59,7 @@ describe('ProfileExporter', () => {
     const profileBar = new Profile('Bar');
     doc.profiles.set(profileFoo.name, profileFoo);
     doc.profiles.set(profileBar.name, profileBar);
-    const exported = exporter.export();
+    const exported = exporter.export().profileDefs;
     expect(exported.length).toBe(1);
     expect(exported[0].name).toBe('Bar');
   });
@@ -78,7 +78,7 @@ describe('ProfileExporter', () => {
     profileBar.parent = 'Foo';
     doc.profiles.set(profileFoo.name, profileFoo);
     doc.profiles.set(profileBar.name, profileBar);
-    const exported = exporter.export();
+    const exported = exporter.export().profileDefs;
     expect(exported.length).toBe(2);
     expect(exported[0].name).toBe('Foo');
     expect(exported[1].name).toBe('Bar');
@@ -94,7 +94,7 @@ describe('ProfileExporter', () => {
     doc.profiles.set(profileFoo.name, profileFoo);
     doc.profiles.set(profileBar.name, profileBar);
     doc.profiles.set(profileBaz.name, profileBaz);
-    const exported = exporter.export();
+    const exported = exporter.export().profileDefs;
     expect(exported.length).toBe(3);
     expect(exported[0].name).toBe('Foo');
     expect(exported[1].name).toBe('Bar');
@@ -112,7 +112,7 @@ describe('ProfileExporter', () => {
     doc.profiles.set(profileFoo.name, profileFoo);
     doc.profiles.set(profileBar.name, profileBar);
     doc.profiles.set(profileBaz.name, profileBaz);
-    const exported = exporter.export();
+    const exported = exporter.export().profileDefs;
     expect(exported.length).toBe(3);
     expect(exported[0].name).toBe('Foo');
     expect(exported[1].name).toBe('Bar');
@@ -130,7 +130,7 @@ describe('ProfileExporter', () => {
     doc.profiles.set(profileFoo.name, profileFoo);
     doc.profiles.set(profileBar.name, profileBar);
     doc.profiles.set(profileBaz.name, profileBaz);
-    const exported = exporter.export();
+    const exported = exporter.export().profileDefs;
     expect(exported.length).toBe(3);
     expect(exported[0].name).toBe('Baz');
     expect(exported[1].name).toBe('Bar');
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 2d38d36..25828eb 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -50,7 +50,7 @@ describe('StructureDefinitionExporter', () => {
     profile.description = 'foo bar foobar';
     doc.profiles.set(profile.name, profile);
     exporter.exportStructDef(profile);
-    const exported = exporter.structDefs[0];
+    const exported = exporter.profileDefs[0];
     expect(exported.name).toBe('Foo');
     expect(exported.id).toBe('foo');
     expect(exported.title).toBe('Foo Profile');
@@ -65,7 +65,7 @@ describe('StructureDefinitionExporter', () => {
     const profile = new Profile('Foo');
     doc.profiles.set(profile.name, profile);
     exporter.exportStructDef(profile);
-    const exported = exporter.structDefs[0];
+    const exported = exporter.profileDefs[0];
     expect(exported.name).toBe('Foo');
     expect(exported.id).toBe('Foo');
     expect(exported.title).toBeUndefined();
@@ -93,7 +93,7 @@ describe('StructureDefinitionExporter', () => {
     extension.description = 'foo bar foobar';
     doc.extensions.set(extension.name, extension);
     exporter.exportStructDef(extension);
-    const exported = exporter.structDefs[0];
+    const exported = exporter.extensionDefs[0];
     expect(exported.name).toBe('Foo');
     expect(exported.id).toBe('foo');
     expect(exported.title).toBe('Foo Profile');
@@ -116,7 +116,7 @@ describe('StructureDefinitionExporter', () => {
     const extension = new Extension('Foo');
     doc.extensions.set(extension.name, extension);
     exporter.exportStructDef(extension);
-    const exported = exporter.structDefs[0];
+    const exported = exporter.extensionDefs[0];
     expect(exported.name).toBe('Foo');
     expect(exported.id).toBe('Foo');
     expect(exported.title).toBeUndefined();
@@ -146,7 +146,7 @@ describe('StructureDefinitionExporter', () => {
     extension.parent = 'http://hl7.org/fhir/StructureDefinition/patient-animal';
     doc.extensions.set(extension.name, extension);
     exporter.exportStructDef(extension);
-    const exported = exporter.structDefs[0];
+    const exported = exporter.extensionDefs[0];
     expect(exported.context).toEqual([
       {
         type: 'element',
@@ -172,7 +172,7 @@ describe('StructureDefinitionExporter', () => {
     rule.max = '1';
     profile.rules.push(rule);
     exporter.exportStructDef(profile);
-    const structDef = exporter.structDefs[0];
+    const structDef = exporter.profileDefs[0];
     expect(structDef).toBeDefined();
     expect(structDef.type).toBe('Resource');
     expect(loggerSpy.getLastMessage()).toMatch(/File: Foo\.fsh.*Line: 3 - 4\D/s);
@@ -189,7 +189,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('Observation');
 
     const baseCard = baseStructDef.findElement('Observation.subject');
@@ -211,7 +211,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('Observation');
 
     const baseCard = baseStructDef.findElement('Observation.status');
@@ -235,7 +235,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('DiagnosticReport');
 
     const baseElement = baseStructDef.findElement('DiagnosticReport.conclusion');
@@ -258,7 +258,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('DiagnosticReport');
 
     const baseElement = baseStructDef.findElement('DiagnosticReport.status');
@@ -281,7 +281,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('http://hl7.org/fhir/StructureDefinition/vitalsigns');
 
     const baseElement = baseStructDef.findElement('Observation.code');
@@ -306,7 +306,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(vsRule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('Appointment');
     const baseElement = baseStructDef.findElement('Appointment.description');
     const changedElement = sd.findElement('Appointment.description');
@@ -325,7 +325,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(vsRule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('Observation');
     const baseElement = baseStructDef.findElement('Observation.category');
     const changedElement = sd.findElement('Observation.category');
@@ -345,7 +345,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(vsRule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('Observation');
     const baseElement = baseStructDef.findElement('Observation.note');
     const changedElement = sd.findElement('Observation.note');
@@ -364,7 +364,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(vsRule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('Observation');
     const baseElement = baseStructDef.findElement('Observation.category');
     const changedElement = sd.findElement('Observation.category');
@@ -387,7 +387,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('Observation');
 
     const baseValue = baseStructDef.findElement('Observation.value[x]');
@@ -411,7 +411,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('Observation');
 
     const baseSubject = baseStructDef.findElement('Observation.subject');
@@ -446,7 +446,7 @@ describe('StructureDefinitionExporter', () => {
     extension.rules.push(rule);
 
     exporter.exportStructDef(extension);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.extensionDefs[0];
     const baseStructDef = resolve('Extension');
 
     const baseValueX = baseStructDef.findElement('Extension.value[x]');
@@ -478,7 +478,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('Observation');
 
     const baseHasMember = baseStructDef.findElement('Observation.hasMember');
@@ -517,7 +517,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[1];
+    const sd = exporter.profileDefs[1];
     const baseStructDef = resolve('Observation');
 
     const baseValue = baseStructDef.findElement('Observation.value[x]');
@@ -549,7 +549,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[1];
+    const sd = exporter.profileDefs[1];
     const baseStructDef = resolve('Observation');
 
     const baseSubject = baseStructDef.findElement('Observation.subject');
@@ -592,7 +592,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[2];
+    const sd = exporter.profileDefs[2];
     const baseStructDef = resolve('Observation');
 
     const baseHasMember = baseStructDef.findElement('Observation.hasMember');
@@ -627,7 +627,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('Observation');
 
     const baseValue = baseStructDef.findElement('Observation.value[x]');
@@ -649,7 +649,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('Observation');
 
     const baseCode = baseStructDef.findElement('Observation.code');
@@ -670,7 +670,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('Observation');
 
     const baseCode = baseStructDef.findElement('Observation.code');
@@ -691,7 +691,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('resprate');
 
     const barSlice = sd.elements.find(e => e.id === 'Observation.code.coding:barSlice');
@@ -709,7 +709,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
 
     const extension = sd.elements.find(e => e.id === 'Observation.extension');
     const valuesetExpression = sd.elements.find(
@@ -736,7 +736,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
 
     const extension = sd.elements.find(e => e.id === 'Observation.modifierExtension');
     const valuesetExpression = sd.elements.find(
@@ -774,7 +774,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(ruleBaz);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[2];
+    const sd = exporter.profileDefs[0];
 
     const extension = sd.elements.find(e => e.id === 'Observation.extension');
     const bar = sd.elements.find(e => e.id === 'Observation.extension:barAlias');
@@ -810,7 +810,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(ruleBar);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
 
     const extension = sd.elements.find(e => e.id === 'Observation.extension');
     const VSExpression = sd.elements.find(e => e.id === 'Observation.extension:VSAlias');
@@ -838,7 +838,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule2);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('resprate');
 
     const barSlice = sd.elements.find(e => e.id === 'Observation.code.coding:barSlice');
@@ -858,7 +858,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('resprate');
 
     const barSlice = sd.elements.find(e => e.id === 'Observation.identifier:barSlice');
@@ -879,7 +879,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
 
     const status = sd.findElement('Observation.status');
     expect(status.short).toBe('foo');
@@ -897,7 +897,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('Observation');
 
     const status = sd.findElement('Observation.status');
@@ -917,7 +917,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     expect(sd.description).toBe('foo');
   });
 
@@ -931,7 +931,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const baseStructDef = resolve('Observation');
 
     expect(sd.description).toBe(baseStructDef.description);
@@ -967,7 +967,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const json = sd.toJSON();
 
     expect(json.differential.element).toHaveLength(1);
@@ -997,7 +997,7 @@ describe('StructureDefinitionExporter', () => {
     profile.rules.push(rule);
 
     exporter.exportStructDef(profile);
-    const sd = exporter.structDefs[0];
+    const sd = exporter.profileDefs[0];
     const json = sd.toJSON();
 
     expect(json.differential.element).toHaveLength(2);
@@ -1012,4 +1012,24 @@ describe('StructureDefinitionExporter', () => {
       min: 1
     });
   });
+
+  // No duplicate structure definitions exported
+  it('should not export duplicate structure definitions', () => {
+    const profile = new Profile('Foo');
+    profile.parent = 'Patient';
+    doc.profiles.set(profile.name, profile);
+
+    const extension = new Extension('Bar');
+    extension.id = 'bar';
+    doc.aliases.set('barAlias', 'Bar');
+    doc.extensions.set(extension.name, extension);
+
+    const ruleBar = new ContainsRule('extension');
+    ruleBar.items = ['barAlias'];
+    profile.rules.push(ruleBar);
+
+    const { profileDefs, extensionDefs } = exporter.export();
+    expect(profileDefs.length).toBe(1);
+    expect(extensionDefs.length).toBe(1);
+  });
 });