| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- type Primitive = string | number | boolean;
- type PatternToType<P> =
- P extends NumberConstructor ? number :
- P extends StringConstructor ? string :
- P extends BooleanConstructor ? boolean :
- P extends RegExp ? string :
- unknown;
- interface FieldSchema<Key extends string = string, T = Primitive> {
- key: Key;
- /**
- * 校验规则
- */
- pattern?: NumberConstructor | StringConstructor | BooleanConstructor | RegExp;
- /**
- * 自定义解析函数
- */
- parse?: (raw: string) => T;
- /**
- * 默认值
- */
- default?: T;
- /**
- * 是否允许重复字段
- */
- multiple?: boolean;
- /** 是否必填(无 default 时使用) */
- required?: boolean;
- }
- type InferField<S extends FieldSchema> = S['multiple'] extends true ? PatternToType<S['pattern']>[] : PatternToType<S['pattern']>;
- type IsRequired<S extends FieldSchema> = S['default'] extends undefined
- ? (S['required'] extends true ? true : false)
- : true;
- export type InferResult<T extends readonly FieldSchema[]> =
- & { [S in T[number] as IsRequired<S> extends true ? S['key'] : never]: InferField<S>; }
- & { [S in T[number] as IsRequired<S> extends true ? never : S['key']]?: InferField<S>; };
- export interface AnalysisOptions {
- /**
- * 严格模式
- */
- strict?: boolean;
- }
- /**
- * Streaming normalize
- * @description 支持
- * - 标准 URL (a=1&b=2)
- * - DSL 语法 (a:1;b:2)
- * - pipe 语法 (el:a|b|c → el=a&el=b&el=c)
- * - JSON 自动识别 (meta={"x":1,"y":[1,2]})
- * @param input
- * @return 标准 URL
- */
- function normalizeInput(input?: string): string {
- if (!input) return '';
- // 去掉前导 ?
- const source = input.startsWith('?') ? input.slice(1) : input;
- const result: string[] = [];
- let key = '';
- let value = '';
- let readingKey = true;
- let inJSON = false;
- let jsonDepth = 0;
- const pushPair = () => {
- if (!key) return;
- if (value.includes('|') && !inJSON) value.split('|').forEach((value) => result.push(`${key}=${value}`));
- else result.push(`${key}=${value}`);
- key = '';
- value = '';
- readingKey = true;
- };
- const jsonStart = ['{', '['];
- const jsonEnd = ['}', ']'];
- for (const char of source) {
- // JSON 开始
- if (!inJSON && jsonStart.includes(char)) {
- inJSON = true;
- jsonDepth = 1;
- value += char;
- continue;
- }
- // JSON 内部
- if (inJSON) {
- value += char;
- if (jsonStart.includes(char)) jsonDepth++;
- if (jsonEnd.includes(char)) jsonDepth--;
- if (jsonDepth === 0) inJSON = false;
- continue;
- }
- if (['&',';'].includes(char) && !inJSON) {
- pushPair();
- continue;
- }
- if ([':', '='].includes(char) && readingKey) {
- readingKey = false;
- continue;
- }
- if (readingKey) key += char;
- else value += char;
- }
- pushPair();
- return result.join('&');
- }
- /**
- * 自动解析 JSON
- */
- function autoParse(raw: string): any {
- const trimmed = raw.trim();
- if (
- (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
- (trimmed.startsWith('[') && trimmed.endsWith(']'))
- ) try { return JSON.parse(trimmed); } catch { return raw; }
- return raw;
- }
- /**
- * 自动解析并转换 pattern 类型
- */
- function parseValue(raw: string, pattern?: FieldSchema['pattern'], parseFn?: FieldSchema['parse'], strict?: boolean): any {
- let value = parseFn ? parseFn(raw) : autoParse(raw);
- // 原始类型自动转换
- if (pattern === String) value = String(value);
- else if (pattern === Number) {
- value = Number(value);
- if (isNaN(value) && strict) throw new Error(`Invalid number: ${raw}`);
- } else if (pattern === Boolean) {
- if (value === 'true' || value === '1' || value === true) value = true;
- else if (value === 'false' || value === '0' || value === false) value = false;
- else if (strict) throw new Error(`Invalid boolean: ${raw}`);
- else value = Boolean(value);
- } else if (pattern instanceof RegExp && !pattern.test(String(value)) && strict) throw new Error(`Invalid value: ${raw}`);
- return value;
- }
- /**
- * 参数解析器
- * @description
- * Raw Input
- * ↓ normalizeInput
- * ↓ URLSearchParams
- * ↓ parseValue (JSON + JS 类型 + pattern)
- * ↓ 类型安全输出
- * @example
- * ```
- * // 标准 URL
- * preset=1&el=debug&el=btn
- * // DSL 语法
- * preset:1;el:debug
- * // Pipe 语法
- * el:a|b|c
- * ```
- *
- * @param schema
- * @param value
- * @param options
- */
- export function analysis<T extends readonly FieldSchema[]>(schema: T, value?: string, options?: AnalysisOptions): InferResult<T> {
- const input = normalizeInput(value);
- const params = new URLSearchParams(input);
- const result: Record<string, unknown> = {};
- for (const { key, default: defaultValue, multiple, required, pattern, parse } of schema) {
- const matches = params.getAll(key);
- if (matches.length === 0) {
- if (defaultValue != null) result[key] = defaultValue;
- else if (required && options?.strict) throw new Error(`Missing required field: ${key}`);
- continue;
- }
- const values = matches.map(raw => parseValue(raw, pattern, parse, options?.strict));
- result[key] = multiple ? values : values[0];
- }
- return result as InferResult<T>;
- }
|