@atproto/lex-builder 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.
Files changed (52) hide show
  1. package/dist/filter.d.ts +7 -0
  2. package/dist/filter.d.ts.map +1 -0
  3. package/dist/filter.js +30 -0
  4. package/dist/filter.js.map +1 -0
  5. package/dist/filtered-indexer.d.ts +2123 -0
  6. package/dist/filtered-indexer.d.ts.map +1 -0
  7. package/dist/filtered-indexer.js +56 -0
  8. package/dist/filtered-indexer.js.map +1 -0
  9. package/dist/formatter.d.ts +13 -0
  10. package/dist/formatter.d.ts.map +1 -0
  11. package/dist/formatter.js +34 -0
  12. package/dist/formatter.js.map +1 -0
  13. package/dist/index.d.ts +6 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +13 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/lex-builder.d.ts +20 -0
  18. package/dist/lex-builder.d.ts.map +1 -0
  19. package/dist/lex-builder.js +123 -0
  20. package/dist/lex-builder.js.map +1 -0
  21. package/dist/lex-def-builder.d.ts +66 -0
  22. package/dist/lex-def-builder.d.ts.map +1 -0
  23. package/dist/lex-def-builder.js +682 -0
  24. package/dist/lex-def-builder.js.map +1 -0
  25. package/dist/lexicon-directory-indexer.d.ts +11 -0
  26. package/dist/lexicon-directory-indexer.d.ts.map +1 -0
  27. package/dist/lexicon-directory-indexer.js +51 -0
  28. package/dist/lexicon-directory-indexer.js.map +1 -0
  29. package/dist/ref-resolver.d.ts +48 -0
  30. package/dist/ref-resolver.d.ts.map +1 -0
  31. package/dist/ref-resolver.js +246 -0
  32. package/dist/ref-resolver.js.map +1 -0
  33. package/dist/ts-lang.d.ts +3 -0
  34. package/dist/ts-lang.d.ts.map +1 -0
  35. package/dist/ts-lang.js +138 -0
  36. package/dist/ts-lang.js.map +1 -0
  37. package/dist/util.d.ts +11 -0
  38. package/dist/util.d.ts.map +1 -0
  39. package/dist/util.js +67 -0
  40. package/dist/util.js.map +1 -0
  41. package/package.json +49 -0
  42. package/src/filter.ts +41 -0
  43. package/src/filtered-indexer.test.ts +83 -0
  44. package/src/filtered-indexer.ts +60 -0
  45. package/src/formatter.ts +42 -0
  46. package/src/index.ts +17 -0
  47. package/src/lex-builder.ts +149 -0
  48. package/src/lex-def-builder.ts +912 -0
  49. package/src/lexicon-directory-indexer.ts +57 -0
  50. package/src/ref-resolver.ts +301 -0
  51. package/src/ts-lang.ts +134 -0
  52. package/src/util.ts +67 -0
