Skip to content

Commit

Permalink
Support extensions on IG.definition in sushi-config.yaml (#1452)
Browse files Browse the repository at this point in the history
* Allow IG.definition extensions in config file

* Provide specific recommendation for unrecognized properties on definition
  • Loading branch information
jafeltra authored Apr 9, 2024
1 parent 6006ab6 commit eabcfee
Show file tree
Hide file tree
Showing 12 changed files with 152 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/fhirtypes/ImplementationGuide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type ImplementationGuideDefinition = {
page?: ImplementationGuideDefinitionPage;
parameter?: ImplementationGuideDefinitionParameter[];
template?: ImplementationGuideDefinitionTemplate[];
extension?: Extension[];
};

export type ImplementationGuideDefinitionGrouping = {
Expand Down
7 changes: 7 additions & 0 deletions src/fshtypes/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down
1 change: 1 addition & 0 deletions src/ig/IGExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down
9 changes: 9 additions & 0 deletions src/import/YAMLConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -331,6 +336,10 @@ export type YAMLConfigurationGlobalMap = {
| ImplementationGuideGlobal['profile'][]; // string[]
};

export type YAMLConfigurationDefinition = {
extension?: Extension[];
};

export type YAMLConfigurationGroupMap = {
[key: string]: {
name?: ImplementationGuide['name'];
Expand Down
12 changes: 12 additions & 0 deletions src/import/YAMLschema.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,18 @@
]
}
},
"definition": {
"type": "object",
"properties": {
"extension": {
"type": "array",
"items": {
"$ref": "#/definitions/extension"
}
}
},
"additionalProperties": false
},
"groups": {
"type": "object",
"additionalProperties": {
Expand Down
16 changes: 16 additions & 0 deletions src/import/importConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
YAMLConfigurationMenuTree,
YAMLConfigurationDependencyMap,
YAMLConfigurationGlobalMap,
YAMLConfigurationDefinition,
YAMLConfigurationGroupMap,
YAMLConfigurationResourceMap,
YAMLConfigurationPageTree,
Expand All @@ -20,6 +21,7 @@ import {
import YAML_SCHEMA from './YAMLschema.json';
import {
Configuration,
ConfigurationDefinition,
ConfigurationGroup,
ConfigurationResource,
ConfigurationMenuItem,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ${
Expand Down
10 changes: 10 additions & 0 deletions test/ig/IGExporter.IG.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ describe('IGExporter', () => {
]
}
],
definition: {
extension: [
{
url: 'http://example.org/example/ig-definition-ext',
valueBoolean: true
}
]
},
resources: [
{
reference: { reference: 'Patient/patient-example' },
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -1185,6 +1194,7 @@ describe('IGExporter', () => {
}
],
definition: {
// NOTE: No definition.extension added in config, so not included here
resource: [
{
reference: {
Expand Down
4 changes: 4 additions & 0 deletions test/ig/IGExporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Expand Down Expand Up @@ -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');
Expand Down
8 changes: 8 additions & 0 deletions test/import/YAMLConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
9 changes: 9 additions & 0 deletions test/import/fixtures/example-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions test/import/fixtures/maximal-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 66 additions & 1 deletion test/import/importConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -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' },
Expand Down Expand Up @@ -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',
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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 = {
Expand Down

0 comments on commit eabcfee

Please sign in to comment.