@atproto/lexicon 0.0.4 → 0.2.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.
Files changed (47) hide show
  1. package/dist/blob-refs.d.ts +67 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +12944 -680
  4. package/dist/index.js.map +4 -4
  5. package/dist/lexicons.d.ts +6 -4
  6. package/dist/serialize.d.ts +12 -0
  7. package/dist/src/index.d.ts +2 -0
  8. package/dist/src/lexicons.d.ts +15 -0
  9. package/dist/src/record/index.d.ts +4 -0
  10. package/dist/src/record/schema.d.ts +9 -0
  11. package/dist/src/record/schemas.d.ts +10 -0
  12. package/dist/src/record/util.d.ts +1 -0
  13. package/dist/src/record/validation.d.ts +24 -0
  14. package/dist/src/record/validator.d.ts +17 -0
  15. package/dist/src/types.d.ts +30268 -0
  16. package/dist/src/util.d.ts +6 -0
  17. package/dist/src/validation.d.ts +6 -0
  18. package/dist/src/validators/blob.d.ts +6 -0
  19. package/dist/src/validators/complex.d.ts +5 -0
  20. package/dist/src/validators/primitives.d.ts +9 -0
  21. package/dist/src/validators/xrpc.d.ts +3 -0
  22. package/dist/tsconfig.build.tsbuildinfo +1 -0
  23. package/dist/types.d.ts +14999 -23510
  24. package/dist/util.d.ts +6 -1
  25. package/dist/validation.d.ts +6 -5
  26. package/dist/validators/blob.d.ts +0 -3
  27. package/dist/validators/complex.d.ts +2 -2
  28. package/dist/validators/formats.d.ts +10 -0
  29. package/dist/validators/primitives.d.ts +2 -2
  30. package/dist/validators/xrpc.d.ts +1 -1
  31. package/package.json +13 -4
  32. package/src/blob-refs.ts +70 -0
  33. package/src/index.ts +2 -0
  34. package/src/lexicons.ts +36 -5
  35. package/src/serialize.ts +93 -0
  36. package/src/types.ts +306 -180
  37. package/src/util.ts +64 -5
  38. package/src/validation.ts +28 -4
  39. package/src/validators/blob.ts +5 -43
  40. package/src/validators/complex.ts +31 -32
  41. package/src/validators/formats.ts +121 -0
  42. package/src/validators/primitives.ts +114 -67
  43. package/src/validators/xrpc.ts +29 -29
  44. package/tests/_scaffolds/lexicons.ts +178 -51
  45. package/tests/general.test.ts +496 -177
  46. package/tsconfig.build.tsbuildinfo +1 -1
  47. package/tsconfig.json +3 -1
@@ -1,4 +1,6 @@
1
- import { Lexicons } from '../src/index'
1
+ import { CID } from 'multiformats/cid'
2
+ import { lexiconDoc, Lexicons } from '../src/index'
3
+ import { object } from '../src/validators/complex'
2
4
  import LexiconDocs from './_scaffolds/lexicons'
3
5
 
4
6
  describe('Lexicons collection', () => {
@@ -40,22 +42,28 @@ describe('General validation', () => {
40
42
  object: { boolean: true },
41
43
  array: ['one', 'two'],
42
44
  boolean: true,
43
- number: 123.45,
44
45
  integer: 123,
45
46
  string: 'string',
46
47
  },
47
48
  array: ['one', 'two'],
48
49
  boolean: true,
49
- number: 123.45,
50
50
  integer: 123,
51
51
  string: 'string',
52
52
  datetime: new Date().toISOString(),
53
+ atUri: 'at://did:web:example.com/com.example.test/self',
54
+ did: 'did:web:example.com',
55
+ cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
56
+ bytes: new Uint8Array([0, 1, 2, 3]),
57
+ cidLink: CID.parse(
58
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
59
+ ),
53
60
  })
54
61
  expect(res.success).toBe(true)
55
62
  }
56
63
  {
57
64
  const res = lex.validate('com.example.kitchenSink', {})
58
65
  expect(res.success).toBe(false)
66
+ if (res.success) throw new Error('Asserted')
59
67
  expect(res.error?.message).toBe('Record must have the property "object"')
60
68
  }
61
69
  })
