diff --git a/src/fhirtypes/ImplementationGuide.ts b/src/fhirtypes/ImplementationGuide.ts index beb596ed1..2749c2034 100644 --- a/src/fhirtypes/ImplementationGuide.ts +++ b/src/fhirtypes/ImplementationGuide.ts @@ -55,6 +55,7 @@ export type ImplementationGuideDefinition = { page?: ImplementationGuideDefinitionPage; parameter?: ImplementationGuideDefinitionParameter[]; template?: ImplementationGuideDefinitionTemplate[]; + extension?: Extension[]; }; export type ImplementationGuideDefinitionGrouping = { diff --git a/src/fshtypes/Configuration.ts b/src/fshtypes/Configuration.ts index e163fc3c0..8416721ab 100644 --- a/src/fshtypes/Configuration.ts +++ b/src/fshtypes/Configuration.ts @@ -80,6 +80,8 @@ export type Configuration = { // the correct URL when generating the IG JSON. groups?: ConfigurationGroup[]; + definition?: ConfigurationDefinition; + // The resources property corresponds to IG.definition.resource. SUSHI can auto-generate all of // the resource entries based on the FSH definitions and/or information in any user-provided // JSON resource files. If the generated entries are not sufficient or complete, however, the @@ -153,6 +155,11 @@ export type ConfigurationGroup = { resources?: string[]; }; +export type ConfigurationDefinition = { + // NOTE: all other IG.definition properties have a top-level configuration property + extension?: Extension[]; +}; + export type ConfigurationResource = ImplementationGuideDefinitionResource & { omit?: boolean }; export type ConfigurationMenuItem = { diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts index a2fe0011f..debc912ab 100644 --- a/src/ig/IGExporter.ts +++ b/src/ig/IGExporter.ts @@ -171,6 +171,7 @@ export class IGExporter { dependsOn: [], global: this.config.global, definition: { + extension: this.config.definition?.extension, // put an empty grouping here to preserve the location of this property (delete later if not needed) grouping: [], resource: [], diff --git a/src/import/YAMLConfiguration.ts b/src/import/YAMLConfiguration.ts index 73c3e5b94..6f51ef0b1 100644 --- a/src/import/YAMLConfiguration.ts +++ b/src/import/YAMLConfiguration.ts @@ -99,6 +99,11 @@ export type YAMLConfiguration = { // generating the IG JSON. global?: YAMLConfigurationGlobalMap; + // NOTE: All of the properties of IG.definition are abstracted to their own top-level configuration + // property. This definition property should only be used to provide extensions that have a context + // of IG.definition. + definition?: YAMLConfigurationDefinition; + // Groups can control certain aspects of the IG generation. The IG documentation recommends that // authors use the default groups that are provided by the templating framework, but if authors // want to use their own instead, they can use the mechanism below. This will create @@ -331,6 +336,10 @@ export type YAMLConfigurationGlobalMap = { | ImplementationGuideGlobal['profile'][]; // string[] }; +export type YAMLConfigurationDefinition = { + extension?: Extension[]; +}; + export type YAMLConfigurationGroupMap = { [key: string]: { name?: ImplementationGuide['name']; diff --git a/src/import/YAMLschema.json b/src/import/YAMLschema.json index 882545a1f..e48e470ad 100644 --- a/src/import/YAMLschema.json +++ b/src/import/YAMLschema.json @@ -178,6 +178,18 @@ ] } }, + "definition": { + "type": "object", + "properties": { + "extension": { + "type": "array", + "items": { + "$ref": "#/definitions/extension" + } + } + }, + "additionalProperties": false + }, "groups": { "type": "object", "additionalProperties": { diff --git a/src/import/importConfiguration.ts b/src/import/importConfiguration.ts index 937e892f6..f1a3d9e20 100644 --- a/src/import/importConfiguration.ts +++ b/src/import/importConfiguration.ts @@ -6,6 +6,7 @@ import { YAMLConfigurationMenuTree, YAMLConfigurationDependencyMap, YAMLConfigurationGlobalMap, + YAMLConfigurationDefinition, YAMLConfigurationGroupMap, YAMLConfigurationResourceMap, YAMLConfigurationPageTree, @@ -20,6 +21,7 @@ import { import YAML_SCHEMA from './YAMLschema.json'; import { Configuration, + ConfigurationDefinition, ConfigurationGroup, ConfigurationResource, ConfigurationMenuItem, @@ -164,6 +166,7 @@ export function importConfiguration(yaml: YAMLConfiguration | string, file: stri dependencies: parseDependencies(yaml.dependencies), global: parseGlobal(yaml.global), groups: parseGroups(yaml.groups), + definition: parseDefinition(yaml.definition), resources: parseResources(yaml.resources, file), pages: parsePages(yaml.pages, file), parameters: parseParameters(yaml, yaml.FSHOnly, file), @@ -624,6 +627,15 @@ function parseGroups(yamlGroups: YAMLConfigurationGroupMap): ConfigurationGroup[ }); } +function parseDefinition(yamlDefinition: YAMLConfigurationDefinition): ConfigurationDefinition { + // NOTE: extension is the only property allowed to be set directly on definition in config file + if (yamlDefinition == null || yamlDefinition.extension == null) { + return; + } + + return { extension: yamlDefinition.extension }; +} + function parseResources( yamlResources: YAMLConfigurationResourceMap, file: string @@ -930,6 +942,10 @@ function detectPotentialMistakes(yaml: YAMLConfiguration) { recommendation += ` If ${ singular ? 'this is a page, it' : 'these are pages, they' } should end with .md, .xml, or .html.`; + } else if (instancePath.startsWith('/definition')) { + recommendation = + 'Only the extension property is allowed under definition. All other definition ' + + 'properties are represented at the top-level of the configuration.'; } logger.warn( `Configuration ${parentProperty}contains unexpected ${ diff --git a/test/ig/IGExporter.IG.test.ts b/test/ig/IGExporter.IG.test.ts index 952477466..0f3965bb6 100644 --- a/test/ig/IGExporter.IG.test.ts +++ b/test/ig/IGExporter.IG.test.ts @@ -128,6 +128,14 @@ describe('IGExporter', () => { ] } ], + definition: { + extension: [ + { + url: 'http://example.org/example/ig-definition-ext', + valueBoolean: true + } + ] + }, resources: [ { reference: { reference: 'Patient/patient-example' }, @@ -262,6 +270,7 @@ describe('IGExporter', () => { } ], definition: { + extension: [{ url: 'http://example.org/example/ig-definition-ext', valueBoolean: true }], resource: [ // resources are ordered by name (case-insensitive) { @@ -1185,6 +1194,7 @@ describe('IGExporter', () => { } ], definition: { + // NOTE: No definition.extension added in config, so not included here resource: [ { reference: { diff --git a/test/ig/IGExporter.test.ts b/test/ig/IGExporter.test.ts index 65d4ccc61..c4bd5884b 100644 --- a/test/ig/IGExporter.test.ts +++ b/test/ig/IGExporter.test.ts @@ -58,6 +58,7 @@ describe('IGExporter', () => { const igContent = fs.readJSONSync(igPath); expect(igContent.id).toBe('fhir.us.minimal'); + expect(igContent.definition.extension).toBeUndefined(); expect(igContent.definition.resource).toHaveLength(0); }); }); @@ -90,6 +91,9 @@ describe('IGExporter', () => { 'ImplementationGuide-fhir.us.example.json' ); const igContent = fs.readJSONSync(igPath); + expect(igContent.definition.extension).toEqual([ + { url: 'http://example.org/example/ig-definition-ext', valueBoolean: true } + ]); expect(igContent.definition.grouping).toHaveLength(2); expect(igContent.definition.grouping[0].id).toBe('GroupA'); expect(igContent.definition.grouping[0].name).toBe('Group A'); diff --git a/test/import/YAMLConfiguration.test.ts b/test/import/YAMLConfiguration.test.ts index d438655c1..bede472e0 100644 --- a/test/import/YAMLConfiguration.test.ts +++ b/test/import/YAMLConfiguration.test.ts @@ -67,6 +67,14 @@ describe('YAMLConfiguration', () => { Patient: 'http://example.org/fhir/StructureDefinition/my-patient-profile', Encounter: 'http://example.org/fhir/StructureDefinition/my-encounter-profile' }); + expect(config.definition).toEqual({ + extension: [ + { + url: 'http://example.org/example/ig-definition-ext', + valueBoolean: true + } + ] + }); expect(config.resources).toEqual({ 'Patient/my-example-patient': { name: 'My Example Patient', diff --git a/test/import/fixtures/example-config.yaml b/test/import/fixtures/example-config.yaml index 1b6412668..56d25579e 100644 --- a/test/import/fixtures/example-config.yaml +++ b/test/import/fixtures/example-config.yaml @@ -86,6 +86,15 @@ global: Patient: http://example.org/fhir/StructureDefinition/my-patient-profile Encounter: http://example.org/fhir/StructureDefinition/my-encounter-profile +# NOTE: All of the properties of IG.definition are abstracted to +# individual top-level configuration properties (below). This +# definition property should only be used to provide extensions +# that have a context of IG.definition. +definition: + extension: + - url: http://example.org/example/ig-definition-ext + valueBoolean: true + # The resources property corresponds to IG.definition.resource. # SUSHI can auto-generate all of the resource entries based on # the FSH definitions and/or information in any user-provided diff --git a/test/import/fixtures/maximal-config.yaml b/test/import/fixtures/maximal-config.yaml index a5158546c..237141092 100644 --- a/test/import/fixtures/maximal-config.yaml +++ b/test/import/fixtures/maximal-config.yaml @@ -84,6 +84,15 @@ global: Patient: http://example.org/fhir/StructureDefinition/my-patient-profile Encounter: http://example.org/fhir/StructureDefinition/my-encounter-profile +# NOTE: All of the properties of IG.definition are abstracted to +# individual top-level configuration properties (below). This +# definition property should only be used to provide extensions +# that have a context of IG.definition. +definition: + extension: + - url: http://example.org/example/ig-definition-ext + valueBoolean: true + # The resources property corresponds to IG.definition.resource. # SUSHI can auto-generate all of the resource entries based on # the FSH definitions and/or information in any user-provided diff --git a/test/import/importConfiguration.test.ts b/test/import/importConfiguration.test.ts index 68b3a7324..d7d61f868 100644 --- a/test/import/importConfiguration.test.ts +++ b/test/import/importConfiguration.test.ts @@ -115,6 +115,14 @@ describe('importConfiguration', () => { profile: 'http://example.org/fhir/StructureDefinition/my-encounter-profile' } ], + definition: { + extension: [ + { + url: 'http://example.org/example/ig-definition-ext', + valueBoolean: true + } + ] + }, resources: [ { reference: { reference: 'Patient/my-example-patient' }, @@ -294,6 +302,14 @@ describe('importConfiguration', () => { profile: 'http://example.org/fhir/StructureDefinition/my-encounter-profile' } ], + definition: { + extension: [ + { + url: 'http://example.org/example/ig-definition-ext', + valueBoolean: true + } + ] + }, resources: [ { reference: { reference: 'Patient/my-example-patient' }, @@ -488,6 +504,10 @@ describe('importConfiguration', () => { minYAML['index.md'] = { title: 'IG Home' }; + minYAML.definition = { + // @ts-ignore + resource: [{ reference: { reference: 'Patient/my-example-patient' } }] + }; const actual = importConfiguration(minYAML, 'test-config.yaml'); const expected: Configuration = { filePath: 'test-config.yaml', @@ -510,9 +530,13 @@ describe('importConfiguration', () => { }; expect(actual).toEqual(expected); expect(loggerSpy.getAllLogs('error')).toHaveLength(0); - expect(loggerSpy.getLastMessage('warn')).toMatch( + expect(loggerSpy.getAllLogs('warn')).toHaveLength(2); + expect(loggerSpy.getMessageAtIndex(0, 'warn')).toMatch( 'Configuration contains unexpected properties: cookie, index.md. Check that these properties are spelled, capitalized, and indented correctly.' ); + expect(loggerSpy.getMessageAtIndex(1, 'warn')).toMatch( + 'Configuration property definition contains unexpected property: resource. Only the extension property is allowed under definition. All other definition properties are represented at the top-level of the configuration.' + ); }); it('should report an error and throw on an invalid YAML config', () => { @@ -1846,6 +1870,47 @@ describe('importConfiguration', () => { }); }); + describe('#definition', () => { + it('should convert the definition extensions to a list', () => { + minYAML.definition = { + extension: [{ url: 'http://example.org/example/ig-definition-ext', valueBoolean: true }] + }; + const config = importConfiguration(minYAML, 'test-config.yaml'); + expect(config.definition).toEqual({ + extension: [ + { + url: 'http://example.org/example/ig-definition-ext', + valueBoolean: true + } + ] + }); + }); + it('should not convert any extra definition properties', () => { + minYAML.definition = { + extension: [{ url: 'http://example.org/example/ig-definition-ext', valueBoolean: true }], + // @ts-ignore + reference: { reference: 'Patient/my-example-patient' } + }; + const config = importConfiguration(minYAML, 'test-config.yaml'); + expect(config.definition).toEqual({ + extension: [ + { + url: 'http://example.org/example/ig-definition-ext', + valueBoolean: true + } + ] + }); + }); + it('should not include a definition property if no extensions are provided', () => { + minYAML.definition = { + // @ts-ignore + reference: { reference: 'Patient/my-example-patient' } + }; + const config = importConfiguration(minYAML, 'test-config.yaml'); + expect(config.definition).toBeUndefined(); + }); + }); + describe('#groups', () => { it('should convert the groups map to a list', () => { minYAML.groups = {