An experimental TypeSpec syntax for Lexicon
56
fork

Configure Feed

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

union, diagnostics

+184 -4
+45
typelex-emitter/src/emitter.ts
··· 4 4 Model, 5 5 ModelProperty, 6 6 Scalar, 7 + Union, 7 8 getDoc, 8 9 getNamespaceFullName, 9 10 isTemplateInstance, ··· 251 252 } 252 253 253 254 switch (type.kind) { 255 + case "Boolean": { 256 + // Handle boolean literal types (e.g., `true` or `false`) 257 + const booleanType = type as any; 258 + const primitive: LexiconPrimitive = { 259 + type: "boolean", 260 + const: booleanType.value, 261 + }; 262 + if (prop) { 263 + const propDesc = getDoc(this.program, prop); 264 + if (propDesc) { 265 + primitive.description = propDesc; 266 + } 267 + } 268 + return primitive; 269 + } 254 270 case "Scalar": 255 271 const primitive = this.scalarToLexiconPrimitive(type as Scalar, prop); 256 272 if (prop && primitive) { ··· 294 310 } 295 311 } 296 312 return obj; 313 + case "Union": 314 + // Handle union types naturally 315 + const unionType = type as Union; 316 + const unionRefs: string[] = []; 317 + 318 + // Iterate through all variants in the union 319 + for (const variant of unionType.variants.values()) { 320 + if (variant.type.kind === "Model") { 321 + const ref = this.getModelReference(variant.type as Model); 322 + if (ref) { 323 + unionRefs.push(ref); 324 + } 325 + } 326 + } 327 + 328 + if (unionRefs.length > 0) { 329 + const unionDef: LexiconUnion = { 330 + type: "union", 331 + refs: unionRefs, 332 + }; 333 + if (prop) { 334 + const propDesc = getDoc(this.program, prop); 335 + if (propDesc) { 336 + unionDef.description = propDesc; 337 + } 338 + } 339 + return unionDef; 340 + } 341 + return null; 297 342 case "Intrinsic": 298 343 // Handle unknown type - return unknown definition 299 344 const unknownDef: any = {
+48 -4
typelex-emitter/test/scenarios.test.ts
··· 1 1 import assert from "assert"; 2 2 import path from "path"; 3 3 import { describe, it } from "vitest"; 4 - import { formatDiagnostic, resolvePath } from "@typespec/compiler"; 4 + import { formatDiagnostic, resolvePath, type Diagnostic } from "@typespec/compiler"; 5 5 import { 6 6 TypeSpecTestLibrary, 7 7 createTestHost, ··· 43 43 const expectationDirectory = path.resolve(scenario, "output"); 44 44 const expectedFiles = await readdirRecursive(expectationDirectory); 45 45 46 + // Fail if there are any diagnostics 47 + if (emitResult.diagnostics.length > 0) { 48 + const formattedDiagnostics = emitResult.diagnostics.map((diag) => { 49 + let formatted = formatDiagnostic(diag); 50 + 51 + // Try to extract source code context 52 + const target = diag.target as any; 53 + if (target?.pos !== undefined) { 54 + // Get the source file from the target node 55 + let node = target; 56 + while (node && !node.file) { 57 + node = node.parent; 58 + } 59 + 60 + if (node?.file) { 61 + const file = node.file; 62 + const pos = target.pos; 63 + const lines = file.text.split("\n"); 64 + 65 + // Calculate line number from position 66 + let currentPos = 0; 67 + let lineNumber = 1; 68 + for (let i = 0; i < lines.length; i++) { 69 + const lineLength = lines[i].length + 1; // +1 for newline 70 + if (currentPos + lineLength > pos) { 71 + lineNumber = i + 1; 72 + break; 73 + } 74 + currentPos += lineLength; 75 + } 76 + 77 + const lineContent = lines[lineNumber - 1] || ""; 78 + formatted += `\n ${String(lineNumber).padStart(3)} | ${lineContent.trimStart()}`; 79 + } 80 + } 81 + 82 + return formatted; 83 + }); 84 + 85 + assert.fail(`Expected no diagnostics but got:\n${formattedDiagnostics.join("\n\n")}`); 86 + } 87 + 46 88 assertFilesAsExpected(emitResult.files, expectedFiles); 47 89 }); 48 90 } ··· 50 92 51 93 interface EmitResult { 52 94 files: Record<string, string>; 53 - diagnostics: string[]; 95 + inputFiles: Record<string, string>; 96 + diagnostics: Diagnostic[]; 54 97 } 55 98 56 99 async function doEmit(files: Record<string, string>): Promise<EmitResult> { ··· 85 128 86 129 return { 87 130 files: outputFiles, 88 - diagnostics: diagnostics.map((x) => formatDiagnostic(x)), 131 + inputFiles: files, 132 + diagnostics: diagnostics, 89 133 }; 90 134 } 91 135 ··· 96 140 for (const fn of Object.keys(expectedFiles)) { 97 141 assert.ok( 98 142 Object.prototype.hasOwnProperty.call(outputFiles, fn), 99 - `expected file ${fn} was not produced`, 143 + `expected file ${fn} was not produced. Actual files: ${Object.keys(outputFiles).join(", ")}`, 100 144 ); 101 145 } 102 146
+29
typelex-emitter/test/scenarios/union-same-ns/input/main.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + // Union with types in same namespace - should use # refs 4 + namespace app.bsky.feed { 5 + model PostView { 6 + @lexFormat("at-uri") 7 + uri: string; 8 + text: string; 9 + } 10 + 11 + model NotFoundPost { 12 + @lexFormat("at-uri") 13 + uri: string; 14 + notFound: true; 15 + } 16 + 17 + model BlockedPost { 18 + @lexFormat("at-uri") 19 + uri: string; 20 + blocked: true; 21 + } 22 + 23 + model ThreadViewPost { 24 + post: PostView; 25 + 26 + @doc("Parent post (may be not found or blocked)") 27 + parent?: PostView | NotFoundPost | BlockedPost; 28 + } 29 + }
+62
typelex-emitter/test/scenarios/union-same-ns/output/app/bsky/feed/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.feed.defs", 4 + "defs": { 5 + "postView": { 6 + "type": "object", 7 + "required": ["uri", "text"], 8 + "properties": { 9 + "uri": { 10 + "type": "string", 11 + "format": "at-uri" 12 + }, 13 + "text": { 14 + "type": "string" 15 + } 16 + } 17 + }, 18 + "notFoundPost": { 19 + "type": "object", 20 + "required": ["uri", "notFound"], 21 + "properties": { 22 + "uri": { 23 + "type": "string", 24 + "format": "at-uri" 25 + }, 26 + "notFound": { 27 + "type": "boolean", 28 + "const": true 29 + } 30 + } 31 + }, 32 + "blockedPost": { 33 + "type": "object", 34 + "required": ["uri", "blocked"], 35 + "properties": { 36 + "uri": { 37 + "type": "string", 38 + "format": "at-uri" 39 + }, 40 + "blocked": { 41 + "type": "boolean", 42 + "const": true 43 + } 44 + } 45 + }, 46 + "threadViewPost": { 47 + "type": "object", 48 + "required": ["post"], 49 + "properties": { 50 + "post": { 51 + "type": "ref", 52 + "ref": "#postView" 53 + }, 54 + "parent": { 55 + "type": "union", 56 + "refs": ["#postView", "#notFoundPost", "#blockedPost"], 57 + "description": "Parent post (may be not found or blocked)" 58 + } 59 + } 60 + } 61 + } 62 + }