Repository: FHIR/sushi
Pull request #1280 (2023-05-26 16:24:54)

Support for Instances of Logical Models
Authors can now use `Instance:` to create instances of logical models (and profiles of logical models). When they do:
* Standard JSON serialization will be used as described by the FHIR spec
* The `resourceType` property will be set to the URL of the logical model
* If the logical model supports an `id` element, it will be populated w/ the instance name (unless configured otherwise)
* If the logical model supports a `meta` element, instances of profiles will have `meta.profile` populated with the URL of the logical model (unless configured otherwise)
* The logical model will be exported as `Binary-{id}`, per requirements of the IG Publisher
* The resource entry in the IG JSON will use the [implemenationguide-resource-format](http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format) extension, per requirements of the IG Publisher

As a result, these instances will be registered in the IG and rendered as simple JSON on their respective pages.

Implements [CIMPL-834](https://standardhealthrecord.atlassian.net/browse/CIMPL-834) and addresses #924.

cmoesel: I've performed a full regression, which came out clean.
cmoesel: Thanks for the great review, @mint-thompson. Your comments have been addressed!

Patch:
diff --git a/src/errors/InstanceOfLogicalProfileError.ts b/src/errors/InstanceOfLogicalProfileError.ts
deleted file mode 100644
index 65ec9be..0000000
--- a/src/errors/InstanceOfLogicalProfileError.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { WithSource } from './WithSource';
-import { SourceInfo } from '../fshtypes';
-
-export class InstanceOfLogicalProfileError extends Error implements WithSource {
-  constructor(
-    public instanceName: string,
-    public instanceOf: string,
-    public sourceInfo: SourceInfo
-  ) {
-    super(
-      `Instance ${instanceName} cannot be created: InstanceOf ${instanceOf} represents a Profile of a logical model.`
-    );
-  }
-}
diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts
index 340e1be..65e8787 100644
--- a/src/export/InstanceExporter.ts
+++ b/src/export/InstanceExporter.ts
@@ -24,7 +24,6 @@ import {
   setImpliedPropertiesOnInstance
 } from '../fhirtypes/common';
 import { InstanceOfNotDefinedError } from '../errors/InstanceOfNotDefinedError';
-import { InstanceOfLogicalProfileError } from '../errors/InstanceOfLogicalProfileError';
 import { AbstractInstanceOfError } from '../errors/AbstractInstanceOfError';
 import { MismatchedTypeError } from '../errors/MismatchedTypeError';
 import { Package } from '.';
@@ -471,28 +470,52 @@ export class InstanceExporter implements Fishable {
     this.validateRequiredChildElements(instanceDef, elements[0], fshDefinition);
   }
 
-  private shouldSetMetaProfile(instanceDef: InstanceDefinition): boolean {
-    switch (this.tank.config.instanceOptions?.setMetaProfile) {
-      case 'never':
-        return false;
-      case 'inline-only':
-        return instanceDef._instanceMeta.usage === 'Inline';
-      case 'standalone-only':
-        return instanceDef._instanceMeta.usage !== 'Inline';
-      case 'always':
-      default:
-        return true;
+  private shouldSetMetaProfile(
+    instanceDef: InstanceDefinition,
+    instanceOfStructureDefinition: StructureDefinition
+  ): boolean {
+    const configSetMeta = this.tank.config.instanceOptions?.setMetaProfile;
+    const isInline = instanceDef._instanceMeta.usage === 'Inline';
+    if (
+      configSetMeta === 'never' ||
+      (configSetMeta === 'inline-only' && !isInline) ||
+      (configSetMeta === 'standalone-only' && isInline)
+    ) {
+      return false;
     }
+    // Config allows it, so set the meta.profile as long as the instance is
+    // a profile and the meta element exists with the right type and card
+    return (
+      instanceOfStructureDefinition.derivation === 'constraint' &&
+      instanceOfStructureDefinition.elements.some(
+        el =>
+          el.path === `${instanceOfStructureDefinition.pathType}.meta` &&
+          el.type?.[0].code === 'Meta' &&
+          el.max === '1' &&
+          (el.base?.max == null || el.base.max === '1')
+      )
+    );
   }
 
-  private shouldSetId(instanceDef: InstanceDefinition): boolean {
-    switch (this.tank.config.instanceOptions?.setId) {
-      case 'standalone-only':
-        return instanceDef._instanceMeta.usage !== 'Inline';
-      case 'always':
-      default:
-        return true;
+  private shouldSetId(
+    instanceDef: InstanceDefinition,
+    instanceOfStructureDefinition: StructureDefinition
+  ): boolean {
+    if (
+      this.tank.config.instanceOptions?.setId === 'standalone-only' &&
+      instanceDef._instanceMeta.usage === 'Inline'
+    ) {
+      return false;
     }
+    // Config allows it, so set the id as long as the id element exists with the
+    // right type and card (note: FHIR resource ids are actually 'string' type)
+    return instanceOfStructureDefinition.elements.some(
+      el =>
+        el.path === `${instanceOfStructureDefinition.pathType}.id` &&
+        ['string', 'id'].includes(el.type?.[0].code) &&
+        el.max === '1' &&
+        (el.base?.max == null || el.base.max === '1')
+    );
   }
 
   /**
@@ -537,7 +560,7 @@ export class InstanceExporter implements Fishable {
               const matchedNames = matchedNamedValues.map(nv => nv.name);
               if (matchedNamedValues.every(nm => isEqual(value, nm.value))) {
                 logger.warn(
-                  `${instanceDef.id} has an array item that is exactly the same as a required slice, but does not ` +
+                  `${instanceDef._instanceMeta.name} has an array item that is exactly the same as a required slice, but does not ` +
                     'use the slice name in its path. This may result in unexpected items in your array. Usually you ' +
                     'can remove this item from your FSH, since FSH instances inherit required assigned values from ' +
                     "their profile. Alternately, you can modify your FSH to use the slice name in the item's path. " +
@@ -549,7 +572,7 @@ export class InstanceExporter implements Fishable {
                 );
               } else {
                 logger.warn(
-                  `${instanceDef.id} has an array item that matches a required slice, but does not use the slice name ` +
+                  `${instanceDef._instanceMeta.name} has an array item that matches a required slice, but does not use the slice name ` +
                     'in its path. This may result in unexpected items in your array. If the item is intended to be ' +
                     "in the slice, modify your FSH to use the slice name in the item's path. See: " +
                     'https://hl7.org/fhir/uv/shorthand/reference.html#sliced-array-paths\n' +
@@ -608,7 +631,8 @@ export class InstanceExporter implements Fishable {
       Type.Resource,
       Type.Profile,
       Type.Extension,
-      Type.Type
+      Type.Type,
+      Type.Logical
     );
 
     if (!json) {
@@ -619,22 +643,12 @@ export class InstanceExporter implements Fishable {
       );
     }
 
-    // Since creating an instance of a Logical is not allowed,
-    // creating an instance of a Profile of a logical model is also not allowed
-    if (json.kind === 'logical' && json.derivation === 'constraint') {
-      throw new InstanceOfLogicalProfileError(
-        fshDefinition.name,
-        fshDefinition.instanceOf,
-        fshDefinition.sourceInfo
-      );
-    }
-
-    if (json.kind !== 'resource') {
+    if (json.kind !== 'resource' && json.kind !== 'logical') {
       // If the instance is not a resource, it should be inline, since it cannot exist as a standalone instance
       isResource = false;
       if (fshDefinition.usage !== 'Inline') {
         logger.warn(
-          `Instance ${fshDefinition.name} is not an instance of a resource, so it should only be used inline on other instances, and it will not be exported to a standalone file. Specify "Usage: #inline" to remove this warning.`,
+          `Instance ${fshDefinition.name} is not an instance of a resource or logical model, so it should only be used inline on other instances, and it will not be exported to a standalone file. Specify "Usage: #inline" to remove this warning.`,
           fshDefinition.sourceInfo
         );
         fshDefinition.usage = 'Inline';
@@ -677,15 +691,15 @@ export class InstanceExporter implements Fishable {
       if (fshDefinition.usage === 'Definition') {
         if (
           instanceOfStructureDefinition.elements.some(
-            element => element.id === `${instanceOfStructureDefinition.type}.url`
+            element => element.id === `${instanceOfStructureDefinition.pathType}.url`
           )
         ) {
-          instanceDef.url = `${this.tank.config.canonical}/${instanceOfStructureDefinition.type}/${fshDefinition.id}`;
+          instanceDef.url = `${this.tank.config.canonical}/${instanceOfStructureDefinition.pathType}/${fshDefinition.id}`;
         }
         if (
           fshDefinition.title &&
           instanceOfStructureDefinition.elements.some(
-            element => element.id === `${instanceOfStructureDefinition.type}.title`
+            element => element.id === `${instanceOfStructureDefinition.pathType}.title`
           )
         ) {
           instanceDef.title = fshDefinition.title;
@@ -693,7 +707,7 @@ export class InstanceExporter implements Fishable {
         if (
           fshDefinition.description &&
           instanceOfStructureDefinition.elements.some(
-            element => element.id === `${instanceOfStructureDefinition.type}.description`
+            element => element.id === `${instanceOfStructureDefinition.pathType}.description`
           )
         ) {
           instanceDef.description = fshDefinition.description;
@@ -702,23 +716,19 @@ export class InstanceExporter implements Fishable {
     }
     if (isResource) {
       instanceDef.resourceType = instanceOfStructureDefinition.type; // ResourceType is determined by the StructureDefinition of the type
-      if (this.shouldSetId(instanceDef)) {
+      if (this.shouldSetId(instanceDef, instanceOfStructureDefinition)) {
         instanceDef.id = fshDefinition.id;
       }
-    } else {
-      instanceDef._instanceMeta.sdType = instanceOfStructureDefinition.type;
     }
+    instanceDef._instanceMeta.sdType = instanceOfStructureDefinition.type;
+    instanceDef._instanceMeta.sdKind = instanceOfStructureDefinition.kind;
 
     // Set Assigned values based on the FSH rules and the Structure Definition
     instanceDef = this.setAssignedValues(fshDefinition, instanceDef, instanceOfStructureDefinition);
     // should we add the instanceOf to meta.profile?
     // if the exact url is not in there, and a versioned url is also not in there, add it to the front.
     // otherwise, add it at the front.
-    if (
-      this.shouldSetMetaProfile(instanceDef) &&
-      isResource &&
-      instanceOfStructureDefinition.derivation === 'constraint'
-    ) {
+    if (this.shouldSetMetaProfile(instanceDef, instanceOfStructureDefinition)) {
       // elements of instanceDef.meta.profile may be objects if they are provided by slices,
       // since they have to keep track of the _sliceName property.
       // this is technically not a match for the defined type of instanceDef.meta.profile,
@@ -767,12 +777,15 @@ export class InstanceExporter implements Fishable {
         instance =>
           instance._instanceMeta.usage !== 'Inline' &&
           instanceDef.resourceType === instance.resourceType &&
-          instanceDef.id === instance.id &&
+          (instanceDef.id ?? instanceDef._instanceMeta.name) ===
+            (instance.id ?? instance._instanceMeta.name) &&
           instanceDef !== instance
       )
     ) {
       logger.error(
-        `Multiple instances of type ${instanceDef.resourceType} with id ${instanceDef.id}. Each non-inline instance of a given type must have a unique id.`,
+        `Multiple instances of type ${instanceDef.resourceType} with id ${
+          instanceDef.id ?? instanceDef._instanceMeta.name
+        }. Each non-inline instance of a given type must have a unique id.`,
         fshDefinition.sourceInfo
       );
     }
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index ab15abc..91b841e 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -377,7 +377,7 @@ export class ElementDefinition {
   }
 
   getPathWithoutBase(): string {
-    return this.path.slice(this.structDef.type.length + 1);
+    return this.path.slice(this.structDef.pathType.length + 1);
   }
 
   /**
@@ -2369,29 +2369,25 @@ export class ElementDefinition {
       if (newElements.length === 0) {
         // If we have exactly one profile to use, use that, otherwise use the code
         const type = profileToUse ?? this.type[0].code;
+        // Fish on standard types, but if this is a logical model, also fish on logical models
+        const typesToFish = [Type.Resource, Type.Type, Type.Profile, Type.Extension];
+        if (this.structDef?.kind === 'logical') {
+          typesToFish.unshift(Type.Logical);
+        }
         // There could possibly be a |version appended to the type, so try to fish
         // for that version but fall back to any version if necessary
         let json = fishForFHIRBestVersion(
           fisher,
           type,
           null, // no source info
-          Type.Resource,
-          Type.Type,
-          Type.Profile,
-          Type.Extension
+          ...typesToFish
         );
         if (!json && profileToUse) {
           logger.warn(
             `SUSHI tried to find profile ${type} but could not find it and instead will try to use ${this.type[0].code}`
           );
           // If we tried to fish based on a profile and didn't find anything, fall back to the type
-          json = fisher.fishForFHIR(
-            this.type[0].code,
-            Type.Resource,
-            Type.Type,
-            Type.Profile,
-            Type.Extension
-          );
+          json = fisher.fishForFHIR(this.type[0].code, ...typesToFish);
         }
         if (json) {
           const def = StructureDefinition.fromJSON(json);
@@ -2402,7 +2398,7 @@ export class ElementDefinition {
           }
           newElements = def.elements.slice(1).map(e => {
             const eClone = e.clone();
-            eClone.id = eClone.id.replace(def.type, `${this.id}`);
+            eClone.id = eClone.id.replace(def.pathType, `${this.id}`);
             eClone.structDef = this.structDef;
             // Capture the original so that diffs only show what changed *after* unfolding
             eClone.captureOriginal();
@@ -2531,7 +2527,7 @@ export class ElementDefinition {
       const commonAncestor = StructureDefinition.fromJSON(fisher.fishForFHIR(sharedAncestry[0]));
       const newElements = commonAncestor.elements.slice(1).map(e => {
         const eClone = e.clone();
-        eClone.id = eClone.id.replace(commonAncestor.type, `${this.id}`);
+        eClone.id = eClone.id.replace(commonAncestor.pathType, `${this.id}`);
         eClone.structDef = this.structDef;
         // Capture the original so that diffs only show what changed *after* unfolding
         eClone.captureOriginal();
diff --git a/src/fhirtypes/InstanceDefinition.ts b/src/fhirtypes/InstanceDefinition.ts
index 2aa4905..70f3a1c 100644
--- a/src/fhirtypes/InstanceDefinition.ts
+++ b/src/fhirtypes/InstanceDefinition.ts
@@ -24,7 +24,9 @@ export class InstanceDefinition {
    * @returns {string} the filename
    */
   getFileName(): string {
-    return sanitize(`${this.resourceType}-${this.id ?? this._instanceMeta.name}.json`, {
+    // Logical instances should use Binary type. See: https://fshschool.org/docs/sushi/tips/#instances-of-logical-models
+    const type = this._instanceMeta.sdKind === 'logical' ? 'Binary' : this.resourceType;
+    return sanitize(`${type}-${this.id ?? this._instanceMeta.name}.json`, {
       replacement: '-'
     });
   }
@@ -57,6 +59,7 @@ type InstanceMeta = {
   description?: string;
   usage?: InstanceUsage;
   sdType?: string;
+  sdKind?: string;
   instanceOfUrl?: string;
 };
 
diff --git a/src/fhirtypes/StructureDefinition.ts b/src/fhirtypes/StructureDefinition.ts
index 41ec1d1..cd76704 100644
--- a/src/fhirtypes/StructureDefinition.ts
+++ b/src/fhirtypes/StructureDefinition.ts
@@ -616,11 +616,11 @@ export class StructureDefinition {
         pathPart.brackets.push('0');
       }
 
-      // Primitive and only primitives have a lower case first letter
+      // Types starting with a lowercase letter and with only alphanumeric characters are primitives.
+      // Logical models can start with a lowercase letter, but they are URLs (so have non-alphanumerics).
       if (
         currentElement.type?.length === 1 &&
-        currentElement.type[0].code.charAt(0) ===
-          currentElement.type[0].code.charAt(0).toLowerCase()
+        /^[a-z][a-zA-Z0-9]*$/.test(currentElement.type[0].code)
       ) {
         pathPart.primitive = true;
       }
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index c1f5830..b545f55 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -46,7 +46,7 @@ function isR4(fhirVersion: string[]) {
 // and http://hl7.org/fhir/5.0.0-snapshot1/resourcelist.html
 const CONFORMANCE_AND_TERMINOLOGY_RESOURCES = new Set([
   'CapabilityStatement',
-  'CapabilityStatement2', // R5
+  'CapabilityStatement2', // pre-release R5
   'StructureDefinition',
   'ImplementationGuide',
   'SearchParameter',
@@ -59,7 +59,7 @@ const CONFORMANCE_AND_TERMINOLOGY_RESOURCES = new Set([
   'CodeSystem',
   'ValueSet',
   'ConceptMap',
-  'ConceptMap2', // R5
+  'ConceptMap2', // pre-release R5
   'NamingSystem',
   'TerminologyCapabilities'
 ]);
@@ -834,9 +834,10 @@ export class IGExporter {
           !this.pkg.resources.some(r => r.type === instance.resourceType)
       )
       .forEach(instance => {
-        const referenceKey = `${instance.resourceType}/${
-          instance.id ?? instance._instanceMeta.name
-        }`;
+        // Logical instances should use Binary type. See: https://fshschool.org/docs/sushi/tips/#instances-of-logical-models
+        const referenceKey = `${
+          instance._instanceMeta.sdKind === 'logical' ? 'Binary' : instance.resourceType
+        }/${instance.id ?? instance._instanceMeta.name}`;
         const configResource = (this.config.resources ?? []).find(
           resource => resource.reference?.reference == referenceKey
         );
@@ -888,9 +889,10 @@ export class IGExporter {
       const exampleUrl = [...metaProfileUrls, pkgResource._instanceMeta.instanceOfUrl ?? ''].find(
         url => {
           const [baseUrl, version] = url.split('|', 2);
-          const availableProfile = this.pkg.fish(baseUrl, Type.Profile);
+          const availableProfileOrLogical = this.pkg.fish(baseUrl, Type.Profile, Type.Logical);
           return (
-            availableProfile != null && (version == null || version === availableProfile.version)
+            availableProfileOrLogical != null &&
+            (version == null || version === availableProfileOrLogical.version)
           );
         }
       );
@@ -905,6 +907,21 @@ export class IGExporter {
     if (configResource?.extension?.length) {
       newResource.extension = configResource.extension;
     }
+    if (
+      pkgResource instanceof InstanceDefinition &&
+      pkgResource._instanceMeta.sdKind === 'logical' &&
+      !configResource?.extension?.some(
+        ext =>
+          ext.url === 'http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format'
+      )
+    ) {
+      // Logical instances should add a special extension. See: https://fshschool.org/docs/sushi/tips/#instances-of-logical-models
+      newResource.extension = newResource.extension ?? [];
+      newResource.extension.push({
+        url: 'http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format',
+        valueCode: 'application/fhir+json'
+      });
+    }
     this.ig.definition.resource.push(newResource);
   }
 
diff --git a/src/utils/Processing.ts b/src/utils/Processing.ts
index 1491b09..0fe8a05 100644
--- a/src/utils/Processing.ts
+++ b/src/utils/Processing.ts
@@ -593,7 +593,6 @@ export function writePreprocessedFSH(outDir: string, inDir: string, tank: FSHTan
   // Specifically, RuleSets have already been applied.
   // Aliases have already been resolved for most cases, but since they may still
   // be used in a slice name, they are included.
-  // TODO: Add Resources and Logicals once they are being imported and stored in docs
   tank.docs.forEach(doc => {
     let fileContent = '';
     // First, get all Aliases. They don't have source information.


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 8e142e4..51560f0 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -23,6 +23,7 @@ import {
   Logical
 } from '../../src/fshtypes';
 import {
+  AddElementRule,
   AssignmentRule,
   CardRule,
   CaretValueRule,
@@ -114,17 +115,6 @@ describe('InstanceExporter', () => {
     expect(loggerSpy.getLastMessage('error')).toMatch(/File: Bogus\.fsh.*Line: 2 - 4\D*/s);
   });
 
-  it('should log a message with source information when the parent is a profile of a logical model', () => {
-    const instance = new Instance('MyServiceInstance')
-      .withFile('Incorrect.fsh')
-      .withLocation([15, 1, 18, 27]);
-    instance.instanceOf = 'ServiceProfile';
-    doc.instances.set(instance.name, instance);
-    const exported = exporter.export().instances;
-    expect(exported.length).toBe(0);
-    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Incorrect\.fsh.*Line: 15 - 18\D*/s);
-  });
-
   it('should log a message with source information when the instanceOf is an abstract specialization', () => {
     const instance = new Instance('MyAbstractInstance')
       .withFile('Abstract.fsh')
@@ -283,8 +273,22 @@ describe('InstanceExporter', () => {
       doc.instances.set(provenanceInstance.name, provenanceInstance);
     });
 
+    // Setting resourceType
+
+    it('should set resourceType to the base resource type we are making an instance of', () => {
+      const boo = new Instance('Boo');
+      boo.instanceOf = 'Patient';
+      const exported = exportInstance(boo);
+      expect(exported.resourceType).toBe('Patient');
+    });
+
+    it('should set resourceType to the base resource type for the profile we are making an instance of', () => {
+      const exported = exportInstance(patientInstance);
+      expect(exported.resourceType).toBe('Patient');
+    });
+
     // Setting Metadata
-    it('should set meta.profile to the defining URL we are making an instance of', () => {
+    it('should set meta.profile to the defining profile URL we are making an instance of', () => {
       const exported = exportInstance(patientInstance);
       expect(exported.meta).toEqual({
         profile: ['http://hl7.org/fhir/us/minimal/StructureDefinition/TestPatient']
@@ -8717,6 +8721,213 @@ describe('InstanceExporter', () => {
       });
     });
 
+    describe('#Logical Models', () => {
+      let carLogical: Logical;
+      let carProfile: Profile;
+      let carInstance: Instance;
+      let carProfileInstance: Instance;
+
+      beforeEach(() => {
+        loggerSpy.reset();
+        carLogical = new Logical('Car');
+        doc.logicals.set(carLogical.name, carLogical);
+        carProfile = new Profile('CarProfile');
+        carProfile.parent = 'Car';
+        doc.profiles.set(carProfile.name, carProfile);
+        carInstance = new Instance('CarInstance');
+        carInstance.instanceOf = 'Car';
+        doc.instances.set(carInstance.name, carInstance);
+        carProfileInstance = new Instance('CarProfileInstance');
+        carProfileInstance.instanceOf = 'CarProfile';
+        doc.instances.set(carProfileInstance.name, carProfileInstance);
+      });
+
+      afterEach(() => {
+        // None of the test expect warnings or errors. All should be clean.
+        expect(loggerSpy.getAllLogs('warn')).toHaveLength(0);
+        expect(loggerSpy.getAllLogs('error')).toHaveLength(0);
+      });
+
+      // Setting resourceType
+
+      it('should set resourceType to the logical type we are making an instance of', () => {
+        const exported = exportInstance(carInstance);
+        expect(exported.resourceType).toBe(
+          'http://hl7.org/fhir/us/minimal/StructureDefinition/Car'
+        );
+      });
+
+      it('should set resourceType to the logical type for the profile of a logical we are making an instance of', () => {
+        const exported = exportInstance(carProfileInstance);
+        expect(exported.resourceType).toBe(
+          'http://hl7.org/fhir/us/minimal/StructureDefinition/Car'
+        );
+      });
+
+      // Setting meta.profile
+
+      it('should not set meta.profile when we are making an instance of a logical', () => {
+        const exported = exportInstance(carInstance);
+        expect(exported.meta).toBeUndefined();
+      });
+
+      it('should not set meta.profile when we are making an instance of a logical even when it has meta', () => {
+        const metaRule = new AddElementRule('meta');
+        metaRule.min = 0;
+        metaRule.max = '1';
+        metaRule.types = [{ type: 'Meta' }];
+        carLogical.rules.push(metaRule);
+        const exported = exportInstance(carInstance);
+        expect(exported.meta).toBeUndefined();
+      });
+
+      it('should not set meta.profile when we are making an instance of a profile of logical that has no meta', () => {
+        const exported = exportInstance(carProfileInstance);
+        expect(exported.meta).toBeUndefined();
+      });
+
+      it('should set meta.profile to the defining profile URL we are making an instance of logical (for profile of logical that has meta)', () => {
+        const metaRule = new AddElementRule('meta');
+        metaRule.min = 0;
+        metaRule.max = '1';
+        metaRule.types = [{ type: 'Meta' }];
+        carLogical.rules.push(metaRule);
+        const exported = exportInstance(carProfileInstance);
+        expect(exported.meta).toEqual({
+          profile: ['http://hl7.org/fhir/us/minimal/StructureDefinition/CarProfile']
+        });
+      });
+
+      it('should not set meta.profile when we are making an instance of a profile of a logical with >1 meta', () => {
+        const metaRule = new AddElementRule('meta');
+        metaRule.min = 0;
+        metaRule.max = '*';
+        metaRule.types = [{ type: 'Meta' }];
+        carLogical.rules.push(metaRule);
+        const exported = exportInstance(carProfileInstance);
+        expect(exported.meta).toBeUndefined();
+      });
+
+      it('should not set meta.profile when we are making an instance of a profile that constrains >1 meta to 1 meta', () => {
+        const metaRule = new AddElementRule('meta');
+        metaRule.min = 0;
+        metaRule.max = '*';
+        metaRule.types = [{ type: 'Meta' }];
+        carLogical.rules.push(metaRule);
+        const cardRule = new CardRule('meta');
+        cardRule.max = '1';
+        carProfile.rules.push(cardRule);
+        const exported = exportInstance(carProfileInstance);
+        expect(exported.meta).toBeUndefined();
+      });
+
+      // Setting id
+
+      it('should not set id for logicals without id element', () => {
+        const exported = exportInstance(carInstance);
+        expect(exported.id).toBeUndefined();
+      });
+
+      it('should set id to instance name for logicals with inherited id element', () => {
+        // Change carLogical parent to Element, which has an id
+        carLogical.parent = 'Element';
+        const exported = exportInstance(carInstance);
+        expect(exported.id).toBe('CarInstance');
+      });
+
+      it('should set id to instance name for logicals with new id element', () => {
+        const idRule = new AddElementRule('id');
+        idRule.min = 0;
+        idRule.max = '1';
+        idRule.types = [{ type: 'id' }];
+        carLogical.rules.push(idRule);
+        const exported = exportInstance(carInstance);
+        expect(exported.id).toBe('CarInstance');
+      });
+
+      it('should not set id for logical with >1 id element', () => {
+        const idRule = new AddElementRule('id');
+        idRule.min = 0;
+        idRule.max = '*';
+        idRule.types = [{ type: 'id' }];
+        carLogical.rules.push(idRule);
+        const exported = exportInstance(carInstance);
+        expect(exported.id).toBeUndefined();
+      });
+
+      it('should not set id for logical with profile constraining >1 id to 1 id', () => {
+        const idRule = new AddElementRule('id');
+        idRule.min = 0;
+        idRule.max = '*';
+        idRule.types = [{ type: 'id' }];
+        carLogical.rules.push(idRule);
+        const cardRule = new CardRule('id');
+        cardRule.max = '1';
+        carProfile.rules.push(cardRule);
+        const exported = exportInstance(carProfileInstance);
+        expect(exported.id).toBeUndefined();
+      });
+
+      // Assignment rules
+
+      it('should export simple assignment rules for a logical model', () => {
+        // Add make and model to Car
+        const makeRule = new AddElementRule('make');
+        makeRule.min = 1;
+        makeRule.max = '1';
+        makeRule.types = [{ type: 'code' }];
+        const modelRule = new AddElementRule('model');
+        modelRule.min = 0;
+        modelRule.max = '1';
+        modelRule.types = [{ type: 'string' }];
+        carLogical.rules.push(makeRule, modelRule);
+
+        // Set the make and model on the instance
+        const makeValueRule = new AssignmentRule('make');
+        makeValueRule.value = new FshCode('Subaru');
+        const modelValueRule = new AssignmentRule('model');
+        modelValueRule.value = 'Legacy';
+        carInstance.rules.push(makeValueRule, modelValueRule);
+
+        const exported = exportInstance(carInstance);
+        expect(exported.toJSON()).toEqual({
+          resourceType: 'http://hl7.org/fhir/us/minimal/StructureDefinition/Car',
+          make: 'Subaru',
+          model: 'Legacy'
+        });
+      });
+
+      it('should export fixed values and assignment rules for a profile of a logical model', () => {
+        // Add make and model to Car
+        const makeRule = new AddElementRule('make');
+        makeRule.min = 1;
+        makeRule.max = '1';
+        makeRule.types = [{ type: 'code' }];
+        const modelRule = new AddElementRule('model');
+        modelRule.min = 0;
+        modelRule.max = '1';
+        modelRule.types = [{ type: 'string' }];
+        carLogical.rules.push(makeRule, modelRule);
+
+        // Add fixed value to CarProfile
+        const jeepRule = new AssignmentRule('make');
+        jeepRule.value = new FshCode('Jeep');
+        carProfile.rules.push(jeepRule);
+
+        // Set the model on the instance of the profile
+        const modelValueRule = new AssignmentRule('model');
+        modelValueRule.value = 'Wrangler';
+        carProfileInstance.rules.push(modelValueRule);
+
+        const exported = exportInstance(carProfileInstance);
+        expect(exported.toJSON()).toEqual({
+          resourceType: 'http://hl7.org/fhir/us/minimal/StructureDefinition/Car',
+          make: 'Jeep',
+          model: 'Wrangler'
+        });
+      });
+    });
+
     describe('#Inline Instances', () => {
       beforeEach(() => {
         const inlineInstance = new Instance('MyInlinePatient');
@@ -9551,6 +9762,19 @@ describe('InstanceExporter', () => {
         /following instance\(s\) of custom resources:.*- FooInstance.*- BarInstance/s
       );
     });
+
+    it('should NOT log a warning when exporting an instance of a logical model', () => {
+      const logical = new Logical('FooLogical');
+      doc.logicals.set(logical.name, logical);
+      const instance = new Instance('FooInstance');
+      instance.instanceOf = 'FooLogical';
+      doc.instances.set(instance.name, instance);
+      sdExporter.export();
+      const exported = exporter.export().instances;
+      expect(exported.length).toBe(1);
+      expect(loggerSpy.getAllMessages('warn').length).toBe(0);
+      expect(loggerSpy.getAllMessages('error').length).toBe(0);
+    });
   });
 
   describe('#insertRules', () => {
diff --git a/test/ig/IGExporter.IG.test.ts b/test/ig/IGExporter.IG.test.ts
index 753c0cf..2c698a3 100644
--- a/test/ig/IGExporter.IG.test.ts
+++ b/test/ig/IGExporter.IG.test.ts
@@ -1059,26 +1059,12 @@ describe('IGExporter', () => {
         ],
         history: {} // to suppress warning for HL7 IGs
       };
-      const customResourceInstance = new InstanceDefinition();
-      customResourceInstance.resourceType = 'CustomResource';
-      customResourceInstance.id = 'FooResource';
-      customResourceInstance._instanceMeta.description = 'CustomResource Description';
-      customResourceInstance._instanceMeta.name = 'CustomResource name';
-      customResourceInstance._instanceMeta.usage = 'Example';
-
-      const patientInstance = new InstanceDefinition();
-      patientInstance.resourceType = 'Patient';
-      patientInstance.id = 'FooPatient';
-      patientInstance._instanceMeta.description = 'Sample description';
-      patientInstance._instanceMeta.name = 'Sample name';
-      patientInstance._instanceMeta.usage = 'Example';
 
       pkg = new Package(config);
       pkg.profiles.push(...pkgProfiles);
       pkg.extensions.push(...pkgExtensions);
       pkg.logicals.push(...pkgLogicals);
       pkg.resources.push(...pkgResources);
-      pkg.instances.push(customResourceInstance, patientInstance);
       exporter = new IGExporter(pkg, defs, fixtures);
     });
 
@@ -1087,6 +1073,22 @@ describe('IGExporter', () => {
     });
 
     it('should generate an implementation guide for simple-ig with package containing logical model and custom resource and also ignore custom resource and its instance(s)', () => {
+      // Add instance of custom resource and base resource
+      const customResourceInstance = new InstanceDefinition();
+      customResourceInstance.resourceType = 'CustomResource';
+      customResourceInstance.id = 'FooResource';
+      customResourceInstance._instanceMeta.description = 'CustomResource Description';
+      customResourceInstance._instanceMeta.name = 'CustomResource name';
+      customResourceInstance._instanceMeta.usage = 'Example';
+      const patientInstance = new InstanceDefinition();
+      patientInstance.resourceType = 'Patient';
+      patientInstance.id = 'FooPatient';
+      patientInstance._instanceMeta.description = 'Sample description';
+      patientInstance._instanceMeta.name = 'Sample name';
+      patientInstance._instanceMeta.usage = 'Example';
+      pkg.instances.push(customResourceInstance, patientInstance);
+
+      // Export
       exporter.export(tempOut);
       const igPath = path.join(
         tempOut,
@@ -1221,6 +1223,83 @@ describe('IGExporter', () => {
         }
       });
     });
+
+    it('should generate an implementation guide for simple-ig with package containing logical model and custom resource and logical model instance', () => {
+      // Remove custom resources
+      pkg.resources.length = 0;
+
+      // Add instance of logical model
+      const customLogicalInstance = new InstanceDefinition();
+      customLogicalInstance.resourceType =
+        'http://hl7.org/fhir/sushi-test/StructureDefinition/CustomLogicalModel';
+      customLogicalInstance.id = 'Bob';
+      customLogicalInstance.username = 'bob';
+      customLogicalInstance.userId = 1;
+      customLogicalInstance._instanceMeta.description = 'Example of CustomLogicalModel';
+      customLogicalInstance._instanceMeta.name = 'Bob';
+      customLogicalInstance._instanceMeta.usage = 'Example';
+      customLogicalInstance._instanceMeta.instanceOfUrl =
+        'http://hl7.org/fhir/sushi-test/StructureDefinition/CustomLogicalModel';
+      customLogicalInstance._instanceMeta.sdType =
+        'http://hl7.org/fhir/sushi-test/StructureDefinition/CustomLogicalModel';
+      customLogicalInstance._instanceMeta.sdKind = 'logical';
+      pkg.instances.push(customLogicalInstance);
+
+      // Export
+      exporter.export(tempOut);
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-sushi-test.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const content = fs.readJSONSync(igPath);
+      // Expectations:
+      // - resource array contains object for 'StructureDefinition/CustomLogicalModel'
+      // - resource array contains object for 'Binary/Bob' with appropriate extension
+      expect(content.definition.resource).toEqual([
+        {
+          reference: { reference: 'Binary/Bob' },
+          name: 'Bob',
+          description: 'Example of CustomLogicalModel',
+          exampleCanonical: 'http://hl7.org/fhir/sushi-test/StructureDefinition/CustomLogicalModel',
+          extension: [
+            {
+              url: 'http://hl7.org/fhir/StructureDefinition/implementationguide-resource-format',
+              valueCode: 'application/fhir+json'
+            }
+          ]
+        },
+        {
+          reference: {
+            reference: 'StructureDefinition/CustomLogicalModel'
+          },
+          name: 'Custom Logical Model Defined with FSH',
+          description:
+            'This is an example of a custom logical model defined using FSH with parent of Element',
+          exampleBoolean: false
+        },
+        {
+          reference: {
+            reference: 'StructureDefinition/sample-complex-extension'
+          },
+          name: 'SampleComplexExtension',
+          description:
+            'Base StructureDefinition for Extension Type: Optional Extension Element - found in all resources.',
+          exampleBoolean: false
+        },
+        {
+          reference: {
+            reference: 'StructureDefinition/sample-observation'
+          },
+          name: 'SampleObservation',
+          description:
+            'Measurements and simple assertions made about a patient, device or other subject.',
+          exampleBoolean: false
+        }
+      ]);
+    });
   });
 
   describe('#simple-ig-meta-profile/package', () => {
@@ -2739,7 +2818,7 @@ describe('IGExporter', () => {
       temp.cleanupSync();
     });
 
-    it('should add logical model and example resource references to the ImplementationGuide resource', () => {
+    it('should add logical model and example resource references (manually listed in config) to the ImplementationGuide resource', () => {
       exporter.export(tempOut);
       const igPath = path.join(
         tempOut,
diff --git a/test/testhelpers/testdefs/r4-definitions/package/StructureDefinition-id.json b/test/testhelpers/testdefs/r4-definitions/package/StructureDefinition-id.json
new file mode 100644
index 0000000..ccd85c9
--- /dev/null
+++ b/test/testhelpers/testdefs/r4-definitions/package/StructureDefinition-id.json
@@ -0,0 +1,1 @@
+{"resourceType":"StructureDefinition","id":"id","meta":{"lastUpdated":"2019-11-01T09:29:23.356+11:00"},"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status","valueCode":"normative"},{"url":"http://hl7.org/fhir/StructureDefinition/structuredefinition-normative-version","valueCode":"4.0.0"}],"url":"http://hl7.org/fhir/StructureDefinition/id","version":"4.0.1","name":"id","status":"active","date":"2019-11-01T09:29:23+11:00","publisher":"HL7 FHIR Standard","contact":[{"telecom":[{"system":"url","value":"http://hl7.org/fhir"}]}],"description":"Base StructureDefinition for id type: Any combination of letters, numerals, \"-\" and \".\", with a length limit of 64 characters.  (This might be an integer, an unprefixed OID, UUID or any other identifier pattern that meets these constraints.)  Ids are case-insensitive.","fhirVersion":"4.0.1","kind":"primitive-type","abstract":false,"type":"id","baseDefinition":"http://hl7.org/fhir/StructureDefinition/string","derivation":"specialization","snapshot":{"element":[{"id":"id","path":"id","short":"Primitive Type id","definition":"Any combination of letters, numerals, \"-\" and \".\", with a length limit of 64 characters.  (This might be an integer, an unprefixed OID, UUID or any other identifier pattern that meets these constraints.)  Ids are case-insensitive.","comment":"RFC 4122","min":0,"max":"*","base":{"path":"id","min":0,"max":"*"},"constraint":[{"key":"ele-1","severity":"error","human":"All FHIR elements must have a @value or children","expression":"hasValue() or (children().count() > id.count())","xpath":"@value|f:*|h:div","source":"http://hl7.org/fhir/StructureDefinition/Element"}],"isModifier":false,"isSummary":false},{"id":"id.id","path":"id.id","representation":["xmlAttr"],"short":"xml:id (or equivalent in JSON)","definition":"unique id for the element within a resource (for internal references)","min":0,"max":"1","base":{"path":"Element.id","min":0,"max":"1"},"type":[{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type","valueUrl":"string"}],"code":"http://hl7.org/fhirpath/System.String"}],"isModifier":false,"isSummary":false},{"id":"id.extension","path":"id.extension","short":"Additional content defined by implementations","definition":"May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance  applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.","comment":"There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions.  The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.","alias":["extensions","user content"],"min":0,"max":"*","base":{"path":"Element.extension","min":0,"max":"*"},"type":[{"code":"Extension"}],"constraint":[{"key":"ele-1","severity":"error","human":"All FHIR elements must have a @value or children","expression":"hasValue() or (children().count() > id.count())","xpath":"@value|f:*|h:div","source":"http://hl7.org/fhir/StructureDefinition/Element"},{"key":"ext-1","severity":"error","human":"Must have either extensions or value[x], not both","expression":"extension.exists() != value.exists()","xpath":"exists(f:extension)!=exists(f:*[starts-with(local-name(.), 'value')])","source":"http://hl7.org/fhir/StructureDefinition/Extension"}],"isModifier":false,"isSummary":false},{"id":"id.value","path":"id.value","representation":["xmlAttr"],"short":"Primitive value for id","definition":"Primitive value for id","min":0,"max":"1","base":{"path":"string.value","min":0,"max":"1"},"type":[{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type","valueUrl":"string"},{"url":"http://hl7.org/fhir/StructureDefinition/regex","valueString":"[A-Za-z0-9\\-\\.]{1,64}"}],"code":"http://hl7.org/fhirpath/System.String"}],"isModifier":false,"isSummary":false}]},"differential":{"element":[{"id":"id","path":"id","short":"Primitive Type id","definition":"Any combination of letters, numerals, \"-\" and \".\", with a length limit of 64 characters.  (This might be an integer, an unprefixed OID, UUID or any other identifier pattern that meets these constraints.)  Ids are case-insensitive.","comment":"RFC 4122","min":0,"max":"*"},{"id":"id.value","path":"id.value","representation":["xmlAttr"],"short":"Primitive value for id","definition":"Primitive value for id","min":0,"max":"1","type":[{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type","valueUrl":"id"},{"url":"http://hl7.org/fhir/StructureDefinition/regex","valueString":"[A-Za-z0-9\\-\\.]{1,64}"}],"code":"http://hl7.org/fhirpath/System.String"}]}]}}
\ No newline at end of file
diff --git a/test/utils/Processing.test.ts b/test/utils/Processing.test.ts
index 14ef31b..c134145 100644
--- a/test/utils/Processing.test.ts
+++ b/test/utils/Processing.test.ts
@@ -1610,6 +1610,12 @@ describe('Processing', () => {
       const myOtherInstance = new InstanceDefinition();
       myOtherInstance.id = 'my-other-instance';
       myOtherInstance.resourceType = 'Observation';
+      const myInstanceOfLogical = new InstanceDefinition();
+      myInstanceOfLogical.id = 'my-instance-of-logical';
+      myInstanceOfLogical.resourceType = 'http://example.com/StructureDefinition/some-logical';
+      myInstanceOfLogical._instanceMeta.sdType =
+        'http://example.com/StructureDefinition/some-logical';
+      myInstanceOfLogical._instanceMeta.sdKind = 'logical';
 
       const myPredefinedProfile = new StructureDefinition();
       myPredefinedProfile.id = 'my-duplicate-profile';
@@ -1646,6 +1652,7 @@ describe('Processing', () => {
         myExtensionInstance,
         myProfileInstance,
         myOtherInstance,
+        myInstanceOfLogical,
         myFSHDefinedInstance
       );
     });
@@ -1667,7 +1674,7 @@ describe('Processing', () => {
         const generatedPath = path.join(tempIGPubRoot, 'fsh-generated', 'resources');
         expect(fs.existsSync(generatedPath)).toBeTruthy();
         const allGeneratedFiles = fs.readdirSync(generatedPath);
-        expect(allGeneratedFiles.length).toBe(14);
+        expect(allGeneratedFiles.length).toBe(15);
         expect(allGeneratedFiles).toContain('StructureDefinition-my-profile.json');
         expect(allGeneratedFiles).toContain('StructureDefinition-my-profile-instance.json');
         expect(allGeneratedFiles).toContain('StructureDefinition-my-extension.json');
@@ -1682,7 +1689,9 @@ describe('Processing', () => {
         expect(allGeneratedFiles).toContain('StructureDefinition-my-model.json');
         expect(allGeneratedFiles).toContain('OperationDefinition-my-operation.json');
         expect(allGeneratedFiles).toContain('Observation-my-other-instance.json');
-        expect(loggerSpy.getLastMessage('info')).toMatch(/Exported 14 FHIR resources/s);
+        // NOTE: Instances of logicals are always output as Binary-{id} regardless of type
+        expect(allGeneratedFiles).toContain('Binary-my-instance-of-logical.json');
+        expect(loggerSpy.getLastMessage('info')).toMatch(/Exported 15 FHIR resources/s);
       });
 
       it('should not allow devious characters in the resource file names', () => {
@@ -1705,7 +1714,7 @@ describe('Processing', () => {
         const angelicPath = path.join(tempDeviousIGPubRoot, 'fsh-generated', 'resources');
         expect(fs.existsSync(angelicPath)).toBeTruthy();
         const allAngelicFiles = fs.readdirSync(angelicPath);
-        expect(allAngelicFiles.length).toBe(16);
+        expect(allAngelicFiles.length).toBe(17);
         expect(allAngelicFiles).toContain(
           'CapabilityStatement--..-..-devious-my-capabilities.json'
         );
@@ -1713,6 +1722,8 @@ describe('Processing', () => {
         expect(allAngelicFiles).toContain('ConceptMap--..-..-devious-my-concept-map.json');
         expect(allAngelicFiles).toContain('Observation--..-..-devious-my-example.json');
         expect(allAngelicFiles).toContain('Observation--..-..-devious-my-other-instance.json');
+        // NOTE: Instances of logicals are always output as Binary-{id} regardless of type
+        expect(allAngelicFiles).toContain('Binary--..-..-devious-my-instance-of-logical.json');
         expect(allAngelicFiles).toContain('OperationDefinition--..-..-devious-my-operation.json');
         expect(allAngelicFiles).toContain('Patient--..-..-devious-my-duplicate-instance.json');
         expect(allAngelicFiles).toContain(
@@ -1730,7 +1741,7 @@ describe('Processing', () => {
         expect(allAngelicFiles).toContain('StructureDefinition--..-..-devious-my-profile.json');
         expect(allAngelicFiles).toContain('StructureDefinition--..-..-devious-my-resource.json');
         expect(allAngelicFiles).toContain('ValueSet--..-..-devious-my-value-set.json');
-        expect(loggerSpy.getLastMessage('info')).toMatch(/Exported 16 FHIR resources/s);
+        expect(loggerSpy.getLastMessage('info')).toMatch(/Exported 17 FHIR resources/s);
       });
 
       it('should not write a resource if that resource already exists in the "input" folder', () => {