Compare commits

...

2 Commits

Author SHA1 Message Date
hypercross 033fb6c894 refactor: command syntax improvement 2026-04-01 21:18:58 +08:00
hypercross d1ea04c442 fix: command parsing 2026-04-01 21:12:34 +08:00
3 changed files with 262 additions and 207 deletions

View File

@ -133,7 +133,9 @@ export function parseCommand(input: string): Command {
function tokenize(input: string): string[] { function tokenize(input: string): string[] {
const tokens: string[] = []; const tokens: string[] = [];
let current = ''; let current = '';
let inQuote: string | null = null; // ' 或 " 或 null let inQuote: string | null = null;
let inBracket = false;
let bracketDepth = 0;
let escaped = false; let escaped = false;
let i = 0; let i = 0;
@ -141,38 +143,57 @@ function tokenize(input: string): string[] {
const char = input[i]; const char = input[i];
if (escaped) { if (escaped) {
// 转义字符:直接添加到当前 token
current += char; current += char;
escaped = false; escaped = false;
} else if (char === '\\') { } else if (char === '\\') {
// 开始转义
escaped = true; escaped = true;
} else if (inQuote) { } else if (inQuote) {
// 在引号内
if (char === inQuote) { if (char === inQuote) {
// 结束引号
inQuote = null; inQuote = null;
} else { } else {
current += char; current += char;
} }
} else if (char === '"' || char === "'") { } else if (char === '"' || char === "'") {
// 开始引号
inQuote = char; inQuote = char;
} else if (char === '[') {
if (inBracket) {
bracketDepth++;
current += char;
} else {
if (current.length > 0) {
tokens.push(current);
current = '';
}
inBracket = true;
bracketDepth = 1;
current = '[';
}
} else if (char === ']') {
if (inBracket) {
bracketDepth--;
current += char;
if (bracketDepth === 0) {
tokens.push(current);
current = '';
inBracket = false;
}
} else {
current += char;
}
} else if (/\s/.test(char)) { } else if (/\s/.test(char)) {
// 空白字符 if (inBracket) {
if (current.length > 0) { current += char;
} else if (current.length > 0) {
tokens.push(current); tokens.push(current);
current = ''; current = '';
} }
} else { } else {
// 普通字符
current += char; current += char;
} }
i++; i++;
} }
// 处理未闭合的引号
if (current.length > 0) { if (current.length > 0) {
tokens.push(current); tokens.push(current);
} }
@ -189,11 +210,13 @@ function tokenize(input: string): string[] {
* - [param...] * - [param...]
* - <param: type> * - <param: type>
* - [param: type] * - [param: type]
* - --flag * - --flag
* - --flag: boolean
* - -f * - -f
* - --option <value>
* - --option: type * - --option: type
* - -o <value> * - --option: type = default
* - --option: type -o
* - --option: type -o = default
* - -o: type * - -o: type
* *
* 使 inline-schema 使 ; , * 使 inline-schema 使 ; ,
@ -203,8 +226,9 @@ function tokenize(input: string): string[] {
* - [string; number][] * - [string; number][]
* *
* @example * @example
* parseCommandSchema('move <from> [to...] [--force] [-f] [--speed <val>]') * parseCommandSchema('move <from> [to...] [--force] [-f] [--speed: number]')
* parseCommandSchema('move <from: [x: string; y: string]> <to: string> [--all: boolean]') * parseCommandSchema('move <from: [x: string; y: string]> <to: string> [--all]')
* parseCommandSchema('move <from> <to> [--speed: number = 10 -s]')
*/ */
export function parseCommandSchema(schemaStr: string): CommandSchema { export function parseCommandSchema(schemaStr: string): CommandSchema {
const schema: CommandSchema = { const schema: CommandSchema = {
@ -219,7 +243,6 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
return schema; return schema;
} }
// 第一个 token 是命令名称
schema.name = tokens[0]; schema.name = tokens[0];
let i = 1; let i = 1;
@ -227,92 +250,39 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
const token = tokens[i]; const token = tokens[i];
if (token.startsWith('[') && token.endsWith(']')) { if (token.startsWith('[') && token.endsWith(']')) {
// 可选参数/标志/选项(方括号内的内容)
const inner = token.slice(1, -1).trim(); const inner = token.slice(1, -1).trim();
if (inner.startsWith('--')) { if (inner.startsWith('--')) {
// 可选长格式标志或选项 const result = parseOptionToken(inner.slice(2), false);
const parts = inner.split(/\s+/); if (result.isFlag) {
const name = parts[0].slice(2); schema.flags.push({ name: result.name, short: result.short });
// 检查是否有类型定义(如 --flag: boolean 或 --opt: string[]
if (name.includes(':')) {
const [optName, typeStr] = name.split(':').map(s => s.trim());
const parsedSchema = defineSchema(typeStr);
schema.options.push({
name: optName,
required: false,
schema: parsedSchema,
});
} else if (parts.length > 1) {
// 可选选项(旧语法:--opt <value>
const valueToken = parts[1];
let typeStr = valueToken;
// 如果是 <value> 格式,提取类型
if (valueToken.startsWith('<') && valueToken.endsWith('>')) {
typeStr = valueToken.slice(1, -1);
}
// 尝试解析为 inline-schema 类型
let parsedSchema: ParsedSchema | undefined;
try {
parsedSchema = defineSchema(typeStr);
} catch {
// 不是有效的 schema使用默认字符串
}
schema.options.push({
name,
required: false,
schema: parsedSchema,
});
} else { } else {
// 可选标志 schema.options.push({
schema.flags.push({ name }); name: result.name,
short: result.short,
required: false,
defaultValue: result.defaultValue,
schema: result.schema,
});
} }
} else if (inner.startsWith('-') && inner.length > 1) { } else if (inner.startsWith('-') && inner.length > 1 && !inner.includes('--')) {
// 可选短格式标志或选项 const result = parseOptionToken(inner.slice(1), false);
const parts = inner.split(/\s+/); if (result.isFlag) {
const short = parts[0].slice(1); schema.flags.push({ name: result.name, short: result.short || result.name });
// 检查是否有类型定义
if (short.includes(':')) {
const [optName, typeStr] = short.split(':').map(s => s.trim());
const parsedSchema = defineSchema(typeStr);
schema.options.push({
name: optName,
short: optName,
required: false,
schema: parsedSchema,
});
} else if (parts.length > 1) {
// 可选选项(旧语法)
const valueToken = parts[1];
let typeStr = valueToken;
if (valueToken.startsWith('<') && valueToken.endsWith('>')) {
typeStr = valueToken.slice(1, -1);
}
let parsedSchema: ParsedSchema | undefined;
try {
parsedSchema = defineSchema(typeStr);
} catch {
// 不是有效的 schema使用默认字符串
}
schema.options.push({
name: short,
short,
required: false,
schema: parsedSchema,
});
} else { } else {
// 可选标志 schema.options.push({
schema.flags.push({ name: short, short }); name: result.name,
short: result.short || result.name,
required: false,
defaultValue: result.defaultValue,
schema: result.schema,
});
} }
} else { } else {
// 可选参数
const isVariadic = inner.endsWith('...'); const isVariadic = inner.endsWith('...');
let paramContent = isVariadic ? inner.slice(0, -3) : inner; let paramContent = isVariadic ? inner.slice(0, -3) : inner;
let parsedSchema: ParsedSchema | undefined; let parsedSchema: ParsedSchema | undefined;
// 检查是否有类型定义(如 [name: string]
if (paramContent.includes(':')) { if (paramContent.includes(':')) {
const [name, typeStr] = paramContent.split(':').map(s => s.trim()); const [name, typeStr] = paramContent.split(':').map(s => s.trim());
try { try {
@ -332,86 +302,38 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
} }
i++; i++;
} else if (token.startsWith('--')) { } else if (token.startsWith('--')) {
// 长格式标志或选项(必需的,因为不在方括号内) const result = parseOptionToken(token.slice(2), true);
const name = token.slice(2); if (result.isFlag) {
const nextToken = tokens[i + 1]; schema.flags.push({ name: result.name, short: result.short });
// 检查是否有类型定义(如 --flag: boolean
if (name.includes(':')) {
const [optName, typeStr] = name.split(':').map(s => s.trim());
const parsedSchema = defineSchema(typeStr);
schema.options.push({
name: optName,
required: true,
schema: parsedSchema,
});
i++;
} else if (nextToken && nextToken.startsWith('<') && nextToken.endsWith('>')) {
// 旧语法:--opt <value>
const valueToken = nextToken;
const typeStr = valueToken.slice(1, -1);
let parsedSchema: ParsedSchema | undefined;
try {
parsedSchema = defineSchema(typeStr);
} catch {
// 不是有效的 schema
}
schema.options.push({
name,
required: true,
schema: parsedSchema,
});
i += 2;
} else { } else {
// 否则是标志 schema.options.push({
schema.flags.push({ name }); name: result.name,
i++; short: result.short,
required: true,
defaultValue: result.defaultValue,
schema: result.schema,
});
} }
i++;
} else if (token.startsWith('-') && token.length > 1 && !/^-?\d+$/.test(token)) { } else if (token.startsWith('-') && token.length > 1 && !/^-?\d+$/.test(token)) {
// 短格式标志或选项(必需的,因为不在方括号内) const result = parseOptionToken(token.slice(1), true);
const short = token.slice(1); if (result.isFlag) {
const nextToken = tokens[i + 1]; schema.flags.push({ name: result.name, short: result.short || result.name });
// 检查是否有类型定义
if (short.includes(':')) {
const [optName, typeStr] = short.split(':').map(s => s.trim());
const parsedSchema = defineSchema(typeStr);
schema.options.push({
name: optName,
short: optName,
required: true,
schema: parsedSchema,
});
i++;
} else if (nextToken && nextToken.startsWith('<') && nextToken.endsWith('>')) {
// 旧语法
const valueToken = nextToken;
const typeStr = valueToken.slice(1, -1);
let parsedSchema: ParsedSchema | undefined;
try {
parsedSchema = defineSchema(typeStr);
} catch {
// 不是有效的 schema
}
schema.options.push({
name: short,
short,
required: true,
schema: parsedSchema,
});
i += 2;
} else { } else {
// 否则是标志 schema.options.push({
schema.flags.push({ name: short, short }); name: result.name,
i++; short: result.short || result.name,
required: true,
defaultValue: result.defaultValue,
schema: result.schema,
});
} }
i++;
} else if (token.startsWith('<') && token.endsWith('>')) { } else if (token.startsWith('<') && token.endsWith('>')) {
// 必需参数
const isVariadic = token.endsWith('...>'); const isVariadic = token.endsWith('...>');
let paramContent = token.replace(/^[<]+|[>.>]+$/g, ''); let paramContent = token.replace(/^[<]+|[>.>]+$/g, '');
let parsedSchema: ParsedSchema | undefined; let parsedSchema: ParsedSchema | undefined;
// 检查是否有类型定义(如 <from: [x: string; y: string]>
if (paramContent.includes(':')) { if (paramContent.includes(':')) {
const colonIndex = paramContent.indexOf(':'); const colonIndex = paramContent.indexOf(':');
const name = paramContent.slice(0, colonIndex).trim(); const name = paramContent.slice(0, colonIndex).trim();
@ -432,7 +354,6 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
}); });
i++; i++;
} else { } else {
// 跳过无法识别的 token
i++; i++;
} }
} }
@ -440,6 +361,92 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
return schema; return schema;
} }
/**
* / token
*/
interface ParsedOptionResult {
name: string;
short?: string;
isFlag: boolean;
schema?: ParsedSchema;
defaultValue?: unknown;
}
/**
* / token
*
* - flag
* - flag: boolean
* - option: type
* - option: type -s
* - option: type = default
* - option: type -s = default
*/
function parseOptionToken(token: string, required: boolean): ParsedOptionResult {
const parts = token.split(/\s+/);
const mainPart = parts[0];
let name: string;
let typeStr: string | undefined;
let isFlag = false;
if (mainPart.endsWith(':')) {
name = mainPart.slice(0, -1).trim();
typeStr = parts[1] || 'string';
} else if (mainPart.includes(':')) {
const [optName, optType] = mainPart.split(':').map(s => s.trim());
name = optName;
typeStr = optType;
} else {
name = mainPart;
isFlag = true;
}
if (typeStr === 'boolean') {
isFlag = true;
typeStr = undefined;
}
let short: string | undefined;
let defaultValue: unknown;
let schema: ParsedSchema | undefined;
for (let i = 1; i < parts.length; i++) {
const part = parts[i];
if (part.startsWith('-') && part.length === 2) {
short = part.slice(1);
} else if (part === '=') {
const valuePart = parts[i + 1];
if (valuePart) {
try {
defaultValue = JSON.parse(valuePart);
} catch {
defaultValue = valuePart;
}
i++;
}
} else if (part.startsWith('=')) {
const valuePart = part.slice(1);
try {
defaultValue = JSON.parse(valuePart);
} catch {
defaultValue = valuePart;
}
}
}
if (typeStr && !isFlag) {
try {
schema = defineSchema(typeStr);
} catch {
// 不是有效的 schema
}
}
return { name, short, isFlag, schema, defaultValue };
}
/** /**
* token <value> [value] * token <value> [value]
*/ */
@ -464,6 +471,7 @@ function isParamPlaceholder(token: string): boolean {
/** /**
* schema tokens * schema tokens
* [...args] [--flag] * [...args] [--flag]
* <param> <param: type>
*/ */
function tokenizeSchema(input: string): string[] { function tokenizeSchema(input: string): string[] {
const tokens: string[] = []; const tokens: string[] = [];
@ -477,13 +485,11 @@ function tokenizeSchema(input: string): string[] {
if (inBracket) { if (inBracket) {
if (char === ']') { if (char === ']') {
// 结束括号,将内容加上括号作为一个 token
tokens.push(`[${bracketContent}]`); tokens.push(`[${bracketContent}]`);
inBracket = false; inBracket = false;
bracketContent = ''; bracketContent = '';
current = ''; current = '';
} else if (char === '[') { } else if (char === '[') {
// 嵌套括号(不支持)
bracketContent += char; bracketContent += char;
} else { } else {
bracketContent += char; bracketContent += char;
@ -501,7 +507,6 @@ function tokenizeSchema(input: string): string[] {
inBracket = true; inBracket = true;
bracketContent = ''; bracketContent = '';
} else if (char === '<') { } else if (char === '<') {
// 尖括号内容作为一个整体
let angleContent = '<'; let angleContent = '<';
i++; i++;
while (i < input.length && input[i] !== '>') { while (i < input.length && input[i] !== '>') {
@ -521,7 +526,6 @@ function tokenizeSchema(input: string): string[] {
tokens.push(current); tokens.push(current);
} }
// 处理未闭合的括号
if (bracketContent.length > 0) { if (bracketContent.length > 0) {
tokens.push(`[${bracketContent}`); tokens.push(`[${bracketContent}`);
} }

View File

@ -21,11 +21,11 @@ describe('parseCommandSchema with inline-schema', () => {
it('should parse schema with typed options', () => { it('should parse schema with typed options', () => {
const schema = parseCommandSchema('move <from> <to> [--all: boolean] [--count: number]'); const schema = parseCommandSchema('move <from> <to> [--all: boolean] [--count: number]');
expect(schema.name).toBe('move'); expect(schema.name).toBe('move');
expect(schema.options).toHaveLength(2); expect(schema.flags).toHaveLength(1);
expect(schema.options[0].name).toBe('all'); expect(schema.options).toHaveLength(1);
expect(schema.flags[0].name).toBe('all');
expect(schema.options[0].name).toBe('count');
expect(schema.options[0].schema).toBeDefined(); expect(schema.options[0].schema).toBeDefined();
expect(schema.options[1].name).toBe('count');
expect(schema.options[1].schema).toBeDefined();
}); });
it('should parse schema with tuple type', () => { it('should parse schema with tuple type', () => {
@ -54,11 +54,11 @@ describe('parseCommandSchema with inline-schema', () => {
it('should parse schema with mixed types', () => { it('should parse schema with mixed types', () => {
const schema = parseCommandSchema( const schema = parseCommandSchema(
'move <from: [x: string; y: string]> <to: string> [--all: boolean] [--count: number]' 'move <from: [x: string; y: string]> <to: string> [--count: number]'
); );
expect(schema.name).toBe('move'); expect(schema.name).toBe('move');
expect(schema.params).toHaveLength(2); expect(schema.params).toHaveLength(2);
expect(schema.options).toHaveLength(2); expect(schema.options).toHaveLength(1);
}); });
it('should parse schema with optional typed param', () => { it('should parse schema with optional typed param', () => {
@ -94,17 +94,6 @@ describe('parseCommandWithSchema', () => {
}); });
it('should parse and validate command with boolean option', () => { it('should parse and validate command with boolean option', () => {
const result = parseCommandWithSchema(
'move meeple1 region1 --all true',
'move <from> <to> [--all: boolean]'
);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.command.options.all).toBe(true);
}
});
it('should parse and validate command with number option', () => {
const result = parseCommandWithSchema( const result = parseCommandWithSchema(
'move meeple1 region1 --count 5', 'move meeple1 region1 --count 5',
'move <from> <to> [--count: number]' 'move <from> <to> [--count: number]'
@ -115,6 +104,17 @@ describe('parseCommandWithSchema', () => {
} }
}); });
it('should parse and validate command with number option', () => {
const result = parseCommandWithSchema(
'move meeple1 region1 --speed 100',
'move <from> <to> [--speed: number]'
);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.command.options.speed).toBe(100);
}
});
it('should fail validation with wrong command name', () => { it('should fail validation with wrong command name', () => {
const result = parseCommandWithSchema( const result = parseCommandWithSchema(
'jump meeple1 region1', 'jump meeple1 region1',
@ -144,22 +144,21 @@ describe('parseCommandWithSchema', () => {
it('should fail validation with missing required option', () => { it('should fail validation with missing required option', () => {
const result = parseCommandWithSchema( const result = parseCommandWithSchema(
'move meeple1 region1', 'move meeple1 region1',
'move <from> <to> [--force: boolean]' 'move <from> <to> [--force]'
); );
// 可选选项,应该通过验证 // 可选标志,应该通过验证
expect(result.valid).toBe(true); expect(result.valid).toBe(true);
}); });
it('should parse complex command with typed params and options', () => { it('should parse complex command with typed params and options', () => {
const result = parseCommandWithSchema( const result = parseCommandWithSchema(
'move [1; 2] region1 --all true --count 3', 'move [1; 2] region1 --count 3',
'move <from: [x: string; y: string]> <to: string> [--all: boolean] [--count: number]' 'move <from: [x: string; y: string]> <to: string> [--count: number]'
); );
expect(result.valid).toBe(true); expect(result.valid).toBe(true);
if (result.valid) { if (result.valid) {
expect(result.command.params[0]).toEqual(['1', '2']); expect(result.command.params[0]).toEqual(['1', '2']);
expect(result.command.params[1]).toBe('region1'); expect(result.command.params[1]).toBe('region1');
expect(result.command.options.all).toBe(true);
expect(result.command.options.count).toBe(3); expect(result.command.options.count).toBe(3);
} }
}); });
@ -207,8 +206,8 @@ describe('validateCommand with schema types', () => {
}); });
it('should validate command with typed options', () => { it('should validate command with typed options', () => {
const schema = parseCommandSchema('move <from> <to> [--all: boolean]'); const schema = parseCommandSchema('move <from> <to> [--count: number]');
const command = parseCommand('move meeple1 region1 --all true'); const command = parseCommand('move meeple1 region1 --count 5');
const result = validateCommand(command, schema); const result = validateCommand(command, schema);
expect(result.valid).toBe(true); expect(result.valid).toBe(true);
}); });

View File

@ -70,42 +70,41 @@ describe('parseCommandSchema', () => {
}); });
it('should parse long options', () => { it('should parse long options', () => {
const schema = parseCommandSchema('move --x <value> [--y value]'); const schema = parseCommandSchema('move --x: string [--y: string]');
expect(schema.options).toEqual([ expect(schema.options).toEqual([
{ name: 'x', required: true }, { name: 'x', required: true, schema: expect.any(Object) },
{ name: 'y', required: false }, { name: 'y', required: false, schema: expect.any(Object) },
]); ]);
}); });
it('should parse short options', () => { it('should parse short options', () => {
const schema = parseCommandSchema('move -x <value> [-y value]'); const schema = parseCommandSchema('move -x: string [-y: string]');
expect(schema.options).toEqual([ expect(schema.options).toEqual([
{ name: 'x', short: 'x', required: true }, { name: 'x', short: 'x', required: true, schema: expect.any(Object) },
{ name: 'y', short: 'y', required: false }, { name: 'y', short: 'y', required: false, schema: expect.any(Object) },
]); ]);
}); });
it('should parse mixed schema', () => { it('should parse mixed schema', () => {
const schema = parseCommandSchema('move <from> <to> [--force] [-f] [--speed <val>] [-s val]'); const schema = parseCommandSchema('move <from> <to> [--force] [-f] [--speed: string -s]');
expect(schema).toEqual({ expect(schema).toEqual({
name: 'move', name: 'move',
params: [ params: [
{ name: 'from', required: true, variadic: false }, { name: 'from', required: true, variadic: false, schema: undefined },
{ name: 'to', required: true, variadic: false }, { name: 'to', required: true, variadic: false, schema: undefined },
], ],
flags: [ flags: [
{ name: 'force' }, { name: 'force' },
{ name: 'f', short: 'f' }, { name: 'f', short: 'f' },
], ],
options: [ options: [
{ name: 'speed', required: false }, { name: 'speed', short: 's', required: false, schema: expect.any(Object), defaultValue: undefined },
{ name: 's', short: 's', required: false },
], ],
}); });
}); });
it('should handle complex schema', () => { it('should handle complex schema', () => {
const schema = parseCommandSchema('place <piece> <region> [x...] [--rotate <angle>] [--force] [-f]'); const schema = parseCommandSchema('place <piece> <region> [x...] [--rotate: number] [--force] [-f]');
expect(schema.name).toBe('place'); expect(schema.name).toBe('place');
expect(schema.params).toHaveLength(3); expect(schema.params).toHaveLength(3);
expect(schema.flags).toHaveLength(2); expect(schema.flags).toHaveLength(2);
@ -172,7 +171,7 @@ describe('validateCommand', () => {
}); });
it('should reject missing required option', () => { it('should reject missing required option', () => {
const schema = parseCommandSchema('move <from> --speed <val>'); const schema = parseCommandSchema('move <from> --speed: string');
const command = parseCommand('move meeple1'); const command = parseCommand('move meeple1');
const result = validateCommand(command, schema); const result = validateCommand(command, schema);
expect(result).toEqual({ expect(result).toEqual({
@ -184,14 +183,14 @@ describe('validateCommand', () => {
}); });
it('should accept present required option', () => { it('should accept present required option', () => {
const schema = parseCommandSchema('move <from> --speed <val>'); const schema = parseCommandSchema('move <from> --speed: string');
const command = parseCommand('move meeple1 --speed 10'); const command = parseCommand('move meeple1 --speed 10');
const result = validateCommand(command, schema); const result = validateCommand(command, schema);
expect(result).toEqual({ valid: true }); expect(result).toEqual({ valid: true });
}); });
it('should accept optional option missing', () => { it('should accept optional option missing', () => {
const schema = parseCommandSchema('move <from> [--speed [val]]'); const schema = parseCommandSchema('move <from> [--speed: string]');
const command = parseCommand('move meeple1'); const command = parseCommand('move meeple1');
const result = validateCommand(command, schema); const result = validateCommand(command, schema);
expect(result).toEqual({ valid: true }); expect(result).toEqual({ valid: true });
@ -206,14 +205,14 @@ describe('validateCommand', () => {
}); });
it('should validate short form option', () => { it('should validate short form option', () => {
const schema = parseCommandSchema('move <from> -s <val>'); const schema = parseCommandSchema('move <from> -s: string');
const command = parseCommand('move meeple1 -s 10'); const command = parseCommand('move meeple1 -s 10');
const result = validateCommand(command, schema); const result = validateCommand(command, schema);
expect(result).toEqual({ valid: true }); expect(result).toEqual({ valid: true });
}); });
it('should provide detailed error messages', () => { it('should provide detailed error messages', () => {
const schema = parseCommandSchema('place <piece> <region> --rotate <angle>'); const schema = parseCommandSchema('place <piece> <region> --rotate: string');
const command = parseCommand('place meeple1'); const command = parseCommand('place meeple1');
const result = validateCommand(command, schema); const result = validateCommand(command, schema);
expect(result.valid).toBe(false); expect(result.valid).toBe(false);
@ -225,7 +224,7 @@ describe('validateCommand', () => {
describe('integration', () => { describe('integration', () => {
it('should work together parse and validate', () => { it('should work together parse and validate', () => {
const schemaStr = 'place <piece> <region> [--x <val>] [--y [val]] [--force] [-f]'; const schemaStr = 'place <piece> <region> [--x: string] [--y: string] [--force] [-f]';
const schema = parseCommandSchema(schemaStr); const schema = parseCommandSchema(schemaStr);
const validCmd = parseCommand('place meeple1 board --x 5 --force'); const validCmd = parseCommand('place meeple1 board --x 5 --force');
@ -235,4 +234,57 @@ describe('integration', () => {
const result = validateCommand(invalidCmd, schema); const result = validateCommand(invalidCmd, schema);
expect(result.valid).toBe(false); expect(result.valid).toBe(false);
}); });
it('should parse short alias syntax', () => {
const schema = parseCommandSchema('move <from> [--verbose: boolean -v]');
expect(schema.flags).toHaveLength(1);
expect(schema.flags[0]).toEqual({ name: 'verbose', short: 'v' });
});
it('should parse short alias for options', () => {
const schema = parseCommandSchema('move <from> [--speed: number -s]');
expect(schema.options).toHaveLength(1);
expect(schema.options[0]).toEqual({
name: 'speed',
short: 's',
required: false,
schema: expect.any(Object),
defaultValue: undefined,
});
});
it('should parse default value syntax', () => {
const schema = parseCommandSchema('move <from> [--speed: number = 10]');
expect(schema.options).toHaveLength(1);
expect(schema.options[0].defaultValue).toBe(10);
});
it('should parse default string value', () => {
const schema = parseCommandSchema('move <from> [--name: string = "default"]');
expect(schema.options).toHaveLength(1);
expect(schema.options[0].defaultValue).toBe('default');
});
it('should parse short alias with default value', () => {
const schema = parseCommandSchema('move <from> [--speed: number -s = 5]');
expect(schema.options).toHaveLength(1);
expect(schema.options[0].short).toBe('s');
expect(schema.options[0].defaultValue).toBe(5);
});
it('should parse command with short alias', () => {
const schema = parseCommandSchema('move <from> [--verbose -v]');
const command = parseCommand('move meeple1 -v');
const result = validateCommand(command, schema);
expect(result.valid).toBe(true);
expect(command.flags.v).toBe(true);
});
it('should parse command with short alias option', () => {
const schema = parseCommandSchema('move <from> [--speed: number -s]');
const command = parseCommand('move meeple1 -s 100');
const result = validateCommand(command, schema);
expect(result.valid).toBe(true);
expect(command.options.s).toBe('100');
});
}); });