Compare commits
2 Commits
c315e0643b
...
033fb6c894
| Author | SHA1 | Date |
|---|---|---|
|
|
033fb6c894 | |
|
|
d1ea04c442 |
|
|
@ -133,7 +133,9 @@ export function parseCommand(input: string): Command {
|
|||
function tokenize(input: string): string[] {
|
||||
const tokens: string[] = [];
|
||||
let current = '';
|
||||
let inQuote: string | null = null; // ' 或 " 或 null
|
||||
let inQuote: string | null = null;
|
||||
let inBracket = false;
|
||||
let bracketDepth = 0;
|
||||
let escaped = false;
|
||||
let i = 0;
|
||||
|
||||
|
|
@ -141,38 +143,57 @@ function tokenize(input: string): string[] {
|
|||
const char = input[i];
|
||||
|
||||
if (escaped) {
|
||||
// 转义字符:直接添加到当前 token
|
||||
current += char;
|
||||
escaped = false;
|
||||
} else if (char === '\\') {
|
||||
// 开始转义
|
||||
escaped = true;
|
||||
} else if (inQuote) {
|
||||
// 在引号内
|
||||
if (char === inQuote) {
|
||||
// 结束引号
|
||||
inQuote = null;
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
} else if (char === '"' || 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)) {
|
||||
// 空白字符
|
||||
if (current.length > 0) {
|
||||
if (inBracket) {
|
||||
current += char;
|
||||
} else if (current.length > 0) {
|
||||
tokens.push(current);
|
||||
current = '';
|
||||
}
|
||||
} else {
|
||||
// 普通字符
|
||||
current += char;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
// 处理未闭合的引号
|
||||
if (current.length > 0) {
|
||||
tokens.push(current);
|
||||
}
|
||||
|
|
@ -189,11 +210,13 @@ function tokenize(input: string): string[] {
|
|||
* - [param...] 可选可变参数
|
||||
* - <param: type> 带类型定义的必需参数
|
||||
* - [param: type] 带类型定义的可选参数
|
||||
* - --flag 长格式标志
|
||||
* - --flag 长格式标志(布尔类型)
|
||||
* - --flag: boolean 长格式标志(布尔类型,与上面等价)
|
||||
* - -f 短格式标志
|
||||
* - --option <value> 长格式选项
|
||||
* - --option: type 带类型的长格式选项
|
||||
* - -o <value> 短格式选项
|
||||
* - --option: type = default 带默认值的选项
|
||||
* - --option: type -o 带短别名的选项
|
||||
* - --option: type -o = default 带短别名和默认值的选项
|
||||
* - -o: type 带类型的短格式选项
|
||||
*
|
||||
* 类型语法使用 inline-schema 格式(使用 ; 而非 ,):
|
||||
|
|
@ -203,8 +226,9 @@ function tokenize(input: string): string[] {
|
|||
* - [string; number][] 元组数组
|
||||
*
|
||||
* @example
|
||||
* parseCommandSchema('move <from> [to...] [--force] [-f] [--speed <val>]')
|
||||
* parseCommandSchema('move <from: [x: string; y: string]> <to: string> [--all: boolean]')
|
||||
* parseCommandSchema('move <from> [to...] [--force] [-f] [--speed: number]')
|
||||
* parseCommandSchema('move <from: [x: string; y: string]> <to: string> [--all]')
|
||||
* parseCommandSchema('move <from> <to> [--speed: number = 10 -s]')
|
||||
*/
|
||||
export function parseCommandSchema(schemaStr: string): CommandSchema {
|
||||
const schema: CommandSchema = {
|
||||
|
|
@ -219,7 +243,6 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
|||
return schema;
|
||||
}
|
||||
|
||||
// 第一个 token 是命令名称
|
||||
schema.name = tokens[0];
|
||||
|
||||
let i = 1;
|
||||
|
|
@ -227,92 +250,39 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
|||
const token = tokens[i];
|
||||
|
||||
if (token.startsWith('[') && token.endsWith(']')) {
|
||||
// 可选参数/标志/选项(方括号内的内容)
|
||||
const inner = token.slice(1, -1).trim();
|
||||
|
||||
if (inner.startsWith('--')) {
|
||||
// 可选长格式标志或选项
|
||||
const parts = inner.split(/\s+/);
|
||||
const name = parts[0].slice(2);
|
||||
|
||||
// 检查是否有类型定义(如 --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,
|
||||
});
|
||||
const result = parseOptionToken(inner.slice(2), false);
|
||||
if (result.isFlag) {
|
||||
schema.flags.push({ name: result.name, short: result.short });
|
||||
} else {
|
||||
// 可选标志
|
||||
schema.flags.push({ name });
|
||||
schema.options.push({
|
||||
name: result.name,
|
||||
short: result.short,
|
||||
required: false,
|
||||
defaultValue: result.defaultValue,
|
||||
schema: result.schema,
|
||||
});
|
||||
}
|
||||
} else if (inner.startsWith('-') && inner.length > 1) {
|
||||
// 可选短格式标志或选项
|
||||
const parts = inner.split(/\s+/);
|
||||
const short = parts[0].slice(1);
|
||||
|
||||
// 检查是否有类型定义
|
||||
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 if (inner.startsWith('-') && inner.length > 1 && !inner.includes('--')) {
|
||||
const result = parseOptionToken(inner.slice(1), false);
|
||||
if (result.isFlag) {
|
||||
schema.flags.push({ name: result.name, short: result.short || result.name });
|
||||
} else {
|
||||
// 可选标志
|
||||
schema.flags.push({ name: short, short });
|
||||
schema.options.push({
|
||||
name: result.name,
|
||||
short: result.short || result.name,
|
||||
required: false,
|
||||
defaultValue: result.defaultValue,
|
||||
schema: result.schema,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 可选参数
|
||||
const isVariadic = inner.endsWith('...');
|
||||
let paramContent = isVariadic ? inner.slice(0, -3) : inner;
|
||||
let parsedSchema: ParsedSchema | undefined;
|
||||
|
||||
// 检查是否有类型定义(如 [name: string])
|
||||
if (paramContent.includes(':')) {
|
||||
const [name, typeStr] = paramContent.split(':').map(s => s.trim());
|
||||
try {
|
||||
|
|
@ -332,86 +302,38 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
|||
}
|
||||
i++;
|
||||
} else if (token.startsWith('--')) {
|
||||
// 长格式标志或选项(必需的,因为不在方括号内)
|
||||
const name = token.slice(2);
|
||||
const nextToken = tokens[i + 1];
|
||||
|
||||
// 检查是否有类型定义(如 --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;
|
||||
const result = parseOptionToken(token.slice(2), true);
|
||||
if (result.isFlag) {
|
||||
schema.flags.push({ name: result.name, short: result.short });
|
||||
} else {
|
||||
// 否则是标志
|
||||
schema.flags.push({ name });
|
||||
i++;
|
||||
schema.options.push({
|
||||
name: result.name,
|
||||
short: result.short,
|
||||
required: true,
|
||||
defaultValue: result.defaultValue,
|
||||
schema: result.schema,
|
||||
});
|
||||
}
|
||||
i++;
|
||||
} else if (token.startsWith('-') && token.length > 1 && !/^-?\d+$/.test(token)) {
|
||||
// 短格式标志或选项(必需的,因为不在方括号内)
|
||||
const short = token.slice(1);
|
||||
const nextToken = tokens[i + 1];
|
||||
|
||||
// 检查是否有类型定义
|
||||
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;
|
||||
const result = parseOptionToken(token.slice(1), true);
|
||||
if (result.isFlag) {
|
||||
schema.flags.push({ name: result.name, short: result.short || result.name });
|
||||
} else {
|
||||
// 否则是标志
|
||||
schema.flags.push({ name: short, short });
|
||||
i++;
|
||||
schema.options.push({
|
||||
name: result.name,
|
||||
short: result.short || result.name,
|
||||
required: true,
|
||||
defaultValue: result.defaultValue,
|
||||
schema: result.schema,
|
||||
});
|
||||
}
|
||||
i++;
|
||||
} else if (token.startsWith('<') && token.endsWith('>')) {
|
||||
// 必需参数
|
||||
const isVariadic = token.endsWith('...>');
|
||||
let paramContent = token.replace(/^[<]+|[>.>]+$/g, '');
|
||||
let parsedSchema: ParsedSchema | undefined;
|
||||
|
||||
// 检查是否有类型定义(如 <from: [x: string; y: string]>)
|
||||
if (paramContent.includes(':')) {
|
||||
const colonIndex = paramContent.indexOf(':');
|
||||
const name = paramContent.slice(0, colonIndex).trim();
|
||||
|
|
@ -432,7 +354,6 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
|||
});
|
||||
i++;
|
||||
} else {
|
||||
// 跳过无法识别的 token
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
|
@ -440,6 +361,92 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
|||
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])
|
||||
*/
|
||||
|
|
@ -464,6 +471,7 @@ function isParamPlaceholder(token: string): boolean {
|
|||
/**
|
||||
* 将 schema 字符串分解为 tokens
|
||||
* 支持方括号分组:[...args] [--flag] 等
|
||||
* 支持尖括号分组:<param> <param: type> 等
|
||||
*/
|
||||
function tokenizeSchema(input: string): string[] {
|
||||
const tokens: string[] = [];
|
||||
|
|
@ -477,13 +485,11 @@ function tokenizeSchema(input: string): string[] {
|
|||
|
||||
if (inBracket) {
|
||||
if (char === ']') {
|
||||
// 结束括号,将内容加上括号作为一个 token
|
||||
tokens.push(`[${bracketContent}]`);
|
||||
inBracket = false;
|
||||
bracketContent = '';
|
||||
current = '';
|
||||
} else if (char === '[') {
|
||||
// 嵌套括号(不支持)
|
||||
bracketContent += char;
|
||||
} else {
|
||||
bracketContent += char;
|
||||
|
|
@ -501,7 +507,6 @@ function tokenizeSchema(input: string): string[] {
|
|||
inBracket = true;
|
||||
bracketContent = '';
|
||||
} else if (char === '<') {
|
||||
// 尖括号内容作为一个整体
|
||||
let angleContent = '<';
|
||||
i++;
|
||||
while (i < input.length && input[i] !== '>') {
|
||||
|
|
@ -521,7 +526,6 @@ function tokenizeSchema(input: string): string[] {
|
|||
tokens.push(current);
|
||||
}
|
||||
|
||||
// 处理未闭合的括号
|
||||
if (bracketContent.length > 0) {
|
||||
tokens.push(`[${bracketContent}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ describe('parseCommandSchema with inline-schema', () => {
|
|||
it('should parse schema with typed options', () => {
|
||||
const schema = parseCommandSchema('move <from> <to> [--all: boolean] [--count: number]');
|
||||
expect(schema.name).toBe('move');
|
||||
expect(schema.options).toHaveLength(2);
|
||||
expect(schema.options[0].name).toBe('all');
|
||||
expect(schema.flags).toHaveLength(1);
|
||||
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[1].name).toBe('count');
|
||||
expect(schema.options[1].schema).toBeDefined();
|
||||
});
|
||||
|
||||
it('should parse schema with tuple type', () => {
|
||||
|
|
@ -54,11 +54,11 @@ describe('parseCommandSchema with inline-schema', () => {
|
|||
|
||||
it('should parse schema with mixed types', () => {
|
||||
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.params).toHaveLength(2);
|
||||
expect(schema.options).toHaveLength(2);
|
||||
expect(schema.options).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should parse schema with optional typed param', () => {
|
||||
|
|
@ -94,17 +94,6 @@ describe('parseCommandWithSchema', () => {
|
|||
});
|
||||
|
||||
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(
|
||||
'move meeple1 region1 --count 5',
|
||||
'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', () => {
|
||||
const result = parseCommandWithSchema(
|
||||
'jump meeple1 region1',
|
||||
|
|
@ -144,22 +144,21 @@ describe('parseCommandWithSchema', () => {
|
|||
it('should fail validation with missing required option', () => {
|
||||
const result = parseCommandWithSchema(
|
||||
'move meeple1 region1',
|
||||
'move <from> <to> [--force: boolean]'
|
||||
'move <from> <to> [--force]'
|
||||
);
|
||||
// 可选选项,应该通过验证
|
||||
// 可选标志,应该通过验证
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse complex command with typed params and options', () => {
|
||||
const result = parseCommandWithSchema(
|
||||
'move [1; 2] region1 --all true --count 3',
|
||||
'move <from: [x: string; y: string]> <to: string> [--all: boolean] [--count: number]'
|
||||
'move [1; 2] region1 --count 3',
|
||||
'move <from: [x: string; y: string]> <to: string> [--count: number]'
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
if (result.valid) {
|
||||
expect(result.command.params[0]).toEqual(['1', '2']);
|
||||
expect(result.command.params[1]).toBe('region1');
|
||||
expect(result.command.options.all).toBe(true);
|
||||
expect(result.command.options.count).toBe(3);
|
||||
}
|
||||
});
|
||||
|
|
@ -207,8 +206,8 @@ describe('validateCommand with schema types', () => {
|
|||
});
|
||||
|
||||
it('should validate command with typed options', () => {
|
||||
const schema = parseCommandSchema('move <from> <to> [--all: boolean]');
|
||||
const command = parseCommand('move meeple1 region1 --all true');
|
||||
const schema = parseCommandSchema('move <from> <to> [--count: number]');
|
||||
const command = parseCommand('move meeple1 region1 --count 5');
|
||||
const result = validateCommand(command, schema);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -70,42 +70,41 @@ describe('parseCommandSchema', () => {
|
|||
});
|
||||
|
||||
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([
|
||||
{ name: 'x', required: true },
|
||||
{ name: 'y', required: false },
|
||||
{ name: 'x', required: true, schema: expect.any(Object) },
|
||||
{ name: 'y', required: false, schema: expect.any(Object) },
|
||||
]);
|
||||
});
|
||||
|
||||
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([
|
||||
{ name: 'x', short: 'x', required: true },
|
||||
{ name: 'y', short: 'y', required: false },
|
||||
{ name: 'x', short: 'x', required: true, schema: expect.any(Object) },
|
||||
{ name: 'y', short: 'y', required: false, schema: expect.any(Object) },
|
||||
]);
|
||||
});
|
||||
|
||||
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({
|
||||
name: 'move',
|
||||
params: [
|
||||
{ name: 'from', required: true, variadic: false },
|
||||
{ name: 'to', required: true, variadic: false },
|
||||
{ name: 'from', required: true, variadic: false, schema: undefined },
|
||||
{ name: 'to', required: true, variadic: false, schema: undefined },
|
||||
],
|
||||
flags: [
|
||||
{ name: 'force' },
|
||||
{ name: 'f', short: 'f' },
|
||||
],
|
||||
options: [
|
||||
{ name: 'speed', required: false },
|
||||
{ name: 's', short: 's', required: false },
|
||||
{ name: 'speed', short: 's', required: false, schema: expect.any(Object), defaultValue: undefined },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
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.params).toHaveLength(3);
|
||||
expect(schema.flags).toHaveLength(2);
|
||||
|
|
@ -172,7 +171,7 @@ describe('validateCommand', () => {
|
|||
});
|
||||
|
||||
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 result = validateCommand(command, schema);
|
||||
expect(result).toEqual({
|
||||
|
|
@ -184,14 +183,14 @@ describe('validateCommand', () => {
|
|||
});
|
||||
|
||||
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 result = validateCommand(command, schema);
|
||||
expect(result).toEqual({ valid: true });
|
||||
});
|
||||
|
||||
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 result = validateCommand(command, schema);
|
||||
expect(result).toEqual({ valid: true });
|
||||
|
|
@ -206,14 +205,14 @@ describe('validateCommand', () => {
|
|||
});
|
||||
|
||||
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 result = validateCommand(command, schema);
|
||||
expect(result).toEqual({ valid: true });
|
||||
});
|
||||
|
||||
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 result = validateCommand(command, schema);
|
||||
expect(result.valid).toBe(false);
|
||||
|
|
@ -225,7 +224,7 @@ describe('validateCommand', () => {
|
|||
|
||||
describe('integration', () => {
|
||||
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 validCmd = parseCommand('place meeple1 board --x 5 --force');
|
||||
|
|
@ -235,4 +234,57 @@ describe('integration', () => {
|
|||
const result = validateCommand(invalidCmd, schema);
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue