From 0960dae59bb8fd86bce74c01469a0bc81fee009d Mon Sep 17 00:00:00 2001 From: alina sireneva Date: Mon, 27 May 2024 00:06:31 +0300 Subject: [PATCH] feat(dispatcher): allow predicates in CallbackDataBuilder .filter() --- .../src/callback-data-builder.test.ts | 62 ++++++++++++++---- .../dispatcher/src/callback-data-builder.ts | 63 +++++++++++++++++-- 2 files changed, 109 insertions(+), 16 deletions(-) diff --git a/packages/dispatcher/src/callback-data-builder.test.ts b/packages/dispatcher/src/callback-data-builder.test.ts index d960dbc2..22eda243 100644 --- a/packages/dispatcher/src/callback-data-builder.test.ts +++ b/packages/dispatcher/src/callback-data-builder.test.ts @@ -58,44 +58,84 @@ describe('CallbackDataBuilder', () => { new PeersIndex(), ) - const getFilterMatch = (filter: UpdateFilter, data: string) => { + const getFilterMatch = async (filter: UpdateFilter, data: string) => { const cb = createCb(data) - const matched = filter(cb) + const matched = await filter(cb) if (!matched) return null // eslint-disable-next-line return (cb as any).match } - it('should create a filter without params', () => { + it('should create a filter without params', async () => { const cdb = new CallbackDataBuilder('prefix', 'foo', 'bar') - expect(getFilterMatch(cdb.filter(), 'prefix:foo:bar')).toEqual({ + expect(await getFilterMatch(cdb.filter(), 'prefix:foo:bar')).toEqual({ foo: 'foo', bar: 'bar', }) - expect(getFilterMatch(cdb.filter(), 'prefix:foo:bar:baz')).toEqual(null) + expect(await getFilterMatch(cdb.filter(), 'prefix:foo:bar:baz')).toEqual(null) }) - it('should create a filter with params', () => { + it('should create a filter with params', async () => { const cdb = new CallbackDataBuilder('prefix', 'foo', 'bar') - expect(getFilterMatch(cdb.filter({ foo: 'foo' }), 'prefix:foo:bar')).toEqual({ + expect(await getFilterMatch(cdb.filter({ foo: 'foo' }), 'prefix:foo:bar')).toEqual({ foo: 'foo', bar: 'bar', }) - expect(getFilterMatch(cdb.filter({ foo: 'foo' }), 'prefix:bar:bar')).toEqual(null) + expect(await getFilterMatch(cdb.filter({ foo: 'foo' }), 'prefix:bar:bar')).toEqual(null) }) - it('should create a filter with regex params', () => { + it('should create a filter with regex params', async () => { const cdb = new CallbackDataBuilder('prefix', 'foo', 'bar') - expect(getFilterMatch(cdb.filter({ foo: /\d+/ }), 'prefix:123:bar')).toEqual({ + expect(await getFilterMatch(cdb.filter({ foo: /\d+/ }), 'prefix:123:bar')).toEqual({ foo: '123', bar: 'bar', }) - expect(getFilterMatch(cdb.filter({ foo: /\d+/ }), 'prefix:bar:bar')).toEqual(null) + expect(await getFilterMatch(cdb.filter({ foo: /\d+/ }), 'prefix:bar:bar')).toEqual(null) + }) + + it('should create a filter with dynamic params', async () => { + const cdb = new CallbackDataBuilder('prefix', 'foo', 'bar') + + expect( + await getFilterMatch( + cdb.filter(() => ({ foo: 'foo' })), + 'prefix:foo:bar', + ), + ).toEqual({ + foo: 'foo', + bar: 'bar', + }) + expect( + await getFilterMatch( + cdb.filter(() => ({ foo: 'foo' })), + 'prefix:bar:baz', + ), + ).toEqual(null) + }) + + it('should create a filter with a predicate matcher', async () => { + const cdb = new CallbackDataBuilder('prefix', 'foo', 'bar') + + expect( + await getFilterMatch( + cdb.filter((_, data) => data.foo === 'foo'), + 'prefix:foo:bar', + ), + ).toEqual({ + foo: 'foo', + bar: 'bar', + }) + expect( + await getFilterMatch( + cdb.filter((_, data) => data.foo === 'foo'), + 'prefix:bar:baz', + ), + ).toEqual(null) }) }) }) diff --git a/packages/dispatcher/src/callback-data-builder.ts b/packages/dispatcher/src/callback-data-builder.ts index 098c10e8..8267c661 100644 --- a/packages/dispatcher/src/callback-data-builder.ts +++ b/packages/dispatcher/src/callback-data-builder.ts @@ -1,4 +1,4 @@ -import { CallbackQuery, MaybeArray, MtArgumentError } from '@mtcute/core' +import { CallbackQuery, MaybeArray, MaybePromise, MtArgumentError } from '@mtcute/core' import { UpdateFilter } from './filters/types.js' @@ -84,18 +84,71 @@ export class CallbackDataBuilder { /** * Create a filter for this callback data. * - * > **Note**: `params` object will be compiled to a RegExp, - * > so avoid using characters that have special meaning in regex, - * > or use RegExp directly to let the IDE guide you. + * You can either pass an object with field names as keys and values as strings or regexes, + * which will be compiled to a RegExp, or a function that will be called with the parsed data. + * Note that the strings will be passed to `RegExp` **directly**, so you may want to escape them. + * + * When using a function, you can either return a boolean, or an object with field names as keys + * and values as strings or regexes. In the latter case, the resulting object will be matched + * against the parsed data the same way as if you passed it directly. * * @param params */ - filter(params: Partial>> = {}): UpdateFilter< + filter( + params: + | (( + upd: CallbackQuery, + parsed: Record, + ) => MaybePromise>> | boolean>) + | Partial>> = {}, + ): UpdateFilter< CallbackQuery, { match: Record } > { + if (typeof params === 'function') { + return async (query) => { + if (!query.dataStr) return false + + const data = this.parse(query.dataStr) + const fnResult = await params(query, data) + + if (typeof fnResult === 'boolean') { + ( + query as CallbackQuery & { + match: Record + } + ).match = data + + return fnResult + } + + // validate result + for (const key in fnResult) { + const value = data[key] + if (value === undefined) return false + + let matchers = fnResult[key] as MaybeArray + if (!Array.isArray(matchers)) matchers = [matchers] + + for (const matcher of matchers) { + if (typeof matcher === 'string') { + if (value !== matcher) return false + } else if (!matcher.test(value)) return false + } + } + + ( + query as CallbackQuery & { + match: Record + } + ).match = data + + return true + } + } + const parts: string[] = [] this._fields.forEach((field) => {