From 36011bc3b7e50e2d12c08b18b2d45aeb9aa6f594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Tue, 2 Apr 2024 21:24:16 +0200 Subject: [PATCH] feat(core): introduce async version of plugin dispatch mechanism (#3994) Refs #3993 --- packages/apidom-ast/src/index.ts | 2 +- packages/apidom-ast/src/traversal/visitor.ts | 32 ++++--- .../openapi-3-1-to-openapi-3-0-3/index.ts | 2 +- packages/apidom-core/src/index.ts | 8 +- packages/apidom-core/src/refractor/index.ts | 4 +- .../src/refractor/plugins/dispatcher/index.ts | 87 +++++++++++++++++++ .../src/refractor/plugins/utils/index.ts | 36 -------- .../refractor/plugins/dispatcher/index.ts | 59 +++++++++++++ 8 files changed, 179 insertions(+), 51 deletions(-) create mode 100644 packages/apidom-core/src/refractor/plugins/dispatcher/index.ts delete mode 100644 packages/apidom-core/src/refractor/plugins/utils/index.ts create mode 100644 packages/apidom-core/test/refractor/plugins/dispatcher/index.ts diff --git a/packages/apidom-ast/src/index.ts b/packages/apidom-ast/src/index.ts index a1cd4602a..3c6996aaf 100644 --- a/packages/apidom-ast/src/index.ts +++ b/packages/apidom-ast/src/index.ts @@ -71,7 +71,6 @@ export { default as ParseResult } from './ParseResult'; export { isParseResult, isLiteral, isPoint, isPosition } from './predicates'; // AST traversal related exports export { - customPromisifySymbol, BREAK, mergeAll as mergeAllVisitors, getVisitFn, @@ -80,3 +79,4 @@ export { isNode, cloneNode, } from './traversal/visitor'; +export type { MergeAllSync, MergeAllAsync } from './traversal/visitor'; diff --git a/packages/apidom-ast/src/traversal/visitor.ts b/packages/apidom-ast/src/traversal/visitor.ts index cb4f5c82c..a0c4fb111 100644 --- a/packages/apidom-ast/src/traversal/visitor.ts +++ b/packages/apidom-ast/src/traversal/visitor.ts @@ -6,8 +6,6 @@ import { ApiDOMStructuredError } from '@swagger-api/apidom-error'; * SPDX-License-Identifier: MIT */ -export const customPromisifySymbol: unique symbol = Symbol.for('nodejs.util.promisify.custom'); - // getVisitFn :: (Visitor, String, Boolean) -> Function export const getVisitFn = (visitor: any, type: string, isLeaving: boolean) => { const typeVisitor = visitor[type]; @@ -60,7 +58,7 @@ export const cloneNode = (node: any) => * `exposeEdits=true` can be used to exoise the edited node from the previous visitors. */ -interface MergeAllBase { +export interface MergeAllSync { ( visitors: any[], options?: { @@ -75,15 +73,27 @@ interface MergeAllBase { enter: (node: any, ...rest: any[]) => any; leave: (node: any, ...rest: any[]) => any; }; + [key: symbol]: MergeAllAsync; } -interface MergeAllPromisify { - [customPromisifySymbol]: MergeAllBase; +export interface MergeAllAsync { + ( + visitors: any[], + options?: { + visitFnGetter?: typeof getVisitFn; + nodeTypeGetter?: typeof getNodeType; + breakSymbol?: typeof BREAK; + deleteNodeSymbol?: any; + skipVisitingNodeSymbol?: boolean; + exposeEdits?: boolean; + }, + ): { + enter: (node: any, ...rest: any[]) => Promise; + leave: (node: any, ...rest: any[]) => Promise; + }; } -type MergeAll = MergeAllBase & MergeAllPromisify; - -export const mergeAll: MergeAll = (( +export const mergeAll: MergeAllSync = (( visitors: any[], { visitFnGetter = getVisitFn, @@ -150,9 +160,9 @@ export const mergeAll: MergeAll = (( return undefined; }, }; -}) as MergeAll; +}) as MergeAllSync; -mergeAll[customPromisifySymbol] = ( +const mergeAllAsync: MergeAllAsync = ( visitors: any[], { visitFnGetter = getVisitFn, @@ -223,6 +233,8 @@ mergeAll[customPromisifySymbol] = ( }; }; +mergeAll[Symbol.for('nodejs.util.promisify.custom')] = mergeAllAsync; + /* eslint-disable no-continue, no-param-reassign */ /** * visit() will walk through an AST using a preorder depth first traversal, calling diff --git a/packages/apidom-converter/src/strategies/openapi-3-1-to-openapi-3-0-3/index.ts b/packages/apidom-converter/src/strategies/openapi-3-1-to-openapi-3-0-3/index.ts index 8a10c1d9c..8e78da050 100644 --- a/packages/apidom-converter/src/strategies/openapi-3-1-to-openapi-3-0-3/index.ts +++ b/packages/apidom-converter/src/strategies/openapi-3-1-to-openapi-3-0-3/index.ts @@ -60,7 +60,7 @@ class OpenAPI31ToOpenAPI30ConvertStrategy extends ConvertStrategy { async convert(file: IFile): Promise { const annotations: AnnotationElement[] = []; - const parseResultElement = dispatchRefractorPlugins( + const parseResultElement: ParseResultElement = dispatchRefractorPlugins( cloneDeep(file.parseResult), [ openAPIVersionRefractorPlugin(), diff --git a/packages/apidom-core/src/index.ts b/packages/apidom-core/src/index.ts index 35cbffb09..ab74bf9b8 100644 --- a/packages/apidom-core/src/index.ts +++ b/packages/apidom-core/src/index.ts @@ -1,4 +1,10 @@ -export { dispatchPlugins as dispatchRefractorPlugins } from './refractor/plugins/utils'; +export { dispatchPluginsSync as dispatchRefractorPlugins } from './refractor/plugins/dispatcher'; +export type { + DispatchPluginsSync, + DispatchPluginsAsync, + DispatchPluginsOptions, +} from './refractor/plugins/dispatcher'; + export { default as refractorPluginElementIdentity } from './refractor/plugins/element-identity'; export { default as refractorPluginSemanticElementIdentity } from './refractor/plugins/semantic-element-identity'; diff --git a/packages/apidom-core/src/refractor/index.ts b/packages/apidom-core/src/refractor/index.ts index 732449d40..2c7f506e9 100644 --- a/packages/apidom-core/src/refractor/index.ts +++ b/packages/apidom-core/src/refractor/index.ts @@ -1,6 +1,6 @@ import { Element } from 'minim'; -import { dispatchPlugins } from './plugins/utils'; +import { dispatchPluginsSync } from './plugins/dispatcher'; import { getNodeType } from '../traversal/visitor'; import { cloneDeep } from '../clone'; import { isElement } from '../predicates'; @@ -32,7 +32,7 @@ const refract = (value: any, { Type, plugins = [] }: RefractOptions): Element => * Run plugins only when necessary. * Running plugins visitors means extra single traversal === performance hit. */ - return dispatchPlugins(element, plugins, { + return dispatchPluginsSync(element, plugins, { toolboxCreator: createToolbox, visitorOptions: { nodeTypeGetter: getNodeType }, }); diff --git a/packages/apidom-core/src/refractor/plugins/dispatcher/index.ts b/packages/apidom-core/src/refractor/plugins/dispatcher/index.ts new file mode 100644 index 000000000..625edda1f --- /dev/null +++ b/packages/apidom-core/src/refractor/plugins/dispatcher/index.ts @@ -0,0 +1,87 @@ +import { Element } from 'minim'; +import { mergeDeepRight, propOr } from 'ramda'; +import { invokeArgs } from 'ramda-adjunct'; + +import createToolbox from '../../toolbox'; +import { getNodeType, mergeAllVisitors, visit } from '../../../traversal/visitor'; + +export interface DispatchPluginsOptions { + toolboxCreator: typeof createToolbox; + visitorOptions: { + nodeTypeGetter: typeof getNodeType; + exposeEdits: boolean; + }; +} + +export interface DispatchPluginsSync { + ( + element: T, + plugins: ((toolbox: any) => object)[], + options?: Record, + ): U; + [key: symbol]: DispatchPluginsAsync; +} + +export interface DispatchPluginsAsync { + ( + element: T, + plugins: ((toolbox: any) => object)[], + options?: Record, + ): Promise; +} + +const defaultDispatchPluginsOptions: DispatchPluginsOptions = { + toolboxCreator: createToolbox, + visitorOptions: { + nodeTypeGetter: getNodeType, + exposeEdits: true, + }, +}; + +export const dispatchPluginsSync: DispatchPluginsSync = ((element, plugins, options = {}) => { + if (plugins.length === 0) return element; + + const mergedOptions = mergeDeepRight( + defaultDispatchPluginsOptions, + options, + ) as DispatchPluginsOptions; + const { toolboxCreator, visitorOptions } = mergedOptions; + const toolbox = toolboxCreator(); + const pluginsSpecs = plugins.map((plugin) => plugin(toolbox)); + const mergedPluginsVisitor = mergeAllVisitors(pluginsSpecs.map(propOr({}, 'visitor')), { + ...visitorOptions, + }); + + pluginsSpecs.forEach(invokeArgs(['pre'], [])); + const newElement = visit(element, mergedPluginsVisitor, visitorOptions as any); + pluginsSpecs.forEach(invokeArgs(['post'], [])); + return newElement; +}) as DispatchPluginsSync; + +export const dispatchPluginsAsync: DispatchPluginsAsync = async ( + element, + plugins, + options = {}, +) => { + if (plugins.length === 0) return element; + + const mergedOptions = mergeDeepRight( + defaultDispatchPluginsOptions, + options, + ) as DispatchPluginsOptions; + const { toolboxCreator, visitorOptions } = mergedOptions; + const toolbox = toolboxCreator(); + const pluginsSpecs = plugins.map((plugin) => plugin(toolbox)); + const mergeAllVisitorsAsync = mergeAllVisitors[Symbol.for('nodejs.util.promisify.custom')]; + const visitAsync = (visit as any)[Symbol.for('nodejs.util.promisify.custom')]; + const mergedPluginsVisitor = mergeAllVisitorsAsync(pluginsSpecs.map(propOr({}, 'visitor')), { + ...visitorOptions, + }); + + await Promise.allSettled(pluginsSpecs.map(invokeArgs(['pre'], []))); + const newElement = await visitAsync(element, mergedPluginsVisitor, visitorOptions as any); + await Promise.allSettled(pluginsSpecs.map(invokeArgs(['post'], []))); + return newElement; +}; + +dispatchPluginsSync[Symbol.for('nodejs.util.promisify.custom')] = dispatchPluginsAsync; diff --git a/packages/apidom-core/src/refractor/plugins/utils/index.ts b/packages/apidom-core/src/refractor/plugins/utils/index.ts deleted file mode 100644 index a0fe925a5..000000000 --- a/packages/apidom-core/src/refractor/plugins/utils/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Element } from 'minim'; -import { mergeDeepRight, propOr } from 'ramda'; -import { invokeArgs } from 'ramda-adjunct'; - -import createToolbox from '../../toolbox'; -import { getNodeType, mergeAllVisitors, visit } from '../../../traversal/visitor'; - -const defaultDispatchPluginsOptions = { - toolboxCreator: createToolbox, - visitorOptions: { - nodeTypeGetter: getNodeType, - exposeEdits: true, - }, -}; - -// eslint-disable-next-line import/prefer-default-export -export const dispatchPlugins = ( - element: T, - plugins: ((toolbox: any) => object)[], - options = {}, -): T => { - if (plugins.length === 0) return element; - - const mergedOptions = mergeDeepRight(defaultDispatchPluginsOptions, options); - const { toolboxCreator, visitorOptions } = mergedOptions; - const toolbox = toolboxCreator(); - const pluginsSpecs = plugins.map((plugin) => plugin(toolbox)); - const mergedPluginsVisitor = mergeAllVisitors(pluginsSpecs.map(propOr({}, 'visitor')), { - ...visitorOptions, - }); - - pluginsSpecs.forEach(invokeArgs(['pre'], [])); - const newElement = visit(element, mergedPluginsVisitor, visitorOptions as any); - pluginsSpecs.forEach(invokeArgs(['post'], [])); - return newElement as T; -}; diff --git a/packages/apidom-core/test/refractor/plugins/dispatcher/index.ts b/packages/apidom-core/test/refractor/plugins/dispatcher/index.ts new file mode 100644 index 000000000..5b6f0f734 --- /dev/null +++ b/packages/apidom-core/test/refractor/plugins/dispatcher/index.ts @@ -0,0 +1,59 @@ +import sinon from 'sinon'; +import { assert } from 'chai'; + +import { + NumberElement, + ObjectElement, + toValue, + dispatchRefractorPlugins as dispatchPluginsSync, +} from '../../../../src'; + +const dispatchPluginsAsync = dispatchPluginsSync[Symbol.for('nodejs.util.promisify.custom')]; + +describe('refrator', function () { + context('plugins', function () { + context('dispatcher', function () { + context('dispatchPluginsSync', function () { + specify('should dispatch plugins synchronously', function () { + const preSpy = sinon.spy(); + const postSpy = sinon.spy(); + const NumberElementSpy = sinon.spy(() => new NumberElement(2)); + const plugin = () => ({ + pre: preSpy, + visitor: { + NumberElement: NumberElementSpy, + }, + post: postSpy, + }); + const objectElement = new ObjectElement({ a: 1 }); + const result = dispatchPluginsSync(objectElement, [plugin]); + + assert.isTrue(preSpy.calledBefore(NumberElementSpy)); + assert.isTrue(postSpy.calledAfter(NumberElementSpy)); + assert.deepEqual(toValue(result), { a: 2 }); + }); + }); + + context('dispatchPluginsASync', function () { + specify('should dispatch plugins asynchronously', async function () { + const preSpy = sinon.spy(); + const postSpy = sinon.spy(); + const NumberElementSpy = sinon.spy(async () => new NumberElement(2)); + const plugin = () => ({ + pre: preSpy, + visitor: { + NumberElement: NumberElementSpy, + }, + post: postSpy, + }); + const objectElement = new ObjectElement({ a: 1 }); + const result = await dispatchPluginsAsync(objectElement, [plugin]); + + assert.isTrue(preSpy.calledBefore(NumberElementSpy)); + assert.isTrue(postSpy.calledAfter(NumberElementSpy)); + assert.deepEqual(toValue(result), { a: 2 }); + }); + }); + }); + }); +});