@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.
- package/CHANGELOG.md +1162 -0
- package/CONTRIBUTING.md +149 -0
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/bin/cli.js +297 -0
- package/changes.json +14 -0
- package/dist/index.cjs +152 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +517 -0
- package/dist/index.d.mts +515 -0
- package/dist/index.d.ts +517 -0
- package/dist/index.mjs +85 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/redoc.cjs +129 -0
- package/dist/lib/redoc.cjs.map +1 -0
- package/dist/lib/redoc.mjs +122 -0
- package/dist/lib/redoc.mjs.map +1 -0
- package/dist/lib/ts.cjs +470 -0
- package/dist/lib/ts.cjs.map +1 -0
- package/dist/lib/ts.mjs +431 -0
- package/dist/lib/ts.mjs.map +1 -0
- package/dist/lib/utils.cjs +295 -0
- package/dist/lib/utils.cjs.map +1 -0
- package/dist/lib/utils.mjs +278 -0
- package/dist/lib/utils.mjs.map +1 -0
- package/dist/transform/components-object.cjs +150 -0
- package/dist/transform/components-object.cjs.map +1 -0
- package/dist/transform/components-object.mjs +127 -0
- package/dist/transform/components-object.mjs.map +1 -0
- package/dist/transform/header-object.cjs +48 -0
- package/dist/transform/header-object.cjs.map +1 -0
- package/dist/transform/header-object.mjs +42 -0
- package/dist/transform/header-object.mjs.map +1 -0
- package/dist/transform/index.cjs +124 -0
- package/dist/transform/index.cjs.map +1 -0
- package/dist/transform/index.mjs +118 -0
- package/dist/transform/index.mjs.map +1 -0
- package/dist/transform/media-type-object.cjs +14 -0
- package/dist/transform/media-type-object.cjs.map +1 -0
- package/dist/transform/media-type-object.mjs +12 -0
- package/dist/transform/media-type-object.mjs.map +1 -0
- package/dist/transform/operation-object.cjs +105 -0
- package/dist/transform/operation-object.cjs.map +1 -0
- package/dist/transform/operation-object.mjs +96 -0
- package/dist/transform/operation-object.mjs.map +1 -0
- package/dist/transform/parameter-object.cjs +11 -0
- package/dist/transform/parameter-object.cjs.map +1 -0
- package/dist/transform/parameter-object.mjs +9 -0
- package/dist/transform/parameter-object.mjs.map +1 -0
- package/dist/transform/parameters-array.cjs +120 -0
- package/dist/transform/parameters-array.cjs.map +1 -0
- package/dist/transform/parameters-array.mjs +114 -0
- package/dist/transform/parameters-array.mjs.map +1 -0
- package/dist/transform/path-item-object.cjs +84 -0
- package/dist/transform/path-item-object.cjs.map +1 -0
- package/dist/transform/path-item-object.mjs +78 -0
- package/dist/transform/path-item-object.mjs.map +1 -0
- package/dist/transform/paths-enum.cjs +35 -0
- package/dist/transform/paths-enum.cjs.map +1 -0
- package/dist/transform/paths-enum.mjs +33 -0
- package/dist/transform/paths-enum.mjs.map +1 -0
- package/dist/transform/paths-object.cjs +134 -0
- package/dist/transform/paths-object.cjs.map +1 -0
- package/dist/transform/paths-object.mjs +128 -0
- package/dist/transform/paths-object.mjs.map +1 -0
- package/dist/transform/request-body-object.cjs +68 -0
- package/dist/transform/request-body-object.cjs.map +1 -0
- package/dist/transform/request-body-object.mjs +62 -0
- package/dist/transform/request-body-object.mjs.map +1 -0
- package/dist/transform/response-object.cjs +123 -0
- package/dist/transform/response-object.cjs.map +1 -0
- package/dist/transform/response-object.mjs +117 -0
- package/dist/transform/response-object.mjs.map +1 -0
- package/dist/transform/responses-object.cjs +36 -0
- package/dist/transform/responses-object.cjs.map +1 -0
- package/dist/transform/responses-object.mjs +30 -0
- package/dist/transform/responses-object.mjs.map +1 -0
- package/dist/transform/schema-object.cjs +502 -0
- package/dist/transform/schema-object.cjs.map +1 -0
- package/dist/transform/schema-object.mjs +493 -0
- package/dist/transform/schema-object.mjs.map +1 -0
- package/dist/transform/webhooks-object.cjs +37 -0
- package/dist/transform/webhooks-object.cjs.map +1 -0
- package/dist/transform/webhooks-object.mjs +31 -0
- package/dist/transform/webhooks-object.mjs.map +1 -0
- package/package.json +102 -0
- package/src/index.ts +108 -0
- package/src/lib/redoc.ts +164 -0
- package/src/lib/ts.ts +616 -0
- package/src/lib/utils.ts +394 -0
- package/src/transform/components-object.ts +161 -0
- package/src/transform/header-object.ts +45 -0
- package/src/transform/index.ts +116 -0
- package/src/transform/media-type-object.ts +18 -0
- package/src/transform/operation-object.ts +104 -0
- package/src/transform/parameter-object.ts +15 -0
- package/src/transform/parameters-array.ts +129 -0
- package/src/transform/path-item-object.ts +102 -0
- package/src/transform/paths-enum.ts +43 -0
- package/src/transform/paths-object.ts +134 -0
- package/src/transform/request-body-object.ts +59 -0
- package/src/transform/response-object.ts +112 -0
- package/src/transform/responses-object.ts +36 -0
- package/src/transform/schema-object.ts +737 -0
- package/src/transform/webhooks-object.ts +27 -0
- package/src/types.ts +735 -0
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { escapePointer, parseRef } from "@redocly/openapi-core/lib/ref-utils.js";
|
|
2
|
+
import c from "ansi-colors";
|
|
3
|
+
import supportsColor from "supports-color";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import type { DiscriminatorObject, OpenAPI3, OpenAPITSOptions, ReferenceObject, SchemaObject } from "../types.js";
|
|
6
|
+
import { tsLiteral, tsModifiers, tsPropertyIndex } from "./ts.js";
|
|
7
|
+
|
|
8
|
+
if (!supportsColor.stdout || supportsColor.stdout.hasBasic === false) {
|
|
9
|
+
c.enabled = false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEBUG_GROUPS: Record<string, c.StyleFunction | undefined> = {
|
|
13
|
+
redoc: c.cyanBright,
|
|
14
|
+
lint: c.yellowBright,
|
|
15
|
+
bundle: c.magentaBright,
|
|
16
|
+
ts: c.blueBright,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export { c };
|
|
20
|
+
|
|
21
|
+
/** Given a discriminator object, get the property name */
|
|
22
|
+
export function createDiscriminatorProperty(
|
|
23
|
+
discriminator: DiscriminatorObject,
|
|
24
|
+
{ path, readonly = false }: { path: string; readonly?: boolean },
|
|
25
|
+
): ts.TypeElement {
|
|
26
|
+
// get the inferred propertyName value from the last section of the path (as the spec suggests to do)
|
|
27
|
+
let value = parseRef(path).pointer.pop();
|
|
28
|
+
// if mapping, and there’s a match, use this rather than the inferred name
|
|
29
|
+
if (discriminator.mapping) {
|
|
30
|
+
// Mapping value can either be a fully-qualified ref (#/components/schemas/XYZ) or a schema name (XYZ)
|
|
31
|
+
const matchedValue = Object.entries(discriminator.mapping).find(
|
|
32
|
+
([, v]) => (!v.startsWith("#") && v === value) || (v.startsWith("#") && parseRef(v).pointer.pop() === value),
|
|
33
|
+
);
|
|
34
|
+
if (matchedValue) {
|
|
35
|
+
value = matchedValue[0]; // why was this designed backwards!?
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return ts.factory.createPropertySignature(
|
|
39
|
+
/* modifiers */ tsModifiers({
|
|
40
|
+
readonly,
|
|
41
|
+
}),
|
|
42
|
+
/* name */ tsPropertyIndex(discriminator.propertyName),
|
|
43
|
+
/* questionToken */ undefined,
|
|
44
|
+
/* type */ tsLiteral(value),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Create a $ref pointer (even from other $refs) */
|
|
49
|
+
export function createRef(parts: (number | string | undefined | null)[]): string {
|
|
50
|
+
let pointer = "#";
|
|
51
|
+
for (const part of parts) {
|
|
52
|
+
if (part === undefined || part === null || part === "") {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const maybeRef = parseRef(String(part)).pointer;
|
|
56
|
+
if (maybeRef.length) {
|
|
57
|
+
for (const refPart of maybeRef) {
|
|
58
|
+
pointer += `/${escapePointer(refPart)}`;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
pointer += `/${escapePointer(part)}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return pointer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Print debug message (cribbed from the `debug` package, but without all the bells & whistles */
|
|
68
|
+
export function debug(msg: string, group?: string, time?: number) {
|
|
69
|
+
if (
|
|
70
|
+
process.env.DEBUG &&
|
|
71
|
+
(!group ||
|
|
72
|
+
process.env.DEBUG === "*" ||
|
|
73
|
+
process.env.DEBUG === "openapi-ts:*" ||
|
|
74
|
+
process.env.DEBUG.toLocaleLowerCase() === `openapi-ts:${group.toLocaleLowerCase()}`)
|
|
75
|
+
) {
|
|
76
|
+
const groupColor = (group && DEBUG_GROUPS[group]) || c.whiteBright;
|
|
77
|
+
const groupName = groupColor(`openapi-ts:${group ?? "info"}`);
|
|
78
|
+
let timeFormatted = "";
|
|
79
|
+
if (typeof time === "number") {
|
|
80
|
+
timeFormatted = c.green(` ${formatTime(time)} `);
|
|
81
|
+
}
|
|
82
|
+
// biome-ignore lint/suspicious/noConsole: this is a CLI
|
|
83
|
+
console.debug(` ${c.bold(groupName)}${timeFormatted}${msg}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Print error message */
|
|
88
|
+
export function error(msg: string) {
|
|
89
|
+
// biome-ignore lint/suspicious/noConsole: this is a CLI
|
|
90
|
+
console.error(c.red(` ✘ ${msg}`));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Format a performance log in a friendly format */
|
|
94
|
+
export function formatTime(t: number) {
|
|
95
|
+
if (typeof t === "number") {
|
|
96
|
+
if (t < 1000) {
|
|
97
|
+
return `${Math.round(10 * t) / 10}ms`;
|
|
98
|
+
}
|
|
99
|
+
if (t < 60000) {
|
|
100
|
+
return `${Math.round(t / 100) / 10}s`;
|
|
101
|
+
}
|
|
102
|
+
return `${Math.round(t / 6000) / 10}m`;
|
|
103
|
+
}
|
|
104
|
+
return t;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Call Object.entries() and optionally sort */
|
|
108
|
+
export function getEntries<T>(
|
|
109
|
+
obj: ArrayLike<T> | Record<string, T>,
|
|
110
|
+
options?: {
|
|
111
|
+
alphabetize?: boolean;
|
|
112
|
+
excludeDeprecated?: boolean;
|
|
113
|
+
},
|
|
114
|
+
) {
|
|
115
|
+
let entries = Object.entries(obj);
|
|
116
|
+
if (options?.alphabetize) {
|
|
117
|
+
entries.sort(([a], [b]) => a.localeCompare(b, "en-us", { numeric: true }));
|
|
118
|
+
}
|
|
119
|
+
if (options?.excludeDeprecated) {
|
|
120
|
+
entries = entries.filter(([, v]) => !(v && typeof v === "object" && "deprecated" in v && v.deprecated));
|
|
121
|
+
}
|
|
122
|
+
return entries;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** resolve a $ref in a schema */
|
|
126
|
+
export function resolveRef<T>(
|
|
127
|
+
schema: any,
|
|
128
|
+
$ref: string,
|
|
129
|
+
{ silent = false, visited = [] }: { silent: boolean; visited?: string[] },
|
|
130
|
+
): T | undefined {
|
|
131
|
+
const { pointer } = parseRef($ref);
|
|
132
|
+
if (!pointer.length) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
let node = schema;
|
|
136
|
+
for (const key of pointer) {
|
|
137
|
+
if (node && typeof node === "object" && node[key]) {
|
|
138
|
+
node = node[key];
|
|
139
|
+
} else {
|
|
140
|
+
warn(`Could not resolve $ref "${$ref}"`, silent);
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// if this is also a $ref, keep tracing
|
|
146
|
+
if (node && typeof node === "object" && node.$ref) {
|
|
147
|
+
if (visited.includes(node.$ref)) {
|
|
148
|
+
warn(`Could not resolve circular $ref "${$ref}"`, silent);
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
return resolveRef(schema, node.$ref, {
|
|
152
|
+
silent,
|
|
153
|
+
visited: [...visited, node.$ref],
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return node;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function createDiscriminatorEnum(values: string[], prevSchema?: SchemaObject): SchemaObject {
|
|
161
|
+
return {
|
|
162
|
+
type: "string",
|
|
163
|
+
enum: values,
|
|
164
|
+
description: prevSchema?.description
|
|
165
|
+
? `${prevSchema.description} (enum property replaced by openapi-typescript)`
|
|
166
|
+
: "discriminator enum property added by openapi-typescript",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Adds or replaces the discriminator enum with the passed `values` in a schema defined by `ref` */
|
|
171
|
+
function patchDiscriminatorEnum(
|
|
172
|
+
schema: SchemaObject,
|
|
173
|
+
ref: string,
|
|
174
|
+
values: string[],
|
|
175
|
+
discriminator: DiscriminatorObject,
|
|
176
|
+
discriminatorRef: string,
|
|
177
|
+
options: OpenAPITSOptions,
|
|
178
|
+
): boolean {
|
|
179
|
+
const resolvedSchema = resolveRef<SchemaObject>(schema, ref, {
|
|
180
|
+
silent: options.silent ?? false,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (resolvedSchema?.allOf) {
|
|
184
|
+
// if the schema is an allOf, we can append a new schema object to the allOf array
|
|
185
|
+
resolvedSchema.allOf.push({
|
|
186
|
+
type: "object",
|
|
187
|
+
// discriminator enum properties always need to be required
|
|
188
|
+
required: [discriminator.propertyName],
|
|
189
|
+
properties: {
|
|
190
|
+
[discriminator.propertyName]: createDiscriminatorEnum(values),
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return true;
|
|
195
|
+
} else if (typeof resolvedSchema === "object" && "type" in resolvedSchema && resolvedSchema.type === "object") {
|
|
196
|
+
// if the schema is an object, we can apply the discriminator enums to its properties
|
|
197
|
+
if (!resolvedSchema.properties) {
|
|
198
|
+
resolvedSchema.properties = {};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// discriminator enum properties always need to be required
|
|
202
|
+
if (!resolvedSchema.required) {
|
|
203
|
+
resolvedSchema.required = [discriminator.propertyName];
|
|
204
|
+
} else if (!resolvedSchema.required.includes(discriminator.propertyName)) {
|
|
205
|
+
resolvedSchema.required.push(discriminator.propertyName);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// add/replace the discriminator enum property
|
|
209
|
+
resolvedSchema.properties[discriminator.propertyName] = createDiscriminatorEnum(
|
|
210
|
+
values,
|
|
211
|
+
resolvedSchema.properties[discriminator.propertyName] as SchemaObject,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
warn(
|
|
218
|
+
`Discriminator mapping has an invalid schema (neither an object schema nor an allOf array): ${ref} => ${values.join(
|
|
219
|
+
", ",
|
|
220
|
+
)} (Discriminator: ${discriminatorRef})`,
|
|
221
|
+
options.silent,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
type InternalDiscriminatorMapping = Record<string, { inferred?: string; defined?: string[] }>;
|
|
228
|
+
|
|
229
|
+
/** Return a key–value map of discriminator objects found in a schema */
|
|
230
|
+
export function scanDiscriminators(schema: OpenAPI3, options: OpenAPITSOptions) {
|
|
231
|
+
// all discriminator objects found in the schema
|
|
232
|
+
const objects: Record<string, DiscriminatorObject> = {};
|
|
233
|
+
|
|
234
|
+
// refs of all mapped schema objects we have successfully handled to infer the discriminator enum value
|
|
235
|
+
const refsHandled: string[] = [];
|
|
236
|
+
|
|
237
|
+
// perform 2 passes: first, collect all discriminator definitions and handle oneOf and mappings
|
|
238
|
+
walk(schema, (obj, path) => {
|
|
239
|
+
const discriminator = obj?.discriminator as DiscriminatorObject | undefined;
|
|
240
|
+
if (!discriminator?.propertyName) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// collect discriminator object for later usage
|
|
245
|
+
const ref = createRef(path);
|
|
246
|
+
|
|
247
|
+
objects[ref] = discriminator;
|
|
248
|
+
|
|
249
|
+
// if a mapping is available we will help Typescript to infer properties by adding the discriminator enum with its single mapped value to each schema
|
|
250
|
+
// we only handle the mapping in advance for discriminator + oneOf compositions right now
|
|
251
|
+
if (!obj?.oneOf || !Array.isArray(obj.oneOf)) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const oneOf: (SchemaObject | ReferenceObject)[] = obj.oneOf;
|
|
256
|
+
const mapping: InternalDiscriminatorMapping = {};
|
|
257
|
+
|
|
258
|
+
// the mapping can be inferred from the oneOf refs next to the discriminator object
|
|
259
|
+
for (const item of oneOf) {
|
|
260
|
+
if ("$ref" in item) {
|
|
261
|
+
// the name of the schema is the inferred discriminator enum value
|
|
262
|
+
const value = item.$ref.split("/").pop();
|
|
263
|
+
|
|
264
|
+
if (value) {
|
|
265
|
+
if (!mapping[item.$ref]) {
|
|
266
|
+
mapping[item.$ref] = { inferred: value };
|
|
267
|
+
} else {
|
|
268
|
+
mapping[item.$ref].inferred = value;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// the mapping can be defined in the discriminator object itself
|
|
275
|
+
if (discriminator.mapping) {
|
|
276
|
+
for (const mappedValue in discriminator.mapping) {
|
|
277
|
+
const mappedRef = discriminator.mapping[mappedValue];
|
|
278
|
+
if (!mappedRef) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!mapping[mappedRef]?.defined) {
|
|
283
|
+
// this overrides inferred values, but we don't need them anymore as soon as we have a defined value
|
|
284
|
+
mapping[mappedRef] = { defined: [] };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
mapping[mappedRef].defined?.push(mappedValue);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const [mappedRef, { inferred, defined }] of Object.entries(mapping)) {
|
|
292
|
+
if (refsHandled.includes(mappedRef)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!inferred && !defined) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// prefer defined values over automatically inferred ones
|
|
301
|
+
// the inferred enum values from the schema might not represent the actual enum values of the discriminator,
|
|
302
|
+
// so if we have defined values, use them instead
|
|
303
|
+
// biome-ignore lint/style/noNonNullAssertion: we just checked for this
|
|
304
|
+
const mappedValues = defined ?? [inferred!];
|
|
305
|
+
|
|
306
|
+
if (
|
|
307
|
+
patchDiscriminatorEnum(schema as unknown as SchemaObject, mappedRef, mappedValues, discriminator, ref, options)
|
|
308
|
+
) {
|
|
309
|
+
refsHandled.push(mappedRef);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// second, collect the schema objects that inherit from discriminators
|
|
315
|
+
// (sometimes this mapping is implicit, so it can’t be done until we know
|
|
316
|
+
// about every discriminator in the document)
|
|
317
|
+
walk(schema, (obj, path) => {
|
|
318
|
+
if (!obj || !Array.isArray(obj.allOf)) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const item of (obj as any).allOf) {
|
|
323
|
+
if ("$ref" in item) {
|
|
324
|
+
if (!objects[item.$ref]) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const ref = createRef(path);
|
|
329
|
+
const discriminator = objects[item.$ref];
|
|
330
|
+
const mappedValues: string[] = [];
|
|
331
|
+
|
|
332
|
+
if (discriminator.mapping) {
|
|
333
|
+
for (const mappedValue in discriminator.mapping) {
|
|
334
|
+
if (discriminator.mapping[mappedValue] === ref) {
|
|
335
|
+
mappedValues.push(mappedValue);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (mappedValues.length > 0) {
|
|
340
|
+
if (
|
|
341
|
+
patchDiscriminatorEnum(
|
|
342
|
+
schema as unknown as SchemaObject,
|
|
343
|
+
ref,
|
|
344
|
+
mappedValues,
|
|
345
|
+
discriminator,
|
|
346
|
+
item.$ref,
|
|
347
|
+
options,
|
|
348
|
+
)
|
|
349
|
+
) {
|
|
350
|
+
refsHandled.push(ref);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
objects[ref] = {
|
|
356
|
+
...objects[item.$ref],
|
|
357
|
+
};
|
|
358
|
+
} else if (item.discriminator?.propertyName) {
|
|
359
|
+
objects[createRef(path)] = { ...item.discriminator };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
return { objects, refsHandled };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Walk through any JSON-serializable (i.e. non-circular) object */
|
|
368
|
+
export function walk(
|
|
369
|
+
obj: unknown,
|
|
370
|
+
cb: (value: Record<string, unknown>, path: (string | number)[]) => void,
|
|
371
|
+
path: (string | number)[] = [],
|
|
372
|
+
): void {
|
|
373
|
+
if (!obj || typeof obj !== "object") {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (Array.isArray(obj)) {
|
|
377
|
+
for (let i = 0; i < obj.length; i++) {
|
|
378
|
+
walk(obj[i], cb, path.concat(i));
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
cb(obj as Record<string, unknown>, path);
|
|
383
|
+
for (const k of Object.keys(obj)) {
|
|
384
|
+
walk((obj as Record<string, unknown>)[k], cb, path.concat(k));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Print warning message */
|
|
389
|
+
export function warn(msg: string, silent = false) {
|
|
390
|
+
if (!silent) {
|
|
391
|
+
// biome-ignore lint/suspicious/noConsole: this is a CLI
|
|
392
|
+
console.warn(c.yellow(` ⚠ ${msg}`));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import * as changeCase from "change-case";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { addJSDocComment, NEVER, QUESTION_TOKEN, tsModifiers, tsPropertyIndex } from "../lib/ts.js";
|
|
5
|
+
import { createRef, debug, getEntries } from "../lib/utils.js";
|
|
6
|
+
import type { ComponentsObject, GlobalContext, SchemaObject, TransformNodeOptions } from "../types.js";
|
|
7
|
+
import transformHeaderObject from "./header-object.js";
|
|
8
|
+
import transformParameterObject from "./parameter-object.js";
|
|
9
|
+
import transformPathItemObject from "./path-item-object.js";
|
|
10
|
+
import transformRequestBodyObject from "./request-body-object.js";
|
|
11
|
+
import transformResponseObject from "./response-object.js";
|
|
12
|
+
import transformSchemaObject from "./schema-object.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Determines if a schema object represents an enum type to prevent duplicate exports
|
|
16
|
+
* when using --root-types and --enum flags together.
|
|
17
|
+
*
|
|
18
|
+
* When both flags are enabled:
|
|
19
|
+
* - --enum flag generates TypeScript enums at the bottom of the file
|
|
20
|
+
* - --root-types flag would normally also export these as root type aliases
|
|
21
|
+
* - This results in duplicate exports (both enum and type alias for the same schema)
|
|
22
|
+
*
|
|
23
|
+
* This function identifies enum schemas so they can be excluded from root type generation,
|
|
24
|
+
* allowing only the TypeScript enum to be generated.
|
|
25
|
+
*
|
|
26
|
+
* @param schema The schema object to check
|
|
27
|
+
* @returns true if the schema represents an enum type
|
|
28
|
+
*/
|
|
29
|
+
export function isEnumSchema(schema: unknown): boolean {
|
|
30
|
+
return (
|
|
31
|
+
typeof schema === "object" &&
|
|
32
|
+
schema !== null &&
|
|
33
|
+
!Array.isArray(schema) &&
|
|
34
|
+
"enum" in schema &&
|
|
35
|
+
Array.isArray((schema as any).enum) &&
|
|
36
|
+
(!("type" in schema) || (schema as any).type !== "object") &&
|
|
37
|
+
!("properties" in schema) &&
|
|
38
|
+
!("additionalProperties" in schema)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type ComponentTransforms = keyof Omit<ComponentsObject, "examples" | "securitySchemes" | "links" | "callbacks">;
|
|
43
|
+
|
|
44
|
+
const transformers: Record<ComponentTransforms, (node: any, options: TransformNodeOptions) => ts.TypeNode> = {
|
|
45
|
+
schemas: transformSchemaObject,
|
|
46
|
+
responses: transformResponseObject,
|
|
47
|
+
parameters: transformParameterObject,
|
|
48
|
+
requestBodies: transformRequestBodyObject,
|
|
49
|
+
headers: transformHeaderObject,
|
|
50
|
+
pathItems: transformPathItemObject,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Transform the ComponentsObject (4.8.7)
|
|
55
|
+
* @see https://spec.openapis.org/oas/latest.html#components-object
|
|
56
|
+
*/
|
|
57
|
+
export default function transformComponentsObject(componentsObject: ComponentsObject, ctx: GlobalContext): ts.Node[] {
|
|
58
|
+
const type: ts.TypeElement[] = [];
|
|
59
|
+
const rootTypeAliases: { [key: string]: ts.TypeAliasDeclaration } = {};
|
|
60
|
+
for (const key of Object.keys(transformers) as ComponentTransforms[]) {
|
|
61
|
+
const componentT = performance.now();
|
|
62
|
+
|
|
63
|
+
const items: ts.TypeElement[] = [];
|
|
64
|
+
if (componentsObject[key]) {
|
|
65
|
+
for (const [name, item] of getEntries<SchemaObject>(componentsObject[key], ctx)) {
|
|
66
|
+
let subType = transformers[key](item, {
|
|
67
|
+
path: createRef(["components", key, name]),
|
|
68
|
+
schema: item,
|
|
69
|
+
ctx,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
let hasQuestionToken = false;
|
|
73
|
+
if (ctx.transform) {
|
|
74
|
+
const result = ctx.transform(item, {
|
|
75
|
+
path: createRef(["components", key, name]),
|
|
76
|
+
schema: item,
|
|
77
|
+
ctx,
|
|
78
|
+
});
|
|
79
|
+
if (result) {
|
|
80
|
+
if ("schema" in result) {
|
|
81
|
+
subType = result.schema;
|
|
82
|
+
hasQuestionToken = result.questionToken;
|
|
83
|
+
} else {
|
|
84
|
+
subType = result;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const property = ts.factory.createPropertySignature(
|
|
90
|
+
/* modifiers */ tsModifiers({ readonly: ctx.immutable }),
|
|
91
|
+
/* name */ tsPropertyIndex(name),
|
|
92
|
+
/* questionToken */ hasQuestionToken ? QUESTION_TOKEN : undefined,
|
|
93
|
+
/* type */ subType,
|
|
94
|
+
);
|
|
95
|
+
addJSDocComment(item as unknown as any, property);
|
|
96
|
+
items.push(property);
|
|
97
|
+
|
|
98
|
+
if (ctx.rootTypes) {
|
|
99
|
+
// Skip enum schemas when generating root types to prevent duplication (only when --enum flag is enabled)
|
|
100
|
+
const shouldSkipEnumSchema = ctx.enum && key === "schemas" && isEnumSchema(item);
|
|
101
|
+
|
|
102
|
+
if (!shouldSkipEnumSchema) {
|
|
103
|
+
const componentKey = changeCase.pascalCase(singularizeComponentKey(key));
|
|
104
|
+
const componentName = ctx.rootTypesKeepCasing && key === "schemas" ? name : changeCase.pascalCase(name);
|
|
105
|
+
let aliasName = `${componentKey}${componentName}`;
|
|
106
|
+
|
|
107
|
+
// Add counter suffix (e.g. "_2") if conflict in name
|
|
108
|
+
let conflictCounter = 1;
|
|
109
|
+
|
|
110
|
+
while (rootTypeAliases[aliasName] !== undefined) {
|
|
111
|
+
conflictCounter++;
|
|
112
|
+
aliasName = `${componentKey}${componentName}_${conflictCounter}`;
|
|
113
|
+
}
|
|
114
|
+
const ref = ts.factory.createTypeReferenceNode(`components['${key}']['${name}']`);
|
|
115
|
+
if (ctx.rootTypesNoSchemaPrefix && key === "schemas") {
|
|
116
|
+
aliasName = aliasName.replace(componentKey, "");
|
|
117
|
+
}
|
|
118
|
+
const typeAlias = ts.factory.createTypeAliasDeclaration(
|
|
119
|
+
/* modifiers */ tsModifiers({ export: true }),
|
|
120
|
+
/* name */ aliasName,
|
|
121
|
+
/* typeParameters */ undefined,
|
|
122
|
+
/* type */ ref,
|
|
123
|
+
);
|
|
124
|
+
rootTypeAliases[aliasName] = typeAlias;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
type.push(
|
|
130
|
+
ts.factory.createPropertySignature(
|
|
131
|
+
/* modifiers */ undefined,
|
|
132
|
+
/* name */ tsPropertyIndex(key),
|
|
133
|
+
/* questionToken */ undefined,
|
|
134
|
+
/* type */ items.length ? ts.factory.createTypeLiteralNode(items) : NEVER,
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
debug(`Transformed components → ${key}`, "ts", performance.now() - componentT);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Extract root types
|
|
142
|
+
let rootTypes: ts.TypeAliasDeclaration[] = [];
|
|
143
|
+
if (ctx.rootTypes) {
|
|
144
|
+
rootTypes = Object.keys(rootTypeAliases).map((k) => rootTypeAliases[k]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return [ts.factory.createTypeLiteralNode(type), ...rootTypes];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function singularizeComponentKey(
|
|
151
|
+
key: `x-${string}` | "schemas" | "responses" | "parameters" | "requestBodies" | "headers" | "pathItems",
|
|
152
|
+
): string {
|
|
153
|
+
switch (key) {
|
|
154
|
+
// Handle special singular case
|
|
155
|
+
case "requestBodies":
|
|
156
|
+
return "requestBody";
|
|
157
|
+
// Default to removing the "s"
|
|
158
|
+
default:
|
|
159
|
+
return key.slice(0, -1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { escapePointer } from "@redocly/openapi-core/lib/ref-utils.js";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { addJSDocComment, tsModifiers, tsPropertyIndex, UNKNOWN } from "../lib/ts.js";
|
|
4
|
+
import { getEntries } from "../lib/utils.js";
|
|
5
|
+
import type { HeaderObject, TransformNodeOptions } from "../types.js";
|
|
6
|
+
import transformMediaTypeObject from "./media-type-object.js";
|
|
7
|
+
import transformSchemaObject from "./schema-object.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Transform HeaderObject nodes (4.8.21)
|
|
11
|
+
* @see https://spec.openapis.org/oas/v3.1.0#header-object
|
|
12
|
+
*/
|
|
13
|
+
export default function transformHeaderObject(headerObject: HeaderObject, options: TransformNodeOptions): ts.TypeNode {
|
|
14
|
+
if (headerObject.schema) {
|
|
15
|
+
return transformSchemaObject(headerObject.schema, options);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (headerObject.content) {
|
|
19
|
+
const type: ts.TypeElement[] = [];
|
|
20
|
+
for (const [contentType, mediaTypeObject] of getEntries(headerObject.content ?? {}, options.ctx)) {
|
|
21
|
+
const nextPath = `${options.path ?? "#"}/${escapePointer(contentType)}`;
|
|
22
|
+
const mediaType =
|
|
23
|
+
"$ref" in mediaTypeObject
|
|
24
|
+
? transformSchemaObject(mediaTypeObject, {
|
|
25
|
+
...options,
|
|
26
|
+
path: nextPath,
|
|
27
|
+
})
|
|
28
|
+
: transformMediaTypeObject(mediaTypeObject, {
|
|
29
|
+
...options,
|
|
30
|
+
path: nextPath,
|
|
31
|
+
});
|
|
32
|
+
const property = ts.factory.createPropertySignature(
|
|
33
|
+
/* modifiers */ tsModifiers({ readonly: options.ctx.immutable }),
|
|
34
|
+
/* name */ tsPropertyIndex(contentType),
|
|
35
|
+
/* questionToken */ undefined,
|
|
36
|
+
/* type */ mediaType,
|
|
37
|
+
);
|
|
38
|
+
addJSDocComment(mediaTypeObject, property);
|
|
39
|
+
type.push(property);
|
|
40
|
+
}
|
|
41
|
+
return ts.factory.createTypeLiteralNode(type);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return UNKNOWN;
|
|
45
|
+
}
|