Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,19 @@ Options:
-p, --preprocessed output FSH produced by preprocessing steps
-r, --require-latest exit with error if this is not the latest version of SUSHI (default: false)
-s, --snapshot generate snapshot in Structure Definition output (default: false)
--incremental reuse unchanged results from the previous build to rebuild faster (or set SUSHI_INCREMENTAL=1)
-c, --config <config> override elements in sushi-config.yaml (supported: 'version', 'status', 'releaselabel') (eg: --config status:draft)
-h, --help display help for command
```

See the [SUSHI documentation](https://fshschool.org/docs/sushi/) for detailed information on using SUSHI.

### Incremental Builds

With `--incremental`, SUSHI keeps what it imported from each FSH file and reuses it in the next build when the file and everything it used from other files are unchanged. The output and messages are the same as a normal build's. This is useful for tools that run SUSHI on every edit: setting the `SUSHI_INCREMENTAL` environment variable to `1` turns it on for every build, for example when the IG Publisher runs SUSHI.

The cache is kept in a `.sushi-cache` folder in the project (it contains its own `.gitignore`), or in the folder named by the `SUSHI_CACHE_DIR` environment variable. It can be deleted at any time. Incremental builds are not used with `--preprocessed`.

## IG Generation

SUSHI supports publishing implementation guides via the new template-based IG Publisher. The template-based publisher is still being developed by the FHIR community. See the [Guidance for HL7 IG Creation](https://build.fhir.org/ig/FHIR/ig-guidance/) for more details.
Expand Down
21 changes: 20 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { exportFHIR, Package } from './export';
import { IGExporter, loadPredefinedResources } from './ig';
import { createFHIRDefinitions } from './fhirdefs';
import { Configuration } from './fshtypes';
import { IncrementalBuild } from './incremental/IncrementalBuild';
import {
logger,
stats,
Expand Down Expand Up @@ -74,6 +75,10 @@ async function app() {
false
)
.option('-s, --snapshot', 'generate snapshot in Structure Definition output', false)
.option(
'--incremental',
'reuse unchanged results from the previous build to rebuild faster (or set SUSHI_INCREMENTAL=1)'
)
.option(
'-c, --config <config>',
"override elements in sushi-config.yaml (supported: 'version', 'status', 'releaselabel') (eg: --config status:draft)",
Expand Down Expand Up @@ -190,6 +195,10 @@ async function runBuild(input: string, program: OptionValues, helpText: string)
if (program.config) {
Object.entries(program.config).forEach(([k, v]) => logger.info(` --config ${k}:${v}`));
}
const incrementalRequested = program.incremental || process.env.SUSHI_INCREMENTAL === '1';
if (incrementalRequested) {
logger.info(' --incremental');
}
logger.info(` ${path.resolve(input || '.')}`);

// The version check hits the network, so only wait on it where the result is needed
Expand Down Expand Up @@ -240,6 +249,14 @@ async function runBuild(input: string, program: OptionValues, helpText: string)

let tank: FSHTank;
let config: Configuration;
// Incremental builds cannot reproduce the preprocessed FSH output, so they are not used with it
if (incrementalRequested && program.preprocessed) {
logger.info('Incremental build: not used, as it cannot produce preprocessed FSH.');
}
const incremental =
incrementalRequested && !program.preprocessed
? new IncrementalBuild(path.resolve(originalInput), getLocalSushiVersion())
: null;

try {
let rawFSH: RawFSH[];
Expand All @@ -263,7 +280,7 @@ async function runBuild(input: string, program: OptionValues, helpText: string)
}
config = readConfig(originalInput);
updateConfig(config, program);
tank = fillTank(rawFSH, config);
tank = incremental ? incremental.importFSH(rawFSH, config) : fillTank(rawFSH, config);
tank.checkDuplicateNameEntities();
} catch (e) {
// If no errors have been logged yet, log this exception so the user knows why we're exiting
Expand Down Expand Up @@ -314,8 +331,10 @@ async function runBuild(input: string, program: OptionValues, helpText: string)

logger.info('Converting FSH to FHIR resources...');
const outPackage = exportFHIR(tank, defs);
incremental?.logSummary();
const { skippedResources } = writeFHIRResources(outDir, outPackage, defs, program.snapshot);
writeFSHIndex(outDir, outPackage, input, skippedResources);
incremental?.finish();

if (program.preprocessed) {
logger.info('Writing preprocessed FSH...');
Expand Down
184 changes: 109 additions & 75 deletions src/export/Package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { StructureDefinition, InstanceDefinition, ValueSet, CodeSystem } from '.
import { Configuration, SourceInfo } from '../fshtypes';
import { Fishable, Type, Metadata } from '../utils/Fishable';

type PackageDefinition = StructureDefinition | ValueSet | CodeSystem | InstanceDefinition;
type FishCandidates = [PackageDefinition[], (def: PackageDefinition) => boolean][];

export class Package implements Fishable {
public readonly profiles: StructureDefinition[] = [];
public readonly extensions: StructureDefinition[] = [];
Expand Down Expand Up @@ -41,7 +44,28 @@ export class Package implements Fishable {
stopOnFirstMatch: boolean
): (StructureDefinition | ValueSet | CodeSystem | InstanceDefinition)[] {
const results: (StructureDefinition | ValueSet | CodeSystem | InstanceDefinition)[] = [];
for (const candidates of this.getCandidates(item, types)) {
let def: PackageDefinition;
for (const [list, matches] of candidates) {
def = list.find(matches);
if (def) {
break;
}
}
if (def) {
if (stopOnFirstMatch) {
return [def];
}
results.push(def);
}
}
return results;
}

/**
* For each type to fish for, the lists to search (in order) and how to recognize a match in each
*/
private getCandidates(item: string, types: Type[]): FishCandidates[] {
// No types passed in means to search ALL supported types
if (types.length === 0) {
types = [
Expand All @@ -59,132 +83,142 @@ export class Package implements Fishable {
const [base, ...versionParts] = item?.split('|') ?? ['', ''];
const version = versionParts.join('|') || null;

for (const type of types) {
let def;
return types.map(type => {
let candidates: FishCandidates;
switch (type) {
case Type.Profile:
def = this.profiles.find(
p =>
(p.id === base || p.name === base || p.url === base) &&
(version == null || p.version === version)
);
if (!def) {
def = this.instances.find(
i =>
candidates = [
[
this.profiles,
(p: StructureDefinition) =>
(p.id === base || p.name === base || p.url === base) &&
(version == null || p.version === version)
],
[
this.instances,
(i: InstanceDefinition) =>
i._instanceMeta.usage === 'Definition' &&
i.resourceType === 'StructureDefinition' &&
i.derivation === 'constraint' &&
i.type !== 'Extension' &&
(i.id === base || i._instanceMeta.name === base || i.url === base) &&
(version == null || i.version === version)
);
}
]
];
break;
case Type.Extension:
def = this.extensions.find(
e =>
(e.id === base || e.name === base || e.url === base) &&
(version == null || e.version === version)
);
if (!def) {
def = this.instances.find(
i =>
candidates = [
[
this.extensions,
(e: StructureDefinition) =>
(e.id === base || e.name === base || e.url === base) &&
(version == null || e.version === version)
],
[
this.instances,
(i: InstanceDefinition) =>
i._instanceMeta.usage === 'Definition' &&
i.resourceType === 'StructureDefinition' &&
i.derivation === 'constraint' &&
i.type === 'Extension' &&
(i.id === base || i._instanceMeta.name === base || i.url === base) &&
(version == null || i.version === version)
);
}
]
];
break;
case Type.Logical:
def = this.logicals.find(
l =>
(l.id === base || l.name === base || l.url === base) &&
(version == null || l.version === version)
);
if (!def) {
def = this.instances.find(
i =>
candidates = [
[
this.logicals,
(l: StructureDefinition) =>
(l.id === base || l.name === base || l.url === base) &&
(version == null || l.version === version)
],
[
this.instances,
(i: InstanceDefinition) =>
i._instanceMeta.usage === 'Definition' &&
i.resourceType === 'StructureDefinition' &&
i.derivation === 'specialization' &&
i.kind === 'logical' &&
(i.id === base || i._instanceMeta.name === base || i.url === base) &&
(version == null || i.version === version)
);
}
]
];
break;
case Type.Resource:
def = this.resources.find(
r =>
(r.id === base || r.name === base || r.url === base) &&
(version == null || r.version === version)
);
if (!def) {
def = this.instances.find(
i =>
candidates = [
[
this.resources,
(r: StructureDefinition) =>
(r.id === base || r.name === base || r.url === base) &&
(version == null || r.version === version)
],
[
this.instances,
(i: InstanceDefinition) =>
i._instanceMeta.usage === 'Definition' &&
i.resourceType === 'StructureDefinition' &&
i.derivation === 'specialization' &&
i.kind === 'resource' &&
(i.id === base || i._instanceMeta.name === base || i.url === base) &&
(version == null || i.version === version)
);
}
]
];
break;
case Type.ValueSet:
def = this.valueSets.find(
vs =>
(vs.id === base || vs.name === base || vs.url === base) &&
(version == null || vs.version === version)
);
if (!def) {
def = this.instances.find(
i =>
candidates = [
[
this.valueSets,
(vs: ValueSet) =>
(vs.id === base || vs.name === base || vs.url === base) &&
(version == null || vs.version === version)
],
[
this.instances,
(i: InstanceDefinition) =>
i._instanceMeta.usage === 'Definition' &&
i.resourceType === 'ValueSet' &&
(i.id === base || i._instanceMeta.name === base || i.url === base) &&
(version == null || i.version === version)
);
}
]
];
break;
case Type.CodeSystem:
def = this.codeSystems.find(
cs =>
(cs.id === base || cs.name === base || cs.url === base) &&
(version == null || cs.version === version)
);
if (!def) {
def = this.instances.find(
i =>
candidates = [
[
this.codeSystems,
(cs: CodeSystem) =>
(cs.id === base || cs.name === base || cs.url === base) &&
(version == null || cs.version === version)
],
[
this.instances,
(i: InstanceDefinition) =>
i._instanceMeta.usage === 'Definition' &&
i.resourceType === 'CodeSystem' &&
(i.id === base || i._instanceMeta.name === base || i.url === base) &&
(version == null || i.version === version)
);
}
]
];
break;
case Type.Instance:
def = this.instances.find(
i =>
(i.id === base || i._instanceMeta.name === base) &&
(version == null || i.version === version)
);
candidates = [
[
this.instances,
(i: InstanceDefinition) =>
(i.id === base || i._instanceMeta.name === base) &&
(version == null || i.version === version)
]
];
break;
case Type.Type: // Package doesn't currently support types
default:
candidates = [];
break;
}
if (def) {
if (stopOnFirstMatch) {
return [def];
}
results.push(def);
}
}
return results;
return candidates;
});
}

fishForFHIR(item: string, ...types: Type[]): any | undefined {
Expand Down
Loading
Loading