Commit b24eac2f authored by nanahira's avatar nanahira

add template rendering

parent 7d4b331d
This diff is collapsed.
......@@ -42,6 +42,7 @@
"@koishijs/plugin-adapter-onebot": "^4.1.5",
"@types/jest": "^27.4.0",
"@types/lodash": "^4.14.178",
"@types/mustache": "^4.1.2",
"@types/node": "^17.0.10",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^4.33.0",
......@@ -60,10 +61,11 @@
"@types/koa": "^2.13.4",
"@types/koa__router": "^8.0.11",
"lodash": "^4.17.21",
"mustache": "^4.2.0",
"reflect-metadata": "^0.1.13",
"typed-reflector": "^1.0.10"
},
"peerDependencies": {
"koishi": "^4.6.0"
"koishi": "^4.7.0"
}
}
......@@ -123,17 +123,17 @@ export const CommandDef = (
export const CommandUse = <T extends Command, R extends any[]>(
callback: (command: Command, ...args: R) => T,
...args: R
) => CommandDef((cmd) => callback(cmd, ...args));
) => CommandDef((cmd, ctx, r) => callback(cmd, ...args));
export const CommandLocale = (locale: string, def: CommandLocaleDef) =>
CommandDef((cmd, ctx) => {
ctx.i18n.define(locale, `commands.${cmd.name}`, def);
CommandDef((cmd, ctx, r) => {
ctx.i18n.define(r(locale), `commands.${cmd.name}`, r(def));
return cmd;
});
export const CommandDescription = (desc: string | Dict<string>) => {
return CommandDef((cmd, ctx) => {
for (const localData of Object.entries(adaptLocaleDict(desc))) {
return CommandDef((cmd, ctx, r) => {
for (const localData of Object.entries(adaptLocaleDict(r(desc)))) {
const [locale, text] = localData;
ctx.i18n.define(locale, `commands.${cmd.name}.description`, text);
}
......@@ -142,48 +142,53 @@ export const CommandDescription = (desc: string | Dict<string>) => {
};
export const CommandAlias = (...names: string[]) =>
CommandDef((cmd) => cmd.alias(...names));
CommandDef((cmd, ctx, r) => cmd.alias(...r(names)));
export const CommandShortcut = (
name: string | RegExp,
config: Command.Shortcut = {},
) => CommandDef((cmd) => cmd.shortcut(name, config));
) => CommandDef((cmd, ctx, r) => cmd.shortcut(r(name), r(config)));
export const CommandUsage = (text: Command.Usage) =>
CommandDef((cmd) => cmd.usage(text));
CommandDef((cmd, ctx, r) => cmd.usage(r(text)));
export const CommandExample = (text: string) =>
CommandDef((cmd) => cmd.example(text));
CommandDef((cmd, ctx, r) => cmd.example(r(text)));
export const CommandOption = (
name: string,
desc: string,
config: CommandOptionConfigWithDescription = {},
) =>
CommandDef((cmd, ctx) =>
applyOptionToCommand(ctx, cmd, { name, desc, config }),
CommandDef((cmd, ctx, r) =>
applyOptionToCommand(ctx, cmd, r({ name, desc, config })),
);
export const CommandUserFields = (fields: FieldCollector<'user'>) =>
CommandDef((cmd) => cmd.userFields(fields));
CommandDef((cmd, ctx, r) => cmd.userFields(r(fields)));
export const CommandChannelFields = (fields: FieldCollector<'channel'>) =>
CommandDef((cmd) => cmd.channelFields(fields));
CommandDef((cmd, ctx, r) => cmd.channelFields(r(fields)));
export const CommandBefore = (callback: Command.Action, append = false) =>
CommandDef((cmd) => cmd.before(callback, append));
CommandDef((cmd, ctx, r) => cmd.before(callback, append));
export const CommandAction = (callback: Command.Action, prepend = false) =>
CommandDef((cmd) => cmd.action(callback, prepend));
CommandDef((cmd, ctx, r) => cmd.action(callback, prepend));
export const CommandTemplate = (name: string, text: string | Dict<string>) =>
CommandDef((cmd, ctx) => {
registerTemplate({ name, text: adaptLocaleDict(text) }, ctx, cmd);
CommandDef((cmd, ctx, r) => {
registerTemplate(
{ name: r(name), text: adaptLocaleDict(r(text)) },
ctx,
cmd,
);
return cmd;
});
// Command put config
export const PutValue = (value: string) => CommandPut.decorate('value', value);
export const PutArgv = (field?: keyof Argv) =>
field ? CommandPut.decorate('argvField', field) : CommandPut.decorate('argv');
export const PutSession = (field?: keyof Session) =>
......@@ -231,8 +236,8 @@ export const TopLevelAction = (action: TopLevelActionDef): ClassDecorator =>
Metadata.append('KoishiTopLevelAction', action);
export const DefineTemplate = (name: string, text: string | Dict<string>) =>
TopLevelAction((ctx) =>
registerTemplate({ name, text: adaptLocaleDict(text) }, ctx),
TopLevelAction((ctx, obj, r) =>
registerTemplate({ name, text: adaptLocaleDict(r(text)) }, ctx),
);
export function DefineLocale(locale: string, dict: I18n.Store): ClassDecorator;
......@@ -245,9 +250,9 @@ export function DefineLocale(
locale: string,
...args: [I18n.Store] | [string, I18n.Node]
): ClassDecorator {
return TopLevelAction((ctx) =>
return TopLevelAction((ctx, obj, r) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
ctx.i18n.define(locale, ...args),
ctx.i18n.define(r(locale), ...r(args)),
);
}
......@@ -117,7 +117,11 @@ export interface CommandOptionConfig {
config?: CommandOptionConfigWithDescription;
}
export type CommandDefinitionFun = (cmd: Command, ctx: Context) => Command;
export type CommandDefinitionFun = (
cmd: Command,
ctx: Context,
renderer: ParamRenderer,
) => Command;
export interface KoishiRouteDef {
path: string;
......@@ -148,7 +152,11 @@ export interface TemplateConfig {
text: Dict<string>;
}
export type TopLevelActionDef = (ctx: Context, obj: any) => void;
export type TopLevelActionDef = (
ctx: Context,
obj: any,
renderer: ParamRenderer,
) => void;
export interface CommandOptionConfigWithDescription extends Argv.OptionConfig {
description?: string | Dict<string>;
......@@ -166,3 +174,5 @@ export interface CommandArgDef {
index: number;
decl?: Argv.Declaration;
}
export type ParamRenderer = <T>(v: T) => T;
......@@ -5,7 +5,11 @@ import {
} from './def';
import { reflector } from './meta/meta-fetch';
import { Context } from 'koishi';
import { getContextFromFilters } from './utility';
import {
generateRenderer,
getContextFromFilters,
renderObject,
} from './utility';
import { DoRegister } from './registry';
import _ from 'lodash';
......@@ -15,7 +19,11 @@ export interface DoRegisterResult<T> extends DoRegister.Config {
}
export class Registrar<T = any> {
constructor(private obj: T, private alternativeObject?: any) {}
constructor(
private obj: T,
private alternativeObject?: any,
private view: Record<any, any> = {},
) {}
getAllFieldsToRegister(): (keyof T)[] {
const arr = reflector.getArray(KoishiDoRegisterKeys, this.obj);
......@@ -43,34 +51,49 @@ export class Registrar<T = any> {
return getContextFromFilters(ctx, contextFilters);
}
register(ctx: Context, key: keyof T, autoScope = true): DoRegisterResult<T> {
register(
ctx: Context,
key: keyof T,
autoScope = true,
extraView: Record<any, any> = {},
): DoRegisterResult<T> {
if (autoScope) {
ctx = this.getScopeContext(ctx, key, true);
}
const _key = key as string;
const data = reflector.get(KoishiDoRegister, this.obj, _key);
let data = reflector.get(KoishiDoRegister, this.obj, _key);
if (!data) return;
data = renderObject(data, { ...this.view, ...extraView });
const result = DoRegister.registry.execute(
data,
ctx,
this.obj,
_key,
this.alternativeObject,
this.view,
);
return { ...data, key: key as keyof T & string, result };
}
registerAll(ctx: Context, autoScope = false) {
registerAll(
ctx: Context,
autoScope = false,
extraView: Record<any, any> = {},
) {
if (autoScope) {
ctx = this.getScopeContext(ctx);
}
this.performTopActions(ctx, false);
return this.getAllFieldsToRegister().map((key) =>
this.register(ctx, key, true),
this.register(ctx, key, true, extraView),
);
}
performTopActions(ctx: Context, autoScope = false) {
performTopActions(
ctx: Context,
autoScope = false,
extraView: Record<any, any> = {},
) {
if (autoScope) {
ctx = this.getScopeContext(ctx);
}
......@@ -81,6 +104,7 @@ export class Registrar<T = any> {
);
}
actions = _.uniq(actions);
actions.forEach((action) => action(ctx, this.obj));
const renderer = generateRenderer({ ...this.view, ...extraView });
actions.forEach((action) => action(ctx, this.obj, renderer));
}
}
import { MappingStruct } from '../def';
import { renderObject } from '../utility';
export class AbstractRegistry<M extends Record<string | number | symbol, any>> {
private map = new Map<keyof M, any>();
......
......@@ -12,6 +12,7 @@ import {
applyNativeTypeToArg,
applyOptionToCommand,
registerTemplate,
renderObject,
} from '../../utility';
import { Metadata } from '../../meta/metadata.decorators';
import { reflector } from '../../meta/meta-fetch';
......@@ -32,6 +33,7 @@ export namespace CommandPut {
renderer: string | undefined;
template: TemplateConfig;
typeClass: void;
value: string;
}
export type Config<K extends keyof ConfigMap = keyof ConfigMap> =
......@@ -41,7 +43,7 @@ export namespace CommandPut {
ConfigMap,
void,
// eslint-disable-next-line @typescript-eslint/ban-types
[Command, Context, Function]
[Command, Context, Function, any]
>();
preRegistry.extend('option', (data, cmd, ctx, nativeType) =>
......@@ -82,7 +84,7 @@ export namespace CommandPut {
registerTemplate(data, ctx, cmd),
);
preRegistry.extend('typeClass', (data, cmd, ctx, nativeType) => {
preRegistry.extend('typeClass', (data, cmd, ctx, nativeType, view) => {
const keys = reflector.getArray('KoishiPutClassFieldKeys', nativeType);
for (const key of keys) {
const meta = reflector.get('KoishiPutClassField', nativeType, key);
......@@ -92,7 +94,13 @@ export namespace CommandPut {
nativeType.prototype,
key,
);
preRegistry.execute(meta, cmd, ctx, propertyNativeType);
preRegistry.execute(
renderObject(meta, view),
cmd,
ctx,
propertyNativeType,
view,
);
}
});
......@@ -100,9 +108,10 @@ export namespace CommandPut {
ConfigMap,
any,
// eslint-disable-next-line @typescript-eslint/ban-types
[Argv, any[], Function]
[Argv, any[], Function, any]
>();
registry.extend('value', (data) => data);
registry.extend('args', (data, argv, args) => args);
registry.extend('arg', (data, argv, args) => args[data.index]);
registry.extend('argv', (data, argv, args) => argv);
......@@ -141,7 +150,7 @@ export namespace CommandPut {
);
registry.extend(
'typeClass',
(data, argv, args, nativeType: { new (): any }) => {
(data, argv, args, nativeType: { new (): any }, view) => {
const keys = reflector.getArray('KoishiPutClassFieldKeys', nativeType);
const obj = new nativeType();
for (const key of keys) {
......@@ -152,7 +161,13 @@ export namespace CommandPut {
nativeType.prototype,
key,
);
obj[key] = registry.execute(meta, argv, args, propertyNativeType);
obj[key] = registry.execute(
renderObject(meta, view),
argv,
args,
propertyNativeType,
view,
);
}
return obj;
},
......
......@@ -14,7 +14,7 @@ import {
import { Metadata } from '../../meta/metadata.decorators';
import { reflector } from '../../meta/meta-fetch';
import { CommandPut } from './command-put';
import { applySelector } from '../../utility';
import { applySelector, generateRenderer, renderObject } from '../../utility';
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace DoRegister {
......@@ -27,7 +27,17 @@ export namespace DoRegister {
class SpecificRegistry extends MethodRegistry<
ConfigMap,
any,
[Context, any, string, any | undefined]
[
Context,
// obj
any,
// key
string,
// extra obj
any | undefined,
// view
any | undefined,
]
> {
define<K extends keyof ConfigMap>(
name: K,
......@@ -120,7 +130,7 @@ export namespace DoRegister {
export const command = registry.define(
'command',
(data, ctx, obj, key, extraObj) => {
(data, ctx, obj, key, extraObj, view) => {
let command = ctx.command(data.def, data.desc, data.config);
const commandDefs = reflector.getProperty(
KoishiCommandDefinition,
......@@ -128,8 +138,9 @@ export namespace DoRegister {
key,
extraObj,
);
const renderer = generateRenderer(view);
for (const commandDef of commandDefs) {
command = commandDef(command, ctx) || command;
command = commandDef(command, ctx, renderer) || command;
}
if (!data.config?.empty) {
if (!data.putOptions) {
......@@ -143,11 +154,23 @@ export namespace DoRegister {
continue;
}
const nativeType = data.paramTypes[i];
CommandPut.preRegistry.execute(putOption, command, ctx, nativeType);
CommandPut.preRegistry.execute(
putOption,
command,
ctx,
nativeType,
view,
);
}
command.action((argv: Argv, ...args: any[]) => {
const params = data.putOptions.map((o, i) =>
CommandPut.registry.execute(o, argv, args, data.paramTypes[i]),
CommandPut.registry.execute(
o,
argv,
args,
data.paramTypes[i],
view,
),
);
return obj[key](...params);
});
......
......@@ -3,9 +3,12 @@ import {
CommandOptionConfig,
ContextSelector,
OnContextFunction,
ParamRenderer,
TemplateConfig,
} from '../def';
import { applyNativeTypeToArg } from './native-type-mapping';
import Mustache from 'mustache';
import _ from 'lodash';
export function applySelector(
ctx: Context,
......@@ -78,3 +81,34 @@ export function applyOptionToCommand(
applyNativeTypeToArg(option, nativeType);
return cmd;
}
export function renderObject<T = any>(
object: T,
view: any,
visited?: Set<any>,
): T;
export function renderObject(object: any, view: any, visited: Set<any>): any {
if (!view || !object) {
return object;
}
visited ??= new Set();
if (typeof object === 'string') {
return Mustache.render(object, view, undefined, { escape: (v) => v });
}
if (visited.has(object)) {
return object;
}
if (Array.isArray(object)) {
visited.add(object);
return (object as any[]).map((item) => renderObject(item, view, visited));
}
if (typeof object === 'object') {
visited.add(object);
return _.mapValues(object, (value) => renderObject(value, view, visited));
}
return object;
}
export function generateRenderer(view: any): ParamRenderer {
return (v) => renderObject(v, view);
}
......@@ -3,6 +3,7 @@ import {
PutArg,
PutObject,
PutOption,
PutValue,
UseCommand,
UseEvent,
UseMiddleware,
......@@ -50,6 +51,15 @@ class MyClass {
async onSkirt(@PutObject() arg: SkirtArg) {
return `I have ${arg.count} ${arg.color} skirts.`;
}
@UseCommand('{{name}}')
@CommandUsage('{{usage}}')
async onGeneric(
@PutOption('{{option}}', '-c <count>') count: number,
@PutValue('{{name}}') name: string,
) {
return `I have ${count} ${name}.`;
}
}
const registrar = new Registrar(new MyClass());
......@@ -105,4 +115,21 @@ describe('Register', () => {
command.execute({ args: ['4'], options: { color: 'red' } }),
).resolves.toBe('I have 4 red skirts.');
});
it('should work on template', () => {
const registrar = new Registrar(new MyClass(), undefined, {
name: 'socks',
usage: 'Socks count',
option: 'count',
});
const result = registrar.register(app, 'onGeneric');
expect(result.type).toBe('command');
const command: Command = result.result;
expect(command._usage).toBe('Socks count');
expect(command.name).toBe('socks');
expect(command._options.count.name).toBe('count');
expect(command.execute({ options: { count: 7 } })).resolves.toBe(
'I have 7 socks.',
);
});
});
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