@atproto/lex-builder 0.0.3 → 0.0.5

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.
@@ -28,12 +28,13 @@ import {
28
28
  import { l } from '@atproto/lex-schema'
29
29
  import {
30
30
  RefResolver,
31
+ RefResolverOptions,
31
32
  ResolvedRef,
32
33
  getPublicIdentifiers,
33
34
  } from './ref-resolver.js'
34
35
  import { isSafeIdentifier } from './ts-lang.js'
35
36
 
36
- export type LexDefBuilderOptions = {
37
+ export type LexDefBuilderOptions = RefResolverOptions & {
37
38
  lib?: string
38
39
  allowLegacyBlobs?: boolean
39
40
  pureAnnotations?: boolean
@@ -51,7 +52,7 @@ export class LexDefBuilder {
51
52
  private readonly doc: LexiconDocument,
52
53
  indexer: LexiconIndexer,
53
54
  ) {
54
- this.refResolver = new RefResolver(doc, file, indexer)
55
+ this.refResolver = new RefResolver(doc, file, indexer, options)
55
56
  }
56
57
 
57
58
  private pure(code: string) {
@@ -131,17 +132,18 @@ export class LexDefBuilder {
131
132
 
132
133
  private async addPermissionSet(hash: string, def: LexiconPermissionSet) {
133
134
  const permission = def.permissions.map((def) => {
134
- const options = stringifyOptionalOptions(def, ['resource', 'type'])
135
+ const options = stringifyOptions(def, undefined, ['resource', 'type'])
135
136
  return this.pure(
136
137
  `l.permission(${JSON.stringify(def.resource)}, ${options})`,
137
138
  )
138
139
  })
139
140
 
140
- const options = stringifyOptionalOptions(def, [
141
- 'type',
142
- 'description',
143
- 'permissions',
144
- ])
141
+ const options = stringifyOptions(def, [
142
+ 'title',
143
+ 'title:lang',
144
+ 'detail',
145
+ 'detail:lang',
146
+ ] satisfies (keyof l.PermissionSetOptions)[])
145
147
 
146
148
  await this.addSchema(hash, def, {
147
149
  schema: this.pure(
@@ -325,11 +327,10 @@ export class LexDefBuilder {
325
327
  // collisions.
326
328
 
327
329
  const itemSchema = await this.compileContainedSchema(def.items)
328
- const options = stringifyOptionalOptions(def, [
329
- 'type',
330
- 'description',
331
- 'items',
332
- ])
330
+ const options = stringifyOptions(def, [
331
+ 'minLength',
332
+ 'maxLength',
333
+ ] satisfies (keyof l.ArraySchemaOptions)[])
333
334
 
334
335
  await this.addSchema(hash, def, {
335
336
  type: `(${await this.compileContainedType(def.items)})[]`,
@@ -424,10 +425,10 @@ export class LexDefBuilder {
424
425
  if (hash === 'main' && validationUtils) {
425
426
  this.addUtils({
426
427
  $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})`),
428
+ $ifMatches: markPure(`${ref.varName}.ifMatches.bind(${ref.varName})`),
429
+ $matches: markPure(`${ref.varName}.matches.bind(${ref.varName})`),
429
430
  $parse: markPure(`${ref.varName}.parse.bind(${ref.varName})`),
430
- $validate: markPure(`${ref.varName}.validate.bind(${ref.varName})`),
431
+ $safeParse: markPure(`${ref.varName}.safeParse.bind(${ref.varName})`),
431
432
  })
432
433
  }
433
434
 
@@ -458,12 +459,7 @@ export class LexDefBuilder {
458
459
  if (!def) return this.pure(`l.params({})`)
459
460
 
460
461
  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})`)
462
+ return this.pure(`l.params({${properties.join(',')}})`)
467
463
  }
468
464
 
469
465
  private async compileErrors(defs?: readonly LexiconError[]) {
@@ -473,29 +469,28 @@ export class LexDefBuilder {
473
469
 
474
470
  private async compileObjectSchema(def: LexiconObject): Promise<string> {
475
471
  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})`)
472
+ return this.pure(`l.object({${properties.join(',')}})`)
482
473
  }
483
474
 
484
475
  private async compilePropertiesSchemas(options: {
485
476
  properties: Record<string, LexiconArray | LexiconArrayItems>
486
477
  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`)
478
+ nullable?: readonly string[]
479
+ }): Promise<string[]> {
480
+ for (const opt of ['required', 'nullable'] as const) {
481
+ if (options[opt]) {
482
+ for (const prop of options[opt]) {
483
+ if (!Object.hasOwn(options.properties, prop)) {
484
+ throw new Error(`No schema found for ${opt} property "${prop}"`)
485
+ }
486
+ }
491
487
  }
492
488
  }
493
489
 
494
490
  return Promise.all(
495
- Object.entries(options.properties).map(
496
- this.compilePropertyEntrySchema,
497
- this,
498
- ),
491
+ Object.entries(options.properties).map((entry) => {
492
+ return this.compilePropertyEntrySchema(entry, options)
493
+ }),
499
494
  )
500
495
  }
501
496
 
@@ -511,13 +506,27 @@ export class LexDefBuilder {
511
506
  )
512
507
  }
513
508
 
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}`
509
+ private async compilePropertyEntrySchema(
510
+ [key, def]: [string, LexiconArray | LexiconArrayItems],
511
+ options: {
512
+ required?: readonly string[]
513
+ nullable?: readonly string[]
514
+ },
515
+ ) {
516
+ const isNullable = options.nullable?.includes(key)
517
+ const isRequired = options.required?.includes(key)
518
+
519
+ let schema = await this.compileContainedSchema(def)
520
+
521
+ if (isNullable) {
522
+ schema = this.pure(`l.nullable(${schema})`)
523
+ }
524
+
525
+ if (!isRequired) {
526
+ schema = this.pure(`l.optional(${schema})`)
527
+ }
528
+
529
+ return `${JSON.stringify(key)}:${schema}`
521
530
  }
522
531
 
523
532
  private async compilePropertyEntryType(
@@ -602,11 +611,10 @@ export class LexDefBuilder {
602
611
 
603
612
  private async compileArraySchema(def: LexiconArray): Promise<string> {
604
613
  const itemSchema = await this.compileContainedSchema(def.items)
605
- const options = stringifyOptionalOptions(def, [
606
- 'type',
607
- 'description',
608
- 'items',
609
- ])
614
+ const options = stringifyOptions(def, [
615
+ 'minLength',
616
+ 'maxLength',
617
+ ] satisfies (keyof l.ArraySchemaOptions)[])
610
618
  return this.pure(`l.array(${itemSchema}, ${options})`)
611
619
  }
612
620
 
@@ -625,7 +633,9 @@ export class LexDefBuilder {
625
633
  private async compileBooleanSchema(def: LexiconBoolean): Promise<string> {
626
634
  if (hasConst(def)) return this.compileConstSchema(def)
627
635
 
628
- const options = stringifyOptionalOptions(def, ['type', 'description'])
636
+ const options = stringifyOptions(def, [
637
+ 'default',
638
+ ] satisfies (keyof l.BooleanSchemaOptions)[])
629
639
  return this.pure(`l.boolean(${options})`)
630
640
  }
631
641
 
@@ -636,24 +646,23 @@ export class LexDefBuilder {
636
646
 
637
647
  private async compileIntegerSchema(def: LexiconInteger): Promise<string> {
638
648
  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
- )
649
+ const schema: l.IntegerSchema = l.integer(def)
650
+ schema.assert(def.const)
644
651
  }
645
652
 
646
653
  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
- }
654
+ const schema: l.IntegerSchema = l.integer(def)
655
+ for (const val of def.enum) schema.assert(val)
651
656
  }
652
657
 
653
658
  if (hasConst(def)) return this.compileConstSchema(def)
654
659
  if (hasEnum(def)) return this.compileEnumSchema(def)
655
660
 
656
- const options = stringifyOptionalOptions(def, ['type', 'description'])
661
+ const options = stringifyOptions(def, [
662
+ 'default',
663
+ 'maximum',
664
+ 'minimum',
665
+ ] satisfies (keyof l.IntegerSchemaOptions)[])
657
666
  return this.pure(`l.integer(${options})`)
658
667
  }
659
668
 
@@ -666,25 +675,24 @@ export class LexDefBuilder {
666
675
 
667
676
  private async compileStringSchema(def: LexiconString): Promise<string> {
668
677
  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
- )
678
+ const schema: l.StringSchema = l.string(def)
679
+ schema.assert(def.const)
674
680
  } 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
- }
681
+ const schema: l.StringSchema = l.string(def)
682
+ for (const val of def.enum) schema.assert(val)
682
683
  }
683
684
 
684
685
  if (hasConst(def)) return this.compileConstSchema(def)
685
686
  if (hasEnum(def)) return this.compileEnumSchema(def)
686
687
 
687
- const options = stringifyOptionalOptions(def, ['type', 'description'])
688
+ const options = stringifyOptions(def, [
689
+ 'default',
690
+ 'format',
691
+ 'maxGraphemes',
692
+ 'minGraphemes',
693
+ 'maxLength',
694
+ 'minLength',
695
+ ] satisfies (keyof l.StringSchemaOptions)[])
688
696
  return this.pure(`l.string(${options})`)
689
697
  }
690
698
 
@@ -693,20 +701,32 @@ export class LexDefBuilder {
693
701
  if (hasEnum(def)) return this.compileEnumType(def)
694
702
 
695
703
  switch (def.format) {
704
+ case undefined:
705
+ break
696
706
  case 'datetime':
697
- return 'l.Datetime'
707
+ return 'l.DatetimeString'
698
708
  case 'uri':
699
- return 'l.Uri'
709
+ return 'l.UriString'
700
710
  case 'at-uri':
701
- return 'l.AtUri'
711
+ return 'l.AtUriString'
702
712
  case 'did':
703
- return 'l.Did'
713
+ return 'l.DidString'
704
714
  case 'handle':
705
- return 'l.Handle'
715
+ return 'l.HandleString'
706
716
  case 'at-identifier':
707
- return 'l.AtIdentifier'
717
+ return 'l.AtIdentifierString'
708
718
  case 'nsid':
709
- return 'l.Nsid'
719
+ return 'l.NsidString'
720
+ case 'tid':
721
+ return 'l.TidString'
722
+ case 'cid':
723
+ return 'l.CidString'
724
+ case 'language':
725
+ return 'l.LanguageString'
726
+ case 'record-key':
727
+ return 'l.RecordKeyString'
728
+ default:
729
+ throw new Error(`Unknown string format: ${def.format}`)
710
730
  }
711
731
 
712
732
  if (def.knownValues?.length) {
@@ -720,7 +740,10 @@ export class LexDefBuilder {
720
740
  }
721
741
 
722
742
  private async compileBytesSchema(def: LexiconBytes): Promise<string> {
723
- const options = stringifyOptionalOptions(def, ['type', 'description'])
743
+ const options = stringifyOptions(def, [
744
+ 'minLength',
745
+ 'maxLength',
746
+ ] satisfies (keyof l.BytesSchemaOptions)[])
724
747
  return this.pure(`l.bytes(${options})`)
725
748
  }
726
749
 
@@ -729,10 +752,12 @@ export class LexDefBuilder {
729
752
  }
730
753
 
731
754
  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'])
755
+ const opts = { ...def, allowLegacy: this.options.allowLegacyBlobs === true }
756
+ const options = stringifyOptions(opts, [
757
+ 'maxSize',
758
+ 'accept',
759
+ 'allowLegacy',
760
+ ] satisfies (keyof l.BlobSchemaOptions)[])
736
761
  return this.pure(`l.blob(${options})`)
737
762
  }
738
763
 
@@ -742,13 +767,12 @@ export class LexDefBuilder {
742
767
  : 'l.BlobRef'
743
768
  }
744
769
 
745
- private async compileCidLinkSchema(def: LexiconCid): Promise<string> {
746
- const options = stringifyOptionalOptions(def, ['type', 'description'])
747
- return this.pure(`l.cidLink(${options})`)
770
+ private async compileCidLinkSchema(_def: LexiconCid): Promise<string> {
771
+ return this.pure(`l.cidLink()`)
748
772
  }
749
773
 
750
774
  private async compileCidLinkType(_def: LexiconCid): Promise<string> {
751
- return 'l.CID'
775
+ return 'l.Cid'
752
776
  }
753
777
 
754
778
  private async compileRefSchema(def: LexiconRef): Promise<string> {
@@ -795,12 +819,15 @@ export class LexDefBuilder {
795
819
 
796
820
  private async compileConstSchema<
797
821
  T extends null | number | string | boolean,
798
- >(def: { const: T; enum?: readonly T[] }): Promise<string> {
822
+ >(def: { const: T; enum?: readonly T[]; default?: T }): Promise<string> {
799
823
  if (hasEnum(def) && !def.enum.includes(def.const)) {
800
824
  return this.pure(`l.never()`)
801
825
  }
802
826
 
803
- return this.pure(`l.literal(${JSON.stringify(def.const)})`)
827
+ const options = stringifyOptions(def, [
828
+ 'default',
829
+ ] satisfies (keyof l.LiteralSchemaOptions<any>)[])
830
+ return this.pure(`l.literal(${JSON.stringify(def.const)}, ${options})`)
804
831
  }
805
832
 
806
833
  private async compileConstType<
@@ -814,14 +841,18 @@ export class LexDefBuilder {
814
841
 
815
842
  private async compileEnumSchema<T extends null | number | string>(def: {
816
843
  enum: readonly T[]
844
+ default?: T
817
845
  }): Promise<string> {
818
846
  if (def.enum.length === 0) {
819
847
  return this.pure(`l.never()`)
820
848
  }
821
- if (def.enum.length === 1) {
849
+ if (def.enum.length === 1 && def.default === undefined) {
822
850
  return this.pure(`l.literal(${JSON.stringify(def.enum[0])})`)
823
851
  }
824
- return this.pure(`l.enum(${JSON.stringify(def.enum)})`)
852
+ const options = stringifyOptions(def, [
853
+ 'default',
854
+ ] satisfies (keyof l.EnumSchemaOptions<any>)[])
855
+ return this.pure(`l.enum(${JSON.stringify(def.enum)}, ${options})`)
825
856
  }
826
857
 
827
858
  private async compileEnumType<T extends null | number | string>(def: {
@@ -887,11 +918,14 @@ function compileJsDoc(description: string) {
887
918
  }`
888
919
  }
889
920
 
890
- function stringifyOptionalOptions<O extends Record<string, unknown>>(
921
+ function stringifyOptions<O extends Record<string, unknown>>(
891
922
  obj: O,
892
- omit?: (keyof O)[],
923
+ include?: (keyof O)[],
924
+ exclude?: (keyof O)[],
893
925
  ) {
894
- const filtered = Object.entries(obj).filter(([k]) => !omit?.includes(k))
926
+ const filtered = Object.entries(obj).filter(
927
+ ([k]) => (!include || include.includes(k)) && !exclude?.includes(k),
928
+ )
895
929
  return filtered.length ? JSON.stringify(Object.fromEntries(filtered)) : ''
896
930
  }
897
931
 
@@ -16,7 +16,7 @@ export class LexiconDirectoryIndexer extends LexiconIterableIndexer {
16
16
 
17
17
  type ReadLexiconsOptions = {
18
18
  lexicons: string
19
- ignoreErrors?: boolean
19
+ ignoreInvalidLexicons?: boolean
20
20
  }
21
21
 
22
22
  async function* readLexicons(
@@ -29,7 +29,7 @@ async function* readLexicons(
29
29
  yield lexiconDocumentSchema.parse(JSON.parse(data))
30
30
  } catch (cause) {
31
31
  const message = `Error parsing lexicon document ${filePath}`
32
- if (options.ignoreErrors) console.error(`${message}:`, cause)
32
+ if (options.ignoreInvalidLexicons) console.error(`${message}:`, cause)
33
33
  else throw new Error(message, { cause })
34
34
  }
35
35
  }
@@ -11,6 +11,10 @@ import {
11
11
  ucFirst,
12
12
  } from './util.js'
13
13
 
14
+ export type RefResolverOptions = {
15
+ importExt?: string
16
+ }
17
+
14
18
  export type ResolvedRef = {
15
19
  varName: string
16
20
  typeName: string
@@ -25,6 +29,7 @@ export class RefResolver {
25
29
  private doc: LexiconDocument,
26
30
  private file: SourceFile,
27
31
  private indexer: LexiconIndexer,
32
+ private options: RefResolverOptions,
28
33
  ) {}
29
34
 
30
35
  public readonly resolve = memoize(
@@ -124,7 +129,7 @@ export class RefResolver {
124
129
  const moduleSpecifier = `${asRelativePath(
125
130
  this.file.getDirectoryPath(),
126
131
  join('/', ...nsid.split('.')),
127
- )}.defs.js`
132
+ )}.defs${this.options.importExt ?? '.js'}`
128
133
 
129
134
  // Lets first make sure the referenced lexicon exists
130
135
  const srcDoc = await this.indexer.get(nsid)
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": ["../../../tsconfig/node.json"],
3
+ "include": ["./src"],
4
+ "exclude": ["**/*.test.ts"],
5
+ "compilerOptions": {
6
+ "noImplicitAny": true,
7
+ "importHelpers": true,
8
+ "target": "ES2023",
9
+ "rootDir": "./src",
10
+ "outDir": "./dist",
11
+ "types": ["node"]
12
+ }
13
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "include": [],
3
+ "references": [
4
+ { "path": "./tsconfig.build.json" },
5
+ { "path": "./tsconfig.tests.json" }
6
+ ]
7
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../../tsconfig/tests.json",
3
+ "include": ["./tests", "./src/**.test.ts"],
4
+ "compilerOptions": {
5
+ "noImplicitAny": true,
6
+ "rootDir": "./",
7
+ "baseUrl": "./"
8
+ }
9
+ }