Commit b276ec6b authored by nanahira's avatar nanahira

finish

parent 2783f91f
export * from './src/decorators';
export * from './src/def';
export * from './src/decorators';
export * from './src/def';
export * from './src/registrar';
export * from './src/registrar-aspect';
import { defaultRegistrar } from '../default-registrar';
import { defaultRegistrar } from '../registrar';
export const { Isolate, UsingService } = defaultRegistrar.scopeDecorators();
export const { UsePlugin } = defaultRegistrar.methodDecorators();
import { defaultRegistrar } from '../default-registrar';
import { defaultRegistrar } from '../registrar';
import { Context } from 'cordis';
import { PluginRegistrar } from '../def/plugin';
import { PluginRegistrar } from '../plugin-def';
const pluginDecorators = defaultRegistrar.pluginDecorators();
......
......@@ -44,3 +44,44 @@ export interface ControlType<
type: T;
condition: Condition<ControlTypeMap[T], any, [Record<string, 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 Prop<T> = T;
export type PartialDeep<T> = T extends
| string
| number
| bigint
| boolean
| null
| undefined
| symbol
| Date
// eslint-disable-next-line @typescript-eslint/ban-types
| Function
? T | undefined
: // Arrays, Sets and Maps and their readonly counterparts have their items made
// deeply partial, but their own instances are left untouched
T extends Array<infer ArrayType>
? Array<PartialDeep<ArrayType>>
: T extends ReadonlyArray<infer ArrayType>
? ReadonlyArray<ArrayType>
: T extends Set<infer SetType>
? Set<PartialDeep<SetType>>
: T extends ReadonlySet<infer SetType>
? ReadonlySet<SetType>
: T extends Map<infer KeyType, infer ValueType>
? Map<PartialDeep<KeyType>, PartialDeep<ValueType>>
: T extends ReadonlyMap<infer KeyType, infer ValueType>
? ReadonlyMap<PartialDeep<KeyType>, PartialDeep<ValueType>>
: // ...and finally, all other objects.
{
[K in keyof T]?: PartialDeep<T[K]>;
};
import { Registrar } from './registrar';
import { Context } from 'cordis';
export const defaultRegistrar = new Registrar(Context);
import { Context, Plugin } from 'cordis';
import Schema from 'schemastery';
import { ClassType } from 'schemastery-gen';
import { Registrar } from '../registrar';
import type { Registrar } from '../registrar';
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace PluginRegistrar {
type Dict<T, K extends keyof any = string> = { [key in K]?: T };
export type PluginClass<Ctx extends Context, C = any, P = any> = new (
ctx: Ctx,
config: C,
......@@ -49,6 +50,32 @@ export namespace PluginRegistrar {
export type PluginDefinition<Ctx extends Context, T extends Plugin = any> =
| PluginDefinitionExact<Ctx, T>
| PluginDefinitionName;
export type ClassPluginConfig<
Ctx extends Context,
P extends PluginClass<Ctx>,
> = P extends PluginClass<infer C> ? C : never;
export type ExactClassPluginConfig<
Ctx extends Context,
P extends PluginClass<Ctx>,
> = P extends PluginClass<Ctx, any, { config: infer IC }>
? IC
: ClassPluginConfig<Ctx, P>;
export type MapPluginToConfig<
Ctx extends Context,
M extends Dict<PluginClass<Ctx>>,
> = {
[K in keyof M]: ClassPluginConfig<Ctx, M[K]>;
};
export type MapPluginToConfigWithSelection<
Ctx extends Context,
M extends Dict<PluginClass<Ctx>>,
> = {
[K in keyof M]: ClassPluginConfig<Ctx, M[K]> & Selection;
};
}
export function PluginDef<Ctx extends Context>(
......
import { PluginSchema } from './decorators';
import { AnyClass, ClassType, Mixin } from 'schemastery-gen';
import { Context } from 'cordis';
import { PluginRegistrar } from './plugin-def';
import PluginClass = PluginRegistrar.PluginClass;
import { PartialDeep } from './def';
export function CreatePluginFactory<
Ctx extends Context,
C,
IC,
P extends { config: IC },
>(
basePlugin: PluginClass<Ctx, C, P>,
baseConfig: ClassType<IC>,
): <S>(specificConfig?: ClassType<S>) => new (
ctx: Context,
config: PartialDeep<S> & C,
) => P & {
config: IC & S;
};
export function CreatePluginFactory<Ctx extends Context>(
basePlugin: PluginClass<Ctx>,
baseConfig: AnyClass,
) {
return (specificConfig: AnyClass) => {
const plugin = class specificPlugin extends basePlugin {};
const config = specificConfig
? Mixin(specificConfig, baseConfig)
: baseConfig;
PluginSchema(config)(plugin);
return plugin;
};
}
......@@ -3,7 +3,14 @@ import { Registrar } from './registrar';
import { generateRenderer, renderObject } from './utility/render-object';
import { extractObjectMethod } from './utility/utility';
import { ControlType } from './def';
import { from, Observable, ObservableInput, ObservedValueOf, of } from 'rxjs';
import {
from,
mergeMap,
Observable,
ObservableInput,
ObservedValueOf,
of,
} from 'rxjs';
import _ from 'lodash';
type RecursiveUnwrapObservable<T> = T extends ObservableInput<any>
......@@ -72,12 +79,12 @@ export class RegistrarAspect<Ctx extends Context, T = any> {
switch (control.type) {
case 'if':
if (!(control as ControlType<'if'>).condition(this, existing))
if (!(control as ControlType<'if'>).condition(this.obj, existing))
return [];
return this.registerWithLoopControl(ctx, key, rest, existing);
case 'for':
return Array.from(
(control as ControlType<'for'>).condition(this, existing),
(control as ControlType<'for'>).condition(this.obj, existing),
).flatMap((item) =>
this.registerWithLoopControl(ctx, key, rest, {
...existing,
......@@ -91,20 +98,19 @@ export class RegistrarAspect<Ctx extends Context, T = any> {
ctx: Ctx,
cb: Registrar.ContextFunction<Ctx, R>,
layers: Registrar.ContextCallbackLayer<Ctx>[],
): Observable<RecursiveUnwrapObservable<R>> {
): Observable<ObservedValueOf<R>> {
const rest = [...layers];
const layer = rest.pop();
if (!layer) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return of(cb(ctx));
return from(cb(ctx));
}
return new Observable((subscriber) => {
layer(ctx, async (nextCtx) => {
if (!rest.length) {
const result = cb(nextCtx);
if (result) {
from(result).subscribe({
const tmpObs = from(result);
tmpObs.subscribe({
next: (v) => subscriber.next(v),
// no error
// no complete
......@@ -147,7 +153,7 @@ export class RegistrarAspect<Ctx extends Context, T = any> {
const keys = this.getAllFieldsToRegister();
return this.runLayers(ctx, (innerCtx) =>
keys.map((key) => this.registerFor(innerCtx, key)),
);
).pipe(mergeMap((v) => v));
}
performTopActions(
......
......@@ -2,19 +2,20 @@ import 'reflect-metadata';
import { Context, Events, Fork } from 'cordis';
import { ClassType, SchemaClass } from 'schemastery-gen';
import { MetadataSetter, Reflector } from 'typed-reflector';
import {
import type {
Awaitable,
Condition,
ControlType,
FunctionParam,
FunctionReturn,
ParamRenderer,
PartialDeep,
PickEventFunction,
TypedMethodDecorator,
} from './def';
import { RegistrarAspect } from './registrar-aspect';
import Schema from 'schemastery';
import { PluginRegistrar } from './def/plugin';
import { PluginRegistrar } from './plugin-def';
import _ from 'lodash';
declare module 'cordis' {
......@@ -28,6 +29,7 @@ export namespace Registrar {
export interface Methods<Ctx extends Context> {
on(event: keyof Events<Ctx>, prepend?: boolean): () => boolean;
plugin(): Awaitable<Fork<Ctx>>;
apply(): Awaitable<any>;
}
export interface MethodLimitations<Ctx extends Context> {
on: PickEventFunction<Events<Ctx>>;
......@@ -404,6 +406,26 @@ export class Registrar<Ctx extends Context> {
this._initializePluginClass();
}
};
Object.defineProperty(newClass, 'name', {
enumerable: true,
configurable: true,
get: () => {
const nameFromMeta = reflector.get(
'CordisPluginPredefineName',
newClass,
);
if (nameFromMeta) {
return nameFromMeta;
}
const nameFromFork = _this.getFork(newClass)?.name;
if (nameFromFork) {
return nameFromFork;
}
return originalClass.name;
},
});
newClass[ThirdEyeSym] = true;
return newClass;
};
}
......@@ -429,6 +451,7 @@ export class Registrar<Ctx extends Context> {
return register(result);
}
}),
Apply: this.decorateMethod('apply', (ctx, fun) => fun()),
};
}
......@@ -550,4 +573,23 @@ export class Registrar<Ctx extends Context> {
}
return targetCtx;
}
starterPluginFactory() {
return <C>(config?: ClassType<C>) => {
const plugin = class StarterPluginBase extends BasePlugin<Ctx, C> {};
if (config) {
this.pluginDecorators().PluginSchema(config)(plugin);
}
return plugin;
};
}
}
export const defaultRegistrar = new Registrar(Context);
export class BasePlugin<Ctx extends Context, C, PC = PartialDeep<C>> {
constructor(public ctx: Ctx, config: PC) {}
@defaultRegistrar.pluginDecorators().InjectConfig()
config: C;
}
import { PluginRegistrar } from '../plugin-def';
import { Context } from 'cordis';
import { ClassType } from 'schemastery-gen';
import { BasePlugin, defaultRegistrar } from '../registrar';
export function getPluginSchema<P extends PluginRegistrar.PluginClass<Context>>(
plugin: P,
): ClassType<
P extends BasePlugin<Context, any, infer PC>
? PC
: P extends PluginRegistrar.PluginClass<Context, infer C>
? C
: never
> {
return (
plugin['Config'] ||
plugin['schema'] ||
defaultRegistrar.reflector.get('CordisPluginPredefineSchema', plugin)
);
}
import { Caller, Provide } from '../src/decorators';
import { DefinePlugin, StarterPlugin } from './utility/decorators';
import { Context } from 'cordis';
@Provide('callerTester')
@DefinePlugin()
class CallerTester extends StarterPlugin() {
@Caller()
caller: string;
}
declare module 'cordis' {
// eslint-disable-next-line @typescript-eslint/no-namespace
interface Context {
callerTester: CallerTester;
}
}
describe('Caller', () => {
let app: Context;
beforeEach(async () => {
app = new Context();
app.plugin(CallerTester);
await app.start();
});
it('should put caller with correct values', async () => {
const ctx1 = app.extend();
const ctx2 = app.extend();
const caller1 = ctx1.callerTester.caller;
const caller2 = ctx2.callerTester.caller;
expect(caller1).toEqual(ctx1);
expect(caller2).toEqual(ctx2);
expect(app.callerTester.caller).toEqual(app);
});
});
import { DefinePlugin, StarterPlugin, UseEvent } from './utility/decorators';
import { SchemaProperty } from 'schemastery-gen';
import { If } from '../src/decorators';
import { Context } from 'cordis';
class MyConfig {
@SchemaProperty()
foo: boolean;
}
declare module 'cordis' {
interface Events<C> {
foo(): string;
}
}
@DefinePlugin()
class MyPlugin extends StarterPlugin(MyConfig) {
@If<MyPlugin>((o) => o.config.foo)
@UseEvent('foo')
onFoo() {
return 'bar';
}
}
describe('Caller', () => {
let app: Context;
beforeEach(async () => {
app = new Context();
await app.start();
});
it('register with true statement', async () => {
app.plugin(MyPlugin, { foo: true });
expect(app.bail('foo')).toBe('bar');
});
it('register with false statement', async () => {
app.plugin(MyPlugin, { foo: false });
expect(app.bail('foo')).toBeUndefined();
});
});
import { Inject, PluginName, UsingService, Fork } from '../src/decorators';
import { RegisterSchema, SchemaProperty } from 'schemastery-gen';
import { DefinePlugin, StarterPlugin } from './utility/decorators';
@RegisterSchema()
class Config {
@SchemaProperty()
foo: string;
}
describe('InjectUsing', () => {
@DefinePlugin()
@UsingService('foo')
class MyFork extends StarterPlugin(Config) {}
@PluginName('foo-plugin')
@UsingService('router')
@DefinePlugin({ using: ['database'], schema: Config })
@UsingService('http')
@Fork(MyFork)
class MyPlugin extends StarterPlugin(Config) {
@Inject(true)
cache: any;
@Inject('assets', true)
assets: any;
@Inject('bots')
bots: any[];
}
it('Should include injected using services', () => {
expect(MyPlugin.name).toBe('foo-plugin');
expect(MyPlugin['Config']).toEqual(Config);
const usingList = (MyPlugin as any).using as string[];
expect(usingList).toBeInstanceOf(Array);
expect(usingList.length).toEqual(6);
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);
expect(usingList.includes('foo')).toEqual(true);
});
});
import { PluginDef } from '../src/plugin-def';
import { Provide, UsePlugin } from '../src/decorators';
import { DefinePlugin, StarterPlugin } from './utility/decorators';
import { Context } from 'cordis';
@Provide('inner')
@DefinePlugin()
class MyInnerPlugin extends StarterPlugin() {}
@DefinePlugin()
class MyPlugin extends StarterPlugin() {
@UsePlugin()
loadOnebot() {
return PluginDef(MyInnerPlugin);
}
}
@DefinePlugin()
class MyLazyPlugin extends StarterPlugin() {
@UsePlugin()
async loadOnebot() {
await new Promise((resolve) => setTimeout(resolve, 1000));
return PluginDef(MyInnerPlugin);
}
}
describe('Inner plugin', () => {
let app: Context;
it('should load inner plugin properly', async () => {
app = new Context();
await app.start();
expect(app['inner']).toBeUndefined();
app.plugin(MyPlugin);
expect(app['inner']).toBeInstanceOf(MyInnerPlugin);
});
it('should wait for plugin to load', async () => {
app = new Context();
app.plugin(MyLazyPlugin);
expect(app['inner']).toBeUndefined();
await app.start();
expect(app['inner']).toBeInstanceOf(MyInnerPlugin);
});
});
import { Inject, Provide } from '../src/decorators';
import { DefinePlugin, StarterPlugin } from './utility/decorators';
import { LifecycleEvents } from '../src/plugin-def';
import { Context } from 'cordis';
declare module 'cordis' {
// eslint-disable-next-line @typescript-eslint/no-namespace
interface Context {
immediateDependency: ImmediateDependency;
nonImmediateDependency: NonImmediateDependency;
myPlugin: TestingBase;
}
}
@Provide('immediateDependency', { immediate: true })
@DefinePlugin()
class ImmediateDependency extends StarterPlugin() {}
@Provide('nonImmediateDependency')
@DefinePlugin()
class NonImmediateDependency extends StarterPlugin() {}
@Provide('myPlugin', { immediate: true })
@DefinePlugin()
class TestingBase extends StarterPlugin() implements LifecycleEvents {
onApply() {
this.applied = true;
}
onConnect() {
this.connected = true;
}
onDisconnect() {
this.disconnected = true;
}
applied = false;
connected = false;
disconnected = false;
}
class MyPlugin extends TestingBase {}
class MyPlugin2 extends TestingBase {
@Inject(true)
immediateDependency: ImmediateDependency;
}
class MyPlugin3 extends TestingBase {
@Inject(true)
nonImmediateDependency: NonImmediateDependency;
}
async function RunApplyTest(app: Context, plugin: any) {
app.plugin(plugin);
await app.start();
const myPlugin = app.myPlugin;
expect(myPlugin.applied).toBe(true);
expect(myPlugin.connected).toBe(true);
expect(myPlugin.disconnected).toBe(false);
app.dispose(plugin);
expect(myPlugin.disconnected).toBe(true);
expect(app.immediateDependency).toBeDefined();
expect(app.nonImmediateDependency).toBeDefined();
await app.stop();
}
describe('Apply and Connect in koishi-thirdeye', () => {
let app: Context;
beforeEach(() => {
app = new Context();
app.plugin(ImmediateDependency);
app.plugin(NonImmediateDependency);
});
it('should be applied and connected', async () => {
await RunApplyTest(app, MyPlugin);
});
it('should be applied and connected with immediate dependency', async () => {
await RunApplyTest(app, MyPlugin2);
});
it('should be applied and connected with non-immediate dependency', async () => {
await RunApplyTest(app, MyPlugin3);
});
});
import { SchemaProperty } from 'schemastery-gen';
import { CreatePluginFactory } from '../src/plugin-factory';
import { DefinePlugin, StarterPlugin, UseEvent } from './utility/decorators';
import { Context } from 'cordis';
class MessageConfig {
@SchemaProperty()
msg: string;
getMsg() {
return this.msg;
}
}
declare module 'cordis' {
interface Events {
foo1(): string;
foo2(): string;
}
}
class Base extends StarterPlugin(MessageConfig) {
@UseEvent('foo1')
onMessage() {
return this.config.getMsg();
}
}
const Factory = CreatePluginFactory(Base, MessageConfig);
class SpecificConfig {
@SchemaProperty()
msg2: string;
getMsg2() {
return this.msg2;
}
}
@DefinePlugin()
class SpecificPlugin extends Factory(SpecificConfig) {
@UseEvent('foo2')
onMessage2() {
return this.config.getMsg2();
}
}
describe('plugin factory', () => {
it('should register SpecificPlugin', async () => {
const app = new Context();
app.plugin(SpecificPlugin, { msg: 'hello', msg2: 'world' });
await app.start();
expect(app.bail('foo1')).toBe('hello');
expect(app.bail('foo2')).toBe('world');
});
});
describe('Sample test.', () => {
it('should pass', () => {
expect(true).toBe(true);
});
});
import { Inject, Provide, UsingService } from '../src/decorators';
import { DefinePlugin, StarterPlugin, UseEvent } from './utility/decorators';
import { Registrar } from '../src/registrar';
import { Context } from 'cordis';
declare module 'cordis' {
interface Context {
myProvider: MyProvider;
myEagerProvider: MyEagerProvider;
myConsumer: MyConsumer;
myUsingConsumer: MyUsingConsumer;
myPartialConsumer: MyPartialConsumer;
dummyProvider: any;
}
interface Events {
'pang'(message: string): Promise<string>;
'pong'(message: string): Promise<string>;
}
}
@Provide('myProvider')
@DefinePlugin()
class MyProvider extends StarterPlugin() {
ping() {
return 'pong';
}
dispose() {
return this.ctx.dispose();
}
}
@Provide('myEagerProvider', { immediate: true })
@DefinePlugin()
class MyEagerProvider extends StarterPlugin() {
ping() {
return 'pong eager';
}
dispose() {
return this.ctx.dispose();
}
}
@Provide('myConsumer', { immediate: true })
@DefinePlugin()
class MyConsumer {
@Inject()
myProvider: MyProvider;
@Inject()
myEagerProvider: MyEagerProvider;
pongResult: string;
eagerPongResult: string;
@UseEvent('internal/service')
async onService(name: string) {
if (name === 'myProvider') {
this.pongResult = this.myProvider.ping();
} else if (name === 'myEagerProvider') {
this.eagerPongResult = this.myEagerProvider.ping();
}
}
}
@Provide('myUsingConsumer', { immediate: true })
@DefinePlugin()
class MyUsingConsumer {
@Inject(true)
myProvider: MyProvider;
@Inject(true)
myEagerProvider: MyEagerProvider;
pongResult: string;
eagerPongResult: string;
@UseEvent('internal/service')
async onService(name: string) {
if (name === 'myProvider') {
this.pongResult = this.myProvider.ping();
} else if (name === 'myEagerProvider') {
this.eagerPongResult = this.myEagerProvider.ping();
}
}
emitResult: string;
}
@Provide('myPartialConsumer', { immediate: true })
@DefinePlugin()
class MyPartialConsumer {
@Inject()
dummyProvider: number;
pongResult: string;
@UsingService('dummyProvider')
@UseEvent('pang')
async onPang(content: string) {
const msg = `pang: ${content}`;
console.log(msg);
return msg;
}
@UseEvent('pong')
async onPong(content: string) {
const msg = `pong: ${content}`;
console.log(msg);
return msg;
}
}
describe('On service', () => {
let app: Context;
it('Should call service', async () => {
app = new Context();
app.plugin(MyProvider);
app.plugin(MyEagerProvider);
app.plugin(MyConsumer);
expect(app.myEagerProvider).toBeDefined();
expect(app.myEagerProvider.ping()).toBe('pong eager');
expect(app.myProvider).toBeUndefined();
await app.start();
expect(app.myConsumer).toBeDefined();
expect(app.myProvider).toBeDefined();
// expect(app.myConsumer.eagerPongResult).toBe('pong eager');
expect(app.myConsumer.pongResult).toBe('pong');
});
it('Should call service with using', async () => {
app = new Context();
app.plugin(MyUsingConsumer);
expect(app.myUsingConsumer).toBeUndefined();
app.plugin(MyProvider);
expect(app.myUsingConsumer).toBeUndefined();
app.plugin(MyEagerProvider);
expect(app.myUsingConsumer).toBeUndefined();
await app.start();
expect(app.myUsingConsumer).toBeDefined();
expect(app.myProvider).toBeDefined();
expect(app.myProvider.ping()).toBe('pong');
expect(app.myEagerProvider).toBeDefined();
expect(app.myEagerProvider.ping()).toBe('pong eager');
//expect(app.myUsingConsumer.eagerPongResult).toBe('pong eager');
//expect(app.myUsingConsumer.pongResult).toBe('pong');
});
it('Should handle partial using deps', async () => {
Context.service('dummyProvider');
app = new Context();
await app.start();
app.plugin(MyPartialConsumer);
expect(app.myPartialConsumer).toBeDefined();
expect(await app.parallel('pang', 'hello')).toEqual('hello');
expect(await app.parallel('pong', 'hello')).toEqual('pong: hello');
app.dummyProvider = { foo: 'bar' };
expect(await app.parallel('pang', 'hello')).toEqual('pang: hello');
expect(await app.parallel('pong', 'hello')).toEqual('pong: hello');
app.dummyProvider = undefined;
expect(await app.parallel('pang', 'hi')).toEqual('hi');
expect(await app.parallel('pong', 'hi')).toEqual('pong: hi');
app.dummyProvider = { foo: 'baz' };
expect(await app.parallel('pang', 'hi')).toEqual('pang: hi');
expect(await app.parallel('pong', 'hi')).toEqual('pong: hi');
});
});
import { defaultRegistrar } from '../../src/registrar';
export const { DefinePlugin } = defaultRegistrar.pluginDecorators();
export const { UseEvent } = defaultRegistrar.methodDecorators();
export const StarterPlugin = defaultRegistrar.starterPluginFactory();
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