refactor: fix command parsing

This commit is contained in:
hypercross 2026-04-02 00:14:43 +08:00
parent a8ff79e4e5
commit ff9d9bd9a1
8 changed files with 111 additions and 122 deletions

View File

@ -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';

View File

@ -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';

View File

@ -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;

View File

@ -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,

View File

@ -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 };
}

View File

@ -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[] {

View File

@ -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);
}

View File

@ -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', () => {