Skip to content

Commit

Permalink
feat(dev-utils): support selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Jun 15, 2021
1 parent 26bcd3a commit fbae116
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 15 deletions.
7 changes: 4 additions & 3 deletions packages/koishi-core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ export namespace Plugin {

export type Config<T extends Plugin> = T extends Function<infer U> ? U : T extends Object<infer U> ? U : never

export interface State extends Meta {
export interface State<T = any> extends Meta {
id?: string
parent?: State
context?: Context
config?: Config<Plugin>
config?: T
plugin?: Plugin
children: Plugin[]
disposables: Disposable[]
Expand Down Expand Up @@ -582,7 +582,8 @@ type DelegateEventMap = {
type EventName = keyof EventMap
type OmitSubstring<S extends string, T extends string> = S extends `${infer L}${T}${infer R}` ? `${L}${R}` : never
type BeforeEventName = OmitSubstring<EventName & string, 'before-'>
type BeforeEventMap = { [E in EventName & string as OmitSubstring<E, 'before-'>]: EventMap[E] }

export type BeforeEventMap = { [E in EventName & string as OmitSubstring<E, 'before-'>]: EventMap[E] }

export interface EventMap extends SessionEventMap, DelegateEventMap {
[Context.middleware]: Middleware
Expand Down
61 changes: 53 additions & 8 deletions packages/koishi-dev-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import * as K from 'koishi-core'

const map = new WeakMap<Object, ((this: K.Context) => void)[]>()
const registry = new WeakMap<Object, Map<Function, (this: K.Context) => void>>()

export interface PluginContext<T = any> extends K.Plugin.Object<T>, K.Context {}

export class PluginContext<T = any> {
protected config: T
readonly state: K.Plugin.State<T>
}

type PluginStatic = typeof K.Plugin & (<T extends new () => any>(name: string) => (factory: T) => T)
type PluginStatic = typeof K.Plugin & (<T extends new () => any>(name?: string) => (factory: T) => T)

export type Plugin = K.Plugin

Expand All @@ -18,7 +19,7 @@ export const Plugin = ((name) => (factory) => {
apply(context: K.Context, config: any) {
const instance = Object.create(context)
instance.config = config
const cbs = map.get(factory.prototype) || []
const cbs = registry.get(factory.prototype) || []
cbs.forEach(cb => cb.call(instance))
factory.prototype.apply?.call(instance)
}
Expand All @@ -27,20 +28,64 @@ export const Plugin = ((name) => (factory) => {

Object.assign(Plugin, K.Plugin)

type MethodDecorator<T> = (target: Object, key: string | symbol, desc: TypedPropertyDescriptor<T>) => void
type MethodDecorator<T = (...args: any[]) => any> = (target: Object, key: string | symbol, desc: TypedPropertyDescriptor<T>) => void | TypedPropertyDescriptor<T>

export type Middleware = K.Middleware

export const Middleware: (prepend?: boolean) => MethodDecorator<K.Middleware> = (prepend) => (target, key, desc) => {
if (!map.has(target)) map.set(target, [])
map.get(target).push(function() {
if (!registry.has(target)) registry.set(target, new Map())
registry.get(target).set(desc.value, function() {
this.middleware(desc.value.bind(this), prepend)
})
}

export const Event: <E extends keyof K.EventMap>(name: E, prepend?: boolean) => MethodDecorator<K.EventMap[E]> = (name, prepend) => (target, key, desc) => {
if (!map.has(target)) map.set(target, [])
map.get(target).push(function() {
if (!registry.has(target)) registry.set(target, new Map())
registry.get(target).set(desc.value, function() {
this.on(name, (desc.value as any).bind(this), prepend)
})
}

export const Before: <E extends keyof K.BeforeEventMap>(name: E, append?: boolean) => MethodDecorator<K.BeforeEventMap[E]> = (name, append) => (target, key, desc) => {
if (!registry.has(target)) registry.set(target, new Map())
registry.get(target).set(desc.value, function() {
this.before(name, (desc.value as any).bind(this), append)
})
}

type PartialSeletor<R extends any[]> = (...values: R) => MethodDecorator

interface Selector<R extends any[]> extends PartialSeletor<R> {
except?: PartialSeletor<R>
}

function createPartialSelector<T extends keyof K.Context>(name: T, except?: boolean): PartialSeletor<K.Context[T] extends (...args: infer R) => any ? R : never> {
return (...values) => (target, key, desc) => {
const map = registry.get(target)
const callback = map?.get(desc.value)
if (!callback) return
map.set(desc.value, function () {
let selector: any = this[name]
if (except) selector = selector.except
callback.call(selector(...values))
})
}
}

function createSelector<T extends keyof K.Context, U>(name: T, target?: U): U & Selector<K.Context[T] extends (...args: infer R) => any ? R : never> {
const value: any = createPartialSelector(name)
value.except = createPartialSelector(name, true)
return Object.assign(value, target)
}

export type User = K.User
export type Channel = K.Channel
export type Platform = K.Platform

export const All = createPartialSelector('all')
export const User = createSelector('user', K.User)
export const Channel = createSelector('channel', K.Channel)
export const Platform = createSelector('platform')
export const Self = createSelector('self')
export const Group = createSelector('group')
export const Private = createSelector('private')
14 changes: 10 additions & 4 deletions packages/koishi-dev-utils/tests/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextFunction, Session } from 'koishi-core'
import { App } from 'koishi-test-utils'
import { Plugin, PluginContext, Middleware, Event } from 'koishi-dev-utils'
import { Plugin, PluginContext, Middleware, Event, User } from 'koishi-dev-utils'
import { expect } from 'chai'
import jest from 'jest-mock'

Expand All @@ -13,9 +13,10 @@ describe('Plugin Context', () => {

@Plugin('test-1')
class MyPlugin extends PluginContext<Config> {
@User.except('456')
@Middleware()
hello(session: Session, next: NextFunction) {
session.send(this.config.text)
session.send(this.state.config.text)
}

@Event('disconnect')
Expand All @@ -25,10 +26,15 @@ describe('Plugin Context', () => {
}

const app = new App().plugin(new MyPlugin(), { text: 'hello!' })
const sess = app.session('123')
const ses1 = app.session('123')
const ses2 = app.session('456')

it('middleware', async () => {
await sess.shouldReply('say hello', 'hello!')
await ses1.shouldReply('say hello', 'hello!')
})

it('selector', async () => {
await ses2.shouldNotReply('say hello')
})

it('event', async () => {
Expand Down

0 comments on commit fbae116

Please sign in to comment.