|
|
@@ -0,0 +1,187 @@
|
|
|
+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>;
|
|
|
+}
|