An experimental TypeSpec syntax for Lexicon

wip

+266 -335
+1 -4
packages/emitter/lib/main.tsp
··· 29 * avatar: ImageBlob; 30 * ``` 31 */ 32 - @Tylex.Private.blob 33 - model Blob<Accept extends valueof unknown = #[], MaxSize extends valueof int32 = 0> { 34 - _blob: never; 35 - } 36 37 38 /**
··· 29 * avatar: ImageBlob; 30 * ``` 31 */ 32 + model Blob<Accept extends valueof unknown = #[], MaxSize extends valueof int32 = 0> {} 33 34 35 /**
-8
packages/emitter/lib/private.decorators.tsp
··· 5 * These are not intended for direct use in user code. 6 */ 7 namespace Tylex.Private; 8 - 9 - /** 10 - * Internal decorator that marks the Blob model. 11 - * This is applied automatically to the Blob model and should not be used directly. 12 - * 13 - * @param target The Blob model 14 - */ 15 - extern dec blob(target: TypeSpec.Reflection.Model);
··· 5 * These are not intended for direct use in user code. 6 */ 7 namespace Tylex.Private;
-13
packages/emitter/src/decorators.ts
··· 11 const maxGraphemesKey = Symbol("maxGraphemes"); 12 const minGraphemesKey = Symbol("minGraphemes"); 13 const recordKey = Symbol("record"); 14 - const blobKey = Symbol("blob"); 15 const requiredKey = Symbol("required"); 16 const readOnlyKey = Symbol("readOnly"); 17 const tokenKey = Symbol("token"); ··· 138 target: Type, 139 ): string | undefined { 140 return program.stateMap(recordKey).get(target); 141 - } 142 - 143 - /** 144 - * @blob private decorator for marking the Blob model 145 - */ 146 - export function $blob(context: DecoratorContext, target: Type) { 147 - // Mark this as a blob model 148 - context.program.stateSet(blobKey).add(target); 149 - } 150 - 151 - export function isBlob(program: Program, target: Type): boolean { 152 - return program.stateSet(blobKey).has(target); 153 } 154 155 /**
··· 11 const maxGraphemesKey = Symbol("maxGraphemes"); 12 const minGraphemesKey = Symbol("minGraphemes"); 13 const recordKey = Symbol("record"); 14 const requiredKey = Symbol("required"); 15 const readOnlyKey = Symbol("readOnly"); 16 const tokenKey = Symbol("token"); ··· 137 target: Type, 138 ): string | undefined { 139 return program.stateMap(recordKey).get(target); 140 } 141 142 /**
+265 -305
packages/emitter/src/emitter.ts
··· 56 getMaxGraphemes, 57 getMinGraphemes, 58 getRecordKey, 59 - isBlob, 60 isRequired, 61 isReadOnly, 62 isToken, ··· 260 261 private createLexicon(id: string, ns: Namespace): LexiconDoc { 262 const description = getDoc(this.program, ns); 263 - const lexicon: LexiconDoc = description 264 - ? { lexicon: 1, id, description, defs: {} } 265 - : { lexicon: 1, id, defs: {} }; 266 - return lexicon; 267 } 268 269 private createMainDef(mainModel: Model): LexRecord | LexObject { ··· 377 378 379 private isBlob(model: Model): boolean { 380 - if (isBlob(this.program, model)) return true; 381 382 - // Check base model 383 - if (model.baseModel && isBlob(this.program, model.baseModel)) return true; 384 385 - // For template instances, check the source model 386 - if ( 387 - isTemplateInstance(model) && 388 - model.sourceModel && 389 - isBlob(this.program, model.sourceModel) 390 - ) { 391 - return true; 392 } 393 394 return false; ··· 397 private createBlobDef(model: Model): LexBlob { 398 const blobDef: LexBlob = { type: "blob" }; 399 400 - // Check both the model itself and the sourceModel for template instances 401 - const templateModel = isTemplateInstance(model) 402 - ? model 403 - : model.sourceModel && isTemplateInstance(model.sourceModel) 404 - ? model.sourceModel 405 - : null; 406 407 - if (templateModel) { 408 - const templateArgs = templateModel.templateMapper?.args; 409 - if (templateArgs?.length >= 2) { 410 - const acceptArg = templateArgs[0]; 411 - let acceptTypes: string[] | undefined; 412 413 - // Handle ArrayValue 414 - if ( 415 - !isType(acceptArg) && 416 - (acceptArg as ArrayValue).valueKind === "ArrayValue" 417 - ) { 418 - const arrayValue = acceptArg as ArrayValue; 419 - if (arrayValue.values?.length > 0) { 420 - acceptTypes = arrayValue.values 421 - .map((v) => { 422 - if ((v as StringValue).valueKind === "StringValue") { 423 - return (v as StringValue).value; 424 - } 425 - return null; 426 - }) 427 - .filter((v) => v !== null) as string[]; 428 - if (!acceptTypes.length) acceptTypes = undefined; 429 - } 430 } 431 - 432 - if (acceptTypes) blobDef.accept = acceptTypes; 433 - 434 - const maxSizeArg = templateArgs[1]; 435 - let maxSize: number | undefined; 436 - 437 - // Handle IndeterminateEntity with Number type 438 - const indeterminate = maxSizeArg as IndeterminateEntity; 439 - if ( 440 - indeterminate.entityKind === "Indeterminate" && 441 - indeterminate.type && 442 - isType(indeterminate.type) && 443 - indeterminate.type.kind === "Number" 444 - ) { 445 - maxSize = (indeterminate.type as NumericLiteral).value; 446 - } 447 448 - if (maxSize !== undefined && maxSize !== 0) blobDef.maxSize = maxSize; 449 } 450 } 451 452 return blobDef; 453 } 454 455 - private processUnion( 456 unionType: Union, 457 prop?: ModelProperty, 458 ): LexObjectProperty | null { 459 - // Parse union variants 460 const variants = this.parseUnionVariants(unionType); 461 462 - // Integer enum (@closed only) 463 - if ( 464 - variants.numericLiterals.length > 0 && 465 - variants.unionRefs.length === 0 && 466 - isClosed(this.program, unionType) 467 - ) { 468 - return this.createIntegerEnumDef( 469 - unionType, 470 - variants.numericLiterals, 471 - prop, 472 - ); 473 - } 474 - 475 // Boolean literals are not supported in Lexicon 476 if (variants.booleanLiterals.length > 0) { 477 this.program.reportDiagnostic({ ··· 484 return null; 485 } 486 487 // String enum (string literals with or without string type) 488 // isStringEnum: has literals + string type + no refs 489 // Closed enum: has literals + no string type + no refs + @closed ··· 494 variants.unionRefs.length === 0 && 495 isClosed(this.program, unionType)) 496 ) { 497 - return this.createStringEnumDef(unionType, variants.stringLiterals, prop); 498 } 499 500 // Model reference union (including empty union with unknown) 501 if (variants.unionRefs.length > 0 || variants.hasUnknown) { 502 - return this.createUnionRefDef(unionType, variants, prop); 503 } 504 505 // Empty union without unknown ··· 592 }; 593 } 594 595 - private createIntegerEnumDef( 596 - unionType: Union, 597 - numericLiterals: number[], 598 - prop?: ModelProperty, 599 - ): LexInteger { 600 - const propDesc = prop ? getDoc(this.program, prop) : undefined; 601 - const defaultValue = prop?.defaultValue 602 - ? serializeValueAsJson(this.program, prop.defaultValue, prop) 603 - : undefined; 604 - 605 - return { 606 - type: "integer", 607 - enum: numericLiterals, 608 - ...(propDesc && { description: propDesc }), 609 - ...(defaultValue !== undefined && 610 - typeof defaultValue === "number" && { default: defaultValue }), 611 - }; 612 - } 613 - 614 - private createStringEnumDef( 615 - unionType: Union, 616 - stringLiterals: string[], 617 - prop?: ModelProperty, 618 - ): LexString { 619 - const isClosedUnion = isClosed(this.program, unionType); 620 - const propDesc = prop ? getDoc(this.program, prop) : undefined; 621 - const defaultValue = prop?.defaultValue 622 - ? serializeValueAsJson(this.program, prop.defaultValue, prop) 623 - : undefined; 624 - 625 - const maxLength = getMaxLength(this.program, unionType); 626 - const minLength = getMinLength(this.program, unionType); 627 - const maxGraphemes = getMaxGraphemes(this.program, unionType); 628 - const minGraphemes = getMinGraphemes(this.program, unionType); 629 - 630 - return { 631 - type: "string", 632 - [isClosedUnion ? "enum" : "knownValues"]: stringLiterals, 633 - ...(propDesc && { description: propDesc }), 634 - ...(defaultValue !== undefined && 635 - typeof defaultValue === "string" && { default: defaultValue }), 636 - ...(maxLength !== undefined && { maxLength }), 637 - ...(minLength !== undefined && { minLength }), 638 - ...(maxGraphemes !== undefined && { maxGraphemes }), 639 - ...(minGraphemes !== undefined && { minGraphemes }), 640 - }; 641 - } 642 - 643 - private createUnionRefDef( 644 - unionType: Union, 645 - variants: ReturnType<typeof this.parseUnionVariants>, 646 - prop?: ModelProperty, 647 - ): LexRefUnion | null { 648 - // Validate: cannot mix refs and string literals 649 - if (variants.stringLiterals.length > 0) { 650 - this.program.reportDiagnostic({ 651 - code: "union-mixed-refs-literals", 652 - severity: "error", 653 - message: 654 - `Union contains both model references and string literals. Atproto unions must be either: ` + 655 - `(1) model references only (type: "union"), or ` + 656 - `(2) string literals + string type (type: "string" with knownValues). ` + 657 - `Separate these into distinct fields or nested unions.`, 658 - target: unionType, 659 - }); 660 - return null; 661 - } 662 - 663 - const isClosedUnion = isClosed(this.program, unionType); 664 - if (isClosedUnion && variants.hasUnknown) { 665 - this.program.reportDiagnostic({ 666 - code: "closed-open-union", 667 - severity: "error", 668 - message: 669 - "@closed decorator cannot be used on open unions (unions containing 'unknown' or 'never'). " + 670 - "Remove the @closed decorator or make the union closed by removing 'unknown'/'never'.", 671 - target: unionType, 672 - }); 673 - } 674 - 675 - const propDesc = prop ? getDoc(this.program, prop) : undefined; 676 - 677 - return { 678 - type: "union", 679 - refs: variants.unionRefs, 680 - ...(propDesc && { description: propDesc }), 681 - ...(isClosedUnion && !variants.hasUnknown && { closed: true }), 682 - }; 683 - } 684 - 685 private addOperationToDefs( 686 lexicon: LexiconDoc, 687 operation: Operation, ··· 773 input?: LexXrpcBody; 774 parameters?: LexXrpcParameters; 775 } { 776 - if (!operation.parameters?.properties?.size) return {}; 777 778 const params = Array.from(operation.parameters.properties) as [ 779 string, 780 ModelProperty, 781 ][]; 782 - const paramCount = params.length; 783 784 - // Validate parameter count 785 - if (paramCount > 2) { 786 this.program.reportDiagnostic({ 787 code: "procedure-too-many-params", 788 severity: "error", ··· 793 return {}; 794 } 795 796 - // Handle parameter count cases 797 - if (paramCount === 1) { 798 - return this.handleSingleProcedureParam(params[0], operation); 799 - } else if (paramCount === 2) { 800 - return this.handleTwoProcedureParams(params[0], params[1], operation); 801 - } 802 - 803 - return {}; 804 - } 805 806 - private handleSingleProcedureParam( 807 - [paramName, param]: [string, ModelProperty], 808 - operation: Operation, 809 - ): { input?: LexXrpcBody; parameters?: LexXrpcParameters } { 810 - // Validate parameter name 811 - if (paramName !== "input") { 812 - this.program.reportDiagnostic({ 813 - code: "procedure-invalid-param-name", 814 - severity: "error", 815 - message: `Procedure parameter must be named "input", got "${paramName}"`, 816 - target: param, 817 - }); 818 - return {}; 819 } 820 821 - const input = this.buildInput(param); 822 - return input ? { input } : {}; 823 - } 824 825 - private handleTwoProcedureParams( 826 - [param1Name, param1]: [string, ModelProperty], 827 - [param2Name, param2]: [string, ModelProperty], 828 - operation: Operation, 829 - ): { input?: LexXrpcBody; parameters?: LexXrpcParameters } { 830 - // Validate first parameter (input) 831 if (param1Name !== "input") { 832 this.program.reportDiagnostic({ 833 code: "procedure-invalid-first-param", ··· 837 }); 838 } 839 840 - // Validate second parameter (parameters) 841 if (param2Name !== "parameters") { 842 this.program.reportDiagnostic({ 843 code: "procedure-invalid-second-param", ··· 847 }); 848 } 849 850 - // Validate that parameters is a plain object 851 if (param2.type.kind !== "Model" || (param2.type as Model).name) { 852 this.program.reportDiagnostic({ 853 code: "procedure-parameters-not-object", ··· 861 const input = this.buildInput(param1); 862 const parameters = this.buildParametersFromModel(param2.type as Model); 863 864 - return { 865 - ...(input && { input }), 866 - ...(parameters && { parameters }), 867 - }; 868 } 869 870 private buildParametersFromModel( ··· 896 897 private buildInput(param: ModelProperty): LexXrpcBody | undefined { 898 const encoding = getEncoding(this.program, param); 899 - if (param.type?.kind !== "Intrinsic") { 900 - const inputSchema = this.typeToLexiconDefinition(param.type); 901 - if (inputSchema) { 902 - const validSchema = this.toValidBodySchema(inputSchema); 903 - if (validSchema) { 904 - return { 905 - encoding: encoding || "application/json", 906 - schema: validSchema, 907 - }; 908 - } 909 - } 910 - } else if (encoding) { 911 - return { encoding }; 912 } 913 - return undefined; 914 } 915 916 private buildOutput(operation: Operation): LexXrpcBody | undefined { 917 const encoding = getEncoding(this.program, operation); 918 - if (operation.returnType?.kind !== "Intrinsic") { 919 - const schema = this.typeToLexiconDefinition(operation.returnType); 920 - if (schema) { 921 - const validSchema = this.toValidBodySchema(schema); 922 - if (validSchema) { 923 - return { encoding: encoding || "application/json", schema: validSchema }; 924 - } 925 - } 926 - } else if (encoding) { 927 - return { encoding }; 928 } 929 - return undefined; 930 } 931 932 private toValidBodySchema( ··· 1142 } 1143 } 1144 1145 - return this.processUnion(unionType, prop); 1146 } 1147 1148 private scalarToLexiconPrimitive( ··· 1151 ): LexObjectProperty | null { 1152 // Check if this scalar (or its base) is bytes type 1153 if (this.isScalarBytes(scalar)) { 1154 - let byteDef: LexBytes = { type: "bytes" }; 1155 - byteDef = this.applyBytesConstraints(byteDef, prop || scalar); 1156 - if (prop) byteDef = this.applyPropertyMetadata(byteDef, prop); 1157 return byteDef; 1158 } 1159 1160 // Check if this scalar (or its base) is cidLink type 1161 if (this.isScalarCidLink(scalar)) { 1162 - let cidLinkDef: LexCidLink = { type: "cid-link" }; 1163 - if (prop) cidLinkDef = this.applyPropertyMetadata(cidLinkDef, prop); 1164 return cidLinkDef; 1165 } 1166 ··· 1173 primitive = { ...primitive, format }; 1174 } 1175 1176 - // Apply constraints 1177 - primitive = this.applyStringConstraints(primitive, prop || scalar); 1178 - primitive = this.applyNumericConstraints(primitive, prop); 1179 1180 // Apply property-specific metadata 1181 if (prop) { ··· 1186 } 1187 1188 private isScalarBytes(scalar: Scalar): boolean { 1189 - if (scalar.name === "bytes") return true; 1190 - if (scalar.baseScalar) return this.isScalarBytes(scalar.baseScalar); 1191 - return false; 1192 } 1193 1194 private isScalarCidLink(scalar: Scalar): boolean { 1195 - if (scalar.name === "cidLink") return true; 1196 - if (scalar.baseScalar) return this.isScalarCidLink(scalar.baseScalar); 1197 - return false; 1198 } 1199 1200 private getBasePrimitiveType(scalar: Scalar): LexObjectProperty { 1201 if (scalar.name === "boolean") { 1202 return { type: "boolean" }; 1203 - } else if ( 1204 - ["integer", "int32", "int64", "int16", "int8"].includes(scalar.name) 1205 - ) { 1206 return { type: "integer" }; 1207 - } else if (["float32", "float64"].includes(scalar.name)) { 1208 - // Lexicon does not support floating-point numbers 1209 this.program.reportDiagnostic({ 1210 code: "float-not-supported", 1211 severity: "error", ··· 1214 }); 1215 return { type: "integer" }; 1216 } 1217 return { type: "string" }; 1218 - } 1219 - 1220 - private applyStringConstraints( 1221 - primitive: LexObjectProperty, 1222 - target: Scalar | ModelProperty, 1223 - ): LexObjectProperty { 1224 - if (primitive.type !== "string") return primitive; 1225 - 1226 - const result = { ...primitive }; 1227 - const maxLength = getMaxLength(this.program, target); 1228 - if (maxLength !== undefined) result.maxLength = maxLength; 1229 - 1230 - const minLength = getMinLength(this.program, target); 1231 - if (minLength !== undefined) result.minLength = minLength; 1232 - 1233 - const maxGraphemes = getMaxGraphemes(this.program, target); 1234 - if (maxGraphemes !== undefined) result.maxGraphemes = maxGraphemes; 1235 - 1236 - const minGraphemes = getMinGraphemes(this.program, target); 1237 - if (minGraphemes !== undefined) result.minGraphemes = minGraphemes; 1238 - 1239 - return result; 1240 - } 1241 - 1242 - private applyBytesConstraints( 1243 - byteDef: LexBytes, 1244 - target: Scalar | ModelProperty, 1245 - ): LexBytes { 1246 - const result = { ...byteDef }; 1247 - const minLength = getMinBytes(this.program, target); 1248 - if (minLength !== undefined) result.minLength = minLength; 1249 - 1250 - const maxLength = getMaxBytes(this.program, target); 1251 - if (maxLength !== undefined) result.maxLength = maxLength; 1252 - 1253 - return result; 1254 - } 1255 - 1256 - private applyNumericConstraints( 1257 - primitive: LexObjectProperty, 1258 - prop?: ModelProperty, 1259 - ): LexObjectProperty { 1260 - if (!prop || primitive.type !== "integer") return primitive; 1261 - 1262 - const result = { ...primitive }; 1263 - const minValue = getMinValue(this.program, prop); 1264 - if (minValue !== undefined) result.minimum = minValue; 1265 - 1266 - const maxValue = getMaxValue(this.program, prop); 1267 - if (maxValue !== undefined) result.maximum = maxValue; 1268 - 1269 - return result; 1270 } 1271 1272 private applyPropertyMetadata<T extends LexObjectProperty>(
··· 56 getMaxGraphemes, 57 getMinGraphemes, 58 getRecordKey, 59 isRequired, 60 isReadOnly, 61 isToken, ··· 259 260 private createLexicon(id: string, ns: Namespace): LexiconDoc { 261 const description = getDoc(this.program, ns); 262 + return { 263 + lexicon: 1, 264 + id, 265 + defs: {}, 266 + ...(description && { description }), 267 + }; 268 } 269 270 private createMainDef(mainModel: Model): LexRecord | LexObject { ··· 378 379 380 private isBlob(model: Model): boolean { 381 + // Check if model itself is named Blob 382 + if (model.name === "Blob") { 383 + return true; 384 + } 385 386 + // Check if it's a template instance of Blob 387 + if (isTemplateInstance(model) && model.sourceModel?.name === "Blob") { 388 + return true; 389 + } 390 391 + // Check base model (model ImageBlob extends Blob<...>) 392 + if (model.baseModel) { 393 + return this.isBlob(model.baseModel); 394 } 395 396 return false; ··· 399 private createBlobDef(model: Model): LexBlob { 400 const blobDef: LexBlob = { type: "blob" }; 401 402 + if (!isTemplateInstance(model)) { 403 + return blobDef; 404 + } 405 406 + const args = model.templateMapper?.args; 407 + if (!args?.length) { 408 + return blobDef; 409 + } 410 411 + // First arg: accept types (array of mime type strings) 412 + if (args.length >= 1) { 413 + const acceptArg = args[0]; 414 + if (isType(acceptArg) || (acceptArg as ArrayValue).valueKind !== "ArrayValue") { 415 + throw new Error("Blob template first argument must be an array of mime types"); 416 + } 417 + const arrayValue = acceptArg as ArrayValue; 418 + const acceptTypes = arrayValue.values.map(v => { 419 + if ((v as StringValue).valueKind !== "StringValue") { 420 + throw new Error("Blob accept types must be strings"); 421 } 422 + return (v as StringValue).value; 423 + }); 424 + if (acceptTypes.length > 0) { 425 + blobDef.accept = acceptTypes; 426 + } 427 + } 428 429 + // Second arg: maxSize (numeric literal) 430 + if (args.length >= 2) { 431 + const maxSizeArg = args[1] as IndeterminateEntity; 432 + if (!isType(maxSizeArg.type) || maxSizeArg.type.kind !== "Number") { 433 + throw new Error("Blob template second argument must be a numeric literal"); 434 + } 435 + const maxSize = (maxSizeArg.type as NumericLiteral).value; 436 + if (maxSize > 0) { 437 + blobDef.maxSize = maxSize; 438 } 439 } 440 441 return blobDef; 442 } 443 444 + private unionToLexiconProperty( 445 unionType: Union, 446 prop?: ModelProperty, 447 ): LexObjectProperty | null { 448 const variants = this.parseUnionVariants(unionType); 449 450 // Boolean literals are not supported in Lexicon 451 if (variants.booleanLiterals.length > 0) { 452 this.program.reportDiagnostic({ ··· 459 return null; 460 } 461 462 + // Integer enum (@closed only) 463 + if ( 464 + variants.numericLiterals.length > 0 && 465 + variants.unionRefs.length === 0 && 466 + isClosed(this.program, unionType) 467 + ) { 468 + const propDesc = prop ? getDoc(this.program, prop) : undefined; 469 + const defaultValue = prop?.defaultValue 470 + ? serializeValueAsJson(this.program, prop.defaultValue, prop) 471 + : undefined; 472 + 473 + return { 474 + type: "integer", 475 + enum: variants.numericLiterals, 476 + ...(propDesc && { description: propDesc }), 477 + ...(defaultValue !== undefined && 478 + typeof defaultValue === "number" && { default: defaultValue }), 479 + }; 480 + } 481 + 482 // String enum (string literals with or without string type) 483 // isStringEnum: has literals + string type + no refs 484 // Closed enum: has literals + no string type + no refs + @closed ··· 489 variants.unionRefs.length === 0 && 490 isClosed(this.program, unionType)) 491 ) { 492 + const isClosedUnion = isClosed(this.program, unionType); 493 + const propDesc = prop ? getDoc(this.program, prop) : undefined; 494 + const defaultValue = prop?.defaultValue 495 + ? serializeValueAsJson(this.program, prop.defaultValue, prop) 496 + : undefined; 497 + 498 + const maxLength = getMaxLength(this.program, unionType); 499 + const minLength = getMinLength(this.program, unionType); 500 + const maxGraphemes = getMaxGraphemes(this.program, unionType); 501 + const minGraphemes = getMinGraphemes(this.program, unionType); 502 + 503 + return { 504 + type: "string", 505 + [isClosedUnion ? "enum" : "knownValues"]: variants.stringLiterals, 506 + ...(propDesc && { description: propDesc }), 507 + ...(defaultValue !== undefined && 508 + typeof defaultValue === "string" && { default: defaultValue }), 509 + ...(maxLength !== undefined && { maxLength }), 510 + ...(minLength !== undefined && { minLength }), 511 + ...(maxGraphemes !== undefined && { maxGraphemes }), 512 + ...(minGraphemes !== undefined && { minGraphemes }), 513 + }; 514 } 515 516 // Model reference union (including empty union with unknown) 517 if (variants.unionRefs.length > 0 || variants.hasUnknown) { 518 + if (variants.stringLiterals.length > 0) { 519 + this.program.reportDiagnostic({ 520 + code: "union-mixed-refs-literals", 521 + severity: "error", 522 + message: 523 + `Union contains both model references and string literals. Atproto unions must be either: ` + 524 + `(1) model references only (type: "union"), or ` + 525 + `(2) string literals + string type (type: "string" with knownValues). ` + 526 + `Separate these into distinct fields or nested unions.`, 527 + target: unionType, 528 + }); 529 + return null; 530 + } 531 + 532 + const isClosedUnion = isClosed(this.program, unionType); 533 + if (isClosedUnion && variants.hasUnknown) { 534 + this.program.reportDiagnostic({ 535 + code: "closed-open-union", 536 + severity: "error", 537 + message: 538 + "@closed decorator cannot be used on open unions (unions containing 'unknown' or 'never'). " + 539 + "Remove the @closed decorator or make the union closed by removing 'unknown'/'never'.", 540 + target: unionType, 541 + }); 542 + } 543 + 544 + const propDesc = prop ? getDoc(this.program, prop) : undefined; 545 + 546 + return { 547 + type: "union", 548 + refs: variants.unionRefs, 549 + ...(propDesc && { description: propDesc }), 550 + ...(isClosedUnion && !variants.hasUnknown && { closed: true }), 551 + }; 552 } 553 554 // Empty union without unknown ··· 641 }; 642 } 643 644 private addOperationToDefs( 645 lexicon: LexiconDoc, 646 operation: Operation, ··· 732 input?: LexXrpcBody; 733 parameters?: LexXrpcParameters; 734 } { 735 + if (!operation.parameters?.properties?.size) { 736 + return {}; 737 + } 738 739 const params = Array.from(operation.parameters.properties) as [ 740 string, 741 ModelProperty, 742 ][]; 743 744 + if (params.length === 0) { 745 + return {}; 746 + } 747 + 748 + if (params.length > 2) { 749 this.program.reportDiagnostic({ 750 code: "procedure-too-many-params", 751 severity: "error", ··· 756 return {}; 757 } 758 759 + // Single parameter: must be named "input" 760 + if (params.length === 1) { 761 + const [paramName, param] = params[0]; 762 + if (paramName !== "input") { 763 + this.program.reportDiagnostic({ 764 + code: "procedure-invalid-param-name", 765 + severity: "error", 766 + message: `Procedure parameter must be named "input", got "${paramName}"`, 767 + target: param, 768 + }); 769 + return {}; 770 + } 771 772 + const input = this.buildInput(param); 773 + if (!input) { 774 + return {}; 775 + } 776 + return { input }; 777 } 778 779 + // Two parameters: must be "input" and "parameters" 780 + const [param1Name, param1] = params[0]; 781 + const [param2Name, param2] = params[1]; 782 783 if (param1Name !== "input") { 784 this.program.reportDiagnostic({ 785 code: "procedure-invalid-first-param", ··· 789 }); 790 } 791 792 if (param2Name !== "parameters") { 793 this.program.reportDiagnostic({ 794 code: "procedure-invalid-second-param", ··· 798 }); 799 } 800 801 if (param2.type.kind !== "Model" || (param2.type as Model).name) { 802 this.program.reportDiagnostic({ 803 code: "procedure-parameters-not-object", ··· 811 const input = this.buildInput(param1); 812 const parameters = this.buildParametersFromModel(param2.type as Model); 813 814 + const result: { input?: LexXrpcBody; parameters?: LexXrpcParameters } = {}; 815 + if (input) { 816 + result.input = input; 817 + } 818 + if (parameters) { 819 + result.parameters = parameters; 820 + } 821 + return result; 822 } 823 824 private buildParametersFromModel( ··· 850 851 private buildInput(param: ModelProperty): LexXrpcBody | undefined { 852 const encoding = getEncoding(this.program, param); 853 + 854 + if (param.type?.kind === "Intrinsic") { 855 + return encoding ? { encoding } : undefined; 856 + } 857 + 858 + const inputSchema = this.typeToLexiconDefinition(param.type); 859 + if (!inputSchema) { 860 + return undefined; 861 + } 862 + 863 + const validSchema = this.toValidBodySchema(inputSchema); 864 + if (!validSchema) { 865 + return undefined; 866 } 867 + 868 + return { 869 + encoding: encoding || "application/json", 870 + schema: validSchema, 871 + }; 872 } 873 874 private buildOutput(operation: Operation): LexXrpcBody | undefined { 875 const encoding = getEncoding(this.program, operation); 876 + 877 + if (operation.returnType?.kind === "Intrinsic") { 878 + return encoding ? { encoding } : undefined; 879 + } 880 + 881 + const schema = this.typeToLexiconDefinition(operation.returnType); 882 + if (!schema) { 883 + return undefined; 884 + } 885 + 886 + const validSchema = this.toValidBodySchema(schema); 887 + if (!validSchema) { 888 + return undefined; 889 } 890 + 891 + return { 892 + encoding: encoding || "application/json", 893 + schema: validSchema, 894 + }; 895 } 896 897 private toValidBodySchema( ··· 1107 } 1108 } 1109 1110 + return this.unionToLexiconProperty(unionType, prop); 1111 } 1112 1113 private scalarToLexiconPrimitive( ··· 1116 ): LexObjectProperty | null { 1117 // Check if this scalar (or its base) is bytes type 1118 if (this.isScalarBytes(scalar)) { 1119 + const byteDef: LexBytes = { type: "bytes" }; 1120 + const target = prop || scalar; 1121 + 1122 + const minLength = getMinBytes(this.program, target); 1123 + if (minLength !== undefined) { 1124 + byteDef.minLength = minLength; 1125 + } 1126 + 1127 + const maxLength = getMaxBytes(this.program, target); 1128 + if (maxLength !== undefined) { 1129 + byteDef.maxLength = maxLength; 1130 + } 1131 + 1132 + if (prop) { 1133 + return this.applyPropertyMetadata(byteDef, prop); 1134 + } 1135 return byteDef; 1136 } 1137 1138 // Check if this scalar (or its base) is cidLink type 1139 if (this.isScalarCidLink(scalar)) { 1140 + const cidLinkDef: LexCidLink = { type: "cid-link" }; 1141 + if (prop) { 1142 + return this.applyPropertyMetadata(cidLinkDef, prop); 1143 + } 1144 return cidLinkDef; 1145 } 1146 ··· 1153 primitive = { ...primitive, format }; 1154 } 1155 1156 + // Apply string constraints 1157 + if (primitive.type === "string") { 1158 + const target = prop || scalar; 1159 + 1160 + const maxLength = getMaxLength(this.program, target); 1161 + if (maxLength !== undefined) { 1162 + primitive.maxLength = maxLength; 1163 + } 1164 + 1165 + const minLength = getMinLength(this.program, target); 1166 + if (minLength !== undefined) { 1167 + primitive.minLength = minLength; 1168 + } 1169 + 1170 + const maxGraphemes = getMaxGraphemes(this.program, target); 1171 + if (maxGraphemes !== undefined) { 1172 + primitive.maxGraphemes = maxGraphemes; 1173 + } 1174 + 1175 + const minGraphemes = getMinGraphemes(this.program, target); 1176 + if (minGraphemes !== undefined) { 1177 + primitive.minGraphemes = minGraphemes; 1178 + } 1179 + } 1180 + 1181 + // Apply numeric constraints 1182 + if (prop && primitive.type === "integer") { 1183 + const minValue = getMinValue(this.program, prop); 1184 + if (minValue !== undefined) { 1185 + primitive.minimum = minValue; 1186 + } 1187 + 1188 + const maxValue = getMaxValue(this.program, prop); 1189 + if (maxValue !== undefined) { 1190 + primitive.maximum = maxValue; 1191 + } 1192 + } 1193 1194 // Apply property-specific metadata 1195 if (prop) { ··· 1200 } 1201 1202 private isScalarBytes(scalar: Scalar): boolean { 1203 + return scalar.name === "bytes" || (scalar.baseScalar ? this.isScalarBytes(scalar.baseScalar) : false); 1204 } 1205 1206 private isScalarCidLink(scalar: Scalar): boolean { 1207 + return scalar.name === "cidLink" || (scalar.baseScalar ? this.isScalarCidLink(scalar.baseScalar) : false); 1208 } 1209 1210 private getBasePrimitiveType(scalar: Scalar): LexObjectProperty { 1211 if (scalar.name === "boolean") { 1212 return { type: "boolean" }; 1213 + } 1214 + 1215 + if (["integer", "int32", "int64", "int16", "int8"].includes(scalar.name)) { 1216 return { type: "integer" }; 1217 + } 1218 + 1219 + if (["float32", "float64"].includes(scalar.name)) { 1220 this.program.reportDiagnostic({ 1221 code: "float-not-supported", 1222 severity: "error", ··· 1225 }); 1226 return { type: "integer" }; 1227 } 1228 + 1229 return { type: "string" }; 1230 } 1231 1232 private applyPropertyMetadata<T extends LexObjectProperty>(
-1
packages/emitter/src/index.ts
··· 21 $maxGraphemes, 22 $minGraphemes, 23 $rec, 24 - $blob, 25 $required, 26 $readOnly, 27 $token,
··· 21 $maxGraphemes, 22 $minGraphemes, 23 $rec, 24 $required, 25 $readOnly, 26 $token,
-4
packages/emitter/src/tsp-index.ts
··· 2 $maxGraphemes, 3 $minGraphemes, 4 $rec, 5 - $blob, 6 $required, 7 $readOnly, 8 $token, ··· 35 inline: $inline, 36 maxBytes: $maxBytes, 37 minBytes: $minBytes, 38 - }, 39 - "Tylex.Private": { 40 - blob: $blob, 41 }, 42 };
··· 2 $maxGraphemes, 3 $minGraphemes, 4 $rec, 5 $required, 6 $readOnly, 7 $token, ··· 34 inline: $inline, 35 maxBytes: $maxBytes, 36 minBytes: $minBytes, 37 }, 38 };