Skip to content

Commit 6811a96

Browse files
authored
Give Extensions Package Highest Priority (#1605)
The current version of SUSHI loads all automatic dependencies first, which gives them lowest priority in resolution. Based on recent discussion, however, the automatically loaded extensions package should have highest priority. These changes update the automatic dependency mechanism to support the notion of automatic dependency priority. Automatic dependencies with low priority (e.g., hl7.fhir.uv.tools, hl7.terminology) are loaded first so they have lowest priority for resolution. Automatic dependencies with high priority (e.g., hl7.fhir.uv.extensions) are loaded last so they have highest priority for resolution. See: https://chat.fhir.org/#narrow/channel/179239-tooling/topic/New.20Implicit.20Package/near/562477807 Fixes #1602
1 parent f579da9 commit 6811a96

File tree

2 files changed

+306
-137
lines changed

2 files changed

+306
-137
lines changed

src/utils/Processing.ts

Lines changed: 107 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import YAML from 'yaml';
66
import semver from 'semver';
77
import { execSync } from 'child_process';
88
import { YAMLMap, Collection } from 'yaml/types';
9-
import { isPlainObject, padEnd, startCase, sortBy, upperFirst } from 'lodash';
9+
import { isPlainObject, padEnd, startCase, sortBy, upperFirst, isEqual, uniqWith } from 'lodash';
1010
import { EOL } from 'os';
1111
import table from 'text-table';
1212
import { OptionValues } from 'commander';
@@ -34,10 +34,16 @@ const EXT_PKG_TO_FHIR_PKG_MAP: { [key: string]: string } = {
3434
'hl7.fhir.extensions.r5': 'hl7.fhir.r5.core#5.0.0'
3535
};
3636

37+
export enum AutomaticDependencyPriority {
38+
Low = 'Low', // load before configured dependencies / FHIR core (lowest resolution priority)
39+
High = 'High' // load after configured dependencies / FHIR core (highest resolution priority)
40+
}
41+
3742
type AutomaticDependency = {
3843
packageId: string;
3944
version: string;
4045
fhirVersions?: FHIRVersionName[];
46+
priority: AutomaticDependencyPriority;
4147
};
4248

4349
type FshFhirMapping = {
@@ -54,35 +60,54 @@ export const AUTOMATIC_DEPENDENCIES: AutomaticDependency[] = [
5460
{
5561
packageId: 'hl7.fhir.uv.tools.r4',
5662
version: 'latest',
57-
fhirVersions: ['R4', 'R4B']
63+
fhirVersions: ['R4', 'R4B'],
64+
priority: AutomaticDependencyPriority.Low
5865
},
5966
{
6067
packageId: 'hl7.fhir.uv.tools.r5',
6168
version: 'latest',
62-
fhirVersions: ['R5', 'R6']
69+
fhirVersions: ['R5', 'R6'],
70+
priority: AutomaticDependencyPriority.Low
6371
},
6472
{
6573
packageId: 'hl7.terminology.r4',
6674
version: 'latest',
67-
fhirVersions: ['R4', 'R4B']
75+
fhirVersions: ['R4', 'R4B'],
76+
priority: AutomaticDependencyPriority.Low
6877
},
6978
{
7079
packageId: 'hl7.terminology.r5',
7180
version: 'latest',
72-
fhirVersions: ['R5', 'R6']
81+
fhirVersions: ['R5', 'R6'],
82+
priority: AutomaticDependencyPriority.Low
7383
},
7484
{
7585
packageId: 'hl7.fhir.uv.extensions.r4',
7686
version: 'latest',
77-
fhirVersions: ['R4', 'R4B']
87+
fhirVersions: ['R4', 'R4B'],
88+
priority: AutomaticDependencyPriority.High
7889
},
7990
{
8091
packageId: 'hl7.fhir.uv.extensions.r5',
8192
version: 'latest',
82-
fhirVersions: ['R5', 'R6']
93+
fhirVersions: ['R5', 'R6'],
94+
priority: AutomaticDependencyPriority.High
8395
}
8496
];
8597

98+
function configuredDependencyMatchesAutomaticDependency(
99+
cd: ImplementationGuideDependsOn,
100+
ad: AutomaticDependency
101+
) {
102+
// hl7.some.package, hl7.some.package.r4, and hl7.some.package.r5 all represent the same content,
103+
// so they are essentially interchangeable and we should allow for any of them in the config.
104+
// See: https://chat.fhir.org/#narrow/stream/179239-tooling/topic/New.20Implicit.20Package/near/325488084
105+
const [configRootId, packageRootId] = [cd.packageId, ad.packageId].map(id =>
106+
/\.r[4-9]$/.test(id) ? id.slice(0, -3) : id
107+
);
108+
return configRootId === packageRootId;
109+
}
110+
86111
export function isSupportedFHIRVersion(version: string): boolean {
87112
// For now, allow current or any 4.x/5.x/6.x version of FHIR except 4.0.0. This is a quick check; not a guarantee. If a user passes
88113
// in an invalid version that passes this test (e.g., 4.99.0), it is still expected to fail when we load dependencies.
@@ -366,23 +391,42 @@ export async function loadExternalDependencies(
366391
}
367392
dependencies.push({ packageId: fhirVersionInfo.packageId, version: fhirVersionInfo.version });
368393

369-
// Load automatic dependencies first so they have lowest priority in resolution
370-
await loadAutomaticDependencies(fhirVersionInfo.version, dependencies, defs);
394+
// First load automatic dependencies with the lowest priority (before configured dependencies and FHIR core)
395+
await loadAutomaticDependencies(
396+
fhirVersionInfo.version,
397+
dependencies,
398+
defs,
399+
AutomaticDependencyPriority.Low
400+
);
371401

372-
// Then load configured dependencies, with FHIR core last so it has highest priority in resolution
402+
// Then load configured dependencies and FHIR core (FHIR core is last so it has higher priority in resolution)
373403
await loadConfiguredDependencies(dependencies, fhirVersionInfo.version, config.filePath, defs);
404+
405+
// Then load automatic dependencies with highest priority (taking precedence over even FHIR core)
406+
// See: https://chat.fhir.org/#narrow/channel/179239-tooling/topic/New.20Implicit.20Package/near/562477575
407+
await loadAutomaticDependencies(
408+
fhirVersionInfo.version,
409+
dependencies,
410+
defs,
411+
AutomaticDependencyPriority.High
412+
);
374413
}
375414

376415
export async function loadAutomaticDependencies(
377416
fhirVersion: string,
378417
configuredDependencies: ImplementationGuideDependsOn[],
379-
defs: FHIRDefinitions
418+
defs: FHIRDefinitions,
419+
priority: AutomaticDependencyPriority
380420
): Promise<void> {
381421
const fhirVersionName = getFHIRVersionInfo(fhirVersion).name;
382422

383-
if (fhirVersionName === 'R4' || fhirVersionName === 'R4B') {
423+
if (
424+
priority === AutomaticDependencyPriority.Low &&
425+
(fhirVersionName === 'R4' || fhirVersionName === 'R4B')
426+
) {
384427
// There are several R5 resources that are allowed for use in R4 and R4B.
385-
// Add them first so they're always available.
428+
// Add them first so they're always available (but are lower priority than
429+
// any other version loaded from an official package).
386430
const R5forR4Map = new Map<string, any>();
387431
R5_DEFINITIONS_NEEDED_IN_R4.forEach(def => R5forR4Map.set(def.id, def));
388432
const virtualR5forR4Package = new InMemoryVirtualPackage(
@@ -397,46 +441,57 @@ export async function loadAutomaticDependencies(
397441
await defs.loadVirtualPackage(virtualR5forR4Package);
398442
}
399443

400-
// Load dependencies serially so dependency loading order is predictable and repeatable
401-
for (const dep of AUTOMATIC_DEPENDENCIES) {
402-
// Skip dependencies not intended for this version of FHIR
403-
if (dep.fhirVersions && !dep.fhirVersions.includes(fhirVersionName)) {
404-
continue;
405-
}
406-
const alreadyConfigured = configuredDependencies.some(cd => {
407-
// hl7.some.package, hl7.some.package.r4, and hl7.some.package.r5 all represent the same content,
408-
// so they are essentially interchangeable and we should allow for any of them in the config.
409-
// See: https://chat.fhir.org/#narrow/stream/179239-tooling/topic/New.20Implicit.20Package/near/325488084
410-
const [configRootId, packageRootId] = [cd.packageId, dep.packageId].map(id =>
411-
/\.r[4-9]$/.test(id) ? id.slice(0, -3) : id
412-
);
413-
return configRootId === packageRootId;
414-
});
415-
if (!alreadyConfigured) {
416-
let status: string;
417-
try {
418-
// Suppress error logs when loading automatic dependencies because many IGs can succeed without them
444+
// Gather all automatic dependencies matching this priority, substituting matching configured dependencies where applicable
445+
const automaticDependencies = uniqWith(
446+
AUTOMATIC_DEPENDENCIES.filter(ad => ad.priority === priority)
447+
.map(autoDep => {
448+
const configuredDeps = configuredDependencies.filter(configuredDep =>
449+
configuredDependencyMatchesAutomaticDependency(configuredDep, autoDep)
450+
);
451+
if (configuredDeps.length) {
452+
// Prefer configured dependencies over automatic dependencies
453+
return configuredDeps;
454+
} else if (autoDep.fhirVersions && !autoDep.fhirVersions.includes(fhirVersionName)) {
455+
// Skip automatic dependencies not intended for this version of FHIR
456+
return [];
457+
}
458+
return autoDep;
459+
})
460+
.flat(),
461+
isEqual
462+
);
463+
// Load automatic dependencies serially so dependency loading order is predictable and repeatable
464+
for (const dep of automaticDependencies) {
465+
const isUserConfigured = !AUTOMATIC_DEPENDENCIES.some(
466+
autoDep => autoDep.packageId === dep.packageId && autoDep.version === dep.version
467+
);
468+
let status: string;
469+
try {
470+
// Suppress error logs when loading non-configured automatic dependencies because many IGs can succeed without them
471+
if (!isUserConfigured) {
419472
defs.setFHIRPackageLoaderLogInterceptor((level: string) => {
420473
return level !== 'error';
421474
});
422-
status = await defs.loadPackage(dep.packageId, dep.version);
423-
} catch (e) {
424-
// This shouldn't happen, but just in case
425-
status = 'FAILED';
426-
if (e.stack) {
427-
logger.debug(e.stack);
428-
}
429-
} finally {
430-
// Unset the log interceptor so it behaves normally after this
475+
}
476+
status = await defs.loadPackage(dep.packageId, dep.version);
477+
} catch (e) {
478+
// This shouldn't happen, but just in case
479+
status = 'FAILED';
480+
if (e.stack) {
481+
logger.debug(e.stack);
482+
}
483+
} finally {
484+
// Unset the log interceptor for non-configured automatic dependencies so it behaves normally after this
485+
if (!isUserConfigured) {
431486
defs.setFHIRPackageLoaderLogInterceptor();
432487
}
433-
if (status !== 'LOADED') {
434-
let message = `Failed to load automatically-provided ${dep.packageId}#${dep.version}`;
435-
if (process.env.FPL_REGISTRY) {
436-
message += ` from custom FHIR package registry ${process.env.FPL_REGISTRY}.`;
437-
}
438-
logger.warn(message);
488+
}
489+
if (status !== 'LOADED' && !isUserConfigured) {
490+
let message = `Failed to load automatically-provided ${dep.packageId}#${dep.version}`;
491+
if (process.env.FPL_REGISTRY) {
492+
message += ` from custom FHIR package registry ${process.env.FPL_REGISTRY}.`;
439493
}
494+
logger.warn(message);
440495
}
441496
}
442497
}
@@ -477,6 +532,11 @@ async function loadConfiguredDependencies(
477532
`Loading supplemental version of FHIR to support extensions from ${dep.packageId}`
478533
);
479534
await defs.loadSupplementalFHIRPackage(EXT_PKG_TO_FHIR_PKG_MAP[dep.packageId]);
535+
} else if (
536+
AUTOMATIC_DEPENDENCIES.some(ad => configuredDependencyMatchesAutomaticDependency(dep, ad))
537+
) {
538+
// skip configured dependencies that override automatic dependencies; they will be loaded at the end
539+
continue;
480540
} else {
481541
await defs.loadPackage(dep.packageId, dep.version).catch(e => {
482542
logger.error(`Failed to load ${dep.packageId}#${dep.version}: ${e.message}`);

0 commit comments

Comments
 (0)