An experimental TypeSpec syntax for Lexicon
0
fork

Configure Feed

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

nits

+118 -113
+118 -113
packages/emitter/src/emitter.ts
··· 353 353 if (!name) return; 354 354 355 355 // Skip @inline unions - they should be inlined, not defined separately 356 - if (isInline(this.program, union)) return; 356 + if (isInline(this.program, union)) { 357 + return; 358 + } 357 359 358 360 const unionDef = this.typeToLexiconDefinition(union, undefined, true); 359 - if (!unionDef) return; 361 + if (!unionDef) { 362 + return; 363 + } 360 364 361 365 // Only string enums can be added as defs 362 366 // Union refs (type: "union") must be inlined at usage sites ··· 375 379 }); 376 380 } 377 381 } 378 - 379 382 380 383 private isBlob(model: Model): boolean { 381 384 // Check if model itself is named Blob ··· 411 414 // First arg: accept types (array of mime type strings) 412 415 if (args.length >= 1) { 413 416 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"); 417 + if ( 418 + isType(acceptArg) || 419 + (acceptArg as ArrayValue).valueKind !== "ArrayValue" 420 + ) { 421 + throw new Error( 422 + "Blob template first argument must be an array of mime types", 423 + ); 416 424 } 417 425 const arrayValue = acceptArg as ArrayValue; 418 - const acceptTypes = arrayValue.values.map(v => { 426 + const acceptTypes = arrayValue.values.map((v) => { 419 427 if ((v as StringValue).valueKind !== "StringValue") { 420 428 throw new Error("Blob accept types must be strings"); 421 429 } ··· 430 438 if (args.length >= 2) { 431 439 const maxSizeArg = args[1] as IndeterminateEntity; 432 440 if (!isType(maxSizeArg.type) || maxSizeArg.type.kind !== "Number") { 433 - throw new Error("Blob template second argument must be a numeric literal"); 441 + throw new Error( 442 + "Blob template second argument must be a numeric literal", 443 + ); 434 444 } 435 445 const maxSize = (maxSizeArg.type as NumericLiteral).value; 436 446 if (maxSize > 0) { ··· 447 457 ): LexObjectProperty | null { 448 458 const variants = this.parseUnionVariants(unionType); 449 459 450 - // Boolean literals are not supported in Lexicon 460 + // Boolean literal unions are not supported in Lexicon 451 461 if (variants.booleanLiterals.length > 0) { 452 462 this.program.reportDiagnostic({ 453 463 code: "boolean-literals-not-supported", ··· 469 479 const defaultValue = prop?.defaultValue 470 480 ? serializeValueAsJson(this.program, prop.defaultValue, prop) 471 481 : undefined; 472 - 473 482 return { 474 483 type: "integer", 475 484 enum: variants.numericLiterals, ··· 494 503 const defaultValue = prop?.defaultValue 495 504 ? serializeValueAsJson(this.program, prop.defaultValue, prop) 496 505 : undefined; 497 - 498 506 const maxLength = getMaxLength(this.program, unionType); 499 507 const minLength = getMinLength(this.program, unionType); 500 508 const maxGraphemes = getMaxGraphemes(this.program, unionType); 501 509 const minGraphemes = getMinGraphemes(this.program, unionType); 502 - 503 510 return { 504 511 type: "string", 505 512 [isClosedUnion ? "enum" : "knownValues"]: variants.stringLiterals, ··· 520 527 code: "union-mixed-refs-literals", 521 528 severity: "error", 522 529 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). ` + 530 + `Union contains both model references and string literals. Lexicon unions must be either: ` + 531 + `(1) model references only (type: "union"), ` + 532 + `(2) string literals + string type (type: "string" with knownValues), or ` + 533 + `(3) integer literals + integer type (type: "integer" with knownValues). ` + 526 534 `Separate these into distinct fields or nested unions.`, 527 535 target: unionType, 528 536 }); ··· 535 543 code: "closed-open-union", 536 544 severity: "error", 537 545 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'.", 546 + "@closed decorator cannot be used on open unions (unions containing `unknown` or `never`). " + 547 + "Remove the @closed decorator or make the union closed by removing `unknown` / `never`.", 540 548 target: unionType, 541 549 }); 542 550 } 543 551 544 552 const propDesc = prop ? getDoc(this.program, prop) : undefined; 545 - 546 553 return { 547 554 type: "union", 548 555 refs: variants.unionRefs, ··· 560 567 this.program.reportDiagnostic({ 561 568 code: "union-empty", 562 569 severity: "error", 563 - message: `Union has no variants. Atproto unions must contain either model references or string literals.`, 570 + message: `Union has no variants. Lexicon unions must contain either model references or literals.`, 564 571 target: unionType, 565 572 }); 566 573 return null; ··· 572 579 code: "string-literal-union-invalid", 573 580 severity: "error", 574 581 message: 575 - 'String literal unions must include "| string" to allow unknown values. ' + 576 - "Use @closed decorator if this is intentionally a fixed enum.", 582 + 'Open string unions must include "| string" to allow unknown values. ' + 583 + "Use @closed decorator if this is intentionally a closed enum.", 577 584 target: unionType, 578 585 }); 579 586 return null; ··· 584 591 code: "union-unexpected-type", 585 592 severity: "error", 586 593 message: 587 - "Unexpected union type - neither string enum nor model refs nor empty", 594 + "Unexpected union type: neither string enum nor model refs nor empty.", 588 595 target: unionType, 589 596 }); 590 597 return null; ··· 674 681 } as LexXrpcProcedure; 675 682 } else if (isSubscription(this.program, operation)) { 676 683 const parameters = this.buildParameters(operation); 677 - const message = this.buildMessage(operation); 684 + const message = this.buildSubscriptionMessage(operation); 678 685 679 686 lexicon.defs[defName] = { 680 687 type: "subscription", ··· 907 914 return null; 908 915 } 909 916 910 - private buildMessage( 917 + private buildSubscriptionMessage( 911 918 operation: Operation, 912 919 ): { schema: LexRefUnion } | undefined { 913 920 if (operation.returnType?.kind === "Union") { ··· 1027 1034 1028 1035 // Determine description: prop description, or inherited scalar description for custom scalars 1029 1036 let description = propDesc; 1030 - if (!description && scalar.baseScalar && scalar.namespace?.name !== "TypeSpec") { 1037 + if ( 1038 + !description && 1039 + scalar.baseScalar && 1040 + scalar.namespace?.name !== "TypeSpec" 1041 + ) { 1031 1042 // Don't inherit description for built-in scalars (formats, bytes, cidLink) 1032 - const isBuiltInScalar = STRING_FORMAT_MAP[scalar.name] || 1033 - this.isScalarBytes(scalar) || 1034 - this.isScalarCidLink(scalar); 1043 + const isBuiltInScalar = 1044 + STRING_FORMAT_MAP[scalar.name] || 1045 + this.isScalarOfType(scalar, "bytes") || 1046 + this.isScalarOfType(scalar, "cidLink"); 1035 1047 if (!isBuiltInScalar) { 1036 1048 description = getDoc(this.program, scalar); 1037 1049 } ··· 1050 1062 return { ...this.createBlobDef(model), description: propDesc }; 1051 1063 } 1052 1064 1053 - // 2. Check for token type - tokens must be referenced, not inlined 1065 + // 2. Check for model reference (named models) 1066 + const modelRef = this.getModelReference(model); 1067 + 1068 + // Tokens must be referenced, not inlined 1054 1069 if (isToken(this.program, model)) { 1055 - const modelRef = this.getModelReference(model); 1056 1070 if (!modelRef) { 1057 1071 this.program.reportDiagnostic({ 1058 1072 code: "token-must-be-named", ··· 1065 1079 return { type: "ref" as const, ref: modelRef, description: propDesc }; 1066 1080 } 1067 1081 1068 - // 3. Check for model reference (named models from other namespaces) 1069 - const modelRef = this.getModelReference(model); 1070 1082 if (modelRef) { 1071 1083 return { type: "ref" as const, ref: modelRef, description: propDesc }; 1072 1084 } ··· 1090 1102 // 4. Inline object 1091 1103 const objDef = this.modelToLexiconObject(model); 1092 1104 // Only add propDesc if the object doesn't already have a description 1093 - return propDesc && !objDef.description ? { ...objDef, description: propDesc } : objDef; 1105 + return propDesc && !objDef.description 1106 + ? { ...objDef, description: propDesc } 1107 + : objDef; 1094 1108 } 1095 1109 1096 1110 private handleUnionType( ··· 1115 1129 prop?: ModelProperty, 1116 1130 ): LexObjectProperty | null { 1117 1131 // Check if this scalar (or its base) is bytes type 1118 - if (this.isScalarBytes(scalar)) { 1132 + if (this.isScalarOfType(scalar, "bytes")) { 1119 1133 const byteDef: LexBytes = { type: "bytes" }; 1120 1134 const target = prop || scalar; 1121 1135 ··· 1136 1150 } 1137 1151 1138 1152 // Check if this scalar (or its base) is cidLink type 1139 - if (this.isScalarCidLink(scalar)) { 1153 + if (this.isScalarOfType(scalar, "cidLink")) { 1140 1154 const cidLinkDef: LexCidLink = { type: "cid-link" }; 1141 1155 if (prop) { 1142 1156 return this.applyPropertyMetadata(cidLinkDef, prop); ··· 1146 1160 1147 1161 // Build primitive with constraints and metadata 1148 1162 let primitive = this.getBasePrimitiveType(scalar); 1163 + if (!primitive) return null; 1149 1164 1150 1165 // Apply format if applicable 1151 1166 const format = STRING_FORMAT_MAP[scalar.name]; ··· 1156 1171 // Apply string constraints 1157 1172 if (primitive.type === "string") { 1158 1173 const target = prop || scalar; 1159 - 1160 1174 const maxLength = getMaxLength(this.program, target); 1161 1175 if (maxLength !== undefined) { 1162 1176 primitive.maxLength = maxLength; 1163 1177 } 1164 - 1165 1178 const minLength = getMinLength(this.program, target); 1166 1179 if (minLength !== undefined) { 1167 1180 primitive.minLength = minLength; 1168 1181 } 1169 - 1170 1182 const maxGraphemes = getMaxGraphemes(this.program, target); 1171 1183 if (maxGraphemes !== undefined) { 1172 1184 primitive.maxGraphemes = maxGraphemes; 1173 1185 } 1174 - 1175 1186 const minGraphemes = getMinGraphemes(this.program, target); 1176 1187 if (minGraphemes !== undefined) { 1177 1188 primitive.minGraphemes = minGraphemes; ··· 1184 1195 if (minValue !== undefined) { 1185 1196 primitive.minimum = minValue; 1186 1197 } 1187 - 1188 1198 const maxValue = getMaxValue(this.program, prop); 1189 1199 if (maxValue !== undefined) { 1190 1200 primitive.maximum = maxValue; ··· 1199 1209 return primitive; 1200 1210 } 1201 1211 1202 - private isScalarBytes(scalar: Scalar): boolean { 1203 - return scalar.name === "bytes" || (scalar.baseScalar ? this.isScalarBytes(scalar.baseScalar) : false); 1212 + private isScalarOfType(scalar: Scalar, typeName: string): boolean { 1213 + if (scalar.name === typeName) { 1214 + return true; 1215 + } 1216 + if (scalar.baseScalar && this.isScalarOfType(scalar.baseScalar, typeName)) { 1217 + return true; 1218 + } 1219 + return false; 1204 1220 } 1205 1221 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" }; 1222 + private getBasePrimitiveType(scalar: Scalar): LexObjectProperty | null { 1223 + // Custom scalars extending valid primitives (like did, atUri, etc. extending string) 1224 + if (scalar.baseScalar) { 1225 + return this.getBasePrimitiveType(scalar.baseScalar); 1213 1226 } 1214 - 1215 - if (["integer", "int32", "int64", "int16", "int8"].includes(scalar.name)) { 1216 - return { type: "integer" }; 1227 + // Valid Lexicon primitive types 1228 + switch (scalar.name) { 1229 + case "boolean": 1230 + return { type: "boolean" }; 1231 + case "string": 1232 + return { type: "string" }; 1233 + case "numeric": 1234 + // TODO: Any way to narrow it down? 1235 + return { type: "integer" }; 1217 1236 } 1218 - 1219 - if (["float32", "float64"].includes(scalar.name)) { 1220 - this.program.reportDiagnostic({ 1221 - code: "float-not-supported", 1222 - severity: "error", 1223 - message: `Floating-point type "${scalar.name}" is not supported in Lexicon. Use integer instead.`, 1224 - target: scalar, 1225 - }); 1226 - return { type: "integer" }; 1227 - } 1228 - 1229 - return { type: "string" }; 1237 + this.program.reportDiagnostic({ 1238 + code: "unknown-scalar-type", 1239 + severity: "error", 1240 + message: `Scalar type "${scalar.name}" is not a valid Lexicon primitive. Valid types: boolean, integer, string`, 1241 + target: scalar, 1242 + }); 1243 + return null; 1230 1244 } 1231 1245 1232 1246 private applyPropertyMetadata<T extends LexObjectProperty>( 1233 1247 primitive: T, 1234 1248 prop: ModelProperty, 1235 1249 ): T { 1236 - const hasReadOnly = isReadOnly(this.program, prop); 1237 - const defaultValue = prop.defaultValue 1238 - ? serializeValueAsJson(this.program, prop.defaultValue, prop) 1239 - : undefined; 1240 - 1241 - if (hasReadOnly) { 1242 - // Validate that readOnly is only used on string, boolean, or integer 1243 - if (!this.isValidConstForType(primitive.type, defaultValue)) { 1244 - this.program.reportDiagnostic({ 1245 - code: "invalid-readonly-type", 1246 - severity: "error", 1247 - message: `@readOnly is only valid for string, boolean, and integer types, but found type: ${primitive.type}`, 1248 - target: prop, 1249 - }); 1250 - return primitive; 1251 - } 1252 - 1250 + let defaultValue; 1251 + if (prop.defaultValue !== undefined) { 1252 + defaultValue = serializeValueAsJson( 1253 + this.program, 1254 + prop.defaultValue, 1255 + prop, 1256 + ); 1257 + } 1258 + if (defaultValue !== undefined) { 1259 + this.assertValidValueForType(primitive.type, defaultValue, prop); 1260 + } 1261 + if (isReadOnly(this.program, prop)) { 1253 1262 if (defaultValue === undefined) { 1254 1263 this.program.reportDiagnostic({ 1255 1264 code: "readonly-missing-default", ··· 1259 1268 }); 1260 1269 return primitive; 1261 1270 } 1262 - 1263 - // Set const value from default 1264 1271 return { ...primitive, const: defaultValue } as T; 1265 - } 1266 - 1267 - if ( 1268 - defaultValue !== undefined && 1269 - this.isValidDefaultForType(primitive.type, defaultValue) 1270 - ) { 1271 - // Normal default value (no @readOnly) 1272 + } else if (defaultValue !== undefined) { 1272 1273 return { ...primitive, default: defaultValue } as T; 1273 1274 } 1274 - 1275 1275 return primitive; 1276 1276 } 1277 1277 1278 - private isValidConstForType( 1278 + private assertValidValueForType( 1279 1279 primitiveType: string, 1280 - constValue: unknown, 1281 - ): boolean { 1282 - return ( 1283 - (primitiveType === "boolean" && typeof constValue === "boolean") || 1284 - (primitiveType === "string" && typeof constValue === "string") || 1285 - (primitiveType === "integer" && typeof constValue === "number") 1286 - ); 1287 - } 1288 - 1289 - private isValidDefaultForType( 1290 - primitiveType: string, 1291 - defaultValue: unknown, 1292 - ): boolean { 1293 - return ( 1294 - (primitiveType === "string" && typeof defaultValue === "string") || 1295 - (primitiveType === "integer" && typeof defaultValue === "number") || 1296 - (primitiveType === "boolean" && typeof defaultValue === "boolean") 1297 - ); 1280 + value: unknown, 1281 + prop: ModelProperty, 1282 + ): void { 1283 + const valid = 1284 + (primitiveType === "boolean" && typeof value === "boolean") || 1285 + (primitiveType === "string" && typeof value === "string") || 1286 + (primitiveType === "integer" && typeof value === "number"); 1287 + if (!valid) { 1288 + this.program.reportDiagnostic({ 1289 + code: "invalid-default-value-type", 1290 + severity: "error", 1291 + message: `Default value type mismatch: expected ${primitiveType}, got ${typeof value}`, 1292 + target: prop, 1293 + }); 1294 + } 1298 1295 } 1299 1296 1300 1297 private getReference( ··· 1305 1302 if (!name || !namespace || namespace.name === "TypeSpec") return null; 1306 1303 1307 1304 // If entity is marked as @inline, don't create a reference - inline it instead 1308 - if (isInline(this.program, entity)) return null; 1309 - 1310 - const namespaceName = getNamespaceFullName(namespace); 1311 - if (!namespaceName) return null; 1305 + if (isInline(this.program, entity)) { 1306 + return null; 1307 + } 1312 1308 1313 1309 const defName = name.charAt(0).toLowerCase() + name.slice(1); 1310 + const namespaceName = getNamespaceFullName(namespace); 1311 + if (!namespaceName) { 1312 + this.program.reportDiagnostic({ 1313 + code: "no-namespace", 1314 + severity: "error", 1315 + message: `Missing namespace definition`, 1316 + target: entity, 1317 + }); 1318 + } 1314 1319 1315 1320 // Local reference (same namespace) 1316 1321 if (