Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/export/InstanceExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
determineKnownSlices,
setImpliedPropertiesOnInstance,
getMatchingContainedReferenceId,
checkForMultipleChoice
checkForMultipleChoice,
isAdditionalResource
} from '../fhirtypes/common';
import { InstanceOfNotDefinedError } from '../errors/InstanceOfNotDefinedError';
import { AbstractInstanceOfError } from '../errors/AbstractInstanceOfError';
Expand Down Expand Up @@ -908,6 +909,15 @@ export class InstanceExporter implements Fishable {
}
if (isResource) {
instanceDef.resourceType = instanceOfStructureDefinition.type; // ResourceType is determined by the StructureDefinition of the type
// The JSON format of an additional resource names the versioned definition of its type.
// See: https://hl7.org/fhir/6.0.0-ballot5/json.html#additional
if (isAdditionalResource(ancestor)) {
// Definitions from this project have no version yet; the IG Publisher assigns the IG version.
const version =
ancestor.version ??
(this.pkg.fish(ancestor.url, Type.Resource) != null ? this.tank.config.version : null);
instanceDef.resourceDefinition = version ? `${ancestor.url}|${version}` : ancestor.url;
}
if (this.shouldSetId(instanceDef, instanceOfStructureDefinition)) {
instanceDef.id = fshDefinition.id;
}
Expand Down
13 changes: 10 additions & 3 deletions src/fhirtypes/InstanceDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { InstanceUsage } from '../fshtypes/InstanceUsage';
export class InstanceDefinition {
_instanceMeta: InstanceMeta = {};
resourceType: string;
resourceDefinition?: string; // only for instances of additional resources
// id?: FHIRId; // provided by HasId mixin
meta?: Meta;
[key: string]: any; // Allow any key value pair on InstanceDefinition due to the high number of potential properties that can be set on a FHIR instance
Expand All @@ -32,9 +33,15 @@ export class InstanceDefinition {
}

toJSON(): any {
const orderedKeys = ['resourceType', '_resourceType', 'id', '_id', 'meta', '_meta'].filter(
key => this[key] != null
);
const orderedKeys = [
'resourceType',
'_resourceType',
'resourceDefinition',
'id',
'_id',
'meta',
'_meta'
].filter(key => this[key] != null);
// _instanceMeta is only needed for lookup and IG config - not a FHIR property
const additionalKeys = difference(Object.keys(this), [...orderedKeys, '_instanceMeta']);
return orderedCloneDeep(this, [...orderedKeys, ...additionalKeys]);
Expand Down
16 changes: 16 additions & 0 deletions src/fhirtypes/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,22 @@ export function isModifierExtension(extension: any): boolean {
);
}

/**
* Determines if a StructureDefinition defines an additional resource, i.e. a resource type that
* is defined outside of the core specification.
* @param {any} structDef - the StructureDefinition JSON of the resource type
* @returns {boolean} true if the StructureDefinition is marked as an additional resource
*/
export function isAdditionalResource(structDef: any): boolean {
return (
structDef?.extension?.some(
(ext: any) =>
ext?.url === 'http://hl7.org/fhir/tools/StructureDefinition/additional-resource' &&
ext.valueBoolean === true
) ?? false
);
}

/**
* Compares two canonical URLs for equality, ignoring a |version on either side. The values are
* compared exactly as-presented first, so identical strings always match; only if that fails is the
Expand Down
165 changes: 165 additions & 0 deletions test/export/InstanceExporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11094,6 +11094,171 @@ describe('InstanceExporter', () => {
});
});

describe('#Additional Resources', () => {
// Resource: AdditionalRecord
// * ^extension[0].url = "http://hl7.org/fhir/tools/StructureDefinition/additional-resource"
// * ^extension[0].valueBoolean = true
// * title 0..1 string "Title"
let additionalRecord: Resource;
let additionalMarker: CaretValueRule;
let recordInstance: Instance;

beforeEach(() => {
additionalRecord = new Resource('AdditionalRecord');
const markerUrl = new CaretValueRule('');
markerUrl.caretPath = 'extension[0].url';
markerUrl.value = 'http://hl7.org/fhir/tools/StructureDefinition/additional-resource';
additionalMarker = new CaretValueRule('');
additionalMarker.caretPath = 'extension[0].valueBoolean';
additionalMarker.value = true;
const titleElement = new AddElementRule('title');
titleElement.min = 0;
titleElement.max = '1';
titleElement.types = [{ type: 'string' }];
titleElement.short = 'Title';
additionalRecord.rules.push(markerUrl, additionalMarker, titleElement);
doc.resources.set(additionalRecord.name, additionalRecord);

// Instance: RecordInstance
// InstanceOf: AdditionalRecord
// * title = "A record"
recordInstance = new Instance('RecordInstance');
recordInstance.instanceOf = 'AdditionalRecord';
const titleRule = new AssignmentRule('title');
titleRule.value = 'A record';
recordInstance.rules.push(titleRule);
doc.instances.set(recordInstance.name, recordInstance);
});

it('should set resourceDefinition to the versioned url of an additional resource', () => {
const exported = exportInstance(recordInstance);
expect(exported.toJSON()).toEqual({
resourceType: 'AdditionalRecord',
resourceDefinition:
'http://hl7.org/fhir/us/minimal/StructureDefinition/AdditionalRecord|1.0.0',
id: 'RecordInstance',
title: 'A record'
});
// resourceDefinition directly follows resourceType, as in the FHIR JSON format
expect(Object.keys(exported.toJSON())).toEqual([
'resourceType',
'resourceDefinition',
'id',
'title'
]);
expect(loggerSpy.getAllLogs('error')).toHaveLength(0);
});

it('should set resourceDefinition to the additional resource for an instance of its profile', () => {
// Profile: RecordProfile
// Parent: AdditionalRecord
// * ^version = "2.0.0"
// * title 1..1
const recordProfile = new Profile('RecordProfile');
recordProfile.parent = 'AdditionalRecord';
const profileVersion = new CaretValueRule('');
profileVersion.caretPath = 'version';
profileVersion.value = '2.0.0';
const titleCard = new CardRule('title');
titleCard.min = 1;
titleCard.max = '1';
recordProfile.rules.push(profileVersion, titleCard);
doc.profiles.set(recordProfile.name, recordProfile);
recordInstance.instanceOf = 'RecordProfile';

const exported = exportInstance(recordInstance);
expect(exported.resourceType).toBe('AdditionalRecord');
expect(exported.resourceDefinition).toBe(
'http://hl7.org/fhir/us/minimal/StructureDefinition/AdditionalRecord|1.0.0'
);
expect(exported.meta.profile).toEqual([
'http://hl7.org/fhir/us/minimal/StructureDefinition/RecordProfile'
]);
expect(loggerSpy.getAllLogs('error')).toHaveLength(0);
});

it('should set resourceDefinition to the version that the additional resource declares', () => {
// * ^version = "0.9.0"
const recordVersion = new CaretValueRule('');
recordVersion.caretPath = 'version';
recordVersion.value = '0.9.0';
additionalRecord.rules.push(recordVersion);

const exported = exportInstance(recordInstance);
expect(exported.resourceDefinition).toBe(
'http://hl7.org/fhir/us/minimal/StructureDefinition/AdditionalRecord|0.9.0'
);
});

it('should set resourceDefinition to the url alone when no version is known', () => {
const unversionedConfig = cloneDeep(minimalConfig);
delete unversionedConfig.version;
tank = new FSHTank([doc], unversionedConfig);
pkg = new Package(tank.config);
fisher = new TestFisher(tank, defs, pkg);
sdExporter = new StructureDefinitionExporter(tank, pkg, fisher);
exporter = new InstanceExporter(tank, pkg, fisher);
sdExporter.export();

const exported = exporter.exportInstance(recordInstance);
expect(exported.resourceDefinition).toBe(
'http://hl7.org/fhir/us/minimal/StructureDefinition/AdditionalRecord'
);
});

it('should set resourceDefinition on an inline instance of an additional resource', () => {
// Instance: InlineRecord
// InstanceOf: AdditionalRecord
// Usage: #inline
recordInstance.usage = 'Inline';
// Instance: RecordBundle
// InstanceOf: Bundle
// * type = #collection
// * entry[0].resource = RecordInstance
const bundle = new Instance('RecordBundle');
bundle.instanceOf = 'Bundle';
const typeRule = new AssignmentRule('type');
typeRule.value = new FshCode('collection');
const entryRule = new AssignmentRule('entry[0].resource');
entryRule.value = 'RecordInstance';
entryRule.isInstance = true;
bundle.rules.push(typeRule, entryRule);
doc.instances.set(bundle.name, bundle);

const exported = exportInstance(bundle);
const entryResource = JSON.parse(JSON.stringify(exported.toJSON())).entry[0].resource;
// whether the inline instance gets an id depends on instanceOptions.setId, which this test does not set
expect(entryResource).toMatchObject({
resourceType: 'AdditionalRecord',
resourceDefinition:
'http://hl7.org/fhir/us/minimal/StructureDefinition/AdditionalRecord|1.0.0',
title: 'A record'
});
expect(Object.keys(entryResource).slice(0, 2)).toEqual([
'resourceType',
'resourceDefinition'
]);
expect(loggerSpy.getAllLogs('error')).toHaveLength(0);
});

it('should not set resourceDefinition when the additional-resource extension is false', () => {
additionalMarker.value = false;
const exported = exportInstance(recordInstance);
expect(exported.resourceType).toBe('AdditionalRecord');
expect(exported.resourceDefinition).toBeUndefined();
});

it('should not set resourceDefinition for a resource defined in the core specification', () => {
const patient = new Instance('PlainPatient');
patient.instanceOf = 'Patient';
doc.instances.set(patient.name, patient);
const exported = exportInstance(patient);
expect(exported.resourceType).toBe('Patient');
expect(exported.resourceDefinition).toBeUndefined();
expect(Object.keys(exported.toJSON())).toEqual(['resourceType', 'id']);
});
});

describe('#Inline Instances', () => {
beforeEach(() => {
const inlinePatient = new Instance('MyInlinePatient');
Expand Down
Loading