params.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. type Primitive = string | number | boolean;
  2. type PatternToType<P> =
  3. P extends NumberConstructor ? number :
  4. P extends StringConstructor ? string :
  5. P extends BooleanConstructor ? boolean :
  6. P extends RegExp ? string :
  7. unknown;
  8. interface FieldSchema<Key extends string = string, T = Primitive> {
  9. key: Key;
  10. /**
  11. * 校验规则
  12. */
  13. pattern?: NumberConstructor | StringConstructor | BooleanConstructor | RegExp;
  14. /**
  15. * 自定义解析函数
  16. */
  17. parse?: (raw: string) => T;
  18. /**
  19. * 默认值
  20. */
  21. default?: T;
  22. /**
  23. * 是否允许重复字段
  24. */
  25. multiple?: boolean;
  26. /** 是否必填(无 default 时使用) */
  27. required?: boolean;
  28. }
  29. type InferField<S extends FieldSchema> = S['multiple'] extends true ? PatternToType<S['pattern']>[] : PatternToType<S['pattern']>;
  30. type IsRequired<S extends FieldSchema> = S['default'] extends undefined
  31. ? (S['required'] extends true ? true : false)
  32. : true;
  33. export type InferResult<T extends readonly FieldSchema[]> =
  34. & { [S in T[number] as IsRequired<S> extends true ? S['key'] : never]: InferField<S>; }
  35. & { [S in T[number] as IsRequired<S> extends true ? never : S['key']]?: InferField<S>; };
  36. export interface AnalysisOptions {
  37. /**
  38. * 严格模式
  39. */
  40. strict?: boolean;
  41. }
  42. /**
  43. * Streaming normalize
  44. * @description 支持
  45. * - 标准 URL (a=1&b=2)
  46. * - DSL 语法 (a:1;b:2)
  47. * - pipe 语法 (el:a|b|c → el=a&el=b&el=c)
  48. * - JSON 自动识别 (meta={"x":1,"y":[1,2]})
  49. * @param input
  50. * @return 标准 URL
  51. */
  52. function normalizeInput(input?: string): string {
  53. if (!input) return '';
  54. // 去掉前导 ?
  55. const source = input.startsWith('?') ? input.slice(1) : input;
  56. const result: string[] = [];
  57. let key = '';
  58. let value = '';
  59. let readingKey = true;
  60. let inJSON = false;
  61. let jsonDepth = 0;
  62. const pushPair = () => {
  63. if (!key) return;
  64. if (value.includes('|') && !inJSON) value.split('|').forEach((value) => result.push(`${key}=${value}`));
  65. else result.push(`${key}=${value}`);
  66. key = '';
  67. value = '';
  68. readingKey = true;
  69. };
  70. const jsonStart = ['{', '['];
  71. const jsonEnd = ['}', ']'];
  72. for (const char of source) {
  73. // JSON 开始
  74. if (!inJSON && jsonStart.includes(char)) {
  75. inJSON = true;
  76. jsonDepth = 1;
  77. value += char;
  78. continue;
  79. }
  80. // JSON 内部
  81. if (inJSON) {
  82. value += char;
  83. if (jsonStart.includes(char)) jsonDepth++;
  84. if (jsonEnd.includes(char)) jsonDepth--;
  85. if (jsonDepth === 0) inJSON = false;
  86. continue;
  87. }
  88. if (['&',';'].includes(char) && !inJSON) {
  89. pushPair();
  90. continue;
  91. }
  92. if ([':', '='].includes(char) && readingKey) {
  93. readingKey = false;
  94. continue;
  95. }
  96. if (readingKey) key += char;
  97. else value += char;
  98. }
  99. pushPair();
  100. return result.join('&');
  101. }
  102. /**
  103. * 自动解析 JSON
  104. */
  105. function autoParse(raw: string): any {
  106. const trimmed = raw.trim();
  107. if (
  108. (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
  109. (trimmed.startsWith('[') && trimmed.endsWith(']'))
  110. ) try { return JSON.parse(trimmed); } catch { return raw; }
  111. return raw;
  112. }
  113. /**
  114. * 自动解析并转换 pattern 类型
  115. */
  116. function parseValue(raw: string, pattern?: FieldSchema['pattern'], parseFn?: FieldSchema['parse'], strict?: boolean): any {
  117. let value = parseFn ? parseFn(raw) : autoParse(raw);
  118. // 原始类型自动转换
  119. if (pattern === String) value = String(value);
  120. else if (pattern === Number) {
  121. value = Number(value);
  122. if (isNaN(value) && strict) throw new Error(`Invalid number: ${raw}`);
  123. } else if (pattern === Boolean) {
  124. if (value === 'true' || value === '1' || value === true) value = true;
  125. else if (value === 'false' || value === '0' || value === false) value = false;
  126. else if (strict) throw new Error(`Invalid boolean: ${raw}`);
  127. else value = Boolean(value);
  128. } else if (pattern instanceof RegExp && !pattern.test(String(value)) && strict) throw new Error(`Invalid value: ${raw}`);
  129. return value;
  130. }
  131. /**
  132. * 参数解析器
  133. * @description
  134. * Raw Input
  135. * ↓ normalizeInput
  136. * ↓ URLSearchParams
  137. * ↓ parseValue (JSON + JS 类型 + pattern)
  138. * ↓ 类型安全输出
  139. * @example
  140. * ```
  141. * // 标准 URL
  142. * preset=1&el=debug&el=btn
  143. * // DSL 语法
  144. * preset:1;el:debug
  145. * // Pipe 语法
  146. * el:a|b|c
  147. * ```
  148. *
  149. * @param schema
  150. * @param value
  151. * @param options
  152. */
  153. export function analysis<T extends readonly FieldSchema[]>(schema: T, value?: string, options?: AnalysisOptions): InferResult<T> {
  154. const input = normalizeInput(value);
  155. const params = new URLSearchParams(input);
  156. const result: Record<string, unknown> = {};
  157. for (const { key, default: defaultValue, multiple, required, pattern, parse } of schema) {
  158. const matches = params.getAll(key);
  159. if (matches.length === 0) {
  160. if (defaultValue != null) result[key] = defaultValue;
  161. else if (required && options?.strict) throw new Error(`Missing required field: ${key}`);
  162. continue;
  163. }
  164. const values = matches.map(raw => parseValue(raw, pattern, parse, options?.strict));
  165. result[key] = multiple ? values : values[0];
  166. }
  167. return result as InferResult<T>;
  168. }