@@ -0,0 +1,912 @@
1
+ import assert from 'node:assert'
2
+ import { SourceFile, VariableDeclarationKind } from 'ts-morph'
3
+ import {
4
+ LexiconArray,
5
+ LexiconArrayItems,
6
+ LexiconBlob,
7
+ LexiconBoolean,
8
+ LexiconBytes,
9
+ LexiconCid,
10
+ LexiconDocument,
11
+ LexiconError,
12
+ LexiconIndexer,
13
+ LexiconInteger,
14
+ LexiconObject,
15
+ LexiconParameters,
16
+ LexiconPayload,
17
+ LexiconPermissionSet,
18
+ LexiconProcedure,
19
+ LexiconQuery,
20
+ LexiconRecord,
21
+ LexiconRef,
22
+ LexiconRefUnion,
23
+ LexiconString,
24
+ LexiconSubscription,
25
+ LexiconToken,
26
+ LexiconUnknown,
27
+ } from '@atproto/lex-document'
28
+ import { l } from '@atproto/lex-schema'
29
+ import {
30
+ RefResolver,
31
+ ResolvedRef,
32
+ getPublicIdentifiers,
33
+ } from './ref-resolver.js'
34
+ import { isSafeIdentifier } from './ts-lang.js'
35
+
36
+ export type LexDefBuilderOptions = {
37
+ lib?: string
38
+ allowLegacyBlobs?: boolean
39
+ pureAnnotations?: boolean
40
+ }
41
+
42
+ /**
43
+ * Utility class to build a TypeScript source file from a lexicon document.
44
+ */
45
+ export class LexDefBuilder {
46
+ private readonly refResolver: RefResolver
47
+
48
+ constructor(
49
+ private readonly options: LexDefBuilderOptions,
50
+ private readonly file: SourceFile,
51
+ private readonly doc: LexiconDocument,
52
+ indexer: LexiconIndexer,
53
+ ) {
54
+ this.refResolver = new RefResolver(doc, file, indexer)
55
+ }
56
+
57
+ private pure(code: string) {
58
+ return this.options.pureAnnotations ? markPure(code) : code
59
+ }
60
+
61
+ async build() {
62
+ this.file.addVariableStatement({
63
+ declarationKind: VariableDeclarationKind.Const,
64
+ declarations: [
65
+ { name: '$nsid', initializer: JSON.stringify(this.doc.id) },
66
+ ],
67
+ })
68
+
69
+ this.file.addExportDeclaration({
70
+ namedExports: [{ name: '$nsid' }],
71
+ })
72
+
73
+ const defs = Object.keys(this.doc.defs)
74
+ if (defs.length) {
75
+ const moduleSpecifier = this.options?.lib ?? '@atproto/lex-schema'
76
+ this.file
77
+ .addImportDeclaration({ moduleSpecifier })
78
+ .addNamedImports([{ name: 'l' }])
79
+
80
+ for (const hash of defs) {
81
+ await this.addDef(hash)
82
+ }
83
+ }
84
+ }
85
+
86
+ private addUtils(definitions: Record<string, undefined | string>) {
87
+ const entries = Object.entries(definitions).filter(
88
+ (e): e is [(typeof e)[0], NonNullable<(typeof e)[1]>] => e[1] != null,
89
+ )
90
+ if (entries.length) {
91
+ this.file.addVariableStatement({
92
+ isExported: true,
93
+ declarationKind: VariableDeclarationKind.Const,
94
+ declarations: entries.map(([name, initializer]) => ({
95
+ name,
96
+ initializer,
97
+ })),
98
+ })
99
+ }
100
+ }
101
+
102
+ private async addDef(hash: string) {
103
+ const def = Object.hasOwn(this.doc.defs, hash) ? this.doc.defs[hash] : null
104
+ if (def == null) return
105
+
106
+ switch (def.type) {
107
+ case 'permission-set':
108
+ return this.addPermissionSet(hash, def)
109
+ case 'procedure':
110
+ return this.addProcedure(hash, def)
111
+ case 'query':
112
+ return this.addQuery(hash, def)
113
+ case 'subscription':
114
+ return this.addSubscription(hash, def)
115
+ case 'record':
116
+ return this.addRecord(hash, def)
117
+ case 'token':
118
+ return this.addToken(hash, def)
119
+ case 'object':
120
+ return this.addObject(hash, def)
121
+ case 'array':
122
+ return this.addArray(hash, def)
123
+ default:
124
+ await this.addSchema(hash, def, {
125
+ type: await this.compileContainedType(def),
126
+ schema: await this.compileContainedSchema(def),
127
+ validationUtils: true,
128
+ })
129
+ }
130
+ }
131
+
132
+ private async addPermissionSet(hash: string, def: LexiconPermissionSet) {
133
+ const permission = def.permissions.map((def) => {
134
+ const options = stringifyOptionalOptions(def, ['resource', 'type'])
135
+ return this.pure(
136
+ `l.permission(${JSON.stringify(def.resource)}, ${options})`,
137
+ )
138
+ })
139
+
140
+ const options = stringifyOptionalOptions(def, [
141
+ 'type',
142
+ 'description',
143
+ 'permissions',
144
+ ])
145
+
146
+ await this.addSchema(hash, def, {
147
+ schema: this.pure(
148
+ `l.permissionSet($nsid, [${permission.join(',')}], ${options})`,
149
+ ),
150
+ })
151
+ }
152
+
153
+ private async addProcedure(hash: string, def: LexiconProcedure) {
154
+ if (hash !== 'main') {
155
+ throw new Error(`Definition ${hash} cannot be of type ${def.type}`)
156
+ }
157
+
158
+ // @TODO Build the types instead of using an inferred type.
159
+
160
+ const ref = await this.addSchema(hash, def, {
161
+ schema: this.pure(`
162
+ l.procedure(
163
+ $nsid,
164
+ ${await this.compileParamsSchema(def.parameters)},
165
+ ${await this.compilePayload(def.input)},
166
+ ${await this.compilePayload(def.output)},
167
+ ${await this.compileErrors(def.errors)}
168
+ )
169
+ `),
170
+ })
171
+
172
+ this.addUtils({
173
+ $params: this.pure(`${ref.varName}.parameters`),
174
+ $input: this.pure(`${ref.varName}.input`),
175
+ $output: this.pure(`${ref.varName}.output`),
176
+ })
177
+
178
+ const parametersTypeStmt = this.file.addTypeAlias({
179
+ isExported: true,
180
+ name: 'Params',
181
+ type: `l.InferProcedureParameters<typeof ${ref.varName}>`,
182
+ })
183
+
184
+ addJsDoc(parametersTypeStmt, def.parameters)
185
+
186
+ const inputTypeStmt = this.file.addTypeAlias({
187
+ isExported: true,
188
+ name: 'Input',
189
+ type: `l.InferProcedureInputBody<typeof ${ref.varName}>`,
190
+ })
191
+
192
+ addJsDoc(inputTypeStmt, def.input)
193
+
194
+ const outputTypeStmt = this.file.addTypeAlias({
195
+ isExported: true,
196
+ name: 'Output',
197
+ type: `l.InferProcedureOutputBody<typeof ${ref.varName}>`,
198
+ })
199
+
200
+ addJsDoc(outputTypeStmt, def.output)
201
+ }
202
+
203
+ private async addQuery(hash: string, def: LexiconQuery) {
204
+ if (hash !== 'main') {
205
+ throw new Error(`Definition ${hash} cannot be of type ${def.type}`)
206
+ }
207
+
208
+ // @TODO Build the types instead of using an inferred type.
209
+
210
+ const ref = await this.addSchema(hash, def, {
211
+ schema: this.pure(`
212
+ l.query(
213
+ $nsid,
214
+ ${await this.compileParamsSchema(def.parameters)},
215
+ ${await this.compilePayload(def.output)},
216
+ ${await this.compileErrors(def.errors)}
217
+ )
218
+ `),
219
+ })
220
+
221
+ this.addUtils({
222
+ $params: `${ref.varName}.parameters`,
223
+ $output: `${ref.varName}.output`,
224
+ })
225
+
226
+ this.file.addTypeAlias({
227
+ isExported: true,
228
+ name: 'Params',
229
+ type: `l.InferQueryParameters<typeof ${ref.varName}>`,
230
+ })
231
+
232
+ this.file.addTypeAlias({
233
+ isExported: true,
234
+ name: 'Output',
235
+ type: `l.InferQueryOutputBody<typeof ${ref.varName}>`,
236
+ })
237
+ }
238
+
239
+ private async addSubscription(hash: string, def: LexiconSubscription) {
240
+ if (hash !== 'main') {
241
+ throw new Error(`Definition ${hash} cannot be of type ${def.type}`)
242
+ }
243
+
244
+ // @TODO Build the types instead of using an inferred type.
245
+
246
+ const ref = await this.addSchema(hash, def, {
247
+ schema: this.pure(`
248
+ l.subscription(
249
+ $nsid,
250
+ ${await this.compileParamsSchema(def.parameters)},
251
+ ${await this.compileBodySchema(def.message?.schema)},
252
+ ${await this.compileErrors(def.errors)}
253
+ )
254
+ `),
255
+ })
256
+
257
+ this.addUtils({
258
+ $params: `${ref.varName}.parameters`,
259
+ $message: `${ref.varName}.message`,
260
+ })
261
+
262
+ this.file.addTypeAlias({
263
+ isExported: true,
264
+ name: 'Params',
265
+ type: `l.InferSubscriptionParameters<typeof ${ref.varName}>`,
266
+ })
267
+
268
+ this.file.addTypeAlias({
269
+ isExported: true,
270
+ name: 'Message',
271
+ type: `l.InferSubscriptionMessage<typeof ${ref.varName}>`,
272
+ })
273
+ }
274
+
275
+ private async addRecord(hash: string, def: LexiconRecord) {
276
+ if (hash !== 'main') {
277
+ throw new Error(`Definition ${hash} cannot be of type ${def.type}`)
278
+ }
279
+
280
+ const key = JSON.stringify(def.key ?? 'any')
281
+ const objectSchema = await this.compileObjectSchema(def.record)
282
+
283
+ const properties = await this.compilePropertiesTypes(def.record)
284
+ properties.unshift(`$type: ${JSON.stringify(l.$type(this.doc.id, hash))}`)
285
+
286
+ await this.addSchema(hash, def, {
287
+ type: `{ ${properties.join(';')} }`,
288
+ schema: (ref) =>
289
+ this.pure(
290
+ `l.record<${key}, ${ref.typeName}>(${key}, $nsid, ${objectSchema})`,
291
+ ),
292
+ objectUtils: true,
293
+ validationUtils: true,
294
+ })
295
+ }
296
+
297
+ private async addObject(hash: string, def: LexiconObject) {
298
+ const objectSchema = await this.compileObjectSchema(def)
299
+
300
+ const properties = await this.compilePropertiesTypes(def)
301
+ properties.unshift(`$type?: ${JSON.stringify(l.$type(this.doc.id, hash))}`)
302
+
303
+ await this.addSchema(hash, def, {
304
+ type: `{ ${properties.join(';')} }`,
305
+ schema: (ref) =>
306
+ this.pure(
307
+ `l.typedObject<${ref.typeName}>($nsid, ${JSON.stringify(hash)}, ${objectSchema})`,
308
+ ),
309
+ objectUtils: true,
310
+ validationUtils: true,
311
+ })
312
+ }
313
+
314
+ private async addToken(hash: string, def: LexiconToken) {
315
+ await this.addSchema(hash, def, {
316
+ schema: this.pure(`l.token($nsid, ${JSON.stringify(hash)})`),
317
+ type: JSON.stringify(l.$type(this.doc.id, hash)),
318
+ validationUtils: true,
319
+ })
320
+ }
321
+
322
+ private async addArray(hash: string, def: LexiconArray) {
323
+ // @TODO It could be nice to expose the array item type as a separate type.
324
+ // This was not done (yet) as there is no easy way to name it to avoid
325
+ // collisions.
326
+
327
+ const itemSchema = await this.compileContainedSchema(def.items)
328
+ const options = stringifyOptionalOptions(def, [
329
+ 'type',
330
+ 'description',
331
+ 'items',
332
+ ])
333
+
334
+ await this.addSchema(hash, def, {
335
+ type: `(${await this.compileContainedType(def.items)})[]`,
336
+ // @NOTE Not using compileArraySchema to allow specifying the generic
337
+ // parameter to l.array<>.
338
+ schema: (ref) =>
339
+ this.pure(
340
+ `l.array<${ref.typeName}[number]>(${itemSchema}, ${options})`,
341
+ ),
342
+ validationUtils: true,
343
+ })
344
+ }
345
+
346
+ private async addSchema(
347
+ hash: string,
348
+ def: { description?: string },
349
+ {
350
+ type,
351
+ schema,
352
+ objectUtils,
353
+ validationUtils,
354
+ }: {
355
+ type?: string | ((ref: ResolvedRef) => string)
356
+ schema?: string | ((ref: ResolvedRef) => string)
357
+ objectUtils?: boolean
358
+ validationUtils?: boolean
359
+ },
360
+ ): Promise<ResolvedRef> {
361
+ const ref = await this.refResolver.resolveLocal(hash)
362
+ const pub = getPublicIdentifiers(hash)
363
+
364
+ // Fool-proofing
365
+ assert(isSafeIdentifier(ref.varName), 'Expected safe type identifier')
366
+ assert(isSafeIdentifier(ref.typeName), 'Expected safe type identifier')
367
+ assert(isSafeIdentifier(pub.typeName), 'Expected safe type identifier')
368
+
369
+ if (type) {
370
+ const typeStmt = this.file.addTypeAlias({
371
+ name: ref.typeName,
372
+ type: typeof type === 'function' ? type(ref) : type,
373
+ })
374
+
375
+ addJsDoc(typeStmt, def)
376
+
377
+ this.file.addExportDeclaration({
378
+ isTypeOnly: true,
379
+ namedExports: [
380
+ {
381
+ name: ref.typeName,
382
+ alias: ref.typeName === pub.typeName ? undefined : pub.typeName,
383
+ },
384
+ ],
385
+ })
386
+ }
387
+
388
+ if (schema) {
389
+ const constStmt = this.file.addVariableStatement({
390
+ declarationKind: VariableDeclarationKind.Const,
391
+ declarations: [
392
+ {
393
+ name: ref.varName,
394
+ initializer: typeof schema === 'function' ? schema(ref) : schema,
395
+ },
396
+ ],
397
+ })
398
+
399
+ addJsDoc(constStmt, def)
400
+
401
+ this.file.addExportDeclaration({
402
+ namedExports: [
403
+ {
404
+ name: ref.varName,
405
+ alias:
406
+ ref.varName === pub.varName
407
+ ? undefined
408
+ : isSafeIdentifier(pub.varName)
409
+ ? pub.varName
410
+ : JSON.stringify(pub.varName),
411
+ },
412
+ ],
413
+ })
414
+ }
415
+
416
+ if (hash === 'main' && objectUtils) {
417
+ this.addUtils({
418
+ $isTypeOf: markPure(`${ref.varName}.isTypeOf.bind(${ref.varName})`),
419
+ $build: markPure(`${ref.varName}.build.bind(${ref.varName})`),
420
+ $type: markPure(`${ref.varName}.$type`),
421
+ })
422
+ }
423
+
424
+ if (hash === 'main' && validationUtils) {
425
+ this.addUtils({
426
+ $assert: markPure(`${ref.varName}.assert.bind(${ref.varName})`),
427
+ $check: markPure(`${ref.varName}.check.bind(${ref.varName})`),
428
+ $maybe: markPure(`${ref.varName}.maybe.bind(${ref.varName})`),
429
+ $parse: markPure(`${ref.varName}.parse.bind(${ref.varName})`),
430
+ $validate: markPure(`${ref.varName}.validate.bind(${ref.varName})`),
431
+ })
432
+ }
433
+
434
+ return ref
435
+ }
436
+
437
+ private async compilePayload(def: LexiconPayload | undefined) {
438
+ if (!def) return this.pure(`l.payload()`)
439
+
440
+ const encodedEncoding = JSON.stringify(def.encoding)
441
+ if (def.schema) {
442
+ const bodySchema = await this.compileBodySchema(def.schema)
443
+ return this.pure(`l.payload(${encodedEncoding}, ${bodySchema})`)
444
+ } else {
445
+ return this.pure(`l.payload(${encodedEncoding})`)
446
+ }
447
+ }
448
+
449
+ private async compileBodySchema(
450
+ def?: LexiconRef | LexiconRefUnion | LexiconObject,
451
+ ): Promise<string> {
452
+ if (!def) return 'undefined'
453
+ if (def.type === 'object') return this.compileObjectSchema(def)
454
+ return this.compileContainedSchema(def)
455
+ }
456
+
457
+ private async compileParamsSchema(def: undefined | LexiconParameters) {
458
+ if (!def) return this.pure(`l.params({})`)
459
+
460
+ const properties = await this.compilePropertiesSchemas(def)
461
+ const options = stringifyOptionalOptions(def, [
462
+ 'type',
463
+ 'description',
464
+ 'properties',
465
+ ])
466
+ return this.pure(`l.params({${properties.join(',')}}, ${options})`)
467
+ }
468
+
469
+ private async compileErrors(defs?: readonly LexiconError[]) {
470
+ if (!defs?.length) return ''
471
+ return JSON.stringify(defs.map((d) => d.name))
472
+ }
473
+
474
+ private async compileObjectSchema(def: LexiconObject): Promise<string> {
475
+ const properties = await this.compilePropertiesSchemas(def)
476
+ const options = stringifyOptionalOptions(def, [
477
+ 'type',
478
+ 'description',
479
+ 'properties',
480
+ ])
481
+ return this.pure(`l.object({${properties.join(',')}}, ${options})`)
482
+ }
483
+
484
+ private async compilePropertiesSchemas(options: {
485
+ properties: Record<string, LexiconArray | LexiconArrayItems>
486
+ required?: readonly string[]
487
+ }) {
488
+ for (const prop of options.required || []) {
489
+ if (!Object.hasOwn(options.properties, prop)) {
490
+ throw new Error(`Required property "${prop}" not found in properties`)
491
+ }
492
+ }
493
+
494
+ return Promise.all(
495
+ Object.entries(options.properties).map(
496
+ this.compilePropertyEntrySchema,
497
+ this,
498
+ ),
499
+ )
500
+ }
501
+
502
+ private async compilePropertiesTypes(options: {
503
+ properties: Record<string, LexiconArray | LexiconArrayItems>
504
+ required?: readonly string[]
505
+ nullable?: readonly string[]
506
+ }) {
507
+ return Promise.all(
508
+ Object.entries(options.properties).map((entry) => {
509
+ return this.compilePropertyEntryType(entry, options)
510
+ }),
511
+ )
512
+ }
513
+
514
+ private async compilePropertyEntrySchema([key, def]: [
515
+ string,
516
+ LexiconArray | LexiconArrayItems,
517
+ ]) {
518
+ const name = JSON.stringify(key)
519
+ const schema = await this.compileContainedSchema(def)
520
+ return `${name}:${schema}`
521
+ }
522
+
523
+ private async compilePropertyEntryType(
524
+ [key, def]: [string, LexiconArray | LexiconArrayItems],
525
+ options: {
526
+ required?: readonly string[]
527
+ nullable?: readonly string[]
528
+ },
529
+ ) {
530
+ const isNullable = options.nullable?.includes(key)
531
+ const isRequired = options.required?.includes(key)
532
+
533
+ const optional = isRequired ? '' : '?'
534
+ const append = isNullable ? ' | null' : ''
535
+
536
+ const jsDoc = compileLeadingTrivia(def.description) || ''
537
+ const name = JSON.stringify(key)
538
+ const type = await this.compileContainedType(def)
539
+
540
+ return `${jsDoc}${name}${optional}:${type}${append}`
541
+ }
542
+
543
+ private async compileContainedSchema(
544
+ def: LexiconArray | LexiconArrayItems,
545
+ ): Promise<string> {
546
+ switch (def.type) {
547
+ case 'unknown':
548
+ return this.compileUnknownSchema(def)
549
+ case 'boolean':
550
+ return this.compileBooleanSchema(def)
551
+ case 'integer':
552
+ return this.compileIntegerSchema(def)
553
+ case 'string':
554
+ return this.compileStringSchema(def)
555
+ case 'bytes':
556
+ return this.compileBytesSchema(def)
557
+ case 'blob':
558
+ return this.compileBlobSchema(def)
559
+ case 'cid-link':
560
+ return this.compileCidLinkSchema(def)
561
+ case 'ref':
562
+ return this.compileRefSchema(def)
563
+ case 'union':
564
+ return this.compileRefUnionSchema(def)
565
+ case 'array':
566
+ return this.compileArraySchema(def)
567
+ default:
568
+ // @ts-expect-error
569
+ throw new Error(`Unsupported def type: ${def.type}`)
570
+ }
571
+ }
572
+
573
+ private async compileContainedType(
574
+ def: LexiconArray | LexiconArrayItems,
575
+ ): Promise<string> {
576
+ switch (def.type) {
577
+ case 'unknown':
578
+ return this.compileUnknownType(def)
579
+ case 'boolean':
580
+ return this.compileBooleanType(def)
581
+ case 'integer':
582
+ return this.compileIntegerType(def)
583
+ case 'string':
584
+ return this.compileStringType(def)
585
+ case 'bytes':
586
+ return this.compileBytesType(def)
587
+ case 'blob':
588
+ return this.compileBlobType(def)
589
+ case 'cid-link':
590
+ return this.compileCidLinkType(def)
591
+ case 'ref':
592
+ return this.compileRefType(def)
593
+ case 'union':
594
+ return this.compileRefUnionType(def)
595
+ case 'array':
596
+ return this.compileArrayType(def)
597
+ default:
598
+ // @ts-expect-error
599
+ throw new Error(`Unsupported def type: ${def.type}`)
600
+ }
601
+ }
602
+
603
+ private async compileArraySchema(def: LexiconArray): Promise<string> {
604
+ const itemSchema = await this.compileContainedSchema(def.items)
605
+ const options = stringifyOptionalOptions(def, [
606
+ 'type',
607
+ 'description',
608
+ 'items',
609
+ ])
610
+ return this.pure(`l.array(${itemSchema}, ${options})`)
611
+ }
612
+
613
+ private async compileArrayType(def: LexiconArray): Promise<string> {
614
+ return `(${await this.compileContainedType(def.items)})[]`
615
+ }
616
+
617
+ private async compileUnknownSchema(_def: LexiconUnknown): Promise<string> {
618
+ return this.pure(`l.unknownObject()`)
619
+ }
620
+
621
+ private async compileUnknownType(_def: LexiconUnknown): Promise<string> {
622
+ return `l.UnknownObject`
623
+ }
624
+
625
+ private async compileBooleanSchema(def: LexiconBoolean): Promise<string> {
626
+ if (hasConst(def)) return this.compileConstSchema(def)
627
+
628
+ const options = stringifyOptionalOptions(def, ['type', 'description'])
629
+ return this.pure(`l.boolean(${options})`)
630
+ }
631
+
632
+ private async compileBooleanType(def: LexiconBoolean): Promise<string> {
633
+ if (hasConst(def)) return this.compileConstType(def)
634
+ return 'boolean'
635
+ }
636
+
637
+ private async compileIntegerSchema(def: LexiconInteger): Promise<string> {
638
+ if (hasConst(def)) {
639
+ const schema = l.integer(def)
640
+ assert(
641
+ schema.check(def.const),
642
+ `Integer const ${def.const} is out of bounds`,
643
+ )
644
+ }
645
+
646
+ if (hasEnum(def)) {
647
+ const schema = l.integer(def)
648
+ for (const val of def.enum) {
649
+ assert(schema.check(val), `Integer enum value ${val} is out of bounds`)
650
+ }
651
+ }
652
+
653
+ if (hasConst(def)) return this.compileConstSchema(def)
654
+ if (hasEnum(def)) return this.compileEnumSchema(def)
655
+
656
+ const options = stringifyOptionalOptions(def, ['type', 'description'])
657
+ return this.pure(`l.integer(${options})`)
658
+ }
659
+
660
+ private async compileIntegerType(def: LexiconInteger): Promise<string> {
661
+ if (hasConst(def)) return this.compileConstType(def)
662
+ if (hasEnum(def)) return this.compileEnumType(def)
663
+
664
+ return 'number'
665
+ }
666
+
667
+ private async compileStringSchema(def: LexiconString): Promise<string> {
668
+ if (hasConst(def)) {
669
+ const schema = l.string(def)
670
+ assert(
671
+ schema.check(def.const),
672
+ `String const "${def.const}" does not match format`,
673
+ )
674
+ } else if (hasEnum(def)) {
675
+ const schema = l.string(def)
676
+ for (const val of def.enum) {
677
+ assert(
678
+ schema.check(val),
679
+ `String enum value "${val}" does not match format`,
680
+ )
681
+ }
682
+ }
683
+
684
+ if (hasConst(def)) return this.compileConstSchema(def)
685
+ if (hasEnum(def)) return this.compileEnumSchema(def)
686
+
687
+ const options = stringifyOptionalOptions(def, ['type', 'description'])
688
+ return this.pure(`l.string(${options})`)
689
+ }
690
+
691
+ private async compileStringType(def: LexiconString): Promise<string> {
692
+ if (hasConst(def)) return this.compileConstType(def)
693
+ if (hasEnum(def)) return this.compileEnumType(def)
694
+
695
+ switch (def.format) {
696
+ case 'datetime':
697
+ return 'l.Datetime'
698
+ case 'uri':
699
+ return 'l.Uri'
700
+ case 'at-uri':
701
+ return 'l.AtUri'
702
+ case 'did':
703
+ return 'l.Did'
704
+ case 'handle':
705
+ return 'l.Handle'
706
+ case 'at-identifier':
707
+ return 'l.AtIdentifier'
708
+ case 'nsid':
709
+ return 'l.Nsid'
710
+ }
711
+
712
+ if (def.knownValues?.length) {
713
+ return (
714
+ def.knownValues.map((v) => JSON.stringify(v)).join(' | ') +
715
+ ' | l.UnknownString'
716
+ )
717
+ }
718
+
719
+ return 'string'
720
+ }
721
+
722
+ private async compileBytesSchema(def: LexiconBytes): Promise<string> {
723
+ const options = stringifyOptionalOptions(def, ['type', 'description'])
724
+ return this.pure(`l.bytes(${options})`)
725
+ }
726
+
727
+ private async compileBytesType(_def: LexiconBytes): Promise<string> {
728
+ return 'Uint8Array'
729
+ }
730
+
731
+ private async compileBlobSchema(def: LexiconBlob): Promise<string> {
732
+ const opts = this.options.allowLegacyBlobs
733
+ ? { ...def, allowLegacy: true }
734
+ : def
735
+ const options = stringifyOptionalOptions(opts, ['type', 'description'])
736
+ return this.pure(`l.blob(${options})`)
737
+ }
738
+
739
+ private async compileBlobType(_def: LexiconBlob): Promise<string> {
740
+ return this.options.allowLegacyBlobs
741
+ ? 'l.BlobRef | l.LegacyBlobRef'
742
+ : 'l.BlobRef'
743
+ }
744
+
745
+ private async compileCidLinkSchema(def: LexiconCid): Promise<string> {
746
+ const options = stringifyOptionalOptions(def, ['type', 'description'])
747
+ return this.pure(`l.cidLink(${options})`)
748
+ }
749
+
750
+ private async compileCidLinkType(_def: LexiconCid): Promise<string> {
751
+ return 'l.CID'
752
+ }
753
+
754
+ private async compileRefSchema(def: LexiconRef): Promise<string> {
755
+ const { varName, typeName } = await this.refResolver.resolve(def.ref)
756
+ // @NOTE "as any" is needed in schemas with circular refs as TypeScript
757
+ // cannot infer the type of a value that depends on its initializer type
758
+ return this.pure(`l.ref<${typeName}>((() => ${varName}) as any)`)
759
+ }
760
+
761
+ private async compileRefType(def: LexiconRef): Promise<string> {
762
+ const ref = await this.refResolver.resolve(def.ref)
763
+ return ref.typeName
764
+ }
765
+
766
+ private async compileRefUnionSchema(def: LexiconRefUnion): Promise<string> {
767
+ if (def.refs.length === 0 && def.closed) {
768
+ return this.pure(`l.never()`)
769
+ }
770
+
771
+ const refs = await Promise.all(
772
+ def.refs.map(async (ref: string) => {
773
+ const { varName, typeName } = await this.refResolver.resolve(ref)
774
+ // @NOTE "as any" is needed in schemas with circular refs as TypeScript
775
+ // cannot infer the type of a value that depends on its initializer type
776
+ return this.pure(`l.typedRef<${typeName}>((() => ${varName}) as any)`)
777
+ }),
778
+ )
779
+
780
+ return this.pure(
781
+ `l.typedUnion([${refs.join(',')}], ${def.closed ?? false})`,
782
+ )
783
+ }
784
+
785
+ private async compileRefUnionType(def: LexiconRefUnion): Promise<string> {
786
+ const types = await Promise.all(
787
+ def.refs.map(async (ref) => {
788
+ const { typeName } = await this.refResolver.resolve(ref)
789
+ return `l.TypedRef<${typeName}>`
790
+ }),
791
+ )
792
+ if (!def.closed) types.push('l.TypedObject')
793
+ return types.join(' | ') || 'never'
794
+ }
795
+
796
+ private async compileConstSchema<
797
+ T extends null | number | string | boolean,
798
+ >(def: { const: T; enum?: readonly T[] }): Promise<string> {
799
+ if (hasEnum(def) && !def.enum.includes(def.const)) {
800
+ return this.pure(`l.never()`)
801
+ }
802
+
803
+ return this.pure(`l.literal(${JSON.stringify(def.const)})`)
804
+ }
805
+
806
+ private async compileConstType<
807
+ T extends null | number | string | boolean,
808
+ >(def: { const: T; enum?: readonly T[] }): Promise<string> {
809
+ if (hasEnum(def) && !def.enum.includes(def.const)) {
810
+ return 'never'
811
+ }
812
+ return JSON.stringify(def.const)
813
+ }
814
+
815
+ private async compileEnumSchema<T extends null | number | string>(def: {
816
+ enum: readonly T[]
817
+ }): Promise<string> {
818
+ if (def.enum.length === 0) {
819
+ return this.pure(`l.never()`)
820
+ }
821
+ if (def.enum.length === 1) {
822
+ return this.pure(`l.literal(${JSON.stringify(def.enum[0])})`)
823
+ }
824
+ return this.pure(`l.enum(${JSON.stringify(def.enum)})`)
825
+ }
826
+
827
+ private async compileEnumType<T extends null | number | string>(def: {
828
+ enum: readonly T[]
829
+ }): Promise<string> {
830
+ return def.enum.map((v) => JSON.stringify(v)).join(' | ') || 'never'
831
+ }
832
+ }
833
+
834
+ type ParsedDescription = {
835
+ description: string
836
+ deprecated: boolean | string
837
+ }
838
+
839
+ function parseDescription(description: string): ParsedDescription {
840
+ if (/deprecated/i.test(description)) {
841
+ const deprecationMatch = description.match(
842
+ /(\s*deprecated\s*(?:--?|:)?\s*([^-]*)(?:-+)?)/i,
843
+ )
844
+ if (deprecationMatch) {
845
+ const [, match, deprecationNotice] = deprecationMatch
846
+ return {
847
+ description: description.replace(match, '').trim(),
848
+ deprecated: deprecationNotice?.trim() || true,
849
+ }
850
+ } else {
851
+ return {
852
+ description: description.trim(),
853
+ deprecated: true,
854
+ }
855
+ }
856
+ }
857
+
858
+ return {
859
+ description: description.trim(),
860
+ deprecated: false,
861
+ }
862
+ }
863
+
864
+ function compileLeadingTrivia(description?: string) {
865
+ if (!description) return undefined
866
+ return `\n\n/**${compileJsDoc(description).replaceAll('\n', '\n * ')}\n */\n`
867
+ }
868
+
869
+ function addJsDoc(
870
+ declaration: { addJsDoc: (text: string) => void },
871
+ def?: { description?: string },
872
+ ) {
873
+ if (def?.description) {
874
+ declaration.addJsDoc(compileJsDoc(def.description))
875
+ }
876
+ }
877
+
878
+ function compileJsDoc(description: string) {
879
+ const parsed = parseDescription(description)
880
+ return `\n${parsed.description}${
881
+ !parsed.deprecated
882
+ ? ''
883
+ : (parsed.description ? '\n\n' : '') +
884
+ (parsed.deprecated === true
885
+ ? '@deprecated'
886
+ : `@deprecated ${parsed.deprecated}`)
887
+ }`
888
+ }
889
+
890
+ function stringifyOptionalOptions<O extends Record<string, unknown>>(
891
+ obj: O,
892
+ omit?: (keyof O)[],
893
+ ) {
894
+ const filtered = Object.entries(obj).filter(([k]) => !omit?.includes(k))
895
+ return filtered.length ? JSON.stringify(Object.fromEntries(filtered)) : ''
896
+ }
897
+
898
+ function hasConst<T extends { const?: unknown }>(
899
+ def: T,
900
+ ): def is T & { const: NonNullable<T['const']> } {
901
+ return def.const != null
902
+ }
903
+
904
+ function hasEnum<T extends { enum?: readonly unknown[] }>(
905
+ def: T,
906
+ ): def is T & { enum: unknown[] } {
907
+ return def.enum != null
908
+ }
909
+
910
+ function markPure<T extends string>(v: T): `/*#__PURE__*/ ${T}` {
911
+ return `/*#__PURE__*/ ${v}`
912
+ }