Commit 6c959cf8 authored by nanahira's avatar nanahira

Merge branch 'put-type'

parents 5fddfa16 50d2d8d6
{
"name": "koishi-decorators",
"version": "1.2.8",
"version": "1.2.9",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "koishi-decorators",
"version": "1.2.8",
"version": "1.2.9",
"license": "MIT",
"dependencies": {
"@types/koa": "^2.13.4",
......
{
"name": "koishi-decorators",
"description": "Decorator defs for Koishi, especially for thirdeye and nestjs.",
"version": "1.2.8",
"version": "1.2.9",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
......
......@@ -60,11 +60,18 @@ export function UseCommand(
const putOptions: CommandPut.Config<keyof CommandPut.ConfigMap>[] =
Reflect.getMetadata(KoishiCommandPutDef, obj.constructor, key) ||
undefined;
// eslint-disable-next-line @typescript-eslint/ban-types
const paramTypes: Function[] = Reflect.getMetadata(
'design:paramtypes',
obj,
key,
);
const metadataDec = DoRegister.command({
def,
desc,
config,
putOptions,
paramTypes,
});
return metadataDec(obj, key, des);
};
......@@ -177,7 +184,8 @@ export const PutArgv = (field?: keyof Argv) =>
field ? CommandPut.decorate('argvField', field) : CommandPut.decorate('argv');
export const PutSession = (field?: keyof Session) =>
field ? CommandPut.decorate('sessionField', field) : PutArgv('session');
export const PutArg = (i: number) => CommandPut.decorate('arg', i);
export const PutArg = (index: number, decl?: Argv.Declaration) =>
CommandPut.decorate('arg', { index, decl });
export const PutArgs = () => CommandPut.decorate('args');
export const PutOption = (
name: string,
......@@ -213,6 +221,7 @@ export const PutTemplate = (name: string, text: string | Dict<string>) =>
name,
text: adaptLocaleDict(text),
});
export const PutObject = () => CommandPut.decorate('typeClass');
export const TopLevelAction = (action: TopLevelActionDef): ClassDecorator =>
Metadata.append('KoishiTopLevelAction', action);
......
// metadatas
import {
CommandDefinitionFun,
MappingStruct,
OnContextFunction,
TopLevelActionDef,
} from './interfaces';
import { DoRegister } from '../registry';
import { CommandPut, DoRegister } from '../registry';
export const KoishiOnContextScope = 'KoishiOnContextScope';
export const KoishiDoRegister = 'KoishiDoRegister';
......@@ -19,8 +20,13 @@ export interface MetadataArrayMap {
KoishiCommandDefinition: CommandDefinitionFun;
KoishiDoRegisterKeys: string;
KoishiTopLevelAction: TopLevelActionDef;
KoishiPutClassFieldKeys: string;
}
export interface MetadataMap {
KoishiDoRegister: DoRegister.Config;
KoishiPutClassField: MappingStruct<
CommandPut.ConfigMap,
keyof CommandPut.ConfigMap
>;
}
......@@ -103,6 +103,8 @@ export interface CommandRegisterConfig<D extends string = string> {
desc?: string;
config?: CommandConfigExtended;
putOptions?: CommandPut.Config[];
// eslint-disable-next-line @typescript-eslint/ban-types
paramTypes: Function[];
}
export interface CommandConfigExtended extends Command.Config {
......@@ -157,3 +159,8 @@ export interface CommandLocaleDef extends Store {
options?: Dict<string>;
messages?: Store;
}
export interface CommandArgDef {
index: number;
decl?: Argv.Declaration;
}
......@@ -32,6 +32,9 @@ export class MethodRegistry<
EXT extends any[] = [],
> extends AbstractRegistry<MethodMap<M, R, EXT>> {
execute<K extends keyof M>(info: MappingStruct<M, K>, ...ext: EXT) {
if (!info) {
return;
}
const fun = this.get(info.type);
if (!fun) {
return;
......
import { Argv, Command, Context, FieldCollector, Session, User } from 'koishi';
import {
CommandArgDef,
CommandOptionConfig,
GenerateMappingStruct,
KoishiCommandPutDef,
......@@ -7,13 +8,19 @@ import {
TemplateConfig,
} from '../../def';
import { MethodRegistry } from '../abstract-registry';
import { applyOptionToCommand, registerTemplate } from '../../utility';
import {
applyNativeTypeToArg,
applyOptionToCommand,
registerTemplate,
} from '../../utility';
import { Metadata } from '../../meta/metadata.decorators';
import { reflector } from '../../meta/meta-fetch';
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace CommandPut {
export interface ConfigMap {
args: void;
arg: number;
arg: CommandArgDef;
argv: void;
argvField: keyof Argv;
option: CommandOptionConfig;
......@@ -24,6 +31,7 @@ export namespace CommandPut {
sessionField: keyof Session;
renderer: string | undefined;
template: TemplateConfig;
typeClass: void;
}
export type Config<K extends keyof ConfigMap = keyof ConfigMap> =
......@@ -32,13 +40,26 @@ export namespace CommandPut {
export const preRegistry = new MethodRegistry<
ConfigMap,
void,
[Command, Context]
// eslint-disable-next-line @typescript-eslint/ban-types
[Command, Context, Function]
>();
preRegistry.extend('option', (data, cmd, ctx) =>
applyOptionToCommand(ctx, cmd, data),
preRegistry.extend('option', (data, cmd, ctx, nativeType) =>
applyOptionToCommand(ctx, cmd, data, nativeType),
);
preRegistry.extend('arg', (data, cmd, ctx, nativeType) => {
let arg = cmd._arguments[data.index];
if (!arg) {
arg = {};
cmd._arguments[data.index] = arg;
}
applyNativeTypeToArg(arg, nativeType);
if (data.decl) {
Object.assign(arg, data.decl);
}
});
preRegistry.extend('user', (data, cmd) => {
if (data) {
cmd.userFields(data);
......@@ -61,10 +82,29 @@ export namespace CommandPut {
registerTemplate(data, ctx, cmd),
);
export const registry = new MethodRegistry<ConfigMap, any, [Argv, any[]]>();
preRegistry.extend('typeClass', (data, cmd, ctx, nativeType) => {
const keys = reflector.getArray('KoishiPutClassFieldKeys', nativeType);
for (const key of keys) {
const meta = reflector.get('KoishiPutClassField', nativeType, key);
if (!meta) continue;
const propertyNativeType = Reflect.getMetadata(
'design:type',
nativeType.prototype,
key,
);
preRegistry.execute(meta, cmd, ctx, propertyNativeType);
}
});
export const registry = new MethodRegistry<
ConfigMap,
any,
// eslint-disable-next-line @typescript-eslint/ban-types
[Argv, any[], Function]
>();
registry.extend('args', (data, argv, args) => args);
registry.extend('arg', (data, argv, args) => args[data]);
registry.extend('arg', (data, argv, args) => args[data.index]);
registry.extend('argv', (data, argv, args) => argv);
registry.extend('argvField', (data, argv, args) => argv[data]);
registry.extend('option', (data, argv, args) => argv.options[data.name]);
......@@ -99,17 +139,47 @@ export namespace CommandPut {
(data, argv, args) => (params: object) =>
argv.session.text(`.${data.name}`, params),
);
registry.extend(
'typeClass',
(data, argv, args, nativeType: { new (): any }) => {
const keys = reflector.getArray('KoishiPutClassFieldKeys', nativeType);
const obj = new nativeType();
for (const key of keys) {
const meta = reflector.get('KoishiPutClassField', nativeType, key);
if (!meta) continue;
const propertyNativeType = Reflect.getMetadata(
'design:type',
nativeType.prototype,
key,
);
obj[key] = registry.execute(meta, argv, args, propertyNativeType);
}
return obj;
},
);
export function decorate<T extends keyof ConfigMap>(
type: T,
data?: ConfigMap[T],
): ParameterDecorator {
return (obj, key: string, index) => {
const objClass = obj.constructor;
const list: Config<T>[] =
Reflect.getMetadata(KoishiCommandPutDef, objClass, key) || [];
list[index] = GenerateMappingStruct(type, data);
Reflect.defineMetadata(KoishiCommandPutDef, list, objClass, key);
): ParameterDecorator & PropertyDecorator {
return (obj, key: string, index?: number) => {
const def = GenerateMappingStruct(type, data);
if (typeof index === 'number') {
// As a parameter decorator
const objClass = obj.constructor;
const list: Config<T>[] =
Reflect.getMetadata(KoishiCommandPutDef, objClass, key) || [];
list[index] = def;
Reflect.defineMetadata(KoishiCommandPutDef, list, objClass, key);
} else {
// As a property decorator
Metadata.set(
'KoishiPutClassField',
def,
'KoishiPutClassFieldKeys',
)(obj, key);
}
return obj;
};
}
}
......@@ -133,12 +133,17 @@ export namespace DoRegister {
obj[key](argv, ...args),
);
} else {
for (const putOption of data.putOptions) {
CommandPut.preRegistry.execute(putOption, command, ctx);
for (let i = 0; i < data.putOptions.length; i++) {
const putOption = data.putOptions[i];
if (!putOption) {
continue;
}
const nativeType = data.paramTypes[i];
CommandPut.preRegistry.execute(putOption, command, ctx, nativeType);
}
command.action((argv: Argv, ...args: any[]) => {
const params = data.putOptions.map((o) =>
CommandPut.registry.execute(o, argv, args),
const params = data.putOptions.map((o, i) =>
CommandPut.registry.execute(o, argv, args, data.paramTypes[i]),
);
return obj[key](...params);
});
......
export * from './utility';
export * from './native-type-mapping';
import { Argv } from 'koishi';
// eslint-disable-next-line @typescript-eslint/ban-types
export const nativeTypeMapping = new Map<Function, Argv.Type>();
nativeTypeMapping.set(String, 'string');
nativeTypeMapping.set(Number, 'number');
nativeTypeMapping.set(Boolean, 'boolean');
nativeTypeMapping.set(Date, 'date');
export function applyNativeTypeToArg(
arg: Argv.Declaration,
// eslint-disable-next-line @typescript-eslint/ban-types
nativeType: Function,
) {
if (arg.type || !nativeType) {
return;
}
if (nativeTypeMapping.has(nativeType)) {
arg.type = nativeTypeMapping.get(nativeType);
}
}
......@@ -5,6 +5,7 @@ import {
OnContextFunction,
TemplateConfig,
} from '../def';
import { applyNativeTypeToArg } from './native-type-mapping';
export function applySelector(
ctx: Context,
......@@ -59,8 +60,11 @@ export function applyOptionToCommand(
ctx: Context,
cmd: Command,
def: CommandOptionConfig,
// eslint-disable-next-line @typescript-eslint/ban-types
nativeType?: Function,
) {
const { name, desc, config } = def;
const { name, config } = def;
const { desc } = def;
if (config?.description) {
const desc = adaptLocaleDict(config.description);
for (const [locale, text] of Object.entries(desc)) {
......@@ -69,5 +73,8 @@ export function applyOptionToCommand(
}
const clonedConfig = { ...(config || {}) };
delete clonedConfig.description;
return cmd.option(name, desc, clonedConfig);
cmd = cmd.option(name, desc, clonedConfig);
const option = cmd._options[name];
applyNativeTypeToArg(option, nativeType);
return cmd;
}
import {
CommandUsage,
PutArg,
PutObject,
PutOption,
UseCommand,
UseEvent,
UseMiddleware,
} from '../src/decorators/decorators';
} from '../src/decorators';
import { App, Command, Next, Session } from 'koishi';
import { Registrar } from '../src/register';
import { EventNameAndPrepend } from '../src/def';
class SkirtArg {
@PutArg(0)
count: number;
@PutOption('color', '-c <color>')
color: string;
}
class MyClass {
@UseMiddleware()
async onPing(session: Session, next: Next) {
......@@ -27,9 +37,19 @@ class MyClass {
@UseCommand('echo', 'hi')
@CommandUsage('foo')
async onEcho(@PutOption('content', '-c <content:string>') content: string) {
async onEcho(@PutOption('content', '-c <content>') content: string) {
return `bot: ${content}`;
}
@UseCommand('count')
async onCount(@PutArg(0) count: number) {
return `I have ${count} dresses.`;
}
@UseCommand('skirt')
async onSkirt(@PutObject() arg: SkirtArg) {
return `I have ${arg.count} ${arg.color} skirts.`;
}
}
const registrar = new Registrar(new MyClass());
......@@ -54,14 +74,34 @@ describe('Register', () => {
expect((result.data as EventNameAndPrepend).name).toBe('message');
});
it('should register command', () => {
it('should register command and infer option type', () => {
const result = registrar.register(app, 'onEcho');
expect(result.type).toBe('command');
const command: Command = result.result;
expect(command._usage).toBe('foo');
expect(command._options.content.name).toBe('content');
expect(command._options.content.type).toBe('string');
expect(command.execute({ options: { content: 'hello' } })).resolves.toBe(
'bot: hello',
);
});
it('should infer argument type', () => {
const result = registrar.register(app, 'onCount');
expect(result.type).toBe('command');
const command: Command = result.result;
expect(command._arguments[0].type).toBe('number');
expect(command.execute({ args: ['4'] })).resolves.toBe('I have 4 dresses.');
});
it('should work on class type', () => {
const result = registrar.register(app, 'onSkirt');
expect(result.type).toBe('command');
const command: Command = result.result;
expect(command._arguments[0].type).toBe('number');
expect(command._options.color.type).toBe('string');
expect(
command.execute({ args: ['4'], options: { color: 'red' } }),
).resolves.toBe('I have 4 red skirts.');
});
});
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