Commit aab01e37 authored by nanahira's avatar nanahira

multi plugin

parent 4603929c
This diff is collapsed.
......@@ -54,7 +54,7 @@
"koishi-decorators": "^2.0.0",
"lodash": "^4.17.21",
"reflect-metadata": "^0.1.13",
"schemastery-gen": "^3.1.4",
"schemastery-gen": "^3.1.6",
"typed-reflector": "^1.0.10"
},
"jest": {
......@@ -75,7 +75,7 @@
"testEnvironment": "node"
},
"peerDependencies": {
"koishi": "^4.6.0",
"koishi": "^4.6.1",
"schemastery": "^3.1.0"
}
}
import 'reflect-metadata';
import { App, Context, Flatten, Keys, Selection, Tables } from 'koishi';
import { App, Context, Flatten, Keys, Schema, Selection, Tables } from 'koishi';
import { Metadata } from './meta/metadata.decorators';
import {
Condition,
......@@ -15,6 +15,7 @@ import {
} from './def';
import { TopLevelAction } from 'koishi-decorators';
import { ModelClassType, ModelRegistrar } from 'cosmotype-decorators';
import { ClassType } from 'schemastery-gen';
// Export all koishi-decorator decorators
......@@ -124,6 +125,9 @@ export function UsingService(
};
}
export const PluginSchema = (schema: Schema | ClassType<any>) =>
Metadata.set('KoishiPredefineSchema', schema);
export const If = <T>(func: Condition<boolean, T>): MethodDecorator =>
Metadata.append('KoishiIf', func);
......
// metadatas
import { Context } from 'koishi';
import { Context, Schema } from 'koishi';
import { Condition, ProvideDefinition, SystemInjectFun } from './interfaces';
import { ClassType } from 'schemastery-gen';
export const KoishiServiceInjectSym = 'KoishiServiceInjectSym';
export const KoishiServiceInjectSymKeys = 'KoishiServiceInjectSymKeys';
......@@ -24,4 +25,5 @@ export interface MetadataArrayMap {
export interface MetadataMap {
KoishiServiceInjectSym: keyof Context.Services;
KoishiSystemInjectSym: SystemInjectFun;
KoishiPredefineSchema: Schema | ClassType<any>;
}
......@@ -22,3 +22,23 @@ export type Condition<R, T = any> = (
config: T extends { config: infer C } ? C : any,
ctx: Context,
) => R;
export interface Instances<T> {
instances: T[];
}
export type AnyClassType = { new (...args: any[]): any };
export type TypeFromClass<T> = T extends { new (...args: any[]): infer U }
? U
: never;
export type ParamsFromClass<T> = T extends { new (...args: infer U): any }
? U
: never;
export type MultiPluginConfig<Inner, Outer> = Instances<Inner> & Outer;
export type ClassPluginConfig<
P extends new (ctx: Context, config: any) => any,
> = P extends new (ctx: Context, config: infer C) => any ? C : never;
import { ClonePlugin } from './utility/clone-plugin';
import { Context } from 'koishi';
import { PartialDeep } from './base-plugin';
import { ClassPluginConfig, MultiPluginConfig, TypeFromClass } from './def';
import { ClassType } from 'schemastery-gen';
import { ToInstancesConfig } from './utility/to-instance-config';
import Schema from 'schemastery';
import { InjectConfig, PluginSchema, UsingService } from './decorators';
import { UseEvent } from 'koishi-decorators';
export class MultiInstancePluginFramework<
InnerPlugin extends new (ctx: Context, config: any) => any,
OuterConfig,
> {
constructor(
public ctx: Context,
config: MultiPluginConfig<
ClassPluginConfig<InnerPlugin>,
PartialDeep<OuterConfig>
>,
) {}
@InjectConfig()
config: MultiPluginConfig<ClassPluginConfig<InnerPlugin>, OuterConfig>;
instances: TypeFromClass<InnerPlugin>[] = [];
_getInnerPlugin(): new (
ctx: Context,
config: ClassPluginConfig<InnerPlugin>,
) => any {
throw new Error(`Not implemented`);
}
_registerInstances() {
const innerPlugin = this._getInnerPlugin();
for (let i = 0; i < this.config.instances.length; i++) {
const clonedInnerPlugin = ClonePlugin(
innerPlugin,
`${innerPlugin.name}_instance_${i}`,
(instance) => this.instances.push(instance),
);
this.ctx.plugin(clonedInnerPlugin, this.config.instances[i]);
}
}
@UseEvent('dispose')
_onThingsDispose() {
delete this.instances;
}
onApply() {
this._registerInstances();
}
}
export function MultiInstancePlugin<
InnerPlugin extends new (ctx: Context, config: any) => any,
OuterConfig,
>(innerPlugin: InnerPlugin, outerConfig?: ClassType<OuterConfig>) {
const pluginClass = class SpecificMultiInstancePlugin extends MultiInstancePluginFramework<
InnerPlugin,
OuterConfig
> {
_getInnerPlugin() {
return innerPlugin;
}
};
const schema = ToInstancesConfig(
(innerPlugin['Config'] ||
innerPlugin['schema'] ||
Schema.any()) as ClassType<ClassPluginConfig<InnerPlugin>>,
outerConfig,
);
if (schema) {
PluginSchema(schema)(pluginClass);
}
if (innerPlugin['using']) {
UsingService(...(innerPlugin['using'] as (keyof Context.Services)[]))(
pluginClass,
);
}
return pluginClass;
}
......@@ -50,14 +50,12 @@ export function DefinePlugin<T = any>(
return function <
C extends {
new (...args: any[]): any;
} & KoishiPluginRegistrationOptions<any>,
} & KoishiPluginRegistrationOptions<T>,
>(originalClass: C) {
const schemaType =
options.schema || reflector.get('KoishiPredefineSchema', originalClass);
const newClass = class extends originalClass implements PluginClass {
static Config =
options.schema &&
((options.schema as Schema).type
? (options.schema as Schema<Partial<T>, T>)
: SchemaClass(options.schema as ClassType<T>));
static Config = schemaType ? SchemaClass(schemaType) : undefined;
static get using() {
const list = reflector
.getArray(KoishiAddUsingList, originalClass)
......@@ -224,14 +222,12 @@ export function DefinePlugin<T = any>(
this._initializePluginClass();
}
};
if (options.name) {
Object.defineProperty(newClass, 'name', {
enumerable: true,
configurable: true,
writable: true,
value: options.name,
});
}
Object.defineProperty(newClass, 'name', {
enumerable: true,
configurable: true,
writable: true,
value: options.name || originalClass.name,
});
return newClass;
};
}
......
import { TypeFromClass } from '../def';
export function ClonePlugin<P extends { new (...args: any[]): any }>(
target: P,
name: string,
callback?: (instance: TypeFromClass<P>) => void,
): P {
const clonedPlugin = class extends target {
constructor(...args: any[]) {
super(...args);
if (callback) {
callback(this as any);
}
}
};
for (const property of ['Config', 'schema', 'using']) {
Object.defineProperty(clonedPlugin, property, {
enumerable: true,
configurable: true,
writable: true,
value: target[property],
});
}
Object.defineProperty(clonedPlugin, 'name', {
enumerable: true,
configurable: true,
writable: true,
value: name,
});
return clonedPlugin;
}
import { OriginalClassSym, SchemaClass, SchemaProperty } from 'schemastery-gen';
import {
AnyClassType,
Instances,
ParamsFromClass,
TypeFromClass,
} from '../def';
import { kSchema } from 'schemastery-gen/dist/src/utility/kschema';
export function ToInstancesConfig<Inner extends new (...args: any[]) => any>(
instanceConfig: Inner,
): new () => Instances<TypeFromClass<Inner>>;
export function ToInstancesConfig<
Inner extends new (...args: any[]) => any,
Outer extends new (...args: any[]) => any,
>(
instanceConfig: Inner,
outerConfig?: Outer,
): new (...args: ParamsFromClass<Outer>) => Instances<TypeFromClass<Inner>> &
TypeFromClass<Outer>;
export function ToInstancesConfig<
Inner extends AnyClassType,
Outer extends AnyClassType,
>(
instanceConfig: Inner,
outerConfig?: Outer,
): new (...args: ParamsFromClass<Outer>) => Instances<TypeFromClass<Inner>> &
TypeFromClass<Outer> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
outerConfig ??= class EmptyConfig {};
if (outerConfig[OriginalClassSym]) {
outerConfig = outerConfig[OriginalClassSym];
}
const instanceConfigClass = class MixedInstancesConfig extends outerConfig {
instances: TypeFromClass<Inner>[];
};
SchemaProperty({
type: SchemaClass(instanceConfig),
default: [],
array: true,
})(instanceConfigClass.prototype, 'instances');
return instanceConfigClass;
}
......@@ -4,6 +4,7 @@ import { Cache, Assets, Bot, Context } from 'koishi';
describe('InjectUsing', () => {
@UsingService('router')
@DefinePlugin({ using: ['database'] })
@UsingService('http')
class MyPlugin {
@Inject(true)
cache: Cache;
......@@ -18,11 +19,12 @@ describe('InjectUsing', () => {
it('Should include injected using services', () => {
const usingList = (MyPlugin as any).using as (keyof Context.Services)[];
expect(usingList).toBeInstanceOf(Array);
expect(usingList.length).toEqual(4);
expect(usingList.length).toEqual(5);
expect(usingList.includes('database')).toEqual(true);
expect(usingList.includes('assets')).toEqual(true);
expect(usingList.includes('cache')).toEqual(true);
expect(usingList.includes('router')).toEqual(true);
expect(usingList.includes('bots')).toEqual(false);
expect(usingList.includes('http')).toEqual(true);
});
});
import { DefinePlugin } from '../src/register';
import { RegisterSchema, SchemaProperty } from 'schemastery-gen';
import { BasePlugin } from '../src/base-plugin';
import { UseCommand } from 'koishi-decorators';
import { MultiInstancePlugin } from '../src/multi-plugin';
import { App } from 'koishi';
class MessageConfig {
@SchemaProperty()
msg: string;
getMsg() {
return this.msg;
}
}
@RegisterSchema()
class InnerMessageConfig extends MessageConfig {}
@RegisterSchema()
class OuterMessageConfig extends MessageConfig {}
@DefinePlugin({ schema: InnerMessageConfig })
class Inner extends BasePlugin<InnerMessageConfig> {
@UseCommand('message')
async onMessage() {
return this.config.getMsg();
}
}
@DefinePlugin()
class Outer extends MultiInstancePlugin(Inner, OuterMessageConfig) {
@UseCommand('message2')
async onMessage() {
return this.config.getMsg();
}
@UseCommand('message3')
async onInnerMessage() {
return this.instances[0].config.getMsg();
}
}
describe('It should register multi plugin instance', () => {
it('register command on condition', async () => {
const app = new App();
console.log(Inner['Config']);
console.log(Outer['Config']);
app.plugin(Outer, { msg: 'hello', instances: [{ msg: 'world' }] });
await app.start();
const innerCommand = app.command('message');
const outerCommand = app.command('message2');
const innerInnerCommand = app.command('message3');
expect(await innerCommand.execute({})).toBe('world');
expect(await outerCommand.execute({})).toBe('hello');
expect(await innerInnerCommand.execute({})).toBe('world');
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment