feat(dispatcher): allow predicates in CallbackDataBuilder .filter()

This commit is contained in:
alina 🌸 2024-05-27 00:06:31 +03:00
parent f0a63f3301
commit 0960dae59b
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
2 changed files with 109 additions and 16 deletions

View file

@ -58,44 +58,84 @@ describe('CallbackDataBuilder', () => {
new PeersIndex(), new PeersIndex(),
) )
const getFilterMatch = (filter: UpdateFilter<CallbackQuery>, data: string) => { const getFilterMatch = async (filter: UpdateFilter<CallbackQuery>, data: string) => {
const cb = createCb(data) const cb = createCb(data)
const matched = filter(cb) const matched = await filter(cb)
if (!matched) return null if (!matched) return null
// eslint-disable-next-line // eslint-disable-next-line
return (cb as any).match 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') 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', foo: 'foo',
bar: 'bar', 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') 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', foo: 'foo',
bar: 'bar', 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') 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', foo: '123',
bar: 'bar', 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)
}) })
}) })
}) })

View file

@ -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' import { UpdateFilter } from './filters/types.js'
@ -84,18 +84,71 @@ export class CallbackDataBuilder<T extends string> {
/** /**
* Create a filter for this callback data. * Create a filter for this callback data.
* *
* > **Note**: `params` object will be compiled to a RegExp, * You can either pass an object with field names as keys and values as strings or regexes,
* > so avoid using characters that have special meaning in regex, * which will be compiled to a RegExp, or a function that will be called with the parsed data.
* > or use RegExp directly to let the IDE guide you. * 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 * @param params
*/ */
filter(params: Partial<Record<T, MaybeArray<string | RegExp>>> = {}): UpdateFilter< filter(
params:
| ((
upd: CallbackQuery,
parsed: Record<T, string>,
) => MaybePromise<Partial<Record<T, MaybeArray<string | RegExp>>> | boolean>)
| Partial<Record<T, MaybeArray<string | RegExp>>> = {},
): UpdateFilter<
CallbackQuery, CallbackQuery,
{ {
match: Record<T, string> match: Record<T, string>
} }
> { > {
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<T, string>
}
).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<string | RegExp>
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<T, string>
}
).match = data
return true
}
}
const parts: string[] = [] const parts: string[] = []
this._fields.forEach((field) => { this._fields.forEach((field) => {