@atproto/lex-schema 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/$type.d.ts +4 -0
- package/dist/core/$type.d.ts.map +1 -0
- package/dist/core/$type.js +7 -0
- package/dist/core/$type.js.map +1 -0
- package/dist/core/record-key.d.ts +4 -0
- package/dist/core/record-key.d.ts.map +1 -0
- package/dist/core/record-key.js +16 -0
- package/dist/core/record-key.js.map +1 -0
- package/dist/core/result.d.ts +57 -0
- package/dist/core/result.d.ts.map +1 -0
- package/dist/core/result.js +74 -0
- package/dist/core/result.js.map +1 -0
- package/dist/core/string-format.d.ts +31 -0
- package/dist/core/string-format.d.ts.map +1 -0
- package/dist/core/string-format.js +81 -0
- package/dist/core/string-format.js.map +1 -0
- package/dist/core/types.d.ts +19 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core.d.ts +6 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +9 -0
- package/dist/core.js.map +1 -0
- package/dist/external.d.ts +86 -0
- package/dist/external.d.ts.map +1 -0
- package/dist/external.js +171 -0
- package/dist/external.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/schema/_parameters.d.ts +17 -0
- package/dist/schema/_parameters.d.ts.map +1 -0
- package/dist/schema/_parameters.js +20 -0
- package/dist/schema/_parameters.js.map +1 -0
- package/dist/schema/array.d.ts +13 -0
- package/dist/schema/array.d.ts.map +1 -0
- package/dist/schema/array.js +40 -0
- package/dist/schema/array.js.map +1 -0
- package/dist/schema/blob.d.ts +32 -0
- package/dist/schema/blob.d.ts.map +1 -0
- package/dist/schema/blob.js +40 -0
- package/dist/schema/blob.js.map +1 -0
- package/dist/schema/boolean.d.ts +11 -0
- package/dist/schema/boolean.d.ts.map +1 -0
- package/dist/schema/boolean.js +20 -0
- package/dist/schema/boolean.js.map +1 -0
- package/dist/schema/bytes.d.ts +12 -0
- package/dist/schema/bytes.d.ts.map +1 -0
- package/dist/schema/bytes.js +31 -0
- package/dist/schema/bytes.js.map +1 -0
- package/dist/schema/cid.d.ts +13 -0
- package/dist/schema/cid.d.ts.map +1 -0
- package/dist/schema/cid.js +22 -0
- package/dist/schema/cid.js.map +1 -0
- package/dist/schema/custom.d.ts +15 -0
- package/dist/schema/custom.d.ts.map +1 -0
- package/dist/schema/custom.js +22 -0
- package/dist/schema/custom.js.map +1 -0
- package/dist/schema/dict.d.ts +18 -0
- package/dist/schema/dict.d.ts.map +1 -0
- package/dist/schema/dict.js +48 -0
- package/dist/schema/dict.js.map +1 -0
- package/dist/schema/discriminated-union.d.ts +34 -0
- package/dist/schema/discriminated-union.d.ts.map +1 -0
- package/dist/schema/discriminated-union.js +93 -0
- package/dist/schema/discriminated-union.js.map +1 -0
- package/dist/schema/enum.d.ts +7 -0
- package/dist/schema/enum.d.ts.map +1 -0
- package/dist/schema/enum.js +19 -0
- package/dist/schema/enum.js.map +1 -0
- package/dist/schema/integer.d.ts +13 -0
- package/dist/schema/integer.d.ts.map +1 -0
- package/dist/schema/integer.js +32 -0
- package/dist/schema/integer.js.map +1 -0
- package/dist/schema/intersection.d.ts +16 -0
- package/dist/schema/intersection.d.ts.map +1 -0
- package/dist/schema/intersection.js +33 -0
- package/dist/schema/intersection.js.map +1 -0
- package/dist/schema/literal.d.ts +7 -0
- package/dist/schema/literal.d.ts.map +1 -0
- package/dist/schema/literal.js +19 -0
- package/dist/schema/literal.js.map +1 -0
- package/dist/schema/never.d.ts +5 -0
- package/dist/schema/never.d.ts.map +1 -0
- package/dist/schema/never.js +11 -0
- package/dist/schema/never.js.map +1 -0
- package/dist/schema/null.d.ts +7 -0
- package/dist/schema/null.d.ts.map +1 -0
- package/dist/schema/null.js +18 -0
- package/dist/schema/null.js.map +1 -0
- package/dist/schema/object.d.ts +47 -0
- package/dist/schema/object.d.ts.map +1 -0
- package/dist/schema/object.js +89 -0
- package/dist/schema/object.js.map +1 -0
- package/dist/schema/params.d.ts +22 -0
- package/dist/schema/params.d.ts.map +1 -0
- package/dist/schema/params.js +115 -0
- package/dist/schema/params.js.map +1 -0
- package/dist/schema/payload.d.ts +19 -0
- package/dist/schema/payload.d.ts.map +1 -0
- package/dist/schema/payload.js +16 -0
- package/dist/schema/payload.js.map +1 -0
- package/dist/schema/permission-set.d.ts +15 -0
- package/dist/schema/permission-set.d.ts.map +1 -0
- package/dist/schema/permission-set.js +16 -0
- package/dist/schema/permission-set.js.map +1 -0
- package/dist/schema/permission.d.ts +9 -0
- package/dist/schema/permission.d.ts.map +1 -0
- package/dist/schema/permission.js +14 -0
- package/dist/schema/permission.js.map +1 -0
- package/dist/schema/procedure.d.ts +17 -0
- package/dist/schema/procedure.d.ts.map +1 -0
- package/dist/schema/procedure.js +20 -0
- package/dist/schema/procedure.js.map +1 -0
- package/dist/schema/query.d.ts +15 -0
- package/dist/schema/query.d.ts.map +1 -0
- package/dist/schema/query.js +18 -0
- package/dist/schema/query.js.map +1 -0
- package/dist/schema/record.d.ts +37 -0
- package/dist/schema/record.d.ts.map +1 -0
- package/dist/schema/record.js +64 -0
- package/dist/schema/record.js.map +1 -0
- package/dist/schema/ref.d.ts +10 -0
- package/dist/schema/ref.d.ts.map +1 -0
- package/dist/schema/ref.js +36 -0
- package/dist/schema/ref.js.map +1 -0
- package/dist/schema/string.d.ts +24 -0
- package/dist/schema/string.d.ts.map +1 -0
- package/dist/schema/string.js +107 -0
- package/dist/schema/string.js.map +1 -0
- package/dist/schema/subscription.d.ts +16 -0
- package/dist/schema/subscription.d.ts.map +1 -0
- package/dist/schema/subscription.js +18 -0
- package/dist/schema/subscription.js.map +1 -0
- package/dist/schema/token.d.ts +10 -0
- package/dist/schema/token.d.ts.map +1 -0
- package/dist/schema/token.js +36 -0
- package/dist/schema/token.js.map +1 -0
- package/dist/schema/typed-object.d.ts +32 -0
- package/dist/schema/typed-object.d.ts.map +1 -0
- package/dist/schema/typed-object.js +40 -0
- package/dist/schema/typed-object.js.map +1 -0
- package/dist/schema/typed-ref.d.ts +30 -0
- package/dist/schema/typed-ref.d.ts.map +1 -0
- package/dist/schema/typed-ref.js +44 -0
- package/dist/schema/typed-ref.js.map +1 -0
- package/dist/schema/typed-union.d.ts +26 -0
- package/dist/schema/typed-union.d.ts.map +1 -0
- package/dist/schema/typed-union.js +54 -0
- package/dist/schema/typed-union.js.map +1 -0
- package/dist/schema/union.d.ts +9 -0
- package/dist/schema/union.d.ts.map +1 -0
- package/dist/schema/union.js +29 -0
- package/dist/schema/union.js.map +1 -0
- package/dist/schema/unknown-object.d.ts +9 -0
- package/dist/schema/unknown-object.d.ts.map +1 -0
- package/dist/schema/unknown-object.js +16 -0
- package/dist/schema/unknown-object.js.map +1 -0
- package/dist/schema/unknown.d.ts +5 -0
- package/dist/schema/unknown.d.ts.map +1 -0
- package/dist/schema/unknown.js +11 -0
- package/dist/schema/unknown.js.map +1 -0
- package/dist/schema.d.ts +34 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +41 -0
- package/dist/schema.js.map +1 -0
- package/dist/util/array-agg.d.ts +20 -0
- package/dist/util/array-agg.d.ts.map +1 -0
- package/dist/util/array-agg.js +42 -0
- package/dist/util/array-agg.js.map +1 -0
- package/dist/validation/property-key.d.ts +2 -0
- package/dist/validation/property-key.d.ts.map +1 -0
- package/dist/validation/property-key.js +3 -0
- package/dist/validation/property-key.js.map +1 -0
- package/dist/validation/validation-error.d.ts +9 -0
- package/dist/validation/validation-error.d.ts.map +1 -0
- package/dist/validation/validation-error.js +27 -0
- package/dist/validation/validation-error.js.map +1 -0
- package/dist/validation/validation-issue.d.ts +45 -0
- package/dist/validation/validation-issue.d.ts.map +1 -0
- package/dist/validation/validation-issue.js +167 -0
- package/dist/validation/validation-issue.js.map +1 -0
- package/dist/validation/validator.d.ts +113 -0
- package/dist/validation/validator.d.ts.map +1 -0
- package/dist/validation/validator.js +209 -0
- package/dist/validation/validator.js.map +1 -0
- package/dist/validation.d.ts +5 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +8 -0
- package/dist/validation.js.map +1 -0
- package/package.json +45 -0
- package/src/core/$type.ts +19 -0
- package/src/core/record-key.ts +15 -0
- package/src/core/result.ts +73 -0
- package/src/core/string-format.ts +124 -0
- package/src/core/types.ts +22 -0
- package/src/core.ts +5 -0
- package/src/external.ts +365 -0
- package/src/index.ts +3 -0
- package/src/schema/_parameters.ts +26 -0
- package/src/schema/array.ts +51 -0
- package/src/schema/blob.ts +82 -0
- package/src/schema/boolean.ts +24 -0
- package/src/schema/bytes.ts +38 -0
- package/src/schema/cid.ts +27 -0
- package/src/schema/custom.ts +36 -0
- package/src/schema/dict.ts +69 -0
- package/src/schema/discriminated-union.ts +144 -0
- package/src/schema/enum.ts +20 -0
- package/src/schema/integer.ts +41 -0
- package/src/schema/intersection.ts +57 -0
- package/src/schema/literal.ts +20 -0
- package/src/schema/never.ts +14 -0
- package/src/schema/null.ts +20 -0
- package/src/schema/object.test.ts +138 -0
- package/src/schema/object.ts +180 -0
- package/src/schema/params.ts +157 -0
- package/src/schema/payload.ts +53 -0
- package/src/schema/permission-set.ts +22 -0
- package/src/schema/permission.ts +15 -0
- package/src/schema/procedure.ts +35 -0
- package/src/schema/query.ts +28 -0
- package/src/schema/record.ts +106 -0
- package/src/schema/ref.ts +47 -0
- package/src/schema/string.ts +139 -0
- package/src/schema/subscription.ts +35 -0
- package/src/schema/token.ts +41 -0
- package/src/schema/typed-object.ts +64 -0
- package/src/schema/typed-ref.ts +68 -0
- package/src/schema/typed-union.ts +106 -0
- package/src/schema/union.ts +40 -0
- package/src/schema/unknown-object.ts +20 -0
- package/src/schema/unknown.ts +10 -0
- package/src/schema.ts +40 -0
- package/src/util/array-agg.test.ts +41 -0
- package/src/util/array-agg.ts +43 -0
- package/src/validation/property-key.ts +1 -0
- package/src/validation/validation-error.ts +32 -0
- package/src/validation/validation-issue.ts +231 -0
- package/src/validation/validator.ts +361 -0
- package/src/validation.ts +4 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { CID, isPlainObject } from '@atproto/lex-data'
|
|
2
|
+
import { arrayAgg } from '../util/array-agg.js'
|
|
3
|
+
import { PropertyKey } from './property-key.js'
|
|
4
|
+
|
|
5
|
+
export interface Issue<I = unknown> {
|
|
6
|
+
readonly input: I
|
|
7
|
+
readonly code: string
|
|
8
|
+
readonly message?: string
|
|
9
|
+
readonly path: readonly PropertyKey[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IssueCustom extends Issue {
|
|
13
|
+
readonly code: 'custom'
|
|
14
|
+
readonly message: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface IssueInvalidFormat extends Issue {
|
|
18
|
+
readonly code: 'invalid_format'
|
|
19
|
+
readonly format: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IssueInvalidType extends Issue {
|
|
23
|
+
readonly code: 'invalid_type'
|
|
24
|
+
readonly expected: readonly string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IssueInvalidValue extends Issue {
|
|
28
|
+
readonly code: 'invalid_value'
|
|
29
|
+
readonly values: readonly unknown[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface IssueRequiredKey extends Issue {
|
|
33
|
+
readonly code: 'required_key'
|
|
34
|
+
readonly key: PropertyKey
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface IssueTooBig extends Issue {
|
|
38
|
+
readonly code: 'too_big'
|
|
39
|
+
readonly maximum: number
|
|
40
|
+
readonly type: 'array' | 'string' | 'integer' | 'grapheme' | 'bytes' | 'blob'
|
|
41
|
+
readonly actual: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface IssueTooSmall extends Issue {
|
|
45
|
+
readonly code: 'too_small'
|
|
46
|
+
readonly minimum: number
|
|
47
|
+
readonly type: 'array' | 'string' | 'integer' | 'grapheme' | 'bytes'
|
|
48
|
+
readonly actual: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ValidationIssue =
|
|
52
|
+
| IssueCustom
|
|
53
|
+
| IssueInvalidFormat
|
|
54
|
+
| IssueInvalidType
|
|
55
|
+
| IssueInvalidValue
|
|
56
|
+
| IssueRequiredKey
|
|
57
|
+
| IssueTooBig
|
|
58
|
+
| IssueTooSmall
|
|
59
|
+
|
|
60
|
+
export function stringifyIssue(issue: ValidationIssue): string {
|
|
61
|
+
const pathStr = issue.path.length ? ` at ${buildJsonPath(issue.path)}` : ''
|
|
62
|
+
|
|
63
|
+
switch (issue.code) {
|
|
64
|
+
case 'invalid_format':
|
|
65
|
+
return `Invalid ${stringifyStringFormat(issue.format)} format${issue.message ? ` (${issue.message})` : ''}${pathStr} (got ${stringifyValue(issue.input)})`
|
|
66
|
+
case 'invalid_type':
|
|
67
|
+
return `Expected ${oneOf(issue.expected.map(stringifyExpectedType))} value type${pathStr} (got ${stringifyType(issue.input)})`
|
|
68
|
+
case 'invalid_value':
|
|
69
|
+
return `Expected ${oneOf(issue.values.map(stringifyValue))}${pathStr} (got ${stringifyValue(issue.input)})`
|
|
70
|
+
case 'required_key':
|
|
71
|
+
return `Missing required key "${String(issue.key)}"${pathStr}`
|
|
72
|
+
case 'too_big':
|
|
73
|
+
return `${issue.type} too big (maximum ${issue.maximum})${pathStr} (got ${issue.actual})`
|
|
74
|
+
case 'too_small':
|
|
75
|
+
return `${issue.type} too small (minimum ${issue.minimum})${pathStr} (got ${issue.actual})`
|
|
76
|
+
case 'custom':
|
|
77
|
+
return `${issue.message}${pathStr}`
|
|
78
|
+
default:
|
|
79
|
+
// @ts-expect-error fool-proofing
|
|
80
|
+
return `${issue.code} validation error${pathStr}`
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function stringifyExpectedType(expected: string): string {
|
|
85
|
+
if (expected === '$typed') {
|
|
86
|
+
return 'an object or record which includes a "$type" property'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return expected
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildJsonPath(path: readonly PropertyKey[]): string {
|
|
93
|
+
let jsonPath = '$'
|
|
94
|
+
for (const segment of path) {
|
|
95
|
+
if (typeof segment === 'number') {
|
|
96
|
+
jsonPath += `[${segment}]`
|
|
97
|
+
} else if (/^[a-zA-Z_$][a-zA-Z0-9_]*$/.test(segment as string)) {
|
|
98
|
+
jsonPath += `.${segment}`
|
|
99
|
+
} else {
|
|
100
|
+
jsonPath += `[${JSON.stringify(segment)}]`
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return jsonPath
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function oneOf(arr: readonly string[]): string {
|
|
107
|
+
if (arr.length === 0) return ''
|
|
108
|
+
if (arr.length === 1) return arr[0]
|
|
109
|
+
return `one of ${arr.slice(0, -1).join(', ')} or ${arr.at(-1)}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function stringifyStringFormat(format: string): string {
|
|
113
|
+
switch (format) {
|
|
114
|
+
case 'datetime':
|
|
115
|
+
return 'datetime'
|
|
116
|
+
case 'language':
|
|
117
|
+
return 'language'
|
|
118
|
+
case 'at-identifier':
|
|
119
|
+
return `AT identifier`
|
|
120
|
+
case 'did':
|
|
121
|
+
return `DID`
|
|
122
|
+
case 'handle':
|
|
123
|
+
return `handle`
|
|
124
|
+
case 'nsid':
|
|
125
|
+
return `NSID`
|
|
126
|
+
case 'cid':
|
|
127
|
+
return `CID string`
|
|
128
|
+
case 'tid':
|
|
129
|
+
return `TID string`
|
|
130
|
+
case 'record-key':
|
|
131
|
+
return `record key`
|
|
132
|
+
default:
|
|
133
|
+
return format
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function stringifyType(value: unknown): string {
|
|
138
|
+
switch (typeof value) {
|
|
139
|
+
case 'object':
|
|
140
|
+
if (value === null) return 'null'
|
|
141
|
+
if (Array.isArray(value)) return 'array'
|
|
142
|
+
if (CID.asCID(value)) return 'cid'
|
|
143
|
+
if (value instanceof Date) return 'date'
|
|
144
|
+
if (value instanceof RegExp) return 'regexp'
|
|
145
|
+
if (value instanceof Map) return 'map'
|
|
146
|
+
if (value instanceof Set) return 'set'
|
|
147
|
+
return 'object'
|
|
148
|
+
case 'number':
|
|
149
|
+
if (Number.isInteger(value)) return 'integer'
|
|
150
|
+
if (Number.isNaN(value)) return 'NaN'
|
|
151
|
+
return 'float'
|
|
152
|
+
default:
|
|
153
|
+
return typeof value
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function stringifyValue(value: unknown): string {
|
|
158
|
+
switch (typeof value) {
|
|
159
|
+
case 'bigint':
|
|
160
|
+
return `${value}n`
|
|
161
|
+
case 'number':
|
|
162
|
+
case 'string':
|
|
163
|
+
case 'boolean':
|
|
164
|
+
return JSON.stringify(value)
|
|
165
|
+
case 'object':
|
|
166
|
+
if (Array.isArray(value)) {
|
|
167
|
+
return `[${stringifyArray(value, stringifyValue)}]`
|
|
168
|
+
}
|
|
169
|
+
if (isPlainObject(value)) {
|
|
170
|
+
return `{${stringifyArray(Object.entries(value), stringifyObjectEntry)}}`
|
|
171
|
+
}
|
|
172
|
+
// fallthrough
|
|
173
|
+
default:
|
|
174
|
+
return stringifyType(value)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function stringifyObjectEntry([key, _value]: [PropertyKey, unknown]): string {
|
|
179
|
+
return `${JSON.stringify(key)}: ...`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function stringifyArray<T>(
|
|
183
|
+
arr: readonly T[],
|
|
184
|
+
fn: (item: T) => string,
|
|
185
|
+
n = 2,
|
|
186
|
+
): string {
|
|
187
|
+
return arr.slice(0, n).map(fn).join(', ') + (arr.length > n ? ', ...' : '')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function aggregateIssues(issues: ValidationIssue[]): ValidationIssue[] {
|
|
191
|
+
// Quick path for common cases
|
|
192
|
+
if (issues.length <= 1) return issues
|
|
193
|
+
if (issues.length === 2 && issues[0].code !== issues[1].code) return issues
|
|
194
|
+
|
|
195
|
+
return [
|
|
196
|
+
// Aggregate invalid_type with identical paths
|
|
197
|
+
...arrayAgg(
|
|
198
|
+
issues.filter((issue) => issue.code === 'invalid_type'),
|
|
199
|
+
(a, b) => comparePropertyPaths(a.path, b.path),
|
|
200
|
+
(issues) => ({
|
|
201
|
+
...issues[0],
|
|
202
|
+
expected: Array.from(new Set(issues.flatMap((iss) => iss.expected))),
|
|
203
|
+
}),
|
|
204
|
+
),
|
|
205
|
+
// Aggregate invalid_value with identical paths
|
|
206
|
+
...arrayAgg(
|
|
207
|
+
issues.filter((issue) => issue.code === 'invalid_value'),
|
|
208
|
+
(a, b) => comparePropertyPaths(a.path, b.path),
|
|
209
|
+
(issues) => ({
|
|
210
|
+
...issues[0],
|
|
211
|
+
values: Array.from(new Set(issues.flatMap((iss) => iss.values))),
|
|
212
|
+
}),
|
|
213
|
+
),
|
|
214
|
+
// Pass through other issues
|
|
215
|
+
...issues.filter(
|
|
216
|
+
(issue) =>
|
|
217
|
+
issue.code !== 'invalid_type' && issue.code !== 'invalid_value',
|
|
218
|
+
),
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function comparePropertyPaths(
|
|
223
|
+
a: readonly PropertyKey[],
|
|
224
|
+
b: readonly PropertyKey[],
|
|
225
|
+
) {
|
|
226
|
+
if (a.length !== b.length) return false
|
|
227
|
+
for (let i = 0; i < a.length; i++) {
|
|
228
|
+
if (a[i] !== b[i]) return false
|
|
229
|
+
}
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { ResultFailure, ResultSuccess, failure, success } from '../core.js'
|
|
2
|
+
import { PropertyKey } from './property-key.js'
|
|
3
|
+
import { ValidationError } from './validation-error.js'
|
|
4
|
+
import {
|
|
5
|
+
IssueTooBig,
|
|
6
|
+
IssueTooSmall,
|
|
7
|
+
ValidationIssue,
|
|
8
|
+
} from './validation-issue.js'
|
|
9
|
+
|
|
10
|
+
export type ValidationSuccess<Value = any> = ResultSuccess<Value>
|
|
11
|
+
export type ValidationFailure = ResultFailure<ValidationError>
|
|
12
|
+
export type ValidationResult<Value = any> =
|
|
13
|
+
| ValidationSuccess<Value>
|
|
14
|
+
| ValidationFailure
|
|
15
|
+
|
|
16
|
+
type ValidationOptions = {
|
|
17
|
+
path?: PropertyKey[]
|
|
18
|
+
|
|
19
|
+
/** @default true */
|
|
20
|
+
allowTransform?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type Infer<T extends Validator> = T['_lex']['output']
|
|
24
|
+
|
|
25
|
+
export abstract class Validator<Output = any> {
|
|
26
|
+
/**
|
|
27
|
+
* This property is used for type inference purposes and does not actually
|
|
28
|
+
* exist at runtime.
|
|
29
|
+
*
|
|
30
|
+
* @deprecated For internal use only (not actually deprecated)
|
|
31
|
+
*/
|
|
32
|
+
_lex!: { output: Output }
|
|
33
|
+
|
|
34
|
+
readonly lexiconType?: string
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @internal **INTERNAL API, DO NOT USE**.
|
|
38
|
+
*
|
|
39
|
+
* Use {@link Validator.assert assert}, {@link Validator.check check},
|
|
40
|
+
* {@link Validator.parse parse} or {@link Validator.validate validate}
|
|
41
|
+
* instead.
|
|
42
|
+
*
|
|
43
|
+
* This method is implemented by subclasses to perform transformation and
|
|
44
|
+
* validation of the input value. Do not call this method directly; as the
|
|
45
|
+
* {@link ValidatorContext.options.allowTransform} option will **not** be
|
|
46
|
+
* enforced. See {@link ValidatorContext.validate} for details. When
|
|
47
|
+
* delegating validation from one validator sub-class implementation to
|
|
48
|
+
* another schema, {@link ValidatorContext.validate} should be used instead
|
|
49
|
+
* of calling {@link Validator.validateInContext}. This will allow to stop the
|
|
50
|
+
* validation process if the value was transformed (by the other schema) but
|
|
51
|
+
* transformations are not allowed.
|
|
52
|
+
*
|
|
53
|
+
* By convention, the {@link ValidationResult} must return the original input
|
|
54
|
+
* value if validation was successful and no transformation was applied (i.e.
|
|
55
|
+
* the input already conformed to the schema). If a default value, or any
|
|
56
|
+
* other transformation was applied, the returned value c&an be different from
|
|
57
|
+
* the input.
|
|
58
|
+
*
|
|
59
|
+
* This convention allows the {@link Validator.check check} and
|
|
60
|
+
* {@link Validator.assert assert} methods to check whether the input value
|
|
61
|
+
* exactly matches the schema (without defaults or transformations), by
|
|
62
|
+
* checking if the returned value is strictly equal to the input.
|
|
63
|
+
*
|
|
64
|
+
* @see {@link ValidatorContext.validate}
|
|
65
|
+
*/
|
|
66
|
+
abstract validateInContext(
|
|
67
|
+
input: unknown,
|
|
68
|
+
ctx: ValidatorContext,
|
|
69
|
+
): ValidationResult<Output>
|
|
70
|
+
|
|
71
|
+
assert(input: unknown): asserts input is Output {
|
|
72
|
+
const result = this.validate(input, { allowTransform: false })
|
|
73
|
+
if (!result.success) throw result.error
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
check(input: unknown): input is Output {
|
|
77
|
+
const result = this.validate(input, { allowTransform: false })
|
|
78
|
+
return result.success
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
maybe<I>(input: I): (I & Output) | undefined {
|
|
82
|
+
return this.check(input) ? input : undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
parse<I>(
|
|
86
|
+
input: I,
|
|
87
|
+
options: ValidationOptions & { allowTransform: false },
|
|
88
|
+
): I & Output
|
|
89
|
+
parse(input: unknown, options?: ValidationOptions): Output
|
|
90
|
+
parse(input: unknown, options?: ValidationOptions): Output {
|
|
91
|
+
const result = ValidatorContext.validate(input, this, options)
|
|
92
|
+
if (!result.success) throw result.error
|
|
93
|
+
return result.value
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
validate<I>(
|
|
97
|
+
input: I,
|
|
98
|
+
options: ValidationOptions & { allowTransform: false },
|
|
99
|
+
): ValidationResult<I & Output>
|
|
100
|
+
validate(
|
|
101
|
+
input: unknown,
|
|
102
|
+
options?: ValidationOptions,
|
|
103
|
+
): ValidationResult<Output>
|
|
104
|
+
validate(
|
|
105
|
+
input: unknown,
|
|
106
|
+
options?: ValidationOptions,
|
|
107
|
+
): ValidationResult<Output> {
|
|
108
|
+
return ValidatorContext.validate(input, this, options)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// @NOTE The built lexicons namespaces will export utility functions that
|
|
112
|
+
// allow accessing the schema's methods without the need to specify ".main."
|
|
113
|
+
// as part of the namespace. This way, a utility for a particular record type
|
|
114
|
+
// can be called like "app.bsky.feed.post.<utility>()" instead of
|
|
115
|
+
// "app.bsky.feed.post.main.<utility>()". Because those utilities could
|
|
116
|
+
// conflict with other schemas (e.g. if there is a lexicon definition at
|
|
117
|
+
// "#<utility>"), those exported utilities will be prefixed with "$". In order
|
|
118
|
+
// to be able to consistently call the utilities, when using the "main" and
|
|
119
|
+
// non "main" definitions, we also expose the same methods with a "$" prefix.
|
|
120
|
+
// Thanks to this, both of the following call will be possible:
|
|
121
|
+
//
|
|
122
|
+
// - "app.bsky.feed.post.$parse(...)" // calls a utility function created by "lex build"
|
|
123
|
+
// - "app.bsky.feed.defs.postView.$parse(...)" // uses the alias defined below on the schema instance
|
|
124
|
+
|
|
125
|
+
$assert(input: unknown): asserts input is Output {
|
|
126
|
+
return this.assert(input)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
$check(input: unknown): input is Output {
|
|
130
|
+
return this.check(input)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
$maybe<I>(input: I): (I & Output) | undefined {
|
|
134
|
+
return this.maybe(input)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
$parse(input: unknown, options?: ValidationOptions): Output {
|
|
138
|
+
return this.parse(input, options)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
$validate(
|
|
142
|
+
input: unknown,
|
|
143
|
+
options?: ValidationOptions,
|
|
144
|
+
): ValidationResult<Output> {
|
|
145
|
+
return this.validate(input, options)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export type ContextualIssue = {
|
|
150
|
+
[Code in ValidationIssue['code']]: Omit<
|
|
151
|
+
Extract<ValidationIssue, { code: Code }>,
|
|
152
|
+
'path'
|
|
153
|
+
> & { path?: PropertyKey | readonly PropertyKey[] }
|
|
154
|
+
}[ValidationIssue['code']]
|
|
155
|
+
|
|
156
|
+
const asIssue = (
|
|
157
|
+
{ path, ...issue }: ContextualIssue,
|
|
158
|
+
currentPath: readonly PropertyKey[],
|
|
159
|
+
): ValidationIssue & { path: PropertyKey[] } => ({
|
|
160
|
+
...issue,
|
|
161
|
+
path: path != null ? currentPath.concat(path) : [...currentPath],
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
export class ValidatorContext {
|
|
165
|
+
/**
|
|
166
|
+
* Creates a new validation context and validates the input using the
|
|
167
|
+
* provided validator.
|
|
168
|
+
*/
|
|
169
|
+
static validate<V>(
|
|
170
|
+
input: unknown,
|
|
171
|
+
validator: Validator<V>,
|
|
172
|
+
options: ValidationOptions = {},
|
|
173
|
+
): ValidationResult<V> {
|
|
174
|
+
const context = new ValidatorContext(options)
|
|
175
|
+
return context.validate(input, validator)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private readonly currentPath: PropertyKey[]
|
|
179
|
+
private readonly issues: ValidationIssue[] = []
|
|
180
|
+
|
|
181
|
+
protected constructor(readonly options: ValidationOptions) {
|
|
182
|
+
// Create a copy because we will be mutating the array during validation.
|
|
183
|
+
this.currentPath = options?.path != null ? [...options.path] : []
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
get path() {
|
|
187
|
+
return [...this.currentPath]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
get allowTransform() {
|
|
191
|
+
// Default to true
|
|
192
|
+
return this.options?.allowTransform !== false
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* This is basically the entry point for validation within a context. Use this
|
|
197
|
+
* method instead of {@link Validator.validateInContext} directly, because
|
|
198
|
+
* this method enforces the {@link ValidationOptions.allowTransform} option.
|
|
199
|
+
*/
|
|
200
|
+
validate<V>(input: unknown, validator: Validator<V>): ValidationResult<V> {
|
|
201
|
+
const result = validator.validateInContext(input, this)
|
|
202
|
+
|
|
203
|
+
if (result.success) {
|
|
204
|
+
if (!this.allowTransform && !Object.is(result.value, input)) {
|
|
205
|
+
// If the value changed, it means that a default (or some other
|
|
206
|
+
// transformation) was applied, meaning that the original value did
|
|
207
|
+
// *not* match the (output) schema. When "allowTransform" is false, we
|
|
208
|
+
// consider this a failure.
|
|
209
|
+
|
|
210
|
+
// This check is the reason why Validator.validateInContext should not
|
|
211
|
+
// be used directly, and ValidatorContext.validate should be used
|
|
212
|
+
// instead, even when delegating validation from one validator to
|
|
213
|
+
// another.
|
|
214
|
+
|
|
215
|
+
// This if block comes before the next one because 'this.issues' will
|
|
216
|
+
// end-up being appended to the returned ValidationError (see the
|
|
217
|
+
// "failure" method below), resulting in a more complete error report.
|
|
218
|
+
return this.issueInvalidValue(input, [result.value])
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (this.issues.length > 0) {
|
|
222
|
+
// Validator returned a success but issues were added via the context.
|
|
223
|
+
// This means the overall validation failed.
|
|
224
|
+
return { success: false, error: new ValidationError(this.issues) }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return result
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
validateChild<I extends object, K extends PropertyKey & keyof I, V>(
|
|
232
|
+
input: I,
|
|
233
|
+
key: K,
|
|
234
|
+
validator: Validator<V>,
|
|
235
|
+
): ValidationResult<V> {
|
|
236
|
+
// Instead of creating a new context, we just push/pop the path segment.
|
|
237
|
+
this.currentPath.push(key)
|
|
238
|
+
try {
|
|
239
|
+
return this.validate(input[key], validator)
|
|
240
|
+
} finally {
|
|
241
|
+
this.currentPath.length--
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
addIssue(issue: ContextualIssue): void {
|
|
246
|
+
this.issues.push(asIssue(issue, this.currentPath))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
success<V>(value: V): ValidationResult<V> {
|
|
250
|
+
return success(value)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
failure(issue: ContextualIssue): ValidationFailure {
|
|
254
|
+
return failure(
|
|
255
|
+
new ValidationError([...this.issues, asIssue(issue, this.currentPath)]),
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
issueInvalidValue(
|
|
260
|
+
input: unknown,
|
|
261
|
+
values: readonly unknown[],
|
|
262
|
+
path?: PropertyKey | readonly PropertyKey[],
|
|
263
|
+
) {
|
|
264
|
+
return this.failure({
|
|
265
|
+
code: 'invalid_value',
|
|
266
|
+
input,
|
|
267
|
+
values,
|
|
268
|
+
path,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
issueInvalidType(
|
|
273
|
+
input: unknown,
|
|
274
|
+
expected: string | readonly string[],
|
|
275
|
+
path?: PropertyKey | readonly PropertyKey[],
|
|
276
|
+
) {
|
|
277
|
+
return this.failure({
|
|
278
|
+
code: 'invalid_type',
|
|
279
|
+
input,
|
|
280
|
+
expected: Array.isArray(expected) ? expected : [expected],
|
|
281
|
+
path,
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
issueInvalidPropertyValue<I>(
|
|
286
|
+
input: I,
|
|
287
|
+
property: keyof I & PropertyKey,
|
|
288
|
+
values: readonly unknown[],
|
|
289
|
+
) {
|
|
290
|
+
return this.issueInvalidValue(input[property], values, property)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
issueInvalidPropertyType<I>(
|
|
294
|
+
input: I,
|
|
295
|
+
property: keyof I & PropertyKey,
|
|
296
|
+
expected: string | readonly string[],
|
|
297
|
+
) {
|
|
298
|
+
return this.issueInvalidType(input[property], expected, property)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
issueRequiredKey(input: object, key: PropertyKey) {
|
|
302
|
+
return this.failure({
|
|
303
|
+
code: 'required_key',
|
|
304
|
+
key,
|
|
305
|
+
input,
|
|
306
|
+
path: key,
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
issueInvalidFormat(input: unknown, format: string, message?: string) {
|
|
311
|
+
return this.failure({
|
|
312
|
+
code: 'invalid_format',
|
|
313
|
+
message,
|
|
314
|
+
format,
|
|
315
|
+
input,
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
issueTooBig(
|
|
320
|
+
input: unknown,
|
|
321
|
+
type: IssueTooBig['type'],
|
|
322
|
+
maximum: number,
|
|
323
|
+
actual: number,
|
|
324
|
+
) {
|
|
325
|
+
return this.failure({
|
|
326
|
+
code: 'too_big',
|
|
327
|
+
type,
|
|
328
|
+
maximum,
|
|
329
|
+
actual,
|
|
330
|
+
input,
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
issueTooSmall(
|
|
335
|
+
input: unknown,
|
|
336
|
+
type: IssueTooSmall['type'],
|
|
337
|
+
minimum: number,
|
|
338
|
+
actual: number,
|
|
339
|
+
) {
|
|
340
|
+
return this.failure({
|
|
341
|
+
code: 'too_small',
|
|
342
|
+
type,
|
|
343
|
+
minimum,
|
|
344
|
+
actual,
|
|
345
|
+
input,
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
custom(
|
|
350
|
+
input: unknown,
|
|
351
|
+
message: string,
|
|
352
|
+
path?: PropertyKey | readonly PropertyKey[],
|
|
353
|
+
) {
|
|
354
|
+
return this.failure({
|
|
355
|
+
code: 'custom',
|
|
356
|
+
input,
|
|
357
|
+
message,
|
|
358
|
+
path,
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
}
|