baldrick-broth
Version:
Build automation tool and task runner
204 lines (180 loc) • 5.72 kB
text/typescript
/**
* Responsibilities: Expands batch models into runnable command line inputs.
* - Resolves loops (each), templated names, and command lines per iteration
* - Validates expanded names and merges per-iteration extra context
*/
import type { AnyCommand, BatchStepModel, Ctx } from './build-model.js';
import { createDataId, getSupportedProperty } from './data-value-utils.js';
import type { CommandLineInput } from './execution.js';
import { stringy } from './field-validation.js';
import { IdGenerator, rootId } from './id-generator.js';
import { type Result, succeed, willFail } from './railway.js';
import { dasherizeTitle } from './string-utils.js';
import {
getCommandLines,
getExpandedName,
mergeTemplateContext,
} from './templating.js';
type ExpandedCommandLineInputs = Result<
CommandLineInput[],
{ messages: string[] }
>;
const getArray = (memoryId: string, ctx: Ctx, name: string): unknown[] => {
const value = getSupportedProperty(memoryId, ctx, name);
if (value === undefined) {
return [];
}
if (Array.isArray(value)) {
return value;
}
return [];
};
type CommandLocalVars = {
commandOpts: AnyCommand;
extra: Record<string, unknown>;
memoryId: string;
};
const expandCommand =
(ctx: Ctx, _batch: BatchStepModel) =>
(current: CommandLocalVars): ExpandedCommandLineInputs => {
const { commandOpts, extra, memoryId } = current;
if (commandOpts.a === 'shell') {
const { run, title, name, multiline } = commandOpts;
const preferredName = name === undefined ? dasherizeTitle(title) : name;
const templateCtx = mergeTemplateContext({
memoryId,
ctx,
command: commandOpts,
extra,
});
const lines = multiline ? getCommandLines(run, templateCtx) : [run];
const expandedName = getExpandedName(preferredName, templateCtx);
const validatedName = stringy.customKey.safeParse(expandedName);
if (!validatedName.success) {
return willFail({
messages: [`Expanded name was not supported: ${expandedName}`],
});
}
const lineInputs: CommandLineInput[] = lines.map((line) => ({
memoryId,
line,
name: expandedName,
opts: commandOpts,
extra,
}));
return succeed(lineInputs);
}
const { name } = commandOpts;
return succeed([{ line: '', name, opts: commandOpts, memoryId, extra }]);
};
const mergeExpandedCommandLineInputs = (
inputsArray: ExpandedCommandLineInputs[],
): ExpandedCommandLineInputs => {
const inputsWithErrors = inputsArray.filter((i) => i.status === 'failure');
if (inputsWithErrors.length > 0) {
const messages = inputsWithErrors.flatMap((i) =>
i.status === 'failure' ? i.error.messages : [],
);
return willFail({ messages });
}
const inputs = inputsArray.flatMap((i) =>
i.status === 'success' ? i.value : [],
);
return succeed(inputs);
};
const expandBatch0 = (
ctx: Ctx,
batch: BatchStepModel,
extra: Record<string, unknown>,
): ExpandedCommandLineInputs => {
const expanded = batch.commands
.map((commandOpts) => ({ commandOpts, extra, memoryId: rootId }))
.map(expandCommand(ctx, batch));
return mergeExpandedCommandLineInputs(expanded);
};
const expandBatch1 = (
ctx: Ctx,
batch: BatchStepModel,
): ExpandedCommandLineInputs => {
const loop0 = batch.each === undefined ? undefined : batch.each[0];
if (loop0 === undefined) {
return willFail({ messages: ['Should have at least one loop'] });
}
const makeId = IdGenerator();
const arr0 = getArray(rootId, ctx, loop0.values).map((value) => ({
name: loop0.name,
value,
}));
const commandLocalVars: CommandLocalVars[] = arr0.flatMap((extra) =>
batch.commands.map((commandOpts) => ({
commandOpts,
extra: {
[createDataId(rootId, extra.name)]: extra.value,
},
memoryId: makeId(),
})),
);
const expanded = commandLocalVars.flatMap(expandCommand(ctx, batch));
return mergeExpandedCommandLineInputs(expanded);
};
const expandBatch2 = (
ctx: Ctx,
batchStep: BatchStepModel,
): ExpandedCommandLineInputs => {
if (batchStep.each === undefined) {
return willFail({
messages: ['batch.each should not be undefined'],
});
}
const [loop0, loop1] = batchStep.each;
if (loop0 === undefined || loop1 === undefined) {
return willFail({
messages: ['The two first items of each should be defined'],
});
}
const makeId = IdGenerator();
const arr0 = getArray(rootId, ctx, loop0.values).map((value) => ({
name: loop0.name,
value,
}));
const arr1 = getArray(rootId, ctx, loop1.values).map((value) => ({
name: loop1.name,
value,
}));
const commandLocalVars: CommandLocalVars[] = arr0.flatMap((extra0) =>
arr1
.map((extra1) => ({ ...extra0, ...extra1 }))
.flatMap((extra) =>
batchStep.commands.map((commandOpts) => ({
commandOpts,
extra: {
[createDataId(rootId, extra.name)]: extra.value,
},
memoryId: makeId(),
})),
),
);
const expanded = commandLocalVars.flatMap(expandCommand(ctx, batchStep));
return mergeExpandedCommandLineInputs(expanded);
};
export const expandBatchStep = (
ctx: Ctx,
batchStep: BatchStepModel,
): ExpandedCommandLineInputs => {
const numberOfLoops =
batchStep.each === undefined ? 0 : batchStep.each.length;
switch (numberOfLoops) {
case 0: {
return expandBatch0(ctx, batchStep, {});
}
case 1: {
return expandBatch1(ctx, batchStep);
}
case 2: {
return expandBatch2(ctx, batchStep);
}
default: {
throw new Error('Too many for each loops');
}
}
};