@cookbook/urlkit 1.1.0 → 1.2.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 +32 -35
- package/dist/{compile-path-COU1uxXm.js → compile-path-oMMfpXR8.js} +321 -84
- package/dist/compile-path-oMMfpXR8.js.map +1 -0
- package/dist/{compile-static-search-CCwJuNC3.js → compile-static-search-c2f2FB_l.js} +2 -2
- package/dist/{compile-static-search-CCwJuNC3.js.map → compile-static-search-c2f2FB_l.js.map} +1 -1
- package/dist/{create-url-contract-DAU3UCJ6.js → create-url-contract-DcYa3Jv4.js} +2 -2
- package/dist/{create-url-contract-DAU3UCJ6.js.map → create-url-contract-DcYa3Jv4.js.map} +1 -1
- package/dist/date/contracts.d.ts +1 -0
- package/dist/date/date-format-string.d.ts +9 -0
- package/dist/index.js +43 -6
- package/dist/index.js.map +1 -1
- package/dist/router-runtime.js +6 -5
- package/dist/router-runtime.js.map +1 -1
- package/dist/schema/date-time.d.ts +7 -1
- package/dist/schema/date.d.ts +2 -2
- package/dist/static.js +4 -3
- package/dist/static.js.map +1 -1
- package/dist/url/path-param-kind.d.ts +2 -1
- package/package.json +1 -1
- package/dist/compile-path-COU1uxXm.js.map +0 -1
- package/dist/url/register-urlkit-path-constraints.d.ts +0 -1
package/README.md
CHANGED
|
@@ -181,7 +181,7 @@ ArticleUrl.build({ params: { slug: 'post-1' } });
|
|
|
181
181
|
// ArticleUrl.build({ pathname: '/articles/post-1' });
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
-
Path params are inferred from the pattern. Built-in `int`, `decimal` and `range` path constraints parse to numbers in standalone `url(...)` contracts.
|
|
184
|
+
Path params are inferred from the pattern. Built-in `int`, `decimal` and `range` path constraints parse to numbers in standalone `url(...)` contracts. PathKit does not expose a `{param:number}` built-in; use `{param:decimal}` for finite decimal path values.
|
|
185
185
|
|
|
186
186
|
```ts
|
|
187
187
|
const UserUrl = url({ path: '/users/{id:int}' });
|
|
@@ -192,7 +192,7 @@ const user = UserUrl.parse('/users/42');
|
|
|
192
192
|
|
|
193
193
|
### Custom path constraints
|
|
194
194
|
|
|
195
|
-
URLKit re-exports PathKit's `createConstraint` and provides global registration helpers for reusable path constraints. Custom constraints infer `string` params by default; built-in `int`, `decimal` and `range` still infer `number`.
|
|
195
|
+
URLKit re-exports PathKit's `createConstraint` and provides global registration helpers for reusable path constraints. Custom constraints infer `string` params by default; built-in `int`, `decimal` and `range` still infer `number`. When a PathKit constraint rejects a value, URLKit wraps it as `UrlKitError` with `code: 'invalid-param'` and preserves the original PathKit error in `error.cause`.
|
|
196
196
|
|
|
197
197
|
```ts
|
|
198
198
|
import { createConstraint, registerPathConstraint, url } from '@cookbook/urlkit';
|
|
@@ -403,35 +403,32 @@ const Reports = search({
|
|
|
403
403
|
});
|
|
404
404
|
```
|
|
405
405
|
|
|
406
|
-
| Builder
|
|
407
|
-
|
|
|
408
|
-
| `date()`
|
|
409
|
-
| `dateTime()`
|
|
410
|
-
| `date({ format: '
|
|
411
|
-
| `
|
|
412
|
-
| `date({ format: { parse, serialize } })`
|
|
406
|
+
| Builder | Serialized format |
|
|
407
|
+
| --------------------------------------------- | -------------------------------------- |
|
|
408
|
+
| `date()` | Date-only `YYYY-MM-DD`. |
|
|
409
|
+
| `dateTime()` | Strict UTC `YYYY-MM-DDTHH:mm:ss.sssZ`. |
|
|
410
|
+
| `date({ format: 'dd-MM-yyyy' })` | Strict custom date format string. |
|
|
411
|
+
| `dateTime({ format: 'dd-MM-yyyy HH:mm:ss' })` | Strict custom date-time format string. |
|
|
412
|
+
| `date({ format: { parse, serialize } })` | Custom runtime date codec. |
|
|
413
|
+
| `dateTime({ format: { parse, serialize } })` | Custom runtime date-time codec. |
|
|
414
|
+
| `date({ format: 'unix-seconds' })` | Finite integer seconds. |
|
|
415
|
+
| `date({ format: 'unix-ms' })` | Finite integer milliseconds. |
|
|
413
416
|
|
|
414
|
-
Custom
|
|
417
|
+
Custom date and date-time format strings are available only in runtime-builder schemas. Supported tokens are `yyyy`, `MM`, `dd`, `HH`, `mm`, `ss`, and `SSS`. Static date defaults use serialized values, not `Date` instances.
|
|
415
418
|
|
|
416
419
|
```ts
|
|
417
420
|
const CustomDate = search({
|
|
418
|
-
from: date({
|
|
419
|
-
|
|
420
|
-
parse(value) {
|
|
421
|
-
const [day, month, year] = value.split('-');
|
|
422
|
-
return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
423
|
-
},
|
|
424
|
-
serialize(value) {
|
|
425
|
-
const day = String(value.getUTCDate()).padStart(2, '0');
|
|
426
|
-
const month = String(value.getUTCMonth() + 1).padStart(2, '0');
|
|
427
|
-
return `${day}-${month}-${value.getUTCFullYear()}`;
|
|
428
|
-
},
|
|
429
|
-
},
|
|
430
|
-
}),
|
|
421
|
+
from: date({ format: 'dd-MM-yyyy' }),
|
|
422
|
+
at: dateTime({ format: 'dd-MM-yyyy HH:mm:ss' }).optional(),
|
|
431
423
|
});
|
|
432
424
|
|
|
433
|
-
CustomDate.build({
|
|
434
|
-
|
|
425
|
+
CustomDate.build({
|
|
426
|
+
search: {
|
|
427
|
+
from: new Date('2026-06-02T00:00:00.000Z'),
|
|
428
|
+
at: new Date('2026-06-02T12:30:05.000Z'),
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
// '?from=02-06-2026&at=02-06-2026+12%3A30%3A05'
|
|
435
432
|
```
|
|
436
433
|
|
|
437
434
|
## Object search
|
|
@@ -639,16 +636,16 @@ try {
|
|
|
639
636
|
}
|
|
640
637
|
```
|
|
641
638
|
|
|
642
|
-
| Code | Meaning
|
|
643
|
-
| -------------------- |
|
|
644
|
-
| `invalid-url` | URL input could not be parsed as a URL.
|
|
645
|
-
| `path-mismatch` | URL pathname does not satisfy the path contract.
|
|
646
|
-
| `missing-param` | Required path param is missing.
|
|
647
|
-
| `invalid-param` | Path param is invalid.
|
|
648
|
-
| `missing-search` | Required search field is missing.
|
|
649
|
-
| `invalid-search` | Search value, unknown search behavior, or object search shape is invalid.
|
|
650
|
-
| `invalid-hash` | Hash value is missing or invalid.
|
|
651
|
-
| `invalid-descriptor` | Contract/schema/static descriptor is invalid at construction/compile time.
|
|
639
|
+
| Code | Meaning |
|
|
640
|
+
| -------------------- | ------------------------------------------------------------------------------------------ |
|
|
641
|
+
| `invalid-url` | URL input could not be parsed as a URL. |
|
|
642
|
+
| `path-mismatch` | URL pathname does not satisfy the path contract. |
|
|
643
|
+
| `missing-param` | Required path param is missing. |
|
|
644
|
+
| `invalid-param` | Path param is invalid. Constraint failures preserve the original PathKit error in `cause`. |
|
|
645
|
+
| `missing-search` | Required search field is missing. |
|
|
646
|
+
| `invalid-search` | Search value, unknown search behavior, or object search shape is invalid. |
|
|
647
|
+
| `invalid-hash` | Hash value is missing or invalid. |
|
|
648
|
+
| `invalid-descriptor` | Contract/schema/static descriptor is invalid at construction/compile time. |
|
|
652
649
|
|
|
653
650
|
## TypeScript inference
|
|
654
651
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import compile from '@cookbook/pathkit/compile';
|
|
2
2
|
import match from '@cookbook/pathkit/match';
|
|
3
|
-
import { hasConstraint, getConstraint, registerConstraint
|
|
3
|
+
import { hasConstraint, getConstraint, registerConstraint } from '@cookbook/pathkit/constraints';
|
|
4
|
+
import tokenize from '@cookbook/pathkit/tokenize';
|
|
4
5
|
|
|
5
6
|
const defaultMessages = {
|
|
6
7
|
'invalid-url': 'Invalid URL.',
|
|
@@ -325,6 +326,209 @@ function validateBooleanDefault(value, context) {
|
|
|
325
326
|
});
|
|
326
327
|
}
|
|
327
328
|
|
|
329
|
+
const supportedTokens = ['yyyy', 'SSS', 'MM', 'dd', 'HH', 'mm', 'ss'];
|
|
330
|
+
const dateTokens = new Set(['yyyy', 'MM', 'dd']);
|
|
331
|
+
const dateTimeTokens = new Set(['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss', 'SSS']);
|
|
332
|
+
function parseDateFormatString(input, format, mode, options = {}) {
|
|
333
|
+
const compiled = compileDateFormatString(format, mode);
|
|
334
|
+
const match = compiled.pattern.exec(input);
|
|
335
|
+
if (!match?.groups) {
|
|
336
|
+
throw createDateFormatStringError(`${getFormatLabel(mode)} value must match format "${format}".`, options);
|
|
337
|
+
}
|
|
338
|
+
const parts = readDateParts(match.groups, compiled);
|
|
339
|
+
assertValidDateParts(parts, mode, options);
|
|
340
|
+
const value = new Date(Date.UTC(0, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second, parts.millisecond));
|
|
341
|
+
value.setUTCFullYear(parts.year);
|
|
342
|
+
if (!matchesDateParts(value, parts)) {
|
|
343
|
+
throw createDateFormatStringError(`${getFormatLabel(mode)} value must be a valid UTC calendar ${mode === 'date' ? 'date' : 'instant'}.`, options);
|
|
344
|
+
}
|
|
345
|
+
return value;
|
|
346
|
+
}
|
|
347
|
+
function serializeDateFormatString(input, format, mode, options = {}) {
|
|
348
|
+
if (!(input instanceof Date) || !Number.isFinite(input.getTime())) {
|
|
349
|
+
throw createDateFormatStringError(`${getFormatLabel(mode)} value must be a valid Date.`, options);
|
|
350
|
+
}
|
|
351
|
+
const compiled = compileDateFormatString(format, mode);
|
|
352
|
+
const year = input.getUTCFullYear();
|
|
353
|
+
if (year < 0 || year > 9999) {
|
|
354
|
+
throw createDateFormatStringError(`${getFormatLabel(mode)} value year must be between 0000 and 9999 for yyyy format strings.`, options);
|
|
355
|
+
}
|
|
356
|
+
if (mode === 'date-time' && !compiled.hasMilliseconds && input.getUTCMilliseconds() !== 0) {
|
|
357
|
+
throw createDateFormatStringError('Date-time value cannot be serialized without losing milliseconds. Include SSS in the format or use a Date with zero milliseconds.', options);
|
|
358
|
+
}
|
|
359
|
+
return compiled.parts
|
|
360
|
+
.map((part) => {
|
|
361
|
+
if (part.kind === 'literal') {
|
|
362
|
+
return part.value;
|
|
363
|
+
}
|
|
364
|
+
return serializeToken(input, part.value);
|
|
365
|
+
})
|
|
366
|
+
.join('');
|
|
367
|
+
}
|
|
368
|
+
function validateDateFormatString(format, mode) {
|
|
369
|
+
compileDateFormatString(format, mode);
|
|
370
|
+
}
|
|
371
|
+
function compileDateFormatString(format, mode) {
|
|
372
|
+
const parts = tokenizeDateFormatString(format, mode);
|
|
373
|
+
validateDateFormatParts(format, parts, mode);
|
|
374
|
+
const pattern = new RegExp(`^${parts
|
|
375
|
+
.map((part) => part.kind === 'literal' ? escapeRegExp(part.value) : getTokenPattern(part.value))
|
|
376
|
+
.join('')}$`);
|
|
377
|
+
return {
|
|
378
|
+
format,
|
|
379
|
+
mode,
|
|
380
|
+
parts,
|
|
381
|
+
pattern,
|
|
382
|
+
hasMilliseconds: parts.some((part) => part.kind === 'token' && part.value === 'SSS'),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function tokenizeDateFormatString(format, mode) {
|
|
386
|
+
if (typeof format !== 'string' || format.length === 0) {
|
|
387
|
+
throw createInvalidDateFormatDescriptorError(`${getFormatLabel(mode)} format must be a non-empty string.`);
|
|
388
|
+
}
|
|
389
|
+
const parts = [];
|
|
390
|
+
let index = 0;
|
|
391
|
+
while (index < format.length) {
|
|
392
|
+
if (format[index] === "'") {
|
|
393
|
+
const literalEnd = format.indexOf("'", index + 1);
|
|
394
|
+
if (literalEnd === -1) {
|
|
395
|
+
throw createInvalidDateFormatDescriptorError(`${getFormatLabel(mode)} format contains an unterminated quoted literal.`);
|
|
396
|
+
}
|
|
397
|
+
const literal = format.slice(index + 1, literalEnd);
|
|
398
|
+
if (literal.length === 0) {
|
|
399
|
+
throw createInvalidDateFormatDescriptorError(`${getFormatLabel(mode)} format contains an empty quoted literal.`);
|
|
400
|
+
}
|
|
401
|
+
parts.push({ kind: 'literal', value: literal });
|
|
402
|
+
index = literalEnd + 1;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const token = findTokenAt(format, index);
|
|
406
|
+
if (token) {
|
|
407
|
+
parts.push({ kind: 'token', value: token });
|
|
408
|
+
index += token.length;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const character = format[index] ?? '';
|
|
412
|
+
if (isAsciiLetter(character)) {
|
|
413
|
+
throw createInvalidDateFormatDescriptorError(`${getFormatLabel(mode)} format contains unsupported token near "${format.slice(index)}".`);
|
|
414
|
+
}
|
|
415
|
+
parts.push({ kind: 'literal', value: character });
|
|
416
|
+
index += 1;
|
|
417
|
+
}
|
|
418
|
+
return mergeAdjacentLiterals(parts);
|
|
419
|
+
}
|
|
420
|
+
function findTokenAt(format, index) {
|
|
421
|
+
return supportedTokens.find((token) => format.startsWith(token, index));
|
|
422
|
+
}
|
|
423
|
+
function mergeAdjacentLiterals(parts) {
|
|
424
|
+
const merged = [];
|
|
425
|
+
for (const part of parts) {
|
|
426
|
+
const previous = merged[merged.length - 1];
|
|
427
|
+
if (part.kind === 'literal' && previous?.kind === 'literal') {
|
|
428
|
+
merged[merged.length - 1] = { kind: 'literal', value: `${previous.value}${part.value}` };
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
merged.push(part);
|
|
432
|
+
}
|
|
433
|
+
return merged;
|
|
434
|
+
}
|
|
435
|
+
function validateDateFormatParts(format, parts, mode) {
|
|
436
|
+
const allowedTokens = mode === 'date' ? dateTokens : dateTimeTokens;
|
|
437
|
+
const requiredTokens = mode === 'date' ? ['yyyy', 'MM', 'dd'] : ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss'];
|
|
438
|
+
const seenTokens = new Set();
|
|
439
|
+
for (const part of parts) {
|
|
440
|
+
if (part.kind === 'literal') {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (!allowedTokens.has(part.value)) {
|
|
444
|
+
throw createInvalidDateFormatDescriptorError(`${getFormatLabel(mode)} format "${format}" contains unsupported token "${part.value}".`);
|
|
445
|
+
}
|
|
446
|
+
if (seenTokens.has(part.value)) {
|
|
447
|
+
throw createInvalidDateFormatDescriptorError(`${getFormatLabel(mode)} format "${format}" contains duplicate token "${part.value}".`);
|
|
448
|
+
}
|
|
449
|
+
seenTokens.add(part.value);
|
|
450
|
+
}
|
|
451
|
+
for (const token of requiredTokens) {
|
|
452
|
+
if (!seenTokens.has(token)) {
|
|
453
|
+
throw createInvalidDateFormatDescriptorError(`${getFormatLabel(mode)} format "${format}" is missing required token "${token}".`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function readDateParts(groups, compiled) {
|
|
458
|
+
return {
|
|
459
|
+
year: Number(groups.yyyy),
|
|
460
|
+
month: Number(groups.MM),
|
|
461
|
+
day: Number(groups.dd),
|
|
462
|
+
hour: compiled.mode === 'date-time' ? Number(groups.HH) : 0,
|
|
463
|
+
minute: compiled.mode === 'date-time' ? Number(groups.mm) : 0,
|
|
464
|
+
second: compiled.mode === 'date-time' ? Number(groups.ss) : 0,
|
|
465
|
+
millisecond: compiled.hasMilliseconds ? Number(groups.SSS) : 0,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function assertValidDateParts(parts, mode, options) {
|
|
469
|
+
if (parts.month < 1 ||
|
|
470
|
+
parts.month > 12 ||
|
|
471
|
+
parts.day < 1 ||
|
|
472
|
+
parts.day > 31 ||
|
|
473
|
+
parts.hour < 0 ||
|
|
474
|
+
parts.hour > 23 ||
|
|
475
|
+
parts.minute < 0 ||
|
|
476
|
+
parts.minute > 59 ||
|
|
477
|
+
parts.second < 0 ||
|
|
478
|
+
parts.second > 59 ||
|
|
479
|
+
parts.millisecond < 0 ||
|
|
480
|
+
parts.millisecond > 999) {
|
|
481
|
+
throw createDateFormatStringError(`${getFormatLabel(mode)} value must be a valid UTC calendar ${mode === 'date' ? 'date' : 'instant'}.`, options);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function matchesDateParts(value, parts) {
|
|
485
|
+
return (value.getUTCFullYear() === parts.year &&
|
|
486
|
+
value.getUTCMonth() === parts.month - 1 &&
|
|
487
|
+
value.getUTCDate() === parts.day &&
|
|
488
|
+
value.getUTCHours() === parts.hour &&
|
|
489
|
+
value.getUTCMinutes() === parts.minute &&
|
|
490
|
+
value.getUTCSeconds() === parts.second &&
|
|
491
|
+
value.getUTCMilliseconds() === parts.millisecond);
|
|
492
|
+
}
|
|
493
|
+
function serializeToken(input, token) {
|
|
494
|
+
switch (token) {
|
|
495
|
+
case 'yyyy':
|
|
496
|
+
return String(input.getUTCFullYear()).padStart(4, '0');
|
|
497
|
+
case 'MM':
|
|
498
|
+
return String(input.getUTCMonth() + 1).padStart(2, '0');
|
|
499
|
+
case 'dd':
|
|
500
|
+
return String(input.getUTCDate()).padStart(2, '0');
|
|
501
|
+
case 'HH':
|
|
502
|
+
return String(input.getUTCHours()).padStart(2, '0');
|
|
503
|
+
case 'mm':
|
|
504
|
+
return String(input.getUTCMinutes()).padStart(2, '0');
|
|
505
|
+
case 'ss':
|
|
506
|
+
return String(input.getUTCSeconds()).padStart(2, '0');
|
|
507
|
+
case 'SSS':
|
|
508
|
+
return String(input.getUTCMilliseconds()).padStart(3, '0');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function getTokenPattern(token) {
|
|
512
|
+
return `(?<${token}>\\d{${token === 'yyyy' || token === 'SSS' ? token.length.toString() : '2'}})`;
|
|
513
|
+
}
|
|
514
|
+
function escapeRegExp(value) {
|
|
515
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
516
|
+
}
|
|
517
|
+
function isAsciiLetter(value) {
|
|
518
|
+
return /^[A-Za-z]$/.test(value);
|
|
519
|
+
}
|
|
520
|
+
function getFormatLabel(mode) {
|
|
521
|
+
return mode === 'date' ? 'Date' : 'Date-time';
|
|
522
|
+
}
|
|
523
|
+
function createDateFormatStringError(message, options) {
|
|
524
|
+
return new UrlKitError(options.code ?? 'invalid-search', message, {
|
|
525
|
+
path: [...(options.path ?? [])],
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
function createInvalidDateFormatDescriptorError(message) {
|
|
529
|
+
return new UrlKitError('invalid-descriptor', message);
|
|
530
|
+
}
|
|
531
|
+
|
|
328
532
|
const datePattern = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
329
533
|
function parseDate(input, options = {}) {
|
|
330
534
|
const match = datePattern.exec(input);
|
|
@@ -596,21 +800,24 @@ const unixMsCodec = {
|
|
|
596
800
|
return serializeUnixMs(input, { code: context.errorCode, path: context.path });
|
|
597
801
|
},
|
|
598
802
|
};
|
|
599
|
-
function date(options = {}) {
|
|
600
|
-
const format = resolveDateFormat(options);
|
|
803
|
+
function date(options = {}, formatStringMode = 'date') {
|
|
804
|
+
const format = resolveDateFormat(options, formatStringMode);
|
|
601
805
|
return createRuntimeSchemaBuilder({
|
|
602
806
|
kind: 'date',
|
|
603
807
|
options: { format },
|
|
604
|
-
codec: getDateCodec(format),
|
|
808
|
+
codec: getDateCodec(format, formatStringMode),
|
|
605
809
|
validateDefault(value, context) {
|
|
606
810
|
validateDateDefault(value, context);
|
|
607
811
|
},
|
|
608
812
|
});
|
|
609
813
|
}
|
|
610
|
-
function getDateCodec(format) {
|
|
814
|
+
function getDateCodec(format, formatStringMode) {
|
|
611
815
|
if (isDateFormatCodec(format)) {
|
|
612
816
|
return createCustomDateCodec(format);
|
|
613
817
|
}
|
|
818
|
+
if (isDateFormatString(format) && !isBuiltInDateFormat(format)) {
|
|
819
|
+
return createDateFormatStringCodec(format, formatStringMode);
|
|
820
|
+
}
|
|
614
821
|
if (format === 'date-time') {
|
|
615
822
|
return dateTimeCodec;
|
|
616
823
|
}
|
|
@@ -622,6 +829,25 @@ function getDateCodec(format) {
|
|
|
622
829
|
}
|
|
623
830
|
return dateOnlyCodec;
|
|
624
831
|
}
|
|
832
|
+
function createDateFormatStringCodec(format, mode) {
|
|
833
|
+
return {
|
|
834
|
+
parse(input, context) {
|
|
835
|
+
return parseDateFormatString(input, format, mode, {
|
|
836
|
+
code: context.errorCode,
|
|
837
|
+
path: context.path,
|
|
838
|
+
});
|
|
839
|
+
},
|
|
840
|
+
normalize(input, context) {
|
|
841
|
+
return validateDate(input, context);
|
|
842
|
+
},
|
|
843
|
+
serialize(input, context) {
|
|
844
|
+
return serializeDateFormatString(input, format, mode, {
|
|
845
|
+
code: context.errorCode,
|
|
846
|
+
path: context.path,
|
|
847
|
+
});
|
|
848
|
+
},
|
|
849
|
+
};
|
|
850
|
+
}
|
|
625
851
|
function createCustomDateCodec(format) {
|
|
626
852
|
return {
|
|
627
853
|
parse(input, context) {
|
|
@@ -635,7 +861,7 @@ function createCustomDateCodec(format) {
|
|
|
635
861
|
},
|
|
636
862
|
};
|
|
637
863
|
}
|
|
638
|
-
function resolveDateFormat(options) {
|
|
864
|
+
function resolveDateFormat(options, formatStringMode) {
|
|
639
865
|
if (!isDateOptions(options)) {
|
|
640
866
|
throw new UrlKitError('invalid-descriptor', 'Date options must be an object.');
|
|
641
867
|
}
|
|
@@ -643,11 +869,18 @@ function resolveDateFormat(options) {
|
|
|
643
869
|
if (isBuiltInDateFormat(format) || isDateFormatCodec(format)) {
|
|
644
870
|
return format;
|
|
645
871
|
}
|
|
646
|
-
|
|
872
|
+
if (isDateFormatString(format)) {
|
|
873
|
+
validateDateFormatString(format, formatStringMode);
|
|
874
|
+
return format;
|
|
875
|
+
}
|
|
876
|
+
throw new UrlKitError('invalid-descriptor', 'Date format must be a supported built-in format, a supported format string, or an explicit codec.');
|
|
647
877
|
}
|
|
648
878
|
function isDateOptions(input) {
|
|
649
879
|
return typeof input === 'object' && input !== null && !Array.isArray(input);
|
|
650
880
|
}
|
|
881
|
+
function isDateFormatString(input) {
|
|
882
|
+
return typeof input === 'string';
|
|
883
|
+
}
|
|
651
884
|
function isBuiltInDateFormat(input) {
|
|
652
885
|
return (input === 'date' || input === 'date-time' || input === 'unix-seconds' || input === 'unix-ms');
|
|
653
886
|
}
|
|
@@ -1036,7 +1269,6 @@ function assertPathMatchFailure(pattern, pathname, segments) {
|
|
|
1036
1269
|
const segment = segments[index];
|
|
1037
1270
|
const pathnameSegment = pathnameSegments[index];
|
|
1038
1271
|
if (!segment || pathnameSegment === undefined) {
|
|
1039
|
-
console.log('ERROR', segment);
|
|
1040
1272
|
throwPathMismatch(pattern, pathname);
|
|
1041
1273
|
}
|
|
1042
1274
|
if (segment.kind === 'literal') {
|
|
@@ -1045,9 +1277,11 @@ function assertPathMatchFailure(pattern, pathname, segments) {
|
|
|
1045
1277
|
}
|
|
1046
1278
|
continue;
|
|
1047
1279
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1280
|
+
const validationFailure = getPathParamValidationFailure(segment, pathnameSegment);
|
|
1281
|
+
if (validationFailure) {
|
|
1282
|
+
throw new UrlKitError('invalid-param', validationFailure.message, {
|
|
1050
1283
|
path: ['params', segment.name],
|
|
1284
|
+
...(validationFailure.cause === undefined ? {} : { cause: validationFailure.cause }),
|
|
1051
1285
|
});
|
|
1052
1286
|
}
|
|
1053
1287
|
}
|
|
@@ -1063,22 +1297,43 @@ function splitPath(pathname) {
|
|
|
1063
1297
|
}
|
|
1064
1298
|
return Object.freeze(normalized.split('/'));
|
|
1065
1299
|
}
|
|
1066
|
-
function
|
|
1067
|
-
if (!value
|
|
1068
|
-
return
|
|
1300
|
+
function getPathParamValidationFailure(segment, value) {
|
|
1301
|
+
if (!value) {
|
|
1302
|
+
return {
|
|
1303
|
+
message: `Path parameter "${segment.name}" is invalid.`,
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
if (!segment.constraint) {
|
|
1307
|
+
return undefined;
|
|
1069
1308
|
}
|
|
1070
1309
|
const constraint = getConstraint(segment.constraint);
|
|
1071
1310
|
if (!constraint) {
|
|
1072
|
-
return
|
|
1311
|
+
return {
|
|
1312
|
+
message: `Path constraint "${segment.constraint}" is not registered.`,
|
|
1313
|
+
};
|
|
1073
1314
|
}
|
|
1074
1315
|
try {
|
|
1075
1316
|
constraint(segment.name, value, segment.constraintParams ?? '');
|
|
1076
|
-
|
|
1077
|
-
|
|
1317
|
+
return undefined;
|
|
1318
|
+
}
|
|
1319
|
+
catch (error) {
|
|
1320
|
+
const causeMessage = getCauseMessage(error);
|
|
1321
|
+
return {
|
|
1322
|
+
message: causeMessage
|
|
1323
|
+
? `Path parameter "${segment.name}" is invalid: ${causeMessage}`
|
|
1324
|
+
: `Path parameter "${segment.name}" is invalid.`,
|
|
1325
|
+
cause: error,
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
function getCauseMessage(error) {
|
|
1330
|
+
if (error instanceof Error && error.message) {
|
|
1331
|
+
return error.message;
|
|
1078
1332
|
}
|
|
1079
|
-
|
|
1080
|
-
return
|
|
1333
|
+
if (typeof error === 'string' && error) {
|
|
1334
|
+
return error;
|
|
1081
1335
|
}
|
|
1336
|
+
return undefined;
|
|
1082
1337
|
}
|
|
1083
1338
|
function throwPathMismatch(pattern, pathname) {
|
|
1084
1339
|
throw new UrlKitError('path-mismatch', `Pathname "${pathname}" does not match pattern "${pattern}".`, {
|
|
@@ -1091,14 +1346,18 @@ function getPathParamKind(segment) {
|
|
|
1091
1346
|
case 'int':
|
|
1092
1347
|
return 'int';
|
|
1093
1348
|
case 'decimal':
|
|
1349
|
+
return 'decimal';
|
|
1094
1350
|
case 'range':
|
|
1095
|
-
return '
|
|
1351
|
+
return 'range';
|
|
1096
1352
|
case 'regex':
|
|
1097
1353
|
return 'regex';
|
|
1098
1354
|
default:
|
|
1099
1355
|
return 'string';
|
|
1100
1356
|
}
|
|
1101
1357
|
}
|
|
1358
|
+
function isNumericPathParamKind(kind) {
|
|
1359
|
+
return kind === 'int' || kind === 'decimal' || kind === 'range';
|
|
1360
|
+
}
|
|
1102
1361
|
|
|
1103
1362
|
function coercePathParam(segment, value, paramsMode) {
|
|
1104
1363
|
if (paramsMode === 'raw') {
|
|
@@ -1114,10 +1373,10 @@ function coercePathParam(segment, value, paramsMode) {
|
|
|
1114
1373
|
}
|
|
1115
1374
|
return parsed;
|
|
1116
1375
|
}
|
|
1117
|
-
if (kind
|
|
1376
|
+
if (isNumericPathParamKind(kind)) {
|
|
1118
1377
|
const parsed = Number(value);
|
|
1119
1378
|
if (!Number.isFinite(parsed)) {
|
|
1120
|
-
throw new UrlKitError('invalid-param', `Path parameter "${segment.name}" must be a finite number.`, {
|
|
1379
|
+
throw new UrlKitError('invalid-param', `Path parameter "${segment.name}" must be a finite decimal number.`, {
|
|
1121
1380
|
path: ['params', segment.name],
|
|
1122
1381
|
});
|
|
1123
1382
|
}
|
|
@@ -1154,79 +1413,57 @@ function isRecord(input) {
|
|
|
1154
1413
|
}
|
|
1155
1414
|
|
|
1156
1415
|
function parsePathPattern(pattern) {
|
|
1157
|
-
|
|
1158
|
-
return Object.freeze(
|
|
1159
|
-
}
|
|
1160
|
-
const normalized = pattern.startsWith('/') ? pattern.slice(1) : pattern;
|
|
1161
|
-
if (normalized === '') {
|
|
1162
|
-
return Object.freeze([]);
|
|
1163
|
-
}
|
|
1164
|
-
return Object.freeze(normalized.split('/').map((segment, index) => parsePathSegment(segment, index)));
|
|
1165
|
-
}
|
|
1166
|
-
function parsePathSegment(segment, index) {
|
|
1167
|
-
if (!segment.startsWith('{') || !segment.endsWith('}')) {
|
|
1168
|
-
return Object.freeze({ kind: 'literal', value: segment });
|
|
1416
|
+
try {
|
|
1417
|
+
return Object.freeze(toParsedPathSegments(tokenize(pattern)));
|
|
1169
1418
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1419
|
+
catch (error) {
|
|
1420
|
+
if (error instanceof UrlKitError) {
|
|
1421
|
+
throw error;
|
|
1422
|
+
}
|
|
1423
|
+
const causeMessage = error instanceof Error && error.message ? `: ${error.message}` : '';
|
|
1424
|
+
throw new UrlKitError('invalid-descriptor', `Path pattern is invalid${causeMessage}.`, {
|
|
1425
|
+
path: ['path'],
|
|
1426
|
+
cause: error,
|
|
1175
1427
|
});
|
|
1176
1428
|
}
|
|
1177
|
-
return Object.freeze({ kind: 'param', ...parsed });
|
|
1178
1429
|
}
|
|
1179
|
-
function
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
if (paramsStart === -1 || !constraintToken.endsWith(')')) {
|
|
1188
|
-
return { name, constraint: constraintToken };
|
|
1430
|
+
function toParsedPathSegments(tokens) {
|
|
1431
|
+
const segments = [];
|
|
1432
|
+
for (const token of tokens) {
|
|
1433
|
+
if (token.type === 'literal') {
|
|
1434
|
+
appendLiteralSegments(segments, token.value);
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
segments.push(toParsedParamSegment(token));
|
|
1189
1438
|
}
|
|
1190
|
-
return
|
|
1191
|
-
name,
|
|
1192
|
-
constraint: constraintToken.slice(0, paramsStart),
|
|
1193
|
-
constraintParams: constraintToken.slice(paramsStart + 1, -1),
|
|
1194
|
-
};
|
|
1439
|
+
return segments.map((segment) => Object.freeze(segment));
|
|
1195
1440
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
if (!
|
|
1202
|
-
|
|
1203
|
-
path: ['params', paramName],
|
|
1204
|
-
});
|
|
1205
|
-
}
|
|
1206
|
-
},
|
|
1207
|
-
verify(paramName, params) {
|
|
1208
|
-
if (params.trim()) {
|
|
1209
|
-
throw new UrlKitError('invalid-descriptor', `Constraint 'number' declared for '${paramName}' parameter does not accept arguments.`, { path: ['path', paramName] });
|
|
1441
|
+
function appendLiteralSegments(segments, value) {
|
|
1442
|
+
if (!value) {
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
for (const segment of value.split('/')) {
|
|
1446
|
+
if (!segment) {
|
|
1447
|
+
continue;
|
|
1210
1448
|
}
|
|
1211
|
-
|
|
1212
|
-
toRegExp() {
|
|
1213
|
-
return '-?(?:\\d+(?:\\.\\d+)?|\\.\\d+)';
|
|
1214
|
-
},
|
|
1215
|
-
});
|
|
1216
|
-
function registerUrlKitPathConstraints() {
|
|
1217
|
-
if (!hasConstraint('number')) {
|
|
1218
|
-
registerConstraint('number', numberConstraint);
|
|
1449
|
+
segments.push({ kind: 'literal', value: segment });
|
|
1219
1450
|
}
|
|
1220
1451
|
}
|
|
1221
|
-
function
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1452
|
+
function toParsedParamSegment(token) {
|
|
1453
|
+
const constraint = token.constraints[0];
|
|
1454
|
+
return {
|
|
1455
|
+
kind: 'param',
|
|
1456
|
+
name: token.name,
|
|
1457
|
+
...(constraint
|
|
1458
|
+
? {
|
|
1459
|
+
constraint: constraint.type,
|
|
1460
|
+
...(constraint.params ? { constraintParams: constraint.params } : {}),
|
|
1461
|
+
}
|
|
1462
|
+
: {}),
|
|
1463
|
+
};
|
|
1226
1464
|
}
|
|
1227
1465
|
|
|
1228
1466
|
function compilePath(pattern, options = {}) {
|
|
1229
|
-
registerUrlKitPathConstraints();
|
|
1230
1467
|
if (options.pathConstraints) {
|
|
1231
1468
|
registerPathConstraints(options.pathConstraints);
|
|
1232
1469
|
}
|
|
@@ -1301,5 +1538,5 @@ function compilePathPattern(pattern) {
|
|
|
1301
1538
|
}
|
|
1302
1539
|
}
|
|
1303
1540
|
|
|
1304
|
-
export { UrlKitError as U, compilePath as a, boolean as b, compileSearchSchema as c, date as d, enumOf as e, registerPathConstraints as f, handleRuntimeSchemaAbsence as g, hasPathConstraint as h, int as i, createSchemaValueError as j, createRuntimeSchemaValueContext as k, compileRuntimeSchemaValue as l, createRuntimeSchemaBuilder as m, number as n, normalizeRuntimeSchemaValue as o, getRuntimeSchemaInternals as p, runtimeSchemaSymbol as q, registerPathConstraint as r, string as s, compileRuntimeSchema as t, normalizeCompiledRuntimeSchemaValue as u,
|
|
1305
|
-
//# sourceMappingURL=compile-path-
|
|
1541
|
+
export { parseDate as A, UrlKitError as U, compilePath as a, boolean as b, compileSearchSchema as c, date as d, enumOf as e, registerPathConstraints as f, handleRuntimeSchemaAbsence as g, hasPathConstraint as h, int as i, createSchemaValueError as j, createRuntimeSchemaValueContext as k, compileRuntimeSchemaValue as l, createRuntimeSchemaBuilder as m, number as n, normalizeRuntimeSchemaValue as o, getRuntimeSchemaInternals as p, runtimeSchemaSymbol as q, registerPathConstraint as r, string as s, compileRuntimeSchema as t, normalizeCompiledRuntimeSchemaValue as u, validateDateFormatString as v, compileStaticHashDescriptor as w, parseUnixSeconds as x, parseUnixMs as y, parseDateTime as z };
|
|
1542
|
+
//# sourceMappingURL=compile-path-oMMfpXR8.js.map
|