@atproto/lexicon 0.0.1
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/README.md +31 -0
- package/build.js +22 -0
- package/dist/index.d.ts +126 -0
- package/dist/index.js +3897 -0
- package/dist/index.js.map +7 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/lexicons.d.ts +15 -0
- package/dist/src/record/index.d.ts +4 -0
- package/dist/src/record/schema.d.ts +9 -0
- package/dist/src/record/schemas.d.ts +10 -0
- package/dist/src/record/util.d.ts +1 -0
- package/dist/src/record/validation.d.ts +24 -0
- package/dist/src/record/validator.d.ts +17 -0
- package/dist/src/record-validator.d.ts +17 -0
- package/dist/src/schema.d.ts +9 -0
- package/dist/src/schemas.d.ts +10 -0
- package/dist/src/types.d.ts +30268 -0
- package/dist/src/util.d.ts +6 -0
- package/dist/src/validation.d.ts +6 -0
- package/dist/src/validators/blob.d.ts +6 -0
- package/dist/src/validators/complex.d.ts +5 -0
- package/dist/src/validators/primitives.d.ts +9 -0
- package/dist/src/validators/xrpc.d.ts +3 -0
- package/dist/src/view-validator.d.ts +13 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.js +35 -0
- package/jest.config.js +6 -0
- package/package.json +21 -0
- package/src/index.ts +2 -0
- package/src/lexicons.ts +203 -0
- package/src/types.ts +318 -0
- package/src/util.ts +107 -0
- package/src/validation.ts +48 -0
- package/src/validators/blob.ts +57 -0
- package/src/validators/complex.ts +152 -0
- package/src/validators/primitives.ts +300 -0
- package/src/validators/xrpc.ts +50 -0
- package/tests/_scaffolds/lexicons.ts +379 -0
- package/tests/general.test.ts +611 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +11 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { NSID } from '@atproto/nsid'
|
|
3
|
+
|
|
4
|
+
// primitives
|
|
5
|
+
// =
|
|
6
|
+
|
|
7
|
+
export const lexBoolean = z.object({
|
|
8
|
+
type: z.literal('boolean'),
|
|
9
|
+
description: z.string().optional(),
|
|
10
|
+
default: z.boolean().optional(),
|
|
11
|
+
const: z.boolean().optional(),
|
|
12
|
+
})
|
|
13
|
+
export type LexBoolean = z.infer<typeof lexBoolean>
|
|
14
|
+
|
|
15
|
+
export const lexNumber = z.object({
|
|
16
|
+
type: z.literal('number'),
|
|
17
|
+
description: z.string().optional(),
|
|
18
|
+
default: z.number().optional(),
|
|
19
|
+
minimum: z.number().optional(),
|
|
20
|
+
maximum: z.number().optional(),
|
|
21
|
+
enum: z.number().array().optional(),
|
|
22
|
+
const: z.number().optional(),
|
|
23
|
+
})
|
|
24
|
+
export type LexNumber = z.infer<typeof lexNumber>
|
|
25
|
+
|
|
26
|
+
export const lexInteger = z.object({
|
|
27
|
+
type: z.literal('integer'),
|
|
28
|
+
description: z.string().optional(),
|
|
29
|
+
default: z.number().int().optional(),
|
|
30
|
+
minimum: z.number().int().optional(),
|
|
31
|
+
maximum: z.number().int().optional(),
|
|
32
|
+
enum: z.number().int().array().optional(),
|
|
33
|
+
const: z.number().int().optional(),
|
|
34
|
+
})
|
|
35
|
+
export type LexInteger = z.infer<typeof lexInteger>
|
|
36
|
+
|
|
37
|
+
export const lexString = z.object({
|
|
38
|
+
type: z.literal('string'),
|
|
39
|
+
description: z.string().optional(),
|
|
40
|
+
default: z.string().optional(),
|
|
41
|
+
minLength: z.number().int().optional(),
|
|
42
|
+
maxLength: z.number().int().optional(),
|
|
43
|
+
enum: z.string().array().optional(),
|
|
44
|
+
const: z.string().optional(),
|
|
45
|
+
knownValues: z.string().array().optional(),
|
|
46
|
+
})
|
|
47
|
+
export type LexString = z.infer<typeof lexString>
|
|
48
|
+
|
|
49
|
+
export const lexDatetime = z.object({
|
|
50
|
+
type: z.literal('datetime'),
|
|
51
|
+
description: z.string().optional(),
|
|
52
|
+
})
|
|
53
|
+
export type LexDatetime = z.infer<typeof lexDatetime>
|
|
54
|
+
|
|
55
|
+
export const lexUnknown = z.object({
|
|
56
|
+
type: z.literal('unknown'),
|
|
57
|
+
description: z.string().optional(),
|
|
58
|
+
})
|
|
59
|
+
export type LexUnknown = z.infer<typeof lexUnknown>
|
|
60
|
+
|
|
61
|
+
export const lexPrimitive = z.union([
|
|
62
|
+
lexBoolean,
|
|
63
|
+
lexNumber,
|
|
64
|
+
lexInteger,
|
|
65
|
+
lexString,
|
|
66
|
+
lexDatetime,
|
|
67
|
+
lexUnknown,
|
|
68
|
+
])
|
|
69
|
+
export type LexPrimitive = z.infer<typeof lexPrimitive>
|
|
70
|
+
|
|
71
|
+
// references
|
|
72
|
+
// =
|
|
73
|
+
|
|
74
|
+
export const lexRef = z.object({
|
|
75
|
+
type: z.literal('ref'),
|
|
76
|
+
description: z.string().optional(),
|
|
77
|
+
ref: z.string(),
|
|
78
|
+
})
|
|
79
|
+
export type LexRef = z.infer<typeof lexRef>
|
|
80
|
+
|
|
81
|
+
export const lexRefUnion = z.object({
|
|
82
|
+
type: z.literal('union'),
|
|
83
|
+
description: z.string().optional(),
|
|
84
|
+
refs: z.string().array(),
|
|
85
|
+
closed: z.boolean().optional(),
|
|
86
|
+
})
|
|
87
|
+
export type LexRefUnion = z.infer<typeof lexRefUnion>
|
|
88
|
+
|
|
89
|
+
export const lexRefVariant = z.union([lexRef, lexRefUnion])
|
|
90
|
+
export type LexRefVariant = z.infer<typeof lexRefVariant>
|
|
91
|
+
|
|
92
|
+
// blobs
|
|
93
|
+
// =
|
|
94
|
+
|
|
95
|
+
export const lexBlob = z.object({
|
|
96
|
+
type: z.literal('blob'),
|
|
97
|
+
description: z.string().optional(),
|
|
98
|
+
accept: z.string().array().optional(),
|
|
99
|
+
maxSize: z.number().optional(),
|
|
100
|
+
})
|
|
101
|
+
export type LexBlob = z.infer<typeof lexBlob>
|
|
102
|
+
|
|
103
|
+
export const lexImage = z.object({
|
|
104
|
+
type: z.literal('image'),
|
|
105
|
+
description: z.string().optional(),
|
|
106
|
+
accept: z.string().array().optional(),
|
|
107
|
+
maxSize: z.number().optional(),
|
|
108
|
+
maxWidth: z.number().int().optional(),
|
|
109
|
+
maxHeight: z.number().int().optional(),
|
|
110
|
+
})
|
|
111
|
+
export type LexImage = z.infer<typeof lexImage>
|
|
112
|
+
|
|
113
|
+
export const lexVideo = z.object({
|
|
114
|
+
type: z.literal('video'),
|
|
115
|
+
description: z.string().optional(),
|
|
116
|
+
accept: z.string().array().optional(),
|
|
117
|
+
maxSize: z.number().optional(),
|
|
118
|
+
maxWidth: z.number().int().optional(),
|
|
119
|
+
maxHeight: z.number().int().optional(),
|
|
120
|
+
maxLength: z.number().int().optional(),
|
|
121
|
+
})
|
|
122
|
+
export type LexVideo = z.infer<typeof lexVideo>
|
|
123
|
+
|
|
124
|
+
export const lexAudio = z.object({
|
|
125
|
+
type: z.literal('audio'),
|
|
126
|
+
description: z.string().optional(),
|
|
127
|
+
accept: z.string().array().optional(),
|
|
128
|
+
maxSize: z.number().optional(),
|
|
129
|
+
maxLength: z.number().int().optional(),
|
|
130
|
+
})
|
|
131
|
+
export type LexAudio = z.infer<typeof lexAudio>
|
|
132
|
+
|
|
133
|
+
export const lexBlobVariant = z.union([lexBlob, lexImage, lexVideo, lexAudio])
|
|
134
|
+
export type LexBlobVariant = z.infer<typeof lexBlobVariant>
|
|
135
|
+
|
|
136
|
+
// complex types
|
|
137
|
+
// =
|
|
138
|
+
|
|
139
|
+
export const lexArray = z.object({
|
|
140
|
+
type: z.literal('array'),
|
|
141
|
+
description: z.string().optional(),
|
|
142
|
+
items: z.union([lexPrimitive, lexBlobVariant, lexRefVariant]),
|
|
143
|
+
minLength: z.number().int().optional(),
|
|
144
|
+
maxLength: z.number().int().optional(),
|
|
145
|
+
})
|
|
146
|
+
export type LexArray = z.infer<typeof lexArray>
|
|
147
|
+
|
|
148
|
+
export const lexToken = z.object({
|
|
149
|
+
type: z.literal('token'),
|
|
150
|
+
description: z.string().optional(),
|
|
151
|
+
})
|
|
152
|
+
export type LexToken = z.infer<typeof lexToken>
|
|
153
|
+
|
|
154
|
+
export const lexObject = z.object({
|
|
155
|
+
type: z.literal('object'),
|
|
156
|
+
description: z.string().optional(),
|
|
157
|
+
required: z.string().array().optional(),
|
|
158
|
+
properties: z
|
|
159
|
+
.record(z.union([lexRefVariant, lexArray, lexBlobVariant, lexPrimitive]))
|
|
160
|
+
.optional(),
|
|
161
|
+
})
|
|
162
|
+
export type LexObject = z.infer<typeof lexObject>
|
|
163
|
+
|
|
164
|
+
// xrpc
|
|
165
|
+
// =
|
|
166
|
+
|
|
167
|
+
export const lexXrpcParameters = z.object({
|
|
168
|
+
type: z.literal('params'),
|
|
169
|
+
description: z.string().optional(),
|
|
170
|
+
required: z.string().array().optional(),
|
|
171
|
+
properties: z.record(lexPrimitive),
|
|
172
|
+
})
|
|
173
|
+
export type LexXrpcParameters = z.infer<typeof lexXrpcParameters>
|
|
174
|
+
|
|
175
|
+
export const lexXrpcBody = z.object({
|
|
176
|
+
description: z.string().optional(),
|
|
177
|
+
encoding: z.string(),
|
|
178
|
+
schema: z.union([lexRefVariant, lexObject]).optional(),
|
|
179
|
+
})
|
|
180
|
+
export type LexXrpcBody = z.infer<typeof lexXrpcBody>
|
|
181
|
+
|
|
182
|
+
export const lexXrpcError = z.object({
|
|
183
|
+
name: z.string(),
|
|
184
|
+
description: z.string().optional(),
|
|
185
|
+
})
|
|
186
|
+
export type LexXrpcError = z.infer<typeof lexXrpcError>
|
|
187
|
+
|
|
188
|
+
export const lexXrpcQuery = z.object({
|
|
189
|
+
type: z.literal('query'),
|
|
190
|
+
description: z.string().optional(),
|
|
191
|
+
parameters: lexXrpcParameters.optional(),
|
|
192
|
+
output: lexXrpcBody.optional(),
|
|
193
|
+
errors: lexXrpcError.array().optional(),
|
|
194
|
+
})
|
|
195
|
+
export type LexXrpcQuery = z.infer<typeof lexXrpcQuery>
|
|
196
|
+
|
|
197
|
+
export const lexXrpcProcedure = z.object({
|
|
198
|
+
type: z.literal('procedure'),
|
|
199
|
+
description: z.string().optional(),
|
|
200
|
+
parameters: lexXrpcParameters.optional(),
|
|
201
|
+
input: lexXrpcBody.optional(),
|
|
202
|
+
output: lexXrpcBody.optional(),
|
|
203
|
+
errors: lexXrpcError.array().optional(),
|
|
204
|
+
})
|
|
205
|
+
export type LexXrpcProcedure = z.infer<typeof lexXrpcProcedure>
|
|
206
|
+
|
|
207
|
+
// database
|
|
208
|
+
// =
|
|
209
|
+
|
|
210
|
+
export const lexRecord = z.object({
|
|
211
|
+
type: z.literal('record'),
|
|
212
|
+
description: z.string().optional(),
|
|
213
|
+
key: z.string().optional(),
|
|
214
|
+
record: lexObject,
|
|
215
|
+
})
|
|
216
|
+
export type LexRecord = z.infer<typeof lexRecord>
|
|
217
|
+
|
|
218
|
+
// core
|
|
219
|
+
// =
|
|
220
|
+
|
|
221
|
+
export const lexUserType = z.union([
|
|
222
|
+
lexRecord,
|
|
223
|
+
|
|
224
|
+
lexXrpcQuery,
|
|
225
|
+
lexXrpcProcedure,
|
|
226
|
+
|
|
227
|
+
lexBlob,
|
|
228
|
+
lexImage,
|
|
229
|
+
lexVideo,
|
|
230
|
+
lexAudio,
|
|
231
|
+
|
|
232
|
+
lexArray,
|
|
233
|
+
lexToken,
|
|
234
|
+
lexObject,
|
|
235
|
+
|
|
236
|
+
lexBoolean,
|
|
237
|
+
lexNumber,
|
|
238
|
+
lexInteger,
|
|
239
|
+
lexString,
|
|
240
|
+
lexDatetime,
|
|
241
|
+
lexUnknown,
|
|
242
|
+
])
|
|
243
|
+
export type LexUserType = z.infer<typeof lexUserType>
|
|
244
|
+
|
|
245
|
+
export const lexiconDoc = z
|
|
246
|
+
.object({
|
|
247
|
+
lexicon: z.literal(1),
|
|
248
|
+
id: z.string().refine((v: string) => NSID.isValid(v), {
|
|
249
|
+
message: 'Must be a valid NSID',
|
|
250
|
+
}),
|
|
251
|
+
revision: z.number().optional(),
|
|
252
|
+
description: z.string().optional(),
|
|
253
|
+
defs: z.record(lexUserType),
|
|
254
|
+
})
|
|
255
|
+
.superRefine((doc: LexiconDoc, ctx) => {
|
|
256
|
+
for (const defId in doc.defs) {
|
|
257
|
+
const def = doc.defs[defId]
|
|
258
|
+
if (
|
|
259
|
+
defId !== 'main' &&
|
|
260
|
+
(def.type === 'record' ||
|
|
261
|
+
def.type === 'procedure' ||
|
|
262
|
+
def.type === 'query')
|
|
263
|
+
) {
|
|
264
|
+
ctx.addIssue({
|
|
265
|
+
code: z.ZodIssueCode.custom,
|
|
266
|
+
message: `Records, procedures, and queries must be the main definition.`,
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
export type LexiconDoc = z.infer<typeof lexiconDoc>
|
|
272
|
+
|
|
273
|
+
// helpers
|
|
274
|
+
// =
|
|
275
|
+
|
|
276
|
+
export function isValidLexiconDoc(v: unknown): v is LexiconDoc {
|
|
277
|
+
return lexiconDoc.safeParse(v).success
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function isObj(obj: unknown): obj is Record<string, unknown> {
|
|
281
|
+
return !!obj && typeof obj === 'object'
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function hasProp<K extends PropertyKey>(
|
|
285
|
+
data: object,
|
|
286
|
+
prop: K,
|
|
287
|
+
): data is Record<K, unknown> {
|
|
288
|
+
return prop in data
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export const discriminatedObject = z.object({ $type: z.string() })
|
|
292
|
+
export type DiscriminatedObject = z.infer<typeof discriminatedObject>
|
|
293
|
+
export function isDiscriminatedObject(
|
|
294
|
+
value: unknown,
|
|
295
|
+
): value is DiscriminatedObject {
|
|
296
|
+
return discriminatedObject.safeParse(value).success
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export class LexiconDocMalformedError extends Error {
|
|
300
|
+
constructor(
|
|
301
|
+
message: string,
|
|
302
|
+
public schemaDef: unknown,
|
|
303
|
+
public issues?: z.ZodIssue[],
|
|
304
|
+
) {
|
|
305
|
+
super(message)
|
|
306
|
+
this.schemaDef = schemaDef
|
|
307
|
+
this.issues = issues
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface ValidationResult {
|
|
312
|
+
success: boolean
|
|
313
|
+
error?: ValidationError
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export class ValidationError extends Error {}
|
|
317
|
+
export class InvalidLexiconError extends Error {}
|
|
318
|
+
export class LexiconDefNotFoundError extends Error {}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Lexicons } from './lexicons'
|
|
2
|
+
import * as ComplexValidators from './validators/complex'
|
|
3
|
+
import {
|
|
4
|
+
LexUserType,
|
|
5
|
+
LexRefVariant,
|
|
6
|
+
ValidationError,
|
|
7
|
+
ValidationResult,
|
|
8
|
+
isDiscriminatedObject,
|
|
9
|
+
} from './types'
|
|
10
|
+
|
|
11
|
+
export function toLexUri(str: string, baseUri?: string): string {
|
|
12
|
+
if (str.startsWith('lex:')) {
|
|
13
|
+
return str
|
|
14
|
+
}
|
|
15
|
+
if (str.startsWith('#')) {
|
|
16
|
+
if (!baseUri) {
|
|
17
|
+
throw new Error(`Unable to resolve uri without anchor: ${str}`)
|
|
18
|
+
}
|
|
19
|
+
return `${baseUri}${str}`
|
|
20
|
+
}
|
|
21
|
+
return `lex:${str}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function validateOneOf(
|
|
25
|
+
lexicons: Lexicons,
|
|
26
|
+
path: string,
|
|
27
|
+
def: LexRefVariant | LexUserType,
|
|
28
|
+
value: unknown,
|
|
29
|
+
mustBeObj = false, // this is the only type constraint we need currently (used by xrpc body schema validators)
|
|
30
|
+
): ValidationResult {
|
|
31
|
+
let error
|
|
32
|
+
|
|
33
|
+
let concreteDefs
|
|
34
|
+
if (def.type === 'union') {
|
|
35
|
+
if (!isDiscriminatedObject(value)) {
|
|
36
|
+
return {
|
|
37
|
+
success: false,
|
|
38
|
+
error: new ValidationError(
|
|
39
|
+
`${path} must be an object which includes the "$type" property`,
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!def.refs.includes(toLexUri(value.$type))) {
|
|
44
|
+
if (def.closed) {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
error: new ValidationError(
|
|
48
|
+
`${path} $type must be one of ${def.refs.join(', ')}`,
|
|
49
|
+
),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { success: true }
|
|
53
|
+
} else {
|
|
54
|
+
concreteDefs = toConcreteTypes(lexicons, {
|
|
55
|
+
type: 'ref',
|
|
56
|
+
ref: value.$type,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
concreteDefs = toConcreteTypes(lexicons, def)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const concreteDef of concreteDefs) {
|
|
64
|
+
const result = mustBeObj
|
|
65
|
+
? ComplexValidators.object(lexicons, path, concreteDef, value)
|
|
66
|
+
: ComplexValidators.validate(lexicons, path, concreteDef, value)
|
|
67
|
+
if (result.success) {
|
|
68
|
+
return result
|
|
69
|
+
}
|
|
70
|
+
error ??= result.error
|
|
71
|
+
}
|
|
72
|
+
if (concreteDefs.length > 1) {
|
|
73
|
+
return {
|
|
74
|
+
success: false,
|
|
75
|
+
error: new ValidationError(
|
|
76
|
+
`${path} did not match any of the expected definitions`,
|
|
77
|
+
),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { success: false, error }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function assertValidOneOf(
|
|
84
|
+
lexicons: Lexicons,
|
|
85
|
+
path: string,
|
|
86
|
+
def: LexRefVariant | LexUserType,
|
|
87
|
+
value: unknown,
|
|
88
|
+
mustBeObj = false,
|
|
89
|
+
) {
|
|
90
|
+
const res = validateOneOf(lexicons, path, def, value, mustBeObj)
|
|
91
|
+
if (!res.success) {
|
|
92
|
+
throw res.error
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function toConcreteTypes(
|
|
97
|
+
lexicons: Lexicons,
|
|
98
|
+
def: LexRefVariant | LexUserType,
|
|
99
|
+
): LexUserType[] {
|
|
100
|
+
if (def.type === 'ref') {
|
|
101
|
+
return [lexicons.getDefOrThrow(def.ref)]
|
|
102
|
+
} else if (def.type === 'union') {
|
|
103
|
+
return def.refs.map((ref) => lexicons.getDefOrThrow(ref)).flat()
|
|
104
|
+
} else {
|
|
105
|
+
return [def]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Lexicons } from './lexicons'
|
|
2
|
+
import { LexRecord, LexXrpcProcedure, LexXrpcQuery } from './types'
|
|
3
|
+
import { assertValidOneOf } from './util'
|
|
4
|
+
|
|
5
|
+
import * as ComplexValidators from './validators/complex'
|
|
6
|
+
import * as XrpcValidators from './validators/xrpc'
|
|
7
|
+
|
|
8
|
+
export function assertValidRecord(
|
|
9
|
+
lexicons: Lexicons,
|
|
10
|
+
def: LexRecord,
|
|
11
|
+
value: unknown,
|
|
12
|
+
) {
|
|
13
|
+
const res = ComplexValidators.object(lexicons, 'Record', def.record, value)
|
|
14
|
+
if (!res.success) throw res.error
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function assertValidXrpcParams(
|
|
18
|
+
lexicons: Lexicons,
|
|
19
|
+
def: LexXrpcProcedure | LexXrpcQuery,
|
|
20
|
+
value: unknown,
|
|
21
|
+
) {
|
|
22
|
+
if (def.parameters) {
|
|
23
|
+
const res = XrpcValidators.params(lexicons, 'Params', def.parameters, value)
|
|
24
|
+
if (!res.success) throw res.error
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function assertValidXrpcInput(
|
|
29
|
+
lexicons: Lexicons,
|
|
30
|
+
def: LexXrpcProcedure,
|
|
31
|
+
value: unknown,
|
|
32
|
+
) {
|
|
33
|
+
if (def.input?.schema) {
|
|
34
|
+
// loop: all input schema definitions
|
|
35
|
+
assertValidOneOf(lexicons, 'Input', def.input.schema, value, true)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function assertValidXrpcOutput(
|
|
40
|
+
lexicons: Lexicons,
|
|
41
|
+
def: LexXrpcProcedure | LexXrpcQuery,
|
|
42
|
+
value: unknown,
|
|
43
|
+
) {
|
|
44
|
+
if (def.output?.schema) {
|
|
45
|
+
// loop: all output schema definitions
|
|
46
|
+
assertValidOneOf(lexicons, 'Output', def.output.schema, value, true)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Lexicons } from '../lexicons'
|
|
2
|
+
import { LexUserType, ValidationResult, ValidationError } from '../types'
|
|
3
|
+
import { isObj, hasProp } from '../types'
|
|
4
|
+
|
|
5
|
+
export function blob(
|
|
6
|
+
lexicons: Lexicons,
|
|
7
|
+
path: string,
|
|
8
|
+
def: LexUserType,
|
|
9
|
+
value: unknown,
|
|
10
|
+
): ValidationResult {
|
|
11
|
+
if (!isObj(value)) {
|
|
12
|
+
return {
|
|
13
|
+
success: false,
|
|
14
|
+
error: new ValidationError(`${path} should be an object`),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (!hasProp(value, 'cid') || typeof value.cid !== 'string') {
|
|
18
|
+
return {
|
|
19
|
+
success: false,
|
|
20
|
+
error: new ValidationError(`${path}/cid should be a string`),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!hasProp(value, 'mimeType') || typeof value.mimeType !== 'string') {
|
|
24
|
+
return {
|
|
25
|
+
success: false,
|
|
26
|
+
error: new ValidationError(`${path}/mimeType should be a string`),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { success: true }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function image(
|
|
33
|
+
lexicons: Lexicons,
|
|
34
|
+
path: string,
|
|
35
|
+
def: LexUserType,
|
|
36
|
+
value: unknown,
|
|
37
|
+
): ValidationResult {
|
|
38
|
+
return blob(lexicons, path, def, value)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function video(
|
|
42
|
+
lexicons: Lexicons,
|
|
43
|
+
path: string,
|
|
44
|
+
def: LexUserType,
|
|
45
|
+
value: unknown,
|
|
46
|
+
): ValidationResult {
|
|
47
|
+
return blob(lexicons, path, def, value)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function audio(
|
|
51
|
+
lexicons: Lexicons,
|
|
52
|
+
path: string,
|
|
53
|
+
def: LexUserType,
|
|
54
|
+
value: unknown,
|
|
55
|
+
): ValidationResult {
|
|
56
|
+
return blob(lexicons, path, def, value)
|
|
57
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Lexicons } from '../lexicons'
|
|
2
|
+
import {
|
|
3
|
+
LexArray,
|
|
4
|
+
LexObject,
|
|
5
|
+
LexUserType,
|
|
6
|
+
ValidationResult,
|
|
7
|
+
ValidationError,
|
|
8
|
+
} from '../types'
|
|
9
|
+
import { validateOneOf } from '../util'
|
|
10
|
+
|
|
11
|
+
import * as Primitives from './primitives'
|
|
12
|
+
import * as Blob from './blob'
|
|
13
|
+
|
|
14
|
+
export function validate(
|
|
15
|
+
lexicons: Lexicons,
|
|
16
|
+
path: string,
|
|
17
|
+
def: LexUserType,
|
|
18
|
+
value: unknown,
|
|
19
|
+
): ValidationResult {
|
|
20
|
+
switch (def.type) {
|
|
21
|
+
case 'boolean':
|
|
22
|
+
return Primitives.boolean(lexicons, path, def, value)
|
|
23
|
+
case 'number':
|
|
24
|
+
return Primitives.number(lexicons, path, def, value)
|
|
25
|
+
case 'integer':
|
|
26
|
+
return Primitives.integer(lexicons, path, def, value)
|
|
27
|
+
case 'string':
|
|
28
|
+
return Primitives.string(lexicons, path, def, value)
|
|
29
|
+
case 'datetime':
|
|
30
|
+
return Primitives.datetime(lexicons, path, def, value)
|
|
31
|
+
case 'unknown':
|
|
32
|
+
return Primitives.unknown(lexicons, path, def, value)
|
|
33
|
+
case 'object':
|
|
34
|
+
return object(lexicons, path, def, value)
|
|
35
|
+
case 'array':
|
|
36
|
+
return array(lexicons, path, def, value)
|
|
37
|
+
case 'blob':
|
|
38
|
+
return Blob.blob(lexicons, path, def, value)
|
|
39
|
+
case 'image':
|
|
40
|
+
return Blob.image(lexicons, path, def, value)
|
|
41
|
+
case 'video':
|
|
42
|
+
return Blob.video(lexicons, path, def, value)
|
|
43
|
+
case 'audio':
|
|
44
|
+
return Blob.audio(lexicons, path, def, value)
|
|
45
|
+
default:
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
error: new ValidationError(`Unexpected lexicon type: ${def.type}`),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function array(
|
|
54
|
+
lexicons: Lexicons,
|
|
55
|
+
path: string,
|
|
56
|
+
def: LexUserType,
|
|
57
|
+
value: unknown,
|
|
58
|
+
): ValidationResult {
|
|
59
|
+
def = def as LexArray
|
|
60
|
+
|
|
61
|
+
// type
|
|
62
|
+
if (!Array.isArray(value)) {
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
error: new ValidationError(`${path} must be an array`),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// maxLength
|
|
70
|
+
if (typeof def.maxLength === 'number') {
|
|
71
|
+
if ((value as Array<unknown>).length > def.maxLength) {
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
error: new ValidationError(
|
|
75
|
+
`${path} must not have more than ${def.maxLength} elements`,
|
|
76
|
+
),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// minLength
|
|
82
|
+
if (typeof def.minLength === 'number') {
|
|
83
|
+
if ((value as Array<unknown>).length < def.minLength) {
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
error: new ValidationError(
|
|
87
|
+
`${path} must not have fewer than ${def.minLength} elements`,
|
|
88
|
+
),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// items
|
|
94
|
+
const itemsDef = def.items
|
|
95
|
+
for (let i = 0; i < (value as Array<unknown>).length; i++) {
|
|
96
|
+
const itemValue = value[i]
|
|
97
|
+
const itemPath = `${path}/${i}`
|
|
98
|
+
const res = validateOneOf(lexicons, itemPath, itemsDef, itemValue)
|
|
99
|
+
if (!res.success) {
|
|
100
|
+
return res
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { success: true }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function object(
|
|
108
|
+
lexicons: Lexicons,
|
|
109
|
+
path: string,
|
|
110
|
+
def: LexUserType,
|
|
111
|
+
value: unknown,
|
|
112
|
+
): ValidationResult {
|
|
113
|
+
def = def as LexObject
|
|
114
|
+
|
|
115
|
+
// type
|
|
116
|
+
if (!value || typeof value !== 'object') {
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: new ValidationError(`${path} must be an object`),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// required
|
|
124
|
+
if (Array.isArray(def.required)) {
|
|
125
|
+
for (const key of def.required) {
|
|
126
|
+
if (!(key in value)) {
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
error: new ValidationError(`${path} must have the property "${key}"`),
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// properties
|
|
136
|
+
if (typeof def.properties === 'object') {
|
|
137
|
+
for (const key in def.properties) {
|
|
138
|
+
const propValue = value[key]
|
|
139
|
+
if (typeof propValue === 'undefined') {
|
|
140
|
+
continue // skip- if required, will have already failed
|
|
141
|
+
}
|
|
142
|
+
const propDef = def.properties[key]
|
|
143
|
+
const propPath = `${path}/${key}`
|
|
144
|
+
const res = validateOneOf(lexicons, propPath, propDef, propValue)
|
|
145
|
+
if (!res.success) {
|
|
146
|
+
return res
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { success: true }
|
|
152
|
+
}
|