@atproto/lex-schema 0.0.11 → 0.0.13
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 +54 -0
- package/dist/core/$type.d.ts +149 -0
- package/dist/core/$type.d.ts.map +1 -1
- package/dist/core/$type.js +44 -0
- package/dist/core/$type.js.map +1 -1
- package/dist/core/record-key.d.ts +44 -0
- package/dist/core/record-key.d.ts.map +1 -1
- package/dist/core/record-key.js +30 -0
- package/dist/core/record-key.js.map +1 -1
- package/dist/core/result.d.ts +85 -4
- package/dist/core/result.d.ts.map +1 -1
- package/dist/core/result.js +60 -4
- package/dist/core/result.js.map +1 -1
- package/dist/core/schema.d.ts +232 -5
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/schema.js +197 -4
- package/dist/core/schema.js.map +1 -1
- package/dist/core/string-format.d.ts +244 -11
- package/dist/core/string-format.d.ts.map +1 -1
- package/dist/core/string-format.js +150 -0
- package/dist/core/string-format.js.map +1 -1
- package/dist/core/types.d.ts +90 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/validation-error.d.ts +60 -0
- package/dist/core/validation-error.d.ts.map +1 -1
- package/dist/core/validation-error.js +60 -0
- package/dist/core/validation-error.js.map +1 -1
- package/dist/core/validation-issue.d.ts +61 -0
- package/dist/core/validation-issue.d.ts.map +1 -1
- package/dist/core/validation-issue.js +54 -1
- package/dist/core/validation-issue.js.map +1 -1
- package/dist/core/validator.d.ts +356 -11
- package/dist/core/validator.d.ts.map +1 -1
- package/dist/core/validator.js +203 -4
- package/dist/core/validator.js.map +1 -1
- package/dist/helpers.d.ts +12 -28
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js.map +1 -1
- package/dist/schema/array.d.ts +46 -0
- package/dist/schema/array.d.ts.map +1 -1
- package/dist/schema/array.js +16 -1
- package/dist/schema/array.js.map +1 -1
- package/dist/schema/blob.d.ts +50 -2
- package/dist/schema/blob.d.ts.map +1 -1
- package/dist/schema/blob.js +44 -2
- package/dist/schema/blob.js.map +1 -1
- package/dist/schema/boolean.d.ts +29 -0
- package/dist/schema/boolean.d.ts.map +1 -1
- package/dist/schema/boolean.js +30 -1
- package/dist/schema/boolean.js.map +1 -1
- package/dist/schema/bytes.d.ts +39 -0
- package/dist/schema/bytes.d.ts.map +1 -1
- package/dist/schema/bytes.js +34 -1
- package/dist/schema/bytes.js.map +1 -1
- package/dist/schema/cid.d.ts +39 -0
- package/dist/schema/cid.d.ts.map +1 -1
- package/dist/schema/cid.js +35 -1
- package/dist/schema/cid.js.map +1 -1
- package/dist/schema/custom.d.ts +67 -1
- package/dist/schema/custom.d.ts.map +1 -1
- package/dist/schema/custom.js +55 -0
- package/dist/schema/custom.js.map +1 -1
- package/dist/schema/dict.d.ts +45 -0
- package/dist/schema/dict.d.ts.map +1 -1
- package/dist/schema/dict.js +46 -1
- package/dist/schema/dict.js.map +1 -1
- package/dist/schema/discriminated-union.d.ts +59 -0
- package/dist/schema/discriminated-union.d.ts.map +1 -1
- package/dist/schema/discriminated-union.js +47 -1
- package/dist/schema/discriminated-union.js.map +1 -1
- package/dist/schema/enum.d.ts +49 -0
- package/dist/schema/enum.d.ts.map +1 -1
- package/dist/schema/enum.js +49 -0
- package/dist/schema/enum.js.map +1 -1
- package/dist/schema/integer.d.ts +43 -0
- package/dist/schema/integer.d.ts.map +1 -1
- package/dist/schema/integer.js +38 -1
- package/dist/schema/integer.js.map +1 -1
- package/dist/schema/intersection.d.ts +55 -0
- package/dist/schema/intersection.d.ts.map +1 -1
- package/dist/schema/intersection.js +50 -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 +45 -0
- package/dist/schema/literal.d.ts.map +1 -1
- package/dist/schema/literal.js +45 -0
- package/dist/schema/literal.js.map +1 -1
- package/dist/schema/never.d.ts +43 -0
- package/dist/schema/never.d.ts.map +1 -1
- package/dist/schema/never.js +44 -1
- package/dist/schema/never.js.map +1 -1
- package/dist/schema/null.d.ts +30 -0
- package/dist/schema/null.d.ts.map +1 -1
- package/dist/schema/null.js +31 -1
- package/dist/schema/null.js.map +1 -1
- package/dist/schema/nullable.d.ts +42 -0
- package/dist/schema/nullable.d.ts.map +1 -1
- package/dist/schema/nullable.js +42 -0
- package/dist/schema/nullable.js.map +1 -1
- package/dist/schema/object.d.ts +57 -0
- package/dist/schema/object.d.ts.map +1 -1
- package/dist/schema/object.js +53 -1
- package/dist/schema/object.js.map +1 -1
- package/dist/schema/optional.d.ts +43 -0
- package/dist/schema/optional.d.ts.map +1 -1
- package/dist/schema/optional.js +43 -0
- package/dist/schema/optional.js.map +1 -1
- package/dist/schema/params.d.ts +96 -12
- package/dist/schema/params.d.ts.map +1 -1
- package/dist/schema/params.js +155 -21
- package/dist/schema/params.js.map +1 -1
- package/dist/schema/payload.d.ts +111 -15
- package/dist/schema/payload.d.ts.map +1 -1
- package/dist/schema/payload.js +73 -3
- package/dist/schema/payload.js.map +1 -1
- package/dist/schema/permission-set.d.ts +58 -0
- package/dist/schema/permission-set.d.ts.map +1 -1
- package/dist/schema/permission-set.js +50 -0
- package/dist/schema/permission-set.js.map +1 -1
- package/dist/schema/permission.d.ts +42 -0
- package/dist/schema/permission.d.ts.map +1 -1
- package/dist/schema/permission.js +39 -0
- package/dist/schema/permission.js.map +1 -1
- package/dist/schema/procedure.d.ts +64 -0
- package/dist/schema/procedure.d.ts.map +1 -1
- package/dist/schema/procedure.js +64 -0
- package/dist/schema/procedure.js.map +1 -1
- package/dist/schema/query.d.ts +55 -0
- package/dist/schema/query.d.ts.map +1 -1
- package/dist/schema/query.js +55 -0
- package/dist/schema/query.js.map +1 -1
- package/dist/schema/record.d.ts +76 -25
- package/dist/schema/record.d.ts.map +1 -1
- package/dist/schema/record.js +21 -0
- package/dist/schema/record.js.map +1 -1
- package/dist/schema/ref.d.ts +51 -0
- package/dist/schema/ref.d.ts.map +1 -1
- package/dist/schema/ref.js +18 -0
- package/dist/schema/ref.js.map +1 -1
- package/dist/schema/refine.d.ts +58 -9
- package/dist/schema/refine.d.ts.map +1 -1
- package/dist/schema/refine.js.map +1 -1
- package/dist/schema/regexp.d.ts +45 -0
- package/dist/schema/regexp.d.ts.map +1 -1
- package/dist/schema/regexp.js +46 -1
- package/dist/schema/regexp.js.map +1 -1
- package/dist/schema/string.d.ts +72 -6
- package/dist/schema/string.d.ts.map +1 -1
- package/dist/schema/string.js +56 -8
- package/dist/schema/string.js.map +1 -1
- package/dist/schema/subscription.d.ts +72 -2
- package/dist/schema/subscription.d.ts.map +1 -1
- package/dist/schema/subscription.js +59 -0
- package/dist/schema/subscription.js.map +1 -1
- package/dist/schema/token.d.ts +48 -0
- package/dist/schema/token.d.ts.map +1 -1
- package/dist/schema/token.js +49 -1
- package/dist/schema/token.js.map +1 -1
- package/dist/schema/typed-object.d.ts +73 -23
- package/dist/schema/typed-object.d.ts.map +1 -1
- package/dist/schema/typed-object.js +20 -1
- package/dist/schema/typed-object.js.map +1 -1
- package/dist/schema/typed-ref.d.ts +54 -0
- package/dist/schema/typed-ref.d.ts.map +1 -1
- package/dist/schema/typed-ref.js +16 -0
- package/dist/schema/typed-ref.js.map +1 -1
- package/dist/schema/typed-union.d.ts +51 -1
- package/dist/schema/typed-union.d.ts.map +1 -1
- package/dist/schema/typed-union.js +52 -2
- package/dist/schema/typed-union.js.map +1 -1
- package/dist/schema/union.d.ts +46 -0
- package/dist/schema/union.d.ts.map +1 -1
- package/dist/schema/union.js +41 -0
- package/dist/schema/union.js.map +1 -1
- package/dist/schema/unknown.d.ts +34 -0
- package/dist/schema/unknown.d.ts.map +1 -1
- package/dist/schema/unknown.js +34 -0
- package/dist/schema/unknown.js.map +1 -1
- package/dist/schema/with-default.d.ts +45 -0
- package/dist/schema/with-default.d.ts.map +1 -1
- package/dist/schema/with-default.js +45 -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/$type.ts +150 -18
- package/src/core/record-key.ts +44 -0
- package/src/core/result.ts +86 -4
- package/src/core/schema.ts +244 -9
- package/src/core/string-format.ts +259 -13
- package/src/core/types.ts +91 -3
- package/src/core/validation-error.ts +60 -0
- package/src/core/validation-issue.ts +68 -2
- package/src/core/validator.ts +373 -12
- package/src/helpers.test.ts +110 -29
- package/src/helpers.ts +54 -25
- package/src/schema/array.test.ts +94 -79
- package/src/schema/array.ts +48 -1
- package/src/schema/blob.ts +50 -1
- package/src/schema/boolean.ts +31 -1
- package/src/schema/bytes.ts +41 -1
- package/src/schema/cid.ts +41 -1
- package/src/schema/custom.ts +68 -1
- package/src/schema/dict.ts +47 -1
- package/src/schema/discriminated-union.ts +61 -1
- package/src/schema/enum.ts +50 -0
- package/src/schema/integer.ts +45 -1
- package/src/schema/intersection.ts +56 -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 +46 -0
- package/src/schema/never.ts +45 -1
- package/src/schema/null.ts +32 -1
- package/src/schema/nullable.ts +43 -0
- package/src/schema/object.ts +59 -1
- package/src/schema/optional.ts +44 -0
- package/src/schema/params.test.ts +133 -38
- package/src/schema/params.ts +237 -37
- package/src/schema/payload.test.ts +3 -3
- package/src/schema/payload.ts +145 -42
- package/src/schema/permission-set.ts +58 -0
- package/src/schema/permission.ts +42 -0
- package/src/schema/procedure.ts +64 -0
- package/src/schema/query.ts +55 -0
- package/src/schema/record.ts +82 -16
- package/src/schema/ref.ts +52 -0
- package/src/schema/refine.ts +58 -9
- package/src/schema/regexp.ts +47 -1
- package/src/schema/string.test.ts +99 -2
- package/src/schema/string.ts +108 -15
- package/src/schema/subscription.ts +72 -2
- package/src/schema/token.ts +50 -1
- package/src/schema/typed-object.ts +81 -16
- package/src/schema/typed-ref.ts +55 -0
- package/src/schema/typed-union.ts +58 -3
- package/src/schema/union.ts +47 -0
- package/src/schema/unknown.ts +35 -0
- package/src/schema/with-default.ts +46 -0
- package/src/schema.ts +2 -1
- package/src/util/if-any.ts +3 -0
- package/dist/schema/unknown-object.d.ts +0 -8
- package/dist/schema/unknown-object.d.ts.map +0 -1
- package/dist/schema/unknown-object.js +0 -19
- package/dist/schema/unknown-object.js.map +0 -1
- package/src/schema/unknown-object.ts +0 -19
|
@@ -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,78 +262,105 @@ 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(),
|
|
266
304
|
age: optional(integer()),
|
|
267
305
|
active: optional(boolean()),
|
|
306
|
+
tags: optional(array(string())),
|
|
307
|
+
ids: optional(array(integer())),
|
|
308
|
+
bools: optional(array(boolean())),
|
|
268
309
|
})
|
|
269
310
|
|
|
270
311
|
it('parses string parameters', () => {
|
|
271
|
-
const
|
|
272
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
312
|
+
const result = schema.fromURLSearchParams('name=Alice')
|
|
273
313
|
expect(result).toEqual({ name: 'Alice' })
|
|
274
314
|
})
|
|
275
315
|
|
|
276
316
|
it('parses and coerces boolean true', () => {
|
|
277
|
-
const
|
|
278
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
317
|
+
const result = schema.fromURLSearchParams('name=Alice&active=true')
|
|
279
318
|
expect(result).toEqual({ name: 'Alice', active: true })
|
|
280
319
|
})
|
|
281
320
|
|
|
282
321
|
it('parses and coerces boolean false', () => {
|
|
283
|
-
const
|
|
284
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
322
|
+
const result = schema.fromURLSearchParams('name=Alice&active=false')
|
|
285
323
|
expect(result).toEqual({ name: 'Alice', active: false })
|
|
286
324
|
})
|
|
287
325
|
|
|
288
326
|
it('parses and coerces integer values', () => {
|
|
289
|
-
const
|
|
290
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
327
|
+
const result = schema.fromURLSearchParams('name=Alice&age=30')
|
|
291
328
|
expect(result).toEqual({ name: 'Alice', age: 30 })
|
|
292
329
|
})
|
|
293
330
|
|
|
294
331
|
it('parses and coerces negative integers', () => {
|
|
295
|
-
const
|
|
296
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
332
|
+
const result = schema.fromURLSearchParams('name=Alice&age=-5')
|
|
297
333
|
expect(result).toEqual({ name: 'Alice', age: -5 })
|
|
298
334
|
})
|
|
299
335
|
|
|
300
336
|
it('does not coerce non-integer numbers', () => {
|
|
301
|
-
const
|
|
302
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
337
|
+
const result = schema.fromURLSearchParams('name=Alice&extra=3.14')
|
|
303
338
|
expect(result).toEqual({ name: 'Alice', extra: '3.14' })
|
|
304
339
|
})
|
|
305
340
|
|
|
306
341
|
it('keeps string values for string schema even if they look like numbers', () => {
|
|
307
|
-
const
|
|
308
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
342
|
+
const result = schema.fromURLSearchParams('name=123')
|
|
309
343
|
expect(result).toEqual({ name: '123' })
|
|
310
344
|
})
|
|
311
345
|
|
|
312
346
|
it('parses multiple values as array', () => {
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
expect(result).toEqual({ name: 'Alice', tag: ['one', 'two'] })
|
|
347
|
+
const result = schema.fromURLSearchParams('name=Alice&tags=one&tags=two')
|
|
348
|
+
expect(result).toEqual({ name: 'Alice', tags: ['one', 'two'] })
|
|
316
349
|
})
|
|
317
350
|
|
|
318
|
-
it('
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
})
|
|
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' })
|
|
323
355
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
)
|
|
328
|
-
const result = schema.fromURLSearchParams(urlParams)
|
|
329
|
-
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'] })
|
|
330
359
|
})
|
|
331
360
|
|
|
332
361
|
it('handles empty URLSearchParams', () => {
|
|
333
|
-
|
|
334
|
-
expect(() => schema.fromURLSearchParams(
|
|
362
|
+
expect(() => schema.fromURLSearchParams(new URLSearchParams())).toThrow()
|
|
363
|
+
expect(() => schema.fromURLSearchParams('')).toThrow()
|
|
335
364
|
})
|
|
336
365
|
|
|
337
366
|
it('handles multiple parameters', () => {
|
|
@@ -346,6 +375,64 @@ describe('ParamsSchema', () => {
|
|
|
346
375
|
extra: 'value',
|
|
347
376
|
})
|
|
348
377
|
})
|
|
378
|
+
|
|
379
|
+
it('coerces single values into arrays in parse mode', () => {
|
|
380
|
+
expect(
|
|
381
|
+
schema.fromURLSearchParams([
|
|
382
|
+
['name', 'Alice'],
|
|
383
|
+
['tags', 'tag1'],
|
|
384
|
+
]),
|
|
385
|
+
).toEqual({ name: 'Alice', tags: ['tag1'] })
|
|
386
|
+
|
|
387
|
+
expect(
|
|
388
|
+
schema.fromURLSearchParams([
|
|
389
|
+
['name', 'Alice'],
|
|
390
|
+
['tags', 'true'],
|
|
391
|
+
]),
|
|
392
|
+
).toEqual({ name: 'Alice', tags: ['true'] })
|
|
393
|
+
|
|
394
|
+
expect(
|
|
395
|
+
schema.fromURLSearchParams([
|
|
396
|
+
['name', 'Alice'],
|
|
397
|
+
['tags', '1'],
|
|
398
|
+
]),
|
|
399
|
+
).toEqual({ name: 'Alice', tags: ['1'] })
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('coerces single boolean values into arrays in parse mode', () => {
|
|
403
|
+
expect(
|
|
404
|
+
schema.fromURLSearchParams([
|
|
405
|
+
['name', 'Alice'],
|
|
406
|
+
['bools', 'true'],
|
|
407
|
+
]),
|
|
408
|
+
).toEqual({ name: 'Alice', bools: [true] })
|
|
409
|
+
|
|
410
|
+
expect(
|
|
411
|
+
schema.fromURLSearchParams([
|
|
412
|
+
['name', 'Alice'],
|
|
413
|
+
['bools', 'false'],
|
|
414
|
+
]),
|
|
415
|
+
).toEqual({ name: 'Alice', bools: [false] })
|
|
416
|
+
|
|
417
|
+
expect(() =>
|
|
418
|
+
schema.fromURLSearchParams([
|
|
419
|
+
['name', 'Alice'],
|
|
420
|
+
['bools', 'notabool'],
|
|
421
|
+
]),
|
|
422
|
+
).toThrow('Expected boolean value type at $.bools (got string)')
|
|
423
|
+
|
|
424
|
+
expect(() =>
|
|
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
|
+
})
|
|
349
436
|
})
|
|
350
437
|
|
|
351
438
|
describe('toURLSearchParams', () => {
|
|
@@ -405,15 +492,23 @@ describe('ParamsSchema', () => {
|
|
|
405
492
|
expect(result.toString()).toBe('name=Alice')
|
|
406
493
|
})
|
|
407
494
|
|
|
495
|
+
it('rejects arrays with multiple types', () => {
|
|
496
|
+
expect(() => {
|
|
497
|
+
schema.toURLSearchParams({
|
|
498
|
+
name: 'Alice',
|
|
499
|
+
// @ts-expect-error
|
|
500
|
+
values: [1, true, 'text'],
|
|
501
|
+
})
|
|
502
|
+
}).toThrow()
|
|
503
|
+
})
|
|
504
|
+
|
|
408
505
|
it('handles arrays with multiple types', () => {
|
|
409
506
|
const result = schema.toURLSearchParams({
|
|
410
507
|
name: 'Alice',
|
|
411
508
|
// @ts-expect-error
|
|
412
|
-
values: [
|
|
509
|
+
values: ['foo', 'bar'],
|
|
413
510
|
})
|
|
414
|
-
expect(result.toString()).toBe(
|
|
415
|
-
'name=Alice&values=1&values=true&values=text',
|
|
416
|
-
)
|
|
511
|
+
expect(result.toString()).toBe('name=Alice&values=foo&values=bar')
|
|
417
512
|
})
|
|
418
513
|
|
|
419
514
|
it('handles undefined input', () => {
|
|
@@ -655,9 +750,9 @@ describe('paramSchema', () => {
|
|
|
655
750
|
expect(result.success).toBe(true)
|
|
656
751
|
})
|
|
657
752
|
|
|
658
|
-
it('
|
|
753
|
+
it('rejects arrays with mixed scalar types', () => {
|
|
659
754
|
const result = paramSchema.safeParse([true, 42, 'text'])
|
|
660
|
-
expect(result.success).toBe(
|
|
755
|
+
expect(result.success).toBe(false)
|
|
661
756
|
})
|
|
662
757
|
|
|
663
758
|
it('validates arrays with negative integers', () => {
|
|
@@ -828,11 +923,11 @@ describe('paramsSchema', () => {
|
|
|
828
923
|
expect(result.success).toBe(true)
|
|
829
924
|
})
|
|
830
925
|
|
|
831
|
-
it('
|
|
926
|
+
it('rejects object with arrays of mixed scalar types', () => {
|
|
832
927
|
const result = paramsSchema.safeParse({
|
|
833
928
|
values: [true, 42, 'text'],
|
|
834
929
|
})
|
|
835
|
-
expect(result.success).toBe(
|
|
930
|
+
expect(result.success).toBe(false)
|
|
836
931
|
})
|
|
837
932
|
|
|
838
933
|
it('validates object with numeric string keys', () => {
|
package/src/schema/params.ts
CHANGED
|
@@ -3,38 +3,123 @@ import {
|
|
|
3
3
|
Infer,
|
|
4
4
|
InferInput,
|
|
5
5
|
InferOutput,
|
|
6
|
+
Issue,
|
|
7
|
+
IssueInvalidType,
|
|
8
|
+
IssueInvalidValue,
|
|
9
|
+
ParseOptions,
|
|
6
10
|
Schema,
|
|
7
11
|
ValidationContext,
|
|
12
|
+
ValidationError,
|
|
8
13
|
Validator,
|
|
9
14
|
WithOptionalProperties,
|
|
10
15
|
} from '../core.js'
|
|
11
16
|
import { lazyProperty } from '../util/lazy-property.js'
|
|
12
17
|
import { memoizedOptions } from '../util/memoize.js'
|
|
13
|
-
import { array } from './array.js'
|
|
14
|
-
import { boolean } from './boolean.js'
|
|
18
|
+
import { ArraySchema, array } from './array.js'
|
|
19
|
+
import { BooleanSchema, boolean } from './boolean.js'
|
|
15
20
|
import { dict } from './dict.js'
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
21
|
+
import { EnumSchema } from './enum.js'
|
|
22
|
+
import { IntegerSchema, integer } from './integer.js'
|
|
23
|
+
import { LiteralSchema } from './literal.js'
|
|
24
|
+
import { OptionalSchema, optional } from './optional.js'
|
|
18
25
|
import { StringSchema, string } from './string.js'
|
|
19
26
|
import { union } from './union.js'
|
|
27
|
+
import { WithDefaultSchema } from './with-default.js'
|
|
20
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Scalar types allowed in URL parameters: boolean, integer, or string.
|
|
31
|
+
*/
|
|
21
32
|
export type ParamScalar = Infer<typeof paramScalarSchema>
|
|
22
33
|
const paramScalarSchema = union([boolean(), integer(), string()])
|
|
23
34
|
|
|
35
|
+
/**
|
|
36
|
+
* A single parameter value: scalar or array of scalars.
|
|
37
|
+
*/
|
|
24
38
|
export type Param = Infer<typeof paramSchema>
|
|
25
|
-
export const paramSchema = union([paramScalarSchema, array(paramScalarSchema)])
|
|
26
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Schema for validating individual parameter values.
|
|
42
|
+
*/
|
|
43
|
+
export const paramSchema = union([
|
|
44
|
+
paramScalarSchema,
|
|
45
|
+
array(boolean()),
|
|
46
|
+
array(integer()),
|
|
47
|
+
array(string()),
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Type for a params object with string keys and optional param values.
|
|
52
|
+
*/
|
|
27
53
|
export type Params = Infer<typeof paramsSchema>
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Schema for validating arbitrary params objects.
|
|
57
|
+
*/
|
|
28
58
|
export const paramsSchema = dict(string(), optional(paramSchema))
|
|
29
59
|
|
|
30
|
-
export type
|
|
31
|
-
|
|
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 =
|
|
84
|
+
| ParamScalarValidator
|
|
85
|
+
| AsArrayParamSchema<ParamScalarValidator>
|
|
86
|
+
|
|
87
|
+
export type ParamValidator =
|
|
88
|
+
| ParamValueValidator
|
|
89
|
+
| OptionalSchema<ParamValueValidator>
|
|
90
|
+
| OptionalSchema<WithDefaultSchema<ParamValueValidator>>
|
|
91
|
+
| WithDefaultSchema<ParamValueValidator>
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Type representing the shape of a params schema definition.
|
|
95
|
+
*
|
|
96
|
+
* Maps parameter names to their validators (must be Param or undefined).
|
|
97
|
+
*/
|
|
98
|
+
export type ParamsShape = {
|
|
99
|
+
[x: string]: ParamValidator
|
|
32
100
|
}
|
|
33
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Schema for validating URL query parameters in Lexicon endpoints.
|
|
104
|
+
*
|
|
105
|
+
* Params are the query string parameters passed to queries, procedures,
|
|
106
|
+
* and subscriptions. Values must be scalars (boolean, integer, string)
|
|
107
|
+
* or arrays of scalars, as they need to be serializable to URL format.
|
|
108
|
+
*
|
|
109
|
+
* Provides methods for converting to/from URLSearchParams.
|
|
110
|
+
*
|
|
111
|
+
* @template TShape - The params shape type mapping names to validators
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* const schema = new ParamsSchema({
|
|
116
|
+
* limit: l.optional(l.integer({ minimum: 1, maximum: 100 })),
|
|
117
|
+
* cursor: l.optional(l.string()),
|
|
118
|
+
* })
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
34
121
|
export class ParamsSchema<
|
|
35
|
-
const TShape extends
|
|
36
|
-
[x: string]: Validator<Param | undefined>
|
|
37
|
-
},
|
|
122
|
+
const TShape extends ParamsShape = ParamsShape,
|
|
38
123
|
> extends Schema<
|
|
39
124
|
WithOptionalProperties<{
|
|
40
125
|
[K in keyof TShape]: InferInput<TShape[K]>
|
|
@@ -43,20 +128,21 @@ export class ParamsSchema<
|
|
|
43
128
|
[K in keyof TShape]: InferOutput<TShape[K]>
|
|
44
129
|
}>
|
|
45
130
|
> {
|
|
131
|
+
readonly type = 'params' as const
|
|
132
|
+
|
|
46
133
|
constructor(readonly shape: TShape) {
|
|
47
134
|
super()
|
|
48
135
|
}
|
|
49
136
|
|
|
50
|
-
get shapeValidators(): Map<string,
|
|
137
|
+
get shapeValidators(): Map<string, ParamValidator> {
|
|
51
138
|
const map = new Map(Object.entries(this.shape))
|
|
52
139
|
|
|
53
140
|
return lazyProperty(this, 'shapeValidators', map)
|
|
54
141
|
}
|
|
55
142
|
|
|
56
143
|
validateInContext(input: unknown, ctx: ValidationContext) {
|
|
57
|
-
// @TODO BETTER SUPPORT Input/Output
|
|
58
144
|
if (!isPlainObject(input)) {
|
|
59
|
-
return ctx.
|
|
145
|
+
return ctx.issueUnexpectedType(input, 'object')
|
|
60
146
|
}
|
|
61
147
|
|
|
62
148
|
// Lazily copy value
|
|
@@ -111,33 +197,38 @@ export class ParamsSchema<
|
|
|
111
197
|
return ctx.success(copy ?? input)
|
|
112
198
|
}
|
|
113
199
|
|
|
114
|
-
fromURLSearchParams(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
200
|
+
fromURLSearchParams(
|
|
201
|
+
input: string | Iterable<[string, string]>,
|
|
202
|
+
options?: ParseOptions,
|
|
203
|
+
): InferOutput<this> {
|
|
204
|
+
const params: Record<string, unknown> = {}
|
|
205
|
+
|
|
206
|
+
const iterable =
|
|
207
|
+
typeof input === 'string' ? new URLSearchParams(input) : input
|
|
208
|
+
const entries =
|
|
209
|
+
iterable instanceof URLSearchParams ? iterable.entries() : iterable
|
|
210
|
+
|
|
211
|
+
for (const [name, value] of entries) {
|
|
212
|
+
const validator = this.shapeValidators.get(name)
|
|
213
|
+
const innerValidator = validator ? unwrapSchema(validator) : undefined
|
|
214
|
+
const expectsArray = innerValidator instanceof ArraySchema
|
|
215
|
+
const scalarValidator = expectsArray
|
|
216
|
+
? unwrapSchema(innerValidator.validator)
|
|
217
|
+
: innerValidator
|
|
218
|
+
|
|
219
|
+
const coerced = coerceParam(name, value, scalarValidator, options)
|
|
220
|
+
|
|
221
|
+
const currentParam = params[name]
|
|
222
|
+
if (currentParam === undefined) {
|
|
223
|
+
params[name] = expectsArray ? [coerced] : coerced
|
|
224
|
+
} else if (Array.isArray(currentParam)) {
|
|
225
|
+
currentParam.push(coerced)
|
|
135
226
|
} else {
|
|
136
|
-
params[
|
|
227
|
+
params[name] = [currentParam, coerced]
|
|
137
228
|
}
|
|
138
229
|
}
|
|
139
230
|
|
|
140
|
-
return this.parse(params)
|
|
231
|
+
return this.parse(params, options)
|
|
141
232
|
}
|
|
142
233
|
|
|
143
234
|
toURLSearchParams(input: InferInput<this>): URLSearchParams {
|
|
@@ -161,8 +252,117 @@ export class ParamsSchema<
|
|
|
161
252
|
}
|
|
162
253
|
}
|
|
163
254
|
|
|
255
|
+
function coerceParam(
|
|
256
|
+
name: string,
|
|
257
|
+
param: string,
|
|
258
|
+
schema?: ParamScalarValidator,
|
|
259
|
+
options?: ParseOptions,
|
|
260
|
+
): ParamScalar {
|
|
261
|
+
let issue: Issue
|
|
262
|
+
|
|
263
|
+
if (!schema) {
|
|
264
|
+
// The param is unknown (not defined in schema), so we don't apply any
|
|
265
|
+
// coercion and just return the string value.
|
|
266
|
+
return param
|
|
267
|
+
} else if (schema instanceof StringSchema) {
|
|
268
|
+
return param
|
|
269
|
+
} else if (schema instanceof IntegerSchema) {
|
|
270
|
+
if (/^-?\d+$/.test(param)) return Number(param)
|
|
271
|
+
issue = new IssueInvalidType(paramPath(name, options), param, ['integer'])
|
|
272
|
+
} else if (schema instanceof BooleanSchema) {
|
|
273
|
+
if (param === 'true') return true
|
|
274
|
+
if (param === 'false') return false
|
|
275
|
+
issue = new IssueInvalidType(paramPath(name, options), param, ['boolean'])
|
|
276
|
+
} else if (schema instanceof LiteralSchema) {
|
|
277
|
+
const { value } = schema
|
|
278
|
+
if (String(value) === param) return value
|
|
279
|
+
issue = new IssueInvalidValue(paramPath(name, options), param, [value])
|
|
280
|
+
} else if (schema instanceof EnumSchema) {
|
|
281
|
+
const { values } = schema
|
|
282
|
+
for (const value of values) {
|
|
283
|
+
if (String(value) === param) return value
|
|
284
|
+
}
|
|
285
|
+
issue = new IssueInvalidValue(paramPath(name, options), param, values)
|
|
286
|
+
} else {
|
|
287
|
+
// This should never happen. If it *does*, it means that the user of
|
|
288
|
+
// lex-schema is mixing different versions of the lib, which is not
|
|
289
|
+
// supported. Throwing an error here is better than silently accepting
|
|
290
|
+
// invalid params and causing unexpected behavior down the line (ie. error
|
|
291
|
+
// message returning the string value instead of the expected
|
|
292
|
+
// boolean/number/string value).
|
|
293
|
+
throw new Error(`Unsupported schema type for param coercion: ${schema}`)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// We were not able to coerce the param to the expected type. There is no
|
|
297
|
+
// point in returning the original string value since it doesn't conform to
|
|
298
|
+
// the expected schema, so we throw a validation error instead. We could
|
|
299
|
+
// return the "param" here, which would cause the validation to fail later on
|
|
300
|
+
// (see fromURLSearchParams()'s return statement). The main benefit of
|
|
301
|
+
// returning the original "param" value is that the error path would include
|
|
302
|
+
// the index of the param in case of array params (e.g. "tags[1]"), which
|
|
303
|
+
// could be helpful for debugging. The cost overhead is not worth it though
|
|
304
|
+
// (IMO).
|
|
305
|
+
throw new ValidationError([issue])
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function paramPath(key: string, options?: ParseOptions) {
|
|
309
|
+
return options?.path ? [...options.path, key] : [key]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Creates a params schema for URL query parameters.
|
|
314
|
+
*
|
|
315
|
+
* Params schemas validate query string parameters for Lexicon endpoints.
|
|
316
|
+
* Values must be boolean, integer, string, or arrays of those types.
|
|
317
|
+
*
|
|
318
|
+
* @param properties - Object mapping parameter names to their validators
|
|
319
|
+
* @returns A new {@link ParamsSchema} instance
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```ts
|
|
323
|
+
* // Simple pagination params
|
|
324
|
+
* const paginationParams = l.params({
|
|
325
|
+
* limit: l.optional(l.withDefault(l.integer({ minimum: 1, maximum: 100 }), 50)),
|
|
326
|
+
* cursor: l.optional(l.string()),
|
|
327
|
+
* })
|
|
328
|
+
*
|
|
329
|
+
* // Required parameter
|
|
330
|
+
* const actorParams = l.params({
|
|
331
|
+
* actor: l.string({ format: 'at-identifier' }),
|
|
332
|
+
* })
|
|
333
|
+
*
|
|
334
|
+
* // Array parameter (multiple values)
|
|
335
|
+
* const filterParams = l.params({
|
|
336
|
+
* tags: l.optional(l.array(l.string())),
|
|
337
|
+
* })
|
|
338
|
+
*
|
|
339
|
+
* // Convert from URL
|
|
340
|
+
* const urlParams = new URLSearchParams('limit=25&cursor=abc')
|
|
341
|
+
* const validated = paginationParams.fromURLSearchParams(urlParams)
|
|
342
|
+
*
|
|
343
|
+
* // Convert to URL
|
|
344
|
+
* const searchParams = paginationParams.toURLSearchParams({ limit: 25 })
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
164
347
|
export const params = /*#__PURE__*/ memoizedOptions(function params<
|
|
165
|
-
const TShape extends
|
|
348
|
+
const TShape extends ParamsShape = NonNullable<unknown>,
|
|
166
349
|
>(properties: TShape = {} as TShape) {
|
|
167
350
|
return new ParamsSchema<TShape>(properties)
|
|
168
351
|
})
|
|
352
|
+
|
|
353
|
+
type UnwrapSchema<S extends Validator> =
|
|
354
|
+
S extends OptionalSchema<infer U>
|
|
355
|
+
? UnwrapSchema<U>
|
|
356
|
+
: S extends WithDefaultSchema<infer U>
|
|
357
|
+
? UnwrapSchema<U>
|
|
358
|
+
: S
|
|
359
|
+
|
|
360
|
+
function unwrapSchema<S extends Validator>(schema: S): UnwrapSchema<S> {
|
|
361
|
+
while (
|
|
362
|
+
schema instanceof OptionalSchema ||
|
|
363
|
+
schema instanceof WithDefaultSchema
|
|
364
|
+
) {
|
|
365
|
+
return unwrapSchema(schema.validator)
|
|
366
|
+
}
|
|
367
|
+
return schema as UnwrapSchema<S>
|
|
368
|
+
}
|
|
@@ -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 { unknown } from './unknown.js'
|
|
7
7
|
|
|
8
8
|
describe('Payload', () => {
|
|
9
9
|
describe('basic construction', () => {
|
|
@@ -113,7 +113,7 @@ describe('Payload', () => {
|
|
|
113
113
|
})
|
|
114
114
|
|
|
115
115
|
it('creates payload with unknown schema', () => {
|
|
116
|
-
const schema =
|
|
116
|
+
const schema = object({})
|
|
117
117
|
const def = payload('application/json', schema)
|
|
118
118
|
expect(def.encoding).toBe('application/json')
|
|
119
119
|
expect(def.schema).toBe(schema)
|
|
@@ -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')
|