@sebspark/openapi-typegen 0.1.1 → 1.0.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/dist/index.js CHANGED
@@ -30,546 +30,812 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
- generate: () => generate
33
+ generate: () => generate2,
34
+ generateTypescript: () => generateTypescript
34
35
  });
35
36
  module.exports = __toCommonJS(src_exports);
36
-
37
- // src/generator.ts
38
- var fastGlob = __toESM(require("fast-glob"));
39
- var import_promises = require("fs/promises");
40
37
  var import_path = require("path");
41
- var import_yaml = require("yaml");
38
+ var import_change_case2 = require("change-case");
39
+ var import_promises = require("fs/promises");
40
+ var YAML = __toESM(require("yaml"));
42
41
 
43
- // src/shared/format.ts
44
- var prettier = require("prettier");
45
- var header = `/**
46
- * This file was auto-generated.
47
- * Do not make direct changes to the file.
48
- */
42
+ // src/generator/common.ts
43
+ var import_change_case = require("change-case");
49
44
 
50
- import {
51
- APIServerDefinition,
52
- BaseClient,
53
- GenericRouteHandler,
54
- } from '@sebspark/openapi-core'
55
-
56
- /* tslint:disable */
57
- /* eslint-disable */`;
58
- var formatFile = async (rows) => {
59
- const withHeader = [header, ...rows];
60
- const code = withHeader.join("\n\n");
61
- const formatted = await prettier.format(code, {
62
- parser: "typescript",
63
- singleQuote: true,
64
- semi: false
65
- });
66
- return formatted;
45
+ // src/generator/document.ts
46
+ var document = ({ title, description }) => {
47
+ if (title || description) {
48
+ const tokens = [];
49
+ tokens.push("/**");
50
+ if (title)
51
+ tokens.push(` * ${title}`);
52
+ if (description)
53
+ tokens.push(` * ${description}`);
54
+ tokens.push(" */\n");
55
+ return tokens.join("\n");
56
+ }
57
+ return "";
67
58
  };
68
-
69
- // src/shared/imports/format.ts
70
- var formatImports = (imports) => {
71
- const rows = [];
72
- const importsMap = imports.reduce(
73
- (map, it) => {
74
- if (!map[it.file])
75
- map[it.file] = /* @__PURE__ */ new Set();
76
- map[it.file].add(it.type);
77
- return map;
78
- },
79
- {}
59
+ var documentClientPath = (path, responses) => documentPath(
60
+ path,
61
+ responses,
62
+ [param("url", "string")],
63
+ [param("opts", "RequestOptions", true)]
64
+ );
65
+ var documentServerPath = (path, responses) => documentPath(path, responses);
66
+ var documentPath = (path, responses, argsBefore = [], argsAfter = []) => {
67
+ const tokens = [];
68
+ tokens.push("/**");
69
+ if (path.title)
70
+ tokens.push(` * ${path.title}`);
71
+ if (path.description)
72
+ tokens.push(` * ${path.description}`);
73
+ tokens.push(" *");
74
+ tokens.push(...argsBefore);
75
+ if (path.args)
76
+ tokens.push(...documentArgs(path.args));
77
+ tokens.push(...argsAfter);
78
+ tokens.push(` * @returns {Promise<${responses}>}`);
79
+ tokens.push(" */");
80
+ return tokens.join("\n");
81
+ };
82
+ var documentArgs = (args) => {
83
+ const tokens = [];
84
+ tokens.push(
85
+ param(
86
+ "args",
87
+ "Object",
88
+ argsOptional(args),
89
+ "The arguments for the request."
90
+ )
80
91
  );
81
- Object.entries(importsMap).forEach(([file, types]) => {
82
- rows.push(`import { ${Array.from(types).join(", ")} } from './${file}'`);
83
- });
84
- return rows;
92
+ tokens.push(...requestArgs(args.path, "params", "Path parameters"));
93
+ tokens.push(...requestArgs(args.query, "query", "Query parameters"));
94
+ tokens.push(...requestArgs(args.header, "headers", "Headers"));
95
+ tokens.push(...requestArgs(args.body, "body", "Request body"));
96
+ return tokens;
97
+ };
98
+ var buildPath = (path, property) => rxProperVariable.test(property) ? `${path}.${property}` : `${path}["${property}"]`;
99
+ var requestArgs = (args, name, title) => {
100
+ if (!args)
101
+ return [];
102
+ const tokens = [];
103
+ const type = args.extends.map((e) => e.type).join(AND) || "Object";
104
+ tokens.push(
105
+ param(
106
+ buildPath("args", name),
107
+ type,
108
+ args.optional,
109
+ `${title} for the request.`
110
+ )
111
+ );
112
+ const properties = args.properties.flatMap(
113
+ (prop) => requestProperty(buildPath("args", name), prop)
114
+ );
115
+ tokens.push(...properties);
116
+ return tokens;
117
+ };
118
+ var requestProperty = (path, property) => {
119
+ const tokens = [];
120
+ const type = property.type.map((t) => t.type).join(OR);
121
+ tokens.push(
122
+ param(
123
+ buildPath(path, property.name),
124
+ type,
125
+ property.optional,
126
+ property.title,
127
+ property.description
128
+ )
129
+ );
130
+ return tokens;
131
+ };
132
+ var param = (name, type, optional = false, title = "", description = "") => {
133
+ const tokens = [];
134
+ tokens.push(
135
+ ` * @param {${type}} ${optional ? "[" : ""}${name}${optional ? "]" : ""}`
136
+ );
137
+ if (optional || title || description) {
138
+ tokens.push(" -");
139
+ if (optional)
140
+ tokens.push(" Optional.");
141
+ if (title)
142
+ tokens.push(` ${title}`);
143
+ if (description)
144
+ tokens.push(` ${description}`);
145
+ }
146
+ return tokens.join("");
85
147
  };
86
148
 
87
- // src/shared/schema/format.ts
88
- var formatDocs = (description) => {
89
- if (description) {
90
- return `
91
- /**
92
- * ${description}
93
- */
94
- `;
95
- } else {
96
- return "";
149
+ // src/generator/common.ts
150
+ var OR = " | ";
151
+ var AND = " & ";
152
+ var generateType = (parsed) => {
153
+ let type;
154
+ switch (parsed.type) {
155
+ case "enum": {
156
+ type = generateEnum(parsed);
157
+ break;
158
+ }
159
+ case "array": {
160
+ type = generateArray(parsed);
161
+ break;
162
+ }
163
+ case "object": {
164
+ type = generateObject(parsed);
165
+ break;
166
+ }
167
+ case "Date":
168
+ case "bigint":
169
+ case "boolean":
170
+ case "null":
171
+ case "number":
172
+ case "string":
173
+ case "symbol":
174
+ case "undefined":
175
+ type = generatePrimitive(parsed);
176
+ break;
177
+ default: {
178
+ type = generateCustom(parsed);
179
+ }
97
180
  }
181
+ return type.replace(/ & \{\s*\}/g, "");
98
182
  };
99
- var formatTypeName = (name) => {
100
- return name.replace(/[^a-zA-Z0-9_$]/g, "_");
183
+ var generateProperty = (property) => {
184
+ const types = property.type.map(generateType);
185
+ return `${document(property)}${propertyName(property.name)}${property.optional ? "?" : ""}: ${types.join(OR)}`;
101
186
  };
102
- var formatParsedType = (parsedType) => {
103
- return `${formatDocs(parsedType.description)}export type ${parsedType.name} = ${parsedType.type}`;
187
+ var preamble = (type) => type.name ? `${document(type)}export type ${typeName(type.name)} = ` : "";
188
+ var typeName = (name) => {
189
+ if (rxProperVariable.test(name))
190
+ return name;
191
+ return (0, import_change_case.pascalCase)(name);
104
192
  };
105
- var formatParsedTypes = (types) => {
106
- const rows = [];
107
- Object.values(types).forEach((values) => rows.push(formatParsedType(values)));
108
- return rows;
193
+ var propertyName = (name) => {
194
+ if (rxProperVariable.test(name))
195
+ return name;
196
+ return `'${name}'`;
109
197
  };
110
- var formatProperties = (properties) => {
111
- const allProps = properties.map(({ name, required, value, description }) => {
112
- return `${formatDocs(description)}'${name}'${required ? "" : "?"}: ${value}`;
113
- }).join(`; `);
114
- return `{${allProps}}`;
198
+ var rxProperVariable = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
199
+ var extensions = (type) => type.extends.map(generateType).concat("").join(AND);
200
+ var generatePrimitive = (parsed) => `${preamble(parsed)}${parsed.type}`;
201
+ var generateCustom = (parsed) => `${preamble(parsed)}${typeName(parsed.type)}`;
202
+ var generateObject = (parsed) => {
203
+ const lines = [];
204
+ lines.push(`${preamble(parsed)}${extensions(parsed)}{`);
205
+ lines.push(...parsed.properties.map(generateProperty));
206
+ lines.push("}");
207
+ return lines.join("\n");
115
208
  };
116
-
117
- // src/shared/schema/parser.ts
118
- var parseRefString = (refString, addImport) => {
119
- const [file, rest] = refString.split("#");
120
- const typeName = formatTypeName(rest.substring(rest.lastIndexOf("/") + 1));
121
- if (file && file.length > 0) {
122
- const [fileName] = file.split(".");
123
- addImport({ file: fileName, type: typeName });
124
- }
125
- return typeName;
126
- };
127
- var parseTypes = (schemas) => {
128
- const imports = [];
129
- const types = Object.entries(schemas).map(([name, schema]) => {
130
- const schemaObj = schema;
131
- return {
132
- name: formatTypeName(name),
133
- type: generateFromSchemaObject(schemaObj, (importData) => {
134
- imports.push(importData);
135
- }),
136
- description: schemaObj.description
137
- };
138
- });
139
- return [types, imports];
140
- };
141
- var guessType = (schema) => {
142
- if (schema.type) {
143
- return schema.type;
144
- } else if (schema.enum) {
145
- return "string";
146
- } else if (schema.properties) {
147
- return "object";
148
- } else {
149
- return void 0;
209
+ var generateArray = (parsed) => {
210
+ const lines = [];
211
+ lines.push(`${preamble(parsed)}${parsed.items.type}[]`);
212
+ return lines.join("\n");
213
+ };
214
+ var generateEnum = (parsed) => {
215
+ return `${preamble(parsed)}${parsed.values.map(serializeValue).join(OR)}`;
216
+ };
217
+ var generateHeader = (header) => {
218
+ return `${preamble(header)}{ ${propertyName(header.name)}${header.optional ? "?" : ""}: ${generateType(header.type)} }`;
219
+ };
220
+ var generateResponseBody = (type) => {
221
+ const customType = type.type;
222
+ if (customType)
223
+ return typeName(customType);
224
+ const body = type;
225
+ if (!body.data && !body.headers)
226
+ return "undefined";
227
+ const tokens = [];
228
+ tokens.push(preamble(body));
229
+ tokens.push("APIResponse<");
230
+ tokens.push(body.data ? generateType(body.data) : "undefined");
231
+ if (body.headers) {
232
+ tokens.push(", ");
233
+ tokens.push(body.headers ? generateHeaders(body.headers) : "undefined");
150
234
  }
235
+ tokens.push(">");
236
+ return tokens.join("");
151
237
  };
152
- var generateFromSchemaObject = (schema, addImport) => {
153
- if ("$ref" in schema) {
154
- const { $ref } = schema;
155
- return parseRefString($ref, addImport);
238
+ var generateHeaders = (headers) => {
239
+ const tokens = [];
240
+ for (const header of headers) {
241
+ tokens.push(
242
+ `${propertyName(header.name)}${header.optional ? "?" : ""}: ${generateType(header.type)}`
243
+ );
156
244
  }
157
- const type = guessType(schema);
158
- let schemaString = "";
159
- if (type) {
160
- if (type === "object") {
161
- schemaString = generateObject(schema, addImport);
162
- } else if (type === "array") {
163
- const arrayType = generateFromSchemaObject(schema.items, addImport);
164
- if (arrayType && arrayType.length) {
165
- schemaString = `(${arrayType})[]`;
166
- } else {
167
- schemaString = "[]";
168
- }
169
- } else {
170
- switch (type) {
171
- case "integer":
172
- schemaString = "number";
173
- break;
174
- case "string": {
175
- if (schema.format === "date-time" || schema.format === "date") {
176
- schemaString = "Date";
177
- } else if (schema.enum) {
178
- schemaString = schema.enum.map((it) => `'${it}'`).join(" | ");
179
- } else {
180
- schemaString = "string";
181
- }
182
- break;
183
- }
184
- default:
185
- schemaString = type;
245
+ return `{${tokens.join(", ")}}`;
246
+ };
247
+ var serializeValue = (value) => {
248
+ if (typeof value === "string")
249
+ return `'${value}'`;
250
+ return value;
251
+ };
252
+
253
+ // src/generator/args.ts
254
+ var generateClientArgs = (args) => generateArgs(args, false);
255
+ var generateServerArgs = (args) => args ? generateArgs(args, true) : "args: Req";
256
+ var parts = ["body", "header", "path", "query"];
257
+ var generateArgs = (args, extendsReq) => {
258
+ if (args) {
259
+ const tokens = [];
260
+ for (const part of parts) {
261
+ const arg = args[part];
262
+ if (arg) {
263
+ const partName = part === "path" ? "params" : part === "header" ? "headers" : part;
264
+ tokens.push(
265
+ `${partName}${arg.optional ? "?" : ""}: ${generateType(arg)}`
266
+ );
186
267
  }
187
268
  }
269
+ if (!tokens.length)
270
+ return "";
271
+ const optional = argsOptional(args);
272
+ return `args${optional ? "?" : ""}: ${extendsReq ? "Req & " : ""}{ ${tokens.join(", ")} }, `;
188
273
  }
189
- if (schema.allOf) {
190
- const allOfString = schema.allOf.map((it) => generateFromSchemaObject(it, addImport)).join(" & ");
191
- if (allOfString.length > 0) {
192
- schemaString = schemaString + " & " + allOfString;
274
+ return "";
275
+ };
276
+ var argsOptional = (args) => (
277
+ // biome-ignore lint/style/noNonNullAssertion: <explanation>
278
+ parts.reduce((o, p) => o && (!args[p] || args[p].optional), true)
279
+ );
280
+
281
+ // src/generator/client.ts
282
+ var generateClient = (name, paths) => {
283
+ var _a;
284
+ const groupedCalls = {};
285
+ for (const path of paths) {
286
+ if (!groupedCalls[path.method]) {
287
+ groupedCalls[path.method] = [];
193
288
  }
289
+ (_a = groupedCalls[path.method]) == null ? void 0 : _a.push(generateCall(path));
194
290
  }
195
- if (schema.anyOf) {
196
- const anyOfString = schema.anyOf.map((it) => generateFromSchemaObject(it, addImport)).map((it) => `Partial<${it}>`).join(" & ");
197
- if (anyOfString.length > 0) {
198
- schemaString = schemaString + " & " + anyOfString;
199
- }
291
+ const client = [];
292
+ const methods = Object.keys(groupedCalls).map(serializeValue).join(OR);
293
+ client.push(`export type ${name}Client = Pick<BaseClient, ${methods}> & {`);
294
+ Object.entries(groupedCalls).forEach(([method, calls]) => {
295
+ client.push(`${method}: {`);
296
+ client.push(...calls);
297
+ client.push("}");
298
+ });
299
+ client.push("}");
300
+ return client.join("\n");
301
+ };
302
+ var generateCall = (path) => {
303
+ const responses = generateResponses(path);
304
+ return `${documentClientPath(path, responses)}
305
+ (
306
+ url: '${path.url}', ${generateClientArgs(path.args)}opts?: RequestOptions,
307
+ ): Promise<${responses}>`;
308
+ };
309
+ var generateResponses = (path) => Object.entries(path.responses).filter(([code]) => parseInt(code, 10) < 400).map(([, type]) => generateResponseBody(type)).join(OR);
310
+
311
+ // src/generator/server.ts
312
+ var generateServer = (name, paths) => {
313
+ const tokens = [];
314
+ tokens.push(`export type ${name}Server = APIServerDefinition & {`);
315
+ for (const [url, methods] of Object.entries(groupPathsByUrl(paths))) {
316
+ tokens.push(generatePath(url, methods));
200
317
  }
201
- if (schema.oneOf) {
202
- const oneOfString = schema.oneOf.map((it) => generateFromSchemaObject(it, addImport)).join(" | ");
203
- if (oneOfString.length > 0) {
204
- schemaString = schemaString + " & (" + oneOfString + ")";
205
- }
318
+ tokens.push("}");
319
+ return tokens.join("\n");
320
+ };
321
+ var groupPathsByUrl = (paths) => paths.reduce(
322
+ (group, path) => {
323
+ if (!group[path.url])
324
+ group[path.url] = [];
325
+ group[path.url].push(path);
326
+ return group;
327
+ },
328
+ {}
329
+ );
330
+ var generatePath = (url, methods) => `'${url}': {
331
+ ${methods.map(generateMethod).join("\n")}
332
+ }`;
333
+ var generateMethod = (path) => {
334
+ const responses = generateResponses2(path.responses);
335
+ return `${path.method}: {
336
+ ${documentServerPath(path, responses)}
337
+ handler: (${generateServerArgs(path.args)}) => Promise<${responses}>
338
+ pre?: GenericRouteHandler | GenericRouteHandler[]
339
+ }`;
340
+ };
341
+ var generateResponses2 = (responses) => Object.entries(responses).filter(([code]) => parseInt(code, 10) < 400).map(([code, response]) => generateResponse(parseInt(code, 10), response)).join(OR);
342
+ var generateResponse = (code, response) => `[${code}, ${generateResponseBody(response)}]`;
343
+
344
+ // src/generator/generator.ts
345
+ var generate = (name, doc) => `
346
+ /**
347
+ * This file was auto-generated.
348
+ * Do not make direct changes to the file.
349
+ */
350
+
351
+ import type {
352
+ APIResponse,
353
+ APIServerDefinition,
354
+ BaseClient,
355
+ GenericRouteHandler,
356
+ RequestOptions,
357
+ } from '@sebspark/openapi-core'
358
+ import type { Request } from 'express'
359
+
360
+ type Req = Pick<Request, 'url' | 'baseUrl' | 'cookies' | 'hostname'>
361
+
362
+ /* tslint:disable */
363
+ /* eslint-disable */
364
+
365
+ ${generateComponents(doc.components)}
366
+
367
+ ${generateServer(name, doc.paths)}
368
+
369
+ ${generateClient(name, doc.paths)}
370
+
371
+ `;
372
+ var generateComponents = (components) => {
373
+ const tokens = [];
374
+ for (const schema of components.schemas) {
375
+ tokens.push(generateType(schema));
206
376
  }
207
- if (!schemaString || !schemaString.length) {
208
- schemaString = "string";
377
+ for (const header of components.headers) {
378
+ tokens.push(generateHeader(header));
209
379
  }
210
- return schemaString;
211
- };
212
- var generateObject = (schema, addImport) => {
213
- const requiredFields = schema.required ?? [];
214
- const properties = Object.entries(
215
- schema.properties ?? []
216
- ).map(([name, schema2]) => ({
217
- [name]: {
218
- name,
219
- value: generateFromSchemaObject(schema2, addImport),
220
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
221
- description: schema2["description"],
222
- required: requiredFields.includes(name)
223
- }
224
- })).reduce((acc, current) => {
225
- for (const key in current) {
226
- acc[key] = current[key];
227
- }
228
- return acc;
229
- }, {});
230
- return formatProperties(Object.values(properties));
380
+ for (const param2 of components.parameters) {
381
+ tokens.push(
382
+ generateType({
383
+ type: "object",
384
+ extends: [],
385
+ name: param2.name,
386
+ properties: [
387
+ {
388
+ name: param2.parameterName,
389
+ type: [param2.type],
390
+ optional: param2.optional
391
+ }
392
+ ]
393
+ })
394
+ );
395
+ }
396
+ for (const req of components.requestBodies) {
397
+ tokens.push(generateType(req));
398
+ }
399
+ for (const res of components.responseBodies) {
400
+ tokens.push(generateResponseBody(res));
401
+ }
402
+ return tokens.join("\n\n");
231
403
  };
232
404
 
233
- // src/shared/schema/generator.ts
234
- var generateSchemas = (components) => {
235
- const [types, imports] = parseTypes(components.components.schemas);
236
- return formatFile([...formatImports(imports), ...formatParsedTypes(types)]);
405
+ // src/generator/formatter.ts
406
+ var import_prettier = require("prettier");
407
+ var options = {
408
+ parser: "typescript",
409
+ singleQuote: true,
410
+ semi: false,
411
+ trailingComma: "all"
237
412
  };
413
+ var format = async (code) => (0, import_prettier.format)(code, options);
238
414
 
239
- // src/format.ts
240
- var formatTitle = (title) => {
241
- return title.replace(/ /g, "").replace(/[^a-zA-Z0-9]/g, "_");
415
+ // src/parser/common.ts
416
+ var parseRef = (ref) => ref.substring(ref.lastIndexOf("/") + 1);
417
+ var parseEnumType = (name, schema) => ({ name, type: "enum", values: schema.enum || [] });
418
+ var findRef = (components, ref) => {
419
+ const [, , path, name] = ref.split("/");
420
+ const schemaPath = components[path];
421
+ if (!schemaPath || !schemaPath[name])
422
+ throw new Error(`Cannot find ref ${ref}`);
423
+ return schemaPath[name];
424
+ };
425
+ var parseDocumentation = (source) => {
426
+ const documented = {};
427
+ if (source.title)
428
+ documented.title = source.title;
429
+ if (source.description)
430
+ documented.description = source.description;
431
+ return documented;
242
432
  };
243
433
 
244
- // src/openapi/format.ts
245
- var import_change_case = require("change-case");
246
- var formatRoutes = (title, routes) => {
247
- const formattedTitle = formatTitle(title);
248
- const routeDefinitions = generateRouteDefinitions(routes);
249
- const server = generateServerAPI(formattedTitle, routeDefinitions);
250
- const client = generateClientAPI(formattedTitle, routeDefinitions);
251
- return server.concat(client);
252
- };
253
- var generateServerAPI = (name, routes) => {
254
- const map = generateServerType(routes);
255
- const rows = [];
256
- rows.push(
257
- `export type ${name}Server = APIServerDefinition & {
258
- ${Object.entries(map).map(
259
- ([url, methods]) => ` '${url}': {
260
- ${Object.entries(methods).map(
261
- ([method, definition]) => ` '${method}': {
262
- handler: (${serializeArgs(definition.args)}) => Promise<[${definition.response.code}, ${definition.response.type}]>
263
- pre?: GenericRouteHandler | GenericRouteHandler[]
264
- }`
265
- ).join("\n")}
266
- },`
267
- ).join("\n")}
268
- }
269
- `
270
- );
271
- return rows;
272
- };
273
- var generateClientAPI = (name, routes) => {
274
- const code = [];
275
- const clientHandlers = [];
276
- const verbMap = generateClientType(routes);
277
- Object.entries(verbMap).forEach(([verb, definitions]) => {
278
- const type = `${name}Client${(0, import_change_case.pascalCase)(verb)}`;
279
- clientHandlers.push(`${verb}: ${type}`);
280
- code.push(`type ${type} = {`);
281
- definitions.forEach((def) => {
282
- const args = serializeArgs(def.args);
283
- const argsString = args ? `, ${args}` : "";
284
- code.push(
285
- `(url: '${def.url}'${argsString}): Promise<${def.response.type}>`
286
- );
287
- });
288
- code.push("}");
289
- });
290
- code.push(
291
- `export type ${name}Client = Pick<BaseClient, ${Object.keys(verbMap).map((v) => `'${v}'`).join(" | ")}> & {`
292
- );
293
- code.push(clientHandlers.join("\n"));
294
- code.push("}");
295
- return code;
296
- };
297
- var generateRouteDefinitions = (routes) => routes.map((route) => ({
298
- method: route.method.toLowerCase(),
299
- response: route.response,
300
- url: route.url,
301
- args: parseArgs(route)
302
- }));
303
- var generateClientType = (routes) => routes.reduce((router, routeDefinition) => {
304
- if (!router[routeDefinition.method])
305
- router[routeDefinition.method] = [];
306
- router[routeDefinition.method].push(routeDefinition);
307
- return router;
308
- }, {});
309
- var generateServerType = (routes) => routes.reduce((router, routeDefinition) => {
310
- const route = router[routeDefinition.url] || {};
311
- route[routeDefinition.method] = routeDefinition;
312
- router[routeDefinition.url] = route;
313
- return router;
314
- }, {});
315
- var parseArgs = (route) => {
316
- const args = {};
317
- if (route.requestParams !== "never")
318
- args.params = route.requestParams;
319
- if (route.requestQuery !== "never")
320
- args.query = route.requestQuery;
321
- if (route.requestHeaders !== "never")
322
- args.headers = route.requestHeaders;
323
- if (route.requestBody !== "never")
324
- args.body = route.requestBody;
325
- if (args.headers || args.params || args.query || args.body)
326
- return args;
434
+ // src/parser/schema.ts
435
+ var parseSchemas = (schemas = {}) => Object.entries(schemas || {}).map(
436
+ ([name, schema]) => parseSchema(name, schema)
437
+ );
438
+ var parseSchema = (name, schemaOrRef) => {
439
+ const ref = schemaOrRef.$ref;
440
+ if (ref) {
441
+ return { name, type: parseRef(ref) };
442
+ }
443
+ const schema = schemaOrRef;
444
+ switch (schema.type) {
445
+ case "array":
446
+ return parseArraySchema(name, schema);
447
+ case "boolean":
448
+ case "integer":
449
+ case "number":
450
+ case "string":
451
+ return schema.enum ? parseEnumType(name, schema) : parsePropertyType(schema)[0];
452
+ default:
453
+ return parseObjectSchema(name, schema);
454
+ }
327
455
  };
328
- var optional = (params) => {
329
- const rxRequiredParams = /(?<!\?\s*)\b['"]?\w+['"]?\s*:\s*\w+/gim;
330
- const anyRequired = rxRequiredParams.test(params) || !params.includes(":");
331
- return anyRequired ? "" : "?";
456
+ var parseObjectSchema = (name, schema) => {
457
+ const type = {
458
+ name,
459
+ type: "object",
460
+ properties: [],
461
+ extends: [],
462
+ ...parseDocumentation(schema)
463
+ };
464
+ if (schema.properties) {
465
+ type.properties = Object.entries(schema.properties).map(
466
+ ([name2, property]) => parseProperty(name2, property, schema.required || [])
467
+ );
468
+ }
469
+ if (schema.allOf) {
470
+ type.extends = schema.allOf.flatMap(parsePropertyType);
471
+ }
472
+ return type;
332
473
  };
333
- var serializeArgs = (args) => {
334
- if (!args)
335
- return "";
336
- const argsString = Object.entries(args).map(([key, value]) => {
337
- return `${key}${optional(value)}: ${value}`;
338
- }).join(",");
339
- return `args${optional(argsString)}: {${argsString}}`;
474
+ var parseArraySchema = (name, schema) => {
475
+ if (schema.type !== "array" || !schema.items)
476
+ throw new Error("Not an array");
477
+ return {
478
+ name,
479
+ type: "array",
480
+ items: parsePropertyType(schema.items)[0],
481
+ ...parseDocumentation(schema)
482
+ };
340
483
  };
341
-
342
- // src/openapi/parser.ts
343
- var parseReferencableObjects = (name, record) => {
344
- const objects = {};
345
- if (record) {
346
- const allParameters = Object.entries(record);
347
- const referenceParameters = allParameters.filter((it) => "$ref" in it[1]).map((it) => [it[0], it[1]]);
348
- const normalParameters = allParameters.filter((it) => !("$ref" in it[1])).map((it) => [it[0], it[1]]);
349
- normalParameters.forEach((it) => {
350
- objects[`#/components/${name}/${it[0]}`] = it[1];
351
- });
352
- referenceParameters.forEach((it) => {
353
- objects[`#/components/${name}/${it[0]}`] = objects[it[1].$ref];
484
+ var parseProperty = (name, schema, required) => {
485
+ const property = {
486
+ name,
487
+ optional: !required.includes(name),
488
+ type: parsePropertyType(schema),
489
+ ...parseDocumentation(schema)
490
+ };
491
+ return property;
492
+ };
493
+ var parsePropertyType = (property) => {
494
+ const ref = property.$ref;
495
+ if (ref) {
496
+ return [{ type: parseRef(ref) }];
497
+ }
498
+ const schemaObject = property;
499
+ if (schemaObject.type) {
500
+ return (Array.isArray(schemaObject.type) ? schemaObject.type : [schemaObject.type]).map((type) => {
501
+ switch (type) {
502
+ case "array": {
503
+ return parseArraySchema(void 0, schemaObject);
504
+ }
505
+ case "object": {
506
+ return parseObjectSchema(void 0, schemaObject);
507
+ }
508
+ case "integer": {
509
+ return { type: "number", ...parseDocumentation(schemaObject) };
510
+ }
511
+ default: {
512
+ return { type, ...parseDocumentation(schemaObject) };
513
+ }
514
+ }
354
515
  });
355
516
  }
356
- return objects;
517
+ return [];
357
518
  };
358
519
 
359
- // src/openapi/paths.ts
360
- var pathGenerator = (globalParameters) => {
361
- const expressifyPath = (path) => path.replace(/{/g, ":").replace(/}/g, "");
362
- const parseResponse = (response, addImports) => {
363
- var _a, _b;
364
- const schema = (_b = (_a = response.content) == null ? void 0 : _a["application/json"]) == null ? void 0 : _b.schema;
365
- if (!schema)
366
- return "";
367
- return generateFromSchemaObject(schema, addImports);
520
+ // src/parser/headers.ts
521
+ var parseHeaders = (schemas = {}) => Object.entries(schemas || {}).map(
522
+ ([name, schema]) => parseHeader(name, schema)
523
+ );
524
+ var parseHeader = (name, schema) => {
525
+ const header = {
526
+ name,
527
+ optional: !schema.required,
528
+ // biome-ignore lint/style/noNonNullAssertion: <explanation>
529
+ type: parseSchema(void 0, schema.schema),
530
+ ...parseDocumentation(schema)
368
531
  };
369
- const getType = (param, addImport) => {
370
- if (param.type)
371
- param.type;
372
- if (param.schema) {
373
- if ("type" in param.schema)
374
- return generateFromSchemaObject(param.schema, addImport);
375
- }
376
- return "any";
532
+ return header;
533
+ };
534
+
535
+ // src/parser/parameters.ts
536
+ var parseParameters = (schemas = {}) => Object.entries(schemas || {}).map(
537
+ ([name, schema]) => parseParameter(name, schema)
538
+ );
539
+ var parseParameter = (name, schema) => {
540
+ const param2 = {
541
+ name,
542
+ in: schema.in,
543
+ parameterName: schema.name,
544
+ optional: !schema.required,
545
+ // biome-ignore lint/style/noNonNullAssertion: <explanation>
546
+ type: parseSchema(void 0, schema.schema),
547
+ ...parseDocumentation(schema)
377
548
  };
378
- const propName = (name) => {
379
- if (name.indexOf("-") === -1)
380
- return name;
381
- return `'${name}'`;
549
+ return param2;
550
+ };
551
+
552
+ // src/parser/args.ts
553
+ var parseArgs = (path, components) => {
554
+ var _a;
555
+ if (!((_a = path.parameters) == null ? void 0 : _a.length) && !path.requestBody)
556
+ return void 0;
557
+ const args = {
558
+ ...parseParameters2(path.parameters, components),
559
+ ...parseRequestBody(path.requestBody, components)
382
560
  };
383
- const generateProps = (params, filter, addImport) => {
384
- const props = params.map((it) => {
385
- if ("$ref" in it) {
386
- return globalParameters[it.$ref];
387
- } else {
388
- return it;
561
+ return args;
562
+ };
563
+ var createArgs = (initializer = {}) => ({
564
+ type: "object",
565
+ extends: [],
566
+ properties: [],
567
+ optional: true,
568
+ ...initializer
569
+ });
570
+ var parseParameters2 = (parameters = [], components = {}) => {
571
+ const args = {};
572
+ for (const p of parameters) {
573
+ const ref = p.$ref;
574
+ if (ref) {
575
+ const part = ref.split("/")[2];
576
+ switch (part) {
577
+ case "parameters": {
578
+ const param2 = findRef(components, ref);
579
+ const arg = args[param2.in] || createArgs({ ...parseDocumentation(param2) });
580
+ arg.optional = arg.optional && !param2.required;
581
+ arg.extends.push({ type: parseRef(ref) });
582
+ args[param2.in] = arg;
583
+ break;
584
+ }
585
+ case "headers": {
586
+ const header = findRef(components, ref);
587
+ const arg = args.header || createArgs();
588
+ const name = parseRef(ref);
589
+ arg.properties.push({
590
+ name,
591
+ optional: !header.required,
592
+ // biome-ignore lint/style/noNonNullAssertion: <explanation>
593
+ type: [{ type: parseSchema(void 0, header.schema).type }],
594
+ ...parseDocumentation(header.schema || {})
595
+ });
596
+ args.header = arg;
597
+ break;
598
+ }
389
599
  }
390
- }).filter((p) => p.in === filter).map(
391
- (p) => `${propName(p.name)}${p.required ? "" : "?"}: ${getType(
392
- p,
393
- addImport
394
- )}`
395
- );
396
- if (props.length)
397
- return `{${props.join(", ")}}`;
398
- return "never";
399
- };
400
- const generateBody = (body, addImport) => {
401
- var _a, _b;
402
- if ((_a = body == null ? void 0 : body.content) == null ? void 0 : _a["application/json"]) {
403
- const content = (_b = body.content["application/json"]) == null ? void 0 : _b.schema;
404
- return generateFromSchemaObject(content, addImport);
600
+ } else {
601
+ const param2 = p;
602
+ const arg = args[param2.in] || createArgs({ ...parseDocumentation(param2) });
603
+ arg.properties.push({
604
+ name: param2.name,
605
+ optional: !param2.required,
606
+ type: [parseSchema(void 0, param2.schema)]
607
+ });
608
+ arg.optional = arg.optional && !param2.required;
609
+ args[param2.in] = arg;
405
610
  }
406
- return "never";
407
- };
408
- const generateResponses = (responses, addImport, errors = false) => {
409
- const responseTypes = Object.entries(responses).map(([strCode, response]) => {
410
- const code = parseInt(strCode, 10);
411
- if (code >= 400 !== errors)
412
- return;
413
- if ("$ref" in response) {
414
- return {
415
- code,
416
- type: parseRefString(response.$ref, addImport)
417
- };
611
+ }
612
+ return args;
613
+ };
614
+ var parseRequestBody = (requestBody, components = {}) => {
615
+ const args = {};
616
+ if (!requestBody)
617
+ return args;
618
+ const ref = requestBody.$ref;
619
+ if (ref) {
620
+ const refBody = findRef(components, ref);
621
+ args.body = createArgs({
622
+ optional: !refBody.required,
623
+ extends: [{ type: parseRef(ref) }]
624
+ });
625
+ } else {
626
+ const body = requestBody;
627
+ const bodyArgs = args.body || createArgs({ optional: !body.required, ...parseDocumentation(body) });
628
+ if (body.content["application/json"]) {
629
+ const schema = body.content["application/json"].schema;
630
+ if (schema) {
631
+ const parsed = parseSchema(void 0, schema);
632
+ if (parsed.type === "object") {
633
+ args.body = {
634
+ ...parsed,
635
+ optional: !body.required
636
+ };
637
+ } else if (parsed.type) {
638
+ args.body = createArgs({
639
+ optional: !body.required,
640
+ extends: [parsed],
641
+ ...parseDocumentation(body)
642
+ });
643
+ }
418
644
  }
419
- return {
420
- code,
421
- type: parseResponse(response, addImport) || "void"
422
- };
423
- }).filter((r) => r);
424
- return responseTypes;
425
- };
426
- const generateRoutes = (path, item, addImport) => {
427
- const verbs = ["get", "post", "put", "patch", "delete"];
428
- const routes = verbs.map((verb) => {
429
- const operation = item[verb];
430
- if (!operation)
431
- return void 0;
432
- const route = {
433
- url: expressifyPath(path),
434
- method: verb,
435
- requestBody: generateBody(
436
- operation.requestBody,
437
- addImport
438
- ),
439
- requestParams: generateProps(
440
- operation.parameters || [],
441
- "path",
442
- addImport
443
- ),
444
- requestQuery: generateProps(
445
- operation.parameters || [],
446
- "query",
447
- addImport
448
- ),
449
- requestHeaders: generateProps(
450
- operation.parameters || [],
451
- "header",
452
- addImport
453
- ),
454
- response: generateResponses(operation.responses, addImport)[0],
455
- errorResponses: generateResponses(
456
- operation.responses,
457
- addImport,
458
- true
459
- )
460
- };
461
- return route;
462
- }).filter((r) => r);
463
- return routes;
464
- };
465
- const generatePaths = (paths) => {
466
- const imports = [];
467
- const routes = Object.entries(paths).flatMap(
468
- ([path, item]) => generateRoutes(path, item, (imp) => imports.push(imp))
469
- );
470
- return [routes, imports];
471
- };
472
- return {
473
- generate: generatePaths
474
- };
645
+ }
646
+ if (bodyArgs.extends.length || bodyArgs.properties.length) {
647
+ args.body = bodyArgs;
648
+ }
649
+ }
650
+ return args;
475
651
  };
476
652
 
477
- // src/openapi/generator.ts
478
- var generateData = (schema) => {
479
- var _a, _b, _c;
480
- const [types, importsFromTypes] = ((_a = schema.components) == null ? void 0 : _a.schemas) ? parseTypes(schema.components.schemas) : [[], []];
481
- const parameters = parseReferencableObjects(
482
- "parameters",
483
- ((_b = schema.components) == null ? void 0 : _b.parameters) || {}
484
- );
485
- const [paths, importsFromPaths] = pathGenerator(parameters).generate(
486
- schema.paths || {}
487
- );
653
+ // src/parser/responseBodies.ts
654
+ var parseResponseBodies = (responses = {}) => {
655
+ const bodies = [];
656
+ for (const [name, b] of Object.entries(responses)) {
657
+ const body = parseResponseBody(name, b);
658
+ bodies.push(body);
659
+ }
660
+ return bodies;
661
+ };
662
+ var parseResponseBody = (name, response) => {
663
+ var _a, _b;
664
+ const ref = response.$ref;
665
+ if (ref)
666
+ return { type: parseRef(ref) };
667
+ const responseObject = response;
668
+ const body = {};
669
+ if (name)
670
+ body.name = name;
671
+ if (responseObject.description)
672
+ body.description = responseObject.description;
673
+ if ((_b = (_a = responseObject.content) == null ? void 0 : _a["application/json"]) == null ? void 0 : _b.schema) {
674
+ const schema = responseObject.content["application/json"].schema;
675
+ body.data = parseSchema(void 0, schema);
676
+ }
677
+ if (responseObject.headers) {
678
+ body.headers = [];
679
+ for (const [headerName, header] of Object.entries(responseObject.headers)) {
680
+ const ref2 = header.$ref;
681
+ if (ref2)
682
+ body.headers.push({
683
+ name: headerName,
684
+ optional: false,
685
+ type: { type: parseRef(ref2) },
686
+ ...parseDocumentation(header)
687
+ });
688
+ else
689
+ body.headers.push(parseHeader(headerName, header));
690
+ }
691
+ }
692
+ return body;
693
+ };
694
+
695
+ // src/parser/paths.ts
696
+ var parsePaths = (doc) => Object.entries(doc.paths || {}).flatMap(
697
+ ([name, path]) => parsePath(name, path, doc.components)
698
+ );
699
+ var parsePath = (url, path, components) => {
700
+ const paths = [];
701
+ const methods = ["delete", "get", "patch", "post", "put"];
702
+ for (const method of methods) {
703
+ if (path[method]) {
704
+ paths.push(
705
+ parseMethod(url, method, path[method], components)
706
+ );
707
+ }
708
+ }
709
+ return paths;
710
+ };
711
+ var parseMethod = (url, method, operation, components) => {
488
712
  return {
489
- types,
490
- paths,
491
- imports: importsFromPaths.concat(importsFromTypes),
492
- title: ((_c = schema.info) == null ? void 0 : _c.title) ?? ""
713
+ method,
714
+ url: parseUrl(url),
715
+ responses: parseResponses(operation.responses),
716
+ args: parseArgs(operation, components),
717
+ ...parseDocumentation(operation)
493
718
  };
494
719
  };
495
- var generateFormattedString = async ({
496
- title,
497
- imports,
498
- paths,
499
- types
500
- }) => {
501
- const formattedImports = formatImports(imports);
502
- const formattedTypes = formatParsedTypes(types);
503
- const formattedRoutes = formatRoutes(title, paths);
504
- const formatted = await formatFile([
505
- ...formattedImports,
506
- ...formattedTypes,
507
- ...formattedRoutes
508
- ]);
509
- return formatted;
510
- };
511
- var generateOpenApi = async (schema) => {
512
- const data = generateData(schema);
513
- const formatted = await generateFormattedString(data);
514
- return formatted;
720
+ var parseUrl = (url) => url.replace(/{([^}]+)}/g, ":$1");
721
+ var parseResponses = (responses) => {
722
+ return Object.assign(
723
+ {},
724
+ ...Object.entries(responses).map(([code, response]) => {
725
+ return {
726
+ [parseInt(code, 10)]: parseResponseBody(void 0, response)
727
+ };
728
+ })
729
+ );
515
730
  };
516
731
 
517
- // src/generator.ts
518
- var getSchemas = async (input) => {
519
- const schemas = {
520
- openApi: {},
521
- // asyncApi: {},
522
- sharedTypes: {}
523
- };
524
- const files = fastGlob.sync(input, { globstar: true, dot: true });
525
- for (const file of files) {
526
- const { name } = (0, import_path.parse)(file);
527
- const content = await (0, import_promises.readFile)(file, "utf-8");
528
- const parsed = file.endsWith(".yaml") || file.endsWith(".yml") ? (0, import_yaml.parse)(content) : JSON.parse(content);
529
- if (parsed["asyncapi"]) {
530
- } else if (parsed["openapi"]) {
531
- schemas.openApi[name] = parsed;
532
- } else if (parsed["components"]) {
533
- schemas.sharedTypes[name] = parsed;
732
+ // src/parser/requestBodies.ts
733
+ var parseRequestBodies = (requestBodies = {}) => {
734
+ const definitions = [];
735
+ for (const [name, requestBody] of Object.entries(requestBodies)) {
736
+ if (requestBody.content["application/json"].schema) {
737
+ definitions.push(
738
+ parseSchema(name, requestBody.content["application/json"].schema)
739
+ );
534
740
  }
535
741
  }
536
- return schemas;
537
- };
538
- var generate = async ({
539
- input,
540
- output
541
- }) => {
542
- if (!input)
543
- throw new Error("You need to supply at least one schema");
544
- const schemas = await getSchemas(input);
545
- const generatedOpenApi = await Promise.all(
546
- Object.entries(schemas.openApi).map(async ([name, schema]) => ({
547
- name,
548
- schema: await generateOpenApi(schema)
549
- }))
550
- );
551
- const generatedSharedTypes = await Promise.all(
552
- Object.entries(schemas.sharedTypes).map(async ([name, schema]) => ({
553
- name,
554
- schema: await generateSchemas(schema)
555
- }))
556
- );
742
+ return definitions;
743
+ };
744
+
745
+ // src/parser/index.ts
746
+ var parseDocument = (schema) => ({
747
+ paths: parsePaths(schema),
748
+ components: parseComponents(schema.components)
749
+ });
750
+ var parseComponents = (components = {}) => ({
751
+ schemas: parseSchemas(components.schemas),
752
+ headers: parseHeaders(components.headers),
753
+ parameters: parseParameters(components.parameters),
754
+ requestBodies: parseRequestBodies(components.requestBodies),
755
+ responseBodies: parseResponseBodies(components.responses)
756
+ });
757
+
758
+ // src/index.ts
759
+ var generateTypescript = async (name, doc) => {
760
+ const parsed = parseDocument(doc);
761
+ const generated = generate(name, parsed);
762
+ const formatted = await format(generated);
763
+ return formatted;
764
+ };
765
+ var generate2 = async (input, output) => {
766
+ const docs = await readDocs(input);
767
+ const generated = await generateDocs(docs);
557
768
  if (!output)
558
- return generatedOpenApi.concat(generatedSharedTypes).map(
559
- ({ name, schema }) => `/**
560
- * ${name}
561
- */
562
- ${schema}
563
- `
564
- ).join("\n");
565
- await (0, import_promises.mkdir)(output, { recursive: true });
566
- for (const { name, schema } of generatedOpenApi.concat(generatedSharedTypes)) {
567
- const path = (0, import_path.join)(output, `${name}.ts`);
568
- await (0, import_promises.writeFile)(path, schema, "utf-8");
769
+ return generated.map((d) => d.ts).join("\n\n");
770
+ await saveDocs(output, generated);
771
+ };
772
+ var readDocs = async (input) => {
773
+ const path = (0, import_path.resolve)(input);
774
+ const stats = await (0, import_promises.stat)(path);
775
+ const filePaths = [];
776
+ if (stats.isFile())
777
+ filePaths.push(path);
778
+ if (stats.isDirectory()) {
779
+ const files = await (0, import_promises.readdir)(path);
780
+ filePaths.push(...files.map((f) => (0, import_path.resolve)(path, f)));
569
781
  }
570
- return;
782
+ const readFiles = [];
783
+ for (const p of filePaths) {
784
+ const { name, ext } = (0, import_path.parse)(p);
785
+ let doc;
786
+ switch (ext) {
787
+ case ".json": {
788
+ console.log(`Reading ${p}`);
789
+ const txt = await (0, import_promises.readFile)(p, "utf8");
790
+ doc = JSON.parse(txt);
791
+ break;
792
+ }
793
+ case ".yml":
794
+ case ".yaml": {
795
+ console.log(`Reading ${p}`);
796
+ const txt = await (0, import_promises.readFile)(p, "utf8");
797
+ doc = YAML.parse(txt);
798
+ break;
799
+ }
800
+ default:
801
+ continue;
802
+ }
803
+ readFiles.push({
804
+ doc,
805
+ name: formatName(name)
806
+ });
807
+ }
808
+ return readFiles;
809
+ };
810
+ var generateDocs = async (files) => {
811
+ const generated = [];
812
+ for (const doc of files) {
813
+ console.log(`Generating ${doc.name}`);
814
+ const ts = await generateTypescript(doc.name, doc.doc);
815
+ generated.push({
816
+ ...doc,
817
+ ts
818
+ });
819
+ }
820
+ return generated;
821
+ };
822
+ var saveDocs = async (output, docs) => {
823
+ const stats = await (0, import_promises.stat)(output);
824
+ const dir = stats.isDirectory() ? output : (0, import_path.parse)(output).dir;
825
+ await (0, import_promises.mkdir)(dir, { recursive: true });
826
+ for (const doc of docs) {
827
+ const path = (0, import_path.resolve)(dir, `${doc.name}.ts`);
828
+ console.log(`Writing ${path}`);
829
+ await (0, import_promises.writeFile)(path, doc.ts, "utf8");
830
+ }
831
+ };
832
+ var formatName = (name) => {
833
+ if (name[0].toUpperCase() === name[0])
834
+ return name;
835
+ return (0, import_change_case2.pascalCase)(name);
571
836
  };
572
837
  // Annotate the CommonJS export names for ESM import in node:
573
838
  0 && (module.exports = {
574
- generate
839
+ generate,
840
+ generateTypescript
575
841
  });