@atproto/lex-schema 0.0.12 → 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 +35 -0
- package/dist/core/schema.d.ts +4 -4
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/schema.js +1 -1
- package/dist/core/schema.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 +10 -2
- package/dist/core/validator.d.ts.map +1 -1
- package/dist/core/validator.js +21 -3
- 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 +84 -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 +13 -17
- package/dist/schema/record.d.ts.map +1 -1
- package/dist/schema/record.js +1 -0
- 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 +11 -15
- package/dist/schema/typed-object.d.ts.map +1 -1
- package/dist/schema/typed-object.js +2 -1
- 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 +1 -0
- 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 +2 -2
- package/src/core/schema.ts +9 -3
- package/src/core/validation-issue.ts +3 -2
- package/src/core/validator.ts +23 -3
- 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 +82 -43
- package/src/schema/params.ts +133 -39
- package/src/schema/payload.test.ts +2 -2
- package/src/schema/payload.ts +3 -4
- package/src/schema/record.ts +19 -8
- 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.ts +19 -8
- package/src/schema/typed-ref.ts +2 -0
- package/src/schema/typed-union.ts +3 -1
- package/src/schema/union.ts +2 -0
- 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,9 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
2
|
+
import { lexMap } from './lex-map.js'
|
|
3
3
|
|
|
4
|
-
describe(
|
|
4
|
+
describe(lexMap, () => {
|
|
5
5
|
describe('basic validation', () => {
|
|
6
|
-
const schema =
|
|
6
|
+
const schema = lexMap()
|
|
7
7
|
|
|
8
8
|
it('accepts empty plain objects', () => {
|
|
9
9
|
const result = schema.safeParse({})
|
|
@@ -106,7 +106,7 @@ describe('UnknownObjectSchema', () => {
|
|
|
106
106
|
})
|
|
107
107
|
|
|
108
108
|
describe('rejects non-plain-objects', () => {
|
|
109
|
-
const schema =
|
|
109
|
+
const schema = lexMap()
|
|
110
110
|
|
|
111
111
|
it('rejects strings', () => {
|
|
112
112
|
const result = schema.safeParse('not an object')
|
|
@@ -208,7 +208,7 @@ describe('UnknownObjectSchema', () => {
|
|
|
208
208
|
})
|
|
209
209
|
|
|
210
210
|
describe('rejects invalid value types', () => {
|
|
211
|
-
const schema =
|
|
211
|
+
const schema = lexMap()
|
|
212
212
|
|
|
213
213
|
it('rejects objects with floating point numbers', () => {
|
|
214
214
|
const result = schema.safeParse({ value: 3.14 })
|
|
@@ -288,7 +288,7 @@ describe('UnknownObjectSchema', () => {
|
|
|
288
288
|
})
|
|
289
289
|
|
|
290
290
|
describe('rejects invalid nested values', () => {
|
|
291
|
-
const schema =
|
|
291
|
+
const schema = lexMap()
|
|
292
292
|
|
|
293
293
|
it('rejects deeply nested invalid values', () => {
|
|
294
294
|
const result = schema.safeParse({
|
|
@@ -335,7 +335,7 @@ describe('UnknownObjectSchema', () => {
|
|
|
335
335
|
})
|
|
336
336
|
|
|
337
337
|
describe('edge cases', () => {
|
|
338
|
-
const schema =
|
|
338
|
+
const schema = lexMap()
|
|
339
339
|
|
|
340
340
|
it('accepts objects with numeric string keys', () => {
|
|
341
341
|
const obj = { '0': 'zero', '1': 'one', '2': 'two' }
|
|
@@ -500,7 +500,7 @@ describe('UnknownObjectSchema', () => {
|
|
|
500
500
|
})
|
|
501
501
|
|
|
502
502
|
describe('large objects', () => {
|
|
503
|
-
const schema =
|
|
503
|
+
const schema = lexMap()
|
|
504
504
|
|
|
505
505
|
it('accepts objects with many keys', () => {
|
|
506
506
|
const obj: Record<string, number> = {}
|
|
@@ -538,7 +538,7 @@ describe('UnknownObjectSchema', () => {
|
|
|
538
538
|
})
|
|
539
539
|
|
|
540
540
|
describe('preservation of input', () => {
|
|
541
|
-
const schema =
|
|
541
|
+
const schema = lexMap()
|
|
542
542
|
|
|
543
543
|
it('preserves the original object reference', () => {
|
|
544
544
|
const input = { key: 'value', count: 42 }
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { LexMap, isPlainObject } from '@atproto/lex-data'
|
|
2
|
+
import { Schema, ValidationContext } from '../core.js'
|
|
3
|
+
import { memoizedOptions } from '../util/memoize.js'
|
|
4
|
+
import { lexValue } from './lex-value.js'
|
|
5
|
+
|
|
6
|
+
const propertyValueSchema = /*#__PURE__*/ lexValue()
|
|
7
|
+
|
|
8
|
+
export type { LexMap }
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* AT Protocol lexicon schema definitions with "type": "unknown" are represented
|
|
12
|
+
* as plain objects with string keys and values that are valid AT Protocol data
|
|
13
|
+
* types (string, integer, boolean, null, bytes, cid, array, or object). This
|
|
14
|
+
* type alias corresponds to the expected structure of such "unknown" schema
|
|
15
|
+
* values.
|
|
16
|
+
*/
|
|
17
|
+
export class LexMapSchema extends Schema<LexMap> {
|
|
18
|
+
readonly type = 'lexMap' as const
|
|
19
|
+
|
|
20
|
+
validateInContext(input: unknown, ctx: ValidationContext) {
|
|
21
|
+
if (!isPlainObject(input)) {
|
|
22
|
+
return ctx.issueUnexpectedType(input, 'object')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const key of Object.keys(input)) {
|
|
26
|
+
// @NOTE We use a lexValue() schema here to recursively validate all
|
|
27
|
+
// nested values, which ensures that the error reporting includes the
|
|
28
|
+
// correct path and type information for any invalid nested values. This
|
|
29
|
+
// allows for more informative error descriptions than a simple "isLexMap"
|
|
30
|
+
// check.
|
|
31
|
+
const r = ctx.validateChild(input, key, propertyValueSchema) // recursively validate all properties
|
|
32
|
+
if (!r.success) return r
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return ctx.success(input)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates a schema that accepts any plain object with string keys and values
|
|
41
|
+
* that are valid AT Protocol data types (string, integer, boolean, null, bytes,
|
|
42
|
+
* cid, array, or object).
|
|
43
|
+
*
|
|
44
|
+
* @see {@link LexMap} from `@atproto/lex-data` for the type definition of valid AT Protocol data types
|
|
45
|
+
* @returns A new {@link LexMapSchema} instance
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* // Accept any object shape
|
|
50
|
+
* const schema = l.lexMap()
|
|
51
|
+
*
|
|
52
|
+
* schema.validate({ any: 'props' }) // success
|
|
53
|
+
* schema.validate([1, 2, 3]) // fails - only plain objects are accepted
|
|
54
|
+
* schema.validate({ foo: new Date() }) // fails - Date is not a valid LexValue
|
|
55
|
+
* schema.validate({ foo: 1.2 }) // fails - 1.2 is not a valid LexValue (not an integer)
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export const lexMap = /*#__PURE__*/ memoizedOptions(function () {
|
|
59
|
+
return new LexMapSchema()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
/** @deprecated Use {@link lexMap} instead */
|
|
63
|
+
export const unknownObject = lexMap
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { parseCid } from '@atproto/lex-data'
|
|
3
|
+
import { lexValue } from './lex-value.js'
|
|
4
|
+
|
|
5
|
+
const schema = lexValue()
|
|
6
|
+
|
|
7
|
+
describe(lexValue, () => {
|
|
8
|
+
describe('valid values', () => {
|
|
9
|
+
for (const { note, value } of [
|
|
10
|
+
{ note: 'string', value: 'hello' },
|
|
11
|
+
{ note: 'boolean true', value: true },
|
|
12
|
+
{ note: 'boolean false', value: false },
|
|
13
|
+
{ note: 'null', value: null },
|
|
14
|
+
{ note: 'integer', value: 42 },
|
|
15
|
+
{ note: 'negative integer', value: -1 },
|
|
16
|
+
{ note: 'zero', value: 0 },
|
|
17
|
+
{ note: 'Uint8Array', value: new Uint8Array([1, 2, 3]) },
|
|
18
|
+
{
|
|
19
|
+
note: 'Cid',
|
|
20
|
+
value: parseCid(
|
|
21
|
+
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
22
|
+
),
|
|
23
|
+
},
|
|
24
|
+
{ note: 'empty plain object', value: {} },
|
|
25
|
+
{
|
|
26
|
+
note: 'object with Lex values',
|
|
27
|
+
value: {
|
|
28
|
+
a: 123,
|
|
29
|
+
b: 'blah',
|
|
30
|
+
c: true,
|
|
31
|
+
d: null,
|
|
32
|
+
e: new Uint8Array([1, 2, 3]),
|
|
33
|
+
f: { nested: 'value' },
|
|
34
|
+
g: [1, 2, 3],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{ note: 'empty array', value: [] },
|
|
38
|
+
{
|
|
39
|
+
note: 'array with Lex values',
|
|
40
|
+
value: [
|
|
41
|
+
123,
|
|
42
|
+
'blah',
|
|
43
|
+
true,
|
|
44
|
+
null,
|
|
45
|
+
new Uint8Array([1, 2, 3]),
|
|
46
|
+
{ nested: 'value' },
|
|
47
|
+
[1, 2, 3],
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
]) {
|
|
51
|
+
test(note, () => {
|
|
52
|
+
const result = schema.safeParse(value)
|
|
53
|
+
expect(result.success).toBe(true)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('invalid values', () => {
|
|
59
|
+
for (const { note, value } of [
|
|
60
|
+
{ note: 'float', value: 42.5 },
|
|
61
|
+
{ note: 'undefined', value: undefined },
|
|
62
|
+
{ note: 'function', value: () => {} },
|
|
63
|
+
{ note: 'Date object', value: new Date() },
|
|
64
|
+
{ note: 'Map object', value: new Map() },
|
|
65
|
+
{ note: 'Set object', value: new Set() },
|
|
66
|
+
{ note: 'class instance', value: new (class A {})() },
|
|
67
|
+
{ note: 'object with function value', value: { a: 123, b: () => {} } },
|
|
68
|
+
{
|
|
69
|
+
note: 'object with undefined value',
|
|
70
|
+
value: { a: 123, b: undefined },
|
|
71
|
+
},
|
|
72
|
+
{ note: 'array with function', value: [123, 'blah', () => {}] },
|
|
73
|
+
{ note: 'array with undefined', value: [123, 'blah', undefined] },
|
|
74
|
+
]) {
|
|
75
|
+
test(note, () => {
|
|
76
|
+
const result = schema.safeParse(value)
|
|
77
|
+
expect(result.success).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { LexValue, isLexScalar, isPlainObject } from '@atproto/lex-data'
|
|
2
|
+
import { Schema, ValidationContext } from '../core.js'
|
|
3
|
+
import { memoizedOptions } from '../util/memoize.js'
|
|
4
|
+
|
|
5
|
+
export type { LexValue }
|
|
6
|
+
|
|
7
|
+
const EXPECTED_TYPES = Object.freeze([
|
|
8
|
+
// Scalar types
|
|
9
|
+
'null',
|
|
10
|
+
'boolean',
|
|
11
|
+
'integer',
|
|
12
|
+
'string',
|
|
13
|
+
'cid',
|
|
14
|
+
'bytes',
|
|
15
|
+
// Recursive types
|
|
16
|
+
'array',
|
|
17
|
+
'object',
|
|
18
|
+
] as const)
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* AT Protocol lexicon values are any valid AT Protocol data types: string,
|
|
22
|
+
* integer, boolean, null, bytes, cid, array, or object.
|
|
23
|
+
*/
|
|
24
|
+
export class LexValueSchema extends Schema<LexValue> {
|
|
25
|
+
readonly type = 'lexValue' as const
|
|
26
|
+
|
|
27
|
+
validateInContext(input: unknown, ctx: ValidationContext) {
|
|
28
|
+
// @NOTE We are *not* using "isLexValue" here to allow for more specific
|
|
29
|
+
// error messages about the path and type of the invalid value. The
|
|
30
|
+
// "isLexValue" check is effectively performed by the recursive validation
|
|
31
|
+
// of child properties below.
|
|
32
|
+
|
|
33
|
+
// @NOTE There are two limitations to the fact that we are not using
|
|
34
|
+
// "isLexValue" here:
|
|
35
|
+
// 1. We cannot detect circular references in objects or arrays, which would
|
|
36
|
+
// cause infinite recursion. However, circular references are not valid
|
|
37
|
+
// AT Protocol data types, so this is not a concern for valid input. This
|
|
38
|
+
// could easily be addressed in the "validateChild" method by keeping
|
|
39
|
+
// track of "parent" objects.
|
|
40
|
+
// 2. We are limited in the recursion depth we can validate due to potential
|
|
41
|
+
// recursion depth limits in JavaScript. However, this is also not a
|
|
42
|
+
// concern for most valid input, as extremely deep nesting is unlikely in
|
|
43
|
+
// typical use cases.
|
|
44
|
+
if (isPlainObject(input)) {
|
|
45
|
+
for (const key of Object.keys(input)) {
|
|
46
|
+
const r = ctx.validateChild(input, key, this) // recursively validate all properties
|
|
47
|
+
if (!r.success) return r
|
|
48
|
+
}
|
|
49
|
+
} else if (Array.isArray(input)) {
|
|
50
|
+
for (let i = 0; i < input.length; i++) {
|
|
51
|
+
const r = ctx.validateChild(input, i, this) // recursively validate all array items
|
|
52
|
+
if (!r.success) return r
|
|
53
|
+
}
|
|
54
|
+
} else if (!isLexScalar(input)) {
|
|
55
|
+
return ctx.issueInvalidType(input, EXPECTED_TYPES)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return ctx.success(input)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates a schema that accepts any valid AT Protocol data type: string,
|
|
64
|
+
* integer, boolean, null, bytes, cid, array, or plain object. Arrays and
|
|
65
|
+
* objects are recursively validated to ensure all nested values are also valid
|
|
66
|
+
* AT Protocol data types.
|
|
67
|
+
*
|
|
68
|
+
* @see {@link LexValue} from `@atproto/lex-data` for the type definition of valid AT Protocol data types
|
|
69
|
+
* @returns A new {@link LexValueSchema} instance
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const schema = l.lexValue()
|
|
74
|
+
*
|
|
75
|
+
* schema.validate('hello') // success
|
|
76
|
+
* schema.validate(42) // success
|
|
77
|
+
* schema.validate(null) // success
|
|
78
|
+
* schema.validate([1, 'two', null]) // success
|
|
79
|
+
* schema.validate({ any: 'props' }) // success
|
|
80
|
+
* schema.validate(new Date()) // fails - Date is not a valid LexValue
|
|
81
|
+
* schema.validate({ foo: 1.2 }) // fails - 1.2 is not a valid LexValue (not an integer)
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export const lexValue = /*#__PURE__*/ memoizedOptions(function () {
|
|
85
|
+
return new LexValueSchema()
|
|
86
|
+
})
|
package/src/schema/literal.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { Schema, ValidationContext } from '../core.js'
|
|
|
18
18
|
export class LiteralSchema<
|
|
19
19
|
const TValue extends null | string | number | boolean,
|
|
20
20
|
> extends Schema<TValue> {
|
|
21
|
+
readonly type = 'literal' as const
|
|
22
|
+
|
|
21
23
|
constructor(readonly value: TValue) {
|
|
22
24
|
super()
|
|
23
25
|
}
|
package/src/schema/never.ts
CHANGED
|
@@ -14,8 +14,10 @@ import { memoizedOptions } from '../util/memoize.js'
|
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
16
|
export class NeverSchema extends Schema<never> {
|
|
17
|
+
readonly type = 'never' as const
|
|
18
|
+
|
|
17
19
|
validateInContext(input: unknown, ctx: ValidationContext) {
|
|
18
|
-
return ctx.
|
|
20
|
+
return ctx.issueUnexpectedType(input, 'never')
|
|
19
21
|
}
|
|
20
22
|
}
|
|
21
23
|
|
package/src/schema/null.ts
CHANGED
|
@@ -15,9 +15,11 @@ import { memoizedOptions } from '../util/memoize.js'
|
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
17
|
export class NullSchema extends Schema<null> {
|
|
18
|
+
readonly type = 'null' as const
|
|
19
|
+
|
|
18
20
|
validateInContext(input: unknown, ctx: ValidationContext) {
|
|
19
21
|
if (input !== null) {
|
|
20
|
-
return ctx.
|
|
22
|
+
return ctx.issueUnexpectedType(input, 'null')
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
return ctx.success(null)
|
package/src/schema/nullable.ts
CHANGED
|
@@ -26,6 +26,8 @@ export class NullableSchema<const TValidator extends Validator> extends Schema<
|
|
|
26
26
|
InferInput<TValidator> | null,
|
|
27
27
|
InferOutput<TValidator> | null
|
|
28
28
|
> {
|
|
29
|
+
readonly type = 'nullable' as const
|
|
30
|
+
|
|
29
31
|
constructor(readonly validator: TValidator) {
|
|
30
32
|
super()
|
|
31
33
|
}
|
package/src/schema/object.ts
CHANGED
|
@@ -43,6 +43,8 @@ export class ObjectSchema<
|
|
|
43
43
|
[K in keyof TShape]: InferOutput<TShape[K]>
|
|
44
44
|
}>
|
|
45
45
|
> {
|
|
46
|
+
readonly type = 'object' as const
|
|
47
|
+
|
|
46
48
|
constructor(readonly shape: TShape) {
|
|
47
49
|
super()
|
|
48
50
|
}
|
|
@@ -55,7 +57,7 @@ export class ObjectSchema<
|
|
|
55
57
|
|
|
56
58
|
validateInContext(input: unknown, ctx: ValidationContext) {
|
|
57
59
|
if (!isPlainObject(input)) {
|
|
58
|
-
return ctx.
|
|
60
|
+
return ctx.issueUnexpectedType(input, 'object')
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
// Lazily copy value
|
package/src/schema/optional.ts
CHANGED
|
@@ -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,19 @@ 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)')
|
|
404
435
|
})
|
|
405
436
|
})
|
|
406
437
|
|
|
@@ -461,15 +492,23 @@ describe('ParamsSchema', () => {
|
|
|
461
492
|
expect(result.toString()).toBe('name=Alice')
|
|
462
493
|
})
|
|
463
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
|
+
|
|
464
505
|
it('handles arrays with multiple types', () => {
|
|
465
506
|
const result = schema.toURLSearchParams({
|
|
466
507
|
name: 'Alice',
|
|
467
508
|
// @ts-expect-error
|
|
468
|
-
values: [
|
|
509
|
+
values: ['foo', 'bar'],
|
|
469
510
|
})
|
|
470
|
-
expect(result.toString()).toBe(
|
|
471
|
-
'name=Alice&values=1&values=true&values=text',
|
|
472
|
-
)
|
|
511
|
+
expect(result.toString()).toBe('name=Alice&values=foo&values=bar')
|
|
473
512
|
})
|
|
474
513
|
|
|
475
514
|
it('handles undefined input', () => {
|
|
@@ -711,9 +750,9 @@ describe('paramSchema', () => {
|
|
|
711
750
|
expect(result.success).toBe(true)
|
|
712
751
|
})
|
|
713
752
|
|
|
714
|
-
it('
|
|
753
|
+
it('rejects arrays with mixed scalar types', () => {
|
|
715
754
|
const result = paramSchema.safeParse([true, 42, 'text'])
|
|
716
|
-
expect(result.success).toBe(
|
|
755
|
+
expect(result.success).toBe(false)
|
|
717
756
|
})
|
|
718
757
|
|
|
719
758
|
it('validates arrays with negative integers', () => {
|
|
@@ -884,11 +923,11 @@ describe('paramsSchema', () => {
|
|
|
884
923
|
expect(result.success).toBe(true)
|
|
885
924
|
})
|
|
886
925
|
|
|
887
|
-
it('
|
|
926
|
+
it('rejects object with arrays of mixed scalar types', () => {
|
|
888
927
|
const result = paramsSchema.safeParse({
|
|
889
928
|
values: [true, 42, 'text'],
|
|
890
929
|
})
|
|
891
|
-
expect(result.success).toBe(
|
|
930
|
+
expect(result.success).toBe(false)
|
|
892
931
|
})
|
|
893
932
|
|
|
894
933
|
it('validates object with numeric string keys', () => {
|