@atproto/lex-schema 0.0.2 → 0.0.4
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 +75 -0
- package/dist/core/$type.d.ts +6 -3
- package/dist/core/$type.d.ts.map +1 -1
- package/dist/core/$type.js +1 -0
- package/dist/core/$type.js.map +1 -1
- package/dist/core/record-key.d.ts +3 -3
- package/dist/core/record-key.d.ts.map +1 -1
- package/dist/core/record-key.js +12 -6
- package/dist/core/record-key.js.map +1 -1
- package/dist/core/result.d.ts.map +1 -1
- package/dist/core/result.js +6 -0
- package/dist/core/result.js.map +1 -1
- package/dist/core/string-format.d.ts +30 -27
- package/dist/core/string-format.d.ts.map +1 -1
- package/dist/core/string-format.js +56 -42
- package/dist/core/string-format.js.map +1 -1
- package/dist/core/types.d.ts +9 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/external.d.ts +31 -28
- package/dist/external.d.ts.map +1 -1
- package/dist/external.js +33 -17
- package/dist/external.js.map +1 -1
- package/dist/schema/_parameters.d.ts +2 -2
- package/dist/schema/_parameters.d.ts.map +1 -1
- package/dist/schema/array.d.ts +5 -6
- package/dist/schema/array.d.ts.map +1 -1
- package/dist/schema/array.js +5 -6
- package/dist/schema/array.js.map +1 -1
- package/dist/schema/blob.d.ts +2 -3
- package/dist/schema/blob.d.ts.map +1 -1
- package/dist/schema/blob.js +1 -2
- package/dist/schema/blob.js.map +1 -1
- package/dist/schema/boolean.d.ts +4 -5
- package/dist/schema/boolean.d.ts.map +1 -1
- package/dist/schema/boolean.js +2 -3
- package/dist/schema/boolean.js.map +1 -1
- package/dist/schema/bytes.d.ts +3 -4
- package/dist/schema/bytes.d.ts.map +1 -1
- package/dist/schema/bytes.js +2 -3
- package/dist/schema/bytes.js.map +1 -1
- package/dist/schema/cid.d.ts +13 -6
- package/dist/schema/cid.d.ts.map +1 -1
- package/dist/schema/cid.js +2 -4
- package/dist/schema/cid.js.map +1 -1
- package/dist/schema/custom.d.ts +3 -4
- package/dist/schema/custom.d.ts.map +1 -1
- package/dist/schema/custom.js +4 -3
- package/dist/schema/custom.js.map +1 -1
- package/dist/schema/dict.d.ts +3 -3
- package/dist/schema/dict.d.ts.map +1 -1
- package/dist/schema/dict.js +1 -1
- package/dist/schema/dict.js.map +1 -1
- package/dist/schema/discriminated-union.d.ts +15 -24
- package/dist/schema/discriminated-union.d.ts.map +1 -1
- package/dist/schema/discriminated-union.js +40 -64
- package/dist/schema/discriminated-union.js.map +1 -1
- package/dist/schema/enum.d.ts +8 -4
- package/dist/schema/enum.d.ts.map +1 -1
- package/dist/schema/enum.js +5 -3
- package/dist/schema/enum.js.map +1 -1
- package/dist/schema/integer.d.ts +3 -4
- package/dist/schema/integer.d.ts.map +1 -1
- package/dist/schema/integer.js +3 -4
- package/dist/schema/integer.js.map +1 -1
- package/dist/schema/intersection.d.ts +22 -14
- package/dist/schema/intersection.d.ts.map +1 -1
- package/dist/schema/intersection.js +12 -22
- package/dist/schema/intersection.js.map +1 -1
- package/dist/schema/literal.d.ts +7 -3
- package/dist/schema/literal.d.ts.map +1 -1
- package/dist/schema/literal.js +5 -3
- package/dist/schema/literal.js.map +1 -1
- package/dist/schema/never.d.ts +2 -2
- package/dist/schema/never.d.ts.map +1 -1
- package/dist/schema/never.js +1 -1
- package/dist/schema/never.js.map +1 -1
- package/dist/schema/null.d.ts +2 -3
- package/dist/schema/null.d.ts.map +1 -1
- package/dist/schema/null.js +1 -2
- package/dist/schema/null.js.map +1 -1
- package/dist/schema/nullable.d.ts +7 -0
- package/dist/schema/nullable.d.ts.map +1 -0
- package/dist/schema/nullable.js +19 -0
- package/dist/schema/nullable.js.map +1 -0
- package/dist/schema/object.d.ts +10 -44
- package/dist/schema/object.d.ts.map +1 -1
- package/dist/schema/object.js +10 -46
- package/dist/schema/object.js.map +1 -1
- package/dist/schema/optional.d.ts +7 -0
- package/dist/schema/optional.d.ts.map +1 -0
- package/dist/schema/optional.js +25 -0
- package/dist/schema/optional.js.map +1 -0
- package/dist/schema/params.d.ts +14 -19
- package/dist/schema/params.d.ts.map +1 -1
- package/dist/schema/params.js +10 -24
- package/dist/schema/params.js.map +1 -1
- package/dist/schema/payload.d.ts +4 -4
- package/dist/schema/payload.d.ts.map +1 -1
- package/dist/schema/payload.js.map +1 -1
- package/dist/schema/permission-set.d.ts +6 -6
- package/dist/schema/permission-set.d.ts.map +1 -1
- package/dist/schema/permission-set.js +1 -2
- package/dist/schema/permission-set.js.map +1 -1
- package/dist/schema/permission.d.ts +0 -1
- package/dist/schema/permission.d.ts.map +1 -1
- package/dist/schema/permission.js +0 -1
- package/dist/schema/permission.js.map +1 -1
- package/dist/schema/procedure.d.ts +8 -9
- package/dist/schema/procedure.d.ts.map +1 -1
- package/dist/schema/procedure.js +0 -1
- package/dist/schema/procedure.js.map +1 -1
- package/dist/schema/query.d.ts +7 -8
- package/dist/schema/query.d.ts.map +1 -1
- package/dist/schema/query.js +0 -1
- package/dist/schema/query.js.map +1 -1
- package/dist/schema/record.d.ts +34 -28
- package/dist/schema/record.d.ts.map +1 -1
- package/dist/schema/record.js +1 -2
- package/dist/schema/record.js.map +1 -1
- package/dist/schema/ref.d.ts +2 -3
- package/dist/schema/ref.d.ts.map +1 -1
- package/dist/schema/ref.js +1 -2
- package/dist/schema/ref.js.map +1 -1
- package/dist/schema/refine.d.ts +18 -0
- package/dist/schema/refine.d.ts.map +1 -0
- package/dist/schema/refine.js +33 -0
- package/dist/schema/refine.js.map +1 -0
- package/dist/schema/regexp.d.ts +7 -0
- package/dist/schema/regexp.d.ts.map +1 -0
- package/dist/schema/regexp.js +22 -0
- package/dist/schema/regexp.js.map +1 -0
- package/dist/schema/string.d.ts +4 -8
- package/dist/schema/string.d.ts.map +1 -1
- package/dist/schema/string.js +6 -3
- package/dist/schema/string.js.map +1 -1
- package/dist/schema/subscription.d.ts +7 -6
- package/dist/schema/subscription.d.ts.map +1 -1
- package/dist/schema/subscription.js.map +1 -1
- package/dist/schema/token.d.ts +2 -3
- package/dist/schema/token.d.ts.map +1 -1
- package/dist/schema/token.js +1 -2
- package/dist/schema/token.js.map +1 -1
- package/dist/schema/typed-object.d.ts +29 -27
- package/dist/schema/typed-object.d.ts.map +1 -1
- package/dist/schema/typed-object.js +1 -2
- package/dist/schema/typed-object.js.map +1 -1
- package/dist/schema/typed-ref.d.ts +2 -2
- package/dist/schema/typed-ref.d.ts.map +1 -1
- package/dist/schema/typed-ref.js +1 -1
- package/dist/schema/typed-ref.js.map +1 -1
- package/dist/schema/typed-union.d.ts +3 -4
- package/dist/schema/typed-union.d.ts.map +1 -1
- package/dist/schema/typed-union.js +3 -10
- package/dist/schema/typed-union.js.map +1 -1
- package/dist/schema/union.d.ts +2 -2
- package/dist/schema/union.d.ts.map +1 -1
- package/dist/schema/union.js +1 -1
- package/dist/schema/union.js.map +1 -1
- package/dist/schema/unknown-object.d.ts +2 -3
- package/dist/schema/unknown-object.d.ts.map +1 -1
- package/dist/schema/unknown-object.js +1 -2
- package/dist/schema/unknown-object.js.map +1 -1
- package/dist/schema/unknown.d.ts +2 -2
- package/dist/schema/unknown.d.ts.map +1 -1
- package/dist/schema/unknown.js +1 -1
- package/dist/schema/unknown.js.map +1 -1
- package/dist/schema.d.ts +4 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +6 -1
- package/dist/schema.js.map +1 -1
- package/dist/util/array-agg.d.ts.map +1 -1
- package/dist/util/array-agg.js +1 -0
- package/dist/util/array-agg.js.map +1 -1
- package/dist/util/lazy-property.d.ts +2 -0
- package/dist/util/lazy-property.d.ts.map +1 -0
- package/dist/util/lazy-property.js +14 -0
- package/dist/util/lazy-property.js.map +1 -0
- package/dist/validation/schema.d.ts +24 -0
- package/dist/validation/schema.d.ts.map +1 -0
- package/dist/validation/schema.js +57 -0
- package/dist/validation/schema.js.map +1 -0
- package/dist/validation/validation-error.d.ts +3 -3
- package/dist/validation/validation-error.d.ts.map +1 -1
- package/dist/validation/validation-error.js +32 -4
- package/dist/validation/validation-error.js.map +1 -1
- package/dist/validation/validation-issue.d.ts +32 -24
- package/dist/validation/validation-issue.d.ts.map +1 -1
- package/dist/validation/validation-issue.js +136 -92
- package/dist/validation/validation-issue.js.map +1 -1
- package/dist/validation/validator.d.ts +20 -50
- package/dist/validation/validator.d.ts.map +1 -1
- package/dist/validation/validator.js +40 -134
- package/dist/validation/validator.js.map +1 -1
- package/dist/validation.d.ts +1 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +1 -0
- package/dist/validation.js.map +1 -1
- package/package.json +8 -4
- package/src/core/$type.ts +7 -4
- package/src/core/record-key.ts +12 -5
- package/src/core/result.ts +6 -0
- package/src/core/string-format.ts +97 -61
- package/src/core/types.ts +12 -6
- package/src/external.ts +92 -70
- package/src/schema/_parameters.test.ts +416 -0
- package/src/schema/array.test.ts +237 -0
- package/src/schema/array.ts +17 -11
- package/src/schema/blob.test.ts +506 -0
- package/src/schema/blob.ts +3 -5
- package/src/schema/boolean.test.ts +116 -0
- package/src/schema/boolean.ts +5 -7
- package/src/schema/bytes.test.ts +226 -0
- package/src/schema/bytes.ts +4 -6
- package/src/schema/cid.test.ts +155 -0
- package/src/schema/cid.ts +14 -8
- package/src/schema/custom.test.ts +413 -0
- package/src/schema/custom.ts +10 -8
- package/src/schema/dict.test.ts +198 -0
- package/src/schema/dict.ts +6 -8
- package/src/schema/discriminated-union.test.ts +675 -0
- package/src/schema/discriminated-union.ts +68 -95
- package/src/schema/enum.test.ts +396 -0
- package/src/schema/enum.ts +12 -5
- package/src/schema/integer.test.ts +312 -0
- package/src/schema/integer.ts +5 -7
- package/src/schema/intersection.test.ts +32 -0
- package/src/schema/intersection.ts +37 -40
- package/src/schema/literal.test.ts +531 -0
- package/src/schema/literal.ts +12 -5
- package/src/schema/never.test.ts +174 -0
- package/src/schema/never.ts +3 -10
- package/src/schema/null.test.ts +79 -0
- package/src/schema/null.ts +3 -5
- package/src/schema/nullable.test.ts +480 -0
- package/src/schema/nullable.ts +23 -0
- package/src/schema/object.test.ts +47 -115
- package/src/schema/object.ts +19 -123
- package/src/schema/optional.test.ts +485 -0
- package/src/schema/optional.ts +31 -0
- package/src/schema/params.test.ts +582 -0
- package/src/schema/params.ts +37 -55
- package/src/schema/payload.test.ts +345 -0
- package/src/schema/payload.ts +5 -5
- package/src/schema/permission-set.test.ts +679 -0
- package/src/schema/permission-set.ts +6 -8
- package/src/schema/permission.test.ts +536 -0
- package/src/schema/permission.ts +0 -2
- package/src/schema/procedure.test.ts +443 -0
- package/src/schema/procedure.ts +11 -13
- package/src/schema/query.test.ts +408 -0
- package/src/schema/query.ts +9 -11
- package/src/schema/record.test.ts +694 -0
- package/src/schema/record.ts +38 -36
- package/src/schema/ref.test.ts +365 -0
- package/src/schema/ref.ts +8 -5
- package/src/schema/refine.test.ts +578 -0
- package/src/schema/refine.ts +85 -0
- package/src/schema/regexp.test.ts +580 -0
- package/src/schema/regexp.ts +22 -0
- package/src/schema/string.test.ts +612 -0
- package/src/schema/string.ts +11 -17
- package/src/schema/subscription.test.ts +689 -0
- package/src/schema/subscription.ts +13 -8
- package/src/schema/token.test.ts +428 -0
- package/src/schema/token.ts +3 -5
- package/src/schema/typed-object.test.ts +612 -0
- package/src/schema/typed-object.ts +23 -20
- package/src/schema/typed-ref.test.ts +823 -0
- package/src/schema/typed-ref.ts +10 -5
- package/src/schema/typed-union.test.ts +378 -0
- package/src/schema/typed-union.ts +6 -15
- package/src/schema/union.test.ts +200 -0
- package/src/schema/union.ts +5 -4
- package/src/schema/unknown-object.test.ts +592 -0
- package/src/schema/unknown-object.ts +3 -5
- package/src/schema/unknown.test.ts +312 -0
- package/src/schema/unknown.ts +3 -3
- package/src/schema.ts +7 -1
- package/src/util/array-agg.ts +1 -0
- package/src/util/lazy-property.ts +14 -0
- package/src/validation/schema.ts +92 -0
- package/src/validation/validation-error.ts +60 -9
- package/src/validation/validation-issue.ts +141 -144
- package/src/validation/validator.ts +67 -206
- package/src/validation.ts +1 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +7 -0
- package/tsconfig.tests.json +9 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { IssueCustom } from '../validation.js'
|
|
2
|
+
import { CustomSchema } from './custom.js'
|
|
3
|
+
|
|
4
|
+
describe('CustomSchema', () => {
|
|
5
|
+
describe('basic validation', () => {
|
|
6
|
+
it('validates input that passes custom assertion', () => {
|
|
7
|
+
const schema = new CustomSchema(
|
|
8
|
+
(input): input is string => typeof input === 'string',
|
|
9
|
+
'Must be a string',
|
|
10
|
+
)
|
|
11
|
+
const result = schema.safeParse('hello')
|
|
12
|
+
expect(result.success).toBe(true)
|
|
13
|
+
if (result.success) {
|
|
14
|
+
expect(result.value).toBe('hello')
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('rejects input that fails custom assertion', () => {
|
|
19
|
+
const schema = new CustomSchema(
|
|
20
|
+
(input): input is string => typeof input === 'string',
|
|
21
|
+
'Must be a string',
|
|
22
|
+
)
|
|
23
|
+
const result = schema.safeParse(123)
|
|
24
|
+
expect(result.success).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('includes custom message in error', () => {
|
|
28
|
+
const schema = new CustomSchema(
|
|
29
|
+
(input): input is string => typeof input === 'string',
|
|
30
|
+
'Custom error message',
|
|
31
|
+
)
|
|
32
|
+
const result = schema.safeParse(123)
|
|
33
|
+
expect(result.success).toBe(false)
|
|
34
|
+
if (!result.success) {
|
|
35
|
+
expect(result.error.message).toContain('Custom error message')
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('complex type guards', () => {
|
|
41
|
+
it('validates objects with specific properties', () => {
|
|
42
|
+
interface User {
|
|
43
|
+
name: string
|
|
44
|
+
age: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const schema = new CustomSchema((input): input is User => {
|
|
48
|
+
return (
|
|
49
|
+
typeof input === 'object' &&
|
|
50
|
+
input !== null &&
|
|
51
|
+
'name' in input &&
|
|
52
|
+
'age' in input &&
|
|
53
|
+
typeof (input as any).name === 'string' &&
|
|
54
|
+
typeof (input as any).age === 'number'
|
|
55
|
+
)
|
|
56
|
+
}, 'Must be a valid User object')
|
|
57
|
+
|
|
58
|
+
expect(schema.matches({ name: 'Alice', age: 30 })).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('rejects objects missing required properties', () => {
|
|
62
|
+
interface User {
|
|
63
|
+
name: string
|
|
64
|
+
age: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const schema = new CustomSchema((input): input is User => {
|
|
68
|
+
return (
|
|
69
|
+
typeof input === 'object' &&
|
|
70
|
+
input !== null &&
|
|
71
|
+
'name' in input &&
|
|
72
|
+
'age' in input &&
|
|
73
|
+
typeof (input as any).name === 'string' &&
|
|
74
|
+
typeof (input as any).age === 'number'
|
|
75
|
+
)
|
|
76
|
+
}, 'Must be a valid User object')
|
|
77
|
+
|
|
78
|
+
expect(schema.matches({ name: 'Alice' })).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('validates arrays with specific element types', () => {
|
|
82
|
+
const schema = new CustomSchema((input): input is number[] => {
|
|
83
|
+
return (
|
|
84
|
+
Array.isArray(input) &&
|
|
85
|
+
input.every((item) => typeof item === 'number')
|
|
86
|
+
)
|
|
87
|
+
}, 'Must be an array of numbers')
|
|
88
|
+
|
|
89
|
+
const result = schema.safeParse([1, 2, 3, 4])
|
|
90
|
+
expect(result.success).toBe(true)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('rejects arrays with mixed types', () => {
|
|
94
|
+
const schema = new CustomSchema((input): input is number[] => {
|
|
95
|
+
return (
|
|
96
|
+
Array.isArray(input) &&
|
|
97
|
+
input.every((item) => typeof item === 'number')
|
|
98
|
+
)
|
|
99
|
+
}, 'Must be an array of numbers')
|
|
100
|
+
|
|
101
|
+
const result = schema.safeParse([1, 'two', 3])
|
|
102
|
+
expect(result.success).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('custom context usage', () => {
|
|
107
|
+
it('can add custom issues through context', () => {
|
|
108
|
+
const schema = new CustomSchema((input, ctx): input is string => {
|
|
109
|
+
if (typeof input !== 'string') {
|
|
110
|
+
ctx.addIssue({
|
|
111
|
+
code: 'invalid_type',
|
|
112
|
+
path: ctx.path,
|
|
113
|
+
input,
|
|
114
|
+
expected: ['string'],
|
|
115
|
+
} as any)
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
return true
|
|
119
|
+
}, 'Must be a string')
|
|
120
|
+
|
|
121
|
+
const result = schema.safeParse(123)
|
|
122
|
+
expect(result.success).toBe(false)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('accesses path from context', () => {
|
|
126
|
+
let capturedPath: any[] = []
|
|
127
|
+
const schema = new CustomSchema((input, ctx): input is string => {
|
|
128
|
+
capturedPath = [...ctx.path]
|
|
129
|
+
return typeof input === 'string'
|
|
130
|
+
}, 'Must be a string')
|
|
131
|
+
|
|
132
|
+
schema.safeParse('test')
|
|
133
|
+
expect(capturedPath).toEqual([])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('validates with custom path', () => {
|
|
137
|
+
const schema = new CustomSchema(
|
|
138
|
+
(input): input is string => typeof input === 'string',
|
|
139
|
+
'Must be a string',
|
|
140
|
+
'customField',
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const result = schema.safeParse(123)
|
|
144
|
+
expect(result.success).toBe(false)
|
|
145
|
+
if (!result.success) {
|
|
146
|
+
expect(result.error.message).toContain('customField')
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('validates with array of paths', () => {
|
|
151
|
+
const schema = new CustomSchema(
|
|
152
|
+
(input): input is string => typeof input === 'string',
|
|
153
|
+
'Must be a string',
|
|
154
|
+
['nested', 'field'],
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
const result = schema.safeParse(123)
|
|
158
|
+
expect(result.success).toBe(false)
|
|
159
|
+
if (!result.success) {
|
|
160
|
+
expect(result.error.message).toContain('nested')
|
|
161
|
+
expect(result.error.message).toContain('field')
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('business logic validation', () => {
|
|
167
|
+
it('validates email format', () => {
|
|
168
|
+
const schema = new CustomSchema((input): input is string => {
|
|
169
|
+
return (
|
|
170
|
+
typeof input === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)
|
|
171
|
+
)
|
|
172
|
+
}, 'Must be a valid email address')
|
|
173
|
+
|
|
174
|
+
const validResult = schema.safeParse('user@example.com')
|
|
175
|
+
expect(validResult.success).toBe(true)
|
|
176
|
+
|
|
177
|
+
const invalidResult = schema.safeParse('not-an-email')
|
|
178
|
+
expect(invalidResult.success).toBe(false)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('validates password strength', () => {
|
|
182
|
+
const schema = new CustomSchema((input): input is string => {
|
|
183
|
+
if (typeof input !== 'string') return false
|
|
184
|
+
return (
|
|
185
|
+
input.length >= 8 &&
|
|
186
|
+
/[A-Z]/.test(input) &&
|
|
187
|
+
/[a-z]/.test(input) &&
|
|
188
|
+
/[0-9]/.test(input)
|
|
189
|
+
)
|
|
190
|
+
}, 'Password must be at least 8 characters with uppercase, lowercase, and numbers')
|
|
191
|
+
|
|
192
|
+
const validResult = schema.safeParse('MyPass123')
|
|
193
|
+
expect(validResult.success).toBe(true)
|
|
194
|
+
|
|
195
|
+
const weakResult = schema.safeParse('weak')
|
|
196
|
+
expect(weakResult.success).toBe(false)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('validates age range', () => {
|
|
200
|
+
const schema = new CustomSchema((input): input is number => {
|
|
201
|
+
return typeof input === 'number' && input >= 18 && input <= 120
|
|
202
|
+
}, 'Age must be between 18 and 120')
|
|
203
|
+
|
|
204
|
+
const validResult = schema.safeParse(25)
|
|
205
|
+
expect(validResult.success).toBe(true)
|
|
206
|
+
|
|
207
|
+
const tooYoungResult = schema.safeParse(15)
|
|
208
|
+
expect(tooYoungResult.success).toBe(false)
|
|
209
|
+
|
|
210
|
+
const tooOldResult = schema.safeParse(150)
|
|
211
|
+
expect(tooOldResult.success).toBe(false)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('validates positive numbers', () => {
|
|
215
|
+
const schema = new CustomSchema((input): input is number => {
|
|
216
|
+
return typeof input === 'number' && input > 0
|
|
217
|
+
}, 'Must be a positive number')
|
|
218
|
+
|
|
219
|
+
const validResult = schema.safeParse(5)
|
|
220
|
+
expect(validResult.success).toBe(true)
|
|
221
|
+
|
|
222
|
+
const zeroResult = schema.safeParse(0)
|
|
223
|
+
expect(zeroResult.success).toBe(false)
|
|
224
|
+
|
|
225
|
+
const negativeResult = schema.safeParse(-5)
|
|
226
|
+
expect(negativeResult.success).toBe(false)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe('edge cases', () => {
|
|
231
|
+
it('handles null input', () => {
|
|
232
|
+
const schema = new CustomSchema(
|
|
233
|
+
(input): input is null => input === null,
|
|
234
|
+
'Must be null',
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
const validResult = schema.safeParse(null)
|
|
238
|
+
expect(validResult.success).toBe(true)
|
|
239
|
+
|
|
240
|
+
const invalidResult = schema.safeParse(undefined)
|
|
241
|
+
expect(invalidResult.success).toBe(false)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('handles undefined input', () => {
|
|
245
|
+
const schema = new CustomSchema(
|
|
246
|
+
(input): input is undefined => input === undefined,
|
|
247
|
+
'Must be undefined',
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
const validResult = schema.safeParse(undefined)
|
|
251
|
+
expect(validResult.success).toBe(true)
|
|
252
|
+
|
|
253
|
+
const invalidResult = schema.safeParse(null)
|
|
254
|
+
expect(invalidResult.success).toBe(false)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('handles empty string', () => {
|
|
258
|
+
const schema = new CustomSchema(
|
|
259
|
+
(input): input is string =>
|
|
260
|
+
typeof input === 'string' && input.length > 0,
|
|
261
|
+
'Must be a non-empty string',
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
const validResult = schema.safeParse('hello')
|
|
265
|
+
expect(validResult.success).toBe(true)
|
|
266
|
+
|
|
267
|
+
const invalidResult = schema.safeParse('')
|
|
268
|
+
expect(invalidResult.success).toBe(false)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('handles empty array', () => {
|
|
272
|
+
const schema = new CustomSchema(
|
|
273
|
+
(input): input is any[] => Array.isArray(input) && input.length > 0,
|
|
274
|
+
'Must be a non-empty array',
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
const validResult = schema.safeParse([1, 2, 3])
|
|
278
|
+
expect(validResult.success).toBe(true)
|
|
279
|
+
|
|
280
|
+
const invalidResult = schema.safeParse([])
|
|
281
|
+
expect(invalidResult.success).toBe(false)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('handles complex nested structures', () => {
|
|
285
|
+
interface ComplexType {
|
|
286
|
+
users: Array<{ name: string; email: string }>
|
|
287
|
+
metadata: { count: number }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const schema = new CustomSchema((input): input is ComplexType => {
|
|
291
|
+
if (typeof input !== 'object' || input === null) return false
|
|
292
|
+
const obj = input as any
|
|
293
|
+
return (
|
|
294
|
+
Array.isArray(obj.users) &&
|
|
295
|
+
obj.users.every(
|
|
296
|
+
(u: any) =>
|
|
297
|
+
typeof u === 'object' &&
|
|
298
|
+
typeof u.name === 'string' &&
|
|
299
|
+
typeof u.email === 'string',
|
|
300
|
+
) &&
|
|
301
|
+
typeof obj.metadata === 'object' &&
|
|
302
|
+
typeof obj.metadata.count === 'number'
|
|
303
|
+
)
|
|
304
|
+
}, 'Must be a valid complex structure')
|
|
305
|
+
|
|
306
|
+
const validResult = schema.safeParse({
|
|
307
|
+
users: [
|
|
308
|
+
{ name: 'Alice', email: 'alice@example.com' },
|
|
309
|
+
{ name: 'Bob', email: 'bob@example.com' },
|
|
310
|
+
],
|
|
311
|
+
metadata: { count: 2 },
|
|
312
|
+
})
|
|
313
|
+
expect(validResult.success).toBe(true)
|
|
314
|
+
|
|
315
|
+
const invalidResult = schema.safeParse({
|
|
316
|
+
users: [{ name: 'Alice' }], // missing email
|
|
317
|
+
metadata: { count: 1 },
|
|
318
|
+
})
|
|
319
|
+
expect(invalidResult.success).toBe(false)
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
describe('type narrowing', () => {
|
|
324
|
+
it('correctly narrows union types', () => {
|
|
325
|
+
type StringOrNumber = string | number
|
|
326
|
+
|
|
327
|
+
const schema = new CustomSchema(
|
|
328
|
+
(input): input is string => typeof input === 'string',
|
|
329
|
+
'Must be a string',
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
const input: StringOrNumber = 'hello'
|
|
333
|
+
|
|
334
|
+
const result = schema.safeParse(input)
|
|
335
|
+
expect(result.success).toBe(true)
|
|
336
|
+
|
|
337
|
+
if (result.success) {
|
|
338
|
+
// Type should be narrowed to string
|
|
339
|
+
const value: string = result.value
|
|
340
|
+
expect(typeof value).toBe('string')
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('validates discriminated unions', () => {
|
|
345
|
+
type Shape =
|
|
346
|
+
| { type: 'circle'; radius: number }
|
|
347
|
+
| { type: 'rectangle'; width: number; height: number }
|
|
348
|
+
|
|
349
|
+
const circleSchema = new CustomSchema((input): input is Shape => {
|
|
350
|
+
return (
|
|
351
|
+
typeof input === 'object' &&
|
|
352
|
+
input !== null &&
|
|
353
|
+
'type' in input &&
|
|
354
|
+
(input as any).type === 'circle' &&
|
|
355
|
+
'radius' in input &&
|
|
356
|
+
typeof (input as any).radius === 'number'
|
|
357
|
+
)
|
|
358
|
+
}, 'Must be a valid circle')
|
|
359
|
+
|
|
360
|
+
const validResult = circleSchema.safeParse({ type: 'circle', radius: 5 })
|
|
361
|
+
expect(validResult.success).toBe(true)
|
|
362
|
+
|
|
363
|
+
const invalidResult = circleSchema.safeParse({
|
|
364
|
+
type: 'rectangle',
|
|
365
|
+
width: 10,
|
|
366
|
+
height: 20,
|
|
367
|
+
})
|
|
368
|
+
expect(invalidResult.success).toBe(false)
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
describe('assertion context behavior', () => {
|
|
373
|
+
it('calls assertion with null as this', () => {
|
|
374
|
+
const assertion = jest.fn(function (
|
|
375
|
+
this: unknown,
|
|
376
|
+
input: unknown,
|
|
377
|
+
): input is string {
|
|
378
|
+
expect(this).toBeNull()
|
|
379
|
+
return typeof input === 'string'
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
new CustomSchema(assertion as any, 'Must be a string').safeParse('test')
|
|
383
|
+
|
|
384
|
+
expect(assertion).toHaveBeenCalledTimes(1)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('provides addIssue method in context', () => {
|
|
388
|
+
const schema = new CustomSchema((input, ctx): input is string => {
|
|
389
|
+
ctx.addIssue(new IssueCustom(ctx.path, input, 'This is a custom issue'))
|
|
390
|
+
return false
|
|
391
|
+
}, 'Must be a string')
|
|
392
|
+
|
|
393
|
+
expect(schema.safeParse('test')).toMatchObject({
|
|
394
|
+
success: false,
|
|
395
|
+
error: {
|
|
396
|
+
issues: [
|
|
397
|
+
{ message: 'This is a custom issue' },
|
|
398
|
+
{ message: 'Must be a string' },
|
|
399
|
+
],
|
|
400
|
+
},
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('provides path array in context', () => {
|
|
405
|
+
const schema = new CustomSchema((input, ctx): input is string => {
|
|
406
|
+
expect(Array.isArray(ctx.path)).toBe(true)
|
|
407
|
+
return typeof input === 'string'
|
|
408
|
+
}, 'Must be a string')
|
|
409
|
+
|
|
410
|
+
schema.safeParse('test')
|
|
411
|
+
})
|
|
412
|
+
})
|
|
413
|
+
})
|
package/src/schema/custom.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { PropertyKey } from '../validation/property-key.js'
|
|
2
1
|
import {
|
|
3
|
-
|
|
2
|
+
Issue,
|
|
3
|
+
IssueCustom,
|
|
4
|
+
PropertyKey,
|
|
5
|
+
Schema,
|
|
4
6
|
ValidationResult,
|
|
5
|
-
Validator,
|
|
6
7
|
ValidatorContext,
|
|
7
|
-
} from '../validation
|
|
8
|
+
} from '../validation.js'
|
|
8
9
|
|
|
9
10
|
export type CustomAssertionContext = {
|
|
10
11
|
path: PropertyKey[]
|
|
11
|
-
addIssue(issue:
|
|
12
|
+
addIssue(issue: Issue): void
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export type CustomAssertion<T = any> = (
|
|
@@ -17,7 +18,7 @@ export type CustomAssertion<T = any> = (
|
|
|
17
18
|
ctx: CustomAssertionContext,
|
|
18
19
|
) => input is T
|
|
19
20
|
|
|
20
|
-
export class CustomSchema<T = unknown> extends
|
|
21
|
+
export class CustomSchema<T = unknown> extends Schema<T> {
|
|
21
22
|
constructor(
|
|
22
23
|
private readonly assertion: CustomAssertion<T>,
|
|
23
24
|
private readonly message: string,
|
|
@@ -26,11 +27,12 @@ export class CustomSchema<T = unknown> extends Validator<T> {
|
|
|
26
27
|
super()
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
validateInContext(
|
|
30
31
|
input: unknown,
|
|
31
32
|
ctx: ValidatorContext,
|
|
32
33
|
): ValidationResult<T> {
|
|
33
34
|
if (this.assertion.call(null, input, ctx)) return ctx.success(input as T)
|
|
34
|
-
|
|
35
|
+
const path = ctx.concatPath(this.path)
|
|
36
|
+
return ctx.failure(new IssueCustom(path, input, this.message))
|
|
35
37
|
}
|
|
36
38
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { BooleanSchema } from './boolean.js'
|
|
2
|
+
import { DictSchema } from './dict.js'
|
|
3
|
+
import { EnumSchema } from './enum.js'
|
|
4
|
+
import { IntegerSchema } from './integer.js'
|
|
5
|
+
import { StringSchema } from './string.js'
|
|
6
|
+
|
|
7
|
+
describe('DictSchema', () => {
|
|
8
|
+
const schema = new DictSchema(new StringSchema({}), new IntegerSchema({}))
|
|
9
|
+
|
|
10
|
+
it('validates plain objects with valid keys and values', () => {
|
|
11
|
+
const result = schema.safeParse({
|
|
12
|
+
count: 42,
|
|
13
|
+
total: 100,
|
|
14
|
+
score: 85,
|
|
15
|
+
})
|
|
16
|
+
expect(result.success).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('validates empty objects', () => {
|
|
20
|
+
const result = schema.safeParse({})
|
|
21
|
+
expect(result.success).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('rejects non-objects', () => {
|
|
25
|
+
const result = schema.safeParse('not an object')
|
|
26
|
+
expect(result.success).toBe(false)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('rejects arrays', () => {
|
|
30
|
+
const result = schema.safeParse([1, 2, 3])
|
|
31
|
+
expect(result.success).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('rejects null', () => {
|
|
35
|
+
const result = schema.safeParse(null)
|
|
36
|
+
expect(result.success).toBe(false)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('rejects undefined', () => {
|
|
40
|
+
const result = schema.safeParse(undefined)
|
|
41
|
+
expect(result.success).toBe(false)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('rejects invalid value types', () => {
|
|
45
|
+
const result = schema.safeParse({
|
|
46
|
+
count: 'not a number',
|
|
47
|
+
})
|
|
48
|
+
expect(result.success).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('rejects when one value is invalid', () => {
|
|
52
|
+
const result = schema.safeParse({
|
|
53
|
+
count: 42,
|
|
54
|
+
invalid: 'string',
|
|
55
|
+
total: 100,
|
|
56
|
+
})
|
|
57
|
+
expect(result.success).toBe(false)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('validates with enum key schema', () => {
|
|
61
|
+
const enumKeySchema = new DictSchema(
|
|
62
|
+
new EnumSchema(['tag1', 'tag2', 'tag3']),
|
|
63
|
+
new BooleanSchema({}),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const result = enumKeySchema.safeParse({
|
|
67
|
+
tag1: true,
|
|
68
|
+
tag2: false,
|
|
69
|
+
tag3: true,
|
|
70
|
+
})
|
|
71
|
+
expect(result.success).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('rejects invalid keys with enum key schema', () => {
|
|
75
|
+
const enumKeySchema = new DictSchema(
|
|
76
|
+
new EnumSchema(['tag1', 'tag2']),
|
|
77
|
+
new BooleanSchema({}),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const result = enumKeySchema.safeParse({
|
|
81
|
+
tag1: true,
|
|
82
|
+
invalidTag: false,
|
|
83
|
+
})
|
|
84
|
+
expect(result.success).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('validates nested dict schemas', () => {
|
|
88
|
+
const nestedSchema = new DictSchema(
|
|
89
|
+
new StringSchema({}),
|
|
90
|
+
new DictSchema(new StringSchema({}), new IntegerSchema({})),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const result = nestedSchema.safeParse({
|
|
94
|
+
group1: { count: 10, total: 20 },
|
|
95
|
+
group2: { score: 85 },
|
|
96
|
+
})
|
|
97
|
+
expect(result.success).toBe(true)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('validates with string key schema constraints', () => {
|
|
101
|
+
const constrainedKeySchema = new DictSchema(
|
|
102
|
+
new StringSchema({ minLength: 3, maxLength: 10 }),
|
|
103
|
+
new IntegerSchema({}),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const result = constrainedKeySchema.safeParse({
|
|
107
|
+
abc: 1,
|
|
108
|
+
defghij: 2,
|
|
109
|
+
})
|
|
110
|
+
expect(result.success).toBe(true)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('rejects keys that do not meet key schema constraints', () => {
|
|
114
|
+
const constrainedKeySchema = new DictSchema(
|
|
115
|
+
new StringSchema({ minLength: 3 }),
|
|
116
|
+
new IntegerSchema({}),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const result = constrainedKeySchema.safeParse({
|
|
120
|
+
ab: 1, // too short
|
|
121
|
+
})
|
|
122
|
+
expect(result.success).toBe(false)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('validates with value schema constraints', () => {
|
|
126
|
+
const constrainedValueSchema = new DictSchema(
|
|
127
|
+
new StringSchema({}),
|
|
128
|
+
new IntegerSchema({ minimum: 0, maximum: 100 }),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
const result = constrainedValueSchema.safeParse({
|
|
132
|
+
score: 50,
|
|
133
|
+
total: 100,
|
|
134
|
+
})
|
|
135
|
+
expect(result.success).toBe(true)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('rejects values that do not meet value schema constraints', () => {
|
|
139
|
+
const constrainedValueSchema = new DictSchema(
|
|
140
|
+
new StringSchema({}),
|
|
141
|
+
new IntegerSchema({ minimum: 0, maximum: 100 }),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
const result = constrainedValueSchema.safeParse({
|
|
145
|
+
score: 150, // too high
|
|
146
|
+
})
|
|
147
|
+
expect(result.success).toBe(false)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('validates dict with string values', () => {
|
|
151
|
+
const stringValueSchema = new DictSchema(
|
|
152
|
+
new StringSchema({}),
|
|
153
|
+
new StringSchema({}),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const result = stringValueSchema.safeParse({
|
|
157
|
+
name: 'Alice',
|
|
158
|
+
city: 'New York',
|
|
159
|
+
country: 'USA',
|
|
160
|
+
})
|
|
161
|
+
expect(result.success).toBe(true)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('validates dict with boolean values', () => {
|
|
165
|
+
const booleanValueSchema = new DictSchema(
|
|
166
|
+
new StringSchema({}),
|
|
167
|
+
new BooleanSchema({}),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
const result = booleanValueSchema.safeParse({
|
|
171
|
+
enabled: true,
|
|
172
|
+
visible: false,
|
|
173
|
+
active: true,
|
|
174
|
+
})
|
|
175
|
+
expect(result.success).toBe(true)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('handles objects with numeric string keys', () => {
|
|
179
|
+
const result = schema.safeParse({
|
|
180
|
+
'0': 1,
|
|
181
|
+
'1': 2,
|
|
182
|
+
'2': 3,
|
|
183
|
+
})
|
|
184
|
+
expect(result.success).toBe(true)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('preserves the original object when no transformations occur', () => {
|
|
188
|
+
const input = { count: 42, total: 100 }
|
|
189
|
+
const result = schema.safeParse(input)
|
|
190
|
+
|
|
191
|
+
if (result.success) {
|
|
192
|
+
// The implementation returns the same object if no changes are needed
|
|
193
|
+
expect(result.value).toBe(input)
|
|
194
|
+
} else {
|
|
195
|
+
throw new Error('Expected validation to succeed')
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
})
|
package/src/schema/dict.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { isPlainObject } from '@atproto/lex-data'
|
|
2
2
|
import {
|
|
3
3
|
Infer,
|
|
4
|
+
Schema,
|
|
4
5
|
ValidationResult,
|
|
5
6
|
Validator,
|
|
6
7
|
ValidatorContext,
|
|
7
8
|
} from '../validation.js'
|
|
8
9
|
|
|
9
10
|
export type DictSchemaOutput<
|
|
10
|
-
KeySchema extends Validator
|
|
11
|
+
KeySchema extends Validator<string>,
|
|
11
12
|
ValueSchema extends Validator,
|
|
12
|
-
> =
|
|
13
|
-
Infer<KeySchema> extends never
|
|
14
|
-
? Record<string, never>
|
|
15
|
-
: Record<Infer<KeySchema> & string, Infer<ValueSchema>>
|
|
13
|
+
> = Record<Infer<KeySchema>, Infer<ValueSchema>>
|
|
16
14
|
|
|
17
15
|
/**
|
|
18
16
|
* @note There is no dictionary in Lexicon schemas. This is a custom extension
|
|
@@ -20,9 +18,9 @@ export type DictSchemaOutput<
|
|
|
20
18
|
* not code generated from a lexicon schema).
|
|
21
19
|
*/
|
|
22
20
|
export class DictSchema<
|
|
23
|
-
const KeySchema extends Validator = any,
|
|
21
|
+
const KeySchema extends Validator<string> = any,
|
|
24
22
|
const ValueSchema extends Validator = any,
|
|
25
|
-
> extends
|
|
23
|
+
> extends Schema<DictSchemaOutput<KeySchema, ValueSchema>> {
|
|
26
24
|
constructor(
|
|
27
25
|
readonly keySchema: KeySchema,
|
|
28
26
|
readonly valueSchema: ValueSchema,
|
|
@@ -30,7 +28,7 @@ export class DictSchema<
|
|
|
30
28
|
super()
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
validateInContext(
|
|
34
32
|
input: unknown,
|
|
35
33
|
ctx: ValidatorContext,
|
|
36
34
|
options?: { ignoredKeys?: { has(k: string): boolean } },
|