@@ -65,7 +73,6 @@ describe('General validation', () => {
65
73
  object: { boolean: true },
66
74
  array: ['one', 'two'],
67
75
  boolean: true,
68
- number: 123.45,
69
76
  integer: 123,
70
77
  string: 'string',
71
78
  })
@@ -74,32 +81,206 @@ describe('General validation', () => {
74
81
  {
75
82
  const res = lex.validate('com.example.kitchenSink#object', {})
76
83
  expect(res.success).toBe(false)
84
+ if (res.success) throw new Error('Asserted')
77
85
  expect(res.error?.message).toBe('Object must have the property "object"')
78
86
  }
79
87
  })
88
+ it('fails when a required property is missing', () => {
89
+ const schema = {
90
+ lexicon: 1,
91
+ id: 'com.example.kitchenSink',
92
+ defs: {
93
+ test: {
94
+ type: 'object',
95
+ required: ['foo'],
96
+ properties: {},
97
+ },
98
+ },
99
+ }
100
+ expect(() => {
101
+ lexiconDoc.parse(schema)
102
+ }).toThrow('Required field \\"foo\\" not defined')
103
+ })
104
+ it('fails when unknown fields are present', () => {
105
+ const schema = {
106
+ lexicon: 1,
107
+ id: 'com.example.unknownFields',
108
+ defs: {
109
+ test: {
110
+ type: 'object',
111
+ foo: 3,
112
+ },
113
+ },
114
+ }
115
+
116
+ expect(() => {
117
+ lexiconDoc.parse(schema)
118
+ }).toThrow("Unrecognized key(s) in object: 'foo'")
119
+ })
120
+ it('fails lexicon parsing when uri is invalid', () => {
121
+ const schema = {
122
+ lexicon: 1,
123
+ id: 'com.example.invalidUri',
124
+ defs: {
125
+ main: {
126
+ type: 'object',
127
+ properties: {
128
+ test: { type: 'ref', ref: 'com.example.invalid#test#test' },
129
+ },
130
+ },
131
+ },
132
+ }
133
+
134
+ expect(() => {
135
+ new Lexicons([schema])
136
+ }).toThrow('Uri can only have one hash segment')
137
+ })
138
+ it('fails validation when ref uri has multiple hash segments', () => {
139
+ const schema = {
140
+ lexicon: 1,
141
+ id: 'com.example.invalidUri',
142
+ defs: {
143
+ main: {
144
+ type: 'object',
145
+ properties: {
146
+ test: { type: 'integer' },
147
+ },
148
+ },
149
+ object: {
150
+ type: 'object',
151
+ required: ['test'],
152
+ properties: {
153
+ test: {
154
+ type: 'union',
155
+ refs: ['com.example.invalidUri'],
156
+ },
157
+ },
158
+ },
159
+ },
160
+ }
161
+ const lexicons = new Lexicons([schema])
162
+ expect(() => {
163
+ lexicons.validate('com.example.invalidUri#object', {
164
+ test: {
165
+ $type: 'com.example.invalidUri#main#main',
166
+ test: 123,
167
+ },
168
+ })
169
+ }).toThrow('Uri can only have one hash segment')
170
+ })
171
+ it('union handles both implicit and explicit #main', () => {
172
+ const schemas = [
173
+ {
174
+ lexicon: 1,
175
+ id: 'com.example.implicitMain',
176
+ defs: {
177
+ main: {
178
+ type: 'object',
179
+ required: ['test'],
180
+ properties: {
181
+ test: { type: 'string' },
182
+ },
183
+ },
184
+ },
185
+ },
186
+ {
187
+ lexicon: 1,
188
+ id: 'com.example.testImplicitMain',
189
+ defs: {
190
+ main: {
191
+ type: 'object',
192
+ required: ['union'],
193
+ properties: {
194
+ union: {
195
+ type: 'union',
196
+ refs: ['com.example.implicitMain'],
197
+ },
198
+ },
199
+ },
200
+ },
201
+ },
202
+ {
203
+ lexicon: 1,
204
+ id: 'com.example.testExplicitMain',
205
+ defs: {
206
+ main: {
207
+ type: 'object',
208
+ required: ['union'],
209
+ properties: {
210
+ union: {
211
+ type: 'union',
212
+ refs: ['com.example.implicitMain#main'],
213
+ },
214
+ },
215
+ },
216
+ },
217
+ },
218
+ ]
219
+
220
+ const lexicon = new Lexicons(schemas)
221
+
222
+ let result = lexicon.validate('com.example.testImplicitMain', {
223
+ union: {
224
+ $type: 'com.example.implicitMain',
225
+ test: 123,
226
+ },
227
+ })
228
+ expect(result.success).toBeFalsy()
229
+ expect(result['error']?.message).toBe('Object/union/test must be a string')
230
+
231
+ result = lexicon.validate('com.example.testImplicitMain', {
232
+ union: {
233
+ $type: 'com.example.implicitMain#main',
234
+ test: 123,
235
+ },
236
+ })
237
+ expect(result.success).toBeFalsy()
238
+ expect(result['error']?.message).toBe('Object/union/test must be a string')
239
+
240
+ result = lexicon.validate('com.example.testExplicitMain', {
241
+ union: {
242
+ $type: 'com.example.implicitMain',
243
+ test: 123,
244
+ },
245
+ })
246
+ expect(result.success).toBeFalsy()
247
+ expect(result['error']?.message).toBe('Object/union/test must be a string')
248
+
249
+ result = lexicon.validate('com.example.testExplicitMain', {
250
+ union: {
251
+ $type: 'com.example.implicitMain#main',
252
+ test: 123,
253
+ },
254
+ })
255
+ expect(result.success).toBeFalsy()
256
+ expect(result['error']?.message).toBe('Object/union/test must be a string')
257
+ })
80
258
  })
