@depup/openapi-typescript 7.13.0-depup.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.
Files changed (106) hide show
  1. package/CHANGELOG.md +1162 -0
  2. package/CONTRIBUTING.md +149 -0
  3. package/LICENSE +21 -0
  4. package/README.md +32 -0
  5. package/bin/cli.js +297 -0
  6. package/changes.json +14 -0
  7. package/dist/index.cjs +152 -0
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.d.cts +517 -0
  10. package/dist/index.d.mts +515 -0
  11. package/dist/index.d.ts +517 -0
  12. package/dist/index.mjs +85 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/lib/redoc.cjs +129 -0
  15. package/dist/lib/redoc.cjs.map +1 -0
  16. package/dist/lib/redoc.mjs +122 -0
  17. package/dist/lib/redoc.mjs.map +1 -0
  18. package/dist/lib/ts.cjs +470 -0
  19. package/dist/lib/ts.cjs.map +1 -0
  20. package/dist/lib/ts.mjs +431 -0
  21. package/dist/lib/ts.mjs.map +1 -0
  22. package/dist/lib/utils.cjs +295 -0
  23. package/dist/lib/utils.cjs.map +1 -0
  24. package/dist/lib/utils.mjs +278 -0
  25. package/dist/lib/utils.mjs.map +1 -0
  26. package/dist/transform/components-object.cjs +150 -0
  27. package/dist/transform/components-object.cjs.map +1 -0
  28. package/dist/transform/components-object.mjs +127 -0
  29. package/dist/transform/components-object.mjs.map +1 -0
  30. package/dist/transform/header-object.cjs +48 -0
  31. package/dist/transform/header-object.cjs.map +1 -0
  32. package/dist/transform/header-object.mjs +42 -0
  33. package/dist/transform/header-object.mjs.map +1 -0
  34. package/dist/transform/index.cjs +124 -0
  35. package/dist/transform/index.cjs.map +1 -0
  36. package/dist/transform/index.mjs +118 -0
  37. package/dist/transform/index.mjs.map +1 -0
  38. package/dist/transform/media-type-object.cjs +14 -0
  39. package/dist/transform/media-type-object.cjs.map +1 -0
  40. package/dist/transform/media-type-object.mjs +12 -0
  41. package/dist/transform/media-type-object.mjs.map +1 -0
  42. package/dist/transform/operation-object.cjs +105 -0
  43. package/dist/transform/operation-object.cjs.map +1 -0
  44. package/dist/transform/operation-object.mjs +96 -0
  45. package/dist/transform/operation-object.mjs.map +1 -0
  46. package/dist/transform/parameter-object.cjs +11 -0
  47. package/dist/transform/parameter-object.cjs.map +1 -0
  48. package/dist/transform/parameter-object.mjs +9 -0
  49. package/dist/transform/parameter-object.mjs.map +1 -0
  50. package/dist/transform/parameters-array.cjs +120 -0
  51. package/dist/transform/parameters-array.cjs.map +1 -0
  52. package/dist/transform/parameters-array.mjs +114 -0
  53. package/dist/transform/parameters-array.mjs.map +1 -0
  54. package/dist/transform/path-item-object.cjs +84 -0
  55. package/dist/transform/path-item-object.cjs.map +1 -0
  56. package/dist/transform/path-item-object.mjs +78 -0
  57. package/dist/transform/path-item-object.mjs.map +1 -0
  58. package/dist/transform/paths-enum.cjs +35 -0
  59. package/dist/transform/paths-enum.cjs.map +1 -0
  60. package/dist/transform/paths-enum.mjs +33 -0
  61. package/dist/transform/paths-enum.mjs.map +1 -0
  62. package/dist/transform/paths-object.cjs +134 -0
  63. package/dist/transform/paths-object.cjs.map +1 -0
  64. package/dist/transform/paths-object.mjs +128 -0
  65. package/dist/transform/paths-object.mjs.map +1 -0
  66. package/dist/transform/request-body-object.cjs +68 -0
  67. package/dist/transform/request-body-object.cjs.map +1 -0
  68. package/dist/transform/request-body-object.mjs +62 -0
  69. package/dist/transform/request-body-object.mjs.map +1 -0
  70. package/dist/transform/response-object.cjs +123 -0
  71. package/dist/transform/response-object.cjs.map +1 -0
  72. package/dist/transform/response-object.mjs +117 -0
  73. package/dist/transform/response-object.mjs.map +1 -0
  74. package/dist/transform/responses-object.cjs +36 -0
  75. package/dist/transform/responses-object.cjs.map +1 -0
  76. package/dist/transform/responses-object.mjs +30 -0
  77. package/dist/transform/responses-object.mjs.map +1 -0
  78. package/dist/transform/schema-object.cjs +502 -0
  79. package/dist/transform/schema-object.cjs.map +1 -0
  80. package/dist/transform/schema-object.mjs +493 -0
  81. package/dist/transform/schema-object.mjs.map +1 -0
  82. package/dist/transform/webhooks-object.cjs +37 -0
  83. package/dist/transform/webhooks-object.cjs.map +1 -0
  84. package/dist/transform/webhooks-object.mjs +31 -0
  85. package/dist/transform/webhooks-object.mjs.map +1 -0
  86. package/package.json +102 -0
  87. package/src/index.ts +108 -0
  88. package/src/lib/redoc.ts +164 -0
  89. package/src/lib/ts.ts +616 -0
  90. package/src/lib/utils.ts +394 -0
  91. package/src/transform/components-object.ts +161 -0
  92. package/src/transform/header-object.ts +45 -0
  93. package/src/transform/index.ts +116 -0
  94. package/src/transform/media-type-object.ts +18 -0
  95. package/src/transform/operation-object.ts +104 -0
  96. package/src/transform/parameter-object.ts +15 -0
  97. package/src/transform/parameters-array.ts +129 -0
  98. package/src/transform/path-item-object.ts +102 -0
  99. package/src/transform/paths-enum.ts +43 -0
  100. package/src/transform/paths-object.ts +134 -0
  101. package/src/transform/request-body-object.ts +59 -0
  102. package/src/transform/response-object.ts +112 -0
  103. package/src/transform/responses-object.ts +36 -0
  104. package/src/transform/schema-object.ts +737 -0
  105. package/src/transform/webhooks-object.ts +27 -0
  106. package/src/types.ts +735 -0
