UNPKG

baldrick-broth

Version:
617 lines (552 loc) 16.9 kB
import { z } from 'zod'; import { stringy } from './field-validation.js'; import { formatMessage, type ValidationError } from './format-message.js'; import { type Result, succeed, willFail } from './railway.js'; const describeEnum = (intro: string, objectValue: { [k: string]: string }) => { const description = [`${intro} with either:`]; for (const [name, title] of Object.entries(objectValue)) { description.push(`${name}: ${title}`); } return description.join('\n'); }; const asEnumKeys = (value: {}) => Object.keys(value) as [string, ...string[]]; /** JSON like */ const literalSchema = z.union([z.string().min(1), z.number(), z.boolean()]); type Literal = z.infer<typeof literalSchema>; type Json = Literal | { [key: string]: Json } | Json[]; const jsonishSchema: z.ZodType<Json> = z .lazy(() => z.union([literalSchema, z.array(jsonishSchema), z.record(jsonishSchema)]) ) .describe('Any JSON document without null values'); const engine = z .object({ telemetry: z .object({ name: z.string().min(1).max(40).optional(), verbosity: z.string().max(100), filepath: stringy.path, }) .describe('Preferences for telemetry'), }) .optional() .describe('Settings for the baldrick-broth engine'); const onShellCommandFinishEnum = { exit: 'Exit the shell after the command has finished.', silent: 'Suppress any output from the command.', trim: 'Remove any leading or trailing whitespace from the output.', json: 'Format the output as a JSON object.', yaml: 'Format the output as a YAML object.', csv: 'Format the output as a CSV file.', save: 'Save the output to a file.', debug: 'Print debugging information about the command execution.', }; const onShellCommandFinish = z .enum(asEnumKeys(onShellCommandFinishEnum)) .describe( describeEnum( 'Options for when the shell command finish', onShellCommandFinishEnum ) ); const linkPage = z .object({ title: stringy.title, url: stringy.url, }) .describe('Info about a link'); const links = z.array(linkPage).optional().describe('A list of useful links'); const metadataStep = { name: stringy.customKey.describe( 'A short name that could be used a key or variable for the step' ), title: stringy.title, description: stringy.description.optional(), motivation: stringy.motivation.optional(), links, }; const metadataCore = { name: stringy.customKey .describe('A short name that could be used a key or variable') .optional(), title: stringy.title, description: stringy.description.optional(), motivation: stringy.motivation.optional(), links, }; const lineShell = z.string().min(1).max(300).describe('A line of shell script'); const advancedShell = z .object({ ...metadataCore, a: z.literal('shell').default('shell'), onFailure: z .array(onShellCommandFinish) .min(1) .default(['exit']) .describe( 'List of flags to describe the default behavior in case of failure' ), onSuccess: z .array(onShellCommandFinish) .min(1) .default(['trim']) .describe( 'List of flags to describe the default behavior in case of success' ), if: stringy.varValue .optional() .describe( 'reference to JSON path that must be satisfied for the step to run' ), stdin: stringy.varValue .optional() .describe('Provide stdin with a value read from a dot prop path'), run: lineShell, multiline: z .boolean() .default(false) .describe('Should the run command spread on multiple lines'), }) .describe('Configuration for the batch shell script'); const valuesLoopEach = z.string().min(1).max(300); const loopEach = z .object({ name: stringy.customKey.describe( 'The name of the variable to be used as an input to the script or command during each iteration' ), values: valuesLoopEach.describe( 'The variable to the array of values to be iterated over' ), }) .describe('Configuration of every loop'); const getPropertyStep = z .strictObject({ a: z.literal('get-property'), ...metadataStep, value: stringy.varValue, }) .describe('Get a property using a dot prop path'); const onStringArraySuccessEnum = { sort: 'Sorts the array of strings in ascending order.', unique: 'Removes any duplicate strings from the array.', filled: 'Removes any empty or undefined elements from the array.', reverse: 'Reverses the order of the elements in the array.', }; const onStringArraySuccess = z .enum(asEnumKeys(onStringArraySuccessEnum)) .describe( describeEnum( 'Options for the transforming the resulting array of string', onStringArraySuccessEnum ) ); const stringArrayFilterByEnum = { 'starts-with': 'Match items that start with a specific value', 'ends-with': 'Match items that end with a specific value', contains: 'Match items that contain a specific value', equals: 'Match items that are equal to a specific value', 'not starts-with': 'Match items that do not start with a specific value', 'not ends-with': 'Match items that do not end with a specific value', 'not contains': 'Match items that do not contain a specific value', 'not equals': 'Match items that are not equal to a specific value', }; const stringArrayFilterBy = z .strictObject({ if: z .enum(asEnumKeys(stringArrayFilterByEnum)) .describe( describeEnum( 'A conditional statement that determines whether or not the string should be kept', stringArrayFilterByEnum ) ), anyOf: z .array(stringy.varValue) .min(1) .max(50) .describe('A list of references to match against'), }) .describe('Filter an array of strings'); const stringArrayStep = z .strictObject({ a: z.literal('string-array'), ...metadataStep, value: stringy.varValue, onSuccess: onStringArraySuccess, filters: z .array(stringArrayFilterBy) .min(1) .max(20) .optional() .describe('A list of filters on strings'), }) .describe('Process on a list of strings'); const concatArrayStep = z .strictObject({ a: z.literal('concat-array'), ...metadataStep, values: z.array(stringy.varValue).min(1).max(20), }) .describe('Concatenate several arrays together'); const splitStringStep = z .strictObject({ a: z.literal('split-string'), ...metadataStep, separator: z .string() .min(1) .max(80) .default(' ') .describe('A separator to split the string'), value: stringy.varValue, }) .describe('Split a string into multiple strings'); const splitLinesStep = z .strictObject({ a: z.literal('split-lines'), ...metadataStep, value: stringy.varValue, }) .describe('Split a string into multiple lines'); const someTruthyArrayStep = z .strictObject({ a: z.literal('some-truthy'), ...metadataStep, values: z.array(stringy.varValue).min(1).max(20), }) .describe('Return true if at least one of values is truthy'); const someFalsyArrayStep = z .strictObject({ a: z.literal('some-falsy'), ...metadataStep, values: z.array(stringy.varValue).min(1).max(20), }) .describe('Return true if at least one of values is falsy'); const everyTruthyArrayStep = z .strictObject({ a: z.literal('every-truthy'), ...metadataStep, values: z.array(stringy.varValue).min(1).max(20), }) .describe('Return true if all the values are truthy'); const everyFalsyArrayStep = z .strictObject({ a: z.literal('every-falsy'), ...metadataStep, values: z.array(stringy.varValue).min(1).max(20), }) .describe('Return true if all the values are falsy'); const notStep = z .strictObject({ a: z.literal('not'), ...metadataStep, value: stringy.varValue, }) .describe('Return the opposite boolean value'); const rangeStep = z .strictObject({ a: z.literal('range'), ...metadataStep, start: z .number() .int() .default(0) .describe('The number to start the range with'), end: z.number().int().describe('The number at the end of the range'), step: z .number() .int() .default(1) .describe('A step to increment the range, usually 1'), }) .describe('Generate a range of numbers'); const invertObjectStep = z .strictObject({ a: z.literal('invert-object'), ...metadataStep, value: stringy.varValue, }) .describe('Invert keys and values into a new object'); const maskJsonStep = z .strictObject({ a: z.literal('mask-object'), ...metadataStep, value: stringy.varValue, mask: z .string() .min(1) .max(100) .describe('JSON mask to select parts of the json object'), }) .describe('Uses JSON mask to select parts of the json object'); const inputPromptStep = z .strictObject({ a: z.literal('prompt-input'), ...metadataStep, message: stringy.promptMessage, }) .describe('Prompt that takes user input and returns a string'); const passwordPromptStep = z .strictObject({ a: z.literal('prompt-password'), ...metadataStep, message: stringy.promptMessage, }) .describe( 'Prompt that takes user input, hides it from the terminal, and returns a string' ); const confirmPromptStep = z .strictObject({ a: z.literal('prompt-confirm'), ...metadataStep, message: stringy.promptMessage, }) .describe('Prompt that returns true or false'); const selectPromptStep = z .strictObject({ a: z.literal('prompt-select'), ...metadataStep, message: stringy.promptMessage, select: stringy.varValue, }) .describe('Prompt that allows the user to select from a list of options'); const choicePromptStep = z .strictObject({ a: z.literal('prompt-choices'), ...metadataStep, message: stringy.promptMessage, choices: z.array(stringy.varValue).min(2).max(30), }) .describe('Prompt that allows the user to choose an option'); const templateStep = z .strictObject({ a: z.literal('template'), ...metadataStep, template: z .string() .min(1) .max(5000) .describe( 'Resolve the handlebars template as a atring. https://handlebarsjs.com/guide/' ), }) .describe('Uses JSON mask to select parts of the json object'); const appendToFileStep = z .strictObject({ a: z.literal('append-to-file'), ...metadataStep, value: stringy.varValue, filename: z .string() .min(1) .max(200) .describe('The name of the file to which data will be appended'), }) .describe('Step where data is appended to a file'); const writeToFileStep = z .strictObject({ a: z.literal('write-to-file'), ...metadataStep, value: stringy.varValue, filename: z .string() .min(1) .max(200) .describe('The name of the file to which data will be written'), }) .describe('Step where data is written to a file'); const anyCommand = z .union([ z.discriminatedUnion('a', [ getPropertyStep, stringArrayStep, concatArrayStep, splitStringStep, splitLinesStep, someTruthyArrayStep, someFalsyArrayStep, everyTruthyArrayStep, everyFalsyArrayStep, notStep, rangeStep, invertObjectStep, maskJsonStep, templateStep, inputPromptStep, confirmPromptStep, passwordPromptStep, selectPromptStep, choicePromptStep, appendToFileStep, writeToFileStep, ]), advancedShell, ]) .describe('A command to be run'); const commands = z .array(anyCommand) .min(1) .max(50) .describe('A list of batch shell scripts to run'); const batchStepNameEmum = { unknown: 'Describe any unknown or uncertain aspects of the process (should not pick this)', main: 'Describe the main step of the process', before: 'Describe a step that need to be taken before the process begins', after: 'Describe a step that need to be taken after the process is completed', }; const batchStep = z .strictObject({ name: z .enum(asEnumKeys(batchStepNameEmum)) .default('unknown') .describe(describeEnum('Name of the step', batchStepNameEmum)), if: stringy.varValue .optional() .describe( 'A conditional statement that determines whether or not this script or command should be executed' ), each: z .array(loopEach) .min(1) .max(3) .optional() .describe( 'An array of values to be iterated over, with each iteration executing the script or command with the current value as an input' ), commands, }) .describe('A batch of shell commands to run'); const parameter = z .object({ description: stringy.description, flags: z.string().min(1).max(80), default: z.string().min(1).max(300).optional(), }) .describe('Settings for a task parameter'); const task = z .object({ name: z .string() .max(1) .default('') .describe('A unique identifier for this task'), title: stringy.title.describe( 'A brief and descriptive title for this task' ), description: stringy.description .optional() .describe( 'A detailed explanation of the purpose and function of this task' ), motivation: stringy.motivation .optional() .describe( 'The reason why this task is necessary within the context of the workflow' ), links: links.describe( 'A list of relevant resources and references related to this task' ), parameters: z .array(parameter) .max(20) .optional() .describe('A list of configurable options for this task'), main: batchStep.describe( 'The primary script or command to be executed for this task' ), before: batchStep.optional(), after: batchStep.optional(), }) .describe('Settings for a task'); const domain = z .object({ title: stringy.title.describe( 'A brief and descriptive title for this workflow' ), description: stringy.description .optional() .describe( 'A detailed explanation of the purpose and function of this workflow' ), tasks: z .record(stringy.customKey, task) .describe('A list of individual tasks that make up this workflow'), }) .describe('A domain for a list of tasks'); const workflows = z .record(stringy.customKey, domain) .describe( 'A collection of related tasks and processes that achieve a specific goal' ); export const schema = z .object({ engine, model: jsonishSchema, workflows, }) .describe('Settings for a baldrick-broth file') .strict(); const lightSchema = z .object({ engine, workflows, }) .describe('Settings for a baldrick-broth file'); const runtimeContext = z.object({ pwd: stringy.path, project: z.object({ name: stringy.title, }), parameters: z.record(z.string(), z.string().or(z.boolean())), }); const context = z.object({ build: schema, task, runtime: runtimeContext, data: z.record(stringy.propPath, jsonishSchema), }); export type BuildModel = z.infer<typeof schema>; export type TaskModel = z.infer<typeof task>; export type BatchStepModel = z.infer<typeof batchStep>; export type AnyCommand = z.infer<typeof anyCommand>; // Export type CommandOptionsModel = z.infer<typeof advancedShell>; export type onCommandSuccess = z.infer<typeof onShellCommandFinish>; export type onCommandFailure = z.infer<typeof onShellCommandFinish>; export type Ctx = z.infer<typeof context>; export type RuntimeContext = z.infer<typeof runtimeContext>; export type AnyDataValue = z.infer<typeof jsonishSchema>; export type OnShellCommandFinish = z.infer<typeof onShellCommandFinish>; export type BuildModelValidation = Result<BuildModel, ValidationError[]>; export const safeParseBuild = (content: unknown): BuildModelValidation => { const result = schema.safeParse(content); if (result.success) { return succeed(result.data); } const { error: { issues }, } = result; const errors = issues.map(formatMessage); return willFail(errors); }; /** Only used by conversion to json schema */ export const getSchema = (_name: 'default') => { return lightSchema; }; export const unsafeParse = (config: Record<string, string>) => (content: unknown) => { const name = `${config['model']}`.trim(); if (name === 'context') { context.parse(content); return content; } if (name === 'batchStep') { batchStep.parse(content); return content; } if (name === 'commands') { z.array(anyCommand).parse(content); return content; } return `${name} is not supported (979839)`; };