@spec2ts/openapi-client 4.0.0 → 4.0.1
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/dist/bin/oapi2tsclient.mjs +1 -1
- package/dist/cli/index.d.mts +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/{command-Djig04Ub.mjs → command-C4XUi5ys.mjs} +4 -4
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{openapi-generator-DfC4jH9J.d.mts → openapi-generator-B6yW0z4d.d.mts} +1 -1
- package/dist/openapi-generator-IB_huQ-a.mjs +414 -0
- package/package.json +5 -4
- package/dist/openapi-generator-DwzcjKrv.mjs +0 -7088
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { i as usage, n as describe, r as handler, t as builder } from "../command-
|
|
2
|
+
import { i as usage, n as describe, r as handler, t as builder } from "../command-C4XUi5ys.mjs";
|
|
3
3
|
import yargs from "yargs";
|
|
4
4
|
import { hideBin } from "yargs/helpers";
|
|
5
5
|
//#region src/bin/oapi2tsclient.ts
|
package/dist/cli/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as OApiGeneratorOptions } from "../openapi-generator-
|
|
1
|
+
import { t as OApiGeneratorOptions } from "../openapi-generator-B6yW0z4d.mjs";
|
|
2
2
|
import { Argv } from "yargs";
|
|
3
3
|
//#region src/cli/command.d.ts
|
|
4
4
|
interface BuildClientFromOpenApiOptions extends OApiGeneratorOptions {
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as usage, n as describe, r as handler, t as builder } from "../command-
|
|
1
|
+
import { i as usage, n as describe, r as handler, t as builder } from "../command-C4XUi5ys.mjs";
|
|
2
2
|
export { builder, describe, handler, usage };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { n as generateClientFromFile } from "./openapi-generator-
|
|
2
|
-
import * as path from "path";
|
|
1
|
+
import { n as generateClientFromFile } from "./openapi-generator-IB_huQ-a.mjs";
|
|
3
2
|
import { cli, printer } from "@spec2ts/core";
|
|
4
|
-
import
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
5
|
//#region src/cli/command.ts
|
|
6
6
|
const usage = "$0 <input..>";
|
|
7
7
|
const describe = "Generate TypeScript HTTP client from OpenAPI specification";
|
|
@@ -137,7 +137,7 @@ async function generatePackage(output, options) {
|
|
|
137
137
|
pkg.devDependencies["@types/node"] = "*";
|
|
138
138
|
}
|
|
139
139
|
if (options.packagePrivate) pkg.private = options.packagePrivate;
|
|
140
|
-
await
|
|
140
|
+
await fs.writeFile(path.join(outputDir, "package.json"), JSON.stringify(pkg, null, 2), "utf8");
|
|
141
141
|
}
|
|
142
142
|
//#endregion
|
|
143
143
|
export { usage as i, describe as n, handler as r, builder as t };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as generateClientFromFile, n as SeparatedClientResult, r as generateClient, t as OApiGeneratorOptions } from "./openapi-generator-
|
|
1
|
+
import { i as generateClientFromFile, n as SeparatedClientResult, r as generateClient, t as OApiGeneratorOptions } from "./openapi-generator-B6yW0z4d.mjs";
|
|
2
2
|
export { OApiGeneratorOptions, SeparatedClientResult, generateClient, generateClientFromFile };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as generateClientFromFile, t as generateClient } from "./openapi-generator-
|
|
1
|
+
import { n as generateClientFromFile, t as generateClient } from "./openapi-generator-IB_huQ-a.mjs";
|
|
2
2
|
export { generateClient, generateClientFromFile };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ParserOptions } from "@spec2ts/jsonschema";
|
|
2
|
-
import
|
|
2
|
+
import ts from "typescript";
|
|
3
3
|
import { OpenAPIObject } from "openapi3-ts/oas31";
|
|
4
4
|
//#region src/lib/openapi-generator.d.ts
|
|
5
5
|
interface OApiGeneratorOptions extends ParserOptions {
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import $RefParser from "@apidevtools/json-schema-ref-parser";
|
|
2
|
+
import * as core from "@spec2ts/core";
|
|
3
|
+
import { createContext, getTypeFromSchema, isReference, resolveReference } from "@spec2ts/jsonschema";
|
|
4
|
+
import { getOperationName, getResponseName, parseReference } from "@spec2ts/openapi";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import ts from "typescript";
|
|
7
|
+
//#region src/lib/util.ts
|
|
8
|
+
function camelCase(str) {
|
|
9
|
+
const words = str.match(/[A-Z\xC0-\xD6\xD8-\xDE_$]?[a-z\xDF-\xF6\xF8-\xFF_$]+|[A-Z\xC0-\xD6\xD8-\xDE_$]+(?![a-z\xDF-\xF6\xF8-\xFF_$])|\d+/g);
|
|
10
|
+
if (!words) return "";
|
|
11
|
+
let result = "";
|
|
12
|
+
const len = words.length;
|
|
13
|
+
for (let i = 0; i < len; i++) {
|
|
14
|
+
let tmp = words[i].toLowerCase();
|
|
15
|
+
if (i !== 0) tmp = tmp[0].toUpperCase() + tmp.substr(1);
|
|
16
|
+
result += tmp;
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/lib/core-parser.ts
|
|
22
|
+
const methods = [
|
|
23
|
+
"GET",
|
|
24
|
+
"PUT",
|
|
25
|
+
"POST",
|
|
26
|
+
"DELETE",
|
|
27
|
+
"OPTIONS",
|
|
28
|
+
"HEAD",
|
|
29
|
+
"PATCH",
|
|
30
|
+
"TRACE"
|
|
31
|
+
];
|
|
32
|
+
const contentTypes = {
|
|
33
|
+
"*/*": "json",
|
|
34
|
+
"application/json": "json",
|
|
35
|
+
"application/x-www-form-urlencoded": "form",
|
|
36
|
+
"multipart/form-data": "multipart"
|
|
37
|
+
};
|
|
38
|
+
function parseOperation(path, item, method, operation, context) {
|
|
39
|
+
const result = {
|
|
40
|
+
name: getOperationVar(method, path, operation.operationId, context),
|
|
41
|
+
paramsVars: {},
|
|
42
|
+
args: [],
|
|
43
|
+
query: [],
|
|
44
|
+
header: [],
|
|
45
|
+
response: core.keywordType.void
|
|
46
|
+
};
|
|
47
|
+
parseArgs(result, item, operation, context);
|
|
48
|
+
parseResponses(result, path, method, operation, context);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function parseArgs(result, item, operation, context) {
|
|
52
|
+
parseParameters(result, item, operation, context);
|
|
53
|
+
if (operation.requestBody) parseRequestBody(result, operation.requestBody, context);
|
|
54
|
+
result.args.push(core.createParameter("options", {
|
|
55
|
+
type: ts.factory.createTypeReferenceNode("RequestOptions", void 0),
|
|
56
|
+
questionToken: true
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
function parseResponses(result, path, method, operation, context) {
|
|
60
|
+
const operationName = getOperationName(method, path, operation.operationId, context);
|
|
61
|
+
if (operation.responses) {
|
|
62
|
+
result.response = getTypeFromResponses(operationName, operation.responses, context);
|
|
63
|
+
result.responseJSON = isJSONResponse(operation.responses, context);
|
|
64
|
+
result.responseVoid = result.response === core.keywordType.void;
|
|
65
|
+
} else {
|
|
66
|
+
result.response = core.keywordType.unknown;
|
|
67
|
+
result.responseJSON = false;
|
|
68
|
+
result.responseVoid = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function parseParameters(result, item, operation, context) {
|
|
72
|
+
const parameters = fixDeepObjects([...resolveReferences(item.parameters, context), ...resolveReferences(operation.parameters, context)]);
|
|
73
|
+
result.query = parameters.filter((p) => p.in === "query");
|
|
74
|
+
result.header = parameters.filter((p) => p.in === "header");
|
|
75
|
+
const argNames = result.paramsVars = createParametersNames(parameters);
|
|
76
|
+
let objectBindingParams = parameters;
|
|
77
|
+
if (context.options.inlineRequired) {
|
|
78
|
+
const [required, optional] = splitParameters(parameters);
|
|
79
|
+
objectBindingParams = optional;
|
|
80
|
+
required.forEach((p) => {
|
|
81
|
+
result.args.push(core.createParameter(argNames[p.name], { type: getTypeFromSchema(p.schema, context) }));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (!objectBindingParams.length) return;
|
|
85
|
+
result.args.push(core.createParameter(core.createObjectBinding(objectBindingParams.map(({ name }) => ({ name: argNames[name] }))), {
|
|
86
|
+
initializer: objectBindingParams.some((p) => p.required) ? void 0 : ts.factory.createObjectLiteralExpression(),
|
|
87
|
+
type: ts.factory.createTypeLiteralNode(objectBindingParams.map((p) => core.createPropertySignature({
|
|
88
|
+
name: argNames[p.name],
|
|
89
|
+
questionToken: !p.required,
|
|
90
|
+
type: getTypeFromSchema(p.schema, context)
|
|
91
|
+
})))
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
function parseRequestBody(result, requestBody, context) {
|
|
95
|
+
const [schema, mode] = getSchemaFromContent(resolveReference(requestBody, context).content);
|
|
96
|
+
const type = getTypeFromSchema(schema, context);
|
|
97
|
+
const bodyVar = result.bodyVar = camelCase(type.name || getReferenceName(schema) || "body");
|
|
98
|
+
result.bodyMode = mode;
|
|
99
|
+
result.args.push(core.createParameter(bodyVar, { type }));
|
|
100
|
+
}
|
|
101
|
+
function createParametersNames(parameters) {
|
|
102
|
+
const argNames = {};
|
|
103
|
+
parameters.forEach(({ name }) => {
|
|
104
|
+
const stripped = camelCase(name.replace(/.+\./, ""));
|
|
105
|
+
argNames[name] = stripped in argNames ? camelCase(name) : stripped;
|
|
106
|
+
});
|
|
107
|
+
return argNames;
|
|
108
|
+
}
|
|
109
|
+
function splitParameters(parameters) {
|
|
110
|
+
const required = [];
|
|
111
|
+
const optional = [];
|
|
112
|
+
parameters.forEach((p) => {
|
|
113
|
+
if (p.required) required.push(p);
|
|
114
|
+
else optional.push(p);
|
|
115
|
+
});
|
|
116
|
+
return [required, optional];
|
|
117
|
+
}
|
|
118
|
+
function getTypeFromResponses(operationName, res, context) {
|
|
119
|
+
const codes = Object.keys(res);
|
|
120
|
+
const types = [];
|
|
121
|
+
codes.forEach((code) => {
|
|
122
|
+
const isOK = isOKResponse(code, codes.length);
|
|
123
|
+
const type = getTypeFromResponse(res[code], context);
|
|
124
|
+
if (!type) console.log(res[code]);
|
|
125
|
+
if (ts.isTypeReferenceNode(type) || core.isKeywordTypeNode(type)) {
|
|
126
|
+
if (isOK) types.push(type);
|
|
127
|
+
} else {
|
|
128
|
+
const name = getResponseName(operationName, code, context);
|
|
129
|
+
context.aliases.push(core.createTypeOrInterfaceDeclaration({
|
|
130
|
+
modifiers: [core.modifier.export],
|
|
131
|
+
name,
|
|
132
|
+
type
|
|
133
|
+
}));
|
|
134
|
+
if (isOK) types.push(ts.factory.createTypeReferenceNode(name, void 0));
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
if (types.length === 1) return types[0];
|
|
138
|
+
return ts.factory.createUnionTypeNode(types);
|
|
139
|
+
}
|
|
140
|
+
function isJSONResponse(responses, context) {
|
|
141
|
+
const codes = Object.keys(responses);
|
|
142
|
+
const resCode = codes.find((code) => isOKResponse(code, codes.length));
|
|
143
|
+
if (!resCode) return false;
|
|
144
|
+
const response = resolveReference(responses[resCode], context);
|
|
145
|
+
return !!response?.content?.["application/json"] || !!response?.content?.["*/*"];
|
|
146
|
+
}
|
|
147
|
+
function isOKResponse(code, codesCount) {
|
|
148
|
+
return codesCount === 1 || parseInt(code, 10) < 400;
|
|
149
|
+
}
|
|
150
|
+
function isMethod(method) {
|
|
151
|
+
return methods.includes(method);
|
|
152
|
+
}
|
|
153
|
+
function getTypeFromResponse(res, context) {
|
|
154
|
+
res = resolveReference(res, context);
|
|
155
|
+
if (!res?.content) return core.keywordType.void;
|
|
156
|
+
return getTypeFromSchema(getSchemaFromContent(res.content)[0], context);
|
|
157
|
+
}
|
|
158
|
+
function getSchemaFromContent(content) {
|
|
159
|
+
const res = Object.entries(contentTypes).find(([t]) => t in content);
|
|
160
|
+
let schema;
|
|
161
|
+
let mode;
|
|
162
|
+
if (res) {
|
|
163
|
+
const [contentType, contentMode] = res;
|
|
164
|
+
mode = contentMode;
|
|
165
|
+
schema = content?.[contentType]?.schema;
|
|
166
|
+
}
|
|
167
|
+
return [schema || { type: "string" }, mode];
|
|
168
|
+
}
|
|
169
|
+
function resolveReferences(array, context) {
|
|
170
|
+
return array?.map((ref) => resolveReference(ref, context)) ?? [];
|
|
171
|
+
}
|
|
172
|
+
function getReferenceName(obj) {
|
|
173
|
+
if (isReference(obj)) return camelCase(obj.$ref.split("/").slice(-1)[0]);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Despite its name, OpenApi's `deepObject` serialization does not support
|
|
177
|
+
* deeply nested objects. As a workaround we detect parameters that contain
|
|
178
|
+
* square brackets and merge them into a single object.
|
|
179
|
+
*/
|
|
180
|
+
function fixDeepObjects(params) {
|
|
181
|
+
const res = [];
|
|
182
|
+
const merged = {};
|
|
183
|
+
params.forEach((p) => {
|
|
184
|
+
const m = /^(.+?)\[(.*?)\]/.exec(p.name);
|
|
185
|
+
if (!m) {
|
|
186
|
+
res.push(p);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const [, name, prop] = m;
|
|
190
|
+
let obj = merged[name];
|
|
191
|
+
if (!obj) {
|
|
192
|
+
obj = merged[name] = {
|
|
193
|
+
name,
|
|
194
|
+
in: p.in,
|
|
195
|
+
style: "deepObject",
|
|
196
|
+
schema: {
|
|
197
|
+
type: "object",
|
|
198
|
+
properties: {}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
res.push(obj);
|
|
202
|
+
}
|
|
203
|
+
obj.schema.properties[prop] = p.schema;
|
|
204
|
+
});
|
|
205
|
+
return res;
|
|
206
|
+
}
|
|
207
|
+
function getOperationVar(verb, path, operationId, context) {
|
|
208
|
+
const id = getOperationVarId(operationId);
|
|
209
|
+
if (id) return id;
|
|
210
|
+
return getPathVar(`${verb} ${path}`, context);
|
|
211
|
+
}
|
|
212
|
+
function getPathVar(path, context) {
|
|
213
|
+
path = path.replace(/\{(.+?)\}/, "by $1").replace(/\{(.+?)\}/g, "and $1");
|
|
214
|
+
let name = camelCase(path);
|
|
215
|
+
const count = context.names[name] = (context.names[name] || 0) + 1;
|
|
216
|
+
if (count > 1) name += count;
|
|
217
|
+
return name;
|
|
218
|
+
}
|
|
219
|
+
function getOperationVarId(id) {
|
|
220
|
+
if (!id) return;
|
|
221
|
+
if (id.match(/[^\w\s]/)) return;
|
|
222
|
+
id = camelCase(id);
|
|
223
|
+
if (core.isValidIdentifier(id)) return id;
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/lib/server-parser.ts
|
|
227
|
+
function parseServers(servers) {
|
|
228
|
+
const props = servers.map((server, i) => [serverName(server, i), generateServerExpression(server)]);
|
|
229
|
+
return core.createObjectLiteral(props);
|
|
230
|
+
}
|
|
231
|
+
function defaultBaseUrl(servers) {
|
|
232
|
+
return ts.factory.createStringLiteral(defaultUrl(servers[0]));
|
|
233
|
+
}
|
|
234
|
+
function serverName(server, index) {
|
|
235
|
+
return server.description ? camelCase(server.description.replace(/\W+/, " ")) : `server${index + 1}`;
|
|
236
|
+
}
|
|
237
|
+
function generateServerExpression(server) {
|
|
238
|
+
return server.variables ? createServerFunction(server.url, server.variables) : ts.factory.createStringLiteral(server.url);
|
|
239
|
+
}
|
|
240
|
+
function createServerFunction(template, vars) {
|
|
241
|
+
const params = [core.createParameter(core.createObjectBinding(Object.entries(vars || {}).map(([name, value]) => {
|
|
242
|
+
return {
|
|
243
|
+
name,
|
|
244
|
+
initializer: createLiteral(value.default)
|
|
245
|
+
};
|
|
246
|
+
})), { type: ts.factory.createTypeLiteralNode(Object.entries(vars || {}).map(([name, value]) => {
|
|
247
|
+
return core.createPropertySignature({
|
|
248
|
+
name,
|
|
249
|
+
type: value.enum ? ts.factory.createUnionTypeNode(createUnion(value.enum)) : ts.factory.createUnionTypeNode([
|
|
250
|
+
core.keywordType.string,
|
|
251
|
+
core.keywordType.number,
|
|
252
|
+
core.keywordType.boolean
|
|
253
|
+
])
|
|
254
|
+
});
|
|
255
|
+
})) })];
|
|
256
|
+
return core.createArrowFunction(params, createTemplate(template));
|
|
257
|
+
}
|
|
258
|
+
function createUnion(strs) {
|
|
259
|
+
return strs.map((e) => ts.factory.createLiteralTypeNode(createLiteral(e)));
|
|
260
|
+
}
|
|
261
|
+
function createTemplate(url) {
|
|
262
|
+
const tokens = url.split(/{([\s\S]+?)}/g);
|
|
263
|
+
const spans = [];
|
|
264
|
+
const len = tokens.length;
|
|
265
|
+
for (let i = 1; i < len; i += 2) {
|
|
266
|
+
const template_factory = i === len - 2 ? ts.factory.createTemplateTail.bind(ts.factory) : ts.factory.createTemplateMiddle.bind(ts.factory);
|
|
267
|
+
spans.push(ts.factory.createTemplateSpan(ts.factory.createIdentifier(tokens[i]), template_factory(tokens[i + 1])));
|
|
268
|
+
}
|
|
269
|
+
return ts.factory.createTemplateExpression(ts.factory.createTemplateHead(tokens[0]), spans);
|
|
270
|
+
}
|
|
271
|
+
function createLiteral(v) {
|
|
272
|
+
switch (typeof v) {
|
|
273
|
+
case "string": return ts.factory.createStringLiteral(v);
|
|
274
|
+
case "boolean": return v ? ts.factory.createTrue() : ts.factory.createFalse();
|
|
275
|
+
case "number": return ts.factory.createNumericLiteral(String(v));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function defaultUrl(server) {
|
|
279
|
+
if (!server) return "/";
|
|
280
|
+
const { url, variables } = server;
|
|
281
|
+
if (!variables) return url;
|
|
282
|
+
return url.replace(/\{(.+?)\}/g, (m, name) => variables[name] ? String(variables[name].default) : m);
|
|
283
|
+
}
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/lib/core-generator.ts
|
|
286
|
+
function generateServers(file, { servers }, context) {
|
|
287
|
+
servers = servers || [];
|
|
288
|
+
if (context.options.baseUrl) servers = [{ url: context.options.baseUrl }];
|
|
289
|
+
const serversConst = core.findFirstVariableStatement(file.statements, "servers");
|
|
290
|
+
const defaultsConst = core.findFirstVariableStatement(file.statements, "defaults");
|
|
291
|
+
if (!serversConst || !defaultsConst) throw new Error("Invalid template: missing servers or defaults const");
|
|
292
|
+
file = core.replaceSourceFileStatement(file, serversConst, core.updateVariableStatementValue(serversConst, "servers", parseServers(servers)));
|
|
293
|
+
file = core.replaceSourceFileStatement(file, defaultsConst, core.updateVariableStatementPropertyValue(defaultsConst, "defaults", "baseUrl", defaultBaseUrl(servers)));
|
|
294
|
+
return file;
|
|
295
|
+
}
|
|
296
|
+
function generateDefaults(file, context) {
|
|
297
|
+
if (context.options.importFetch) {
|
|
298
|
+
const defaultsConst = core.findFirstVariableStatement(file.statements, "defaults");
|
|
299
|
+
if (!defaultsConst) throw new Error("Invalid template: missing defaults const");
|
|
300
|
+
file = core.prependSourceFileStatements(file, core.createDefaultImportDeclaration({
|
|
301
|
+
moduleSpecifier: context.options.importFetch,
|
|
302
|
+
name: "fetch",
|
|
303
|
+
bindings: ["RequestInit", "Headers"]
|
|
304
|
+
}), core.createNamespaceImportDeclaration({
|
|
305
|
+
moduleSpecifier: "form-data",
|
|
306
|
+
name: "FormData"
|
|
307
|
+
}));
|
|
308
|
+
file = core.replaceSourceFileStatement(file, defaultsConst, core.updateVariableStatementPropertyValue(defaultsConst, "defaults", "fetch", core.toExpression("fetch")));
|
|
309
|
+
}
|
|
310
|
+
return file;
|
|
311
|
+
}
|
|
312
|
+
function generateFunctions(file, spec, context) {
|
|
313
|
+
const paths = Object.fromEntries(Object.entries(spec.paths ?? {}).filter(([path]) => !context.options.prefix || path.startsWith(context.options.prefix)));
|
|
314
|
+
const functions = Object.entries(paths).map(([path, pathSpec]) => {
|
|
315
|
+
const item = resolveReference(pathSpec, context);
|
|
316
|
+
return Object.entries(item).filter(([verb]) => isMethod(verb.toUpperCase())).map(([verb, entry]) => generateFunction(path, item, verb.toUpperCase(), entry, context));
|
|
317
|
+
}).flat();
|
|
318
|
+
if (context.options.typesPath && context.typesFile) {
|
|
319
|
+
context.typesFile = core.updateSourceFileStatements(context.typesFile, context.aliases);
|
|
320
|
+
file = core.updateSourceFileStatements(file, [
|
|
321
|
+
core.createNamedImportDeclaration({
|
|
322
|
+
isTypeOnly: true,
|
|
323
|
+
moduleSpecifier: context.options.typesPath,
|
|
324
|
+
bindings: context.aliases.map((a) => a.name.text)
|
|
325
|
+
}),
|
|
326
|
+
...file.statements,
|
|
327
|
+
...functions
|
|
328
|
+
]);
|
|
329
|
+
} else file = core.appendSourceFileStatements(file, ...context.aliases, ...functions);
|
|
330
|
+
return file;
|
|
331
|
+
}
|
|
332
|
+
function generateFunction(path, item, method, operation, context) {
|
|
333
|
+
const { name, query, header, paramsVars, args, bodyMode, bodyVar, response, responseVoid, responseJSON } = parseOperation(path, item, method, operation, context);
|
|
334
|
+
const url = generateUrl(path, generateQs(query, paramsVars));
|
|
335
|
+
const init = [ts.factory.createSpreadAssignment(ts.factory.createIdentifier("options"))];
|
|
336
|
+
if (method !== "GET") init.push(ts.factory.createPropertyAssignment("method", ts.factory.createStringLiteral(method)));
|
|
337
|
+
if (bodyVar) init.push(core.createPropertyAssignment("body", ts.factory.createIdentifier(bodyVar)));
|
|
338
|
+
if (header.length) init.push(ts.factory.createPropertyAssignment("headers", ts.factory.createObjectLiteralExpression([ts.factory.createSpreadAssignment(ts.factory.createPropertyAccessChain(ts.factory.createIdentifier("options"), core.questionDotToken, ts.factory.createIdentifier("headers"))), ...header.map(({ name }) => core.createPropertyAssignment(name, ts.factory.createIdentifier(paramsVars[name])))], true)));
|
|
339
|
+
const fetchArgs = [url];
|
|
340
|
+
if (init.length) {
|
|
341
|
+
const initObj = ts.factory.createObjectLiteralExpression(init, true);
|
|
342
|
+
fetchArgs.push(bodyMode ? callFunction("http", bodyMode, [initObj]) : initObj);
|
|
343
|
+
}
|
|
344
|
+
return core.addComment(core.createFunctionDeclaration(name, {
|
|
345
|
+
modifiers: [core.modifier.export, core.modifier.async],
|
|
346
|
+
type: ts.factory.createTypeReferenceNode("Promise", [ts.factory.createTypeReferenceNode("ApiResponse", [response])])
|
|
347
|
+
}, args, core.block(ts.factory.createReturnStatement(ts.factory.createAwaitExpression(callFunction("http", responseJSON ? "fetchJson" : responseVoid ? "fetchVoid" : "fetch", fetchArgs))))), operation.summary || operation.description);
|
|
348
|
+
}
|
|
349
|
+
function generateQs(parameters, paramsVars) {
|
|
350
|
+
if (!parameters.length) return;
|
|
351
|
+
const paramsByFormatter = groupByFormatter(parameters);
|
|
352
|
+
return callFunction("QS", "query", Object.entries(paramsByFormatter).map(([format, params]) => {
|
|
353
|
+
return callFunction("QS", format, [core.createObjectLiteral(params.map((p) => [p.name, paramsVars[p.name]]))]);
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
function generateUrl(path, qs) {
|
|
357
|
+
const spans = [];
|
|
358
|
+
const head = path.replace(/(.*?)\{(.+?)\}(.*?)(?=\{|$)/g, (_, head, name, literal) => {
|
|
359
|
+
const expression = camelCase(name);
|
|
360
|
+
spans.push({
|
|
361
|
+
expression: ts.factory.createIdentifier(expression),
|
|
362
|
+
literal
|
|
363
|
+
});
|
|
364
|
+
return head;
|
|
365
|
+
});
|
|
366
|
+
if (qs) spans.push({
|
|
367
|
+
expression: qs,
|
|
368
|
+
literal: ""
|
|
369
|
+
});
|
|
370
|
+
return core.createTemplateString(head, spans);
|
|
371
|
+
}
|
|
372
|
+
function callFunction(ns, name, args) {
|
|
373
|
+
return core.createCall(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(ns), name), { args });
|
|
374
|
+
}
|
|
375
|
+
function groupByFormatter(parameters) {
|
|
376
|
+
const res = {};
|
|
377
|
+
parameters.forEach((param) => {
|
|
378
|
+
const formatter = getFormatter(param);
|
|
379
|
+
res[formatter] = res[formatter] || [];
|
|
380
|
+
res[formatter].push(param);
|
|
381
|
+
});
|
|
382
|
+
return res;
|
|
383
|
+
}
|
|
384
|
+
function getFormatter({ style, explode }) {
|
|
385
|
+
if (style === "spaceDelimited") return "space";
|
|
386
|
+
if (style === "pipeDelimited") return "pipe";
|
|
387
|
+
if (style === "deepObject") return "deep";
|
|
388
|
+
if (style === "form") return explode === false ? "form" : "explode";
|
|
389
|
+
return explode ? "explode" : "form";
|
|
390
|
+
}
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region src/lib/openapi-generator.ts
|
|
393
|
+
async function generateClientFromFile(file, options = {}) {
|
|
394
|
+
return generateClient(await $RefParser.parse(file), {
|
|
395
|
+
cwd: path.resolve(path.dirname(file)) + "/",
|
|
396
|
+
...options
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
async function generateClient(spec, options = {}) {
|
|
400
|
+
if (!options.parseReference) options.parseReference = parseReference;
|
|
401
|
+
const context = await createContext(spec, options);
|
|
402
|
+
let file = await core.createSourceFileFromFile(import.meta.dirname + "/templates/_client.tpl.ts");
|
|
403
|
+
if (context.options.typesPath) context.typesFile = ts.createSourceFile("types.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
|
404
|
+
file = generateServers(file, spec, context);
|
|
405
|
+
file = generateDefaults(file, context);
|
|
406
|
+
file = generateFunctions(file, spec, context);
|
|
407
|
+
if (context.options.typesPath) return {
|
|
408
|
+
client: file,
|
|
409
|
+
types: context.typesFile
|
|
410
|
+
};
|
|
411
|
+
return file;
|
|
412
|
+
}
|
|
413
|
+
//#endregion
|
|
414
|
+
export { generateClientFromFile as n, generateClient as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spec2ts/openapi-client",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "Utility to convert OpenAPI v3 specifications to Typescript HTTP client using TypeScript native compiler",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ast",
|
|
@@ -70,9 +70,10 @@
|
|
|
70
70
|
"prepublishOnly": "npm run test:prepublish && npm run build"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"@
|
|
74
|
-
"@spec2ts/
|
|
75
|
-
"@spec2ts/
|
|
73
|
+
"@apidevtools/json-schema-ref-parser": "^10.1.0",
|
|
74
|
+
"@spec2ts/core": "^4.0.1",
|
|
75
|
+
"@spec2ts/jsonschema": "^4.0.1",
|
|
76
|
+
"@spec2ts/openapi": "^4.0.1",
|
|
76
77
|
"openapi3-ts": "^4.6.0",
|
|
77
78
|
"typescript": "^6.0.0",
|
|
78
79
|
"yargs": "^18.0.0"
|