An experimental TypeSpec syntax for Lexicon
56
fork

Configure Feed

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

ok

+115 -68
-20
packages/emitter/lib/decorators.tsp
··· 33 33 */ 34 34 extern dec readOnly(target: unknown); 35 35 36 - /** 37 - * Specifies an enum of allowed values for a field. 38 - * Maps to lexicon's "enum" field. 39 - * Supports boolean, string, and integer values. 40 - * 41 - * @param values - Array of allowed values 42 - * 43 - * @example Integer enum 44 - * ```typespec 45 - * @lexEnum([1, 2, 3, 5, 8, 13]) 46 - * fibNumber?: integer; 47 - * ``` 48 - * 49 - * @example Boolean enum 50 - * ```typespec 51 - * @lexEnum([true]) 52 - * mustBeTrue?: boolean; 53 - * ``` 54 - */ 55 - extern dec lexEnum(target: unknown, ...values: unknown[]); 56 36 57 37 /** 58 38 * Marks a model as a record type with a specific key type.
-32
packages/emitter/src/decorators.ts
··· 3 3 4 4 const maxGraphemesKey = Symbol("maxGraphemes"); 5 5 const minGraphemesKey = Symbol("minGraphemes"); 6 - const enumKey = Symbol("enum"); 7 6 const recordKey = Symbol("record"); 8 7 const blobKey = Symbol("blob"); 9 8 const requiredKey = Symbol("required"); ··· 124 123 target: Type, 125 124 ): number | undefined { 126 125 return program.stateMap(minGraphemesKey).get(target); 127 - } 128 - 129 - /** 130 - * @lexEnum decorator for enum values (boolean, string, or integer arrays) 131 - */ 132 - export function $lexEnum( 133 - context: DecoratorContext, 134 - target: Type, 135 - ...values: Type[] 136 - ) { 137 - const enumValues: (boolean | string | number)[] = []; 138 - for (const value of values) { 139 - const v = value as any; 140 - if (v.kind === "Boolean") { 141 - enumValues.push(v.value); 142 - } else if (v.kind === "String") { 143 - enumValues.push(v.value); 144 - } else if (v.kind === "Number" || v.kind === "Numeric") { 145 - enumValues.push(v.value); 146 - } 147 - } 148 - if (enumValues.length > 0) { 149 - context.program.stateMap(enumKey).set(target, enumValues); 150 - } 151 - } 152 - 153 - export function getLexEnum( 154 - program: Program, 155 - target: Type, 156 - ): (boolean | string | number)[] | undefined { 157 - return program.stateMap(enumKey).get(target); 158 126 } 159 127 160 128 /**
+103 -12
packages/emitter/src/emitter.ts
··· 32 32 import { 33 33 getMaxGraphemes, 34 34 getMinGraphemes, 35 - getLexEnum, 36 35 getRecordKey, 37 36 isBlob, 38 37 isRequired, ··· 116 115 this.processNamespace(childNs); 117 116 } 118 117 return; 118 + } 119 + 120 + // Check for TypeSpec enum syntax and throw error 121 + if (ns.enums && ns.enums.size > 0) { 122 + for (const [_, enumType] of ns.enums) { 123 + this.program.reportDiagnostic({ 124 + code: "enum-not-supported", 125 + severity: "error", 126 + message: "TypeSpec enum syntax is not supported. Use @closed @inline union instead.", 127 + target: enumType, 128 + }); 129 + } 119 130 } 120 131 121 132 const namespaceType = this.classifyNamespace(ns); ··· 407 418 // Parse union variants 408 419 const variants = this.parseUnionVariants(unionType); 409 420 410 - // Case 1: String enum (string literals with or without string type) 421 + // Integer enum (@closed only) 422 + if ( 423 + variants.numericLiterals.length > 0 && 424 + variants.unionRefs.length === 0 && 425 + isClosed(this.program, unionType) 426 + ) { 427 + return this.createIntegerEnumDef(unionType, variants.numericLiterals, prop); 428 + } 429 + 430 + // Boolean enum (@closed only) 431 + if ( 432 + variants.booleanLiterals.length > 0 && 433 + variants.unionRefs.length === 0 && 434 + isClosed(this.program, unionType) 435 + ) { 436 + return this.createBooleanEnumDef(unionType, variants.booleanLiterals, prop); 437 + } 438 + 439 + // String enum (string literals with or without string type) 411 440 // isStringEnum: has literals + string type + no refs 412 441 // Closed enum: has literals + no string type + no refs + @closed 413 442 if ( ··· 420 449 return this.createStringEnumDef(unionType, variants.stringLiterals, prop); 421 450 } 422 451 423 - // Case 2: Model reference union (including empty union with unknown) 452 + // Model reference union (including empty union with unknown) 424 453 if (variants.unionRefs.length > 0 || variants.hasUnknown) { 425 454 return this.createUnionRefDef(unionType, variants, prop); 426 455 } 427 456 428 - // Case 3: Empty union without unknown 429 - if (variants.stringLiterals.length === 0) { 457 + // Empty union without unknown 458 + if ( 459 + variants.stringLiterals.length === 0 && 460 + variants.numericLiterals.length === 0 && 461 + variants.booleanLiterals.length === 0 462 + ) { 430 463 this.program.reportDiagnostic({ 431 464 code: "union-empty", 432 465 severity: "error", ··· 436 469 return null; 437 470 } 438 471 439 - // Case 4: Invalid string literal union (has literals but no string type and not @closed) 472 + // Invalid string literal union (has literals but no string type and not @closed) 440 473 if (variants.stringLiterals.length > 0 && !variants.hasStringType) { 441 474 this.program.reportDiagnostic({ 442 475 code: "string-literal-union-invalid", ··· 463 496 private parseUnionVariants(unionType: Union) { 464 497 const unionRefs: string[] = []; 465 498 const stringLiterals: string[] = []; 499 + const numericLiterals: number[] = []; 500 + const booleanLiterals: boolean[] = []; 466 501 let hasStringType = false; 467 502 let hasUnknown = false; 468 503 ··· 475 510 case "String": 476 511 stringLiterals.push((variant.type as any).value); 477 512 break; 513 + case "Number": 514 + numericLiterals.push((variant.type as any).value); 515 + break; 516 + case "Boolean": 517 + booleanLiterals.push((variant.type as any).value); 518 + break; 478 519 case "Scalar": 479 520 if ((variant.type as Scalar).name === "string") { 480 521 hasStringType = true; ··· 495 536 return { 496 537 unionRefs, 497 538 stringLiterals, 539 + numericLiterals, 540 + booleanLiterals, 498 541 hasStringType, 499 542 hasUnknown, 500 543 isStringEnum, 501 544 }; 545 + } 546 + 547 + private createIntegerEnumDef( 548 + unionType: Union, 549 + numericLiterals: number[], 550 + prop?: ModelProperty, 551 + ): LexiconDefinition { 552 + const primitive: any = { 553 + type: "integer", 554 + enum: numericLiterals, 555 + }; 556 + 557 + // Add property-specific metadata 558 + if (prop) { 559 + const propDesc = getDoc(this.program, prop); 560 + if (propDesc) primitive.description = propDesc; 561 + 562 + const defaultValue = (prop as any).default; 563 + if ( 564 + defaultValue?.value !== undefined && 565 + typeof defaultValue.value === "number" 566 + ) { 567 + primitive.default = defaultValue.value; 568 + } 569 + } 570 + 571 + return primitive; 572 + } 573 + 574 + private createBooleanEnumDef( 575 + unionType: Union, 576 + booleanLiterals: boolean[], 577 + prop?: ModelProperty, 578 + ): LexiconDefinition { 579 + const primitive: any = { 580 + type: "boolean", 581 + enum: booleanLiterals, 582 + }; 583 + 584 + // Add property-specific metadata 585 + if (prop) { 586 + const propDesc = getDoc(this.program, prop); 587 + if (propDesc) primitive.description = propDesc; 588 + 589 + const defaultValue = (prop as any).default; 590 + if ( 591 + defaultValue?.value !== undefined && 592 + typeof defaultValue.value === "boolean" 593 + ) { 594 + primitive.default = defaultValue.value; 595 + } 596 + } 597 + 598 + return primitive; 502 599 } 503 600 504 601 private createStringEnumDef( ··· 1114 1211 ) { 1115 1212 // Normal default value (no @readOnly) 1116 1213 primitive.default = defaultValue; 1117 - } 1118 - 1119 - // Apply enum values 1120 - const enumValues = getLexEnum(this.program, prop); 1121 - if (enumValues !== undefined && enumValues.length > 0) { 1122 - primitive.enum = enumValues; 1123 1214 } 1124 1215 } 1125 1216
-2
packages/emitter/src/tsp-index.ts
··· 1 1 import { 2 2 $maxGraphemes, 3 3 $minGraphemes, 4 - $lexEnum, 5 4 $record, 6 5 $blob, 7 6 $required, ··· 23 22 "": { 24 23 maxGraphemes: $maxGraphemes, 25 24 minGraphemes: $minGraphemes, 26 - lexEnum: $lexEnum, 27 25 record: $record, 28 26 required: $required, 29 27 readOnly: $readOnly,
+12 -2
packages/emitter/test/spec/basic/input/com/example/integerConstraints.tsp
··· 1 1 import "@tlex/emitter"; 2 2 3 3 namespace com.example.integerConstraints { 4 + @closed 5 + @inline 6 + union FibonacciNumbers { 7 + 1, 8 + 2, 9 + 3, 10 + 5, 11 + 8, 12 + 13, 13 + } 14 + 4 15 @doc("Integer field constraints") 5 16 model Main { 6 17 @doc("Integer between 1 and 100") ··· 9 20 withMinMax?: integer; 10 21 11 22 @doc("Fibonacci numbers only") 12 - @lexEnum(1, 2, 3, 5, 8, 13) 13 - withEnum?: integer; 23 + withEnum?: FibonacciNumbers; 14 24 15 25 @doc("Integer with default value") 16 26 withDefault?: integer = 42;