refactor: fix command parsing
This commit is contained in:
parent
a8ff79e4e5
commit
ff9d9bd9a1
|
|
@ -1,5 +1,4 @@
|
|||
import {Command, CommandSchema, parseCommand, parseCommandSchema} from "../utils/command";
|
||||
import { defineSchema, type ParseError } from 'inline-schema';
|
||||
import {Command, CommandSchema, parseCommand, parseCommandSchema, applyCommandSchema} from "../utils/command";
|
||||
|
||||
export type RuleState = 'running' | 'yielded' | 'waiting' | 'invoking' | 'done';
|
||||
|
||||
|
|
@ -50,31 +49,7 @@ function parseYieldedSchema(value: string | CommandSchema): CommandSchema {
|
|||
}
|
||||
|
||||
function parseCommandWithSchema(command: Command, schema: CommandSchema): Command {
|
||||
const parsedParams: unknown[] = [...command.params];
|
||||
for (let i = 0; i < command.params.length; i++) {
|
||||
const paramSchema = schema.params[i]?.schema;
|
||||
if (paramSchema && typeof command.params[i] === 'string') {
|
||||
try {
|
||||
parsedParams[i] = paramSchema.parse(command.params[i] as string);
|
||||
} catch {
|
||||
// keep original value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parsedOptions: Record<string, unknown> = { ...command.options };
|
||||
for (const [key, value] of Object.entries(command.options)) {
|
||||
const optSchema = schema.options.find(o => o.name === key || o.short === key);
|
||||
if (optSchema?.schema && typeof value === 'string') {
|
||||
try {
|
||||
parsedOptions[key] = optSchema.schema.parse(value);
|
||||
} catch {
|
||||
// keep original value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { ...command, params: parsedParams, options: parsedOptions };
|
||||
return applyCommandSchema(command, schema).command;
|
||||
}
|
||||
|
||||
function pushContextToGame(game: GameContextLike, ctx: RuleContext<unknown>) {
|
||||
|
|
@ -231,17 +206,19 @@ export function dispatchCommand(game: GameContextLike, input: string): RuleConte
|
|||
|
||||
if (game.rules.has(command.name)) {
|
||||
const ruleDef = game.rules.get(command.name)!;
|
||||
const typedCommand = parseCommandWithSchema(command, ruleDef.schema);
|
||||
|
||||
const parent = findYieldedParent(game);
|
||||
|
||||
return invokeRule(game, command, ruleDef, parent);
|
||||
return invokeRule(game, typedCommand, ruleDef, parent);
|
||||
}
|
||||
|
||||
for (let i = game.ruleContexts.length - 1; i >= 0; i--) {
|
||||
const ctx = game.ruleContexts[i];
|
||||
if (ctx.state === 'yielded' && ctx.schema) {
|
||||
if (validateYieldedSchema(command, ctx.schema)) {
|
||||
const result = ctx.generator.next(command);
|
||||
const typedCommand = parseCommandWithSchema(command, ctx.schema);
|
||||
const result = ctx.generator.next(typedCommand);
|
||||
if (result.done) {
|
||||
ctx.resolution = result.value;
|
||||
ctx.state = 'done';
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export { createRule, dispatchCommand } from './core/rule';
|
|||
|
||||
// Utils
|
||||
export type { Command, CommandSchema, CommandParamSchema, CommandOptionSchema, CommandFlagSchema } from './utils/command';
|
||||
export { parseCommand, parseCommandSchema, validateCommand } from './utils/command';
|
||||
export { parseCommand, parseCommandSchema, validateCommand, parseCommandWithSchema, applyCommandSchema } from './utils/command';
|
||||
|
||||
export type { Entity, EntityAccessor } from './utils/entity';
|
||||
export { createEntityCollection } from './utils/entity';
|
||||
|
|
|
|||
|
|
@ -125,8 +125,8 @@ export function createTurnRule() {
|
|||
const playCmd = received as Command;
|
||||
if (playCmd.name !== 'play') continue;
|
||||
|
||||
const row = Number(playCmd.params[1]);
|
||||
const col = Number(playCmd.params[2]);
|
||||
const row = playCmd.params[1] as number;
|
||||
const col = playCmd.params[2] as number;
|
||||
|
||||
if (isNaN(row) || isNaN(col) || row < 0 || row > 2 || col < 0 || col > 2) continue;
|
||||
if (isCellOccupied(this, row, col)) continue;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export { parseCommand } from './command/command-parse.js';
|
||||
export { parseCommandSchema } from './command/schema-parse.js';
|
||||
export { validateCommand, parseCommandWithSchema } from './command/command-validate.js';
|
||||
export { validateCommand, parseCommandWithSchema, applyCommandSchema } from './command/command-validate.js';
|
||||
export type {
|
||||
Command,
|
||||
CommandParamSchema,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
import { type ParseError } from 'inline-schema';
|
||||
import type { Command, CommandSchema } from './types.js';
|
||||
|
||||
function validateCommandCore(command: Command, schema: CommandSchema): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (schema.name !== '' && command.name !== schema.name) {
|
||||
errors.push(`命令名称不匹配:期望 "${schema.name}",实际 "${command.name}"`);
|
||||
}
|
||||
|
||||
const requiredParams = schema.params.filter(p => p.required);
|
||||
const variadicParam = schema.params.find(p => p.variadic);
|
||||
|
||||
if (command.params.length < requiredParams.length) {
|
||||
errors.push(`参数不足:至少需要 ${requiredParams.length} 个参数,实际 ${command.params.length} 个`);
|
||||
}
|
||||
|
||||
if (!variadicParam && command.params.length > schema.params.length) {
|
||||
errors.push(`参数过多:最多 ${schema.params.length} 个参数,实际 ${command.params.length} 个`);
|
||||
}
|
||||
|
||||
const requiredOptions = schema.options.filter(o => o.required);
|
||||
for (const opt of requiredOptions) {
|
||||
const hasOption = opt.name in command.options || (opt.short && opt.short in command.options);
|
||||
if (!hasOption) {
|
||||
errors.push(`缺少必需选项:--${opt.name}${opt.short ? ` 或 -${opt.short}` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function applyCommandSchema(
|
||||
command: Command,
|
||||
schema: CommandSchema
|
||||
): { command: Command; valid: true } | { command: Command; valid: false; errors: string[] } {
|
||||
const errors = validateCommandCore(command, schema);
|
||||
if (errors.length > 0) {
|
||||
return { command, valid: false, errors };
|
||||
}
|
||||
|
||||
const parseErrors: string[] = [];
|
||||
|
||||
const parsedParams: unknown[] = [...command.params];
|
||||
for (let i = 0; i < command.params.length; i++) {
|
||||
const paramValue = command.params[i];
|
||||
const paramSchema = schema.params[i]?.schema;
|
||||
|
||||
if (paramSchema && typeof paramValue === 'string') {
|
||||
try {
|
||||
parsedParams[i] = paramSchema.parse(paramValue);
|
||||
} catch (e) {
|
||||
const err = e as ParseError;
|
||||
parseErrors.push(`参数 "${schema.params[i]?.name}" 解析失败:${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parsedOptions: Record<string, unknown> = { ...command.options };
|
||||
for (const [key, value] of Object.entries(command.options)) {
|
||||
const optSchema = schema.options.find(o => o.name === key || o.short === key);
|
||||
if (optSchema?.schema && typeof value === 'string') {
|
||||
try {
|
||||
parsedOptions[key] = optSchema.schema.parse(value);
|
||||
} catch (e) {
|
||||
const err = e as ParseError;
|
||||
parseErrors.push(`选项 "--${key}" 解析失败:${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = { ...command, params: parsedParams, options: parsedOptions };
|
||||
|
||||
if (parseErrors.length > 0) {
|
||||
return { command: result, valid: false, errors: parseErrors };
|
||||
}
|
||||
|
||||
return { command: result, valid: true };
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
import type { Command } from './types.js';
|
||||
import type { Command, CommandSchema } from './types.js';
|
||||
import { applyCommandSchema } from './command-validate.js';
|
||||
|
||||
export function parseCommand(input: string): Command {
|
||||
export function parseCommand(input: string): Command;
|
||||
export function parseCommand(input: string, schema: CommandSchema): Command;
|
||||
export function parseCommand(input: string, schema?: CommandSchema): Command {
|
||||
const tokens = tokenize(input);
|
||||
|
||||
if (tokens.length === 0) {
|
||||
|
|
@ -44,7 +47,14 @@ export function parseCommand(input: string): Command {
|
|||
}
|
||||
}
|
||||
|
||||
return { name, flags, options, params };
|
||||
const command = { name, flags, options, params };
|
||||
|
||||
if (schema) {
|
||||
const result = applyCommandSchema(command, schema);
|
||||
return result.command;
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
function tokenize(input: string): string[] {
|
||||
|
|
|
|||
|
|
@ -1,48 +1,19 @@
|
|||
import { type ParseError } from 'inline-schema';
|
||||
import type { Command, CommandSchema } from './types.js';
|
||||
import { parseCommand } from './command-parse.js';
|
||||
import { parseCommandSchema } from './schema-parse.js';
|
||||
import { applyCommandSchema as applyCommandSchemaCore } from './command-apply.js';
|
||||
|
||||
export { applyCommandSchemaCore as applyCommandSchema };
|
||||
|
||||
export function validateCommand(
|
||||
command: Command,
|
||||
schema: CommandSchema
|
||||
): { valid: true } | { valid: false; errors: string[] } {
|
||||
const errors = validateCommandCore(command, schema);
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { valid: false, errors };
|
||||
}
|
||||
|
||||
const result = applyCommandSchemaCore(command, schema);
|
||||
if (result.valid) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
function validateCommandCore(command: Command, schema: CommandSchema): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (command.name !== schema.name) {
|
||||
errors.push(`命令名称不匹配:期望 "${schema.name}",实际 "${command.name}"`);
|
||||
}
|
||||
|
||||
const requiredParams = schema.params.filter(p => p.required);
|
||||
const variadicParam = schema.params.find(p => p.variadic);
|
||||
|
||||
if (command.params.length < requiredParams.length) {
|
||||
errors.push(`参数不足:至少需要 ${requiredParams.length} 个参数,实际 ${command.params.length} 个`);
|
||||
}
|
||||
|
||||
if (!variadicParam && command.params.length > schema.params.length) {
|
||||
errors.push(`参数过多:最多 ${schema.params.length} 个参数,实际 ${command.params.length} 个`);
|
||||
}
|
||||
|
||||
const requiredOptions = schema.options.filter(o => o.required);
|
||||
for (const opt of requiredOptions) {
|
||||
const hasOption = opt.name in command.options || (opt.short && opt.short in command.options);
|
||||
if (!hasOption) {
|
||||
errors.push(`缺少必需选项:--${opt.name}${opt.short ? ` 或 -${opt.short}` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
return { valid: false, errors: result.errors };
|
||||
}
|
||||
|
||||
export function parseCommandWithSchema(
|
||||
|
|
@ -51,53 +22,5 @@ export function parseCommandWithSchema(
|
|||
): { command: Command; valid: true } | { command: Command; valid: false; errors: string[] } {
|
||||
const schema = parseCommandSchema(schemaStr);
|
||||
const command = parseCommand(input);
|
||||
|
||||
const errors = validateCommandCore(command, schema);
|
||||
if (errors.length > 0) {
|
||||
return { command, valid: false, errors };
|
||||
}
|
||||
|
||||
const parseErrors: string[] = [];
|
||||
|
||||
const parsedParams: unknown[] = [];
|
||||
for (let i = 0; i < command.params.length; i++) {
|
||||
const paramValue = command.params[i];
|
||||
const paramSchema = schema.params[i]?.schema;
|
||||
|
||||
if (paramSchema) {
|
||||
try {
|
||||
const parsed = typeof paramValue === 'string'
|
||||
? paramSchema.parse(paramValue)
|
||||
: paramValue;
|
||||
parsedParams.push(parsed);
|
||||
} catch (e) {
|
||||
const err = e as ParseError;
|
||||
parseErrors.push(`参数 "${schema.params[i]?.name}" 解析失败:${err.message}`);
|
||||
}
|
||||
} else {
|
||||
parsedParams.push(paramValue);
|
||||
}
|
||||
}
|
||||
|
||||
const parsedOptions: Record<string, unknown> = { ...command.options };
|
||||
for (const [key, value] of Object.entries(command.options)) {
|
||||
const optSchema = schema.options.find(o => o.name === key || o.short === key);
|
||||
if (optSchema?.schema && typeof value === 'string') {
|
||||
try {
|
||||
parsedOptions[key] = optSchema.schema.parse(value);
|
||||
} catch (e) {
|
||||
const err = e as ParseError;
|
||||
parseErrors.push(`选项 "--${key}" 解析失败:${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parseErrors.length > 0) {
|
||||
return { command: { ...command, params: parsedParams, options: parsedOptions }, valid: false, errors: parseErrors };
|
||||
}
|
||||
|
||||
return {
|
||||
command: { ...command, params: parsedParams, options: parsedOptions },
|
||||
valid: true,
|
||||
};
|
||||
return applyCommandSchemaCore(command, schema);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ describe('Rule System', () => {
|
|||
const ctx = game.dispatchCommand('attack goblin --power 5');
|
||||
|
||||
expect(ctx!.state).toBe('done');
|
||||
expect(ctx!.resolution).toEqual({ target: 'goblin', power: '5' });
|
||||
expect(ctx!.resolution).toEqual({ target: 'goblin', power: 5 });
|
||||
});
|
||||
|
||||
it('should complete immediately if generator does not yield', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue