refactor: change CommandSchema's options/flags to be Records

This commit is contained in:
hypercross 2026-04-02 08:29:40 +08:00
parent f1b1741db8
commit 281cbf845d
7 changed files with 64 additions and 101 deletions

View File

@ -71,7 +71,7 @@ function commandMatchesSchema(command: Command, schema: CommandSchema): boolean
return false; return false;
} }
const requiredOptions = schema.options.filter(o => o.required); const requiredOptions = Object.values(schema.options).filter(o => o.required);
for (const opt of requiredOptions) { for (const opt of requiredOptions) {
const hasOption = opt.name in command.options || (opt.short && opt.short in command.options); const hasOption = opt.name in command.options || (opt.short && opt.short in command.options);
if (!hasOption) { if (!hasOption) {
@ -131,7 +131,7 @@ function handleGeneratorResult<T>(
ctx.state = 'invoking'; ctx.state = 'invoking';
return invokeChildRule(host, yielded.rule, yielded.command, ctx as RuleContext<unknown>); return invokeChildRule(host, yielded.rule, yielded.command, ctx as RuleContext<unknown>);
} else { } else {
ctx.schema = parseYieldedSchema({ name: '', params: [], options: [], flags: [] }); ctx.schema = parseYieldedSchema({ name: '', params: [], options: {}, flags: {} });
ctx.state = 'yielded'; ctx.state = 'yielded';
} }
} else { } else {

View File

@ -19,7 +19,7 @@ function validateCommandCore(command: Command, schema: CommandSchema): string[]
errors.push(`参数过多:最多 ${schema.params.length} 个参数,实际 ${command.params.length}`); errors.push(`参数过多:最多 ${schema.params.length} 个参数,实际 ${command.params.length}`);
} }
const requiredOptions = schema.options.filter(o => o.required); const requiredOptions = Object.values(schema.options).filter(o => o.required);
for (const opt of requiredOptions) { for (const opt of requiredOptions) {
const hasOption = opt.name in command.options || (opt.short && opt.short in command.options); const hasOption = opt.name in command.options || (opt.short && opt.short in command.options);
if (!hasOption) { if (!hasOption) {
@ -58,7 +58,7 @@ export function applyCommandSchema(
const parsedOptions: Record<string, unknown> = { ...command.options }; const parsedOptions: Record<string, unknown> = { ...command.options };
for (const [key, value] of Object.entries(command.options)) { for (const [key, value] of Object.entries(command.options)) {
const optSchema = schema.options.find(o => o.name === key || o.short === key); const optSchema = schema.options[key] ?? (key.length === 1 ? Object.values(schema.options).find(o => o.short === key) : undefined);
if (optSchema?.schema && typeof value === 'string') { if (optSchema?.schema && typeof value === 'string') {
try { try {
parsedOptions[key] = optSchema.schema.parse(value); parsedOptions[key] = optSchema.schema.parse(value);

View File

@ -5,8 +5,8 @@ export function parseCommandSchema(schemaStr: string, name?: string): CommandSch
const schema: CommandSchema = { const schema: CommandSchema = {
name: name ?? '', name: name ?? '',
params: [], params: [],
options: [], options: {},
flags: [], flags: {},
}; };
const tokens = tokenizeSchema(schemaStr); const tokens = tokenizeSchema(schemaStr);
@ -27,28 +27,28 @@ export function parseCommandSchema(schemaStr: string, name?: string): CommandSch
if (inner.startsWith('--')) { if (inner.startsWith('--')) {
const result = parseOptionToken(inner.slice(2), false); const result = parseOptionToken(inner.slice(2), false);
if (result.isFlag) { if (result.isFlag) {
schema.flags.push({ name: result.name, short: result.short }); schema.flags[result.name] = { name: result.name, short: result.short };
} else { } else {
schema.options.push({ schema.options[result.name] = {
name: result.name, name: result.name,
short: result.short, short: result.short,
required: false, required: false,
defaultValue: result.defaultValue, defaultValue: result.defaultValue,
schema: result.schema, schema: result.schema,
}); };
} }
} else if (inner.startsWith('-') && inner.length > 1 && !inner.includes('--')) { } else if (inner.startsWith('-') && inner.length > 1 && !inner.includes('--')) {
const result = parseOptionToken(inner.slice(1), false); const result = parseOptionToken(inner.slice(1), false);
if (result.isFlag) { if (result.isFlag) {
schema.flags.push({ name: result.name, short: result.short || result.name }); schema.flags[result.name] = { name: result.name, short: result.short || result.name };
} else { } else {
schema.options.push({ schema.options[result.name] = {
name: result.name, name: result.name,
short: result.short || result.name, short: result.short || result.name,
required: false, required: false,
defaultValue: result.defaultValue, defaultValue: result.defaultValue,
schema: result.schema, schema: result.schema,
}); };
} }
} else { } else {
const isVariadic = inner.endsWith('...'); const isVariadic = inner.endsWith('...');
@ -78,29 +78,29 @@ export function parseCommandSchema(schemaStr: string, name?: string): CommandSch
} else if (token.startsWith('--')) { } else if (token.startsWith('--')) {
const result = parseOptionToken(token.slice(2), true); const result = parseOptionToken(token.slice(2), true);
if (result.isFlag) { if (result.isFlag) {
schema.flags.push({ name: result.name, short: result.short }); schema.flags[result.name] = { name: result.name, short: result.short };
} else { } else {
schema.options.push({ schema.options[result.name] = {
name: result.name, name: result.name,
short: result.short, short: result.short,
required: true, required: true,
defaultValue: result.defaultValue, defaultValue: result.defaultValue,
schema: result.schema, schema: result.schema,
}); };
} }
i++; 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 result = parseOptionToken(token.slice(1), true);
if (result.isFlag) { if (result.isFlag) {
schema.flags.push({ name: result.name, short: result.short || result.name }); schema.flags[result.name] = { name: result.name, short: result.short || result.name };
} else { } else {
schema.options.push({ schema.options[result.name] = {
name: result.name, name: result.name,
short: result.short || result.name, short: result.short || result.name,
required: true, required: true,
defaultValue: result.defaultValue, defaultValue: result.defaultValue,
schema: result.schema, schema: result.schema,
}); };
} }
i++; i++;
} else if (token.startsWith('<') && token.endsWith('>')) { } else if (token.startsWith('<') && token.endsWith('>')) {

View File

@ -30,8 +30,8 @@ export type CommandFlagSchema = {
export type CommandSchema = { export type CommandSchema = {
name: string; name: string;
params: CommandParamSchema[]; params: CommandParamSchema[];
options: CommandOptionSchema[]; options: Record<string, CommandOptionSchema>;
flags: CommandFlagSchema[]; flags: Record<string, CommandFlagSchema>;
} }
export interface ParsedOptionResult { export interface ParsedOptionResult {

View File

@ -28,8 +28,8 @@ describe('Rule System', () => {
expect(rule.schema.params[0].required).toBe(true); expect(rule.schema.params[0].required).toBe(true);
expect(rule.schema.params[1].name).toBe('to'); expect(rule.schema.params[1].name).toBe('to');
expect(rule.schema.params[1].required).toBe(true); expect(rule.schema.params[1].required).toBe(true);
expect(rule.schema.flags).toHaveLength(1); expect(Object.keys(rule.schema.flags)).toHaveLength(1);
expect(rule.schema.flags[0].name).toBe('force'); expect(rule.schema.flags.force.name).toBe('force');
}); });
it('should create a generator when called', () => { it('should create a generator when called', () => {

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.flags).toHaveLength(1); expect(Object.keys(schema.flags)).toHaveLength(1);
expect(schema.options).toHaveLength(1); expect(Object.keys(schema.options)).toHaveLength(1);
expect(schema.flags[0].name).toBe('all'); expect(schema.flags.all.name).toBe('all');
expect(schema.options[0].name).toBe('count'); expect(schema.options.count.name).toBe('count');
expect(schema.options[0].schema).toBeDefined(); expect(schema.options.count.schema).toBeDefined();
}); });
it('should parse schema with tuple type', () => { it('should parse schema with tuple type', () => {
@ -58,7 +58,7 @@ describe('parseCommandSchema with inline-schema', () => {
); );
expect(schema.name).toBe('move'); expect(schema.name).toBe('move');
expect(schema.params).toHaveLength(2); expect(schema.params).toHaveLength(2);
expect(schema.options).toHaveLength(1); expect(Object.keys(schema.options)).toHaveLength(1);
}); });
it('should parse schema with optional typed param', () => { it('should parse schema with optional typed param', () => {
@ -73,9 +73,9 @@ describe('parseCommandSchema with inline-schema', () => {
it('should parse schema with optional typed option', () => { it('should parse schema with optional typed option', () => {
const schema = parseCommandSchema('move <from> [--speed: number]'); const schema = parseCommandSchema('move <from> [--speed: number]');
expect(schema.name).toBe('move'); expect(schema.name).toBe('move');
expect(schema.options).toHaveLength(1); expect(Object.keys(schema.options)).toHaveLength(1);
expect(schema.options[0].required).toBe(false); expect(schema.options.speed.required).toBe(false);
expect(schema.options[0].schema).toBeDefined(); expect(schema.options.speed.schema).toBeDefined();
}); });
}); });

View File

@ -7,18 +7,8 @@ describe('parseCommandSchema', () => {
expect(schema).toEqual({ expect(schema).toEqual({
name: '', name: '',
params: [], params: [],
options: [], options: {},
flags: [], flags: {},
});
});
it('should parse command name only', () => {
const schema = parseCommandSchema('move');
expect(schema).toEqual({
name: 'move',
params: [],
options: [],
flags: [],
}); });
}); });
@ -55,34 +45,36 @@ describe('parseCommandSchema', () => {
it('should parse long flags', () => { it('should parse long flags', () => {
const schema = parseCommandSchema('move [--force] [--quiet]'); const schema = parseCommandSchema('move [--force] [--quiet]');
expect(schema.flags).toEqual([ expect(schema.flags).toEqual({
{ name: 'force' }, force: { name: 'force', short: undefined },
{ name: 'quiet' }, quiet: { name: 'quiet', short: undefined },
]); });
}); });
it('should parse short flags', () => { it('should parse short flags', () => {
const schema = parseCommandSchema('move [-f] [-q]'); const schema = parseCommandSchema('move [-f] [-q]');
expect(schema.flags).toEqual([ expect(schema.flags).toEqual({
{ name: 'f', short: 'f' }, f: { name: 'f', short: 'f' },
{ name: 'q', short: 'q' }, q: { name: 'q', short: 'q' },
]); });
}); });
it('should parse long options', () => { it('should parse long options', () => {
const schema = parseCommandSchema('move --x: string [--y: string]'); const schema = parseCommandSchema('move --x: string [--y: string]');
expect(schema.options).toEqual([ expect(Object.keys(schema.options)).toEqual(['x', 'y']);
{ name: 'x', required: true, schema: expect.any(Object) }, expect(schema.options.x).toMatchObject({ name: 'x', required: true });
{ name: 'y', required: false, schema: expect.any(Object) }, expect(schema.options.x.schema).toBeDefined();
]); expect(schema.options.y).toMatchObject({ name: 'y', required: false });
expect(schema.options.y.schema).toBeDefined();
}); });
it('should parse short options', () => { it('should parse short options', () => {
const schema = parseCommandSchema('move -x: string [-y: string]'); const schema = parseCommandSchema('move -x: string [-y: string]');
expect(schema.options).toEqual([ expect(Object.keys(schema.options)).toEqual(['x', 'y']);
{ name: 'x', short: 'x', required: true, schema: expect.any(Object) }, expect(schema.options.x).toMatchObject({ name: 'x', short: 'x', required: true });
{ name: 'y', short: 'y', required: false, schema: expect.any(Object) }, expect(schema.options.x.schema).toBeDefined();
]); expect(schema.options.y).toMatchObject({ name: 'y', short: 'y', required: false });
expect(schema.options.y.schema).toBeDefined();
}); });
it('should parse mixed schema', () => { it('should parse mixed schema', () => {
@ -93,13 +85,13 @@ describe('parseCommandSchema', () => {
{ name: 'from', required: true, variadic: false, schema: undefined }, { name: 'from', required: true, variadic: false, schema: undefined },
{ name: 'to', required: true, variadic: false, schema: undefined }, { name: 'to', required: true, variadic: false, schema: undefined },
], ],
flags: [ flags: {
{ name: 'force' }, force: { name: 'force', short: undefined },
{ name: 'f', short: 'f' }, f: { name: 'f', short: 'f' },
], },
options: [ options: {
{ name: 'speed', short: 's', required: false, schema: expect.any(Object), defaultValue: undefined }, speed: { name: 'speed', short: 's', required: false, schema: expect.any(Object), defaultValue: undefined },
], },
}); });
}); });
@ -107,8 +99,8 @@ describe('parseCommandSchema', () => {
const schema = parseCommandSchema('place <piece> <region> [x...] [--rotate: number] [--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(Object.keys(schema.flags)).toHaveLength(2);
expect(schema.options).toHaveLength(1); expect(Object.keys(schema.options)).toHaveLength(1);
}); });
}); });
@ -237,39 +229,8 @@ describe('integration', () => {
it('should parse short alias syntax', () => { it('should parse short alias syntax', () => {
const schema = parseCommandSchema('move <from> [--verbose: boolean -v]'); const schema = parseCommandSchema('move <from> [--verbose: boolean -v]');
expect(schema.flags).toHaveLength(1); expect(Object.keys(schema.flags)).toHaveLength(1);
expect(schema.flags[0]).toEqual({ name: 'verbose', short: 'v' }); expect(schema.flags.verbose).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', () => { it('should parse command with short alias', () => {
@ -288,3 +249,5 @@ describe('integration', () => {
expect(command.options.s).toBe('100'); expect(command.options.s).toBe('100');
}); });
}); });