An experimental TypeSpec syntax for Lexicon

yea

+48 -61
+38 -51
packages/emitter/src/emitter.ts
··· 76 76 outputDir: string; 77 77 } 78 78 79 - // Constants for atproto format scalars 80 - const FORMAT_SCALARS = new Set([ 81 - "datetime", 82 - "did", 83 - "handle", 84 - "atUri", 85 - "cid", 86 - "tid", 87 - "nsid", 88 - "recordKey", 89 - "uri", 90 - "language", 91 - "atIdentifier", 92 - "bytes", 93 - "cidLink", 94 - ]); 95 - 96 - const FORMAT_MAP: Record<string, string> = { 79 + // Constants for string format scalars (type: "string" with format field) 80 + const STRING_FORMAT_MAP: Record<string, string> = { 97 81 did: "did", 98 82 handle: "handle", 99 83 atUri: "at-uri", ··· 1079 1063 // Determine description: prop description, or inherited scalar description for custom scalars 1080 1064 let description = propDesc; 1081 1065 if (!description && scalar.baseScalar && scalar.namespace?.name !== "TypeSpec") { 1082 - if (!FORMAT_SCALARS.has(scalar.name)) { 1066 + // Don't inherit description for built-in scalars (formats, bytes, cidLink) 1067 + const isBuiltInScalar = STRING_FORMAT_MAP[scalar.name] || 1068 + this.isScalarBytes(scalar) || 1069 + this.isScalarCidLink(scalar); 1070 + if (!isBuiltInScalar) { 1083 1071 description = getDoc(this.program, scalar); 1084 1072 } 1085 1073 } ··· 1180 1168 let primitive = this.getBasePrimitiveType(scalar); 1181 1169 1182 1170 // Apply format if applicable 1183 - const format = FORMAT_MAP[scalar.name]; 1171 + const format = STRING_FORMAT_MAP[scalar.name]; 1184 1172 if (format && primitive.type === "string") { 1185 1173 primitive = { ...primitive, format }; 1186 1174 } ··· 1217 1205 ) { 1218 1206 return { type: "integer" }; 1219 1207 } else if (["float32", "float64"].includes(scalar.name)) { 1220 - return { type: "integer" }; // Note: lexicon uses integer for floats 1208 + // Lexicon does not support floating-point numbers 1209 + this.program.reportDiagnostic({ 1210 + code: "float-not-supported", 1211 + severity: "error", 1212 + message: `Floating-point type "${scalar.name}" is not supported in Lexicon. Use integer instead.`, 1213 + target: scalar, 1214 + }); 1215 + return { type: "integer" }; 1221 1216 } 1222 1217 return { type: "string" }; 1223 1218 } ··· 1342 1337 ); 1343 1338 } 1344 1339 1345 - private getModelReference(model: Model): string | null { 1346 - if (!model.name || !model.namespace || model.namespace.name === "TypeSpec") 1347 - return null; 1340 + private getReference( 1341 + entity: Model | Union, 1342 + name: string | undefined, 1343 + namespace: Namespace | undefined, 1344 + ): string | null { 1345 + if (!name || !namespace || namespace.name === "TypeSpec") return null; 1348 1346 1349 - // If model is marked as @inline, don't create a reference - inline it instead 1350 - if (isInline(this.program, model)) return null; 1347 + // If entity is marked as @inline, don't create a reference - inline it instead 1348 + if (isInline(this.program, entity)) return null; 1351 1349 1352 - const namespaceName = getNamespaceFullName(model.namespace); 1350 + const namespaceName = getNamespaceFullName(namespace); 1353 1351 if (!namespaceName) return null; 1354 1352 1355 - const defName = model.name.charAt(0).toLowerCase() + model.name.slice(1); 1353 + const defName = name.charAt(0).toLowerCase() + name.slice(1); 1356 1354 1355 + // Local reference (same namespace) 1357 1356 if ( 1358 1357 this.currentLexiconId === namespaceName || 1359 1358 this.currentLexiconId === `${namespaceName}.defs` ··· 1361 1360 return `#${defName}`; 1362 1361 } 1363 1362 1364 - return model.name === "Main" 1365 - ? namespaceName 1366 - : `${namespaceName}#${defName}`; 1367 - } 1368 - 1369 - private getUnionReference(union: Union): string | null { 1370 - const unionName = union.name; 1371 - const namespace = union.namespace; 1372 - if (!unionName || !namespace || namespace.name === "TypeSpec") return null; 1373 - 1374 - // If union is marked as @inline, don't create a reference - inline it instead 1375 - if (isInline(this.program, union)) return null; 1376 - 1377 - const namespaceName = getNamespaceFullName(namespace); 1378 - if (!namespaceName) return null; 1379 - 1380 - const defName = unionName.charAt(0).toLowerCase() + unionName.slice(1); 1381 - 1382 - if ( 1383 - this.currentLexiconId === namespaceName || 1384 - this.currentLexiconId === `${namespaceName}.defs` 1385 - ) { 1386 - return `#${defName}`; 1363 + // Cross-namespace reference: Main models reference just the namespace 1364 + if (entity.kind === "Model" && name === "Main") { 1365 + return namespaceName; 1387 1366 } 1388 1367 1389 1368 return `${namespaceName}#${defName}`; 1369 + } 1370 + 1371 + private getModelReference(model: Model): string | null { 1372 + return this.getReference(model, model.name, model.namespace); 1373 + } 1374 + 1375 + private getUnionReference(union: Union): string | null { 1376 + return this.getReference(union, union.name, union.namespace); 1390 1377 } 1391 1378 1392 1379 private modelToLexiconArray(
+10 -10
packages/example/src/lexicons.ts
··· 16 16 defs: { 17 17 postRef: { 18 18 type: 'object', 19 - description: 'Reference to a post', 20 - required: ['uri', 'cid'], 21 19 properties: { 22 20 uri: { 23 21 type: 'string', ··· 28 26 description: 'CID of the post', 29 27 }, 30 28 }, 29 + description: 'Reference to a post', 30 + required: ['uri', 'cid'], 31 31 }, 32 32 replyRef: { 33 33 type: 'object', 34 - description: 'Reference to a parent post in a reply chain', 35 - required: ['root', 'parent'], 36 34 properties: { 37 35 root: { 38 36 type: 'ref', ··· 45 43 description: 'Direct parent post being replied to', 46 44 }, 47 45 }, 46 + description: 'Reference to a parent post in a reply chain', 47 + required: ['root', 'parent'], 48 48 }, 49 49 entity: { 50 50 type: 'object', 51 - description: 'Text entity (mention, link, or tag)', 52 - required: ['start', 'end', 'type', 'value'], 53 51 properties: { 54 52 start: { 55 53 type: 'integer', ··· 68 66 description: 'Entity value (handle, URL, or tag)', 69 67 }, 70 68 }, 69 + description: 'Text entity (mention, link, or tag)', 70 + required: ['start', 'end', 'type', 'value'], 71 71 }, 72 72 notificationType: { 73 73 type: 'string', ··· 85 85 key: 'tid', 86 86 record: { 87 87 type: 'object', 88 - required: ['subject', 'createdAt'], 89 88 properties: { 90 89 subject: { 91 90 type: 'string', ··· 97 96 description: 'When the follow was created', 98 97 }, 99 98 }, 99 + required: ['subject', 'createdAt'], 100 100 }, 101 101 description: 'A follow relationship', 102 102 }, ··· 111 111 key: 'tid', 112 112 record: { 113 113 type: 'object', 114 - required: ['subject', 'createdAt'], 115 114 properties: { 116 115 subject: { 117 116 type: 'ref', ··· 124 123 description: 'When the like was created', 125 124 }, 126 125 }, 126 + required: ['subject', 'createdAt'], 127 127 }, 128 128 description: 'A like on a post', 129 129 }, ··· 138 138 key: 'tid', 139 139 record: { 140 140 type: 'object', 141 - required: ['text', 'createdAt'], 142 141 properties: { 143 142 text: { 144 143 type: 'string', ··· 170 169 description: 'Post the user is replying to', 171 170 }, 172 171 }, 172 + required: ['text', 'createdAt'], 173 173 }, 174 174 description: 'A post in the feed', 175 175 }, ··· 216 216 key: 'tid', 217 217 record: { 218 218 type: 'object', 219 - required: ['subject', 'createdAt'], 220 219 properties: { 221 220 subject: { 222 221 type: 'ref', ··· 229 228 description: 'When the repost was created', 230 229 }, 231 230 }, 231 + required: ['subject', 'createdAt'], 232 232 }, 233 233 description: 'A repost of another post', 234 234 },