An experimental TypeSpec syntax for Lexicon
56
fork

Configure Feed

Select the types of activity you want to include in your feed.

wip

+616 -323
-1
packages/emitter/package.json
··· 36 36 "@typespec/compiler": "^1.4.0" 37 37 }, 38 38 "devDependencies": { 39 - "@atproto/lexicon": "^0.5.1", 40 39 "@types/node": "^20.0.0", 41 40 "@typespec/http": "^1.4.0", 42 41 "@typespec/openapi": "^1.4.0",
+1 -1
packages/emitter/src/decorators.ts
··· 241 241 continue; 242 242 } 243 243 244 - const model = errorModel as any; 244 + const model = errorModel as Model; 245 245 246 246 // Validate that error model is empty (no properties) 247 247 if (model.properties && model.properties.size > 0) {
+404 -321
packages/emitter/src/emitter.ts
··· 10 10 NumericLiteral, 11 11 BooleanLiteral, 12 12 IntrinsicType, 13 + ArrayValue, 14 + StringValue, 15 + IndeterminateEntity, 13 16 getDoc, 14 17 getNamespaceFullName, 15 18 isTemplateInstance, ··· 22 25 getMinItems, 23 26 isArrayModelType, 24 27 serializeValueAsJson, 28 + Operation, 25 29 } from "@typespec/compiler"; 26 30 import { join, dirname } from "path"; 27 31 import type { 28 32 LexiconDoc, 29 - LexUserType, 30 33 LexObject, 31 34 LexArray, 32 35 LexBlob, 33 - LexPrimitive, 34 - LexIpldType, 36 + LexXrpcQuery, 37 + LexXrpcProcedure, 38 + LexXrpcSubscription, 39 + LexObjectProperty, 40 + LexArrayItem, 41 + LexXrpcParameterProperty, 42 + LexInteger, 43 + LexString, 44 + LexRefUnion, 45 + LexUserType, 46 + LexRecord, 47 + LexXrpcBody, 48 + LexXrpcParameters, 49 + LexBytes, 50 + LexCidLink, 51 + LexRef, 35 52 LexRefVariant, 36 - } from "@atproto/lexicon"; 53 + } from "./types.js"; 37 54 38 55 import { 39 56 getMaxGraphemes, ··· 89 106 language: "language", 90 107 atIdentifier: "at-identifier", 91 108 }; 92 - 93 - // Array items can only be: primitives, IPLD types, refs/unions, or blobs 94 - type LexArrayItem = LexPrimitive | LexIpldType | LexRefVariant | LexBlob; 95 109 96 110 export class TylexEmitter { 97 111 private lexicons = new Map<string, LexiconDoc>(); ··· 268 282 return lexicon; 269 283 } 270 284 271 - private createMainDef(mainModel: Model): any { 285 + private createMainDef(mainModel: Model): LexRecord | LexObject { 272 286 const modelDescription = getDoc(this.program, mainModel); 273 287 const recordKey = getRecordKey(this.program, mainModel); 274 288 const modelDef = this.modelToLexiconObject(mainModel, !!modelDescription); 275 289 276 290 if (recordKey) { 277 - const recordDef: any = { 291 + const recordDef: LexRecord = { 278 292 type: "record", 279 293 key: recordKey, 280 294 record: modelDef, ··· 321 335 const description = getDoc(this.program, model); 322 336 323 337 if (isToken(this.program, model)) { 324 - lexicon.defs[defName] = this.addDescription( 325 - { type: "token" }, 326 - description, 327 - ); 338 + lexicon.defs[defName] = { type: "token", description }; 328 339 return; 329 340 } 330 341 331 342 if (isArrayModelType(this.program, model)) { 332 343 const arrayDef = this.modelToLexiconArray(model); 333 344 if (arrayDef) { 334 - lexicon.defs[defName] = this.addDescription(arrayDef, description); 345 + lexicon.defs[defName] = { ...arrayDef, description }; 335 346 return; 336 347 } 337 348 } 338 349 339 350 const modelDef = this.modelToLexiconObject(model); 340 - lexicon.defs[defName] = this.addDescription(modelDef, description); 351 + lexicon.defs[defName] = { ...modelDef, description }; 341 352 } 342 353 343 354 private addScalarToDefs(lexicon: LexiconDoc, scalar: Scalar) { ··· 346 357 347 358 const defName = scalar.name.charAt(0).toLowerCase() + scalar.name.slice(1); 348 359 const scalarDef = this.scalarToLexiconPrimitive(scalar, undefined); 349 - const description = getDoc(this.program, scalar); 350 - lexicon.defs[defName] = this.addDescription(scalarDef, description); 360 + if (scalarDef) { 361 + const description = getDoc(this.program, scalar); 362 + lexicon.defs[defName] = { ...scalarDef, description } as LexUserType; 363 + } 351 364 } 352 365 353 366 private addUnionToDefs(lexicon: LexiconDoc, union: Union) { ··· 357 370 // Skip @inline unions - they should be inlined, not defined separately 358 371 if (isInline(this.program, union)) return; 359 372 360 - const unionDef: any = this.typeToLexiconDefinition(union, undefined, true); 373 + const unionDef = this.typeToLexiconDefinition(union, undefined, true); 361 374 if (!unionDef) return; 362 375 363 - if ( 364 - unionDef.type === "union" || 365 - (unionDef.type === "string" && (unionDef.knownValues || unionDef.enum)) 366 - ) { 376 + // Only string enums can be added as defs 377 + // Union refs (type: "union") must be inlined at usage sites 378 + if (unionDef.type === "string" && (unionDef.knownValues || unionDef.enum)) { 367 379 const defName = name.charAt(0).toLowerCase() + name.slice(1); 368 380 const description = getDoc(this.program, union); 369 - lexicon.defs[defName] = this.addDescription(unionDef, description); 381 + lexicon.defs[defName] = { ...unionDef, description }; 382 + } else if (unionDef.type === "union") { 383 + this.program.reportDiagnostic({ 384 + code: "union-refs-not-allowed-as-def", 385 + severity: "error", 386 + message: 387 + `Named unions of model references cannot be defined as standalone defs. ` + 388 + `Use @inline to inline them at usage sites, or use string enums instead.`, 389 + target: union, 390 + }); 370 391 } 371 392 } 372 393 373 - private addDescription(obj: any, description?: string): any { 374 - if (description && !obj.description) { 375 - obj.description = description; 376 - } 377 - return obj; 378 - } 379 394 380 395 private isBlob(model: Model): boolean { 381 - return !!( 382 - isBlob(this.program, model) || 383 - (isTemplateInstance(model) && 384 - model.templateNode && 385 - isBlob(this.program, model.templateNode as any)) || 386 - (model.baseModel && isBlob(this.program, model.baseModel)) 387 - ); 396 + if (isBlob(this.program, model)) return true; 397 + 398 + // Check base model 399 + if (model.baseModel && isBlob(this.program, model.baseModel)) return true; 400 + 401 + // For template instances, check the source model 402 + if ( 403 + isTemplateInstance(model) && 404 + model.sourceModel && 405 + isBlob(this.program, model.sourceModel) 406 + ) { 407 + return true; 408 + } 409 + 410 + return false; 388 411 } 389 412 390 413 private createBlobDef(model: Model): LexBlob { 391 414 const blobDef: LexBlob = { type: "blob" }; 392 415 393 - if (isTemplateInstance(model)) { 394 - const templateArgs = model.templateMapper?.args; 416 + // Check both the model itself and the sourceModel for template instances 417 + const templateModel = isTemplateInstance(model) 418 + ? model 419 + : model.sourceModel && isTemplateInstance(model.sourceModel) 420 + ? model.sourceModel 421 + : null; 422 + 423 + if (templateModel) { 424 + const templateArgs = templateModel.templateMapper?.args; 395 425 if (templateArgs?.length >= 2) { 396 - const acceptArg = templateArgs[0] as any; 426 + const acceptArg = templateArgs[0]; 397 427 let acceptTypes: string[] | undefined; 398 428 399 - if (acceptArg?.type?.kind === "Tuple") { 400 - const tuple = acceptArg.type; 401 - if (tuple.values?.length > 0) { 402 - acceptTypes = tuple.values 403 - .map((v: Type) => 404 - v.kind === "String" ? (v as StringLiteral).value : null, 405 - ) 406 - .filter((v: string | null) => v !== null) as string[]; 429 + // Handle ArrayValue 430 + if ( 431 + !isType(acceptArg) && 432 + (acceptArg as ArrayValue).valueKind === "ArrayValue" 433 + ) { 434 + const arrayValue = acceptArg as ArrayValue; 435 + if (arrayValue.values?.length > 0) { 436 + acceptTypes = arrayValue.values 437 + .map((v) => { 438 + if ((v as StringValue).valueKind === "StringValue") { 439 + return (v as StringValue).value; 440 + } 441 + return null; 442 + }) 443 + .filter((v) => v !== null) as string[]; 407 444 if (!acceptTypes.length) acceptTypes = undefined; 408 445 } 409 446 } 410 447 411 448 if (acceptTypes) blobDef.accept = acceptTypes; 412 449 413 - const maxSizeArg = templateArgs[1] as any; 414 - const maxSize = 415 - maxSizeArg?.value ?? 416 - (maxSizeArg?.type?.kind === "Number" 417 - ? Number(maxSizeArg.type.value) 418 - : undefined); 450 + const maxSizeArg = templateArgs[1]; 451 + let maxSize: number | undefined; 452 + 453 + // Handle IndeterminateEntity with Number type 454 + const indeterminate = maxSizeArg as IndeterminateEntity; 455 + if ( 456 + indeterminate.entityKind === "Indeterminate" && 457 + indeterminate.type && 458 + isType(indeterminate.type) && 459 + indeterminate.type.kind === "Number" 460 + ) { 461 + maxSize = (indeterminate.type as NumericLiteral).value; 462 + } 463 + 419 464 if (maxSize !== undefined && maxSize !== 0) blobDef.maxSize = maxSize; 420 465 } 421 466 } ··· 426 471 private processUnion( 427 472 unionType: Union, 428 473 prop?: ModelProperty, 429 - ): LexUserType | null { 474 + ): LexObjectProperty | null { 430 475 // Parse union variants 431 476 const variants = this.parseUnionVariants(unionType); 432 477 ··· 443 488 ); 444 489 } 445 490 446 - // Boolean enum (@closed only) 447 - if ( 448 - variants.booleanLiterals.length > 0 && 449 - variants.unionRefs.length === 0 && 450 - isClosed(this.program, unionType) 451 - ) { 452 - return this.createBooleanEnumDef( 453 - unionType, 454 - variants.booleanLiterals, 455 - prop, 456 - ); 491 + // Boolean literals are not supported in Lexicon 492 + if (variants.booleanLiterals.length > 0) { 493 + this.program.reportDiagnostic({ 494 + code: "boolean-literals-not-supported", 495 + severity: "error", 496 + message: 497 + "Boolean literal unions are not supported in Lexicon. Use boolean type with const or default instead.", 498 + target: unionType, 499 + }); 500 + return null; 457 501 } 458 502 459 503 // String enum (string literals with or without string type) ··· 568 612 unionType: Union, 569 613 numericLiterals: number[], 570 614 prop?: ModelProperty, 571 - ): LexUserType { 572 - const primitive: any = { 615 + ): LexInteger { 616 + const propDesc = prop ? getDoc(this.program, prop) : undefined; 617 + const defaultValue = prop?.defaultValue 618 + ? serializeValueAsJson(this.program, prop.defaultValue, prop) 619 + : undefined; 620 + 621 + return { 573 622 type: "integer", 574 623 enum: numericLiterals, 575 - }; 576 - 577 - // Add property-specific metadata 578 - if (prop) { 579 - const propDesc = getDoc(this.program, prop); 580 - if (propDesc) primitive.description = propDesc; 581 - 582 - const defaultValue = prop.defaultValue 583 - ? serializeValueAsJson(this.program, prop.defaultValue, prop) 584 - : undefined; 585 - if (defaultValue !== undefined && typeof defaultValue === "number") { 586 - primitive.default = defaultValue; 587 - } 588 - } 589 - 590 - return primitive; 591 - } 592 - 593 - private createBooleanEnumDef( 594 - unionType: Union, 595 - booleanLiterals: boolean[], 596 - prop?: ModelProperty, 597 - ): LexUserType { 598 - const primitive: any = { 599 - type: "boolean", 600 - enum: booleanLiterals, 624 + ...(propDesc && { description: propDesc }), 625 + ...(defaultValue !== undefined && 626 + typeof defaultValue === "number" && { default: defaultValue }), 601 627 }; 602 - 603 - // Add property-specific metadata 604 - if (prop) { 605 - const propDesc = getDoc(this.program, prop); 606 - if (propDesc) primitive.description = propDesc; 607 - 608 - const defaultValue = prop.defaultValue 609 - ? serializeValueAsJson(this.program, prop.defaultValue, prop) 610 - : undefined; 611 - if (defaultValue !== undefined && typeof defaultValue === "boolean") { 612 - primitive.default = defaultValue; 613 - } 614 - } 615 - 616 - return primitive; 617 628 } 618 629 619 630 private createStringEnumDef( 620 631 unionType: Union, 621 632 stringLiterals: string[], 622 633 prop?: ModelProperty, 623 - ): LexUserType { 624 - // Use "enum" for @closed unions, "knownValues" for open unions 634 + ): LexString { 625 635 const isClosedUnion = isClosed(this.program, unionType); 626 - const primitive: any = { 627 - type: "string", 628 - [isClosedUnion ? "enum" : "knownValues"]: stringLiterals, 629 - }; 636 + const propDesc = prop ? getDoc(this.program, prop) : undefined; 637 + const defaultValue = prop?.defaultValue 638 + ? serializeValueAsJson(this.program, prop.defaultValue, prop) 639 + : undefined; 630 640 631 - // Apply constraints 632 641 const maxLength = getMaxLength(this.program, unionType); 633 - if (maxLength !== undefined) primitive.maxLength = maxLength; 634 - 635 642 const minLength = getMinLength(this.program, unionType); 636 - if (minLength !== undefined) primitive.minLength = minLength; 637 - 638 643 const maxGraphemes = getMaxGraphemes(this.program, unionType); 639 - if (maxGraphemes !== undefined) primitive.maxGraphemes = maxGraphemes; 640 - 641 644 const minGraphemes = getMinGraphemes(this.program, unionType); 642 - if (minGraphemes !== undefined) primitive.minGraphemes = minGraphemes; 643 645 644 - // Add property-specific metadata 645 - if (prop) { 646 - const propDesc = getDoc(this.program, prop); 647 - if (propDesc) primitive.description = propDesc; 648 - 649 - const defaultValue = prop.defaultValue 650 - ? serializeValueAsJson(this.program, prop.defaultValue, prop) 651 - : undefined; 652 - if (defaultValue !== undefined && typeof defaultValue === "string") { 653 - primitive.default = defaultValue; 654 - } 655 - } 656 - 657 - return primitive; 646 + return { 647 + type: "string", 648 + [isClosedUnion ? "enum" : "knownValues"]: stringLiterals, 649 + ...(propDesc && { description: propDesc }), 650 + ...(defaultValue !== undefined && 651 + typeof defaultValue === "string" && { default: defaultValue }), 652 + ...(maxLength !== undefined && { maxLength }), 653 + ...(minLength !== undefined && { minLength }), 654 + ...(maxGraphemes !== undefined && { maxGraphemes }), 655 + ...(minGraphemes !== undefined && { minGraphemes }), 656 + }; 658 657 } 659 658 660 659 private createUnionRefDef( 661 660 unionType: Union, 662 661 variants: ReturnType<typeof this.parseUnionVariants>, 663 662 prop?: ModelProperty, 664 - ): LexUserType | null { 663 + ): LexRefUnion | null { 665 664 // Validate: cannot mix refs and string literals 666 665 if (variants.stringLiterals.length > 0) { 667 666 this.program.reportDiagnostic({ ··· 677 676 return null; 678 677 } 679 678 680 - const unionDef: any = { type: "union", refs: variants.unionRefs }; 681 - 682 - // Handle closed unions 683 - if (isClosed(this.program, unionType)) { 684 - if (variants.hasUnknown) { 685 - this.program.reportDiagnostic({ 686 - code: "closed-open-union", 687 - severity: "error", 688 - message: 689 - "@closed decorator cannot be used on open unions (unions containing 'unknown' or 'never'). " + 690 - "Remove the @closed decorator or make the union closed by removing 'unknown'/'never'.", 691 - target: unionType, 692 - }); 693 - } else { 694 - unionDef.closed = true; 695 - } 679 + const isClosedUnion = isClosed(this.program, unionType); 680 + if (isClosedUnion && variants.hasUnknown) { 681 + this.program.reportDiagnostic({ 682 + code: "closed-open-union", 683 + severity: "error", 684 + message: 685 + "@closed decorator cannot be used on open unions (unions containing 'unknown' or 'never'). " + 686 + "Remove the @closed decorator or make the union closed by removing 'unknown'/'never'.", 687 + target: unionType, 688 + }); 696 689 } 697 690 698 691 const propDesc = prop ? getDoc(this.program, prop) : undefined; 699 - return this.addDescription(unionDef, propDesc); 692 + 693 + return { 694 + type: "union", 695 + refs: variants.unionRefs, 696 + ...(propDesc && { description: propDesc }), 697 + ...(isClosedUnion && !variants.hasUnknown && { closed: true }), 698 + }; 700 699 } 701 700 702 701 private addOperationToDefs( 703 702 lexicon: LexiconDoc, 704 - operation: any, 703 + operation: Operation, 705 704 defName: string, 706 705 ) { 707 706 const description = getDoc(this.program, operation); 707 + const errors = getErrors(this.program, operation); 708 708 709 709 if (isQuery(this.program, operation)) { 710 - const queryDef: any = { type: "query" }; 711 - this.addDescription(queryDef, description); 712 - this.addParameters(queryDef, operation); 713 - this.addOutput(queryDef, operation); 714 - this.addErrors(queryDef, operation); 715 - lexicon.defs[defName] = queryDef; 710 + const parameters = this.buildParameters(operation); 711 + const output = this.buildOutput(operation); 712 + 713 + lexicon.defs[defName] = { 714 + type: "query", 715 + ...(description && { description }), 716 + ...(parameters && { parameters }), 717 + ...(output && { output }), 718 + ...(errors?.length && { errors }), 719 + } as LexXrpcQuery; 716 720 } else if (isProcedure(this.program, operation)) { 717 - const procedureDef: any = { type: "procedure" }; 718 - this.addDescription(procedureDef, description); 719 - this.addProcedureParams(procedureDef, operation); 720 - this.addOutput(procedureDef, operation); 721 - this.addErrors(procedureDef, operation); 722 - lexicon.defs[defName] = procedureDef; 721 + const { input, parameters } = this.buildProcedureParams(operation); 722 + const output = this.buildOutput(operation); 723 + 724 + lexicon.defs[defName] = { 725 + type: "procedure", 726 + ...(description && { description }), 727 + ...(input && { input }), 728 + ...(parameters && { parameters }), 729 + ...(output && { output }), 730 + ...(errors?.length && { errors }), 731 + } as LexXrpcProcedure; 723 732 } else if (isSubscription(this.program, operation)) { 724 - const subscriptionDef: any = { type: "subscription" }; 725 - this.addDescription(subscriptionDef, description); 726 - this.addParameters(subscriptionDef, operation); 727 - this.addMessage(subscriptionDef, operation); 728 - this.addErrors(subscriptionDef, operation); 729 - lexicon.defs[defName] = subscriptionDef; 733 + const parameters = this.buildParameters(operation); 734 + const message = this.buildMessage(operation); 735 + 736 + lexicon.defs[defName] = { 737 + type: "subscription", 738 + ...(description && { description }), 739 + ...(parameters && { parameters }), 740 + ...(message && { message }), 741 + ...(errors?.length && { errors }), 742 + } as LexXrpcSubscription; 730 743 } 731 744 } 732 745 733 - private addParameters(def: any, operation: any) { 734 - if (!operation.parameters?.properties?.size) return; 746 + private buildParameters(operation: Operation): LexXrpcParameters | undefined { 747 + if (!operation.parameters?.properties?.size) return undefined; 735 748 736 - const params: any = { type: "params", properties: {} }; 749 + const properties: Record<string, LexXrpcParameterProperty> = {}; 737 750 const required: string[] = []; 738 751 739 752 for (const [paramName, param] of operation.parameters.properties) { 740 753 const paramDef = this.typeToLexiconDefinition(param.type, param); 741 - if (paramDef) { 742 - params.properties[paramName] = paramDef; 754 + if (paramDef && this.isXrpcParameterProperty(paramDef)) { 755 + properties[paramName] = paramDef; 743 756 if (!param.optional) required.push(paramName); 744 757 } 745 758 } 746 759 747 - if (required.length) params.required = required; 748 - def.parameters = params; 760 + return { 761 + type: "params" as const, 762 + properties, 763 + ...(required.length && { required }), 764 + }; 749 765 } 750 766 751 - private addProcedureParams(def: any, operation: any) { 752 - if (!operation.parameters?.properties?.size) return; 767 + private isXrpcParameterProperty( 768 + type: LexObjectProperty, 769 + ): type is LexXrpcParameterProperty { 770 + // XRPC parameters can only be primitives or arrays of primitives 771 + if (type.type === "array") { 772 + const arrayType = type as LexArray; 773 + return ( 774 + arrayType.items.type === "boolean" || 775 + arrayType.items.type === "integer" || 776 + arrayType.items.type === "string" || 777 + arrayType.items.type === "unknown" 778 + ); 779 + } 780 + return ( 781 + type.type === "boolean" || 782 + type.type === "integer" || 783 + type.type === "string" || 784 + type.type === "unknown" 785 + ); 786 + } 787 + 788 + private buildProcedureParams(operation: Operation): { 789 + input?: LexXrpcBody; 790 + parameters?: LexXrpcParameters; 791 + } { 792 + if (!operation.parameters?.properties?.size) return {}; 753 793 754 794 const params = Array.from(operation.parameters.properties) as [ 755 795 string, 756 - any, 796 + ModelProperty, 757 797 ][]; 758 798 const paramCount = params.length; 759 799 ··· 766 806 "Procedures can have at most 2 parameters (input and/or parameters)", 767 807 target: operation, 768 808 }); 769 - return; 809 + return {}; 770 810 } 771 811 772 812 // Handle parameter count cases 773 - switch (paramCount) { 774 - case 1: 775 - this.handleSingleProcedureParam(def, params[0], operation); 776 - break; 777 - case 2: 778 - this.handleTwoProcedureParams(def, params[0], params[1], operation); 779 - break; 813 + if (paramCount === 1) { 814 + return this.handleSingleProcedureParam(params[0], operation); 815 + } else if (paramCount === 2) { 816 + return this.handleTwoProcedureParams(params[0], params[1], operation); 780 817 } 818 + 819 + return {}; 781 820 } 782 821 783 822 private handleSingleProcedureParam( 784 - def: any, 785 - [paramName, param]: [string, any], 786 - operation: any, 787 - ) { 823 + [paramName, param]: [string, ModelProperty], 824 + operation: Operation, 825 + ): { input?: LexXrpcBody; parameters?: LexXrpcParameters } { 788 826 // Validate parameter name 789 827 if (paramName !== "input") { 790 828 this.program.reportDiagnostic({ ··· 793 831 message: `Procedure parameter must be named "input", got "${paramName}"`, 794 832 target: param, 795 833 }); 796 - return; 834 + return {}; 797 835 } 798 836 799 - this.addInput(def, param); 837 + const input = this.buildInput(param); 838 + return input ? { input } : {}; 800 839 } 801 840 802 841 private handleTwoProcedureParams( 803 - def: any, 804 - [param1Name, param1]: [string, any], 805 - [param2Name, param2]: [string, any], 806 - operation: any, 807 - ) { 842 + [param1Name, param1]: [string, ModelProperty], 843 + [param2Name, param2]: [string, ModelProperty], 844 + operation: Operation, 845 + ): { input?: LexXrpcBody; parameters?: LexXrpcParameters } { 808 846 // Validate first parameter (input) 809 847 if (param1Name !== "input") { 810 848 this.program.reportDiagnostic({ ··· 836 874 }); 837 875 } 838 876 839 - // Add input 840 - this.addInput(def, param1); 877 + const input = this.buildInput(param1); 878 + const parameters = this.buildParametersFromModel(param2.type as Model); 841 879 842 - // Add parameters 843 - this.addParametersFromModel(def, param2.type as Model); 880 + return { 881 + ...(input && { input }), 882 + ...(parameters && { parameters }), 883 + }; 844 884 } 845 885 846 - private addParametersFromModel(def: any, parametersModel: Model) { 886 + private buildParametersFromModel( 887 + parametersModel: Model, 888 + ): LexXrpcParameters | undefined { 847 889 if (parametersModel.kind !== "Model" || !parametersModel.properties) { 848 - return; 890 + return undefined; 849 891 } 850 892 851 - const paramsObj: any = { type: "params", properties: {} }; 893 + const properties: Record<string, LexXrpcParameterProperty> = {}; 852 894 const required: string[] = []; 853 895 854 896 for (const [propName, prop] of parametersModel.properties) { 855 897 const propDef = this.typeToLexiconDefinition(prop.type, prop); 856 - if (propDef) { 857 - paramsObj.properties[propName] = propDef; 898 + if (propDef && this.isXrpcParameterProperty(propDef)) { 899 + properties[propName] = propDef; 858 900 if (!prop.optional) { 859 901 required.push(propName); 860 902 } 861 903 } 862 904 } 863 905 864 - if (required.length > 0) { 865 - paramsObj.required = required; 866 - } 867 - 868 - def.parameters = paramsObj; 906 + return { 907 + type: "params" as const, 908 + properties, 909 + ...(required.length > 0 && { required }), 910 + }; 869 911 } 870 912 871 - private addInput(def: any, param: any) { 913 + private buildInput(param: ModelProperty): LexXrpcBody | undefined { 872 914 const encoding = getEncoding(this.program, param); 873 915 if (param.type?.kind !== "Intrinsic") { 874 916 const inputSchema = this.typeToLexiconDefinition(param.type); 875 917 if (inputSchema) { 876 - def.input = { 877 - encoding: encoding || "application/json", 878 - schema: inputSchema, 879 - }; 918 + const validSchema = this.toValidBodySchema(inputSchema); 919 + if (validSchema) { 920 + return { 921 + encoding: encoding || "application/json", 922 + schema: validSchema, 923 + }; 924 + } 880 925 } 881 926 } else if (encoding) { 882 - def.input = { encoding }; 927 + return { encoding }; 883 928 } 929 + return undefined; 884 930 } 885 931 886 - private addOutput(def: any, operation: any) { 932 + private buildOutput(operation: Operation): LexXrpcBody | undefined { 887 933 const encoding = getEncoding(this.program, operation); 888 934 if (operation.returnType?.kind !== "Intrinsic") { 889 935 const schema = this.typeToLexiconDefinition(operation.returnType); 890 936 if (schema) { 891 - def.output = { encoding: encoding || "application/json", schema }; 937 + const validSchema = this.toValidBodySchema(schema); 938 + if (validSchema) { 939 + return { encoding: encoding || "application/json", schema: validSchema }; 940 + } 892 941 } 893 942 } else if (encoding) { 894 - def.output = { encoding }; 943 + return { encoding }; 895 944 } 945 + return undefined; 896 946 } 897 947 898 - private addMessage(def: any, operation: any) { 948 + private toValidBodySchema( 949 + schema: LexObjectProperty, 950 + ): LexRefVariant | LexObject | null { 951 + if ( 952 + schema.type === "ref" || 953 + schema.type === "union" || 954 + schema.type === "object" 955 + ) { 956 + return schema as LexRefVariant | LexObject; 957 + } 958 + return null; 959 + } 960 + 961 + private buildMessage( 962 + operation: Operation, 963 + ): { schema: LexRefUnion } | undefined { 899 964 if (operation.returnType?.kind === "Union") { 900 965 const messageSchema = this.typeToLexiconDefinition(operation.returnType); 901 - if (messageSchema) { 902 - def.message = { schema: messageSchema }; 966 + if (messageSchema && messageSchema.type === "union") { 967 + return { schema: messageSchema }; 903 968 } 904 969 } else if (operation.returnType?.kind !== "Intrinsic") { 905 970 this.program.reportDiagnostic({ ··· 909 974 target: operation, 910 975 }); 911 976 } 912 - } 913 - 914 - private addErrors(def: any, operation: any) { 915 - const errors = getErrors(this.program, operation); 916 - if (errors?.length) def.errors = errors; 977 + return undefined; 917 978 } 918 979 919 980 private modelToLexiconObject( ··· 922 983 ): LexObject { 923 984 const required: string[] = []; 924 985 const nullable: string[] = []; 925 - const properties: any = {}; 986 + const properties: Record<string, LexObjectProperty> = {}; 926 987 927 988 for (const [name, prop] of model.properties) { 928 989 if (!prop.optional) { ··· 966 1027 if (propDef) properties[name] = propDef; 967 1028 } 968 1029 969 - const obj: any = { type: "object" }; 970 1030 const description = includeModelDescription 971 1031 ? getDoc(this.program, model) 972 1032 : undefined; 973 - if (description) obj.description = description; 974 - if (required.length) obj.required = required; 975 - if (nullable.length) obj.nullable = nullable; 976 - obj.properties = properties; 977 - return obj; 1033 + 1034 + return { 1035 + type: "object", 1036 + properties, 1037 + ...(description && { description }), 1038 + ...(required.length && { required }), 1039 + ...(nullable.length && { nullable }), 1040 + }; 978 1041 } 979 1042 980 1043 private typeToLexiconDefinition( 981 1044 type: Type, 982 1045 prop?: ModelProperty, 983 1046 isDefining?: boolean, 984 - ): LexUserType | null { 1047 + ): LexObjectProperty | null { 985 1048 const propDesc = prop ? getDoc(this.program, prop) : undefined; 986 1049 987 1050 switch (type.kind) { ··· 992 1055 case "Union": 993 1056 return this.handleUnionType(type as Union, prop, isDefining, propDesc); 994 1057 case "Intrinsic": 995 - return this.addDescription({ type: "unknown" }, propDesc); 1058 + return { type: "unknown" as const, description: propDesc }; 996 1059 default: 997 1060 // Unhandled type kind 998 1061 this.program.reportDiagnostic({ ··· 1009 1072 scalar: Scalar, 1010 1073 prop?: ModelProperty, 1011 1074 propDesc?: string, 1012 - ): LexUserType | null { 1075 + ): LexObjectProperty | null { 1013 1076 const primitive = this.scalarToLexiconPrimitive(scalar, prop); 1014 1077 if (!primitive) return null; 1015 1078 1016 - if (propDesc) { 1017 - primitive.description = propDesc; 1018 - } else if (scalar.baseScalar && scalar.namespace?.name !== "TypeSpec") { 1019 - // For custom scalars that extend base types, inherit description if not a format scalar 1079 + // Determine description: prop description, or inherited scalar description for custom scalars 1080 + let description = propDesc; 1081 + if (!description && scalar.baseScalar && scalar.namespace?.name !== "TypeSpec") { 1020 1082 if (!FORMAT_SCALARS.has(scalar.name)) { 1021 - const scalarDesc = getDoc(this.program, scalar); 1022 - if (scalarDesc) primitive.description = scalarDesc; 1083 + description = getDoc(this.program, scalar); 1023 1084 } 1024 1085 } 1025 - return primitive; 1086 + 1087 + return { ...primitive, description }; 1026 1088 } 1027 1089 1028 1090 private handleModelType( 1029 1091 model: Model, 1030 1092 prop?: ModelProperty, 1031 1093 propDesc?: string, 1032 - ): LexUserType | null { 1094 + ): LexObjectProperty | null { 1033 1095 // 1. Check for Blob type 1034 1096 if (this.isBlob(model)) { 1035 - return this.addDescription(this.createBlobDef(model), propDesc); 1097 + return { ...this.createBlobDef(model), description: propDesc }; 1036 1098 } 1037 1099 1038 - // 2. Check for model reference (named models from other namespaces) 1100 + // 2. Check for token type - tokens must be referenced, not inlined 1101 + if (isToken(this.program, model)) { 1102 + const modelRef = this.getModelReference(model); 1103 + if (!modelRef) { 1104 + this.program.reportDiagnostic({ 1105 + code: "token-must-be-named", 1106 + severity: "error", 1107 + message: "Token types must be named and referenced, not used inline", 1108 + target: model, 1109 + }); 1110 + return null; 1111 + } 1112 + return { type: "ref" as const, ref: modelRef, description: propDesc }; 1113 + } 1114 + 1115 + // 3. Check for model reference (named models from other namespaces) 1039 1116 const modelRef = this.getModelReference(model); 1040 1117 if (modelRef) { 1041 - return this.addDescription({ type: "ref", ref: modelRef }, propDesc); 1118 + return { type: "ref" as const, ref: modelRef, description: propDesc }; 1042 1119 } 1043 1120 1044 1121 // 3. Check for array type ··· 1054 1131 }); 1055 1132 return null; 1056 1133 } 1057 - return this.addDescription(arrayDef, propDesc); 1134 + return { ...arrayDef, description: propDesc }; 1058 1135 } 1059 1136 1060 1137 // 4. Inline object 1061 - return this.addDescription(this.modelToLexiconObject(model), propDesc); 1138 + const objDef = this.modelToLexiconObject(model); 1139 + // Only add propDesc if the object doesn't already have a description 1140 + return propDesc && !objDef.description ? { ...objDef, description: propDesc } : objDef; 1062 1141 } 1063 1142 1064 1143 private handleUnionType( ··· 1066 1145 prop?: ModelProperty, 1067 1146 isDefining?: boolean, 1068 1147 propDesc?: string, 1069 - ): LexUserType | null { 1148 + ): LexObjectProperty | null { 1070 1149 // Check if this is a named union that should be referenced 1071 1150 if (!isDefining) { 1072 1151 const unionRef = this.getUnionReference(unionType); 1073 1152 if (unionRef) { 1074 - return this.addDescription({ type: "ref", ref: unionRef }, propDesc); 1153 + return { type: "ref" as const, ref: unionRef, description: propDesc }; 1075 1154 } 1076 1155 } 1077 1156 ··· 1081 1160 private scalarToLexiconPrimitive( 1082 1161 scalar: Scalar, 1083 1162 prop?: ModelProperty, 1084 - ): LexUserType | null { 1163 + ): LexObjectProperty | null { 1085 1164 // Check if this scalar (or its base) is bytes type 1086 - const isBytes = this.isScalarBytes(scalar); 1087 - 1088 - if (isBytes) { 1089 - const byteDef: any = { type: "bytes" }; 1090 - 1091 - // Apply byte constraints 1092 - this.applyBytesConstraints(byteDef, prop || scalar); 1093 - 1094 - // Apply property-specific metadata 1095 - if (prop) { 1096 - this.applyPropertyMetadata(byteDef, prop); 1097 - } 1098 - 1165 + if (this.isScalarBytes(scalar)) { 1166 + let byteDef: LexBytes = { type: "bytes" }; 1167 + byteDef = this.applyBytesConstraints(byteDef, prop || scalar); 1168 + if (prop) byteDef = this.applyPropertyMetadata(byteDef, prop); 1099 1169 return byteDef; 1100 1170 } 1101 1171 1102 1172 // Check if this scalar (or its base) is cidLink type 1103 - const isCidLink = this.isScalarCidLink(scalar); 1104 - 1105 - if (isCidLink) { 1106 - const cidLinkDef: any = { type: "cid-link" }; 1107 - 1108 - // Apply property-specific metadata 1109 - if (prop) { 1110 - this.applyPropertyMetadata(cidLinkDef, prop); 1111 - } 1112 - 1173 + if (this.isScalarCidLink(scalar)) { 1174 + let cidLinkDef: LexCidLink = { type: "cid-link" }; 1175 + if (prop) cidLinkDef = this.applyPropertyMetadata(cidLinkDef, prop); 1113 1176 return cidLinkDef; 1114 1177 } 1115 1178 1116 - // Determine base primitive type 1117 - const primitive: any = this.getBasePrimitiveType(scalar); 1179 + // Build primitive with constraints and metadata 1180 + let primitive = this.getBasePrimitiveType(scalar); 1118 1181 1119 1182 // Apply format if applicable 1120 1183 const format = FORMAT_MAP[scalar.name]; 1121 - if (format) primitive.format = format; 1184 + if (format && primitive.type === "string") { 1185 + primitive = { ...primitive, format }; 1186 + } 1122 1187 1123 1188 // Apply constraints 1124 - this.applyStringConstraints(primitive, prop || scalar); 1125 - this.applyNumericConstraints(primitive, prop); 1189 + primitive = this.applyStringConstraints(primitive, prop || scalar); 1190 + primitive = this.applyNumericConstraints(primitive, prop); 1126 1191 1127 1192 // Apply property-specific metadata 1128 1193 if (prop) { 1129 - this.applyPropertyMetadata(primitive, prop); 1194 + primitive = this.applyPropertyMetadata(primitive, prop); 1130 1195 } 1131 1196 1132 1197 return primitive; ··· 1144 1209 return false; 1145 1210 } 1146 1211 1147 - private getBasePrimitiveType(scalar: Scalar): any { 1212 + private getBasePrimitiveType(scalar: Scalar): LexObjectProperty { 1148 1213 if (scalar.name === "boolean") { 1149 1214 return { type: "boolean" }; 1150 1215 } else if ( ··· 1152 1217 ) { 1153 1218 return { type: "integer" }; 1154 1219 } else if (["float32", "float64"].includes(scalar.name)) { 1155 - return { type: "number" }; 1220 + return { type: "integer" }; // Note: lexicon uses integer for floats 1156 1221 } 1157 1222 return { type: "string" }; 1158 1223 } 1159 1224 1160 1225 private applyStringConstraints( 1161 - primitive: any, 1226 + primitive: LexObjectProperty, 1162 1227 target: Scalar | ModelProperty, 1163 - ) { 1228 + ): LexObjectProperty { 1229 + if (primitive.type !== "string") return primitive; 1230 + 1231 + const result = { ...primitive }; 1164 1232 const maxLength = getMaxLength(this.program, target); 1165 - if (maxLength !== undefined) primitive.maxLength = maxLength; 1233 + if (maxLength !== undefined) result.maxLength = maxLength; 1166 1234 1167 1235 const minLength = getMinLength(this.program, target); 1168 - if (minLength !== undefined) primitive.minLength = minLength; 1236 + if (minLength !== undefined) result.minLength = minLength; 1169 1237 1170 1238 const maxGraphemes = getMaxGraphemes(this.program, target); 1171 - if (maxGraphemes !== undefined) primitive.maxGraphemes = maxGraphemes; 1239 + if (maxGraphemes !== undefined) result.maxGraphemes = maxGraphemes; 1172 1240 1173 1241 const minGraphemes = getMinGraphemes(this.program, target); 1174 - if (minGraphemes !== undefined) primitive.minGraphemes = minGraphemes; 1242 + if (minGraphemes !== undefined) result.minGraphemes = minGraphemes; 1243 + 1244 + return result; 1175 1245 } 1176 1246 1177 - private applyBytesConstraints(byteDef: any, target: Scalar | ModelProperty) { 1247 + private applyBytesConstraints( 1248 + byteDef: LexBytes, 1249 + target: Scalar | ModelProperty, 1250 + ): LexBytes { 1251 + const result = { ...byteDef }; 1178 1252 const minLength = getMinBytes(this.program, target); 1179 - if (minLength !== undefined) byteDef.minLength = minLength; 1253 + if (minLength !== undefined) result.minLength = minLength; 1180 1254 1181 1255 const maxLength = getMaxBytes(this.program, target); 1182 - if (maxLength !== undefined) byteDef.maxLength = maxLength; 1256 + if (maxLength !== undefined) result.maxLength = maxLength; 1257 + 1258 + return result; 1183 1259 } 1184 1260 1185 - private applyNumericConstraints(primitive: any, prop?: ModelProperty) { 1186 - if ( 1187 - !prop || 1188 - (primitive.type !== "integer" && primitive.type !== "number") 1189 - ) { 1190 - return; 1191 - } 1261 + private applyNumericConstraints( 1262 + primitive: LexObjectProperty, 1263 + prop?: ModelProperty, 1264 + ): LexObjectProperty { 1265 + if (!prop || primitive.type !== "integer") return primitive; 1192 1266 1267 + const result = { ...primitive }; 1193 1268 const minValue = getMinValue(this.program, prop); 1194 - if (minValue !== undefined) primitive.minimum = minValue; 1269 + if (minValue !== undefined) result.minimum = minValue; 1195 1270 1196 1271 const maxValue = getMaxValue(this.program, prop); 1197 - if (maxValue !== undefined) primitive.maximum = maxValue; 1272 + if (maxValue !== undefined) result.maximum = maxValue; 1273 + 1274 + return result; 1198 1275 } 1199 1276 1200 - private applyPropertyMetadata(primitive: any, prop: ModelProperty) { 1201 - // Check if @readOnly is present 1277 + private applyPropertyMetadata<T extends LexObjectProperty>( 1278 + primitive: T, 1279 + prop: ModelProperty, 1280 + ): T { 1202 1281 const hasReadOnly = isReadOnly(this.program, prop); 1203 - 1204 - // Apply default value as const if @readOnly is present 1205 - // In TypeSpec 1.x, the default value is accessed via defaultValue property 1206 1282 const defaultValue = prop.defaultValue 1207 1283 ? serializeValueAsJson(this.program, prop.defaultValue, prop) 1208 1284 : undefined; ··· 1216 1292 message: `@readOnly is only valid for string, boolean, and integer types, but found type: ${primitive.type}`, 1217 1293 target: prop, 1218 1294 }); 1219 - return; 1295 + return primitive; 1220 1296 } 1221 1297 1222 1298 if (defaultValue === undefined) { ··· 1226 1302 message: "@readOnly requires a default value assignment", 1227 1303 target: prop, 1228 1304 }); 1229 - return; 1305 + return primitive; 1230 1306 } 1231 1307 1232 - // Set const value from default, don't emit default field 1233 - primitive.const = defaultValue; 1234 - } else if ( 1308 + // Set const value from default 1309 + return { ...primitive, const: defaultValue } as T; 1310 + } 1311 + 1312 + if ( 1235 1313 defaultValue !== undefined && 1236 1314 this.isValidDefaultForType(primitive.type, defaultValue) 1237 1315 ) { 1238 1316 // Normal default value (no @readOnly) 1239 - primitive.default = defaultValue; 1317 + return { ...primitive, default: defaultValue } as T; 1240 1318 } 1319 + 1320 + return primitive; 1241 1321 } 1242 1322 1243 - private isValidConstForType(primitiveType: string, constValue: any): boolean { 1323 + private isValidConstForType( 1324 + primitiveType: string, 1325 + constValue: unknown, 1326 + ): boolean { 1244 1327 return ( 1245 1328 (primitiveType === "boolean" && typeof constValue === "boolean") || 1246 1329 (primitiveType === "string" && typeof constValue === "string") || ··· 1250 1333 1251 1334 private isValidDefaultForType( 1252 1335 primitiveType: string, 1253 - defaultValue: any, 1336 + defaultValue: unknown, 1254 1337 ): boolean { 1255 1338 return ( 1256 1339 (primitiveType === "string" && typeof defaultValue === "string") ||
+211
packages/emitter/src/types.ts
··· 1 + /** 2 + * Simplified TypeSpec emitter types for Atproto Lexicon 3 + * 4 + * These types mirror the atproto lexicon structure but use plain TypeScript 5 + * instead of Zod schemas. This avoids type conflicts from discriminated unions 6 + * while remaining structurally compatible at runtime. 7 + */ 8 + 9 + // Primitives 10 + export type LexBoolean = { 11 + type: "boolean"; 12 + description?: string; 13 + default?: boolean; 14 + const?: boolean; 15 + }; 16 + 17 + export type LexInteger = { 18 + type: "integer"; 19 + description?: string; 20 + default?: number; 21 + minimum?: number; 22 + maximum?: number; 23 + enum?: number[]; 24 + const?: number; 25 + }; 26 + 27 + export type LexString = { 28 + type: "string"; 29 + format?: string; 30 + description?: string; 31 + default?: string; 32 + minLength?: number; 33 + maxLength?: number; 34 + minGraphemes?: number; 35 + maxGraphemes?: number; 36 + enum?: string[]; 37 + const?: string; 38 + knownValues?: string[]; 39 + }; 40 + 41 + export type LexUnknown = { 42 + type: "unknown"; 43 + description?: string; 44 + }; 45 + 46 + export type LexPrimitive = LexBoolean | LexInteger | LexString | LexUnknown; 47 + 48 + // IPLD types 49 + export type LexBytes = { 50 + type: "bytes"; 51 + description?: string; 52 + maxLength?: number; 53 + minLength?: number; 54 + }; 55 + 56 + export type LexCidLink = { 57 + type: "cid-link"; 58 + description?: string; 59 + }; 60 + 61 + export type LexIpldType = LexBytes | LexCidLink; 62 + 63 + // References 64 + export type LexRef = { 65 + type: "ref"; 66 + description?: string; 67 + ref: string; 68 + }; 69 + 70 + export type LexRefUnion = { 71 + type: "union"; 72 + description?: string; 73 + refs: string[]; 74 + closed?: boolean; 75 + }; 76 + 77 + export type LexRefVariant = LexRef | LexRefUnion; 78 + 79 + // Blobs 80 + export type LexBlob = { 81 + type: "blob"; 82 + description?: string; 83 + accept?: string[]; 84 + maxSize?: number; 85 + }; 86 + 87 + // Arrays 88 + export type LexArrayItem = LexPrimitive | LexIpldType | LexRefVariant | LexBlob; 89 + 90 + export type LexArray = { 91 + type: "array"; 92 + description?: string; 93 + items: LexArrayItem; 94 + minLength?: number; 95 + maxLength?: number; 96 + }; 97 + 98 + // Objects - use a looser type for properties to avoid discriminated union issues 99 + export type LexObjectProperty = 100 + | LexArray 101 + | LexPrimitive 102 + | LexIpldType 103 + | LexRefVariant 104 + | LexBlob 105 + | LexObject; // Allow nested objects 106 + 107 + export type LexObject = { 108 + type: "object"; 109 + description?: string; 110 + required?: string[]; 111 + nullable?: string[]; 112 + properties: Record<string, LexObjectProperty>; 113 + }; 114 + 115 + // Token 116 + export type LexToken = { 117 + type: "token"; 118 + description?: string; 119 + }; 120 + 121 + // XRPC types 122 + export type LexPrimitiveArray = { 123 + type: "array"; 124 + description?: string; 125 + items: LexPrimitive; 126 + minLength?: number; 127 + maxLength?: number; 128 + }; 129 + 130 + export type LexXrpcParameterProperty = LexPrimitive | LexPrimitiveArray; 131 + 132 + export type LexXrpcParameters = { 133 + type: "params"; 134 + description?: string; 135 + required?: string[]; 136 + properties: Record<string, LexXrpcParameterProperty>; 137 + }; 138 + 139 + export type LexXrpcBody = { 140 + description?: string; 141 + encoding: string; 142 + schema?: LexRefVariant | LexObject; 143 + }; 144 + 145 + export type LexXrpcSubscriptionMessage = { 146 + description?: string; 147 + schema?: LexRefVariant | LexObject; 148 + }; 149 + 150 + export type LexXrpcError = { 151 + name: string; 152 + description?: string; 153 + }; 154 + 155 + export type LexXrpcQuery = { 156 + type: "query"; 157 + description?: string; 158 + parameters?: LexXrpcParameters; 159 + output?: LexXrpcBody; 160 + errors?: LexXrpcError[]; 161 + }; 162 + 163 + export type LexXrpcProcedure = { 164 + type: "procedure"; 165 + description?: string; 166 + parameters?: LexXrpcParameters; 167 + input?: LexXrpcBody; 168 + output?: LexXrpcBody; 169 + errors?: LexXrpcError[]; 170 + }; 171 + 172 + export type LexXrpcSubscription = { 173 + type: "subscription"; 174 + description?: string; 175 + parameters?: LexXrpcParameters; 176 + message?: LexXrpcSubscriptionMessage; 177 + errors?: LexXrpcError[]; 178 + }; 179 + 180 + // Records 181 + export type LexRecord = { 182 + type: "record"; 183 + description?: string; 184 + key?: string; 185 + record: LexObject; 186 + }; 187 + 188 + // Union of all user-definable types 189 + export type LexUserType = 190 + | LexRecord 191 + | LexXrpcQuery 192 + | LexXrpcProcedure 193 + | LexXrpcSubscription 194 + | LexBlob 195 + | LexArray 196 + | LexToken 197 + | LexObject 198 + | LexPrimitive 199 + | LexIpldType; 200 + 201 + // Lexicon document 202 + export type LexiconDoc = { 203 + lexicon: 1; 204 + id: string; 205 + revision?: number; 206 + description?: string; 207 + defs: Record<string, LexUserType>; 208 + }; 209 + 210 + // Helper type for objects that can have descriptions 211 + export type WithDescription<T> = T & { description?: string };