@atproto/lexicon 0.0.4 → 0.1.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/src/validation.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { Lexicons } from './lexicons'
2
- import { LexRecord, LexXrpcProcedure, LexXrpcQuery } from './types'
2
+ import {
3
+ LexRecord,
4
+ LexXrpcProcedure,
5
+ LexXrpcQuery,
6
+ LexXrpcSubscription,
7
+ } from './types'
3
8
  import { assertValidOneOf } from './util'
4
9
 
5
10
  import * as ComplexValidators from './validators/complex'
@@ -12,16 +17,18 @@ export function assertValidRecord(
12
17
  ) {
13
18
  const res = ComplexValidators.object(lexicons, 'Record', def.record, value)
14
19
  if (!res.success) throw res.error
20
+ return res.value
15
21
  }
16
22
 
17
23
  export function assertValidXrpcParams(
18
24
  lexicons: Lexicons,
19
- def: LexXrpcProcedure | LexXrpcQuery,
25
+ def: LexXrpcProcedure | LexXrpcQuery | LexXrpcSubscription,
20
26
  value: unknown,
21
27
  ) {
22
28
  if (def.parameters) {
23
29
  const res = XrpcValidators.params(lexicons, 'Params', def.parameters, value)
24
30
  if (!res.success) throw res.error
31
+ return res.value
25
32
  }
26
33
  }
27
34
 
@@ -32,7 +39,7 @@ export function assertValidXrpcInput(
32
39
  ) {
33
40
  if (def.input?.schema) {
34
41
  // loop: all input schema definitions
35
- assertValidOneOf(lexicons, 'Input', def.input.schema, value, true)
42
+ return assertValidOneOf(lexicons, 'Input', def.input.schema, value, true)
36
43
  }
37
44
  }
38
45
 
@@ -43,6 +50,23 @@ export function assertValidXrpcOutput(
43
50
  ) {
44
51
  if (def.output?.schema) {
45
52
  // loop: all output schema definitions
46
- assertValidOneOf(lexicons, 'Output', def.output.schema, value, true)
53
+ return assertValidOneOf(lexicons, 'Output', def.output.schema, value, true)
54
+ }
55
+ }
56
+
57
+ export function assertValidXrpcMessage(
58
+ lexicons: Lexicons,
59
+ def: LexXrpcSubscription,
60
+ value: unknown,
61
+ ) {
62
+ if (def.message?.schema) {
63
+ // loop: all output schema definitions
64
+ return assertValidOneOf(
65
+ lexicons,
66
+ 'Message',
67
+ def.message.schema,
68
+ value,
69
+ true,
70
+ )
47
71
  }
48
72
  }
@@ -1,6 +1,6 @@
1
+ import { BlobRef } from '../blob-refs'
1
2
  import { Lexicons } from '../lexicons'
2
3
  import { LexUserType, ValidationResult, ValidationError } from '../types'
3
- import { isObj, hasProp } from '../types'
4
4
 
