import { SDKTelemetry } from '../../telemetry/provider.js'; import { ParsersPipelineState } from '../definitions/ParsersPipelineState.js'; import { SpecifyError, specifyErrors } from '../../errors/index.js'; import { ParserToolbox } from '../ParserToolbox.js'; import { makeRemoteParsersPipelinesFromRules } from './makeRemoteParsersPipelinesFromRules.js'; import { BuiltInParserRuleSignature } from '../../builtInParsers/internals/BuiltInParserRuleSignature.js'; import { makeLocalBuiltInParsersPipelineFromRule } from './makeLocalBuiltInParsersPipelineFromRule.js'; import { validateRuleOrBuildErrorParserFunction } from './validateRuleOrBuildErrorParserFunction.js'; function matchRemotelyExecutedRule( candidate: Record, ): candidate is { shouldExecuteRemotely: boolean } & Record { return ( candidate && typeof candidate === 'object' && !Array.isArray(candidate) && 'shouldExecuteRemotely' in candidate && (candidate as { shouldExecuteRemotely: boolean }).shouldExecuteRemotely === true ); } /** * Build many pipelines resolved by a single HTTP request * @param maybeRules * @param context */ export function createPipelineStatesFromRules( maybeRules: Array>, context: { isRemote: boolean; builtInParserKind: 'generation' | 'utility' | 'all'; indexLabel?: number | string; personalAccessToken?: string; }, ): Array { const telemetry = new SDKTelemetry(); for (const rule of maybeRules) { // Validate the shallow structure of the rule if (typeof rule !== 'object' || rule === null || Array.isArray(rule)) { throw new SpecifyError({ errorKey: specifyErrors.PARSERS_ENGINE_VALIDATION_ERROR.errorKey, publicMessage: 'Invalid rule argument. Must be a Rule configuration object.', }); } // Track the initialization of the built-in parser try { const castedRule = rule as BuiltInParserRuleSignature; if ('parsers' in castedRule) { for (const parser of castedRule.parsers) { telemetry.track('Built In Parser Initialized', { isFromRule: true, parserName: parser.name, options: parser.options ?? {}, output: parser.output ?? {}, }); } } } catch (error) {} } // Segregate the rules for remote and local execution const { rulesToExecuteRemotely, rulesToExecuteLocally } = maybeRules.reduce<{ rulesToExecuteRemotely: Array>; rulesToExecuteLocally: Array>; }>( ( acc, rule, // We need to keep the order of the rules before segregation _sortingIndex, ) => { // Remote environment always executes locally if (context.isRemote) { acc.rulesToExecuteLocally.push({ _sortingIndex, ...rule }); return acc; } if (matchRemotelyExecutedRule(rule)) { acc.rulesToExecuteRemotely.push({ _sortingIndex, ...rule }); } else { acc.rulesToExecuteLocally.push({ _sortingIndex, ...rule }); } return acc; }, { rulesToExecuteRemotely: [], rulesToExecuteLocally: [], }, ); // Turn remote rules into pipelines wrapping the RPC calls const remotePipelineStates = makeRemoteParsersPipelinesFromRules( rulesToExecuteRemotely.map( ({ _sortingIndex, ...rule }) => rule, ) as Array, { personalAccessToken: context.personalAccessToken, }, ).map<{ _sortingIndex: number; state: ParsersPipelineState; }>((parserFunction, index) => { const localRuleWithIndex = rulesToExecuteRemotely[index]; /* v8 ignore next 5 */ if (!localRuleWithIndex) throw new SpecifyError({ errorKey: specifyErrors.PARSERS_ENGINE_UNKNOWN_ERROR.errorKey, publicMessage: 'Design Error :: Rule must exists.', }); const { _sortingIndex, ...rule } = localRuleWithIndex; const result = validateRuleOrBuildErrorParserFunction( rule, [(rule.name as string | undefined) ?? (_sortingIndex as number)], context, ); return { _sortingIndex: localRuleWithIndex._sortingIndex as number, state: { // We override the function with the one that will throw the validation errors fn: result.isValid ? parserFunction : result.parserFunctionWithError, toolbox: new ParserToolbox({ pipelineName: localRuleWithIndex.name ? `#${localRuleWithIndex._sortingIndex} [remote] Rule: ${localRuleWithIndex.name}` : `#${localRuleWithIndex._sortingIndex} [remote] Rule`, }), isFromRule: true, isRemotelyExecutedRule: true, status: 'pending', next: undefined, }, }; }); // Turn local rules into pipelines const localPipelineStates = rulesToExecuteLocally.map<{ _sortingIndex: number; state: ParsersPipelineState; }>(({ _sortingIndex, ...rule }) => { const result = validateRuleOrBuildErrorParserFunction( rule, [(rule.name as string | undefined) ?? (_sortingIndex as number)], context, ); return { _sortingIndex: _sortingIndex as number, state: { fn: result.isValid ? // We override the function with the one that will throw the validation errors makeLocalBuiltInParsersPipelineFromRule(result.rule, { isRemote: context.isRemote, personalAccessToken: context.personalAccessToken, }) : result.parserFunctionWithError, toolbox: new ParserToolbox({ pipelineName: (rule as any)?.name ? `#${_sortingIndex} Rule: ${(rule as any)?.name}` : `#${_sortingIndex} Rule`, }), isFromRule: true, isRemotelyExecutedRule: false, status: 'pending', next: undefined, }, }; }); return ( [...remotePipelineStates, ...localPipelineStates] // We sort the states by their initial indexes to keep the order of the rules .sort((a, b) => a._sortingIndex - b._sortingIndex) .map(({ state }) => state) ); }