From 148d0be0fd15211307ca4f2f699a128873d52145 Mon Sep 17 00:00:00 2001 From: Julia Afeltra <30803904+jafeltra@users.noreply.github.com> Date: Mon, 26 Feb 2024 11:02:21 -0500 Subject: [PATCH] Allow path-resource paths with a wildcard (#1427) * Allow path-resource paths with wildcard to add deeply nested files * Use what Node 16 knows * Fix up tests * Use what Node 18 knows * Fix path separator and simplify logic - Simplify logic when looking for one level and recurive directories - Update comment to reflect new logic - Use consitent / path separator in the path-resource parameter * Allow parameters list to be empty in config --- .github/workflows/ci-workflow.yml | 2 +- src/ig/IGExporter.ts | 25 +++++--- src/import/importConfiguration.ts | 2 +- test/ig/IGExporter.IG.test.ts | 61 ++++++++++++++++++- .../jack/examples/Patient-Jack.json | 13 ++++ .../john/Patient-John.json | 13 ++++ 6 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 test/ig/fixtures/customized-ig-with-nested-resources/input/resources/path-resource-double-nest/jack/examples/Patient-Jack.json create mode 100644 test/ig/fixtures/customized-ig-with-nested-resources/input/resources/path-resource-double-nest/john/Patient-John.json diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index a7dfac79c..0ffeae8ec 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [16, 18] + node-version: [18, 20] steps: - uses: actions/checkout@v1 diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts index c25bf419c..a2fe0011f 100644 --- a/src/ig/IGExporter.ts +++ b/src/ig/IGExporter.ts @@ -942,8 +942,8 @@ export class IGExporter { * capabilities, extensions, models, operations, profiles, resources, vocabulary, examples * Based on: https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#root.input * - * NOTE: This does not include files nested in subfolders in supported paths since the - * IG Exporter does not handle those well. + * NOTE: This only includes files nested in subfolders when specified in the path-resource + * parameter, which is based on how the IG Publisher works. * * This function has similar operation to addResources, and both should be * analyzed when making changes to either. @@ -963,14 +963,23 @@ export class IGExporter { const predefinedResourcePaths = pathEnds.map(pathEnd => path.join(this.inputPath, 'input', pathEnd) ); - let pathResourceDirectories: string[]; + const pathResourceDirectories: string[] = []; const pathResources = this.config.parameters ?.filter(parameter => parameter.value && parameter.code === 'path-resource') .map(parameter => parameter.value); if (pathResources) { - pathResourceDirectories = pathResources - .map(directoryPath => path.join(this.inputPath, directoryPath)) - .filter(directoryPath => existsSync(directoryPath)); + pathResources.forEach(directoryPath => { + const fullPath = path.join(this.inputPath, ...directoryPath.split('/')); + if (existsSync(fullPath)) { + pathResourceDirectories.push(fullPath); + } else if (directoryPath.endsWith('/*') && existsSync(fullPath.slice(0, -2))) { + pathResourceDirectories.push( + ...readdirSync(fullPath.slice(0, -2), { withFileTypes: true, recursive: true }) + .filter(file => file.isDirectory()) + .map(dir => path.join(dir.path, dir.name)) + ); + } + }); if (pathResourceDirectories) predefinedResourcePaths.push(...pathResourceDirectories); } const deeplyNestedFiles: string[] = []; @@ -987,7 +996,9 @@ export class IGExporter { path.dirname(file) !== dirPath && !pathResourceDirectories?.includes(path.dirname(file)) ) { - deeplyNestedFiles.push(file); + if (!deeplyNestedFiles.includes(file)) { + deeplyNestedFiles.push(file); + } continue; } const resourceJSON: InstanceDefinition = this.fhirDefs.getPredefinedResource(file); diff --git a/src/import/importConfiguration.ts b/src/import/importConfiguration.ts index d7a084346..937e892f6 100644 --- a/src/import/importConfiguration.ts +++ b/src/import/importConfiguration.ts @@ -748,7 +748,7 @@ function parseParameters( } if (yamlConfig.parameters) { for (const [code, values] of Object.entries(yamlConfig.parameters)) { - normalizeToArray(values).forEach(value => parameters.push({ code, value: `${value}` })); + normalizeToArray(values)?.forEach(value => parameters.push({ code, value: `${value}` })); } } else if (parameters.length === 0) { return; // return undefined rather than an empty [] diff --git a/test/ig/IGExporter.IG.test.ts b/test/ig/IGExporter.IG.test.ts index 7a7572edd..952477466 100644 --- a/test/ig/IGExporter.IG.test.ts +++ b/test/ig/IGExporter.IG.test.ts @@ -2921,7 +2921,7 @@ describe('IGExporter', () => { config.parameters = []; config.parameters.push({ code: 'path-resource', - value: path.join('input', 'resources', 'path-resource-nest') + value: 'input/resources/path-resource-nest' }); const pkg = new Package(config); exporter = new IGExporter(pkg, defs, path.resolve(fixtures)); @@ -2973,6 +2973,12 @@ describe('IGExporter', () => { ); expect(warning).toInclude(path.join('nested1', 'StructureDefinition-MyTitlePatient.json')); expect(warning).toInclude(path.join('nested2', 'ValueSet-MyVS.json')); + expect(warning).toInclude( + path.join('path-resource-double-nest', 'john', 'Patient-John.json') + ); + expect(warning).toInclude( + path.join('path-resource-double-nest', 'jack', 'examples', 'Patient-Jack.json') + ); expect(warning).not.toInclude('Patient-BarPatient.json'); expect(warning).not.toInclude('StructureDefinition-MyPatient.json'); }); @@ -2989,10 +2995,63 @@ describe('IGExporter', () => { ); expect(warning).toInclude(path.join('nested1', 'StructureDefinition-MyTitlePatient.json')); expect(warning).toInclude(path.join('nested2', 'ValueSet-MyVS.json')); + // path-resource-double-nest is not included in config + expect(warning).toInclude( + path.join('path-resource-double-nest', 'john', 'Patient-John.json') + ); + expect(warning).toInclude( + path.join('path-resource-double-nest', 'jack', 'examples', 'Patient-Jack.json') + ); + // path-resource-nest is included in config expect(warning).not.toInclude( path.join('path-resource-nest', 'StructureDefinition-MyCorrectlyNestedPatient.json') ); }); + + it('should not warn on deeply nested resources that are included in the path-resource parameter with a directory and wildcard', () => { + const config = cloneDeep(minimalConfig); + config.parameters = []; + config.parameters.push({ + code: 'path-resource', + value: 'input/resources/path-resource-double-nest/*' + }); + const pkg = new Package(config); + exporter = new IGExporter(pkg, defs, path.resolve(fixtures)); + exporter.export(tempOut); + expect(loggerSpy.getAllMessages('warn')).toHaveLength(1); + const warning = loggerSpy.getFirstMessage('warn'); + expect(warning).not.toInclude( + path.join('path-resource-double-nest', 'john', 'Patient-John.json') + ); + expect(warning).not.toInclude( + path.join('path-resource-double-nest', 'jack', 'examples', 'Patient-Jack.json') + ); + }); + + it('should warn on deeply nested resources that are included in the path-resource parameter with a directory but NO wildcard', () => { + const config = cloneDeep(minimalConfig); + config.parameters = []; + config.parameters.push({ + code: 'path-resource', + // NOTE: file path does not include the "*" portion (it just lists a directory), which is not sufficient + value: 'input/resources/path-resource-double-nest' + }); + const pkg = new Package(config); + exporter = new IGExporter(pkg, defs, path.resolve(fixtures)); + exporter.export(tempOut); + expect(loggerSpy.getAllMessages('warn')).toHaveLength(1); + const warning = loggerSpy.getFirstMessage('warn'); + const warningLines = warning.split('\n'); + const johnLine = warningLines.filter(w => + w.includes(path.join('path-resource-double-nest', 'john', 'Patient-John.json')) + ); + const jackLine = warningLines.filter(w => + w.includes(path.join('path-resource-double-nest', 'john', 'Patient-John.json')) + ); + // Check that both nested files are logged in the warning, but check that they're only there once + expect(johnLine).toHaveLength(1); + expect(jackLine).toHaveLength(1); + }); }); describe('#customized-ig-with-logical-model-example', () => { diff --git a/test/ig/fixtures/customized-ig-with-nested-resources/input/resources/path-resource-double-nest/jack/examples/Patient-Jack.json b/test/ig/fixtures/customized-ig-with-nested-resources/input/resources/path-resource-double-nest/jack/examples/Patient-Jack.json new file mode 100644 index 000000000..366338c52 --- /dev/null +++ b/test/ig/fixtures/customized-ig-with-nested-resources/input/resources/path-resource-double-nest/jack/examples/Patient-Jack.json @@ -0,0 +1,13 @@ +{ + "resourceType": "Patient", + "id": "Jack", + "name": [ + { + "family": "Anyperson", + "given": [ + "Jack", + "C." + ] + } + ] +} \ No newline at end of file diff --git a/test/ig/fixtures/customized-ig-with-nested-resources/input/resources/path-resource-double-nest/john/Patient-John.json b/test/ig/fixtures/customized-ig-with-nested-resources/input/resources/path-resource-double-nest/john/Patient-John.json new file mode 100644 index 000000000..5b6ceb2cc --- /dev/null +++ b/test/ig/fixtures/customized-ig-with-nested-resources/input/resources/path-resource-double-nest/john/Patient-John.json @@ -0,0 +1,13 @@ +{ + "resourceType": "Patient", + "id": "John", + "name": [ + { + "family": "Anyperson", + "given": [ + "John", + "B." + ] + } + ] +} \ No newline at end of file