import { z } from 'zod'; import { createSDTFEngine, SDTFEngine, SpecifyDesignTokenFormat, } from '@specifyapp/specify-design-token-format'; import { SDKTelemetry } from '../../telemetry/provider.js'; import { ParsersPipelineState } from '../definitions/ParsersPipelineState.js'; import { ParsersEngineDataBox, parsersEngineDataBoxSchema, SDTFEngineDataBox, } from '../definitions/parsersEngineDataBox.js'; import { SpecifyError, specifyErrors, SpecifyParsersEngineErrorKey } from '../../errors/index.js'; import { ParsersEngineResults } from './ParsersEngineResults.js'; export type ParsersEngineExecutionContext = { isRemote: boolean; resolveRepositoryTokenTree: ( owner: string, name: string, ) => Promise< | { status: 'success'; tokenTree: SpecifyDesignTokenFormat } | { status: 'error'; content: string; originalError: unknown } >; }; /** * Prepare and execute the parsers engine by running asynchronously all the parsers pipeline in parallel. * @internal * @param pipelineStates * @param executionContext * @param dataBox */ export function makeExecuteParsersEngine( pipelineStates: Array, executionContext: ParsersEngineExecutionContext, dataBox: ParsersEngineDataBox, ) { const isOnlyRemotelyExecutedRules = pipelineStates.every(state => state.isRemotelyExecutedRule); let hasBeenExecuted = false; return async function execute() { if (hasBeenExecuted) { throw new SpecifyError({ errorKey: specifyErrors.PARSERS_ENGINE_PARSER_EXECUTION_FAILED.errorKey, publicMessage: 'The parsers engine has already been executed.', }); } else { hasBeenExecuted = true; } let engine: SDTFEngine | undefined; if (dataBox.type === 'SDTF Engine') { engine = dataBox.engine; } else if (dataBox.type === 'SDTF') { engine = createSDTFEngine(dataBox.graph, dataBox.metadata); } else if (dataBox.type === 'repository') { if (!isOnlyRemotelyExecutedRules || executionContext.isRemote) { const result = await executionContext.resolveRepositoryTokenTree( dataBox.owner, dataBox.name, ); if (result.status === 'success') { engine = createSDTFEngine(result.tokenTree); } else { // Return prematurely if the repository could not be resolved return new ParsersEngineResults( pipelineStates.map(state => ({ pipelineName: state.toolbox.pipelineName, isFromRule: state.isFromRule, status: 'error', output: null, next: undefined, errorMessages: [ { type: 'error', content: result.content, errorKey: specifyErrors.PARSERS_ENGINE_RESOLVE_REPOSITORY_FAILED.errorKey, }, ], warningMessages: [], informationMessages: [], })), ); } } } // Run the pipelines in parallel const execResults = await Promise.allSettled( pipelineStates.map(state => { let input: SDTFEngineDataBox; if (state.isFromRule) { if (engine) { input = { type: 'SDTF Engine', engine: state.isRemotelyExecutedRule ? engine : engine.clone(), }; } else { // We allow arbitrary data boxes to be passed to the built-in parsers input = parsersEngineDataBoxSchema.parse(dataBox) as SDTFEngineDataBox; } } else { if (engine) { input = { type: 'SDTF Engine', engine: engine.clone(), }; } else { throw new SpecifyError({ errorKey: specifyErrors.PARSERS_ENGINE_PARSER_EXECUTION_FAILED.errorKey, publicMessage: `No SDTF engine provided for parser "${state.toolbox.pipelineName}".`, }); } } return state.fn(input, state.toolbox).then(next => parsersEngineDataBoxSchema.parse(next, { path: [state.toolbox.pipelineName], }), ); }), ); // Iterate over the results for (let i = 0; i < execResults.length; i++) { const result = execResults[i]; const state = pipelineStates[i]; state.status = result.status; if (result.status === 'rejected') { let errorKey: SpecifyParsersEngineErrorKey = specifyErrors.PARSERS_ENGINE_UNKNOWN_ERROR.errorKey; let content = 'Unknown error'; if (result.reason instanceof z.ZodError) { errorKey = specifyErrors.PARSERS_ENGINE_VALIDATION_ERROR.errorKey; content = result.reason.message; } else if (result.reason instanceof SpecifyError) { errorKey = result.reason.errorKey as SpecifyParsersEngineErrorKey; content = result.reason.message; /* v8 ignore next 3 */ } else if (result.reason instanceof Error) { content = result.reason.message; } state.toolbox.populateMessage({ type: 'error', content, errorKey, }); } else { state.next = result.value; } } new SDKTelemetry().track('Parsers Engine Executed', { isRemote: executionContext.isRemote, pipelines: pipelineStates.map(state => ({ pipelineName: state.toolbox.pipelineName, isFromRule: state.isFromRule, status: state.status === 'fulfilled' ? 'success' : 'error', errorMessages: state.toolbox.errorMessages, warningMessages: state.toolbox.warningMessages, })), }); return new ParsersEngineResults( pipelineStates.map(state => { return { pipelineName: state.toolbox.pipelineName, isFromRule: state.isFromRule, status: state.status === 'fulfilled' ? 'success' : 'error', output: state.toolbox.output, next: state.next, errorMessages: state.toolbox.errorMessages, warningMessages: state.toolbox.warningMessages, informationMessages: state.toolbox.informationMessages, }; }), ); }; }