81
259
 
82
260
  describe('Record validation', () => {
83
261
  const lex = new Lexicons(LexiconDocs)
84
262
 
85
- it('Passes valid schemas', () => {
86
- lex.assertValidRecord('com.example.kitchenSink', {
87
- $type: 'com.example.kitchenSink',
88
- object: {
89
- object: { boolean: true },
90
- array: ['one', 'two'],
91
- boolean: true,
92
- number: 123.45,
93
- integer: 123,
94
- string: 'string',
95
- },
263
+ const passingSink = {
264
+ $type: 'com.example.kitchenSink',
265
+ object: {
266
+ object: { boolean: true },
96
267
  array: ['one', 'two'],
97
268
  boolean: true,
98
- number: 123.45,
99
269
  integer: 123,
100
270
  string: 'string',
101
- datetime: new Date().toISOString(),
102
- })
271
+ },
272
+ array: ['one', 'two'],
273
+ boolean: true,
274
+ integer: 123,
275
+ string: 'string',
276
+ bytes: new Uint8Array([0, 1, 2, 3]),
277
+ cidLink: CID.parse(
278
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
279
+ ),
280
+ }
281
+
282
+ it('Passes valid schemas', () => {
283
+ lex.assertValidRecord('com.example.kitchenSink', passingSink)
103
284
  })
104
285
 
105
286
  it('Fails invalid input types', () => {
@@ -129,10 +310,22 @@ describe('Record validation', () => {
129
310
  $type: 'com.example.kitchenSink',
130
311
  array: ['one', 'two'],
131
312
  boolean: true,
132
- number: 123.45,
133
313
  integer: 123,
134
314
  string: 'string',
135
315
  datetime: new Date().toISOString(),
316
+ atUri: 'at://did:web:example.com/com.example.test/self',
317
+ did: 'did:web:example.com',
318
+ cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
319
+ bytes: new Uint8Array([0, 1, 2, 3]),
320
+ cidLink: CID.parse(
321
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
322
+ ),
323
+ }),
324
+ ).toThrow('Record must have the property "object"')
325
+ expect(() =>
326
+ lex.assertValidRecord('com.example.kitchenSink', {
327
+ ...passingSink,
328
+ object: undefined,
136
329
  }),
137
330
  ).toThrow('Record must have the property "object"')
138
331
  })
@@ -140,130 +333,49 @@ describe('Record validation', () => {
140
333
  it('Fails incorrect types', () => {
141
334
  expect(() =>
142
335
  lex.assertValidRecord('com.example.kitchenSink', {
143
- $type: 'com.example.kitchenSink',
336
+ ...passingSink,
144
337
  object: {
338
+ ...passingSink.object,
145
339
  object: { boolean: '1234' },
146
- array: ['one', 'two'],
147
- boolean: true,
148
- number: 123.45,
149
- integer: 123,
150
- string: 'string',
151
340
  },
152
- array: ['one', 'two'],
153
- boolean: true,
154
- number: 123.45,
155
- integer: 123,
156
- string: 'string',
157
- datetime: new Date().toISOString(),
158
341
  }),
159
342
  ).toThrow('Record/object/object/boolean must be a boolean')
160
343
  expect(() =>
161
344
  lex.assertValidRecord('com.example.kitchenSink', {
162
- $type: 'com.example.kitchenSink',
345
+ ...passingSink,
163
346
  object: true,
164
- array: ['one', 'two'],
165
- boolean: true,
166
- number: 123.45,
167
- integer: 123,
168
- string: 'string',
169
- datetime: new Date().toISOString(),
170
347
  }),
171
348
  ).toThrow('Record/object must be an object')
172
349
  expect(() =>
173
350
  lex.assertValidRecord('com.example.kitchenSink', {
174
- $type: 'com.example.kitchenSink',
175
- object: {
176
- object: { boolean: true },
177
- array: ['one', 'two'],
178
- boolean: true,
179
- number: 123.45,
180
- integer: 123,
181
- string: 'string',
182
- },
351
+ ...passingSink,
183
352
  array: 1234,
184
- boolean: true,
185
- number: 123.45,
186
- integer: 123,
187
- string: 'string',
188
- datetime: new Date().toISOString(),
189
353
  }),
190
354
  ).toThrow('Record/array must be an array')
191
355
  expect(() =>
192
356
  lex.assertValidRecord('com.example.kitchenSink', {
193
- $type: 'com.example.kitchenSink',
194
- object: {
195
- object: { boolean: true },
196
- array: ['one', 'two'],
197
- boolean: true,
198
- number: 123.45,
199
- integer: 123,
200
- string: 'string',
201
- },
202
- array: ['one', 'two'],
203
- boolean: true,
204
- number: 'string',
205
- integer: 123,
206
- string: 'string',
207
- datetime: new Date().toISOString(),
208
- }),
209
- ).toThrow('Record/number must be a number')
210
- expect(() =>
211
- lex.assertValidRecord('com.example.kitchenSink', {
212
- $type: 'com.example.kitchenSink',
213
- object: {
214
- object: { boolean: true },
215
- array: ['one', 'two'],
216
- boolean: true,
217
- number: 123.45,
218
- integer: 123,
219
- string: 'string',
220
- },
221
- array: ['one', 'two'],
222
- boolean: true,
223
- number: 123.45,
357
+ ...passingSink,
224
358
  integer: true,
225
- string: 'string',
226
- datetime: new Date().toISOString(),
227
359
  }),
228
- ).toThrow('Record/integer must be a number')
360
+ ).toThrow('Record/integer must be an integer')
229
361
  expect(() =>
230
362
  lex.assertValidRecord('com.example.kitchenSink', {
231
- $type: 'com.example.kitchenSink',
232
- object: {
233
- object: { boolean: true },
234
- array: ['one', 'two'],
235
- boolean: true,
236
- number: 123.45,
237
- integer: 123,
238
- string: 'string',
239
- },
240
- array: ['one', 'two'],
241
- boolean: true,
242
- number: 123.45,
243
- integer: 123,
363
+ ...passingSink,
244
364
  string: {},
245
- datetime: new Date().toISOString(),
246
365
  }),
247
366
  ).toThrow('Record/string must be a string')
248
367
  expect(() =>
249
368
  lex.assertValidRecord('com.example.kitchenSink', {
250
- $type: 'com.example.kitchenSink',
251
- object: {
252
- object: { boolean: true },
253
- array: ['one', 'two'],
254
- boolean: true,
255
- number: 123.45,
256
- integer: 123,
257
- string: 'string',
258
- },
259
- array: ['one', 'two'],
260
- boolean: true,
261
- number: 123.45,
262
- integer: 123,
263
- string: 'string',
264
- datetime: 1234,
369
+ ...passingSink,
370
+ bytes: 1234,
265
371
  }),
266
- ).toThrow('Record/datetime must be a string')
372
+ ).toThrow('Record/bytes must be a byte array')
373
+ expect(() =>
374
+ lex.assertValidRecord('com.example.kitchenSink', {
375
+ ...passingSink,
376
+ cidLink: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
377
+ }),
378
+ ).toThrow('Record/cidLink must be a CID')
267
379
  })
268
380
 
269
381
  it('Handles optional properties correctly', () => {
@@ -272,6 +384,25 @@ describe('Record validation', () => {
272
384
  })
273
385
  })
274
386
 
387
+ it('Handles default properties correctly', () => {
388
+ const result = lex.assertValidRecord('com.example.default', {
389
+ $type: 'com.example.default',
390
+ object: {},
391
+ })
392
+ expect(result).toEqual({
393
+ $type: 'com.example.default',
394
+ boolean: false,
395
+ integer: 0,
396
+ string: '',
397
+ object: {
398
+ boolean: true,
399
+ integer: 1,
400
+ string: 'x',
401
+ },
402
+ })
403
+ expect(result).not.toHaveProperty('datetime')
404
+ })
405
+
275
406
  it('Handles unions correctly', () => {
276
407
  lex.assertValidRecord('com.example.union', {
277
408
  $type: 'com.example.union',
@@ -280,7 +411,6 @@ describe('Record validation', () => {
280
411
  object: { boolean: true },
281
412
  array: ['one', 'two'],
282
413
  boolean: true,
283
- number: 123.45,
284
414
  integer: 123,
285
415
  string: 'string',
286
416
  },
@@ -355,6 +485,21 @@ describe('Record validation', () => {
355
485
  ).toThrow('Record/array must not have more than 4 elements')
356
486
  })
357
487
 
488
+ it('Applies array item constraints', () => {
489
+ expect(() =>
490
+ lex.assertValidRecord('com.example.arrayLength', {
491
+ $type: 'com.example.arrayLength',
492
+ array: [1, '2', 3],
493
+ }),
494
+ ).toThrow('Record/array/1 must be an integer')
495
+ expect(() =>
496
+ lex.assertValidRecord('com.example.arrayLength', {
497
+ $type: 'com.example.arrayLength',
498
+ array: [1, undefined, 3],
499
+ }),
500
+ ).toThrow('Record/array/1 must be an integer')
501
+ })
502
+
358
503
  it('Applies boolean const constraint', () => {
359
504
  lex.assertValidRecord('com.example.boolConst', {
360
505
  $type: 'com.example.boolConst',
@@ -368,51 +513,6 @@ describe('Record validation', () => {
368
513
  ).toThrow('Record/boolean must be false')
369
514
  })
370
515
 
371
- it('Applies number range constraint', () => {
372
- lex.assertValidRecord('com.example.numberRange', {
373
- $type: 'com.example.numberRange',
374
- number: 2.5,
375
- })
376
- expect(() =>
377
- lex.assertValidRecord('com.example.numberRange', {
378
- $type: 'com.example.numberRange',
379
- number: 1,
380
- }),
381
- ).toThrow('Record/number can not be less than 2')
382
- expect(() =>
383
- lex.assertValidRecord('com.example.numberRange', {
384
- $type: 'com.example.numberRange',
385
- number: 5,
386
- }),
387
- ).toThrow('Record/number can not be greater than 4')
388
- })
389
-
390
- it('Applies number enum constraint', () => {
391
- lex.assertValidRecord('com.example.numberEnum', {
392
- $type: 'com.example.numberEnum',
393
- number: 1.5,
394
- })
395
- expect(() =>
396
- lex.assertValidRecord('com.example.numberEnum', {
397
- $type: 'com.example.numberEnum',
398
- number: 0,
399
- }),
400
- ).toThrow('Record/number must be one of (1|1.5|2)')
401
- })
402
-
403
- it('Applies number const constraint', () => {
404
- lex.assertValidRecord('com.example.numberConst', {
405
- $type: 'com.example.numberConst',
406
- number: 0,
407
- })
408
- expect(() =>
409
- lex.assertValidRecord('com.example.numberConst', {
410
- $type: 'com.example.numberConst',
411
- number: 1,
412
- }),
413
- ).toThrow('Record/number must be 0')
414
- })
415
-
416
516
  it('Applies integer range constraint', () => {
417
517
  lex.assertValidRecord('com.example.integerRange', {
418
518
  $type: 'com.example.integerRange',
@@ -484,6 +584,31 @@ describe('Record validation', () => {
484
584
  string: '12345',
485
585
  }),
486
586
  ).toThrow('Record/string must not be longer than 4 characters')
587
+ expect(() =>
588
+ lex.assertValidRecord('com.example.stringLength', {
589
+ $type: 'com.example.stringLength',
590
+ string: '👨‍👩‍👧‍👧',
591
+ }),
592
+ ).toThrow('Record/string must not be longer than 4 characters')
593
+ })
594
+
595
+ it('Applies grapheme string length constraint', () => {
596
+ lex.assertValidRecord('com.example.stringLengthGrapheme', {
597
+ $type: 'com.example.stringLengthGrapheme',
598
+ string: '12👨‍👩‍👧‍👧',
599
+ })
600
+ expect(() =>
601
+ lex.assertValidRecord('com.example.stringLengthGrapheme', {
602
+ $type: 'com.example.stringLengthGrapheme',
603
+ string: '👨‍👩‍👧‍👧',
604
+ }),
605
+ ).toThrow('Record/string must not be shorter than 2 graphemes')
606
+ expect(() =>
607
+ lex.assertValidRecord('com.example.stringLengthGrapheme', {
608
+ $type: 'com.example.stringLengthGrapheme',
609
+ string: '12345',
610
+ }),
611
+ ).toThrow('Record/string must not be longer than 4 graphemes')
487
612
  })
488
613
 
489
614
  it('Applies string enum constraint', () => {
@@ -537,36 +662,230 @@ describe('Record validation', () => {
537
662
  }),
538
663
  ).toThrow('Record/datetime must be an iso8601 formatted datetime')
539
664
  })
665
+
666
+ it('Applies uri formatting constraint', () => {
667
+ for (const uri of [
668
+ 'https://example.com',
669
+ 'https://example.com/with/path',
670
+ 'https://example.com/with/path?and=query',
671
+ 'at://bsky.social',
672
+ 'did:example:test',
673
+ ]) {
674
+ lex.assertValidRecord('com.example.uri', {
675
+ $type: 'com.example.uri',
676
+ uri,
677
+ })
678
+ }
679
+ expect(() =>
680
+ lex.assertValidRecord('com.example.uri', {
681
+ $type: 'com.example.uri',
682
+ uri: 'not a uri',
683
+ }),
684
+ ).toThrow('Record/uri must be a uri')
685
+ })
686
+
687
+ it('Applies at-uri formatting constraint', () => {
688
+ lex.assertValidRecord('com.example.atUri', {
689
+ $type: 'com.example.atUri',
690
+ atUri: 'at://did:web:example.com/com.example.test/self',
691
+ })
692
+ expect(() =>
693
+ lex.assertValidRecord('com.example.atUri', {
694
+ $type: 'com.example.atUri',
695
+ atUri: 'http://not-atproto.com',
696
+ }),
697
+ ).toThrow('Record/atUri must be a valid at-uri')
698
+ })
699
+
700
+ it('Applies did formatting constraint', () => {
701
+ lex.assertValidRecord('com.example.did', {
702
+ $type: 'com.example.did',
703
+ did: 'did:web:example.com',
704
+ })
705
+ lex.assertValidRecord('com.example.did', {
706
+ $type: 'com.example.did',
707
+ did: 'did:plc:12345678abcdefghijklmnop',
708
+ })
709
+
710
+ expect(() =>
711
+ lex.assertValidRecord('com.example.did', {
712
+ $type: 'com.example.did',
713
+ did: 'bad did',
714
+ }),
715
+ ).toThrow('Record/did must be a valid did')
716
+ expect(() =>
717
+ lex.assertValidRecord('com.example.did', {
718
+ $type: 'com.example.did',
719
+ did: 'did:short',
720
+ }),
721
+ ).toThrow('Record/did must be a valid did')
722
+ })
723
+
724
+ it('Applies handle formatting constraint', () => {
725
+ lex.assertValidRecord('com.example.handle', {
726
+ $type: 'com.example.handle',
727
+ handle: 'test.bsky.social',
728
+ })
729
+ lex.assertValidRecord('com.example.handle', {
730
+ $type: 'com.example.handle',
731
+ handle: 'bsky.test',
732
+ })
733
+
734
+ expect(() =>
735
+ lex.assertValidRecord('com.example.handle', {
736
+ $type: 'com.example.handle',
737
+ handle: 'bad handle',
738
+ }),
739
+ ).toThrow('Record/handle must be a valid handle')
740
+ expect(() =>
741
+ lex.assertValidRecord('com.example.handle', {
742
+ $type: 'com.example.handle',
743
+ handle: '-bad-.test',
744
+ }),
745
+ ).toThrow('Record/handle must be a valid handle')
746
+ })
747
+
748
+ it('Applies at-identifier formatting constraint', () => {
749
+ lex.assertValidRecord('com.example.atIdentifier', {
750
+ $type: 'com.example.atIdentifier',
751
+ atIdentifier: 'bsky.test',
752
+ })
753
+ lex.assertValidRecord('com.example.atIdentifier', {
754
+ $type: 'com.example.atIdentifier',
755
+ atIdentifier: 'did:plc:12345678abcdefghijklmnop',
756
+ })
757
+
758
+ expect(() =>
759
+ lex.assertValidRecord('com.example.atIdentifier', {
760
+ $type: 'com.example.atIdentifier',
761
+ atIdentifier: 'bad id',
762
+ }),
763
+ ).toThrow('Record/atIdentifier must be a valid did or a handle')
764
+ expect(() =>
765
+ lex.assertValidRecord('com.example.atIdentifier', {
766
+ $type: 'com.example.atIdentifier',
767
+ atIdentifier: '-bad-.test',
768
+ }),
769
+ ).toThrow('Record/atIdentifier must be a valid did or a handle')
770
+ })
771
+
772
+ it('Applies nsid formatting constraint', () => {
773
+ lex.assertValidRecord('com.example.nsid', {
774
+ $type: 'com.example.nsid',
775
+ nsid: 'com.atproto.test',
776
+ })
777
+ lex.assertValidRecord('com.example.nsid', {
778
+ $type: 'com.example.nsid',
779
+ nsid: 'app.bsky.nested.test',
780
+ })
781
+
782
+ expect(() =>
783
+ lex.assertValidRecord('com.example.nsid', {
784
+ $type: 'com.example.nsid',
785
+ nsid: 'bad nsid',
786
+ }),
787
+ ).toThrow('Record/nsid must be a valid nsid')
788
+ expect(() =>
789
+ lex.assertValidRecord('com.example.nsid', {
790
+ $type: 'com.example.nsid',
791
+ nsid: 'com.bad-.foo',
792
+ }),
793
+ ).toThrow('Record/nsid must be a valid nsid')
794
+ })
795
+
796
+ it('Applies cid formatting constraint', () => {
797
+ lex.assertValidRecord('com.example.cid', {
798
+ $type: 'com.example.cid',
799
+ cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
800
+ })
801
+ expect(() =>
802
+ lex.assertValidRecord('com.example.cid', {
803
+ $type: 'com.example.cid',
804
+ cid: 'abapsdofiuwrpoiasdfuaspdfoiu',
805
+ }),
806
+ ).toThrow('Record/cid must be a cid string')
807
+ })
808
+
809
+ it('Applies language formatting constraint', () => {
810
+ lex.assertValidRecord('com.example.language', {
811
+ $type: 'com.example.language',
812
+ language: 'en-US-boont',
813
+ })
814
+ expect(() =>
815
+ lex.assertValidRecord('com.example.language', {
816
+ $type: 'com.example.language',
817
+ language: 'not-a-language-',
818
+ }),
819
+ ).toThrow('Record/language must be a well-formed BCP 47 language tag')
820
+ })
821
+
822
+ it('Applies bytes length constraints', () => {
823
+ lex.assertValidRecord('com.example.byteLength', {
824
+ $type: 'com.example.byteLength',
825
+ bytes: new Uint8Array([1, 2, 3]),
826
+ })
827
+ expect(() =>
828
+ lex.assertValidRecord('com.example.byteLength', {
829
+ $type: 'com.example.byteLength',
830
+ bytes: new Uint8Array([1]),
831
+ }),
832
+ ).toThrow('Record/bytes must not be smaller than 2 bytes')
833
+ expect(() =>
834
+ lex.assertValidRecord('com.example.byteLength', {
835
+ $type: 'com.example.byteLength',
836
+ bytes: new Uint8Array([1, 2, 3, 4, 5]),
837
+ }),
838
+ ).toThrow('Record/bytes must not be larger than 4 bytes')
839
+ })
540
840
  })
541
841
 
542
842
  describe('XRPC parameter validation', () => {
543
843
  const lex = new Lexicons(LexiconDocs)
544
844
 
545
845
  it('Passes valid parameters', () => {
546
- lex.assertValidXrpcParams('com.example.query', {
846
+ const queryResult = lex.assertValidXrpcParams('com.example.query', {
847
+ boolean: true,
848
+ integer: 123,
849
+ string: 'string',
850
+ array: ['x', 'y'],
851
+ })
852
+ expect(queryResult).toEqual({
853
+ boolean: true,
854
+ integer: 123,
855
+ string: 'string',
856
+ array: ['x', 'y'],
857
+ def: 0,
858
+ })
859
+ const paramResult = lex.assertValidXrpcParams('com.example.procedure', {
547
860
  boolean: true,
548
- number: 123.45,
549
861
  integer: 123,
550
862
  string: 'string',
863
+ array: ['x', 'y'],
864
+ def: 1,
551
865
  })
552
- lex.assertValidXrpcParams('com.example.procedure', {
866
+ expect(paramResult).toEqual({
553
867
  boolean: true,
554
- number: 123.45,
555
868
  integer: 123,
556
869
  string: 'string',
870
+ array: ['x', 'y'],
871
+ def: 1,
557
872
  })
558
873
  })
559
874
 
560
875
  it('Handles required correctly', () => {
561
876
  lex.assertValidXrpcParams('com.example.query', {
562
877
  boolean: true,
563
- number: 123.45,
564
878
  integer: 123,
565
879
  })
566
880
  expect(() =>
567
881
  lex.assertValidXrpcParams('com.example.query', {
568
882
  boolean: true,
569
- number: 123.45,
883
+ }),
884
+ ).toThrow('Params must have the property "integer"')
885
+ expect(() =>
886
+ lex.assertValidXrpcParams('com.example.query', {
887
+ boolean: true,
888
+ integer: undefined,
570
889
  }),
571
890
  ).toThrow('Params must have the property "integer"')
572
891
  })
@@ -575,19 +894,19 @@ describe('XRPC parameter validation', () => {
575
894
  expect(() =>
576
895
  lex.assertValidXrpcParams('com.example.query', {
577
896
  boolean: 'string',
578
- number: 123.45,
579
897
  integer: 123,
580
898
  string: 'string',
581
899
  }),
582
900
  ).toThrow('boolean must be a boolean')
583
901
  expect(() =>
584
- lex.assertValidXrpcParams('com.example.procedure', {
902
+ lex.assertValidXrpcParams('com.example.query', {
585
903
  boolean: true,
586
- number: true,
904
+ float: 123.45,
587
905
  integer: 123,
588
906
  string: 'string',
907
+ array: 'x',
589
908
  }),
590
- ).toThrow('number must be a number')
909
+ ).toThrow('array must be an array')
591
910
  })
592
911
  })
593
912
 
@@ -599,7 +918,7 @@ describe('XRPC input validation', () => {
599
918
  object: { boolean: true },
600
919
  array: ['one', 'two'],
601
920
  boolean: true,
602
- number: 123.45,
921
+ float: 123.45,
603
922
  integer: 123,
604
923
  string: 'string',
605
924
  })
@@ -612,7 +931,7 @@ describe('XRPC input validation', () => {
612
931
  object: { boolean: 'string' },
613
932
  array: ['one', 'two'],
614
933
  boolean: true,
615
- number: 123.45,
934
+ float: 123.45,
616
935
  integer: 123,
617
936
  string: 'string',
618
937
  }),
@@ -631,7 +950,7 @@ describe('XRPC output validation', () => {
631
950
  object: { boolean: true },
632
951
  array: ['one', 'two'],
633
952
  boolean: true,
634
- number: 123.45,
953
+ float: 123.45,
635
954
  integer: 123,
636
955
  string: 'string',
637
956
  })
@@ -639,7 +958,7 @@ describe('XRPC output validation', () => {
639
958
  object: { boolean: true },
640
959
  array: ['one', 'two'],
641
960
  boolean: true,
642
- number: 123.45,
961
+ float: 123.45,
643
962
  integer: 123,
644
963
  string: 'string',
645
964
  })
@@ -652,7 +971,7 @@ describe('XRPC output validation', () => {
652
971
  object: { boolean: 'string' },
653
972
  array: ['one', 'two'],
654
973
  boolean: true,
655
- number: 123.45,
974
+ float: 123.45,
656
975
  integer: 123,
657
976
  string: 'string',
658
977
  }),