feat: add inline-schema for command schema

This commit is contained in:
hyper
2026-04-01 19:04:09 +08:00
parent 95015b090c
commit c315e0643b
4 changed files with 819 additions and 25 deletions
+257 -23
View File
@@ -1,8 +1,10 @@
export type Command = {
import { defineSchema, type ParsedSchema, ParseError } from 'inline-schema';
export type Command = {
name: string;
flags: Record<string, true>;
options: Record<string, string>;
params: string[];
options: Record<string, unknown>;
params: unknown[];
}
/**
@@ -15,6 +17,8 @@ export type CommandParamSchema = {
required: boolean;
/** 是否可变参数(可以接收多个值) */
variadic: boolean;
/** 参数类型 schema(用于解析和验证) */
schema?: ParsedSchema;
}
/**
@@ -28,7 +32,9 @@ export type CommandOptionSchema = {
/** 是否必需 */
required: boolean;
/** 默认值 */
defaultValue?: string;
defaultValue?: unknown;
/** 选项类型 schema(用于解析和验证) */
schema?: ParsedSchema;
}
/**
@@ -75,9 +81,9 @@ export function parseCommand(input: string): Command {
}
const name = tokens[0];
const params: string[] = [];
const params: unknown[] = [];
const flags: Record<string, true> = {};
const options: Record<string, string> = {};
const options: Record<string, unknown> = {};
let i = 1;
while (i < tokens.length) {
@@ -181,13 +187,24 @@ function tokenize(input: string): string[] {
* - [param] 可选参数
* - <param...> 必需可变参数
* - [param...] 可选可变参数
* - <param: type> 带类型定义的必需参数
* - [param: type] 带类型定义的可选参数
* - --flag 长格式标志
* - -f 短格式标志
* - --option <value> 长格式选项
* - --option: type 带类型的长格式选项
* - -o <value> 短格式选项
* - -o: type 带类型的短格式选项
*
* 类型语法使用 inline-schema 格式(使用 ; 而非 ,):
* - string, number, boolean
* - [string; number] 元组
* - string[] 数组
* - [string; number][] 元组数组
*
* @example
* parseCommandSchema('move <from> [to...] [--force] [-f] [--speed <val>]')
* parseCommandSchema('move <from: [x: string; y: string]> <to: string> [--all: boolean]')
*/
export function parseCommandSchema(schemaStr: string): CommandSchema {
const schema: CommandSchema = {
@@ -212,18 +229,40 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
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);
// 如果有额外的部分,则是选项(如 --opt value 或 --opt <value>
if (parts.length > 1) {
// 可选选项
// 检查是否有类型定义(如 --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 {
// 可选标志
@@ -233,14 +272,35 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
// 可选短格式标志或选项
const parts = inner.split(/\s+/);
const short = parts[0].slice(1);
// 如果有额外的部分,则是选项
if (parts.length > 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 {
// 可选标志
@@ -249,12 +309,25 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
} else {
// 可选参数
const isVariadic = inner.endsWith('...');
const name = isVariadic ? inner.slice(0, -3) : inner;
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 {
parsedSchema = defineSchema(typeStr);
} catch {
// 不是有效的 schema
}
paramContent = name;
}
schema.params.push({
name,
name: paramContent,
required: false,
variadic: isVariadic,
schema: parsedSchema,
});
}
i++;
@@ -263,11 +336,30 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
const name = token.slice(2);
const nextToken = tokens[i + 1];
// 如果下一个 token 是 <value> 格式,则是选项
if (nextToken && nextToken.startsWith('<') && nextToken.endsWith('>')) {
// 检查是否有类型定义(如 --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 {
@@ -280,12 +372,32 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
const short = token.slice(1);
const nextToken = tokens[i + 1];
// 如果下一个 token 是 <value> 格式,则是选项
if (nextToken && nextToken.startsWith('<') && nextToken.endsWith('>')) {
// 检查是否有类型定义
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 {
@@ -296,12 +408,27 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
} else if (token.startsWith('<') && token.endsWith('>')) {
// 必需参数
const isVariadic = token.endsWith('...>');
const name = token.replace(/^[<]+|[>.>]+$/g, '');
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();
const typeStr = paramContent.slice(colonIndex + 1).trim();
try {
parsedSchema = defineSchema(typeStr);
} catch (e) {
// 不是有效的 schema
}
paramContent = name;
}
schema.params.push({
name,
name: paramContent,
required: true,
variadic: isVariadic,
schema: parsedSchema,
});
i++;
} else {
@@ -450,3 +577,110 @@ export function validateCommand(
return { valid: true };
}
/**
* 根据 schema 解析并验证命令,返回类型化的命令对象
* 如果 schema 中定义了类型,会自动解析参数和选项的值
*
* @param input 命令行输入字符串
* @param schemaStr 命令 schema 字符串
* @returns 解析后的命令对象和验证结果
*
* @example
* const result = parseCommandWithSchema(
* 'move [1; 2] region1 --all true',
* 'move <from: [x: string; y: string]> <to: string> [--all: boolean]'
* );
* // result.command.params[0] = ['1', '2'] (已解析为元组)
* // result.command.options.all = true (已解析为布尔值)
*/
export function parseCommandWithSchema(
input: string,
schemaStr: string
): { command: Command; valid: true } | { command: Command; valid: false; errors: string[] } {
const schema = parseCommandSchema(schemaStr);
const command = parseCommand(input);
// 验证命令名称
if (command.name !== schema.name) {
return {
command,
valid: false,
errors: [`命令名称不匹配:期望 "${schema.name}",实际 "${command.name}"`],
};
}
const errors: string[] = [];
// 验证参数数量
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}`);
return { command, valid: false, errors };
}
if (!variadicParam && command.params.length > schema.params.length) {
errors.push(`参数过多:最多 ${schema.params.length} 个参数,实际 ${command.params.length}`);
return { command, valid: false, errors };
}
// 验证必需的选项
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}` : ''}`);
}
}
if (errors.length > 0) {
return { command, valid: false, errors };
}
// 使用 schema 解析参数值
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 {
// 如果是字符串值,使用 schema 解析
const parsed = typeof paramValue === 'string'
? paramSchema.parse(paramValue)
: paramValue;
parsedParams.push(parsed);
} catch (e) {
const err = e as ParseError;
errors.push(`参数 "${schema.params[i]?.name}" 解析失败:${err.message}`);
}
} else {
parsedParams.push(paramValue);
}
}
// 使用 schema 解析选项值
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;
errors.push(`选项 "--${key}" 解析失败:${err.message}`);
}
}
}
if (errors.length > 0) {
return { command: { ...command, params: parsedParams, options: parsedOptions }, valid: false, errors };
}
return {
command: { ...command, params: parsedParams, options: parsedOptions },
valid: true,
};
}