@atproto/lex-schema 0.0.12 → 0.0.14
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/CHANGELOG.md +53 -0
- package/dist/core/schema.d.ts +27 -36
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/schema.js +68 -54
- package/dist/core/schema.js.map +1 -1
- package/dist/core/string-format.d.ts +1 -14
- package/dist/core/string-format.d.ts.map +1 -1
- package/dist/core/string-format.js +12 -9
- package/dist/core/string-format.js.map +1 -1
- package/dist/core/validation-error.d.ts +5 -5
- package/dist/core/validation-error.d.ts.map +1 -1
- package/dist/core/validation-error.js +8 -8
- package/dist/core/validation-error.js.map +1 -1
- package/dist/core/validation-issue.js +3 -1
- package/dist/core/validation-issue.js.map +1 -1
- package/dist/core/validator.d.ts +16 -8
- package/dist/core/validator.d.ts.map +1 -1
- package/dist/core/validator.js +24 -6
- package/dist/core/validator.js.map +1 -1
- package/dist/helpers.d.ts +10 -11
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js.map +1 -1
- package/dist/schema/array.d.ts +1 -0
- package/dist/schema/array.d.ts.map +1 -1
- package/dist/schema/array.js +2 -1
- package/dist/schema/array.js.map +1 -1
- package/dist/schema/blob.d.ts +4 -2
- package/dist/schema/blob.d.ts.map +1 -1
- package/dist/schema/blob.js +5 -2
- package/dist/schema/blob.js.map +1 -1
- package/dist/schema/boolean.d.ts +1 -0
- package/dist/schema/boolean.d.ts.map +1 -1
- package/dist/schema/boolean.js +2 -1
- package/dist/schema/boolean.js.map +1 -1
- package/dist/schema/bytes.d.ts +1 -0
- package/dist/schema/bytes.d.ts.map +1 -1
- package/dist/schema/bytes.js +2 -1
- package/dist/schema/bytes.js.map +1 -1
- package/dist/schema/cid.d.ts +1 -0
- package/dist/schema/cid.d.ts.map +1 -1
- package/dist/schema/cid.js +2 -1
- package/dist/schema/cid.js.map +1 -1
- package/dist/schema/custom.d.ts +1 -0
- package/dist/schema/custom.d.ts.map +1 -1
- package/dist/schema/custom.js +1 -0
- package/dist/schema/custom.js.map +1 -1
- package/dist/schema/dict.d.ts +1 -0
- package/dist/schema/dict.d.ts.map +1 -1
- package/dist/schema/dict.js +2 -1
- package/dist/schema/dict.js.map +1 -1
- package/dist/schema/discriminated-union.d.ts +1 -0
- package/dist/schema/discriminated-union.d.ts.map +1 -1
- package/dist/schema/discriminated-union.js +2 -1
- package/dist/schema/discriminated-union.js.map +1 -1
- package/dist/schema/enum.d.ts +1 -0
- package/dist/schema/enum.d.ts.map +1 -1
- package/dist/schema/enum.js +1 -0
- package/dist/schema/enum.js.map +1 -1
- package/dist/schema/integer.d.ts +1 -0
- package/dist/schema/integer.d.ts.map +1 -1
- package/dist/schema/integer.js +2 -1
- package/dist/schema/integer.js.map +1 -1
- package/dist/schema/intersection.d.ts +1 -0
- package/dist/schema/intersection.d.ts.map +1 -1
- package/dist/schema/intersection.js +1 -0
- package/dist/schema/intersection.js.map +1 -1
- package/dist/schema/lex-map.d.ts +37 -0
- package/dist/schema/lex-map.d.ts.map +1 -0
- package/dist/schema/lex-map.js +60 -0
- package/dist/schema/lex-map.js.map +1 -0
- package/dist/schema/lex-value.d.ts +35 -0
- package/dist/schema/lex-value.d.ts.map +1 -0
- package/dist/schema/lex-value.js +87 -0
- package/dist/schema/lex-value.js.map +1 -0
- package/dist/schema/literal.d.ts +1 -0
- package/dist/schema/literal.d.ts.map +1 -1
- package/dist/schema/literal.js +1 -0
- package/dist/schema/literal.js.map +1 -1
- package/dist/schema/never.d.ts +1 -0
- package/dist/schema/never.d.ts.map +1 -1
- package/dist/schema/never.js +2 -1
- package/dist/schema/never.js.map +1 -1
- package/dist/schema/null.d.ts +1 -0
- package/dist/schema/null.d.ts.map +1 -1
- package/dist/schema/null.js +2 -1
- package/dist/schema/null.js.map +1 -1
- package/dist/schema/nullable.d.ts +1 -0
- package/dist/schema/nullable.d.ts.map +1 -1
- package/dist/schema/nullable.js +1 -0
- package/dist/schema/nullable.js.map +1 -1
- package/dist/schema/object.d.ts +1 -0
- package/dist/schema/object.d.ts.map +1 -1
- package/dist/schema/object.js +2 -1
- package/dist/schema/object.js.map +1 -1
- package/dist/schema/optional.d.ts +1 -0
- package/dist/schema/optional.d.ts.map +1 -1
- package/dist/schema/optional.js +1 -0
- package/dist/schema/optional.js.map +1 -1
- package/dist/schema/params.d.ts +14 -10
- package/dist/schema/params.d.ts.map +1 -1
- package/dist/schema/params.js +87 -24
- package/dist/schema/params.js.map +1 -1
- package/dist/schema/payload.d.ts.map +1 -1
- package/dist/schema/payload.js +3 -3
- package/dist/schema/payload.js.map +1 -1
- package/dist/schema/record.d.ts +21 -19
- package/dist/schema/record.d.ts.map +1 -1
- package/dist/schema/record.js +22 -12
- package/dist/schema/record.js.map +1 -1
- package/dist/schema/ref.d.ts +1 -0
- package/dist/schema/ref.d.ts.map +1 -1
- package/dist/schema/ref.js +1 -0
- package/dist/schema/ref.js.map +1 -1
- package/dist/schema/regexp.d.ts +1 -0
- package/dist/schema/regexp.d.ts.map +1 -1
- package/dist/schema/regexp.js +2 -1
- package/dist/schema/regexp.js.map +1 -1
- package/dist/schema/string.d.ts +22 -6
- package/dist/schema/string.d.ts.map +1 -1
- package/dist/schema/string.js +16 -9
- package/dist/schema/string.js.map +1 -1
- package/dist/schema/token.d.ts +1 -0
- package/dist/schema/token.d.ts.map +1 -1
- package/dist/schema/token.js +2 -1
- package/dist/schema/token.js.map +1 -1
- package/dist/schema/typed-object.d.ts +20 -16
- package/dist/schema/typed-object.d.ts.map +1 -1
- package/dist/schema/typed-object.js +23 -13
- package/dist/schema/typed-object.js.map +1 -1
- package/dist/schema/typed-ref.d.ts +1 -0
- package/dist/schema/typed-ref.d.ts.map +1 -1
- package/dist/schema/typed-ref.js +1 -0
- package/dist/schema/typed-ref.js.map +1 -1
- package/dist/schema/typed-union.d.ts +1 -0
- package/dist/schema/typed-union.d.ts.map +1 -1
- package/dist/schema/typed-union.js +2 -1
- package/dist/schema/typed-union.js.map +1 -1
- package/dist/schema/union.d.ts +1 -0
- package/dist/schema/union.d.ts.map +1 -1
- package/dist/schema/union.js +2 -1
- package/dist/schema/union.js.map +1 -1
- package/dist/schema/unknown.d.ts +1 -0
- package/dist/schema/unknown.d.ts.map +1 -1
- package/dist/schema/unknown.js +1 -0
- package/dist/schema/unknown.js.map +1 -1
- package/dist/schema/with-default.d.ts +1 -0
- package/dist/schema/with-default.d.ts.map +1 -1
- package/dist/schema/with-default.js +1 -0
- package/dist/schema/with-default.js.map +1 -1
- package/dist/schema.d.ts +2 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -1
- package/dist/schema.js.map +1 -1
- package/dist/util/if-any.d.ts +2 -0
- package/dist/util/if-any.d.ts.map +1 -0
- package/dist/util/if-any.js +3 -0
- package/dist/util/if-any.js.map +1 -0
- package/package.json +3 -3
- package/src/core/schema.ts +76 -62
- package/src/core/string-format.ts +14 -17
- package/src/core/validation-error.ts +10 -10
- package/src/core/validation-issue.ts +3 -2
- package/src/core/validator.ts +32 -12
- package/src/helpers.test.ts +1 -1
- package/src/helpers.ts +53 -19
- package/src/schema/array.ts +3 -1
- package/src/schema/blob.ts +4 -1
- package/src/schema/boolean.ts +3 -1
- package/src/schema/bytes.ts +3 -1
- package/src/schema/cid.ts +3 -1
- package/src/schema/custom.ts +2 -0
- package/src/schema/dict.ts +3 -1
- package/src/schema/discriminated-union.ts +3 -1
- package/src/schema/enum.ts +2 -0
- package/src/schema/integer.ts +3 -1
- package/src/schema/intersection.ts +2 -0
- package/src/schema/{unknown-object.test.ts → lex-map.test.ts} +9 -9
- package/src/schema/lex-map.ts +63 -0
- package/src/schema/lex-value.test.ts +81 -0
- package/src/schema/lex-value.ts +86 -0
- package/src/schema/literal.ts +2 -0
- package/src/schema/never.ts +3 -1
- package/src/schema/null.ts +3 -1
- package/src/schema/nullable.ts +2 -0
- package/src/schema/object.ts +3 -1
- package/src/schema/optional.ts +2 -0
- package/src/schema/params.test.ts +98 -43
- package/src/schema/params.ts +136 -39
- package/src/schema/payload.test.ts +2 -2
- package/src/schema/payload.ts +3 -4
- package/src/schema/record.ts +38 -22
- package/src/schema/ref.ts +2 -0
- package/src/schema/regexp.ts +3 -1
- package/src/schema/string.test.ts +99 -2
- package/src/schema/string.ts +58 -15
- package/src/schema/token.ts +3 -1
- package/src/schema/typed-object.test.ts +38 -0
- package/src/schema/typed-object.ts +40 -24
- package/src/schema/typed-ref.ts +2 -0
- package/src/schema/typed-union.ts +3 -1
- package/src/schema/union.ts +4 -2
- package/src/schema/unknown.ts +2 -0
- package/src/schema/with-default.ts +2 -0
- package/src/schema.ts +2 -1
- package/src/util/if-any.ts +3 -0
- package/dist/schema/unknown-object.d.ts +0 -42
- package/dist/schema/unknown-object.d.ts.map +0 -1
- package/dist/schema/unknown-object.js +0 -50
- package/dist/schema/unknown-object.js.map +0 -1
- package/src/schema/unknown-object.ts +0 -53
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { array } from './array.js'
|
|
3
3
|
import { boolean } from './boolean.js'
|
|
4
|
+
import { enumSchema } from './enum.js'
|
|
4
5
|
import { integer } from './integer.js'
|
|
6
|
+
import { literal } from './literal.js'
|
|
5
7
|
import { optional } from './optional.js'
|
|
6
8
|
import { paramSchema, params, paramsSchema } from './params.js'
|
|
7
9
|
import { string } from './string.js'
|
|
@@ -260,6 +262,42 @@ describe('ParamsSchema', () => {
|
|
|
260
262
|
})
|
|
261
263
|
})
|
|
262
264
|
|
|
265
|
+
describe('coercion', () => {
|
|
266
|
+
it('throws for invalid enum values', () => {
|
|
267
|
+
const schema = params({
|
|
268
|
+
status: enumSchema(['active', 'inactive']),
|
|
269
|
+
})
|
|
270
|
+
expect(() => schema.fromURLSearchParams('status=unknown')).toThrow(
|
|
271
|
+
'Expected one of "active" or "inactive"',
|
|
272
|
+
)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('throws for invalid const values', () => {
|
|
276
|
+
const schema = params({
|
|
277
|
+
version: literal(42),
|
|
278
|
+
})
|
|
279
|
+
expect(() => schema.fromURLSearchParams('version=99')).toThrow(
|
|
280
|
+
'Expected 42',
|
|
281
|
+
)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('handles negative integer enum values', () => {
|
|
285
|
+
const schema = params({
|
|
286
|
+
offset: enumSchema([-10, 0, 10]),
|
|
287
|
+
})
|
|
288
|
+
const result = schema.fromURLSearchParams('offset=-10')
|
|
289
|
+
expect(result).toEqual({ offset: -10 })
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('handles boolean const false', () => {
|
|
293
|
+
const schema = params({
|
|
294
|
+
disabled: literal(false),
|
|
295
|
+
})
|
|
296
|
+
const result = schema.fromURLSearchParams('disabled=false')
|
|
297
|
+
expect(result).toEqual({ disabled: false })
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
|
|
263
301
|
describe('fromURLSearchParams', () => {
|
|
264
302
|
const schema = params({
|
|
265
303
|
name: string(),
|
|
@@ -271,70 +309,58 @@ describe('ParamsSchema', () => {
|
|
|
271
309
|
})
|
|
272
310
|
|
|
273
311
|
it('parses string parameters', () => {
|
|
274
|
-
const
|
|
275
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
312
|
+
const result = schema.fromURLSearchParams('name=Alice')
|
|
276
313
|
expect(result).toEqual({ name: 'Alice' })
|
|
277
314
|
})
|
|
278
315
|
|
|
279
316
|
it('parses and coerces boolean true', () => {
|
|
280
|
-
const
|
|
281
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
317
|
+
const result = schema.fromURLSearchParams('name=Alice&active=true')
|
|
282
318
|
expect(result).toEqual({ name: 'Alice', active: true })
|
|
283
319
|
})
|
|
284
320
|
|
|
285
321
|
it('parses and coerces boolean false', () => {
|
|
286
|
-
const
|
|
287
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
322
|
+
const result = schema.fromURLSearchParams('name=Alice&active=false')
|
|
288
323
|
expect(result).toEqual({ name: 'Alice', active: false })
|
|
289
324
|
})
|
|
290
325
|
|
|
291
326
|
it('parses and coerces integer values', () => {
|
|
292
|
-
const
|
|
293
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
327
|
+
const result = schema.fromURLSearchParams('name=Alice&age=30')
|
|
294
328
|
expect(result).toEqual({ name: 'Alice', age: 30 })
|
|
295
329
|
})
|
|
296
330
|
|
|
297
331
|
it('parses and coerces negative integers', () => {
|
|
298
|
-
const
|
|
299
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
332
|
+
const result = schema.fromURLSearchParams('name=Alice&age=-5')
|
|
300
333
|
expect(result).toEqual({ name: 'Alice', age: -5 })
|
|
301
334
|
})
|
|
302
335
|
|
|
303
336
|
it('does not coerce non-integer numbers', () => {
|
|
304
|
-
const
|
|
305
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
337
|
+
const result = schema.fromURLSearchParams('name=Alice&extra=3.14')
|
|
306
338
|
expect(result).toEqual({ name: 'Alice', extra: '3.14' })
|
|
307
339
|
})
|
|
308
340
|
|
|
309
341
|
it('keeps string values for string schema even if they look like numbers', () => {
|
|
310
|
-
const
|
|
311
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
342
|
+
const result = schema.fromURLSearchParams('name=123')
|
|
312
343
|
expect(result).toEqual({ name: '123' })
|
|
313
344
|
})
|
|
314
345
|
|
|
315
346
|
it('parses multiple values as array', () => {
|
|
316
|
-
const
|
|
317
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
347
|
+
const result = schema.fromURLSearchParams('name=Alice&tags=one&tags=two')
|
|
318
348
|
expect(result).toEqual({ name: 'Alice', tags: ['one', 'two'] })
|
|
319
349
|
})
|
|
320
350
|
|
|
321
|
-
it('
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
})
|
|
351
|
+
it('does not coerce numeric values of unknown params', () => {
|
|
352
|
+
expect(
|
|
353
|
+
schema.fromURLSearchParams('name=Alice&num=1&num=2&num=3&foo=3'),
|
|
354
|
+
).toEqual({ name: 'Alice', num: ['1', '2', '3'], foo: '3' })
|
|
326
355
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
)
|
|
331
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
332
|
-
expect(result).toEqual({ name: 'Alice', val: [true, 123, 'text'] })
|
|
356
|
+
expect(
|
|
357
|
+
schema.fromURLSearchParams('name=Alice&val=true&val=123&val=text'),
|
|
358
|
+
).toEqual({ name: 'Alice', val: ['true', '123', 'text'] })
|
|
333
359
|
})
|
|
334
360
|
|
|
335
361
|
it('handles empty URLSearchParams', () => {
|
|
336
|
-
|
|
337
|
-
expect(() => schema.fromURLSearchParams(
|
|
362
|
+
expect(() => schema.fromURLSearchParams(new URLSearchParams())).toThrow()
|
|
363
|
+
expect(() => schema.fromURLSearchParams('')).toThrow()
|
|
338
364
|
})
|
|
339
365
|
|
|
340
366
|
it('handles multiple parameters', () => {
|
|
@@ -393,14 +419,35 @@ describe('ParamsSchema', () => {
|
|
|
393
419
|
['name', 'Alice'],
|
|
394
420
|
['bools', 'notabool'],
|
|
395
421
|
]),
|
|
396
|
-
).toThrow('Expected boolean value type at $.bools
|
|
422
|
+
).toThrow('Expected boolean value type at $.bools (got string)')
|
|
397
423
|
|
|
398
424
|
expect(() =>
|
|
399
|
-
schema.fromURLSearchParams(
|
|
400
|
-
[
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
425
|
+
schema.fromURLSearchParams(
|
|
426
|
+
[
|
|
427
|
+
['name', 'Alice'],
|
|
428
|
+
['bools', '2'],
|
|
429
|
+
],
|
|
430
|
+
{
|
|
431
|
+
path: ['foo', 'bar'],
|
|
432
|
+
},
|
|
433
|
+
),
|
|
434
|
+
).toThrow('Expected boolean value type at $.foo.bar.bools (got string)')
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('ignores empty string values', () => {
|
|
438
|
+
const result = schema.fromURLSearchParams([
|
|
439
|
+
['name', 'Alice'],
|
|
440
|
+
['extra', ''],
|
|
441
|
+
])
|
|
442
|
+
expect(result).toEqual({ name: 'Alice' })
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it('ignores empty string values for known parameters', () => {
|
|
446
|
+
const result = schema.fromURLSearchParams([
|
|
447
|
+
['name', 'Alice'],
|
|
448
|
+
['age', ''],
|
|
449
|
+
])
|
|
450
|
+
expect(result).toEqual({ name: 'Alice' })
|
|
404
451
|
})
|
|
405
452
|
})
|
|
406
453
|
|
|
@@ -461,15 +508,23 @@ describe('ParamsSchema', () => {
|
|
|
461
508
|
expect(result.toString()).toBe('name=Alice')
|
|
462
509
|
})
|
|
463
510
|
|
|
511
|
+
it('rejects arrays with multiple types', () => {
|
|
512
|
+
expect(() => {
|
|
513
|
+
schema.toURLSearchParams({
|
|
514
|
+
name: 'Alice',
|
|
515
|
+
// @ts-expect-error
|
|
516
|
+
values: [1, true, 'text'],
|
|
517
|
+
})
|
|
518
|
+
}).toThrow()
|
|
519
|
+
})
|
|
520
|
+
|
|
464
521
|
it('handles arrays with multiple types', () => {
|
|
465
522
|
const result = schema.toURLSearchParams({
|
|
466
523
|
name: 'Alice',
|
|
467
524
|
// @ts-expect-error
|
|
468
|
-
values: [
|
|
525
|
+
values: ['foo', 'bar'],
|
|
469
526
|
})
|
|
470
|
-
expect(result.toString()).toBe(
|
|
471
|
-
'name=Alice&values=1&values=true&values=text',
|
|
472
|
-
)
|
|
527
|
+
expect(result.toString()).toBe('name=Alice&values=foo&values=bar')
|
|
473
528
|
})
|
|
474
529
|
|
|
475
530
|
it('handles undefined input', () => {
|
|
@@ -711,9 +766,9 @@ describe('paramSchema', () => {
|
|
|
711
766
|
expect(result.success).toBe(true)
|
|
712
767
|
})
|
|
713
768
|
|
|
714
|
-
it('
|
|
769
|
+
it('rejects arrays with mixed scalar types', () => {
|
|
715
770
|
const result = paramSchema.safeParse([true, 42, 'text'])
|
|
716
|
-
expect(result.success).toBe(
|
|
771
|
+
expect(result.success).toBe(false)
|
|
717
772
|
})
|
|
718
773
|
|
|
719
774
|
it('validates arrays with negative integers', () => {
|
|
@@ -884,11 +939,11 @@ describe('paramsSchema', () => {
|
|
|
884
939
|
expect(result.success).toBe(true)
|
|
885
940
|
})
|
|
886
941
|
|
|
887
|
-
it('
|
|
942
|
+
it('rejects object with arrays of mixed scalar types', () => {
|
|
888
943
|
const result = paramsSchema.safeParse({
|
|
889
944
|
values: [true, 42, 'text'],
|
|
890
945
|
})
|
|
891
|
-
expect(result.success).toBe(
|
|
946
|
+
expect(result.success).toBe(false)
|
|
892
947
|
})
|
|
893
948
|
|
|
894
949
|
it('validates object with numeric string keys', () => {
|
package/src/schema/params.ts
CHANGED
|
@@ -3,6 +3,11 @@ import {
|
|
|
3
3
|
Infer,
|
|
4
4
|
InferInput,
|
|
5
5
|
InferOutput,
|
|
6
|
+
Issue,
|
|
7
|
+
IssueInvalidType,
|
|
8
|
+
IssueInvalidValue,
|
|
9
|
+
LexValidationError,
|
|
10
|
+
ParseOptions,
|
|
6
11
|
Schema,
|
|
7
12
|
ValidationContext,
|
|
8
13
|
Validator,
|
|
@@ -13,7 +18,9 @@ import { memoizedOptions } from '../util/memoize.js'
|
|
|
13
18
|
import { ArraySchema, array } from './array.js'
|
|
14
19
|
import { BooleanSchema, boolean } from './boolean.js'
|
|
15
20
|
import { dict } from './dict.js'
|
|
21
|
+
import { EnumSchema } from './enum.js'
|
|
16
22
|
import { IntegerSchema, integer } from './integer.js'
|
|
23
|
+
import { LiteralSchema } from './literal.js'
|
|
17
24
|
import { OptionalSchema, optional } from './optional.js'
|
|
18
25
|
import { StringSchema, string } from './string.js'
|
|
19
26
|
import { union } from './union.js'
|
|
@@ -33,7 +40,12 @@ export type Param = Infer<typeof paramSchema>
|
|
|
33
40
|
/**
|
|
34
41
|
* Schema for validating individual parameter values.
|
|
35
42
|
*/
|
|
36
|
-
export const paramSchema = union([
|
|
43
|
+
export const paramSchema = union([
|
|
44
|
+
paramScalarSchema,
|
|
45
|
+
array(boolean()),
|
|
46
|
+
array(integer()),
|
|
47
|
+
array(string()),
|
|
48
|
+
])
|
|
37
49
|
|
|
38
50
|
/**
|
|
39
51
|
* Type for a params object with string keys and optional param values.
|
|
@@ -45,14 +57,34 @@ export type Params = Infer<typeof paramsSchema>
|
|
|
45
57
|
*/
|
|
46
58
|
export const paramsSchema = dict(string(), optional(paramSchema))
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
export type ParamScalarValidator =
|
|
61
|
+
// @NOTE In order to properly coerce URLSearchParams, we need to distinguish
|
|
62
|
+
// between scalar and array validators, requiring to be able to detect which
|
|
63
|
+
// schema types are being used, restricting the allowed param validators here.
|
|
64
|
+
| LiteralSchema<string>
|
|
65
|
+
| LiteralSchema<number>
|
|
66
|
+
| LiteralSchema<boolean>
|
|
67
|
+
| EnumSchema<string>
|
|
68
|
+
| EnumSchema<number>
|
|
69
|
+
// | EnumSchema<boolean> // Boolean lexicon definitions don't allow "enum"
|
|
70
|
+
| StringSchema<any>
|
|
71
|
+
| BooleanSchema
|
|
72
|
+
| IntegerSchema
|
|
73
|
+
|
|
74
|
+
type AsArrayParamSchema<TSchema extends Validator> =
|
|
75
|
+
// This allows to "distribute" any union of scalar validators into a union of
|
|
76
|
+
// arrays of those validators, instead of an array of union. If TSchema is
|
|
77
|
+
// BooleanSchema | IntegerSchema, we want the result to be
|
|
78
|
+
// ArraySchema<BooleanSchema> | ArraySchema<IntegerSchema>, not
|
|
79
|
+
// ArraySchema<BooleanSchema | IntegerSchema>, since the latter would allow
|
|
80
|
+
// arrays with mixed types (e.g. [true, 42]), which we don't want.
|
|
81
|
+
TSchema extends any ? ArraySchema<TSchema> : never
|
|
82
|
+
|
|
83
|
+
export type ParamValueValidator =
|
|
53
84
|
| ParamScalarValidator
|
|
54
|
-
|
|
|
55
|
-
|
|
85
|
+
| AsArrayParamSchema<ParamScalarValidator>
|
|
86
|
+
|
|
87
|
+
export type ParamValidator =
|
|
56
88
|
| ParamValueValidator
|
|
57
89
|
| OptionalSchema<ParamValueValidator>
|
|
58
90
|
| OptionalSchema<WithDefaultSchema<ParamValueValidator>>
|
|
@@ -63,7 +95,7 @@ type ParamValidator =
|
|
|
63
95
|
*
|
|
64
96
|
* Maps parameter names to their validators (must be Param or undefined).
|
|
65
97
|
*/
|
|
66
|
-
export type
|
|
98
|
+
export type ParamsShape = {
|
|
67
99
|
[x: string]: ParamValidator
|
|
68
100
|
}
|
|
69
101
|
|
|
@@ -87,7 +119,7 @@ export type ParamsSchemaShape = {
|
|
|
87
119
|
* ```
|
|
88
120
|
*/
|
|
89
121
|
export class ParamsSchema<
|
|
90
|
-
const TShape extends
|
|
122
|
+
const TShape extends ParamsShape = ParamsShape,
|
|
91
123
|
> extends Schema<
|
|
92
124
|
WithOptionalProperties<{
|
|
93
125
|
[K in keyof TShape]: InferInput<TShape[K]>
|
|
@@ -96,6 +128,8 @@ export class ParamsSchema<
|
|
|
96
128
|
[K in keyof TShape]: InferOutput<TShape[K]>
|
|
97
129
|
}>
|
|
98
130
|
> {
|
|
131
|
+
readonly type = 'params' as const
|
|
132
|
+
|
|
99
133
|
constructor(readonly shape: TShape) {
|
|
100
134
|
super()
|
|
101
135
|
}
|
|
@@ -108,7 +142,7 @@ export class ParamsSchema<
|
|
|
108
142
|
|
|
109
143
|
validateInContext(input: unknown, ctx: ValidationContext) {
|
|
110
144
|
if (!isPlainObject(input)) {
|
|
111
|
-
return ctx.
|
|
145
|
+
return ctx.issueUnexpectedType(input, 'object')
|
|
112
146
|
}
|
|
113
147
|
|
|
114
148
|
// Lazily copy value
|
|
@@ -163,42 +197,41 @@ export class ParamsSchema<
|
|
|
163
197
|
return ctx.success(copy ?? input)
|
|
164
198
|
}
|
|
165
199
|
|
|
166
|
-
fromURLSearchParams(
|
|
167
|
-
|
|
200
|
+
fromURLSearchParams(
|
|
201
|
+
input: string | Iterable<[string, string]>,
|
|
202
|
+
options?: ParseOptions,
|
|
203
|
+
): InferOutput<this> {
|
|
204
|
+
const params: Record<string, unknown> = {}
|
|
168
205
|
|
|
169
|
-
|
|
206
|
+
const iterable =
|
|
207
|
+
typeof input === 'string' ? new URLSearchParams(input) : input
|
|
170
208
|
const entries =
|
|
171
209
|
iterable instanceof URLSearchParams ? iterable.entries() : iterable
|
|
172
210
|
|
|
173
|
-
for (const [
|
|
174
|
-
|
|
175
|
-
|
|
211
|
+
for (const [name, value] of entries) {
|
|
212
|
+
// Ignore empty strings
|
|
213
|
+
if (!value) continue
|
|
214
|
+
|
|
215
|
+
const validator = this.shapeValidators.get(name)
|
|
216
|
+
const innerValidator = validator ? unwrapSchema(validator) : undefined
|
|
217
|
+
const expectsArray = innerValidator instanceof ArraySchema
|
|
176
218
|
const scalarValidator = expectsArray
|
|
177
|
-
?
|
|
178
|
-
:
|
|
179
|
-
|
|
180
|
-
const coerced
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
: value === 'true'
|
|
184
|
-
? true
|
|
185
|
-
: value === 'false'
|
|
186
|
-
? false
|
|
187
|
-
: /^-?\d+$/.test(value)
|
|
188
|
-
? Number(value)
|
|
189
|
-
: value
|
|
190
|
-
|
|
191
|
-
const currentParam = params[key]
|
|
219
|
+
? unwrapSchema(innerValidator.validator)
|
|
220
|
+
: innerValidator
|
|
221
|
+
|
|
222
|
+
const coerced = coerceParam(name, value, scalarValidator, options)
|
|
223
|
+
|
|
224
|
+
const currentParam = params[name]
|
|
192
225
|
if (currentParam === undefined) {
|
|
193
|
-
params[
|
|
226
|
+
params[name] = expectsArray ? [coerced] : coerced
|
|
194
227
|
} else if (Array.isArray(currentParam)) {
|
|
195
228
|
currentParam.push(coerced)
|
|
196
229
|
} else {
|
|
197
|
-
params[
|
|
230
|
+
params[name] = [currentParam, coerced]
|
|
198
231
|
}
|
|
199
232
|
}
|
|
200
233
|
|
|
201
|
-
return this.parse(params)
|
|
234
|
+
return this.parse(params, options)
|
|
202
235
|
}
|
|
203
236
|
|
|
204
237
|
toURLSearchParams(input: InferInput<this>): URLSearchParams {
|
|
@@ -222,6 +255,63 @@ export class ParamsSchema<
|
|
|
222
255
|
}
|
|
223
256
|
}
|
|
224
257
|
|
|
258
|
+
function coerceParam(
|
|
259
|
+
name: string,
|
|
260
|
+
param: string,
|
|
261
|
+
schema?: ParamScalarValidator,
|
|
262
|
+
options?: ParseOptions,
|
|
263
|
+
): ParamScalar {
|
|
264
|
+
let issue: Issue
|
|
265
|
+
|
|
266
|
+
if (!schema) {
|
|
267
|
+
// The param is unknown (not defined in schema), so we don't apply any
|
|
268
|
+
// coercion and just return the string value.
|
|
269
|
+
return param
|
|
270
|
+
} else if (schema instanceof StringSchema) {
|
|
271
|
+
return param
|
|
272
|
+
} else if (schema instanceof IntegerSchema) {
|
|
273
|
+
if (/^-?\d+$/.test(param)) return Number(param)
|
|
274
|
+
issue = new IssueInvalidType(paramPath(name, options), param, ['integer'])
|
|
275
|
+
} else if (schema instanceof BooleanSchema) {
|
|
276
|
+
if (param === 'true') return true
|
|
277
|
+
if (param === 'false') return false
|
|
278
|
+
issue = new IssueInvalidType(paramPath(name, options), param, ['boolean'])
|
|
279
|
+
} else if (schema instanceof LiteralSchema) {
|
|
280
|
+
const { value } = schema
|
|
281
|
+
if (String(value) === param) return value
|
|
282
|
+
issue = new IssueInvalidValue(paramPath(name, options), param, [value])
|
|
283
|
+
} else if (schema instanceof EnumSchema) {
|
|
284
|
+
const { values } = schema
|
|
285
|
+
for (const value of values) {
|
|
286
|
+
if (String(value) === param) return value
|
|
287
|
+
}
|
|
288
|
+
issue = new IssueInvalidValue(paramPath(name, options), param, values)
|
|
289
|
+
} else {
|
|
290
|
+
// This should never happen. If it *does*, it means that the user of
|
|
291
|
+
// lex-schema is mixing different versions of the lib, which is not
|
|
292
|
+
// supported. Throwing an error here is better than silently accepting
|
|
293
|
+
// invalid params and causing unexpected behavior down the line (ie. error
|
|
294
|
+
// message returning the string value instead of the expected
|
|
295
|
+
// boolean/number/string value).
|
|
296
|
+
throw new Error(`Unsupported schema type for param coercion: ${schema}`)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// We were not able to coerce the param to the expected type. There is no
|
|
300
|
+
// point in returning the original string value since it doesn't conform to
|
|
301
|
+
// the expected schema, so we throw a validation error instead. We could
|
|
302
|
+
// return the "param" here, which would cause the validation to fail later on
|
|
303
|
+
// (see fromURLSearchParams()'s return statement). The main benefit of
|
|
304
|
+
// returning the original "param" value is that the error path would include
|
|
305
|
+
// the index of the param in case of array params (e.g. "tags[1]"), which
|
|
306
|
+
// could be helpful for debugging. The cost overhead is not worth it though
|
|
307
|
+
// (IMO).
|
|
308
|
+
throw new LexValidationError([issue])
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function paramPath(key: string, options?: ParseOptions) {
|
|
312
|
+
return options?.path ? [...options.path, key] : [key]
|
|
313
|
+
}
|
|
314
|
+
|
|
225
315
|
/**
|
|
226
316
|
* Creates a params schema for URL query parameters.
|
|
227
317
|
*
|
|
@@ -258,17 +348,24 @@ export class ParamsSchema<
|
|
|
258
348
|
* ```
|
|
259
349
|
*/
|
|
260
350
|
export const params = /*#__PURE__*/ memoizedOptions(function params<
|
|
261
|
-
const TShape extends
|
|
351
|
+
const TShape extends ParamsShape = NonNullable<unknown>,
|
|
262
352
|
>(properties: TShape = {} as TShape) {
|
|
263
353
|
return new ParamsSchema<TShape>(properties)
|
|
264
354
|
})
|
|
265
355
|
|
|
266
|
-
|
|
356
|
+
type UnwrapSchema<S extends Validator> =
|
|
357
|
+
S extends OptionalSchema<infer U>
|
|
358
|
+
? UnwrapSchema<U>
|
|
359
|
+
: S extends WithDefaultSchema<infer U>
|
|
360
|
+
? UnwrapSchema<U>
|
|
361
|
+
: S
|
|
362
|
+
|
|
363
|
+
function unwrapSchema<S extends Validator>(schema: S): UnwrapSchema<S> {
|
|
267
364
|
while (
|
|
268
365
|
schema instanceof OptionalSchema ||
|
|
269
366
|
schema instanceof WithDefaultSchema
|
|
270
367
|
) {
|
|
271
|
-
|
|
368
|
+
return unwrapSchema(schema.validator)
|
|
272
369
|
}
|
|
273
|
-
return schema
|
|
370
|
+
return schema as UnwrapSchema<S>
|
|
274
371
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { integer } from './integer.js'
|
|
3
|
+
import { lexMap } from './lex-map.js'
|
|
3
4
|
import { object } from './object.js'
|
|
4
5
|
import { payload } from './payload.js'
|
|
5
6
|
import { string } from './string.js'
|
|
6
|
-
import { unknownObject } from './unknown-object.js'
|
|
7
7
|
|
|
8
8
|
describe('Payload', () => {
|
|
9
9
|
describe('basic construction', () => {
|
|
@@ -224,7 +224,7 @@ describe('Payload', () => {
|
|
|
224
224
|
'application/json',
|
|
225
225
|
object({
|
|
226
226
|
success: string(),
|
|
227
|
-
data:
|
|
227
|
+
data: lexMap(),
|
|
228
228
|
}),
|
|
229
229
|
)
|
|
230
230
|
expect(def.encoding).toBe('application/json')
|
package/src/schema/payload.ts
CHANGED
|
@@ -107,15 +107,13 @@ export class Payload<
|
|
|
107
107
|
* encoding.
|
|
108
108
|
*/
|
|
109
109
|
matchesEncoding(contentType: string | undefined): boolean {
|
|
110
|
-
const mime = contentType?.split(';', 1)[0].trim()
|
|
111
|
-
|
|
112
110
|
const { encoding } = this
|
|
113
111
|
|
|
114
112
|
// Handle undefined cases
|
|
115
113
|
if (encoding === undefined) {
|
|
116
114
|
// Expecting no body
|
|
117
|
-
return
|
|
118
|
-
} else if (
|
|
115
|
+
return contentType == null
|
|
116
|
+
} else if (contentType == null) {
|
|
119
117
|
// Expecting a body, but got no content-type
|
|
120
118
|
return false
|
|
121
119
|
}
|
|
@@ -124,6 +122,7 @@ export class Payload<
|
|
|
124
122
|
return true
|
|
125
123
|
}
|
|
126
124
|
|
|
125
|
+
const mime = contentType?.split(';', 1)[0].trim()
|
|
127
126
|
if (encoding.endsWith('/*')) {
|
|
128
127
|
return mime.startsWith(encoding.slice(0, -1))
|
|
129
128
|
}
|
package/src/schema/record.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ValidationContext,
|
|
12
12
|
Validator,
|
|
13
13
|
} from '../core.js'
|
|
14
|
+
import { lazyProperty } from '../util/lazy-property.js'
|
|
14
15
|
import { literal } from './literal.js'
|
|
15
16
|
import { string } from './string.js'
|
|
16
17
|
|
|
@@ -22,6 +23,13 @@ import { string } from './string.js'
|
|
|
22
23
|
export type InferRecordKey<R extends RecordSchema> =
|
|
23
24
|
R extends RecordSchema<infer TKey> ? RecordKeySchemaOutput<TKey> : never
|
|
24
25
|
|
|
26
|
+
export type TypedRecord<
|
|
27
|
+
TType extends NsidString,
|
|
28
|
+
TValue extends { $type?: unknown } = { $type?: unknown },
|
|
29
|
+
> = TValue extends { $type: TType }
|
|
30
|
+
? TValue
|
|
31
|
+
: $Typed<Exclude<TValue, Unknown$TypedObject>, TType>
|
|
32
|
+
|
|
25
33
|
/**
|
|
26
34
|
* Schema for AT Protocol records with a type identifier and key constraints.
|
|
27
35
|
*
|
|
@@ -50,6 +58,8 @@ export class RecordSchema<
|
|
|
50
58
|
$Typed<InferInput<TShape>, TType>,
|
|
51
59
|
$Typed<InferOutput<TShape>, TType>
|
|
52
60
|
> {
|
|
61
|
+
readonly type = 'record' as const
|
|
62
|
+
|
|
53
63
|
keySchema: RecordKeySchema<TKey>
|
|
54
64
|
|
|
55
65
|
constructor(
|
|
@@ -61,28 +71,6 @@ export class RecordSchema<
|
|
|
61
71
|
this.keySchema = recordKey(key)
|
|
62
72
|
}
|
|
63
73
|
|
|
64
|
-
isTypeOf<X extends { $type?: unknown }>(
|
|
65
|
-
value: X,
|
|
66
|
-
): value is X extends { $type: TType }
|
|
67
|
-
? X
|
|
68
|
-
: $Typed<Exclude<X, Unknown$TypedObject>, TType> {
|
|
69
|
-
return value.$type === this.$type
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
build(
|
|
73
|
-
input: Omit<InferInput<this>, '$type'>,
|
|
74
|
-
): $Typed<InferOutput<this>, TType> {
|
|
75
|
-
return this.parse($typed(input, this.$type))
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
$isTypeOf<X extends { $type?: unknown }>(value: X) {
|
|
79
|
-
return this.isTypeOf<X>(value)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
$build(input: Omit<InferInput<this>, '$type'>) {
|
|
83
|
-
return this.build(input)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
74
|
validateInContext(input: unknown, ctx: ValidationContext) {
|
|
87
75
|
const result = ctx.validate(input, this.schema)
|
|
88
76
|
|
|
@@ -96,6 +84,34 @@ export class RecordSchema<
|
|
|
96
84
|
|
|
97
85
|
return result
|
|
98
86
|
}
|
|
87
|
+
|
|
88
|
+
build(
|
|
89
|
+
input: Omit<InferInput<this>, '$type'>,
|
|
90
|
+
): $Typed<InferOutput<this>, TType> {
|
|
91
|
+
return this.parse($typed(input, this.$type))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
isTypeOf<TValue extends { $type?: unknown }>(
|
|
95
|
+
value: TValue,
|
|
96
|
+
): value is TypedRecord<TType, TValue> {
|
|
97
|
+
return value.$type === this.$type
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Bound alias for {@link build} for compatibility with generated utilities.
|
|
102
|
+
* @see {@link build}
|
|
103
|
+
*/
|
|
104
|
+
get $build(): typeof this.build {
|
|
105
|
+
return lazyProperty(this, '$build', this.build.bind(this))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Bound alias for {@link isTypeOf} for compatibility with generated utilities.
|
|
110
|
+
* @see {@link isTypeOf}
|
|
111
|
+
*/
|
|
112
|
+
get $isTypeOf(): typeof this.isTypeOf {
|
|
113
|
+
return lazyProperty(this, '$isTypeOf', this.isTypeOf.bind(this))
|
|
114
|
+
}
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
export type RecordKeySchemaOutput<Key extends LexiconRecordKey> =
|
package/src/schema/ref.ts
CHANGED
package/src/schema/regexp.ts
CHANGED
|
@@ -18,13 +18,15 @@ import { Schema, ValidationContext } from '../core.js'
|
|
|
18
18
|
export class RegexpSchema<
|
|
19
19
|
TValue extends string = string,
|
|
20
20
|
> extends Schema<TValue> {
|
|
21
|
+
readonly type = 'regexp' as const
|
|
22
|
+
|
|
21
23
|
constructor(public readonly pattern: RegExp) {
|
|
22
24
|
super()
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
validateInContext(input: unknown, ctx: ValidationContext) {
|
|
26
28
|
if (typeof input !== 'string') {
|
|
27
|
-
return ctx.
|
|
29
|
+
return ctx.issueUnexpectedType(input, 'string')
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
if (!this.pattern.test(input)) {
|