An experimental TypeSpec syntax for Lexicon

wip

+40 -187
+1
packages/emitter/package.json
··· 36 36 "@typespec/compiler": "^1.4.0" 37 37 }, 38 38 "devDependencies": { 39 + "@atproto/lexicon": "^0.5.1", 39 40 "@types/node": "^20.0.0", 40 41 "@typespec/http": "^1.4.0", 41 42 "@typespec/openapi": "^1.4.0",
+39 -34
packages/emitter/src/emitter.ts
··· 6 6 Scalar, 7 7 Union, 8 8 Namespace, 9 - Operation, 10 9 StringLiteral, 11 10 NumericLiteral, 12 11 BooleanLiteral, ··· 26 25 } from "@typespec/compiler"; 27 26 import { join, dirname } from "path"; 28 27 import type { 29 - LexiconDocument, 30 - LexiconDefinition, 31 - LexiconObject, 32 - LexiconPrimitive, 33 - LexiconArray, 34 - LexiconRef, 35 - LexiconUnion, 36 - LexiconBlob, 37 - LexiconBytes, 38 - } from "./types.js"; 28 + LexiconDoc, 29 + LexUserType, 30 + LexObject, 31 + LexArray, 32 + LexBlob, 33 + LexPrimitive, 34 + LexIpldType, 35 + LexRefVariant, 36 + } from "@atproto/lexicon"; 37 + 39 38 import { 40 39 getMaxGraphemes, 41 40 getMinGraphemes, ··· 91 90 atIdentifier: "at-identifier", 92 91 }; 93 92 93 + // Array items can only be: primitives, IPLD types, refs/unions, or blobs 94 + type LexArrayItem = LexPrimitive | LexIpldType | LexRefVariant | LexBlob; 95 + 94 96 export class TylexEmitter { 95 - private lexicons = new Map<string, LexiconDocument>(); 97 + private lexicons = new Map<string, LexiconDoc>(); 96 98 private currentLexiconId: string | null = null; 97 99 98 100 constructor( ··· 258 260 this.currentLexiconId = null; 259 261 } 260 262 261 - private createLexicon(id: string, ns: Namespace): LexiconDocument { 263 + private createLexicon(id: string, ns: Namespace): LexiconDoc { 262 264 const description = getDoc(this.program, ns); 263 - const lexicon: LexiconDocument = description 265 + const lexicon: LexiconDoc = description 264 266 ? { lexicon: 1, id, description, defs: {} } 265 267 : { lexicon: 1, id, defs: {} }; 266 268 return lexicon; ··· 287 289 return modelDef; 288 290 } 289 291 290 - private addDefs(lexicon: LexiconDocument, ns: Namespace, models: Model[]) { 292 + private addDefs(lexicon: LexiconDoc, ns: Namespace, models: Model[]) { 291 293 for (const model of models) { 292 294 this.addModelToDefs(lexicon, model); 293 295 } ··· 301 303 } 302 304 } 303 305 304 - private addModelToDefs(lexicon: LexiconDocument, model: Model) { 306 + private addModelToDefs(lexicon: LexiconDoc, model: Model) { 305 307 if (model.name[0] !== model.name[0].toUpperCase()) { 306 308 this.program.reportDiagnostic({ 307 309 code: "invalid-model-name", ··· 338 340 lexicon.defs[defName] = this.addDescription(modelDef, description); 339 341 } 340 342 341 - private addScalarToDefs(lexicon: LexiconDocument, scalar: Scalar) { 343 + private addScalarToDefs(lexicon: LexiconDoc, scalar: Scalar) { 342 344 if (scalar.namespace?.name === "TypeSpec") return; 343 345 if (scalar.baseScalar?.namespace?.name === "TypeSpec") return; 344 346 ··· 348 350 lexicon.defs[defName] = this.addDescription(scalarDef, description); 349 351 } 350 352 351 - private addUnionToDefs(lexicon: LexiconDocument, union: Union) { 353 + private addUnionToDefs(lexicon: LexiconDoc, union: Union) { 352 354 const name = union.name; 353 355 if (!name) return; 354 356 ··· 385 387 ); 386 388 } 387 389 388 - private createBlobDef(model: Model): LexiconBlob { 389 - const blobDef: LexiconBlob = { type: "blob" }; 390 + private createBlobDef(model: Model): LexBlob { 391 + const blobDef: LexBlob = { type: "blob" }; 390 392 391 393 if (isTemplateInstance(model)) { 392 394 const templateArgs = model.templateMapper?.args; ··· 424 426 private processUnion( 425 427 unionType: Union, 426 428 prop?: ModelProperty, 427 - ): LexiconDefinition | null { 429 + ): LexUserType | null { 428 430 // Parse union variants 429 431 const variants = this.parseUnionVariants(unionType); 430 432 ··· 566 568 unionType: Union, 567 569 numericLiterals: number[], 568 570 prop?: ModelProperty, 569 - ): LexiconDefinition { 571 + ): LexUserType { 570 572 const primitive: any = { 571 573 type: "integer", 572 574 enum: numericLiterals, ··· 592 594 unionType: Union, 593 595 booleanLiterals: boolean[], 594 596 prop?: ModelProperty, 595 - ): LexiconDefinition { 597 + ): LexUserType { 596 598 const primitive: any = { 597 599 type: "boolean", 598 600 enum: booleanLiterals, ··· 618 620 unionType: Union, 619 621 stringLiterals: string[], 620 622 prop?: ModelProperty, 621 - ): LexiconDefinition { 623 + ): LexUserType { 622 624 // Use "enum" for @closed unions, "knownValues" for open unions 623 625 const isClosedUnion = isClosed(this.program, unionType); 624 626 const primitive: any = { ··· 659 661 unionType: Union, 660 662 variants: ReturnType<typeof this.parseUnionVariants>, 661 663 prop?: ModelProperty, 662 - ): LexiconDefinition | null { 664 + ): LexUserType | null { 663 665 // Validate: cannot mix refs and string literals 664 666 if (variants.stringLiterals.length > 0) { 665 667 this.program.reportDiagnostic({ ··· 698 700 } 699 701 700 702 private addOperationToDefs( 701 - lexicon: LexiconDocument, 703 + lexicon: LexiconDoc, 702 704 operation: any, 703 705 defName: string, 704 706 ) { ··· 917 919 private modelToLexiconObject( 918 920 model: Model, 919 921 includeModelDescription: boolean = true, 920 - ): LexiconObject { 922 + ): LexObject { 921 923 const required: string[] = []; 922 924 const nullable: string[] = []; 923 925 const properties: any = {}; ··· 979 981 type: Type, 980 982 prop?: ModelProperty, 981 983 isDefining?: boolean, 982 - ): LexiconDefinition | null { 984 + ): LexUserType | null { 983 985 const propDesc = prop ? getDoc(this.program, prop) : undefined; 984 986 985 987 switch (type.kind) { ··· 1007 1009 scalar: Scalar, 1008 1010 prop?: ModelProperty, 1009 1011 propDesc?: string, 1010 - ): LexiconDefinition | null { 1012 + ): LexUserType | null { 1011 1013 const primitive = this.scalarToLexiconPrimitive(scalar, prop); 1012 1014 if (!primitive) return null; 1013 1015 ··· 1027 1029 model: Model, 1028 1030 prop?: ModelProperty, 1029 1031 propDesc?: string, 1030 - ): LexiconDefinition | null { 1032 + ): LexUserType | null { 1031 1033 // 1. Check for Blob type 1032 1034 if (this.isBlob(model)) { 1033 1035 return this.addDescription(this.createBlobDef(model), propDesc); ··· 1064 1066 prop?: ModelProperty, 1065 1067 isDefining?: boolean, 1066 1068 propDesc?: string, 1067 - ): LexiconDefinition | null { 1069 + ): LexUserType | null { 1068 1070 // Check if this is a named union that should be referenced 1069 1071 if (!isDefining) { 1070 1072 const unionRef = this.getUnionReference(unionType); ··· 1079 1081 private scalarToLexiconPrimitive( 1080 1082 scalar: Scalar, 1081 1083 prop?: ModelProperty, 1082 - ): LexiconDefinition | null { 1084 + ): LexUserType | null { 1083 1085 // Check if this scalar (or its base) is bytes type 1084 1086 const isBytes = this.isScalarBytes(scalar); 1085 1087 ··· 1307 1309 private modelToLexiconArray( 1308 1310 model: Model, 1309 1311 prop?: ModelProperty, 1310 - ): LexiconArray | null { 1312 + ): LexArray | null { 1311 1313 const arrayModel = model.sourceModel || model; 1312 1314 const itemType = arrayModel.templateMapper?.args?.[0]; 1313 1315 ··· 1323 1325 return null; 1324 1326 } 1325 1327 1326 - const arrayDef: LexiconArray = { type: "array", items: itemDef }; 1328 + const arrayDef: LexArray = { 1329 + type: "array", 1330 + items: itemDef as LexArrayItem, 1331 + }; 1327 1332 1328 1333 if (prop) { 1329 1334 const maxItems = getMaxItems(this.program, prop);
-153
packages/emitter/src/types.ts
··· 1 - export interface LexiconDocument { 2 - lexicon: 1; 3 - id: string; 4 - revision?: number; 5 - description?: string; 6 - defs: Record<string, LexiconDefinition>; 7 - } 8 - 9 - export type LexiconDefinition = 10 - | LexiconRecord 11 - | LexiconQuery 12 - | LexiconProcedure 13 - | LexiconSubscription 14 - | LexiconObject 15 - | LexiconToken 16 - | LexiconArray 17 - | LexiconBlob 18 - | LexiconPrimitive 19 - | LexiconRef 20 - | LexiconUnion 21 - | LexiconUnknown 22 - | LexiconCidLink 23 - | LexiconBytes 24 - | LexiconParams; 25 - 26 - export interface LexiconRecord { 27 - type: "record"; 28 - description?: string; 29 - key?: string; 30 - record: LexiconObject; 31 - } 32 - 33 - export interface LexiconQuery { 34 - type: "query"; 35 - description?: string; 36 - parameters?: LexiconParams; 37 - output?: LexiconXrpcBody; 38 - errors?: LexiconXrpcError[]; 39 - } 40 - 41 - export interface LexiconProcedure { 42 - type: "procedure"; 43 - description?: string; 44 - parameters?: LexiconParams; 45 - input?: LexiconXrpcBody; 46 - output?: LexiconXrpcBody; 47 - errors?: LexiconXrpcError[]; 48 - } 49 - 50 - export interface LexiconSubscription { 51 - type: "subscription"; 52 - description?: string; 53 - parameters?: LexiconParams; 54 - message?: LexiconXrpcMessage; 55 - errors?: LexiconXrpcError[]; 56 - } 57 - 58 - export interface LexiconParams { 59 - type: "params"; 60 - description?: string; 61 - required?: string[]; 62 - properties: Record<string, LexiconPrimitive | LexiconArray>; 63 - } 64 - 65 - export interface LexiconObject { 66 - type: "object"; 67 - description?: string; 68 - required?: string[]; 69 - nullable?: string[]; 70 - properties?: Record<string, LexiconDefinition>; 71 - } 72 - 73 - export interface LexiconArray { 74 - type: "array"; 75 - description?: string; 76 - items: LexiconDefinition; 77 - minLength?: number; 78 - maxLength?: number; 79 - } 80 - 81 - export interface LexiconToken { 82 - type: "token"; 83 - description?: string; 84 - } 85 - 86 - export interface LexiconBlob { 87 - type: "blob"; 88 - description?: string; 89 - accept?: string[]; 90 - maxSize?: number; 91 - } 92 - 93 - export interface LexiconPrimitive { 94 - type: "string" | "boolean" | "integer" | "number"; 95 - description?: string; 96 - default?: any; 97 - const?: any; 98 - enum?: string[] | number[]; 99 - minimum?: number; 100 - maximum?: number; 101 - minLength?: number; 102 - maxLength?: number; 103 - minGraphemes?: number; 104 - maxGraphemes?: number; 105 - format?: string; 106 - knownValues?: string[]; 107 - } 108 - 109 - export interface LexiconRef { 110 - type: "ref"; 111 - description?: string; 112 - ref: string; 113 - } 114 - 115 - export interface LexiconUnion { 116 - type: "union"; 117 - description?: string; 118 - refs: string[]; 119 - closed?: boolean; 120 - } 121 - 122 - export interface LexiconUnknown { 123 - type: "unknown"; 124 - description?: string; 125 - } 126 - 127 - export interface LexiconCidLink { 128 - type: "cid-link"; 129 - description?: string; 130 - } 131 - 132 - export interface LexiconBytes { 133 - type: "bytes"; 134 - description?: string; 135 - minLength?: number; 136 - maxLength?: number; 137 - } 138 - 139 - export interface LexiconXrpcBody { 140 - description?: string; 141 - encoding: string; 142 - schema?: LexiconDefinition; 143 - } 144 - 145 - export interface LexiconXrpcMessage { 146 - description?: string; 147 - schema?: LexiconDefinition; 148 - } 149 - 150 - export interface LexiconXrpcError { 151 - name: string; 152 - description?: string; 153 - }