diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts index f2a9bb6f6..5c61aae41 100644 --- a/src/export/InstanceExporter.ts +++ b/src/export/InstanceExporter.ts @@ -746,7 +746,13 @@ export class InstanceExporter implements Fishable { } exportInstance(fshDefinition: Instance): InstanceDefinition { - if (this.pkg.instances.some(i => i._instanceMeta.name === fshDefinition.name)) { + if ( + this.pkg.instances.some( + i => + i._instanceMeta.name === fshDefinition.name && + i._instanceMeta.versionId === fshDefinition.versionId + ) + ) { return; } @@ -815,6 +821,9 @@ export class InstanceExporter implements Fishable { if (fshDefinition.title) { instanceDef._instanceMeta.title = fshDefinition.title; } + if (fshDefinition.versionId) { + instanceDef._instanceMeta.versionId = fshDefinition.versionId; + } if (fshDefinition.description) { instanceDef._instanceMeta.description = fshDefinition.description; } @@ -952,6 +961,7 @@ export class InstanceExporter implements Fishable { instanceDef.resourceType === instance.resourceType && (instanceDef.id ?? instanceDef._instanceMeta.name) === (instance.id ?? instance._instanceMeta.name) && + instanceDef._instanceMeta.versionId === instance._instanceMeta.versionId && instanceDef !== instance ) ) { diff --git a/src/fhirtypes/InstanceDefinition.ts b/src/fhirtypes/InstanceDefinition.ts index 70f3a1c2d..48ee97ba4 100644 --- a/src/fhirtypes/InstanceDefinition.ts +++ b/src/fhirtypes/InstanceDefinition.ts @@ -26,7 +26,9 @@ export class InstanceDefinition { getFileName(): string { // 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`, { + const versionString = this._instanceMeta.versionId ? `_v${this._instanceMeta.versionId}` : ''; + const filename = `${type}-${this.id ?? this._instanceMeta.name}${versionString}.json`; + return sanitize(filename, { replacement: '-' }); } @@ -61,6 +63,7 @@ type InstanceMeta = { sdType?: string; sdKind?: string; instanceOfUrl?: string; + versionId?: string; }; // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/fshtypes/Instance.ts b/src/fshtypes/Instance.ts index a9d5aaab9..5a6138a14 100644 --- a/src/fshtypes/Instance.ts +++ b/src/fshtypes/Instance.ts @@ -10,6 +10,7 @@ export class Instance extends FshEntity { instanceOf: string; description?: string; usage?: InstanceUsage; + versionId?: string; rules: (AssignmentRule | InsertRule | PathRule)[]; constructor(public name: string) { @@ -17,6 +18,7 @@ export class Instance extends FshEntity { this.id = name; // init same as name this.rules = []; this.usage = 'Example'; // init to Example (default) + this.versionId = undefined; // init to undefined (default) } get constructorName() { diff --git a/src/import/FSHImporter.ts b/src/import/FSHImporter.ts index 96c00d7e1..c0d904aaa 100644 --- a/src/import/FSHImporter.ts +++ b/src/import/FSHImporter.ts @@ -448,15 +448,29 @@ export class FSHImporter extends FSHVisitor { const instance = new Instance(ctx.name().getText()) .withLocation(location) .withFile(this.currentFile); - if (this.docs.some(doc => doc.instances.has(instance.name))) { - logger.error(`Skipping Instance: an Instance named ${instance.name} already exists.`, { - file: this.currentFile, - location - }); - } else { + { try { this.parseInstance(instance, location, ctx.instanceMetadata(), ctx.instanceRule()); - this.currentDoc.instances.set(instance.name, instance); + if ( + this.docs.some( + doc => + doc.instances.has(instance.name) && + Array.from(doc.instances.values()).some( + entity => entity.name === instance.name && entity.versionId === instance.versionId + ) + ) + ) { + const versionString = instance.versionId ? `with versionId ${instance.versionId} ` : ''; + logger.error( + `Skipping Instance: an Instance named ${instance.name} ${versionString}already exists.`, + { + file: this.currentFile, + location + } + ); + } else { + this.currentDoc.instances.set(instance.name, instance); + } } catch (e) { logger.error(e.message, instance.sourceInfo); if (e.stack) { @@ -518,6 +532,9 @@ export class FSHImporter extends FSHVisitor { const rule = this.visitInstanceRule(instanceRule); if (rule) { instance.rules.push(rule); + if (rule instanceof AssignmentRule && rule.path === 'meta.versionId') { + instance.versionId = rule.value.toString(); + } } }); } diff --git a/src/utils/Processing.ts b/src/utils/Processing.ts index 353dd38ab..450ed2dab 100644 --- a/src/utils/Processing.ts +++ b/src/utils/Processing.ts @@ -550,6 +550,7 @@ export function writeFHIRResources( toJSON: (snapshot: boolean) => any; url?: string; id?: string; + versionId?: string; resourceType?: string; }[] ) => { @@ -560,7 +561,8 @@ export function writeFHIRResources( predef => predef.url === resource.url && predef.resourceType === resource.resourceType && - predef.id === resource.id + predef.id === resource.id && + predef.versionId === resource.versionId ) ) { checkNullValuesOnArray(resource); diff --git a/test/import/FSHImporter.Instance.test.ts b/test/import/FSHImporter.Instance.test.ts index d830bfed0..91e0c3f3b 100644 --- a/test/import/FSHImporter.Instance.test.ts +++ b/test/import/FSHImporter.Instance.test.ts @@ -410,6 +410,46 @@ describe('FSHImporter', () => { ); expect(loggerSpy.getLastMessage('error')).toMatch(/File: File2\.fsh.*Line: 2 - 4\D*/s); }); + + it('should not produce an error when encountering an instance with a name used by another instance in another file but has a unique versionId', () => { + const input1 = ` + Instance: SomeInstance + InstanceOf: Patient + Title: "Instance" + * meta + * meta.versionId = "1" + `; + + const input2 = ` + Instance: SomeInstance + InstanceOf: Patient + Title: "Instance" + * meta + * meta.versionId = "2" + `; + + const input3 = ` + Instance: SomeInstance + InstanceOf: Patient + Title: "Instance" + `; + + const result = importText([ + new RawFSH(input1, 'File1.fsh'), + new RawFSH(input2, 'File2.fsh'), + new RawFSH(input3, 'File2.fsh') + ]); + expect(result.reduce((sum, d2) => sum + d2.instances.size, 0)).toBe(3); + const instance1 = result[0].instances.get('SomeInstance'); + const instance2 = result[1].instances.get('SomeInstance'); + const instance3 = result[2].instances.get('SomeInstance'); + expect(instance1.title).toBe('Instance'); + expect(instance1.versionId).toBe('1'); + expect(instance1.title).toBe('Instance'); + expect(instance2.versionId).toBe('2'); + expect(instance3.title).toBe('Instance'); + expect(instance3.versionId).toBe(undefined); + }); }); }); });