Commit a48d0701 authored by nanahira's avatar nanahira

refas and remove lodash and satori-decorators

parent b0bb076c
export * from 'satori-decorators';
export * from 'cordis-decorators';
export * from './src/cosmotype-exports';
export * from './src/decorators';
export * from './src/def';
export * from './src/registrar';
export * from './src/reply-session';
export * from './src/utility/select-context';
export * from './src/plugin-operators';
This diff is collapsed.
......@@ -43,12 +43,15 @@
"rimraf": "^3.0.2",
"supertest": "^6.1.6",
"ts-jest": "^29.0.3",
"typescript": "^4.9.4"
"typescript": "^4.9.4",
"ws": "^8.12.0"
},
"dependencies": {
"minato-decorators": "^2.2.1",
"rxjs": "^7.5.6",
"satori-decorators": "^1.1.3"
"@types/koa": "^2.13.5",
"@types/koa__router": "^12.0.0",
"cordis-decorators": "^1.0.24",
"minato-decorators": "^2.2.2",
"rxjs": "^7.5.6"
},
"jest": {
"moduleFileExtensions": [
......@@ -69,6 +72,6 @@
"testEnvironment": "node"
},
"peerDependencies": {
"koishi": "^4.11.0"
"koishi": "^4.11.1"
}
}
......@@ -12,12 +12,24 @@ import {
} from '../utility/utility';
import { Argv, Command, Dict, FieldCollector, Session, User } from 'koishi';
import { applyNativeTypeToArg } from '../utility/native-type-mapping';
import { TypedMethodDecorator } from 'satori-decorators';
import { TypedMethodDecorator } from 'cordis-decorators';
export * from 'satori-decorators/dist/src/decorators/common';
export * from 'cordis-decorators/dist/src/decorators/common';
const methodDecorators = koishiRegistrar.methodDecorators();
export const {
OnAnywhere,
OnNowhere,
OnUser,
OnSelf,
OnGuild,
OnChannel,
OnPlatform,
OnPrivate,
OnSelection,
} = koishiRegistrar.selectorDecorators();
export const {
UseEvent,
UseBeforeEvent,
......
export * from 'satori-decorators/dist/src/decorators/http';
import { koishiRegistrar } from '../registrar';
const { RouterMethod } = koishiRegistrar.methodDecorators();
export const Get = (path: string) => RouterMethod('get', path);
export const Post = (path: string) => RouterMethod('post', path);
export const Put = (path: string) => RouterMethod('put', path);
export const Delete = (path: string) => RouterMethod('delete', path);
export const Patch = (path: string) => RouterMethod('patch', path);
export const Options = (path: string) => RouterMethod('options', path);
export const Head = (path: string) => RouterMethod('head', path);
export const All = (path: string) => RouterMethod('all', path);
export * from './common';
export * from './http';
export * from './plugin';
export * from 'cordis-decorators/dist/src/decorators';
......@@ -2,7 +2,7 @@ import { koishiRegistrar } from '../registrar';
import { ModelClassType, ModelRegistrar } from 'minato-decorators';
import { Flatten, Keys, Tables } from 'koishi';
export * from 'satori-decorators/dist/src/decorators/plugin';
export * from 'cordis-decorators/dist/src/decorators/plugin';
export const { DefinePlugin } = koishiRegistrar.pluginDecorators();
export const UseModel = koishiRegistrar.decorateTopLevelAction(
......
export * from './map-plugin';
export * from './merge-plugin';
export * from './multi-plugin';
import {
MappingPluginBase,
MapPluginToConfig,
MapPluginToConfigWithSelection,
} from './mapping-base';
import {
ClassType,
CreatePluginFactory,
getPluginSchema,
PluginRegistrar,
SchemaProperty,
} from 'cordis-decorators';
import PluginClass = PluginRegistrar.PluginClass;
import { Context, Dict } from 'koishi';
function MappedConfig<
Ctx extends Context,
M extends Dict<PluginClass<Context>>,
>(dict: M): ClassType<MapPluginToConfigWithSelection<Ctx, M>> {
const PropertySchema = class SpecificPropertySchema {} as ClassType<
MapPluginToConfigWithSelection<Ctx, M>
>;
for (const [key, plugin] of Object.entries(dict)) {
SchemaProperty({
type: getPluginSchema(plugin),
})(PropertySchema.prototype, key);
}
return PropertySchema;
}
export function MapPlugin<
Ctx extends Context,
M extends Dict<PluginClass<Ctx>>,
OuterConfig,
>(dict: M, outerConfig?: ClassType<OuterConfig>) {
const basePlugin = class SpecificMapPlugin extends MappingPluginBase<
Ctx,
M,
MapPluginToConfig<Ctx, M>,
Partial<MapPluginToConfigWithSelection<Ctx, M>>
> {
_getDict() {
return dict;
}
_getPluginConfig(key: keyof M): any {
return this.config[key];
}
};
const schema = MappedConfig(dict);
const factory = CreatePluginFactory(basePlugin, schema);
return factory(outerConfig);
}
import { BasePlugin, PartialDeep, PluginRegistrar } from 'cordis-decorators';
import { selectContext, Selection } from '../utility/select-context';
import PluginClass = PluginRegistrar.PluginClass;
import { Context, Dict } from 'koishi';
import { koishiRegistrar } from '../registrar';
import { ClonePlugin } from '../utility/clone-plugin';
import ClassPluginConfig = PluginRegistrar.ClassPluginConfig;
import { Apply } from 'cordis-decorators/dist/src/decorators';
export interface WithSelection {
$filter?: Selection;
}
export type MapPluginToConfig<
Ctx extends Context,
M extends Dict<PluginClass<Ctx>>,
> = {
[K in keyof M]: ClassPluginConfig<M[K]>;
};
export type MapPluginToConfigWithSelection<
Ctx extends Context,
M extends Dict<PluginClass<Ctx>>,
> = {
[K in keyof M]: ClassPluginConfig<M[K]> & WithSelection;
};
export class MappingPluginBase<
Ctx extends Context,
M extends Dict<PluginClass<Ctx>>,
C,
PC = PartialDeep<C>,
> extends BasePlugin<Ctx, C, PC> {
_getDict(): M {
throw new Error('not implemented');
}
_instanceMap = new Map<string, PluginClass<Ctx>>();
getInstance<K extends keyof M>(key: K): M[K] {
return this._instanceMap?.get(key as string) as M[K];
}
_getPluginConfig(key: keyof M): any {
return {};
}
@Apply()
_registerInstances() {
const dict = this._getDict();
for (const [key, plugin] of Object.entries(dict)) {
const config = this._getPluginConfig(key);
if (config == null) continue;
const ctx = config['$filter']
? selectContext(this.ctx, config.$filter)
: this.ctx;
const clonedPlugin = ClonePlugin(
plugin,
`${this.constructor.name}_${plugin.name}_dict_${key}`,
(o) => this._instanceMap.set(key, o),
);
ctx.plugin(clonedPlugin, config);
}
}
@(koishiRegistrar.methodDecorators().UseEvent('dispose'))
_onThingsDispose() {
delete this._instanceMap;
}
}
import { MappingPluginBase, MapPluginToConfig } from './mapping-base';
import {
ClassType,
CreatePluginFactory,
getPluginSchema,
Mixin,
PluginRegistrar,
} from 'cordis-decorators';
import PluginClass = PluginRegistrar.PluginClass;
import { Context, Dict } from 'koishi';
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (
x: infer R,
) => any
? R
: never;
type MergePluginConfig<
Ctx extends Context,
M extends Dict<PluginClass<Ctx>>,
> = UnionToIntersection<MapPluginToConfig<Ctx, M>[keyof M]>;
export function MergePlugin<
Ctx extends Context,
M extends Dict<PluginClass<Ctx>>,
OuterConfig,
>(dict: M, outerConfig?: ClassType<OuterConfig>) {
const basePlugin = class SpecificMapPlugin extends MappingPluginBase<
Ctx,
M,
MergePluginConfig<Ctx, M>,
MergePluginConfig<Ctx, M>
> {
_getDict() {
return dict;
}
_getPluginConfig(key: keyof M): any {
return this.config;
}
};
const schemas = Object.values(dict)
.map((plugin) => getPluginSchema(plugin))
.filter((v) => !!v);
const factory = CreatePluginFactory(basePlugin, Mixin(...schemas));
return factory(outerConfig);
}
import {
BasePlugin,
ClassType,
CreatePluginFactory,
PluginRegistrar,
SchemaClass,
SchemaProperty,
TypeFromClass,
} from 'cordis-decorators';
import { selectContext } from '../utility/select-context';
import { ClonePlugin } from '../utility/clone-plugin';
import PluginClass = PluginRegistrar.PluginClass;
import { koishiRegistrar } from '../registrar';
import { Context, Schema } from 'koishi';
import ClassPluginConfig = PluginRegistrar.ClassPluginConfig;
import { UsingService, Apply } from 'cordis-decorators/dist/src/decorators';
import { WithSelection } from './mapping-base';
export interface Instances<T> {
instances: T[];
}
export function ToInstancesConfig<Inner extends new (...args: any[]) => any>(
instanceConfig: Inner,
): new () => Instances<TypeFromClass<Inner>> {
const instanceConfigClass = class InstancesConfig {
instances: TypeFromClass<Inner>[];
};
SchemaProperty({
type: SchemaClass(instanceConfig),
default: [],
array: true,
})(instanceConfigClass.prototype, 'instances');
return instanceConfigClass;
}
export class MultiInstancePluginFramework<
Ctx extends Context,
InnerPlugin extends PluginClass<Ctx>,
> extends BasePlugin<
Ctx,
Instances<ClassPluginConfig<InnerPlugin>>,
Instances<ClassPluginConfig<InnerPlugin> & WithSelection>
> {
instances: TypeFromClass<InnerPlugin>[] = [];
_getInnerPlugin(): InnerPlugin {
throw new Error(`Not implemented`);
}
@Apply()
_registerInstances() {
const innerPlugin = this._getInnerPlugin();
for (let i = 0; i < this.config.instances.length; i++) {
const clonedInnerPlugin = ClonePlugin(
innerPlugin,
`${this.constructor.name}_${innerPlugin.name}_instance_${i}`,
(instance) => this.instances.push(instance),
);
const instanceConfig = this.config.instances[i];
const instanceContext = instanceConfig['$filter']
? selectContext(this.ctx, instanceConfig['$filter'])
: this.ctx;
instanceContext.plugin(clonedInnerPlugin, instanceConfig);
}
}
@(koishiRegistrar.methodDecorators().UseEvent('dispose'))
_onThingsDispose() {
delete this.instances;
}
}
export function MultiInstancePlugin<
Ctx extends Context,
InnerPlugin extends PluginClass<Ctx>,
OuterConfig,
>(innerPlugin: InnerPlugin, outerConfig?: ClassType<OuterConfig>) {
const basePlugin = class SpecificMultiInstancePlugin extends MultiInstancePluginFramework<
Ctx,
InnerPlugin
> {
_getInnerPlugin() {
return innerPlugin;
}
};
const schema = ToInstancesConfig(
(innerPlugin['Config'] ||
innerPlugin['schema'] ||
Schema.any()) as ClassType<ClassPluginConfig<InnerPlugin>>,
);
const factory = CreatePluginFactory(basePlugin, schema);
const plugin = factory(outerConfig);
if (innerPlugin['using']) {
UsingService(...(innerPlugin['using'] as string[]))(plugin);
}
return plugin;
}
import { RegisterMeta, SatoriRegistrar } from 'satori-decorators';
import { RegisterMeta, Registrar } from 'cordis-decorators';
import {
BeforeEventMap,
Command,
......@@ -16,10 +16,26 @@ import {
CommandReturnType,
CommandTransformer,
} from './def';
import { IncomingMessage } from 'http';
import {
DefaultContext,
DefaultState,
ParameterizedContext,
Next as KoaNext,
} from 'koa';
import { RouterParamContext } from '@koa/router';
import WebSocket from 'ws';
import { selectContext, Selection } from './utility/select-context';
export type KoaContext = ParameterizedContext<
DefaultState,
DefaultContext & RouterParamContext<DefaultState, DefaultContext>,
any
>;
type PutMeta = RegisterMeta<CommandPut, { pre?: RegisterMeta<CommandPutPre> }>;
declare module 'satori-decorators' {
declare module 'cordis-decorators' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Registrar {
interface MetadataMap {
......@@ -35,7 +51,7 @@ declare module 'satori-decorators' {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export class KoishiRegistrar extends SatoriRegistrar<Context> {
export class KoishiRegistrar extends Registrar<Context> {
decorateCommandTransformer<A extends any[]>(
transformer: CommandTransformer<A>,
) {
......@@ -70,6 +86,34 @@ export class KoishiRegistrar extends SatoriRegistrar<Context> {
override methodDecorators() {
return {
...super.methodDecorators(),
RouterMethod: this.decorateMethod(
'route',
(
{ ctx },
fun: (ctx: KoaContext, Next: KoaNext) => Promise<any>,
method:
| 'get'
| 'post'
| 'put'
| 'delete'
| 'patch'
| 'options'
| 'head'
| 'all',
path: string,
) => {
const _path = path.startsWith('/') ? path : `/${path}`;
return ctx.router[method](_path, (koaCtx, next) => fun(koaCtx, next));
},
),
Ws: this.decorateMethod(
'ws',
(
{ ctx },
action: (socket: WebSocket, request: IncomingMessage) => any,
path: string,
) => ctx.router.ws(path.startsWith('/') ? path : `/${path}`, action),
),
UseMiddleware: this.decorateMethod(
'middleware',
(
......@@ -180,6 +224,34 @@ export class KoishiRegistrar extends SatoriRegistrar<Context> {
),
};
}
selectorDecorators() {
return {
OnAnywhere: this.decorateTransformer((ctx) => ctx.any()),
OnNowhere: this.decorateTransformer((ctx) => ctx.never()),
OnUser: this.decorateTransformer((ctx, ...values: string[]) =>
ctx.user(...values),
),
OnSelf: this.decorateTransformer((ctx, ...values: string[]) =>
ctx.self(...values),
),
OnGuild: this.decorateTransformer((ctx, ...values: string[]) =>
ctx.guild(...values),
),
OnChannel: this.decorateTransformer((ctx, ...values: string[]) =>
ctx.channel(...values),
),
OnPlatform: this.decorateTransformer((ctx, ...values: string[]) =>
ctx.platform(...values),
),
OnPrivate: this.decorateTransformer((ctx, ...values: string[]) =>
ctx.private(...values),
),
OnSelection: this.decorateTransformer((ctx, selection: Selection) =>
selectContext(ctx, selection),
),
};
}
}
export const koishiRegistrar = new KoishiRegistrar(Context);
......
import { TypeFromClass } from 'cordis-decorators';
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 { Context, makeArray, MaybeArray } from 'koishi';
const selectors = [
'user',
'guild',
'channel',
'self',
'private',
'platform',
] as const;
export type SelectorType = typeof selectors[number];
export type SelectorValue = boolean | MaybeArray<string | number>;
export type BaseSelection = { [K in SelectorType]?: SelectorValue };
export interface Selection extends BaseSelection {
and?: Selection[];
or?: Selection[];
not?: Selection;
}
export function selectContext<Ctx extends Context>(
root: Ctx,
options: Selection,
) {
let ctx = root;
// basic selectors
for (const type of selectors) {
const value = options[type];
if (value === true) {
ctx = ctx[type]();
} else if (value === false) {
ctx = ctx.exclude(ctx[type]());
} else if (value !== undefined) {
// we turn everything into string
ctx = ctx[type](...makeArray(value).map((item) => '' + item));
}
}
// intersect
if (options.and) {
for (const selection of options.and) {
ctx = ctx.intersect(selectContext<Ctx>(root, selection));
}
}
// union
if (options.or) {
let ctx2: Context = ctx.never();
for (const selection of options.or) {
ctx2 = ctx2.union(selectContext<Ctx>(root, selection));
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
ctx = ctx.intersect(ctx2);
}
// exclude
if (options.not) {
ctx = ctx.exclude(selectContext<Ctx>(root, options.not));
}
return ctx;
}
import { App } from 'koishi';
import { MapPlugin, MergePlugin } from '../src/plugin-operators';
import { SchemaProperty } from 'cordis-decorators';
import { DefinePlugin, UseEvent } from '../src/decorators';
import { StarterPlugin } from '../src/registrar';
declare module 'cordis' {
interface Events {
dressColor(): string;
skirtSize(): string;
wearingStrip(): string;
}
}
class DressConfig {
@SchemaProperty()
color: string;
}
@DefinePlugin()
class DressPlugin extends StarterPlugin(DressConfig) {
@UseEvent('dressColor')
dressColor() {
return this.config.color;
}
}
class SkirtConfig {
@SchemaProperty({ default: 'S' })
size: string;
}
@DefinePlugin()
class SkirtPlugin extends StarterPlugin(SkirtConfig) {
@UseEvent('skirtSize')
skirtSize() {
return this.config.size;
}
}
class WearingConfig {
@SchemaProperty()
strip: string;
}
@DefinePlugin()
class WearingPlugin extends MapPlugin(
{ dress: DressPlugin, skirt: SkirtPlugin },
WearingConfig,
) {
@UseEvent('wearingStrip')
wearingStrip() {
return this.config.strip;
}
}
@DefinePlugin()
class MergedWearingPlugin extends MergePlugin(
{ dress: DressPlugin, skirt: SkirtPlugin },
WearingConfig,
) {
@UseEvent('wearingStrip')
wearingStrip() {
return this.config.strip;
}
}
describe('register map plugin instance', () => {
it('should work on each level', async () => {
const app = new App();
app.plugin(WearingPlugin, {
dress: { color: 'red' },
skirt: { size: 'XL' },
strip: 'pink',
});
await app.start();
expect(app.bail('dressColor')).toBe('red');
expect(app.bail('skirtSize')).toBe('XL');
expect(app.bail('wearingStrip')).toBe('pink');
});
it('should partial register', async () => {
const app = new App();
app.plugin(WearingPlugin, {
dress: { color: 'red' },
strip: 'pink',
});
await app.start();
expect(app.bail('dressColor')).toBe('red');
expect(app.bail('wearingStrip')).toBe('pink');
expect(app.bail('skirtSize')).toBe('S');
});
it('should work on merge plugin', async () => {
const app = new App();
app.plugin(MergedWearingPlugin, {
color: 'red',
size: 'XL',
strip: 'pink',
});
await app.start();
expect(app.bail('dressColor')).toBe('red');
expect(app.bail('skirtSize')).toBe('XL');
expect(app.bail('wearingStrip')).toBe('pink');
});
});
import { RegisterSchema, SchemaProperty } from 'cordis-decorators';
import { MultiInstancePlugin } from '../src/plugin-operators';
import { App, Schema } from 'koishi';
import { StarterPlugin } from '../src/registrar';
import { DefinePlugin, UseEvent } from '../src/decorators';
declare module 'cordis' {
interface Events<C> {
message1: string;
message2: string;
message3: string;
}
}
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 StarterPlugin(InnerMessageConfig) {
@UseEvent('message1')
onMessage() {
return this.config.getMsg();
}
}
@DefinePlugin({ schema: Schema.object({ msg: Schema.string() }) })
class Inner2 extends StarterPlugin(InnerMessageConfig) {
@UseEvent('message1')
onMessage() {
return this.config.msg;
}
}
@DefinePlugin()
class Outer extends MultiInstancePlugin(Inner, OuterMessageConfig) {
@UseEvent('message2')
onMessage() {
return this.config.getMsg();
}
@UseEvent('message3')
onInnerMessage() {
return this.instances[0].config.getMsg();
}
}
@DefinePlugin()
class Outer2 extends MultiInstancePlugin(Inner2, OuterMessageConfig) {
@UseEvent('message2')
onMessage() {
return this.config.getMsg();
}
@UseEvent('message3')
onInnerMessage() {
return this.instances[0].config.msg;
}
}
describe('register multi plugin instance', () => {
it('should work on schemastery-gen', async () => {
const app = new App();
app.plugin(Outer, { msg: 'hello', instances: [{ msg: 'world' }] });
await app.start();
expect(app.bail('message1')).toBe('world');
expect(app.bail('message2')).toBe('hello');
expect(app.bail('message3')).toBe('world');
});
it('should work on common schemastery', async () => {
const app = new App();
app.plugin(Outer2, { msg: 'hello', instances: [{ msg: 'world' }] });
await app.start();
expect(app.bail('message1')).toBe('world');
expect(app.bail('message2')).toBe('hello');
expect(app.bail('message3')).toBe('world');
});
});
import { OnGuild, OnPlatform } from '../src/decorators';
import { App, Session } from 'koishi';
import { koishiRegistrar } from '../src/registrar';
@OnPlatform('discord')
class MyClass {
@OnGuild('1111111111')
foo() {}
}
describe('Scope', () => {
let app: App;
beforeEach(async () => {
app = new App();
await app.start();
});
it('should check scope', () => {
const correctSession = {
guildId: '1111111111',
platform: 'discord',
} as Session;
const wrongSession1 = {
guildId: '2222222222',
platform: 'discord',
} as Session;
const wrongSession2 = {
guildId: '1111111111',
platform: 'telegram',
} as Session;
const registrar = koishiRegistrar.aspect(new MyClass());
const globalCtx = registrar.getScopeContext(app);
const methodCtx = registrar.getScopeContext(app, 'foo', {}, true);
expect(globalCtx.filter(correctSession)).toBe(true);
expect(globalCtx.filter(wrongSession1)).toBe(true);
expect(globalCtx.filter(wrongSession2)).toBe(false);
expect(methodCtx.filter(correctSession)).toBe(true);
expect(methodCtx.filter(wrongSession1)).toBe(false);
expect(methodCtx.filter(wrongSession2)).toBe(false);
});
});
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