Skip to content

Commit 8191970

Browse files
authored
Automatically extract enums when using enum names as values (#1383)
* Automatically extract enums when using enum names as values * Add changeset Signed-off-by: Sora Morimoto <[email protected]> --------- Signed-off-by: Sora Morimoto <[email protected]>
1 parent 792e96c commit 8191970

File tree

6 files changed

+362
-2
lines changed

6 files changed

+362
-2
lines changed

.changeset/four-pears-watch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"swagger-typescript-api": patch
3+
---
4+
5+
Automatically extract enums when using enum names as values.

src/configuration.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,5 +441,8 @@ export class CodeGenConfig {
441441

442442
update = (update: Partial<GenerateApiConfiguration["config"]>) => {
443443
objectAssign(this, update);
444+
if (this.enumNamesAsValues) {
445+
this.extractEnums = true;
446+
}
444447
};
445448
}
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`inline enum > --enum-names-as-values with inline enum 1`] = `
4+
"/* eslint-disable */
5+
/* tslint:disable */
6+
// @ts-nocheck
7+
/*
8+
* ---------------------------------------------------------------
9+
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
10+
* ## ##
11+
* ## AUTHOR: acacode ##
12+
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
13+
* ---------------------------------------------------------------
14+
*/
15+
16+
export interface Pet {
17+
/** pet status in the store */
18+
status?: PetStatusEnum;
19+
}
20+
21+
/** pet status in the store */
22+
export enum PetStatusEnum {
23+
Available = "available",
24+
Pending = "pending",
25+
Sold = "sold",
26+
}
27+
28+
export type QueryParamsType = Record<string | number, any>;
29+
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
30+
31+
export interface FullRequestParams extends Omit<RequestInit, "body"> {
32+
/** set parameter to \`true\` for call \`securityWorker\` for this request */
33+
secure?: boolean;
34+
/** request path */
35+
path: string;
36+
/** content type of request body */
37+
type?: ContentType;
38+
/** query params */
39+
query?: QueryParamsType;
40+
/** format of response (i.e. response.json() -> format: "json") */
41+
format?: ResponseFormat;
42+
/** request body */
43+
body?: unknown;
44+
/** base url */
45+
baseUrl?: string;
46+
/** request cancellation token */
47+
cancelToken?: CancelToken;
48+
}
49+
50+
export type RequestParams = Omit<
51+
FullRequestParams,
52+
"body" | "method" | "query" | "path"
53+
>;
54+
55+
export interface ApiConfig<SecurityDataType = unknown> {
56+
baseUrl?: string;
57+
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
58+
securityWorker?: (
59+
securityData: SecurityDataType | null,
60+
) => Promise<RequestParams | void> | RequestParams | void;
61+
customFetch?: typeof fetch;
62+
}
63+
64+
export interface HttpResponse<D extends unknown, E extends unknown = unknown>
65+
extends Response {
66+
data: D;
67+
error: E;
68+
}
69+
70+
type CancelToken = Symbol | string | number;
71+
72+
export enum ContentType {
73+
Json = "application/json",
74+
JsonApi = "application/vnd.api+json",
75+
FormData = "multipart/form-data",
76+
UrlEncoded = "application/x-www-form-urlencoded",
77+
Text = "text/plain",
78+
}
79+
80+
export class HttpClient<SecurityDataType = unknown> {
81+
public baseUrl: string = "";
82+
private securityData: SecurityDataType | null = null;
83+
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
84+
private abortControllers = new Map<CancelToken, AbortController>();
85+
private customFetch = (...fetchParams: Parameters<typeof fetch>) =>
86+
fetch(...fetchParams);
87+
88+
private baseApiParams: RequestParams = {
89+
credentials: "same-origin",
90+
headers: {},
91+
redirect: "follow",
92+
referrerPolicy: "no-referrer",
93+
};
94+
95+
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
96+
Object.assign(this, apiConfig);
97+
}
98+
99+
public setSecurityData = (data: SecurityDataType | null) => {
100+
this.securityData = data;
101+
};
102+
103+
protected encodeQueryParam(key: string, value: any) {
104+
const encodedKey = encodeURIComponent(key);
105+
return \`\${encodedKey}=\${encodeURIComponent(typeof value === "number" ? value : \`\${value}\`)}\`;
106+
}
107+
108+
protected addQueryParam(query: QueryParamsType, key: string) {
109+
return this.encodeQueryParam(key, query[key]);
110+
}
111+
112+
protected addArrayQueryParam(query: QueryParamsType, key: string) {
113+
const value = query[key];
114+
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
115+
}
116+
117+
protected toQueryString(rawQuery?: QueryParamsType): string {
118+
const query = rawQuery || {};
119+
const keys = Object.keys(query).filter(
120+
(key) => "undefined" !== typeof query[key],
121+
);
122+
return keys
123+
.map((key) =>
124+
Array.isArray(query[key])
125+
? this.addArrayQueryParam(query, key)
126+
: this.addQueryParam(query, key),
127+
)
128+
.join("&");
129+
}
130+
131+
protected addQueryParams(rawQuery?: QueryParamsType): string {
132+
const queryString = this.toQueryString(rawQuery);
133+
return queryString ? \`?\${queryString}\` : "";
134+
}
135+
136+
private contentFormatters: Record<ContentType, (input: any) => any> = {
137+
[ContentType.Json]: (input: any) =>
138+
input !== null && (typeof input === "object" || typeof input === "string")
139+
? JSON.stringify(input)
140+
: input,
141+
[ContentType.JsonApi]: (input: any) =>
142+
input !== null && (typeof input === "object" || typeof input === "string")
143+
? JSON.stringify(input)
144+
: input,
145+
[ContentType.Text]: (input: any) =>
146+
input !== null && typeof input !== "string"
147+
? JSON.stringify(input)
148+
: input,
149+
[ContentType.FormData]: (input: any) => {
150+
if (input instanceof FormData) {
151+
return input;
152+
}
153+
154+
return Object.keys(input || {}).reduce((formData, key) => {
155+
const property = input[key];
156+
formData.append(
157+
key,
158+
property instanceof Blob
159+
? property
160+
: typeof property === "object" && property !== null
161+
? JSON.stringify(property)
162+
: \`\${property}\`,
163+
);
164+
return formData;
165+
}, new FormData());
166+
},
167+
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
168+
};
169+
170+
protected mergeRequestParams(
171+
params1: RequestParams,
172+
params2?: RequestParams,
173+
): RequestParams {
174+
return {
175+
...this.baseApiParams,
176+
...params1,
177+
...(params2 || {}),
178+
headers: {
179+
...(this.baseApiParams.headers || {}),
180+
...(params1.headers || {}),
181+
...((params2 && params2.headers) || {}),
182+
},
183+
};
184+
}
185+
186+
protected createAbortSignal = (
187+
cancelToken: CancelToken,
188+
): AbortSignal | undefined => {
189+
if (this.abortControllers.has(cancelToken)) {
190+
const abortController = this.abortControllers.get(cancelToken);
191+
if (abortController) {
192+
return abortController.signal;
193+
}
194+
return void 0;
195+
}
196+
197+
const abortController = new AbortController();
198+
this.abortControllers.set(cancelToken, abortController);
199+
return abortController.signal;
200+
};
201+
202+
public abortRequest = (cancelToken: CancelToken) => {
203+
const abortController = this.abortControllers.get(cancelToken);
204+
205+
if (abortController) {
206+
abortController.abort();
207+
this.abortControllers.delete(cancelToken);
208+
}
209+
};
210+
211+
public request = async <T = any, E = any>({
212+
body,
213+
secure,
214+
path,
215+
type,
216+
query,
217+
format,
218+
baseUrl,
219+
cancelToken,
220+
...params
221+
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
222+
const secureParams =
223+
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
224+
this.securityWorker &&
225+
(await this.securityWorker(this.securityData))) ||
226+
{};
227+
const requestParams = this.mergeRequestParams(params, secureParams);
228+
const queryString = query && this.toQueryString(query);
229+
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
230+
const responseFormat = format || requestParams.format;
231+
232+
return this.customFetch(
233+
\`\${baseUrl || this.baseUrl || ""}\${path}\${queryString ? \`?\${queryString}\` : ""}\`,
234+
{
235+
...requestParams,
236+
headers: {
237+
...(requestParams.headers || {}),
238+
...(type && type !== ContentType.FormData
239+
? { "Content-Type": type }
240+
: {}),
241+
},
242+
signal:
243+
(cancelToken
244+
? this.createAbortSignal(cancelToken)
245+
: requestParams.signal) || null,
246+
body:
247+
typeof body === "undefined" || body === null
248+
? null
249+
: payloadFormatter(body),
250+
},
251+
).then(async (response) => {
252+
const r = response.clone() as HttpResponse<T, E>;
253+
r.data = null as unknown as T;
254+
r.error = null as unknown as E;
255+
256+
const data = !responseFormat
257+
? r
258+
: await response[responseFormat]()
259+
.then((data) => {
260+
if (r.ok) {
261+
r.data = data;
262+
} else {
263+
r.error = data;
264+
}
265+
return r;
266+
})
267+
.catch((e) => {
268+
r.error = e;
269+
return r;
270+
});
271+
272+
if (cancelToken) {
273+
this.abortControllers.delete(cancelToken);
274+
}
275+
276+
if (!response.ok) throw data;
277+
return data;
278+
});
279+
};
280+
}
281+
282+
/**
283+
* @title Inline enum example
284+
* @version 1.0.0
285+
*/
286+
export class Api<
287+
SecurityDataType extends unknown,
288+
> extends HttpClient<SecurityDataType> {}
289+
"
290+
`;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"openapi": "3.0.0",
3+
"info": { "title": "Inline enum example", "version": "1.0.0" },
4+
"paths": {},
5+
"components": {
6+
"schemas": {
7+
"Pet": {
8+
"type": "object",
9+
"properties": {
10+
"status": {
11+
"type": "string",
12+
"description": "pet status in the store",
13+
"enum": ["available", "pending", "sold"]
14+
}
15+
}
16+
}
17+
}
18+
}
19+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as fs from "node:fs/promises";
2+
import * as os from "node:os";
3+
import * as path from "node:path";
4+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
5+
import { generateApi } from "../../../src/index.js";
6+
7+
describe("inline enum", async () => {
8+
let tmpdir = "";
9+
10+
beforeAll(async () => {
11+
tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api"));
12+
});
13+
14+
afterAll(async () => {
15+
await fs.rm(tmpdir, { recursive: true });
16+
});
17+
18+
test("--enum-names-as-values with inline enum", async () => {
19+
await generateApi({
20+
fileName: "schema",
21+
input: path.resolve(import.meta.dirname, "inline-schema.json"),
22+
output: tmpdir,
23+
silent: true,
24+
enumNamesAsValues: true,
25+
});
26+
27+
const content = await fs.readFile(path.join(tmpdir, "schema.ts"), {
28+
encoding: "utf8",
29+
});
30+
31+
expect(content).toMatchSnapshot();
32+
});
33+
});

tests/spec/jsonapi-media-type/__snapshots__/basic.test.ts.snap

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ exports[`basic > jsonapi-media-type 1`] = `
1616
export interface Pet {
1717
/** @format uuid */
1818
id?: string;
19-
type: "pet";
19+
type: PetTypeEnum;
2020
attributes: {
2121
name: string;
22-
status?: "available" | "pending" | "sold";
22+
status?: PetStatusEnum;
2323
photoUrls: string[];
2424
};
2525
}
@@ -29,6 +29,16 @@ export interface Error {
2929
detail?: string;
3030
}
3131
32+
export enum PetTypeEnum {
33+
Pet = "pet",
34+
}
35+
36+
export enum PetStatusEnum {
37+
Available = "available",
38+
Pending = "pending",
39+
Sold = "sold",
40+
}
41+
3242
export type QueryParamsType = Record<string | number, any>;
3343
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
3444

0 commit comments

Comments
 (0)