5
5
  export function blob(
6
6
  lexicons: Lexicons,
@@ -8,50 +8,12 @@ export function blob(
8
8
  def: LexUserType,
9
9
  value: unknown,
10
10
  ): ValidationResult {
11
- if (!isObj(value)) {
11
+ // check
12
+ if (!value || !(value instanceof BlobRef)) {
12
13
  return {
13
14
  success: false,
14
- error: new ValidationError(`${path} should be an object`),
15
+ error: new ValidationError(`${path} should be a blob ref`),
15
16
  }
16
17
  }
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)
18
+ return { success: true, value }
57
19
  }
@@ -20,14 +20,16 @@ export function validate(
20
20
  switch (def.type) {
21
21
  case 'boolean':
22
22
  return Primitives.boolean(lexicons, path, def, value)
23
- case 'number':
24
- return Primitives.number(lexicons, path, def, value)
23
+ case 'float':
24
+ return Primitives.float(lexicons, path, def, value)
25
25
  case 'integer':
26
26
  return Primitives.integer(lexicons, path, def, value)
27
27
  case 'string':
28
28
  return Primitives.string(lexicons, path, def, value)
29
- case 'datetime':
30
- return Primitives.datetime(lexicons, path, def, value)
29
+ case 'bytes':
30
+ return Primitives.bytes(lexicons, path, def, value)
31
+ case 'cid-link':
32
+ return Primitives.cidLink(lexicons, path, def, value)
31
33
  case 'unknown':
32
34
  return Primitives.unknown(lexicons, path, def, value)
33
35
  case 'object':
@@ -36,12 +38,6 @@ export function validate(
36
38
  return array(lexicons, path, def, value)
37
39
  case 'blob':
38
40
  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
41
  default:
46
42
  return {
47
43
  success: false,
@@ -53,11 +49,9 @@ export function validate(
53
49
  export function array(
54
50
  lexicons: Lexicons,
55
51
  path: string,
56
- def: LexUserType,
52
+ def: LexArray,
57
53
  value: unknown,
58
54
  ): ValidationResult {
59
- def = def as LexArray
60
-
61
55
  // type
62
56
  if (!Array.isArray(value)) {
63
57
  return {
@@ -101,7 +95,7 @@ export function array(
101
95
  }
102
96
  }
103
97
 
104
- return { success: true }
98
+ return { success: true, value }
105
99
  }
106
100
 
107
101
  export function object(
@@ -120,33 +114,40 @@ export function object(
120
114
  }
121
115
  }
122
116
 
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
- }
117
+ const requiredProps = new Set(def.required)
118
+ const nullableProps = new Set(def.nullable)
134
119
 
135
120
  // properties
121
+ let resultValue = value
136
122
  if (typeof def.properties === 'object') {
137
123
  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
124
+ if (value[key] === null && nullableProps.has(key)) {
125
+ continue
141
126
  }
142
127
  const propDef = def.properties[key]
143
128
  const propPath = `${path}/${key}`
144
- const res = validateOneOf(lexicons, propPath, propDef, propValue)
145
- if (!res.success) {
146
- return res
129
+ const validated = validateOneOf(lexicons, propPath, propDef, value[key])
130
+ const propValue = validated.success ? validated.value : value[key]
131
+ const propIsUndefined = typeof propValue === 'undefined'
132
+ // Return error for bad validation, giving required rule precedence
133
+ if (propIsUndefined && requiredProps.has(key)) {
134
+ return {
135
+ success: false,
136
+ error: new ValidationError(`${path} must have the property "${key}"`),
137
+ }
138
+ } else if (!propIsUndefined && !validated.success) {
139
+ return validated
140
+ }
141
+ // Adjust value based on e.g. applied defaults, cloning shallowly if there was a changed value
142
+ if (propValue !== value[key]) {
143
+ if (resultValue === value) {
144
+ // Lazy shallow clone
145
+ resultValue = { ...value }
146
+ }
147
+ resultValue[key] = propValue
147
148
  }
148
149
  }
149
150
  }
150
151
 
151
- return { success: true }
152
+ return { success: true, value: resultValue }
152
153
  }
@@ -0,0 +1,107 @@
1
+ import { ensureValidAtUri } from '@atproto/uri'
2
+ import { isValidISODateString } from 'iso-datestring-validator'
3
+ import { CID } from 'multiformats/cid'
4
+ import { ValidationResult, ValidationError } from '../types'
5
+ import { ensureValidDid, ensureValidHandle } from '@atproto/identifier'
6
+ import { ensureValidNsid } from '@atproto/nsid'
7
+
8
+ export function datetime(path: string, value: string): ValidationResult {
9
+ try {
10
+ if (!isValidISODateString(value)) {
11
+ throw new Error()
12
+ }
13
+ } catch {
14
+ return {
15
+ success: false,
16
+ error: new ValidationError(
17
+ `${path} must be an iso8601 formatted datetime`,
18
+ ),
19
+ }
20
+ }
21
+ return { success: true, value }
22
+ }
23
+
24
+ export function uri(path: string, value: string): ValidationResult {
25
+ const isUri = value.match(/^\w+:(?:\/\/)?[^\s/][^\s]*$/) !== null
26
+ if (!isUri) {
27
+ return {
28
+ success: false,
29
+ error: new ValidationError(`${path} must be a uri`),
30
+ }
31
+ }
32
+ return { success: true, value }
33
+ }
34
+
35
+ export function atUri(path: string, value: string): ValidationResult {
36
+ try {
37
+ ensureValidAtUri(value)
38
+ } catch {
39
+ return {
40
+ success: false,
41
+ error: new ValidationError(`${path} must be a valid at-uri`),
42
+ }
43
+ }
44
+ return { success: true, value }
45
+ }
46
+
47
+ export function did(path: string, value: string): ValidationResult {
48
+ try {
49
+ ensureValidDid(value)
50
+ } catch {
51
+ return {
52
+ success: false,
53
+ error: new ValidationError(`${path} must be a valid did`),
54
+ }
55
+ }
56
+ return { success: true, value }
57
+ }
58
+
59
+ export function handle(path: string, value: string): ValidationResult {
60
+ try {
61
+ ensureValidHandle(value)
62
+ } catch {
63
+ return {
64
+ success: false,
65
+ error: new ValidationError(`${path} must be a valid handle`),
66
+ }
67
+ }
68
+ return { success: true, value }
69
+ }
70
+
71
+ export function atIdentifier(path: string, value: string): ValidationResult {
72
+ const isDid = did(path, value)
73
+ if (!isDid.success) {
74
+ const isHandle = handle(path, value)
75
+ if (!isHandle.success) {
76
+ return {
77
+ success: false,
78
+ error: new ValidationError(`${path} must be a valid did or a handle`),
79
+ }
80
+ }
81
+ }
82
+ return { success: true, value }
83
+ }
84
+
85
+ export function nsid(path: string, value: string): ValidationResult {
86
+ try {
87
+ ensureValidNsid(value)
88
+ } catch {
89
+ return {
90
+ success: false,
91
+ error: new ValidationError(`${path} must be a valid nsid`),
92
+ }
93
+ }
94
+ return { success: true, value }
95
+ }
96
+
97
+ export function cid(path: string, value: string): ValidationResult {
98
+ try {
99
+ CID.parse(value)
100
+ } catch {
101
+ return {
102
+ success: false,
103
+ error: new ValidationError(`${path} must be a cid string`),
104
+ }
105
+ }
106
+ return { success: true, value }
107
+ }
@@ -1,14 +1,16 @@
1
- import { isValidISODateString } from 'iso-datestring-validator'
1
+ import { utf8Len, graphemeLen } from '@atproto/common-web'
2
+ import { CID } from 'multiformats/cid'
2
3
  import { Lexicons } from '../lexicons'
4
+ import * as formats from './formats'
3
5
  import {
4
6
  LexUserType,
5
7
  LexBoolean,
6
- LexNumber,
8
+ LexFloat,
7
9
  LexInteger,
8
10
  LexString,
9
- LexDatetime,
10
11
  ValidationResult,
11
12
  ValidationError,
13
+ LexBytes,
12
14
  } from '../types'
13
15
 
14
16
  export function validate(
@@ -20,14 +22,16 @@ export function validate(
20
22
  switch (def.type) {
21
23
  case 'boolean':
22
24
  return boolean(lexicons, path, def, value)
23
- case 'number':
24
- return number(lexicons, path, def, value)
25
+ case 'float':
26
+ return float(lexicons, path, def, value)
25
27
  case 'integer':
26
28
  return integer(lexicons, path, def, value)
27
29
  case 'string':
28
30
  return string(lexicons, path, def, value)
29
- case 'datetime':
30
- return datetime(lexicons, path, def, value)
31
+ case 'bytes':
32
+ return bytes(lexicons, path, def, value)
33
+ case 'cid-link':
34
+ return cidLink(lexicons, path, def, value)
31
35
  case 'unknown':
32
36
  return unknown(lexicons, path, def, value)
33
37
  default:
@@ -48,9 +52,9 @@ export function boolean(
48
52
 
49
53
  // type
50
54
  const type = typeof value
51
- if (type == 'undefined') {
55
+ if (type === 'undefined') {
52
56
  if (typeof def.default === 'boolean') {
53
- return { success: true }
57
+ return { success: true, value: def.default }
54
58
  }
55
59
  return {
56
60
  success: false,
@@ -73,22 +77,22 @@ export function boolean(
73
77
  }
74
78
  }
75
79
 
76
- return { success: true }
80
+ return { success: true, value }
77
81
  }
78
82
 
79
- export function number(
83
+ export function float(
80
84
  lexicons: Lexicons,
81
85
  path: string,
82
86
  def: LexUserType,
83
87
  value: unknown,
84
88
  ): ValidationResult {
85
- def = def as LexNumber
89
+ def = def as LexFloat
86
90
 
87
91
  // type
88
92
  const type = typeof value
89
- if (type == 'undefined') {
93
+ if (type === 'undefined') {
90
94
  if (typeof def.default === 'number') {
91
- return { success: true }
95
+ return { success: true, value: def.default }
92
96
  }
93
97
  return {
94
98
  success: false,
@@ -147,7 +151,7 @@ export function number(
147
151
  }
148
152
  }
149
153
 
150
- return { success: true }
154
+ return { success: true, value }
151
155
  }
152
156
 
153
157
  export function integer(
@@ -159,9 +163,11 @@ export function integer(
159
163
  def = def as LexInteger
160
164
 
161
165
  // run number validation
162
- const numRes = number(lexicons, path, def, value)
166
+ const numRes = float(lexicons, path, def, value)
163
167
  if (!numRes.success) {
164
168
  return numRes
169
+ } else {
170
+ value = numRes.value
165
171
  }
166
172
 
167
173
  // whole numbers only
@@ -172,7 +178,7 @@ export function integer(
172
178
  }
173
179
  }
174
180
 
175
- return { success: true }
181
+ return { success: true, value }
176
182
  }
177
183
 
178
184
  export function string(
@@ -184,16 +190,15 @@ export function string(
184
190
  def = def as LexString
185
191
 
186
192
  // type
187
- const type = typeof value
188
- if (type == 'undefined') {
193
+ if (typeof value === 'undefined') {
189
194
  if (typeof def.default === 'string') {
190
- return { success: true }
195
+ return { success: true, value: def.default }
191
196
  }
192
197
  return {
193
198
  success: false,
194
199
  error: new ValidationError(`${path} must be a string`),
195
200
  }
196
- } else if (type !== 'string') {
201
+ } else if (typeof value !== 'string') {
197
202
  return {
198
203
  success: false,
199
204
  error: new ValidationError(`${path} must be a string`),
@@ -224,7 +229,7 @@ export function string(
224
229
 
225
230
  // maxLength
226
231
  if (typeof def.maxLength === 'number') {
227
- if ((value as string).length > def.maxLength) {
232
+ if (utf8Len(value) > def.maxLength) {
228
233
  return {
229
234
  success: false,
230
235
  error: new ValidationError(
@@ -236,7 +241,7 @@ export function string(
236
241
 
237
242
  // minLength
238
243
  if (typeof def.minLength === 'number') {
239
- if ((value as string).length < def.minLength) {
244
+ if (utf8Len(value) < def.minLength) {
240
245
  return {
241
246
  success: false,
242
247
  error: new ValidationError(
@@ -246,40 +251,110 @@ export function string(
246
251
  }
247
252
  }
248
253
 
249
- return { success: true }
254
+ // maxGraphemes
255
+ if (typeof def.maxGraphemes === 'number') {
256
+ if (graphemeLen(value) > def.maxGraphemes) {
257
+ return {
258
+ success: false,
259
+ error: new ValidationError(
260
+ `${path} must not be longer than ${def.maxGraphemes} graphemes`,
261
+ ),
262
+ }
263
+ }
264
+ }
265
+
266
+ // minGraphemes
267
+ if (typeof def.minGraphemes === 'number') {
268
+ if (graphemeLen(value) < def.minGraphemes) {
269
+ return {
270
+ success: false,
271
+ error: new ValidationError(
272
+ `${path} must not be shorter than ${def.minGraphemes} graphemes`,
273
+ ),
274
+ }
275
+ }
276
+ }
277
+
278
+ if (typeof def.format === 'string') {
279
+ switch (def.format) {
280
+ case 'datetime':
281
+ return formats.datetime(path, value)
282
+ case 'uri':
283
+ return formats.uri(path, value)
284
+ case 'at-uri':
285
+ return formats.atUri(path, value)
286
+ case 'did':
287
+ return formats.did(path, value)
288
+ case 'handle':
289
+ return formats.handle(path, value)
290
+ case 'at-identifier':
291
+ return formats.atIdentifier(path, value)
292
+ case 'nsid':
293
+ return formats.nsid(path, value)
294
+ case 'cid':
295
+ return formats.cid(path, value)
296
+ }
297
+ }
298
+
299
+ return { success: true, value }
250
300
  }
251
301
 
252
- export function datetime(
302
+ export function bytes(
253
303
  lexicons: Lexicons,
254
304
  path: string,
255
305
  def: LexUserType,
256
306
  value: unknown,
257
307
  ): ValidationResult {
258
- def = def as LexDatetime
308
+ def = def as LexBytes
259
309
 
260
- // type
261
- const type = typeof value
262
- if (type !== 'string') {
310
+ if (!value || !(value instanceof Uint8Array)) {
263
311
  return {
264
312
  success: false,
265
- error: new ValidationError(`${path} must be a string`),
313
+ error: new ValidationError(`${path} must be a byte array`),
266
314
  }
267
315
  }
268
316
 
269
- // valid iso-8601
270
- {
271
- try {
272
- if (typeof value !== 'string' || !isValidISODateString(value)) {
273
- throw new ValidationError(
274
- `${path} must be an iso8601 formatted datetime`,
275
- )
317
+ // maxLength
318
+ if (typeof def.maxLength === 'number') {
319
+ if (value.byteLength > def.maxLength) {
320
+ return {
321
+ success: false,
322
+ error: new ValidationError(
323
+ `${path} must not be larger than ${def.maxLength} bytes`,
324
+ ),
325
+ }
326
+ }
327
+ }
328
+
329
+ // minLength
330
+ if (typeof def.minLength === 'number') {
331
+ if (value.byteLength < def.minLength) {
332
+ return {
333
+ success: false,
334
+ error: new ValidationError(
335
+ `${path} must not be smaller than ${def.minLength} bytes`,
336
+ ),
276
337
  }
277
- } catch {
278
- throw new ValidationError(`${path} must be an iso8601 formatted datetime`)
279
338
  }
280
339
  }
281
340
 
282
- return { success: true }
341
+ return { success: true, value }
342
+ }
343
+
344
+ export function cidLink(
345
+ lexicons: Lexicons,
346
+ path: string,
347
+ def: LexUserType,
348
+ value: unknown,
349
+ ): ValidationResult {
350
+ if (CID.asCID(value) === null) {
351
+ return {
352
+ success: false,
353
+ error: new ValidationError(`${path} must be a CID`),
354
+ }
355
+ }
356
+
357
+ return { success: true, value }
283
358
  }
284
359
 
285
360
  export function unknown(
@@ -296,5 +371,5 @@ export function unknown(
296
371
  }
297
372
  }
298
373
 
299
- return { success: true }
374
+ return { success: true, value }
300
375
  }