@@ -0,0 +1,737 @@
1
+ import { parseRef } from "@redocly/openapi-core/lib/ref-utils.js";
2
+ import ts from "typescript";
3
+ import {
4
+ addJSDocComment,
5
+ BOOLEAN,
6
+ NEVER,
7
+ NULL,
8
+ NUMBER,
9
+ oapiRef,
10
+ QUESTION_TOKEN,
11
+ STRING,
12
+ tsArrayLiteralExpression,
13
+ tsEnum,
14
+ tsIntersection,
15
+ tsIsPrimitive,
16
+ tsLiteral,
17
+ tsModifiers,
18
+ tsNullable,
19
+ tsOmit,
20
+ tsPropertyIndex,
21
+ tsRecord,
22
+ tsUnion,
23
+ tsWithRequired,
24
+ UNDEFINED,
25
+ UNKNOWN,
26
+ } from "../lib/ts.js";
27
+ import { createDiscriminatorProperty, createRef, getEntries } from "../lib/utils.js";
28
+ import type { ReferenceObject, SchemaObject, TransformNodeOptions } from "../types.js";
29
+
30
+ /**
31
+ * Transform SchemaObject nodes (4.8.24)
32
+ * @see https://spec.openapis.org/oas/v3.1.0#schema-object
33
+ */
34
+ export default function transformSchemaObject(
35
+ schemaObject: SchemaObject | ReferenceObject,
36
+ options: TransformNodeOptions,
37
+ fromAdditionalProperties = false,
38
+ ): ts.TypeNode {
39
+ const type = transformSchemaObjectWithComposition(schemaObject, options, fromAdditionalProperties);
40
+ if (typeof options.ctx.postTransform === "function") {
41
+ const postTransformResult = options.ctx.postTransform(type, options);
42
+ if (postTransformResult) {
43
+ return postTransformResult;
44
+ }
45
+ }
46
+ return type;
47
+ }
48
+
49
+ /**
50
+ * Transform SchemaObjects
51
+ */
52
+ export function transformSchemaObjectWithComposition(
53
+ schemaObject: SchemaObject | ReferenceObject,
54
+ options: TransformNodeOptions,
55
+ fromAdditionalProperties = false,
56
+ ): ts.TypeNode {
57
+ /**
58
+ * Unexpected types & edge cases
59
+ */
60
+
61
+ // missing/falsy type returns `never`
62
+ if (!schemaObject) {
63
+ return NEVER;
64
+ }
65
+ // `true` returns `unknown` (this exists, but is untyped)
66
+ if ((schemaObject as unknown) === true) {
67
+ return UNKNOWN;
68
+ }
69
+ // for any other unexpected type, throw error
70
+ if (Array.isArray(schemaObject) || typeof schemaObject !== "object") {
71
+ throw new Error(
72
+ `Expected SchemaObject, received ${Array.isArray(schemaObject) ? "Array" : typeof schemaObject} at ${options.path}`,
73
+ );
74
+ }
75
+
76
+ /**
77
+ * ReferenceObject
78
+ */
79
+ if ("$ref" in schemaObject) {
80
+ return oapiRef(schemaObject.$ref);
81
+ }
82
+
83
+ /**
84
+ * const (valid for any type)
85
+ */
86
+ if (schemaObject.const !== null && schemaObject.const !== undefined) {
87
+ return tsLiteral(schemaObject.const);
88
+ }
89
+
90
+ /**
91
+ * enum (non-objects)
92
+ * note: enum is valid for any type, but for objects, handle in oneOf below
93
+ */
94
+ if (
95
+ Array.isArray(schemaObject.enum) &&
96
+ (!("type" in schemaObject) || schemaObject.type !== "object") &&
97
+ !("properties" in schemaObject) &&
98
+ !("additionalProperties" in schemaObject)
99
+ ) {
100
+ // hoist enum to top level if string/number enum and option is enabled
101
+ if (shouldTransformToTsEnum(options, schemaObject)) {
102
+ let enumName = parseRef(options.path ?? "").pointer.join("/");
103
+ // allow #/components/schemas to have simpler names
104
+ enumName = enumName.replace("components/schemas", "");
105
+ const metadata = schemaObject.enum.map((_, i) => ({
106
+ name: schemaObject["x-enum-varnames"]?.[i] ?? schemaObject["x-enumNames"]?.[i],
107
+ description: schemaObject["x-enum-descriptions"]?.[i] ?? schemaObject["x-enumDescriptions"]?.[i],
108
+ }));
109
+
110
+ // enums can contain null values, but dont want to output them
111
+ let hasNull = false;
112
+ const validSchemaEnums = schemaObject.enum.filter((enumValue) => {
113
+ if (enumValue === null) {
114
+ hasNull = true;
115
+ return false;
116
+ }
117
+
118
+ return true;
119
+ });
120
+ const enumType = tsEnum(enumName, validSchemaEnums as (string | number)[], metadata, {
121
+ shouldCache: options.ctx.dedupeEnums,
122
+ export: true,
123
+ // readonly: TS enum do not support the readonly modifier
124
+ });
125
+ if (!options.ctx.injectFooter.includes(enumType)) {
126
+ options.ctx.injectFooter.push(enumType);
127
+ }
128
+ const ref = ts.factory.createTypeReferenceNode(enumType.name);
129
+ return hasNull ? tsUnion([ref, NULL]) : ref;
130
+ }
131
+ const enumType = schemaObject.enum.map(tsLiteral);
132
+ if ((Array.isArray(schemaObject.type) && schemaObject.type.includes("null")) || schemaObject.nullable) {
133
+ enumType.push(NULL);
134
+ }
135
+
136
+ const unionType = tsUnion(enumType);
137
+
138
+ // hoist array with valid enum values to top level if string/number enum and option is enabled
139
+ if (options.ctx.enumValues && schemaObject.enum.every((v) => typeof v === "string" || typeof v === "number")) {
140
+ const parsed = parseRef(options.path ?? "");
141
+ let enumValuesVariableName = parsed.pointer.join("/");
142
+ // allow #/components/schemas to have simpler names
143
+ enumValuesVariableName = enumValuesVariableName.replace("components/schemas", "");
144
+ enumValuesVariableName = `${enumValuesVariableName}Values`;
145
+
146
+ // build a ref path for the type that ignores union indices (anyOf/oneOf) so
147
+ // type references remain stable even when names include union positions
148
+ const cleanedPointer: string[] = [];
149
+ // Track ALL properties after a oneOf/anyOf that need Extract<> narrowing.
150
+ // We apply Extract<> before EVERY property access after a union index because:
151
+ // - When the property exists on ALL variants, Extract<> is a no-op (returns same type)
152
+ // - When the property only exists on SOME variants, it correctly narrows the union
153
+ // - When both variants have same property name but different inner schemas,
154
+ // we still narrow at each level to handle nested unions correctly
155
+ // This robust approach handles both simple and complex union structures.
156
+ const extractProperties: string[] = [];
157
+ for (let i = 0; i < parsed.pointer.length; i++) {
158
+ // Example: #/paths/analytics/data/get/responses/400/content/application/json/anyOf/0/message
159
+ const segment = parsed.pointer[i];
160
+ if ((segment === "anyOf" || segment === "oneOf") && i < parsed.pointer.length - 1) {
161
+ const next = parsed.pointer[i + 1];
162
+ if (/^\d+$/.test(next)) {
163
+ // If we encounter something like "anyOf/0", we want to skip that part of the path
164
+ i++;
165
+ // Collect ALL remaining segments after the union index.
166
+ // Each one will be wrapped with Extract<> to safely narrow the type
167
+ // at each level, handling both top-level and nested union variants.
168
+ const remainingSegments = parsed.pointer.slice(i + 1);
169
+ for (const seg of remainingSegments) {
170
+ // Skip union keywords and indices, only add actual property names
171
+ if (seg !== "anyOf" && seg !== "oneOf" && !/^\d+$/.test(seg)) {
172
+ extractProperties.push(seg);
173
+ }
174
+ }
175
+ continue;
176
+ }
177
+ }
178
+ cleanedPointer.push(segment);
179
+ }
180
+ const cleanedRefPath = createRef(cleanedPointer);
181
+
182
+ const enumValuesArray = tsArrayLiteralExpression(
183
+ enumValuesVariableName,
184
+ // If fromAdditionalProperties is true we are dealing with a record type and we should append [string] to the generated type
185
+ fromAdditionalProperties
186
+ ? ts.factory.createIndexedAccessTypeNode(
187
+ oapiRef(cleanedRefPath, undefined, { deep: true, extractProperties }),
188
+ ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("string")),
189
+ )
190
+ : oapiRef(cleanedRefPath, undefined, { deep: true, extractProperties }),
191
+ schemaObject.enum as (string | number)[],
192
+ {
193
+ export: true,
194
+ readonly: true,
195
+ injectFooter: options.ctx.injectFooter,
196
+ },
197
+ );
198
+
199
+ options.ctx.injectFooter.push(enumValuesArray);
200
+ }
201
+
202
+ return unionType;
203
+ }
204
+
205
+ /**
206
+ * Object + composition (anyOf/allOf/oneOf) types
207
+ */
208
+
209
+ /** Collect oneOf/anyOf */
210
+ function collectUnionCompositions(items: (SchemaObject | ReferenceObject)[], unionKey: "anyOf" | "oneOf") {
211
+ const output: ts.TypeNode[] = [];
212
+ for (const [index, item] of items.entries()) {
213
+ output.push(
214
+ transformSchemaObject(item, {
215
+ ...options,
216
+ // include index in path so generated names from nested enums/enumValues are unique
217
+ path: createRef([options.path, unionKey, String(index)]),
218
+ }),
219
+ );
220
+ }
221
+
222
+ return output;
223
+ }
224
+
225
+ /** Collect allOf with Omit<> for discriminators */
226
+ function collectAllOfCompositions(items: (SchemaObject | ReferenceObject)[], required?: string[]): ts.TypeNode[] {
227
+ const output: ts.TypeNode[] = [];
228
+ for (const item of items) {
229
+ let itemType: ts.TypeNode;
230
+ // if this is a $ref, use WithRequired<X, Y> if parent specifies required properties
231
+ // (but only for valid keys)
232
+ if ("$ref" in item) {
233
+ itemType = transformSchemaObject(item, options);
234
+
235
+ const resolved = options.ctx.resolve<SchemaObject>(item.$ref);
236
+
237
+ // make keys required, if necessary
238
+ if (
239
+ resolved &&
240
+ typeof resolved === "object" &&
241
+ "properties" in resolved &&
242
+ // we have already handled this item (discriminator property was already added as required)
243
+ !options.ctx.discriminators.refsHandled.includes(item.$ref)
244
+ ) {
245
+ // add WithRequired<X, Y> if necessary
246
+ const validRequired = (required ?? []).filter((key) => !!resolved.properties?.[key]);
247
+ if (validRequired.length) {
248
+ itemType = tsWithRequired(itemType, validRequired, options.ctx.injectFooter);
249
+ }
250
+ }
251
+ }
252
+ // otherwise, if this is a schema object, combine parent `required[]` with its own, if any
253
+ else {
254
+ const itemRequired = [...(required ?? [])];
255
+ if (typeof item === "object" && Array.isArray(item.required)) {
256
+ itemRequired.push(...item.required);
257
+ }
258
+ itemType = transformSchemaObject({ ...item, required: itemRequired }, options);
259
+ }
260
+
261
+ const discriminator =
262
+ ("$ref" in item && options.ctx.discriminators.objects[item.$ref]) || (item as any).discriminator;
263
+ if (discriminator) {
264
+ output.push(tsOmit(itemType, [discriminator.propertyName]));
265
+ } else {
266
+ output.push(itemType);
267
+ }
268
+ }
269
+ return output;
270
+ }
271
+
272
+ // compile final type
273
+ let finalType: ts.TypeNode | undefined;
274
+
275
+ // core + allOf: intersect
276
+ const coreObjectType = transformSchemaObjectCore(schemaObject, options);
277
+ const allOfType = collectAllOfCompositions(schemaObject.allOf ?? [], schemaObject.required);
278
+ if (coreObjectType || allOfType.length) {
279
+ const allOf: ts.TypeNode | undefined = allOfType.length ? tsIntersection(allOfType) : undefined;
280
+ finalType = tsIntersection([...(coreObjectType ? [coreObjectType] : []), ...(allOf ? [allOf] : [])]);
281
+ }
282
+ // anyOf: union
283
+ // (note: this may seem counterintuitive, but as TypeScript’s unions are not true XORs, they mimic behavior closer to anyOf than oneOf)
284
+ const anyOfType = collectUnionCompositions(schemaObject.anyOf ?? [], "anyOf");
285
+ if (anyOfType.length) {
286
+ finalType = tsUnion([...(finalType ? [finalType] : []), ...anyOfType]);
287
+ }
288
+ // oneOf: union (within intersection with other types, if any)
289
+ const oneOfType = collectUnionCompositions(
290
+ schemaObject.oneOf ||
291
+ ("type" in schemaObject &&
292
+ schemaObject.type === "object" &&
293
+ (schemaObject.enum as (SchemaObject | ReferenceObject)[])) ||
294
+ [],
295
+ "oneOf",
296
+ );
297
+ if (oneOfType.length) {
298
+ // note: oneOf is the only type that may include primitives
299
+ if (oneOfType.every(tsIsPrimitive)) {
300
+ finalType = tsUnion([...(finalType ? [finalType] : []), ...oneOfType]);
301
+ } else {
302
+ finalType = tsIntersection([...(finalType ? [finalType] : []), tsUnion(oneOfType)]);
303
+ }
304
+ }
305
+
306
+ // When no final type can be generated, fall back to unknown type (or related variants)
307
+ if (!finalType) {
308
+ if ("type" in schemaObject) {
309
+ finalType = tsRecord(STRING, options.ctx.emptyObjectsUnknown ? UNKNOWN : NEVER);
310
+ } else {
311
+ finalType = UNKNOWN;
312
+ }
313
+ }
314
+
315
+ if (finalType !== UNKNOWN && schemaObject.nullable) {
316
+ finalType = tsNullable([finalType]);
317
+ }
318
+
319
+ return finalType;
320
+ }
321
+
322
+ /**
323
+ * Check if the given OAPI enum should be transformed to a TypeScript enum
324
+ */
325
+ function shouldTransformToTsEnum(options: TransformNodeOptions, schemaObject: SchemaObject): boolean {
326
+ // Enum conversion not enabled or no enum present
327
+ if (!options.ctx.enum || !schemaObject.enum) {
328
+ return false;
329
+ }
330
+
331
+ // Enum must have string, number or null values
332
+ if (!schemaObject.enum.every((v) => ["string", "number", null].includes(typeof v))) {
333
+ return false;
334
+ }
335
+
336
+ // If conditionalEnums is enabled, only convert if x-enum-* metadata is present
337
+ if (options.ctx.conditionalEnums) {
338
+ const hasEnumMetadata =
339
+ Array.isArray(schemaObject["x-enum-varnames"]) ||
340
+ Array.isArray(schemaObject["x-enumNames"]) ||
341
+ Array.isArray(schemaObject["x-enum-descriptions"]) ||
342
+ Array.isArray(schemaObject["x-enumDescriptions"]);
343
+ if (!hasEnumMetadata) {
344
+ return false;
345
+ }
346
+ }
347
+
348
+ return true;
349
+ }
350
+
351
+ /**
352
+ * Handle SchemaObject minus composition (anyOf/allOf/oneOf)
353
+ */
354
+ function transformSchemaObjectCore(schemaObject: SchemaObject, options: TransformNodeOptions): ts.TypeNode | undefined {
355
+ if ("type" in schemaObject && schemaObject.type) {
356
+ if (typeof options.ctx.transform === "function") {
357
+ const result = options.ctx.transform(schemaObject, options);
358
+ if (result && typeof result === "object") {
359
+ if ("schema" in result) {
360
+ if (result.questionToken) {
361
+ return ts.factory.createUnionTypeNode([result.schema, UNDEFINED]);
362
+ } else {
363
+ return result.schema;
364
+ }
365
+ } else {
366
+ return result;
367
+ }
368
+ }
369
+ }
370
+
371
+ // primitives
372
+ // type: null
373
+ if (schemaObject.type === "null") {
374
+ return NULL;
375
+ }
376
+ // type: string
377
+ if (schemaObject.type === "string") {
378
+ return STRING;
379
+ }
380
+ // type: number / type: integer
381
+ if (schemaObject.type === "number" || schemaObject.type === "integer") {
382
+ return NUMBER;
383
+ }
384
+ // type: boolean
385
+ if (schemaObject.type === "boolean") {
386
+ return BOOLEAN;
387
+ }
388
+
389
+ // type: array (with support for tuples)
390
+ if (schemaObject.type === "array") {
391
+ // default to `unknown[]`
392
+ let itemType: ts.TypeNode = UNKNOWN;
393
+ // tuple type
394
+ if (schemaObject.prefixItems || Array.isArray(schemaObject.items)) {
395
+ const prefixItems = schemaObject.prefixItems ?? (schemaObject.items as (SchemaObject | ReferenceObject)[]);
396
+ itemType = ts.factory.createTupleTypeNode(prefixItems.map((item) => transformSchemaObject(item, options)));
397
+ }
398
+ // standard array type
399
+ else if (schemaObject.items) {
400
+ if (hasKey(schemaObject.items, "type") && schemaObject.items.type === "array") {
401
+ itemType = ts.factory.createArrayTypeNode(transformSchemaObject(schemaObject.items, options));
402
+ } else {
403
+ itemType = transformSchemaObject(schemaObject.items, options);
404
+ }
405
+ }
406
+
407
+ const min: number =
408
+ typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0 ? schemaObject.minItems : 0;
409
+ const max: number | undefined =
410
+ typeof schemaObject.maxItems === "number" && schemaObject.maxItems >= 0 && min <= schemaObject.maxItems
411
+ ? schemaObject.maxItems
412
+ : undefined;
413
+ const estimateCodeSize = typeof max !== "number" ? min : (max * (max + 1) - min * (min - 1)) / 2;
414
+ if (
415
+ options.ctx.arrayLength &&
416
+ (min !== 0 || max !== undefined) &&
417
+ estimateCodeSize < 30 // "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice
418
+ ) {
419
+ if (min === max) {
420
+ const elements: ts.TypeNode[] = [];
421
+ for (let i = 0; i < min; i++) {
422
+ elements.push(itemType);
423
+ }
424
+ return tsUnion([ts.factory.createTupleTypeNode(elements)]);
425
+ } else if ((schemaObject.maxItems as number) > 0) {
426
+ // if maxItems is set, then return a union of all permutations of possible tuple types
427
+ const members: ts.TypeNode[] = [];
428
+ // populate 1 short of min …
429
+ for (let i = 0; i <= (max ?? 0) - min; i++) {
430
+ const elements: ts.TypeNode[] = [];
431
+ for (let j = min; j < i + min; j++) {
432
+ elements.push(itemType);
433
+ }
434
+ members.push(ts.factory.createTupleTypeNode(elements));
435
+ }
436
+ return tsUnion(members);
437
+ }
438
+ // if maxItems not set, then return a simple tuple type the length of `min`
439
+ else {
440
+ const elements: ts.TypeNode[] = [];
441
+ for (let i = 0; i < min; i++) {
442
+ elements.push(itemType);
443
+ }
444
+ elements.push(ts.factory.createRestTypeNode(ts.factory.createArrayTypeNode(itemType)));
445
+ return ts.factory.createTupleTypeNode(elements);
446
+ }
447
+ }
448
+
449
+ const finalType =
450
+ ts.isTupleTypeNode(itemType) || ts.isArrayTypeNode(itemType)
451
+ ? itemType
452
+ : ts.factory.createArrayTypeNode(itemType); // wrap itemType in array type, but only if not a tuple or array already
453
+
454
+ return options.ctx.immutable
455
+ ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, finalType)
456
+ : finalType;
457
+ }
458
+
459
+ // polymorphic, or 3.1 nullable
460
+ if (Array.isArray(schemaObject.type) && !Array.isArray(schemaObject)) {
461
+ // skip any primitive types that appear in oneOf as well
462
+ const uniqueTypes: ts.TypeNode[] = [];
463
+ if (Array.isArray(schemaObject.oneOf)) {
464
+ for (const t of schemaObject.type) {
465
+ if (
466
+ (t === "boolean" || t === "string" || t === "number" || t === "integer" || t === "null") &&
467
+ schemaObject.oneOf.find((o) => typeof o === "object" && "type" in o && o.type === t)
468
+ ) {
469
+ continue;
470
+ }
471
+ uniqueTypes.push(
472
+ t === "null" || t === null
473
+ ? NULL
474
+ : transformSchemaObject(
475
+ { ...schemaObject, type: t, oneOf: undefined } as SchemaObject, // don’t stack oneOf transforms
476
+ options,
477
+ ),
478
+ );
479
+ }
480
+ } else {
481
+ for (const t of schemaObject.type) {
482
+ if (t === "null" || t === null) {
483
+ uniqueTypes.push(NULL);
484
+ } else {
485
+ uniqueTypes.push(transformSchemaObject({ ...schemaObject, type: t } as SchemaObject, options));
486
+ }
487
+ }
488
+ }
489
+ return tsUnion(uniqueTypes);
490
+ }
491
+ }
492
+
493
+ // type: object
494
+ const coreObjectType: ts.TypeElement[] = [];
495
+
496
+ // discriminators: explicit mapping on schema object
497
+ for (const k of ["allOf", "anyOf"] as const) {
498
+ if (!schemaObject[k]) {
499
+ continue;
500
+ }
501
+ // for all magic inheritance, we will have already gathered it into
502
+ // ctx.discriminators. But stop objects from referencing their own
503
+ // discriminator meant for children (!schemaObject.discriminator)
504
+ // and don't add discriminator properties if we already added/patched
505
+ // them (options.ctx.discriminators.refsHandled.includes(options.path!).
506
+ const discriminator =
507
+ !schemaObject.discriminator &&
508
+ !options.ctx.discriminators.refsHandled.includes(options.path ?? "") &&
509
+ options.ctx.discriminators.objects[options.path ?? ""];
510
+ if (discriminator) {
511
+ coreObjectType.unshift(
512
+ createDiscriminatorProperty(discriminator, {
513
+ path: options.path ?? "",
514
+ readonly: options.ctx.immutable,
515
+ }),
516
+ );
517
+ break;
518
+ }
519
+ }
520
+
521
+ if (
522
+ ("properties" in schemaObject && schemaObject.properties && Object.keys(schemaObject.properties).length) ||
523
+ ("additionalProperties" in schemaObject && schemaObject.additionalProperties) ||
524
+ ("patternProperties" in schemaObject && schemaObject.patternProperties) ||
525
+ ("$defs" in schemaObject && schemaObject.$defs)
526
+ ) {
527
+ // properties
528
+ if (Object.keys(schemaObject.properties ?? {}).length) {
529
+ for (const [k, v] of getEntries(schemaObject.properties ?? {}, options.ctx)) {
530
+ if ((typeof v !== "object" && typeof v !== "boolean") || Array.isArray(v)) {
531
+ throw new Error(
532
+ `${options.path}: invalid property ${k}. Expected Schema Object or boolean, got ${
533
+ Array.isArray(v) ? "Array" : typeof v
534
+ }`,
535
+ );
536
+ }
537
+
538
+ const { $ref, readOnly, writeOnly, hasDefault } =
539
+ typeof v === "object"
540
+ ? {
541
+ $ref: "$ref" in v && v.$ref,
542
+ readOnly: "readOnly" in v && v.readOnly,
543
+ writeOnly: "writeOnly" in v && v.writeOnly,
544
+ hasDefault: "default" in v && v.default !== undefined,
545
+ }
546
+ : {};
547
+
548
+ // handle excludeDeprecated option
549
+ if (options.ctx.excludeDeprecated) {
550
+ const resolved = $ref ? options.ctx.resolve<SchemaObject>($ref) : v;
551
+ if ((resolved as SchemaObject)?.deprecated) {
552
+ continue;
553
+ }
554
+ }
555
+ let optional =
556
+ schemaObject.required?.includes(k) ||
557
+ (schemaObject.required === undefined && options.ctx.propertiesRequiredByDefault) ||
558
+ (hasDefault &&
559
+ options.ctx.defaultNonNullable &&
560
+ !options.path?.includes("parameters") &&
561
+ !options.path?.includes("requestBody") &&
562
+ !options.path?.includes("requestBodies")) // can’t be required, even with defaults
563
+ ? undefined
564
+ : QUESTION_TOKEN;
565
+ let type = $ref
566
+ ? oapiRef($ref)
567
+ : transformSchemaObject(v, {
568
+ ...options,
569
+ path: createRef([options.path, k]),
570
+ });
571
+
572
+ if (typeof options.ctx.transform === "function") {
573
+ const result = options.ctx.transform(v as SchemaObject, options);
574
+ if (result && typeof result === "object") {
575
+ if ("schema" in result) {
576
+ type = result.schema;
577
+ optional = result.questionToken ? QUESTION_TOKEN : optional;
578
+ } else {
579
+ type = result;
580
+ }
581
+ }
582
+ }
583
+
584
+ type = wrapWithReadWriteMarker(type, !!readOnly, !!writeOnly, options.ctx);
585
+
586
+ let property = ts.factory.createPropertySignature(
587
+ /* modifiers */ tsModifiers({
588
+ readonly: options.ctx.immutable || (!options.ctx.readWriteMarkers && readOnly),
589
+ }),
590
+ /* name */ tsPropertyIndex(k),
591
+ /* questionToken */ optional,
592
+ /* type */ type,
593
+ );
594
+
595
+ // Apply transformProperty hook if available
596
+ if (typeof options.ctx.transformProperty === "function") {
597
+ const result = options.ctx.transformProperty(property, v as SchemaObject, {
598
+ ...options,
599
+ path: createRef([options.path, k]),
600
+ });
601
+ if (result) {
602
+ property = result;
603
+ }
604
+ }
605
+
606
+ addJSDocComment(v, property);
607
+ coreObjectType.push(property);
608
+ }
609
+ }
610
+
611
+ // $defs
612
+ if (schemaObject.$defs && typeof schemaObject.$defs === "object" && Object.keys(schemaObject.$defs).length) {
613
+ const defKeys: ts.TypeElement[] = [];
614
+ for (const [k, v] of Object.entries(schemaObject.$defs)) {
615
+ const defReadOnly = "readOnly" in v && !!v.readOnly;
616
+ const defWriteOnly = "writeOnly" in v && !!v.writeOnly;
617
+ const defType = wrapWithReadWriteMarker(
618
+ transformSchemaObject(v, { ...options, path: createRef([options.path, "$defs", k]) }),
619
+ defReadOnly,
620
+ defWriteOnly,
621
+ options.ctx,
622
+ );
623
+
624
+ let property = ts.factory.createPropertySignature(
625
+ /* modifiers */ tsModifiers({
626
+ readonly: options.ctx.immutable || (!options.ctx.readWriteMarkers && defReadOnly),
627
+ }),
628
+ /* name */ tsPropertyIndex(k),
629
+ /* questionToken */ undefined,
630
+ /* type */ defType,
631
+ );
632
+
633
+ // Apply transformProperty hook if available
634
+ if (typeof options.ctx.transformProperty === "function") {
635
+ const result = options.ctx.transformProperty(property, v as SchemaObject, {
636
+ ...options,
637
+ path: createRef([options.path, "$defs", k]),
638
+ });
639
+ if (result) {
640
+ property = result;
641
+ }
642
+ }
643
+
644
+ addJSDocComment(v, property);
645
+ defKeys.push(property);
646
+ }
647
+ coreObjectType.push(
648
+ ts.factory.createPropertySignature(
649
+ /* modifiers */ undefined,
650
+ /* name */ tsPropertyIndex("$defs"),
651
+ /* questionToken */ undefined,
652
+ /* type */ ts.factory.createTypeLiteralNode(defKeys),
653
+ ),
654
+ );
655
+ }
656
+
657
+ // additionalProperties / patternProperties
658
+ const hasExplicitAdditionalProperties =
659
+ typeof schemaObject.additionalProperties === "object" && Object.keys(schemaObject.additionalProperties).length;
660
+ const hasImplicitAdditionalProperties =
661
+ schemaObject.additionalProperties === true ||
662
+ (typeof schemaObject.additionalProperties === "object" &&
663
+ Object.keys(schemaObject.additionalProperties).length === 0);
664
+ const hasExplicitPatternProperties =
665
+ typeof schemaObject.patternProperties === "object" && Object.keys(schemaObject.patternProperties).length;
666
+ const stringIndexTypes = [];
667
+ if (hasExplicitAdditionalProperties) {
668
+ stringIndexTypes.push(transformSchemaObject(schemaObject.additionalProperties as SchemaObject, options, true));
669
+ }
670
+ if (hasImplicitAdditionalProperties || (!schemaObject.additionalProperties && options.ctx.additionalProperties)) {
671
+ stringIndexTypes.push(UNKNOWN);
672
+ }
673
+ if (hasExplicitPatternProperties) {
674
+ for (const [_, v] of getEntries(schemaObject.patternProperties ?? {}, options.ctx)) {
675
+ stringIndexTypes.push(transformSchemaObject(v, options));
676
+ }
677
+ }
678
+
679
+ if (stringIndexTypes.length === 0) {
680
+ return coreObjectType.length ? ts.factory.createTypeLiteralNode(coreObjectType) : undefined;
681
+ }
682
+
683
+ const stringIndexType = tsUnion(stringIndexTypes);
684
+
685
+ return tsIntersection([
686
+ ...(coreObjectType.length ? [ts.factory.createTypeLiteralNode(coreObjectType)] : []),
687
+ ts.factory.createTypeLiteralNode([
688
+ ts.factory.createIndexSignature(
689
+ /* modifiers */ tsModifiers({
690
+ readonly: options.ctx.immutable,
691
+ }),
692
+ /* parameters */ [
693
+ ts.factory.createParameterDeclaration(
694
+ /* modifiers */ undefined,
695
+ /* dotDotDotToken */ undefined,
696
+ /* name */ ts.factory.createIdentifier("key"),
697
+ /* questionToken */ undefined,
698
+ /* type */ STRING,
699
+ ),
700
+ ],
701
+ /* type */ stringIndexType,
702
+ ),
703
+ ]),
704
+ ]);
705
+ }
706
+
707
+ return coreObjectType.length ? ts.factory.createTypeLiteralNode(coreObjectType) : undefined;
708
+ }
709
+
710
+ /**
711
+ * Check if an object has a key
712
+ * @param possibleObject - The object to check
713
+ * @param key - The key to check for
714
+ * @returns True if the object has the key, false otherwise
715
+ */
716
+ function hasKey<K extends string>(possibleObject: unknown, key: K): possibleObject is { [key in K]: unknown } {
717
+ return typeof possibleObject === "object" && possibleObject !== null && key in possibleObject;
718
+ }
719
+
720
+ /** Wrap type with $Read or $Write marker when readWriteMarkers flag is enabled */
721
+ function wrapWithReadWriteMarker(
722
+ type: ts.TypeNode,
723
+ readOnly: boolean,
724
+ writeOnly: boolean,
725
+ ctx: { readWriteMarkers: boolean },
726
+ ): ts.TypeNode {
727
+ if (!ctx.readWriteMarkers || (readOnly && writeOnly)) {
728
+ return type;
729
+ }
730
+ if (readOnly) {
731
+ return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("$Read"), [type]);
732
+ }
733
+ if (writeOnly) {
734
+ return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("$Write"), [type]);
735
+ }
736
+ return type;
737
+ }