From ff9d9bd9a1552ef7a3b5b141d1eea7e17c569f6c Mon Sep 17 00:00:00 2001 From: hypercross Date: Thu, 2 Apr 2026 00:14:43 +0800 Subject: [PATCH] refactor: fix command parsing --- src/core/rule.ts | 35 ++-------- src/index.ts | 2 +- src/samples/tic-tac-toe.ts | 4 +- src/utils/command.ts | 2 +- src/utils/command/command-apply.ts | 79 +++++++++++++++++++++++ src/utils/command/command-parse.ts | 16 ++++- src/utils/command/command-validate.ts | 93 +++------------------------ tests/core/rule.test.ts | 2 +- 8 files changed, 111 insertions(+), 122 deletions(-) create mode 100644 src/utils/command/command-apply.ts diff --git a/src/core/rule.ts b/src/core/rule.ts index 887a0c6..7a4d032 100644 --- a/src/core/rule.ts +++ b/src/core/rule.ts @@ -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 = { ...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) { @@ -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'; diff --git a/src/index.ts b/src/index.ts index 8cf82c8..73e31d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/samples/tic-tac-toe.ts b/src/samples/tic-tac-toe.ts index 5e7b9fc..442e129 100644 --- a/src/samples/tic-tac-toe.ts +++ b/src/samples/tic-tac-toe.ts @@ -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; diff --git a/src/utils/command.ts b/src/utils/command.ts index 2bc769e..f623630 100644 --- a/src/utils/command.ts +++ b/src/utils/command.ts @@ -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, diff --git a/src/utils/command/command-apply.ts b/src/utils/command/command-apply.ts new file mode 100644 index 0000000..1a2b41b --- /dev/null +++ b/src/utils/command/command-apply.ts @@ -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 = { ...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 }; +} diff --git a/src/utils/command/command-parse.ts b/src/utils/command/command-parse.ts index c3aa8b2..df0cbfa 100644 --- a/src/utils/command/command-parse.ts +++ b/src/utils/command/command-parse.ts @@ -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[] { diff --git a/src/utils/command/command-validate.ts b/src/utils/command/command-validate.ts index 6e4a6f8..05b30b9 100644 --- a/src/utils/command/command-validate.ts +++ b/src/utils/command/command-validate.ts @@ -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 }; } - - 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 = { ...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); } diff --git a/tests/core/rule.test.ts b/tests/core/rule.test.ts index ecef65b..ac97fc3 100644 --- a/tests/core/rule.test.ts +++ b/tests/core/rule.test.ts @@ -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', () => {