@flink-app/ts-source-to-json-schema 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,808 @@
1
+ // ============================================================================
2
+ // Emitter - Transforms AST nodes into JSON Schema (2020-12 draft)
3
+ // ============================================================================
4
+ export class Emitter {
5
+ declarations = new Map();
6
+ options;
7
+ constructor(declarations, options = {}) {
8
+ for (const decl of declarations) {
9
+ this.declarations.set(decl.name, decl);
10
+ }
11
+ this.options = {
12
+ includeSchema: options.includeSchema ?? true,
13
+ schemaVersion: options.schemaVersion ?? "https://json-schema.org/draft/2020-12/schema",
14
+ strictObjects: options.strictObjects ?? false,
15
+ rootType: options.rootType ?? "",
16
+ includeJSDoc: options.includeJSDoc ?? true,
17
+ additionalProperties: options.additionalProperties,
18
+ followImports: options.followImports ?? "none",
19
+ baseDir: options.baseDir ?? "",
20
+ };
21
+ }
22
+ emit() {
23
+ const defs = {};
24
+ // Emit all declarations into $defs
25
+ for (const [name, decl] of this.declarations) {
26
+ defs[name] = this.emitDeclaration(decl);
27
+ }
28
+ // If a root type is specified, use it as the root schema
29
+ if (this.options.rootType && defs[this.options.rootType]) {
30
+ const root = defs[this.options.rootType];
31
+ // Check if root type is self-referential (directly or transitively)
32
+ const isSelfReferential = this.isTransitivelySelfReferential(this.options.rootType, defs);
33
+ if (isSelfReferential) {
34
+ // Keep root in $defs and make root a $ref to it
35
+ const result = {
36
+ $ref: `#/$defs/${this.options.rootType}`,
37
+ };
38
+ if (this.options.includeSchema) {
39
+ result.$schema = this.options.schemaVersion;
40
+ }
41
+ result.$defs = defs;
42
+ return result;
43
+ }
44
+ // Not self-referential, emit normally
45
+ delete defs[this.options.rootType];
46
+ const result = { ...root };
47
+ if (this.options.includeSchema) {
48
+ result.$schema = this.options.schemaVersion;
49
+ }
50
+ if (Object.keys(defs).length > 0) {
51
+ result.$defs = defs;
52
+ }
53
+ return result;
54
+ }
55
+ // Otherwise wrap everything under $defs
56
+ const result = {};
57
+ if (this.options.includeSchema) {
58
+ result.$schema = this.options.schemaVersion;
59
+ }
60
+ result.$defs = defs;
61
+ return result;
62
+ }
63
+ /**
64
+ * Emits schemas for all declarations at once.
65
+ * More efficient than calling emit() multiple times.
66
+ * Each schema is standalone with only its transitively referenced types in definitions.
67
+ */
68
+ emitAll() {
69
+ const schemas = {};
70
+ for (const [typeName, decl] of this.declarations) {
71
+ schemas[typeName] = this.emitDeclarationStandalone(decl);
72
+ }
73
+ return schemas;
74
+ }
75
+ /**
76
+ * Emits a declaration as a standalone schema with minimal definitions.
77
+ * Only includes types that are transitively referenced.
78
+ * Uses "definitions" (draft-07 style) instead of "$defs" for compatibility.
79
+ */
80
+ emitDeclarationStandalone(decl) {
81
+ // Find direct references from this declaration (not including the type itself)
82
+ const directRefs = this.findDirectReferences(decl);
83
+ // Collect all types transitively referenced (excluding the starting type)
84
+ const referencedTypes = new Set();
85
+ for (const ref of directRefs) {
86
+ this.collectTransitiveReferences(ref, referencedTypes);
87
+ }
88
+ // Check if this type is self-referential
89
+ const selfReferenced = referencedTypes.has(decl.name);
90
+ // Emit the main schema
91
+ const schema = this.emitDeclaration(decl);
92
+ // Build minimal definitions object
93
+ const definitions = {};
94
+ // If type is self-referential, include it in its own definitions
95
+ if (selfReferenced) {
96
+ definitions[decl.name] = schema;
97
+ }
98
+ // Add all referenced types
99
+ for (const typeName of referencedTypes) {
100
+ if (typeName !== decl.name) {
101
+ const referencedDecl = this.declarations.get(typeName);
102
+ if (referencedDecl) {
103
+ definitions[typeName] = this.emitDeclaration(referencedDecl);
104
+ }
105
+ }
106
+ }
107
+ // Convert $ref paths from #/$defs/ to #/definitions/
108
+ const schemaWithDefinitions = this.convertRefsToDefinitions(schema);
109
+ const convertedDefinitions = {};
110
+ for (const [name, def] of Object.entries(definitions)) {
111
+ convertedDefinitions[name] = this.convertRefsToDefinitions(def);
112
+ }
113
+ // Build result schema with $schema if enabled
114
+ const result = {
115
+ ...schemaWithDefinitions,
116
+ definitions: Object.keys(convertedDefinitions).length > 0 ? convertedDefinitions : {}
117
+ };
118
+ // Add $schema if enabled (default: true)
119
+ if (this.options.includeSchema !== false) {
120
+ result.$schema = this.options.schemaVersion || "https://json-schema.org/draft/2020-12/schema";
121
+ }
122
+ return result;
123
+ }
124
+ /**
125
+ * Recursively collects all type names that are transitively referenced by a given type.
126
+ */
127
+ collectTransitiveReferences(startTypeName, visited = new Set()) {
128
+ if (visited.has(startTypeName)) {
129
+ return visited;
130
+ }
131
+ visited.add(startTypeName);
132
+ const decl = this.declarations.get(startTypeName);
133
+ if (!decl)
134
+ return visited;
135
+ // Find all direct references in this declaration
136
+ const directRefs = this.findDirectReferences(decl);
137
+ // Recursively collect references from those types
138
+ for (const ref of directRefs) {
139
+ this.collectTransitiveReferences(ref, visited);
140
+ }
141
+ return visited;
142
+ }
143
+ /**
144
+ * Finds all direct type references in a declaration.
145
+ */
146
+ findDirectReferences(decl) {
147
+ const refs = [];
148
+ const collectRefs = (typeNode) => {
149
+ switch (typeNode.kind) {
150
+ case "reference":
151
+ // Skip built-in types
152
+ if (!this.isBuiltInType(typeNode.name)) {
153
+ refs.push(typeNode.name);
154
+ }
155
+ // Also collect from type arguments
156
+ if (typeNode.typeArgs) {
157
+ typeNode.typeArgs.forEach(arg => collectRefs(arg));
158
+ }
159
+ break;
160
+ case "object":
161
+ typeNode.properties.forEach(p => collectRefs(p.type));
162
+ if (typeNode.indexSignature) {
163
+ collectRefs(typeNode.indexSignature.keyType);
164
+ collectRefs(typeNode.indexSignature.valueType);
165
+ }
166
+ break;
167
+ case "array":
168
+ collectRefs(typeNode.element);
169
+ break;
170
+ case "tuple":
171
+ typeNode.elements.forEach(e => collectRefs(e.type));
172
+ break;
173
+ case "union":
174
+ typeNode.members.forEach(m => collectRefs(m));
175
+ break;
176
+ case "intersection":
177
+ typeNode.members.forEach(m => collectRefs(m));
178
+ break;
179
+ case "parenthesized":
180
+ collectRefs(typeNode.inner);
181
+ break;
182
+ case "record":
183
+ collectRefs(typeNode.keyType);
184
+ collectRefs(typeNode.valueType);
185
+ break;
186
+ case "mapped":
187
+ collectRefs(typeNode.constraint);
188
+ collectRefs(typeNode.valueType);
189
+ break;
190
+ }
191
+ };
192
+ if (decl.kind === "interface") {
193
+ decl.properties.forEach(p => collectRefs(p.type));
194
+ if (decl.extends) {
195
+ decl.extends.forEach(e => collectRefs(e));
196
+ }
197
+ if (decl.indexSignature) {
198
+ collectRefs(decl.indexSignature.keyType);
199
+ collectRefs(decl.indexSignature.valueType);
200
+ }
201
+ }
202
+ else if (decl.kind === "type_alias") {
203
+ collectRefs(decl.type);
204
+ }
205
+ // Enums don't have type references
206
+ return refs;
207
+ }
208
+ /**
209
+ * Checks if a type name refers to a built-in type that shouldn't be in definitions.
210
+ */
211
+ isBuiltInType(name) {
212
+ const builtIns = [
213
+ "Date", "Promise", "Array", "Set", "Map", "Record",
214
+ "Partial", "Required", "Pick", "Omit", "Readonly", "NonNullable"
215
+ ];
216
+ return builtIns.includes(name);
217
+ }
218
+ /**
219
+ * Recursively converts $ref paths from #/$defs/ to #/definitions/ in a schema.
220
+ */
221
+ convertRefsToDefinitions(schema) {
222
+ if (typeof schema !== "object" || schema === null) {
223
+ return schema;
224
+ }
225
+ const converted = {};
226
+ for (const [key, value] of Object.entries(schema)) {
227
+ if (key === "$ref" && typeof value === "string") {
228
+ // Convert #/$defs/TypeName to #/definitions/TypeName
229
+ converted[key] = value.replace(/^#\/\$defs\//, "#/definitions/");
230
+ }
231
+ else if (Array.isArray(value)) {
232
+ converted[key] = value.map(item => typeof item === "object" ? this.convertRefsToDefinitions(item) : item);
233
+ }
234
+ else if (typeof value === "object" && value !== null) {
235
+ converted[key] = this.convertRefsToDefinitions(value);
236
+ }
237
+ else {
238
+ converted[key] = value;
239
+ }
240
+ }
241
+ return converted;
242
+ }
243
+ /**
244
+ * Checks if a schema contains a reference to a specific type name.
245
+ * Used to detect self-referential types.
246
+ */
247
+ containsReference(schema, typeName) {
248
+ if (typeof schema !== "object" || schema === null) {
249
+ return false;
250
+ }
251
+ // Check if this is a direct reference to the type
252
+ if (schema.$ref === `#/$defs/${typeName}`) {
253
+ return true;
254
+ }
255
+ // Recursively check all properties
256
+ for (const value of Object.values(schema)) {
257
+ if (Array.isArray(value)) {
258
+ for (const item of value) {
259
+ if (typeof item === "object" && this.containsReference(item, typeName)) {
260
+ return true;
261
+ }
262
+ }
263
+ }
264
+ else if (typeof value === "object" && value !== null) {
265
+ if (this.containsReference(value, typeName)) {
266
+ return true;
267
+ }
268
+ }
269
+ }
270
+ return false;
271
+ }
272
+ /**
273
+ * Checks if a type is transitively self-referential.
274
+ * This includes both direct recursion (A → A) and mutual recursion (A → B → A).
275
+ */
276
+ isTransitivelySelfReferential(typeName, defs) {
277
+ // Helper to check if typeName is reachable from startType
278
+ const canReach = (startType, targetType, visited = new Set()) => {
279
+ if (startType === targetType) {
280
+ return true;
281
+ }
282
+ if (visited.has(startType)) {
283
+ return false;
284
+ }
285
+ visited.add(startType);
286
+ const schema = defs[startType];
287
+ if (!schema) {
288
+ return false;
289
+ }
290
+ // Get all types referenced by startType
291
+ const refs = this.getReferencedTypes(schema);
292
+ for (const ref of refs) {
293
+ if (ref === targetType) {
294
+ return true;
295
+ }
296
+ if (canReach(ref, targetType, visited)) {
297
+ return true;
298
+ }
299
+ }
300
+ return false;
301
+ };
302
+ // Check if typeName can reach itself (directly or transitively)
303
+ const refs = this.getReferencedTypes(defs[typeName]);
304
+ for (const ref of refs) {
305
+ if (canReach(ref, typeName)) {
306
+ return true;
307
+ }
308
+ }
309
+ return false;
310
+ }
311
+ /**
312
+ * Extracts all type names referenced in a schema.
313
+ */
314
+ getReferencedTypes(schema) {
315
+ const refs = [];
316
+ const collectRefs = (obj) => {
317
+ if (typeof obj !== "object" || obj === null) {
318
+ return;
319
+ }
320
+ if (obj.$ref && typeof obj.$ref === "string") {
321
+ const match = obj.$ref.match(/^#\/\$defs\/(.+)$/);
322
+ if (match) {
323
+ refs.push(match[1]);
324
+ }
325
+ }
326
+ for (const value of Object.values(obj)) {
327
+ if (Array.isArray(value)) {
328
+ value.forEach(collectRefs);
329
+ }
330
+ else if (typeof value === "object" && value !== null) {
331
+ collectRefs(value);
332
+ }
333
+ }
334
+ };
335
+ collectRefs(schema);
336
+ return refs;
337
+ }
338
+ // ---------------------------------------------------------------------------
339
+ // Declaration emission
340
+ // ---------------------------------------------------------------------------
341
+ emitDeclaration(decl) {
342
+ switch (decl.kind) {
343
+ case "interface": return this.emitInterface(decl);
344
+ case "type_alias": return this.emitTypeAlias(decl);
345
+ case "enum": return this.emitEnum(decl);
346
+ }
347
+ }
348
+ emitInterface(decl) {
349
+ const schema = this.emitObjectType(decl.properties, decl.indexSignature, decl.tags);
350
+ // Handle extends - merge parent properties via allOf
351
+ if (decl.extends && decl.extends.length > 0) {
352
+ const allOf = decl.extends.map((typeNode) => {
353
+ // If it's a simple reference, use $ref
354
+ if (typeNode.kind === "reference" && !typeNode.typeArgs) {
355
+ return { $ref: `#/$defs/${typeNode.name}` };
356
+ }
357
+ // If it's a utility type or complex type, resolve it inline
358
+ return this.emitType(typeNode);
359
+ });
360
+ // Only add the interface's own properties if it has any
361
+ const hasOwnProperties = decl.properties.length > 0 || decl.indexSignature !== undefined;
362
+ if (hasOwnProperties) {
363
+ allOf.push(schema);
364
+ }
365
+ // If only one schema in allOf, unwrap it
366
+ if (allOf.length === 1) {
367
+ const result = allOf[0];
368
+ if (this.options.includeJSDoc && decl.description)
369
+ result.description = decl.description;
370
+ return result;
371
+ }
372
+ const result = { allOf };
373
+ if (this.options.includeJSDoc && decl.description)
374
+ result.description = decl.description;
375
+ return result;
376
+ }
377
+ if (this.options.includeJSDoc && decl.description)
378
+ schema.description = decl.description;
379
+ return schema;
380
+ }
381
+ emitTypeAlias(decl) {
382
+ const schema = this.emitType(decl.type);
383
+ if (this.options.includeJSDoc) {
384
+ if (decl.description)
385
+ schema.description = decl.description;
386
+ // Apply @additionalProperties tag if this is an object type
387
+ if (decl.tags?.additionalProperties !== undefined && schema.type === "object") {
388
+ const value = decl.tags.additionalProperties.toLowerCase();
389
+ if (value === "true") {
390
+ schema.additionalProperties = true;
391
+ }
392
+ else if (value === "false") {
393
+ schema.additionalProperties = false;
394
+ }
395
+ }
396
+ }
397
+ return schema;
398
+ }
399
+ emitEnum(decl) {
400
+ const schema = {
401
+ enum: decl.members.map(m => m.value),
402
+ };
403
+ if (this.options.includeJSDoc && decl.description)
404
+ schema.description = decl.description;
405
+ // If all values are strings, add type: "string"
406
+ if (decl.members.every(m => typeof m.value === "string")) {
407
+ schema.type = "string";
408
+ }
409
+ else if (decl.members.every(m => typeof m.value === "number")) {
410
+ schema.type = "number";
411
+ }
412
+ return schema;
413
+ }
414
+ // ---------------------------------------------------------------------------
415
+ // Type node emission
416
+ // ---------------------------------------------------------------------------
417
+ emitType(node) {
418
+ switch (node.kind) {
419
+ case "primitive": return this.emitPrimitive(node.value);
420
+ case "literal_string": return { const: node.value };
421
+ case "literal_number": return { const: node.value };
422
+ case "literal_boolean": return { const: node.value };
423
+ case "object":
424
+ return this.emitObjectType(node.properties, node.indexSignature);
425
+ case "array":
426
+ return { type: "array", items: this.emitType(node.element) };
427
+ case "tuple":
428
+ return this.emitTuple(node);
429
+ case "union":
430
+ return this.emitUnion(node.members);
431
+ case "intersection":
432
+ return this.emitIntersection(node.members);
433
+ case "reference":
434
+ return this.emitReference(node);
435
+ case "parenthesized":
436
+ return this.emitType(node.inner);
437
+ case "record":
438
+ return this.emitRecord(node.keyType, node.valueType);
439
+ case "template_literal":
440
+ return { type: "string" }; // Best we can do without regex generation
441
+ case "mapped":
442
+ return { type: "object" }; // Fallback
443
+ default:
444
+ return {};
445
+ }
446
+ }
447
+ emitPrimitive(value) {
448
+ switch (value) {
449
+ case "string": return { type: "string" };
450
+ case "number": return { type: "number" };
451
+ case "boolean": return { type: "boolean" };
452
+ case "null": return { type: "null" };
453
+ case "undefined": return {}; // no JSON Schema equivalent
454
+ case "bigint": return { type: "integer" };
455
+ case "any": return {}; // accepts anything
456
+ case "unknown": return {}; // accepts anything
457
+ case "void": return {}; // no value
458
+ case "never": return { not: {} }; // matches nothing
459
+ case "object": return { type: "object" };
460
+ default: return {};
461
+ }
462
+ }
463
+ emitObjectType(properties, indexSignature, tags) {
464
+ const schema = { type: "object" };
465
+ const props = {};
466
+ const required = [];
467
+ for (const prop of properties) {
468
+ const propSchema = this.emitType(prop.type);
469
+ // Apply JSDoc tags
470
+ if (this.options.includeJSDoc) {
471
+ if (prop.description)
472
+ propSchema.description = prop.description;
473
+ if (prop.tags)
474
+ this.applyJSDocTags(propSchema, prop.tags);
475
+ }
476
+ if (prop.readonly)
477
+ propSchema.readOnly = true;
478
+ props[prop.name] = propSchema;
479
+ if (!prop.optional)
480
+ required.push(prop.name);
481
+ }
482
+ if (Object.keys(props).length > 0) {
483
+ schema.properties = props;
484
+ }
485
+ if (required.length > 0) {
486
+ schema.required = required;
487
+ }
488
+ // Handle additionalProperties in order of precedence:
489
+ // 1. Index signature
490
+ // 2. @additionalProperties JSDoc tag
491
+ // 3. strictObjects option
492
+ // 4. additionalProperties option
493
+ if (indexSignature) {
494
+ schema.additionalProperties = this.emitType(indexSignature.valueType);
495
+ }
496
+ else if (this.options.includeJSDoc && tags?.additionalProperties !== undefined) {
497
+ const value = tags.additionalProperties.toLowerCase();
498
+ if (value === "true") {
499
+ schema.additionalProperties = true;
500
+ }
501
+ else if (value === "false") {
502
+ schema.additionalProperties = false;
503
+ }
504
+ }
505
+ else if (this.options.strictObjects) {
506
+ schema.additionalProperties = false;
507
+ }
508
+ else if (this.options.additionalProperties !== undefined) {
509
+ schema.additionalProperties = this.options.additionalProperties;
510
+ }
511
+ return schema;
512
+ }
513
+ emitTuple(node) {
514
+ const schema = { type: "array" };
515
+ const requiredCount = node.elements.filter(e => !e.optional && !e.rest).length;
516
+ const hasRest = node.elements.some(e => e.rest);
517
+ if (hasRest) {
518
+ // Separate fixed elements and rest element
519
+ const fixed = node.elements.filter(e => !e.rest);
520
+ const rest = node.elements.find(e => e.rest);
521
+ schema.prefixItems = fixed.map(e => this.emitType(e.type));
522
+ schema.minItems = requiredCount;
523
+ if (rest) {
524
+ schema.items = this.emitType(rest.type);
525
+ }
526
+ }
527
+ else {
528
+ schema.prefixItems = node.elements.map(e => this.emitType(e.type));
529
+ schema.minItems = requiredCount;
530
+ schema.maxItems = node.elements.length;
531
+ }
532
+ return schema;
533
+ }
534
+ emitUnion(members) {
535
+ // Flatten nested unions
536
+ const flat = this.flattenUnion(members);
537
+ // Check if all members are string/number literals → use enum
538
+ const allStringLiterals = flat.every(m => m.kind === "literal_string");
539
+ if (allStringLiterals) {
540
+ return {
541
+ type: "string",
542
+ enum: flat.map(m => m.value),
543
+ };
544
+ }
545
+ const allNumberLiterals = flat.every(m => m.kind === "literal_number");
546
+ if (allNumberLiterals) {
547
+ return {
548
+ type: "number",
549
+ enum: flat.map(m => m.value),
550
+ };
551
+ }
552
+ // Check for nullable: T | null → make T nullable
553
+ const nullIndex = flat.findIndex(m => m.kind === "primitive" && m.value === "null");
554
+ const undefinedIndex = flat.findIndex(m => m.kind === "primitive" && m.value === "undefined");
555
+ const nonNullMembers = flat.filter(m => !(m.kind === "primitive" && (m.value === "null" || m.value === "undefined")));
556
+ if ((nullIndex !== -1 || undefinedIndex !== -1) && nonNullMembers.length === 1) {
557
+ // Simple nullable: string | null
558
+ const schema = this.emitType(nonNullMembers[0]);
559
+ if (typeof schema.type === "string") {
560
+ schema.type = [schema.type, "null"];
561
+ }
562
+ else {
563
+ return { anyOf: [schema, { type: "null" }] };
564
+ }
565
+ return schema;
566
+ }
567
+ // General union → anyOf
568
+ const schemas = flat.map(m => this.emitType(m));
569
+ return { anyOf: schemas };
570
+ }
571
+ flattenUnion(members) {
572
+ const result = [];
573
+ for (const m of members) {
574
+ if (m.kind === "union") {
575
+ result.push(...this.flattenUnion(m.members));
576
+ }
577
+ else {
578
+ result.push(m);
579
+ }
580
+ }
581
+ return result;
582
+ }
583
+ emitIntersection(members) {
584
+ // If all members are objects, try to merge them
585
+ const schemas = members.map(m => this.emitType(m));
586
+ if (schemas.length === 1)
587
+ return schemas[0];
588
+ return { allOf: schemas };
589
+ }
590
+ emitReference(node) {
591
+ // Handle built-in Date type
592
+ if (node.name === "Date" && !node.typeArgs) {
593
+ return { type: "string", format: "date-time" };
594
+ }
595
+ // Handle well-known utility types
596
+ if (node.typeArgs && node.typeArgs.length > 0) {
597
+ const resolved = this.resolveUtilityType(node.name, node.typeArgs);
598
+ if (resolved)
599
+ return resolved;
600
+ }
601
+ // If the declaration exists and is simple, we could inline it,
602
+ // but using $ref is more correct and handles circular refs
603
+ return { $ref: `#/$defs/${node.name}` };
604
+ }
605
+ emitRecord(keyType, valueType) {
606
+ const schema = { type: "object" };
607
+ // If key is a union of string literals, emit explicit properties
608
+ if (keyType.kind === "union") {
609
+ const allStringLiterals = keyType.members.every(m => m.kind === "literal_string");
610
+ if (allStringLiterals) {
611
+ const valueSchema = this.emitType(valueType);
612
+ schema.properties = {};
613
+ schema.required = [];
614
+ for (const m of keyType.members) {
615
+ const key = m.value;
616
+ schema.properties[key] = { ...valueSchema };
617
+ schema.required.push(key);
618
+ }
619
+ return schema;
620
+ }
621
+ }
622
+ if (keyType.kind === "literal_string") {
623
+ const valueSchema = this.emitType(valueType);
624
+ schema.properties = { [keyType.value]: valueSchema };
625
+ schema.required = [keyType.value];
626
+ return schema;
627
+ }
628
+ // General Record<string, V>
629
+ schema.additionalProperties = this.emitType(valueType);
630
+ return schema;
631
+ }
632
+ // ---------------------------------------------------------------------------
633
+ // Utility type resolution
634
+ // ---------------------------------------------------------------------------
635
+ resolveUtilityType(name, typeArgs) {
636
+ switch (name) {
637
+ case "Partial":
638
+ return this.resolvePartial(typeArgs[0]);
639
+ case "Required":
640
+ return this.resolveRequired(typeArgs[0]);
641
+ case "Pick":
642
+ if (typeArgs.length === 2)
643
+ return this.resolvePick(typeArgs[0], typeArgs[1]);
644
+ return null;
645
+ case "Omit":
646
+ if (typeArgs.length === 2)
647
+ return this.resolveOmit(typeArgs[0], typeArgs[1]);
648
+ return null;
649
+ case "Readonly":
650
+ return this.emitType(typeArgs[0]); // Schema doesn't enforce readonly
651
+ case "NonNullable":
652
+ return this.emitType(typeArgs[0]); // Already non-null in JSON
653
+ case "Set":
654
+ return { type: "array", items: this.emitType(typeArgs[0]), uniqueItems: true };
655
+ case "Map":
656
+ if (typeArgs.length === 2) {
657
+ return { type: "object", additionalProperties: this.emitType(typeArgs[1]) };
658
+ }
659
+ return null;
660
+ default:
661
+ return null;
662
+ }
663
+ }
664
+ resolvePartial(target) {
665
+ // For references, look up the declaration and make all properties optional
666
+ if (target.kind === "reference") {
667
+ const decl = this.declarations.get(target.name);
668
+ if (decl && (decl.kind === "interface" || (decl.kind === "type_alias" && decl.type.kind === "object"))) {
669
+ const props = decl.kind === "interface" ? decl.properties : decl.type.properties;
670
+ const schema = this.emitObjectType(props.map((p) => ({ ...p, optional: true })));
671
+ return schema;
672
+ }
673
+ }
674
+ // For inline objects
675
+ if (target.kind === "object") {
676
+ return this.emitObjectType(target.properties.map(p => ({ ...p, optional: true })));
677
+ }
678
+ // Fallback: emit as-is
679
+ return this.emitType(target);
680
+ }
681
+ resolveRequired(target) {
682
+ if (target.kind === "reference") {
683
+ const decl = this.declarations.get(target.name);
684
+ if (decl && (decl.kind === "interface" || (decl.kind === "type_alias" && decl.type.kind === "object"))) {
685
+ const props = decl.kind === "interface" ? decl.properties : decl.type.properties;
686
+ return this.emitObjectType(props.map((p) => ({ ...p, optional: false })));
687
+ }
688
+ }
689
+ if (target.kind === "object") {
690
+ return this.emitObjectType(target.properties.map(p => ({ ...p, optional: false })));
691
+ }
692
+ return this.emitType(target);
693
+ }
694
+ resolvePick(target, keys) {
695
+ const keyNames = this.extractKeyNames(keys);
696
+ if (!keyNames)
697
+ return this.emitType(target);
698
+ const props = this.getProperties(target);
699
+ if (!props)
700
+ return this.emitType(target);
701
+ return this.emitObjectType(props.filter(p => keyNames.has(p.name)));
702
+ }
703
+ resolveOmit(target, keys) {
704
+ const keyNames = this.extractKeyNames(keys);
705
+ if (!keyNames)
706
+ return this.emitType(target);
707
+ const props = this.getProperties(target);
708
+ if (!props)
709
+ return this.emitType(target);
710
+ return this.emitObjectType(props.filter(p => !keyNames.has(p.name)));
711
+ }
712
+ extractKeyNames(node) {
713
+ if (node.kind === "literal_string")
714
+ return new Set([node.value]);
715
+ if (node.kind === "union") {
716
+ const names = new Set();
717
+ for (const m of node.members) {
718
+ if (m.kind === "literal_string")
719
+ names.add(m.value);
720
+ else
721
+ return null;
722
+ }
723
+ return names;
724
+ }
725
+ return null;
726
+ }
727
+ getProperties(node) {
728
+ if (node.kind === "object")
729
+ return node.properties;
730
+ if (node.kind === "reference") {
731
+ const decl = this.declarations.get(node.name);
732
+ if (!decl)
733
+ return null;
734
+ if (decl.kind === "interface")
735
+ return decl.properties;
736
+ if (decl.kind === "type_alias" && decl.type.kind === "object")
737
+ return decl.type.properties;
738
+ }
739
+ return null;
740
+ }
741
+ // ---------------------------------------------------------------------------
742
+ // JSDoc tag application
743
+ // ---------------------------------------------------------------------------
744
+ applyJSDocTags(schema, tags) {
745
+ for (const [key, value] of Object.entries(tags)) {
746
+ switch (key) {
747
+ case "minimum":
748
+ schema.minimum = Number(value);
749
+ break;
750
+ case "maximum":
751
+ schema.maximum = Number(value);
752
+ break;
753
+ case "minLength":
754
+ schema.minLength = Number(value);
755
+ break;
756
+ case "maxLength":
757
+ schema.maxLength = Number(value);
758
+ break;
759
+ case "pattern":
760
+ schema.pattern = value;
761
+ break;
762
+ case "format":
763
+ schema.format = value;
764
+ break;
765
+ case "default":
766
+ try {
767
+ schema.default = JSON.parse(value);
768
+ }
769
+ catch {
770
+ schema.default = value;
771
+ }
772
+ break;
773
+ case "example":
774
+ case "examples":
775
+ try {
776
+ if (!schema.examples)
777
+ schema.examples = [];
778
+ schema.examples.push(JSON.parse(value));
779
+ }
780
+ catch {
781
+ if (!schema.examples)
782
+ schema.examples = [];
783
+ schema.examples.push(value);
784
+ }
785
+ break;
786
+ case "deprecated":
787
+ schema.deprecated = true;
788
+ break;
789
+ case "title":
790
+ schema.title = value;
791
+ break;
792
+ case "additionalProperties":
793
+ // Only apply to object types
794
+ if (schema.type === "object") {
795
+ const lowerValue = value.toLowerCase();
796
+ if (lowerValue === "true") {
797
+ schema.additionalProperties = true;
798
+ }
799
+ else if (lowerValue === "false") {
800
+ schema.additionalProperties = false;
801
+ }
802
+ }
803
+ break;
804
+ }
805
+ }
806
+ }
807
+ }
808
+ //# sourceMappingURL=emitter.js.map