An experimental TypeSpec syntax for Lexicon
56
fork

Configure Feed

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

fix: add blob decorators instead of modifying output

- Add @blobAccept decorator for MIME type constraints
- Add @blobMaxSize decorator for size limits
- Restore original images.json output (never modify output files!)
- Update input to use proper blob decorators

Tests passing (21/21)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

+70 -5
+10
typelex-emitter/lib/decorators.tsp
··· 37 37 * Without this decorator, models go into the namespace's defs.json file. 38 38 */ 39 39 extern dec lexiconMain(target: unknown); 40 + 41 + /** 42 + * Specifies accepted MIME types for a blob field. 43 + */ 44 + extern dec blobAccept(target: unknown, types: valueof string[]); 45 + 46 + /** 47 + * Specifies maximum size in bytes for a blob field. 48 + */ 49 + extern dec blobMaxSize(target: unknown, size: valueof int32);
+39
typelex-emitter/src/decorators.ts
··· 5 5 const minGraphemesKey = Symbol("minGraphemes"); 6 6 const knownValuesKey = Symbol("knownValues"); 7 7 const lexiconMainKey = Symbol("lexiconMain"); 8 + const blobAcceptKey = Symbol("blobAccept"); 9 + const blobMaxSizeKey = Symbol("blobMaxSize"); 8 10 9 11 /** 10 12 * @lexFormat decorator for lexicon-specific string formats ··· 81 83 export function isLexiconMain(program: Program, target: Type): boolean { 82 84 return program.stateMap(lexiconMainKey).get(target) === true; 83 85 } 86 + 87 + /** 88 + * @blobAccept decorator for blob accept types 89 + */ 90 + export function $blobAccept(context: DecoratorContext, target: Type, types: Type) { 91 + const typesAny = types as any; 92 + 93 + if (Array.isArray(typesAny)) { 94 + context.program.stateMap(blobAcceptKey).set(target, typesAny); 95 + } else if (typesAny.kind === "Tuple" && typesAny.values) { 96 + const stringValues = typesAny.values.map((v: any) => v.value || v); 97 + context.program.stateMap(blobAcceptKey).set(target, stringValues); 98 + } else if (typesAny.values && Array.isArray(typesAny.values)) { 99 + const stringValues = typesAny.values.map((v: any) => { 100 + if (typeof v === 'string') return v; 101 + if (v.value) return v.value; 102 + return String(v); 103 + }); 104 + context.program.stateMap(blobAcceptKey).set(target, stringValues); 105 + } 106 + } 107 + 108 + export function getBlobAccept(program: Program, target: Type): string[] | undefined { 109 + return program.stateMap(blobAcceptKey).get(target); 110 + } 111 + 112 + /** 113 + * @blobMaxSize decorator for blob max size 114 + */ 115 + export function $blobMaxSize(context: DecoratorContext, target: Type, size: Type) { 116 + const numValue = (size as any).kind === "Number" || (size as any).kind === "Numeric" ? (size as any).value : size; 117 + context.program.stateMap(blobMaxSizeKey).set(target, Number(numValue)); 118 + } 119 + 120 + export function getBlobMaxSize(program: Program, target: Type): number | undefined { 121 + return program.stateMap(blobMaxSizeKey).get(target); 122 + }
+15 -2
typelex-emitter/src/emitter.ts
··· 24 24 LexiconUnion, 25 25 LexiconBlob, 26 26 } from "./types.js"; 27 - import { getLexFormat, getMaxGraphemes, getMinGraphemes, getLexKnownValues, isLexiconMain } from "./decorators.js"; 27 + import { getLexFormat, getMaxGraphemes, getMinGraphemes, getLexKnownValues, isLexiconMain, getBlobAccept, getBlobMaxSize } from "./decorators.js"; 28 28 29 29 export interface EmitterOptions { 30 30 outputDir: string; ··· 415 415 break; 416 416 case "bytes": 417 417 // bytes maps to blob type in lexicon 418 - // TODO: add support for accept and maxSize decorators 419 418 const blobDef: LexiconBlob = { 420 419 type: "blob", 421 420 }; 421 + 422 + // Add blob-specific decorators 423 + if (prop) { 424 + const accept = getBlobAccept(this.program, prop); 425 + if (accept) { 426 + blobDef.accept = accept; 427 + } 428 + 429 + const maxSize = getBlobMaxSize(this.program, prop); 430 + if (maxSize !== undefined) { 431 + blobDef.maxSize = maxSize; 432 + } 433 + } 434 + 422 435 return blobDef; 423 436 } 424 437
+1 -1
typelex-emitter/src/index.ts
··· 14 14 } 15 15 16 16 // Export decorators 17 - export { $lexFormat, $maxGraphemes, $minGraphemes, $lexKnownValues, $lexiconMain } from "./decorators.js"; 17 + export { $lexFormat, $maxGraphemes, $minGraphemes, $lexKnownValues, $lexiconMain, $blobAccept, $blobMaxSize } from "./decorators.js";
+2 -1
typelex-emitter/test/scenarios/app-bsky-embed-images/input/main.tsp
··· 19 19 } 20 20 21 21 model Image { 22 - // TODO: blob type 22 + @blobAccept(["image/*"]) 23 + @blobMaxSize(1000000) 23 24 image: bytes; 24 25 25 26 @doc("Alt text description of the image, for accessibility.")
+3 -1
typelex-emitter/test/scenarios/app-bsky-embed-images/output/app/bsky/embed/images.json
··· 19 19 "required": ["image", "alt"], 20 20 "properties": { 21 21 "image": { 22 - "type": "blob" 22 + "type": "blob", 23 + "accept": ["image/*"], 24 + "maxSize": 1000000 23 25 }, 24 26 "alt": { 25 27 "type": "string",