aetherjs-router 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +541 -0
- package/index.js +21 -0
- package/package.json +41 -0
- package/src/aether-adapter.js +98 -0
- package/src/examples/basic-router.js +796 -0
- package/src/path-compiler.js +660 -0
- package/src/route-factory.js +326 -0
- package/src/router-factory.js +840 -0
- package/src/test/benchmark/router-benchmark.test.js +561 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
// src/path-compiler.js - Enhanced path compiler with query parameter support
|
|
2
|
+
// Custom path parameter parser with query parameter support
|
|
3
|
+
class PathParser {
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
this.options = {
|
|
6
|
+
sensitive: false,
|
|
7
|
+
strict: false,
|
|
8
|
+
end: true,
|
|
9
|
+
delimiter: '/',
|
|
10
|
+
delimiters: './',
|
|
11
|
+
parseQuery: false, // New option to enable query parameter parsing
|
|
12
|
+
...options
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse path pattern, extract parameter names and regular expressions
|
|
18
|
+
* Now supports query parameter patterns like /users?id=:id&lang=:lang
|
|
19
|
+
* @param {string} path - Path pattern, e.g., '/users/:id' or '/users?id=:id'
|
|
20
|
+
* @returns {Object} Contains regular expression and parameter keys array
|
|
21
|
+
*/
|
|
22
|
+
parse(path) {
|
|
23
|
+
// Separate path and query string
|
|
24
|
+
const [pathPart, queryPart] = path.split('?');
|
|
25
|
+
|
|
26
|
+
// Parse path portion
|
|
27
|
+
const pathResult = this._parsePath(pathPart);
|
|
28
|
+
|
|
29
|
+
// Parse query string portion if enabled
|
|
30
|
+
if (this.options.parseQuery && queryPart) {
|
|
31
|
+
const queryResult = this._parseQuery(queryPart);
|
|
32
|
+
return {
|
|
33
|
+
regex: pathResult.regex,
|
|
34
|
+
keys: [...pathResult.keys, ...queryResult.keys],
|
|
35
|
+
hasQuery: true,
|
|
36
|
+
queryPattern: queryPart
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
...pathResult,
|
|
42
|
+
hasQuery: false
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse the path portion (original implementation)
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
_parsePath(path) {
|
|
51
|
+
const keys = [];
|
|
52
|
+
let pattern = '';
|
|
53
|
+
let inParam = false;
|
|
54
|
+
let paramName = '';
|
|
55
|
+
let optional = false;
|
|
56
|
+
let patternPart = '';
|
|
57
|
+
|
|
58
|
+
// Escape regex special characters
|
|
59
|
+
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < path.length; i++) {
|
|
62
|
+
const char = path[i];
|
|
63
|
+
|
|
64
|
+
if (char === ':' && !inParam) {
|
|
65
|
+
// Start parameter matching
|
|
66
|
+
inParam = true;
|
|
67
|
+
paramName = '';
|
|
68
|
+
patternPart = '';
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (inParam) {
|
|
73
|
+
if (char === '(') {
|
|
74
|
+
// Start custom pattern
|
|
75
|
+
let depth = 1;
|
|
76
|
+
i++;
|
|
77
|
+
while (i < path.length && depth > 0) {
|
|
78
|
+
if (path[i] === '(') depth++;
|
|
79
|
+
else if (path[i] === ')') depth--;
|
|
80
|
+
if (depth > 0) patternPart += path[i];
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
i--; // Step back one position
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (char === '?' && i === path.length - 1) {
|
|
88
|
+
// Optional parameter
|
|
89
|
+
optional = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (char === this.options.delimiter || i === path.length - 1) {
|
|
94
|
+
// Parameter ends
|
|
95
|
+
if (i === path.length - 1) {
|
|
96
|
+
paramName += char;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Add parameter key
|
|
100
|
+
keys.push({
|
|
101
|
+
name: paramName,
|
|
102
|
+
optional: optional,
|
|
103
|
+
pattern: patternPart || '[^\\/]+',
|
|
104
|
+
type: 'path' // Mark as path parameter
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Build regex part
|
|
108
|
+
const patternStr = patternPart || '[^\\/]+';
|
|
109
|
+
pattern += `(${patternStr})`;
|
|
110
|
+
|
|
111
|
+
// Reset state
|
|
112
|
+
inParam = false;
|
|
113
|
+
paramName = '';
|
|
114
|
+
optional = false;
|
|
115
|
+
patternPart = '';
|
|
116
|
+
|
|
117
|
+
if (i === path.length - 1 && char !== this.options.delimiter) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
paramName += char;
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// Regular characters
|
|
125
|
+
if (char === '*') {
|
|
126
|
+
// Wildcard matching
|
|
127
|
+
keys.push({
|
|
128
|
+
name: 'wildcard',
|
|
129
|
+
optional: false,
|
|
130
|
+
pattern: '.*',
|
|
131
|
+
type: 'path'
|
|
132
|
+
});
|
|
133
|
+
pattern += '(.*)';
|
|
134
|
+
} else if (char === '?') {
|
|
135
|
+
// Optional character
|
|
136
|
+
pattern += '.?';
|
|
137
|
+
} else if (char === '+') {
|
|
138
|
+
// One or more characters
|
|
139
|
+
pattern += '.+';
|
|
140
|
+
} else if ('()[]{}|^$'.includes(char)) {
|
|
141
|
+
// Regex special characters, need escaping
|
|
142
|
+
pattern += '\\' + char;
|
|
143
|
+
} else {
|
|
144
|
+
pattern += escapeRegex(char);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Handle the last parameter
|
|
150
|
+
if (inParam && paramName) {
|
|
151
|
+
keys.push({
|
|
152
|
+
name: paramName,
|
|
153
|
+
optional: optional,
|
|
154
|
+
pattern: patternPart || '[^\\/]+',
|
|
155
|
+
type: 'path'
|
|
156
|
+
});
|
|
157
|
+
pattern += `(${patternPart || '[^\\/]+'})`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Build complete regular expression
|
|
161
|
+
let regexStr = pattern;
|
|
162
|
+
if (!this.options.strict) {
|
|
163
|
+
regexStr = regexStr.replace(/\\\//g, '[\\/]?');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (this.options.end) {
|
|
167
|
+
regexStr += '$';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const flags = this.options.sensitive ? '' : 'i';
|
|
171
|
+
const regex = new RegExp('^' + regexStr, flags);
|
|
172
|
+
|
|
173
|
+
return { regex, keys };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse query string portion for query parameters
|
|
178
|
+
* Supports patterns like ?id=:id&lang=:lang
|
|
179
|
+
* @private
|
|
180
|
+
*/
|
|
181
|
+
_parseQuery(queryString) {
|
|
182
|
+
const keys = [];
|
|
183
|
+
const params = new URLSearchParams(queryString);
|
|
184
|
+
|
|
185
|
+
for (const [key, value] of params) {
|
|
186
|
+
if (value.startsWith(':')) {
|
|
187
|
+
// Query parameter with pattern
|
|
188
|
+
const paramName = value.slice(1);
|
|
189
|
+
const isOptional = paramName.endsWith('?');
|
|
190
|
+
const cleanName = isOptional ? paramName.slice(0, -1) : paramName;
|
|
191
|
+
|
|
192
|
+
keys.push({
|
|
193
|
+
name: cleanName,
|
|
194
|
+
optional: isOptional,
|
|
195
|
+
pattern: '[^&]*', // Default pattern for query values
|
|
196
|
+
type: 'query',
|
|
197
|
+
queryKey: key
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { keys };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Extract parameters from full URL including query string
|
|
207
|
+
* @param {string} url - Full URL with query string
|
|
208
|
+
* @param {Object} compiled - Compiled route information
|
|
209
|
+
* @returns {Object} Extracted parameters
|
|
210
|
+
*/
|
|
211
|
+
extractParams(url, compiled) {
|
|
212
|
+
const [path, queryString] = url.split('?');
|
|
213
|
+
const params = {};
|
|
214
|
+
|
|
215
|
+
// Extract path parameters
|
|
216
|
+
const pathMatch = compiled.regex.exec(path);
|
|
217
|
+
if (pathMatch) {
|
|
218
|
+
compiled.keys.forEach((key, index) => {
|
|
219
|
+
if (key.type === 'path' && pathMatch[index + 1] !== undefined) {
|
|
220
|
+
params[key.name] = decodeURIComponent(pathMatch[index + 1]);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Extract query parameters
|
|
226
|
+
if (queryString && compiled.hasQuery) {
|
|
227
|
+
const queryParams = new URLSearchParams(queryString);
|
|
228
|
+
compiled.keys.forEach(key => {
|
|
229
|
+
if (key.type === 'query' && queryParams.has(key.queryKey)) {
|
|
230
|
+
params[key.name] = queryParams.get(key.queryKey);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return params;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Factory function to create path compiler (with caching)
|
|
241
|
+
* @param {Object} options - Compiler options
|
|
242
|
+
* @returns {Object} Path compiler instance
|
|
243
|
+
*/
|
|
244
|
+
export function createPathCompiler(options = {}) {
|
|
245
|
+
const cache = new Map();
|
|
246
|
+
const parser = new PathParser(options);
|
|
247
|
+
|
|
248
|
+
let hits = 0;
|
|
249
|
+
let misses = 0;
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
/**
|
|
253
|
+
* Compile path pattern to regular expression
|
|
254
|
+
* Now supports query parameter patterns
|
|
255
|
+
* @param {string} path - Path pattern, e.g., '/users?id=:id&lang=:lang'
|
|
256
|
+
* @param {Object} options - Compilation options
|
|
257
|
+
* @returns {Object} Compiled regex and keys
|
|
258
|
+
*/
|
|
259
|
+
compile(path, options = {}) {
|
|
260
|
+
const cacheKey = `${path}:${JSON.stringify(options)}`;
|
|
261
|
+
|
|
262
|
+
// Return cached result if available
|
|
263
|
+
if (cache.has(cacheKey)) {
|
|
264
|
+
hits++;
|
|
265
|
+
return cache.get(cacheKey);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
misses++;
|
|
269
|
+
|
|
270
|
+
// Merge options
|
|
271
|
+
const mergedOptions = { ...parser.options, ...options };
|
|
272
|
+
const tempParser = new PathParser(mergedOptions);
|
|
273
|
+
const result = tempParser.parse(path);
|
|
274
|
+
|
|
275
|
+
// Cache result
|
|
276
|
+
cache.set(cacheKey, result);
|
|
277
|
+
|
|
278
|
+
return result;
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Extract parameters from URL with query string support
|
|
283
|
+
* @param {string} url - Full URL
|
|
284
|
+
* @param {string} pattern - Path pattern
|
|
285
|
+
* @param {Object} options - Compilation options
|
|
286
|
+
* @returns {Object|null} Parameter object or null
|
|
287
|
+
*/
|
|
288
|
+
extractParams(url, pattern, options = {}) {
|
|
289
|
+
const compiled = this.compile(pattern, options);
|
|
290
|
+
const parser = new PathParser({ ...this.options, ...options });
|
|
291
|
+
return parser.extractParams(url, compiled);
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Clear compilation cache
|
|
296
|
+
*/
|
|
297
|
+
clearCache() {
|
|
298
|
+
cache.clear();
|
|
299
|
+
hits = 0;
|
|
300
|
+
misses = 0;
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get cache statistics
|
|
305
|
+
* @returns {Object} Cache statistics
|
|
306
|
+
*/
|
|
307
|
+
getCacheStats() {
|
|
308
|
+
return {
|
|
309
|
+
size: cache.size,
|
|
310
|
+
hits,
|
|
311
|
+
misses,
|
|
312
|
+
hitRate: hits + misses > 0 ? (hits / (hits + misses)) * 100 : 0
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Precompile common path patterns
|
|
318
|
+
* @param {Array} patterns - Array of path patterns
|
|
319
|
+
*/
|
|
320
|
+
precompile(patterns) {
|
|
321
|
+
patterns.forEach(pattern => {
|
|
322
|
+
this.compile(pattern);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Common path pattern regex generators
|
|
330
|
+
*/
|
|
331
|
+
export const pathPatterns = {
|
|
332
|
+
// Numeric ID
|
|
333
|
+
id: '\\d+',
|
|
334
|
+
|
|
335
|
+
// Alphanumeric ID
|
|
336
|
+
slug: '[a-zA-Z0-9_-]+',
|
|
337
|
+
|
|
338
|
+
// UUID
|
|
339
|
+
uuid: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
|
|
340
|
+
|
|
341
|
+
// Filename
|
|
342
|
+
filename: '[^\\/]+',
|
|
343
|
+
|
|
344
|
+
// File extension
|
|
345
|
+
extension: '\\.[a-zA-Z0-9]+',
|
|
346
|
+
|
|
347
|
+
// Year-month-day
|
|
348
|
+
date: '\\d{4}-\\d{2}-\\d{2}',
|
|
349
|
+
|
|
350
|
+
// Version number
|
|
351
|
+
version: '\\d+(\\.\\d+)*',
|
|
352
|
+
|
|
353
|
+
// Query parameter patterns
|
|
354
|
+
query: {
|
|
355
|
+
id: '\\d+',
|
|
356
|
+
slug: '[a-zA-Z0-9_-]+',
|
|
357
|
+
uuid: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
|
|
358
|
+
page: '\\d+',
|
|
359
|
+
limit: '\\d+',
|
|
360
|
+
sort: '[a-zA-Z_]+',
|
|
361
|
+
order: '(asc|desc)'
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Quick compilation function (using default compiler)
|
|
367
|
+
*/
|
|
368
|
+
const defaultCompiler = createPathCompiler();
|
|
369
|
+
|
|
370
|
+
export function compilePath(path, options = {}) {
|
|
371
|
+
return defaultCompiler.compile(path, options);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Path compiler class (for type checking)
|
|
376
|
+
*/
|
|
377
|
+
export class PathCompiler {
|
|
378
|
+
constructor(options = {}) {
|
|
379
|
+
this._impl = createPathCompiler(options);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
compile(path, options = {}) {
|
|
383
|
+
return this._impl.compile(path, options);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
extractParams(url, pattern, options = {}) {
|
|
387
|
+
return this._impl.extractParams(url, pattern, options);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
clearCache() {
|
|
391
|
+
return this._impl.clearCache();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
getCacheStats() {
|
|
395
|
+
return this._impl.getCacheStats();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
precompile(patterns) {
|
|
399
|
+
return this._impl.precompile(patterns);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Utility functions with query parameter support
|
|
405
|
+
*/
|
|
406
|
+
export const pathUtils = {
|
|
407
|
+
/**
|
|
408
|
+
* Check if path matches pattern (including query parameters)
|
|
409
|
+
* @param {string} url - Full URL to check
|
|
410
|
+
* @param {string} pattern - Path pattern (may include query parameters)
|
|
411
|
+
* @param {Object} options - Compilation options
|
|
412
|
+
* @returns {boolean} Whether it matches
|
|
413
|
+
*/
|
|
414
|
+
match(url, pattern, options = {}) {
|
|
415
|
+
const [path, queryString] = url.split('?');
|
|
416
|
+
const { regex } = compilePath(pattern, options);
|
|
417
|
+
|
|
418
|
+
// Check if pattern has query parameters
|
|
419
|
+
if (pattern.includes('?')) {
|
|
420
|
+
const [pathPattern] = pattern.split('?');
|
|
421
|
+
const pathRegex = compilePath(pathPattern, options).regex;
|
|
422
|
+
|
|
423
|
+
// Match path first
|
|
424
|
+
if (!pathRegex.test(path)) {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// If pattern has query parameters, check if they match
|
|
429
|
+
if (queryString) {
|
|
430
|
+
const queryParams = new URLSearchParams(queryString);
|
|
431
|
+
const patternParams = new URLSearchParams(pattern.split('?')[1]);
|
|
432
|
+
|
|
433
|
+
for (const [key, value] of patternParams) {
|
|
434
|
+
if (value.startsWith(':')) {
|
|
435
|
+
// This is a query parameter pattern, skip validation
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Static query parameter value must match
|
|
440
|
+
if (!queryParams.has(key) || queryParams.get(key) !== value) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Simple path matching
|
|
450
|
+
return regex.test(path);
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Extract parameters from URL (including query parameters)
|
|
455
|
+
* @param {string} url - Full URL to parse
|
|
456
|
+
* @param {string} pattern - Path pattern (may include query parameters)
|
|
457
|
+
* @param {Object} options - Compilation options
|
|
458
|
+
* @returns {Object|null} Parameter object or null
|
|
459
|
+
*/
|
|
460
|
+
extractParams(url, pattern, options = {}) {
|
|
461
|
+
const compiler = createPathCompiler({ ...options, parseQuery: true });
|
|
462
|
+
return compiler.extractParams(url, pattern);
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Build URL with query parameters
|
|
467
|
+
* @param {string} pattern - Path pattern
|
|
468
|
+
* @param {Object} params - Parameter object (including query params)
|
|
469
|
+
* @returns {string} Built URL
|
|
470
|
+
*/
|
|
471
|
+
buildUrl(pattern, params = {}) {
|
|
472
|
+
let [pathPart, queryPart] = pattern.split('?');
|
|
473
|
+
|
|
474
|
+
// Replace path parameters
|
|
475
|
+
pathPart = pathPart.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)($[^)]+$)?(\?)?/g, (match, paramName) => {
|
|
476
|
+
return params[paramName] !== undefined ? params[paramName] : match;
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Replace wildcards in path
|
|
480
|
+
pathPart = pathPart.replace(/\*/g, () => {
|
|
481
|
+
return params['wildcard'] !== undefined ? params['wildcard'] : '*';
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Build query string if pattern has query parameters
|
|
485
|
+
let queryString = '';
|
|
486
|
+
if (queryPart) {
|
|
487
|
+
const queryParams = new URLSearchParams(queryPart);
|
|
488
|
+
const resultParams = new URLSearchParams();
|
|
489
|
+
|
|
490
|
+
for (const [key, value] of queryParams) {
|
|
491
|
+
if (value.startsWith(':')) {
|
|
492
|
+
const paramName = value.slice(1).replace(/\?$/, '');
|
|
493
|
+
if (params[paramName] !== undefined) {
|
|
494
|
+
resultParams.set(key, params[paramName]);
|
|
495
|
+
} else if (!value.endsWith('?')) {
|
|
496
|
+
// Required query parameter missing
|
|
497
|
+
throw new Error(`Missing required query parameter: ${paramName}`);
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
resultParams.set(key, value);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
queryString = resultParams.toString();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Add additional query parameters not in pattern
|
|
508
|
+
const additionalParams = new URLSearchParams();
|
|
509
|
+
Object.keys(params).forEach(key => {
|
|
510
|
+
// Skip path parameters and wildcard
|
|
511
|
+
if (!pathPart.includes(`:${key}`) && key !== 'wildcard') {
|
|
512
|
+
// Check if this is a query parameter from pattern
|
|
513
|
+
const isPatternParam = queryPart && queryPart.includes(`:${key}`);
|
|
514
|
+
if (!isPatternParam) {
|
|
515
|
+
additionalParams.set(key, params[key]);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const additionalQuery = additionalParams.toString();
|
|
521
|
+
|
|
522
|
+
// Combine all query parameters
|
|
523
|
+
let finalQuery = '';
|
|
524
|
+
if (queryString && additionalQuery) {
|
|
525
|
+
finalQuery = `?${queryString}&${additionalQuery}`;
|
|
526
|
+
} else if (queryString) {
|
|
527
|
+
finalQuery = `?${queryString}`;
|
|
528
|
+
} else if (additionalQuery) {
|
|
529
|
+
finalQuery = `?${additionalQuery}`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return pathPart + finalQuery;
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Validate parameters (including query parameters)
|
|
537
|
+
* @param {Object} params - Parameter object
|
|
538
|
+
* @param {Object} validators - Validator object
|
|
539
|
+
* @returns {Object} Validation result
|
|
540
|
+
*/
|
|
541
|
+
validateParams(params, validators) {
|
|
542
|
+
const errors = [];
|
|
543
|
+
const validated = {};
|
|
544
|
+
|
|
545
|
+
Object.keys(validators).forEach(key => {
|
|
546
|
+
const value = params[key];
|
|
547
|
+
const validator = validators[key];
|
|
548
|
+
|
|
549
|
+
if (validator.required && (value === undefined || value === null || value === '')) {
|
|
550
|
+
errors.push(`Parameter "${key}" is required`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (value !== undefined && value !== null) {
|
|
555
|
+
// Type validation
|
|
556
|
+
if (validator.type) {
|
|
557
|
+
const type = typeof value;
|
|
558
|
+
if (validator.type === 'number' && isNaN(Number(value))) {
|
|
559
|
+
errors.push(`Parameter "${key}" must be a number`);
|
|
560
|
+
} else if (validator.type === 'integer' && !Number.isInteger(Number(value))) {
|
|
561
|
+
errors.push(`Parameter "${key}" must be an integer`);
|
|
562
|
+
} else if (validator.type === 'string' && typeof value !== 'string') {
|
|
563
|
+
errors.push(`Parameter "${key}" must be a string`);
|
|
564
|
+
} else if (validator.type === 'boolean' && typeof value !== 'boolean' && value !== 'true' && value !== 'false') {
|
|
565
|
+
errors.push(`Parameter "${key}" must be a boolean`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Pattern validation
|
|
570
|
+
if (validator.pattern && !new RegExp(validator.pattern).test(value)) {
|
|
571
|
+
errors.push(`Parameter "${key}" has invalid format`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Enum validation
|
|
575
|
+
if (validator.enum && !validator.enum.includes(value)) {
|
|
576
|
+
errors.push(`Parameter "${key}" must be one of: ${validator.enum.join(', ')}`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Range validation
|
|
580
|
+
if (validator.min !== undefined && Number(value) < validator.min) {
|
|
581
|
+
errors.push(`Parameter "${key}" cannot be less than ${validator.min}`);
|
|
582
|
+
}
|
|
583
|
+
if (validator.max !== undefined && Number(value) > validator.max) {
|
|
584
|
+
errors.push(`Parameter "${key}" cannot be greater than ${validator.max}`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Length validation
|
|
588
|
+
if (validator.minLength !== undefined && value.length < validator.minLength) {
|
|
589
|
+
errors.push(`Parameter "${key}" length cannot be less than ${validator.minLength}`);
|
|
590
|
+
}
|
|
591
|
+
if (validator.maxLength !== undefined && value.length > validator.maxLength) {
|
|
592
|
+
errors.push(`Parameter "${key}" length cannot be greater than ${validator.maxLength}`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
validated[key] = value;
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
return {
|
|
600
|
+
isValid: errors.length === 0,
|
|
601
|
+
errors,
|
|
602
|
+
validated
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Example usage with query parameter support
|
|
609
|
+
*/
|
|
610
|
+
export const examples = {
|
|
611
|
+
basic: () => {
|
|
612
|
+
const compiler = createPathCompiler({ parseQuery: true });
|
|
613
|
+
|
|
614
|
+
// Compile path pattern with query parameters
|
|
615
|
+
const result = compiler.compile('/users?id=:id&lang=:lang');
|
|
616
|
+
console.log('Compilation result:', result);
|
|
617
|
+
|
|
618
|
+
// Test matching with query parameters
|
|
619
|
+
const match = pathUtils.match('/users?id=123&lang=en', '/users?id=:id&lang=:lang');
|
|
620
|
+
console.log('Match /users?id=123&lang=en:', match);
|
|
621
|
+
|
|
622
|
+
// Extract parameters
|
|
623
|
+
const params = pathUtils.extractParams('/users?id=123&lang=en', '/users?id=:id&lang=:lang');
|
|
624
|
+
console.log('Extracted parameters:', params);
|
|
625
|
+
},
|
|
626
|
+
|
|
627
|
+
advanced: () => {
|
|
628
|
+
const compiler = createPathCompiler({ parseQuery: true });
|
|
629
|
+
|
|
630
|
+
// Complex patterns with query parameters
|
|
631
|
+
const patterns = [
|
|
632
|
+
'/users/:id', // Path parameter only
|
|
633
|
+
'/users?id=:id', // Query parameter only
|
|
634
|
+
'/users/:id?page=:page&limit=:limit', // Mixed path and query params
|
|
635
|
+
'/search?q=:query&page=:page?', // Optional query parameter
|
|
636
|
+
'/api/:version/users?sort=:sort&order=:order' // Versioned API with query params
|
|
637
|
+
];
|
|
638
|
+
|
|
639
|
+
patterns.forEach(pattern => {
|
|
640
|
+
const { regex, keys } = compiler.compile(pattern);
|
|
641
|
+
console.log(`Pattern: ${pattern}`);
|
|
642
|
+
console.log(`Regex: ${regex}`);
|
|
643
|
+
console.log(`Parameters: ${keys.map(k => `${k.name} (${k.type})`).join(', ')}`);
|
|
644
|
+
console.log('---');
|
|
645
|
+
});
|
|
646
|
+
},
|
|
647
|
+
|
|
648
|
+
buildUrlExample: () => {
|
|
649
|
+
// Build URL with parameters
|
|
650
|
+
const url = pathUtils.buildUrl('/users/:id?page=:page&lang=:lang', {
|
|
651
|
+
id: '123',
|
|
652
|
+
page: '2',
|
|
653
|
+
lang: 'en',
|
|
654
|
+
extra: 'value' // Additional query parameter
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
console.log('Built URL:', url);
|
|
658
|
+
// Output: /users/123?page=2&lang=en&extra=value
|
|
659
|
+
}
|
|
660
|
+
};
|