import { SpecifyError } from '../../errors/SpecifyError.js'; import { FilesOutput, filesOutputFileSchema, ParserOutput } from '../definitions/parserOutput.js'; import type { ParsersEngineDataBox, SDTFDataBox, SerializedParsersEngineDataBox, } from '../definitions/parsersEngineDataBox.js'; import type { ParsersEngineErrorMessage, ParsersEngineInformationMessage, ParsersEngineWarningMessage, } from '../definitions/ParsersEngineMessage.js'; import { SerializedParsersEngineResults } from './SerializedParsersEngineResult.js'; export type ParsersEngineResult = { pipelineName: string; isFromRule: boolean; status: 'success' | 'error'; output: ParserOutput | null; next: ParsersEngineDataBox | undefined; errorMessages: Array; warningMessages: Array; informationMessages: Array; }; export class ParsersEngineResults> { #results: R; constructor(results: R) { this.#results = results; } /** * Get the raw results of the parser pipelines execution */ get all() { return this.#results; } /** * Whether any of the pipelines returned an error */ get hasError() { return this.#results.some(result => result.errorMessages.length > 0); } /** * Whether any of the pipelines returned warnings */ get hasWarning() { return this.#results.some(result => result.warningMessages.length > 0); } /** * Gather the error messages from all the pipelines */ get allErrorMessages() { return this.#results.flatMap(result => result.errorMessages); } /** * Gather the warning messages from all the pipelines */ get allWarningMessages() { return this.#results.flatMap(result => result.warningMessages); } /** * Gather the information messages from all the pipelines */ get allInformationMessages() { return this.#results.flatMap(result => result.informationMessages); } /** * Print error messages to the console */ logErrorMessages() { this.#results.forEach(result => { result.errorMessages.forEach(message => console.log(`${result.pipelineName}: ${message.content} - ${message.errorKey}`), ); }); } /** * Print warning messages to the console */ logWarningMessages() { this.#results.forEach(result => { result.warningMessages.forEach(message => console.log(`${result.pipelineName}: ${message.content} - ${message.errorKey}`), ); }); } /** * Print information messages to the console */ logInformationMessages() { this.#results.forEach(result => { result.informationMessages.forEach(message => console.log(`${result.pipelineName}: ${message.content}`), ); }); } /** * Print a summary of the execution results to the console * @param withOutputContent - print the content of the produced output(s) * @param withNextContent - print the content of the next data box */ debug( { withOutputContent, withNextContent, }: { withOutputContent?: boolean; withNextContent?: boolean; } = { withOutputContent: false, withNextContent: false, }, ) { /* v8 ignore start */ console.log('Execution results:'); for (const state of this.#results) { console.log(`→ [${state.status}] ${state.pipelineName}`); console.log(' ⇢ Messages:'); if ( state.errorMessages.length === 0 && state.warningMessages.length === 0 && state.informationMessages.length === 0 ) { console.log(' -'); } for (const message of state.errorMessages) { console.log(` [error] ${message.errorKey} - ${message.content}`); } for (const message of state.warningMessages) { console.log(` [warning] ${message.errorKey} - ${message.content}`); } for (const message of state.informationMessages) { console.log(` [info] ${message.content}`); } console.log(' ⇢ Output:'); if (state.output === null) { console.log(' -'); } else { switch (state.output.type) { case 'files': state.output.files.forEach(file => { console.log(` [file] ${file.path}`); if (withOutputContent) { switch (file.content.type) { case 'text': console.log(`[content start]\n${file.content.text}\n[content end]`); break; case 'url': console.log(` [url] ${file.content.url}`); break; } } }); break; case 'JSON': console.log(` [JSON] ${JSON.stringify(state.output.json)}`); break; case 'SDTF': console.log(` [SDTF] ${JSON.stringify(state.output.graph)}`); break; default: console.log(' [Unknown]'); break; } } console.log(' ⇢ Next:'); if (state.next) { console.log(` type: ${state.next.type}`); if (withNextContent) { switch (state.next.type) { case 'SDTF': { console.log(` [SDTF] ${JSON.stringify(state.next.graph)}`); break; } case 'JSON': { console.log(` [JSON] ${JSON.stringify(state.next.json)}`); break; } case 'custom': { console.log(` [custom] ${state.next.data}`); break; } case 'vector': { console.log( ` [vector] ${state.next.assets.map(file => file.path).join(',\n ')}`, ); break; } case 'bitmap': { console.log( ` [bitmap] ${state.next.assets.map(file => file.path).join(',\n ')}`, ); break; } case 'asset': { console.log( ` [asset] ${state.next.assets.map(file => file.path).join(',\n ')}`, ); break; } } } } else { console.log(' -'); } console.log('\n'); } /* v8 ignore stop */ } /** * Map over any output of the parsers. This method is quite convenient if you want to run some post-process on the output. * @param fn */ mapOutput(fn: (output: ParserOutput | null) => ParserOutput | null | void) { for (let x = 0; x < this.#results.length; x++) { const maybeOutput = fn(this.#results[x].output); if (maybeOutput !== undefined) { this.#results[x].output = maybeOutput; } } return this; } /** * Map over output files. This method is quite convenient if you want to run some post-process on the files * @param {(file: { path: string; content: { type: "text"; text: string; } | { type: "url"; url: string; }}) => { path: string; content: { type: "text"; text: string; } | { type: "url"; url: string; }}} fn - The mapping function */ mapFiles(fn: (file: FilesOutput['files'][number]) => FilesOutput['files'][number] | void) { for (let x = 0; x < this.#results.length; x++) { const result = this.#results[x]; if (result.output === null) { continue; } if (result.output.type !== 'files') { continue; } result.output.files = result.output.files.map(file => { const result = fn(file); if (result === undefined) { return file; } return filesOutputFileSchema.parse(result); }); } return this; } /** * Write the outputs of the parsers to the file system * @param [directoryPath] - Root directory where all the files will be written */ async writeToDisk(directoryPath?: string) { const fsHelper = await import('../../fileSystem/index.js'); return Promise.allSettled( this.#results.map(result => { if (result.output) { return fsHelper.writeParserOutputToFileSystem(result.output, { directoryPath }); } else { return Promise.reject(`${result.pipelineName} - No output to write to disk`); } }), ).then(fsResults => fsResults.reduce<{ errors: Array; outputPaths: Array; }>( (acc, curr) => { if (curr.status === 'rejected') { acc.errors.push(curr.reason as SpecifyError); } else { acc.outputPaths = acc.outputPaths.concat(curr.value); } return acc; }, { errors: [], outputPaths: [], }, ), ); } /** * Serialize the results before HTTP transmission */ serialize(): SerializedParsersEngineResults { return this.#results.map(state => { let next = state.next ?? null; if (state.next && state.next.type === 'SDTF Engine') { const { tokenTree, metadata } = state.next.engine.exportEngineState(); const serialized: SDTFDataBox = { type: 'SDTF', graph: tokenTree, metadata, }; next = serialized; } return { ...state, next: next as SerializedParsersEngineDataBox | null, }; }); } }