An experimental TypeSpec syntax for Lexicon

add @default, don't inline scalars unless @inline

authored by danabra.mov and committed by Tangled 6c9b730d de0a1477

Changed files
+576 -16
packages
+25
packages/emitter/lib/decorators.tsp
··· 163 163 extern dec errors(target: unknown, ...errors: unknown[]); 164 164 165 165 /** 166 + * Specifies a default value for a scalar or union definition. 167 + * Only valid on standalone scalar or union defs (not @inline). 168 + * The value must match the underlying type (string, integer, or boolean). 169 + * For unions with token refs, you can pass a model reference directly. 170 + * 171 + * @param value - The default value (literal or model reference for tokens) 172 + * 173 + * @example Scalar with default 174 + * ```typespec 175 + * @default("standard") 176 + * scalar Mode extends string; 177 + * ``` 178 + * 179 + * @example Union with token default 180 + * ```typespec 181 + * @default(Inperson) 182 + * union EventMode { Hybrid, Inperson, Virtual, string } 183 + * 184 + * @token 185 + * model Inperson {} 186 + * ``` 187 + */ 188 + extern dec `default`(target: unknown, value: unknown); 189 + 190 + /** 166 191 * Marks a namespace as external, preventing it from emitting JSON output. 167 192 * This decorator can only be applied to namespaces. 168 193 * Useful for importing definitions from other lexicons without re-emitting them.
+17
packages/emitter/src/decorators.ts
··· 25 25 const maxBytesKey = Symbol("maxBytes"); 26 26 const minBytesKey = Symbol("minBytes"); 27 27 const externalKey = Symbol("external"); 28 + const defaultKey = Symbol("default"); 28 29 29 30 /** 30 31 * @maxBytes decorator for maximum length of bytes type ··· 294 295 295 296 export function isReadOnly(program: Program, target: Type): boolean { 296 297 return program.stateSet(readOnlyKey).has(target); 298 + } 299 + 300 + /** 301 + * @default decorator for setting default values on scalars and unions 302 + * The value can be a literal (string, number, boolean) or a model reference for tokens 303 + */ 304 + export function $default(context: DecoratorContext, target: Type, value: any) { 305 + // Just store the raw value - let the emitter handle unwrapping and validation 306 + context.program.stateMap(defaultKey).set(target, value); 307 + } 308 + 309 + export function getDefault( 310 + program: Program, 311 + target: Type, 312 + ): any | undefined { 313 + return program.stateMap(defaultKey).get(target); 297 314 } 298 315 299 316 /**
+235 -16
packages/emitter/src/emitter.ts
··· 68 68 getMaxBytes, 69 69 getMinBytes, 70 70 isExternal, 71 + getDefault, 71 72 } from "./decorators.js"; 72 73 73 74 export interface EmitterOptions { ··· 97 98 private program: Program, 98 99 private options: EmitterOptions, 99 100 ) {} 101 + 102 + /** 103 + * Process the raw default value from the decorator, unwrapping TypeSpec value objects 104 + * and returning either a primitive (string, number, boolean) or a Type (for model references) 105 + */ 106 + private processDefaultValue(rawValue: any): string | number | boolean | Type | undefined { 107 + if (rawValue === undefined) return undefined; 108 + 109 + // TypeSpec may wrap values - check if this is a value object first 110 + if (rawValue && typeof rawValue === 'object' && rawValue.valueKind) { 111 + if (rawValue.valueKind === "StringValue") { 112 + return rawValue.value; 113 + } else if (rawValue.valueKind === "NumericValue" || rawValue.valueKind === "NumberValue") { 114 + return rawValue.value; 115 + } else if (rawValue.valueKind === "BooleanValue") { 116 + return rawValue.value; 117 + } 118 + return undefined; // Unsupported valueKind 119 + } 120 + 121 + // Check if it's a Type object (Model, String, Number, Boolean literals) 122 + if (rawValue && typeof rawValue === 'object' && rawValue.kind) { 123 + if (rawValue.kind === "String") { 124 + return (rawValue as StringLiteral).value; 125 + } else if (rawValue.kind === "Number") { 126 + return (rawValue as NumericLiteral).value; 127 + } else if (rawValue.kind === "Boolean") { 128 + return (rawValue as BooleanLiteral).value; 129 + } else if (rawValue.kind === "Model") { 130 + // Return the model itself for token references 131 + return rawValue as Model; 132 + } 133 + return undefined; // Unsupported kind 134 + } 135 + 136 + // Direct primitive value 137 + if (typeof rawValue === 'string' || typeof rawValue === 'number' || typeof rawValue === 'boolean') { 138 + return rawValue; 139 + } 140 + 141 + return undefined; 142 + } 100 143 101 144 async emit() { 102 145 const globalNs = this.program.getGlobalNamespaceType(); ··· 356 399 } 357 400 358 401 private addScalarToDefs(lexicon: LexiconDoc, scalar: Scalar) { 402 + // Only skip if the scalar itself is in TypeSpec namespace (built-in scalars) 359 403 if (scalar.namespace?.name === "TypeSpec") return; 360 - if (scalar.baseScalar?.namespace?.name === "TypeSpec") return; 361 404 362 405 // Skip @inline scalars - they should be inlined, not defined separately 363 406 if (isInline(this.program, scalar)) { ··· 368 411 const scalarDef = this.scalarToLexiconPrimitive(scalar, undefined); 369 412 if (scalarDef) { 370 413 const description = getDoc(this.program, scalar); 371 - lexicon.defs[defName] = { ...scalarDef, description } as LexUserType; 414 + 415 + // Apply @default decorator if present 416 + const rawDefault = getDefault(this.program, scalar); 417 + const defaultValue = this.processDefaultValue(rawDefault); 418 + let defWithDefault: any = { ...scalarDef }; 419 + 420 + if (defaultValue !== undefined) { 421 + // Check if it's a Type (model reference for tokens) 422 + if (typeof defaultValue === 'object' && 'kind' in defaultValue) { 423 + // For model references, we need to resolve to NSID 424 + // This shouldn't happen for scalars, only unions support token refs 425 + this.program.reportDiagnostic({ 426 + code: "invalid-default-on-scalar", 427 + severity: "error", 428 + message: "@default on scalars must be a literal value (string, number, or boolean), not a model reference", 429 + target: scalar, 430 + }); 431 + } else { 432 + // Validate that the default value matches the type 433 + this.assertValidValueForType(scalarDef.type, defaultValue, scalar); 434 + defWithDefault = { ...defWithDefault, default: defaultValue }; 435 + } 436 + } 437 + 438 + // Apply integer constraints for standalone scalar defs 439 + if (scalarDef.type === "integer") { 440 + const minValue = getMinValue(this.program, scalar); 441 + if (minValue !== undefined) { 442 + (defWithDefault as any).minimum = minValue; 443 + } 444 + const maxValue = getMaxValue(this.program, scalar); 445 + if (maxValue !== undefined) { 446 + (defWithDefault as any).maximum = maxValue; 447 + } 448 + } 449 + 450 + lexicon.defs[defName] = { ...defWithDefault, description } as LexUserType; 372 451 } 373 452 } 374 453 ··· 391 470 if (unionDef.type === "string" && (unionDef.knownValues || unionDef.enum)) { 392 471 const defName = name.charAt(0).toLowerCase() + name.slice(1); 393 472 const description = getDoc(this.program, union); 394 - lexicon.defs[defName] = { ...unionDef, description }; 473 + 474 + // Apply @default decorator if present 475 + const rawDefault = getDefault(this.program, union); 476 + const defaultValue = this.processDefaultValue(rawDefault); 477 + let defWithDefault: any = { ...unionDef }; 478 + 479 + if (defaultValue !== undefined) { 480 + // Check if it's a Type (model reference for tokens) 481 + if (typeof defaultValue === 'object' && 'kind' in defaultValue) { 482 + // Resolve the model reference to its NSID 483 + const tokenModel = defaultValue as Model; 484 + const tokenRef = this.getModelReference(tokenModel, true); // fullyQualified=true 485 + if (tokenRef) { 486 + defWithDefault = { ...defWithDefault, default: tokenRef }; 487 + } else { 488 + this.program.reportDiagnostic({ 489 + code: "invalid-default-token", 490 + severity: "error", 491 + message: "@default value must be a valid token model reference", 492 + target: union, 493 + }); 494 + } 495 + } else { 496 + // Literal value - validate it matches the union type 497 + if (typeof defaultValue !== "string") { 498 + this.program.reportDiagnostic({ 499 + code: "invalid-default-value-type", 500 + severity: "error", 501 + message: `Default value type mismatch: expected string, got ${typeof defaultValue}`, 502 + target: union, 503 + }); 504 + } else { 505 + defWithDefault = { ...defWithDefault, default: defaultValue }; 506 + } 507 + } 508 + } 509 + 510 + lexicon.defs[defName] = { ...defWithDefault, description }; 395 511 } else if (unionDef.type === "union") { 396 512 this.program.reportDiagnostic({ 397 513 code: "union-refs-not-allowed-as-def", ··· 401 517 `Use @inline to inline them at usage sites, use @token models for known values, or use string literals.`, 402 518 target: union, 403 519 }); 520 + } else if (unionDef.type === "integer" && (unionDef as any).enum) { 521 + // Integer enums can also be defs 522 + const defName = name.charAt(0).toLowerCase() + name.slice(1); 523 + const description = getDoc(this.program, union); 524 + 525 + // Apply @default decorator if present 526 + const rawDefault = getDefault(this.program, union); 527 + const defaultValue = this.processDefaultValue(rawDefault); 528 + let defWithDefault = { ...unionDef }; 529 + 530 + if (defaultValue !== undefined) { 531 + if (typeof defaultValue === "number") { 532 + defWithDefault = { ...defWithDefault, default: defaultValue }; 533 + } else { 534 + this.program.reportDiagnostic({ 535 + code: "invalid-default-value-type", 536 + severity: "error", 537 + message: `Default value type mismatch: expected integer, got ${typeof defaultValue}`, 538 + target: union, 539 + }); 540 + } 541 + } 542 + 543 + lexicon.defs[defName] = { ...defWithDefault, description }; 404 544 } 405 545 } 406 546 ··· 1158 1298 prop?: ModelProperty, 1159 1299 propDesc?: string, 1160 1300 ): LexObjectProperty | null { 1301 + // Check if this scalar should be referenced instead of inlined 1302 + const scalarRef = this.getScalarReference(scalar); 1303 + if (scalarRef) { 1304 + // Check if property has a default value that would conflict with the scalar's @default 1305 + if (prop?.defaultValue !== undefined) { 1306 + const scalarDefaultRaw = getDefault(this.program, scalar); 1307 + const scalarDefault = this.processDefaultValue(scalarDefaultRaw); 1308 + const propDefault = serializeValueAsJson(this.program, prop.defaultValue, prop); 1309 + 1310 + // If the scalar has a different default, or if the property has a default but the scalar doesn't, error 1311 + if (scalarDefault !== propDefault) { 1312 + this.program.reportDiagnostic({ 1313 + code: "conflicting-defaults", 1314 + severity: "error", 1315 + message: scalarDefault !== undefined 1316 + ? `Property default value conflicts with scalar's @default decorator. The scalar "${scalar.name}" has @default(${JSON.stringify(scalarDefault)}) but property has default value ${JSON.stringify(propDefault)}. Either remove the property default, mark the scalar @inline, or make the defaults match.` 1317 + : `Property has a default value but the referenced scalar "${scalar.name}" does not. Either add @default to the scalar, mark it @inline to allow property-level defaults, or remove the property default.`, 1318 + target: prop, 1319 + }); 1320 + } 1321 + } 1322 + 1323 + return { type: "ref" as const, ref: scalarRef, description: propDesc }; 1324 + } 1325 + 1326 + // Inline the scalar 1161 1327 const primitive = this.scalarToLexiconPrimitive(scalar, prop); 1162 1328 if (!primitive) return null; 1163 1329 ··· 1246 1412 if (!isDefining) { 1247 1413 const unionRef = this.getUnionReference(unionType); 1248 1414 if (unionRef) { 1415 + // Check if property has a default value that would conflict with the union's @default 1416 + if (prop?.defaultValue !== undefined) { 1417 + const unionDefaultRaw = getDefault(this.program, unionType); 1418 + const unionDefault = this.processDefaultValue(unionDefaultRaw); 1419 + const propDefault = serializeValueAsJson(this.program, prop.defaultValue, prop); 1420 + 1421 + // For union defaults that are model references, we need to resolve them for comparison 1422 + let resolvedUnionDefault: string | number | boolean | undefined = unionDefault as any; 1423 + if (unionDefault && typeof unionDefault === 'object' && 'kind' in unionDefault && unionDefault.kind === 'Model') { 1424 + const ref = this.getModelReference(unionDefault as Model, true); 1425 + resolvedUnionDefault = ref || undefined; 1426 + } 1427 + 1428 + // If the union has a different default, or if the property has a default but the union doesn't, error 1429 + if (resolvedUnionDefault !== propDefault) { 1430 + this.program.reportDiagnostic({ 1431 + code: "conflicting-defaults", 1432 + severity: "error", 1433 + message: unionDefault !== undefined 1434 + ? `Property default value conflicts with union's @default decorator. The union "${unionType.name}" has @default(${JSON.stringify(resolvedUnionDefault)}) but property has default value ${JSON.stringify(propDefault)}. Either remove the property default, mark the union @inline, or make the defaults match.` 1435 + : `Property has a default value but the referenced union "${unionType.name}" does not. Either add @default to the union, mark it @inline to allow property-level defaults, or remove the property default.`, 1436 + target: prop, 1437 + }); 1438 + } 1439 + } 1440 + 1249 1441 return { type: "ref" as const, ref: unionRef, description: propDesc }; 1250 1442 } 1251 1443 } ··· 1271 1463 // Check if this scalar (or its base) is bytes type 1272 1464 if (this.isScalarOfType(scalar, "bytes")) { 1273 1465 const byteDef: LexBytes = { type: "bytes" }; 1274 - const target = prop || scalar; 1275 1466 1276 - const minLength = getMinBytes(this.program, target); 1467 + // Check scalar first for its own constraints, then property overrides 1468 + const minLength = getMinBytes(this.program, scalar) ?? (prop ? getMinBytes(this.program, prop) : undefined); 1277 1469 if (minLength !== undefined) { 1278 1470 byteDef.minLength = minLength; 1279 1471 } 1280 1472 1281 - const maxLength = getMaxBytes(this.program, target); 1473 + const maxLength = getMaxBytes(this.program, scalar) ?? (prop ? getMaxBytes(this.program, prop) : undefined); 1282 1474 if (maxLength !== undefined) { 1283 1475 byteDef.maxLength = maxLength; 1284 1476 } ··· 1310 1502 1311 1503 // Apply string constraints 1312 1504 if (primitive.type === "string") { 1313 - const target = prop || scalar; 1314 - const maxLength = getMaxLength(this.program, target); 1505 + // Check scalar first for its own constraints, then property overrides 1506 + const maxLength = getMaxLength(this.program, scalar) ?? (prop ? getMaxLength(this.program, prop) : undefined); 1315 1507 if (maxLength !== undefined) { 1316 1508 primitive.maxLength = maxLength; 1317 1509 } 1318 - const minLength = getMinLength(this.program, target); 1510 + const minLength = getMinLength(this.program, scalar) ?? (prop ? getMinLength(this.program, prop) : undefined); 1319 1511 if (minLength !== undefined) { 1320 1512 primitive.minLength = minLength; 1321 1513 } 1322 - const maxGraphemes = getMaxGraphemes(this.program, target); 1514 + const maxGraphemes = getMaxGraphemes(this.program, scalar) ?? (prop ? getMaxGraphemes(this.program, prop) : undefined); 1323 1515 if (maxGraphemes !== undefined) { 1324 1516 primitive.maxGraphemes = maxGraphemes; 1325 1517 } 1326 - const minGraphemes = getMinGraphemes(this.program, target); 1518 + const minGraphemes = getMinGraphemes(this.program, scalar) ?? (prop ? getMinGraphemes(this.program, prop) : undefined); 1327 1519 if (minGraphemes !== undefined) { 1328 1520 primitive.minGraphemes = minGraphemes; 1329 1521 } 1330 1522 } 1331 1523 1332 1524 // Apply numeric constraints 1333 - if (prop && primitive.type === "integer") { 1334 - const minValue = getMinValue(this.program, prop); 1525 + if (primitive.type === "integer") { 1526 + // Check scalar first for its own constraints, then property overrides 1527 + const minValue = getMinValue(this.program, scalar) ?? (prop ? getMinValue(this.program, prop) : undefined); 1335 1528 if (minValue !== undefined) { 1336 1529 primitive.minimum = minValue; 1337 1530 } 1338 - const maxValue = getMaxValue(this.program, prop); 1531 + const maxValue = getMaxValue(this.program, scalar) ?? (prop ? getMaxValue(this.program, prop) : undefined); 1339 1532 if (maxValue !== undefined) { 1340 1533 primitive.maximum = maxValue; 1341 1534 } ··· 1431 1624 private assertValidValueForType( 1432 1625 primitiveType: string, 1433 1626 value: unknown, 1434 - prop: ModelProperty, 1627 + target: ModelProperty | Scalar | Union, 1435 1628 ): void { 1436 1629 const valid = 1437 1630 (primitiveType === "boolean" && typeof value === "boolean") || ··· 1442 1635 code: "invalid-default-value-type", 1443 1636 severity: "error", 1444 1637 message: `Default value type mismatch: expected ${primitiveType}, got ${typeof value}`, 1445 - target: prop, 1638 + target: target, 1446 1639 }); 1447 1640 } 1448 1641 } ··· 1507 1700 1508 1701 private getUnionReference(union: Union): string | null { 1509 1702 return this.getReference(union, union.name, union.namespace); 1703 + } 1704 + 1705 + private getScalarReference(scalar: Scalar): string | null { 1706 + // Built-in TypeSpec scalars (string, integer, boolean themselves) should not be referenced 1707 + if (scalar.namespace?.name === "TypeSpec") return null; 1708 + 1709 + // @inline scalars should be inlined, not referenced 1710 + if (isInline(this.program, scalar)) return null; 1711 + 1712 + // Scalars without names or namespace can't be referenced 1713 + if (!scalar.name || !scalar.namespace) return null; 1714 + 1715 + const defName = scalar.name.charAt(0).toLowerCase() + scalar.name.slice(1); 1716 + const namespaceName = getNamespaceFullName(scalar.namespace); 1717 + if (!namespaceName) return null; 1718 + 1719 + // Local reference (same namespace) - use short ref 1720 + if ( 1721 + this.currentLexiconId === namespaceName || 1722 + this.currentLexiconId === `${namespaceName}.defs` 1723 + ) { 1724 + return `#${defName}`; 1725 + } 1726 + 1727 + // Cross-namespace reference 1728 + return `${namespaceName}#${defName}`; 1510 1729 } 1511 1730 1512 1731 private modelToLexiconArray(
+2
packages/emitter/src/tsp-index.ts
··· 15 15 $maxBytes, 16 16 $minBytes, 17 17 $external, 18 + $default, 18 19 } from "./decorators.js"; 19 20 20 21 /** @internal */ ··· 36 37 maxBytes: $maxBytes, 37 38 minBytes: $minBytes, 38 39 external: $external, 40 + default: $default, 39 41 }, 40 42 };
+2
packages/emitter/test/integration/atproto/input/app/bsky/actor/defs.tsp
··· 232 232 prioritizeFollowedUsers?: boolean; 233 233 } 234 234 235 + @inline 235 236 @maxLength(640) 236 237 @maxGraphemes(64) 237 238 scalar InterestTag extends string; ··· 292 293 @required did: did; 293 294 } 294 295 296 + @inline 295 297 @maxLength(100) 296 298 scalar NudgeToken extends string; 297 299
+30
packages/emitter/test/spec/basic/input/com/example/scalarDefaults.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace com.example.scalarDefaults { 4 + /** Test default decorator on scalars */ 5 + model Main { 6 + /** Uses string scalar with default */ 7 + mode?: Mode; 8 + 9 + /** Uses integer scalar with default */ 10 + limit?: Limit; 11 + 12 + /** Uses boolean scalar with default */ 13 + enabled?: Enabled; 14 + } 15 + 16 + /** A string type with a default value */ 17 + @default("standard") 18 + @maxLength(50) 19 + scalar Mode extends string; 20 + 21 + /** An integer type with a default value */ 22 + @default(50) 23 + @minValue(1) 24 + @maxValue(100) 25 + scalar Limit extends integer; 26 + 27 + /** A boolean type with a default value */ 28 + @default(true) 29 + scalar Enabled extends boolean; 30 + }
+22
packages/emitter/test/spec/basic/input/com/example/scalarDefs.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace com.example.scalarDefs { 4 + /** Scalar defs should create standalone defs like models and unions */ 5 + model Main { 6 + /** Uses a custom string scalar with constraints */ 7 + tag?: Tag; 8 + 9 + /** Uses a custom integer scalar with constraints */ 10 + count?: Count; 11 + } 12 + 13 + /** A custom string type with length constraints */ 14 + @maxLength(100) 15 + @maxGraphemes(50) 16 + scalar Tag extends string; 17 + 18 + /** A custom integer type with value constraints */ 19 + @minValue(1) 20 + @maxValue(100) 21 + scalar Count extends integer; 22 + }
+22
packages/emitter/test/spec/basic/input/com/example/scalarInline.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace com.example.scalarInline { 4 + /** Test inline decorator on scalars */ 5 + model Main { 6 + /** Inline scalar - should not create a def */ 7 + tag?: Tag; 8 + 9 + /** Non-inline scalar - should create a def */ 10 + category?: Category; 11 + } 12 + 13 + /** An inline scalar should be inlined at usage sites */ 14 + @inline 15 + @maxLength(50) 16 + @maxGraphemes(25) 17 + scalar Tag extends string; 18 + 19 + /** A regular scalar should create a standalone def */ 20 + @maxLength(100) 21 + scalar Category extends string; 22 + }
+53
packages/emitter/test/spec/basic/input/com/example/unionDefaults.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace com.example.unionDefaults { 4 + /** Test default decorator on unions */ 5 + model Main { 6 + /** Union with token refs and default */ 7 + eventMode?: EventMode; 8 + 9 + /** Union with string literals and default */ 10 + sortOrder?: SortOrder; 11 + 12 + /** Union with integer literals and default */ 13 + priority?: Priority; 14 + } 15 + 16 + /** Union of tokens with default pointing to a token */ 17 + @default(Inperson) 18 + union EventMode { 19 + Hybrid, 20 + Inperson, 21 + Virtual, 22 + string, 23 + } 24 + 25 + /** A hybrid event */ 26 + @token 27 + model Hybrid {} 28 + 29 + /** An in-person event */ 30 + @token 31 + model Inperson {} 32 + 33 + /** A virtual event */ 34 + @token 35 + model Virtual {} 36 + 37 + /** Union of string literals with default */ 38 + @default("asc") 39 + union SortOrder { 40 + "asc", 41 + "desc", 42 + string, 43 + } 44 + 45 + /** Union of integer literals with default (closed enum) */ 46 + @default(1) 47 + @closed 48 + union Priority { 49 + 1, 50 + 2, 51 + 3, 52 + } 53 + }
+45
packages/emitter/test/spec/basic/output/com/example/scalarDefaults.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.example.scalarDefaults", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "properties": { 8 + "mode": { 9 + "type": "ref", 10 + "ref": "#mode", 11 + "description": "Uses string scalar with default" 12 + }, 13 + "limit": { 14 + "type": "ref", 15 + "ref": "#limit", 16 + "description": "Uses integer scalar with default" 17 + }, 18 + "enabled": { 19 + "type": "ref", 20 + "ref": "#enabled", 21 + "description": "Uses boolean scalar with default" 22 + } 23 + }, 24 + "description": "Test default decorator on scalars" 25 + }, 26 + "mode": { 27 + "type": "string", 28 + "maxLength": 50, 29 + "default": "standard", 30 + "description": "A string type with a default value" 31 + }, 32 + "limit": { 33 + "type": "integer", 34 + "minimum": 1, 35 + "maximum": 100, 36 + "default": 50, 37 + "description": "An integer type with a default value" 38 + }, 39 + "enabled": { 40 + "type": "boolean", 41 + "default": true, 42 + "description": "A boolean type with a default value" 43 + } 44 + } 45 + }
+34
packages/emitter/test/spec/basic/output/com/example/scalarDefs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.example.scalarDefs", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "properties": { 8 + "tag": { 9 + "type": "ref", 10 + "ref": "#tag", 11 + "description": "Uses a custom string scalar with constraints" 12 + }, 13 + "count": { 14 + "type": "ref", 15 + "ref": "#count", 16 + "description": "Uses a custom integer scalar with constraints" 17 + } 18 + }, 19 + "description": "Scalar defs should create standalone defs like models and unions" 20 + }, 21 + "tag": { 22 + "type": "string", 23 + "maxLength": 100, 24 + "maxGraphemes": 50, 25 + "description": "A custom string type with length constraints" 26 + }, 27 + "count": { 28 + "type": "integer", 29 + "minimum": 1, 30 + "maximum": 100, 31 + "description": "A custom integer type with value constraints" 32 + } 33 + } 34 + }
+28
packages/emitter/test/spec/basic/output/com/example/scalarInline.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.example.scalarInline", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "properties": { 8 + "tag": { 9 + "type": "string", 10 + "maxLength": 50, 11 + "maxGraphemes": 25, 12 + "description": "Inline scalar - should not create a def" 13 + }, 14 + "category": { 15 + "type": "ref", 16 + "ref": "#category", 17 + "description": "Non-inline scalar - should create a def" 18 + } 19 + }, 20 + "description": "Test inline decorator on scalars" 21 + }, 22 + "category": { 23 + "type": "string", 24 + "maxLength": 100, 25 + "description": "A regular scalar should create a standalone def" 26 + } 27 + } 28 + }
+61
packages/emitter/test/spec/basic/output/com/example/unionDefaults.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.example.unionDefaults", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "properties": { 8 + "eventMode": { 9 + "type": "ref", 10 + "ref": "#eventMode", 11 + "description": "Union with token refs and default" 12 + }, 13 + "sortOrder": { 14 + "type": "ref", 15 + "ref": "#sortOrder", 16 + "description": "Union with string literals and default" 17 + }, 18 + "priority": { 19 + "type": "ref", 20 + "ref": "#priority", 21 + "description": "Union with integer literals and default" 22 + } 23 + }, 24 + "description": "Test default decorator on unions" 25 + }, 26 + "eventMode": { 27 + "type": "string", 28 + "knownValues": [ 29 + "com.example.unionDefaults#hybrid", 30 + "com.example.unionDefaults#inperson", 31 + "com.example.unionDefaults#virtual" 32 + ], 33 + "default": "com.example.unionDefaults#inperson", 34 + "description": "Union of tokens with default pointing to a token" 35 + }, 36 + "hybrid": { 37 + "type": "token", 38 + "description": "A hybrid event" 39 + }, 40 + "inperson": { 41 + "type": "token", 42 + "description": "An in-person event" 43 + }, 44 + "virtual": { 45 + "type": "token", 46 + "description": "A virtual event" 47 + }, 48 + "sortOrder": { 49 + "type": "string", 50 + "knownValues": ["asc", "desc"], 51 + "default": "asc", 52 + "description": "Union of string literals with default" 53 + }, 54 + "priority": { 55 + "type": "integer", 56 + "enum": [1, 2, 3], 57 + "default": 1, 58 + "description": "Union of integer literals with default (closed enum)" 59 + } 60 + } 61 + }