Skip to content

Commit 9d1d74e

Browse files
committed
Implement const object-style enum generation in place of traditional TypeScript
enum keyword objects. Closes #1040.
1 parent 9950c7f commit 9d1d74e

File tree

9 files changed

+440
-0
lines changed

9 files changed

+440
-0
lines changed

.changeset/honest-feet-worry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"swagger-typescript-api": minor
3+
---
4+
5+
Implement const object-style enum generation

index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ const generateCommand = defineCommand({
181181
description: 'generate all "enum" types as union types (T1 | T2 | TN)',
182182
default: codeGenBaseConfig.generateUnionEnums,
183183
},
184+
"generate-const-object-enums": {
185+
type: "boolean",
186+
description:
187+
'generate all "enum" types as pairs of const objects and types derived from those objects\' keys. Mutually exclusive with, and pre-empted by, generateUnionEnums',
188+
default: codeGenBaseConfig.generateConstObjectEnums, // TODO: collapse enum booleans into a single field taking an enum?
189+
},
184190
"http-client": {
185191
type: "string",
186192
description: `http client type (possible values: ${Object.values(
@@ -311,6 +317,7 @@ const generateCommand = defineCommand({
311317
generateResponses: args.responses,
312318
generateRouteTypes: args["route-types"],
313319
generateUnionEnums: args["generate-union-enums"],
320+
generateConstObjectEnums: args["generate-const-object-enums"],
314321
httpClientType:
315322
args["http-client"] || args.axios
316323
? HTTP_CLIENT.AXIOS

src/configuration.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const TsKeyword = {
3333
Record: "Record",
3434
Intersection: "&",
3535
Union: "|",
36+
Const: "const",
3637
};
3738

3839
const TsCodeGenKeyword = {
@@ -54,6 +55,8 @@ export class CodeGenConfig {
5455
/** CLI flag */
5556
generateUnionEnums = false;
5657
/** CLI flag */
58+
generateConstObjectEnums = false;
59+
/** CLI flag */
5760
addReadonly = false;
5861
enumNamesAsValues = false;
5962
/** parsed swagger schema from getSwaggerObject() */

src/schema-parser/schema-formatters.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@ export class SchemaFormatters {
2929
};
3030
}
3131

32+
if (this.config.generateConstObjectEnums) {
33+
const entries = parsedSchema.content
34+
.map(({ key, value }) => {
35+
return `${key}: ${value}`;
36+
})
37+
.join(",\n ");
38+
return {
39+
...parsedSchema,
40+
$content: parsedSchema.content,
41+
typeIdentifier: this.config.Ts.Keyword.Const,
42+
content: `{\n ${entries}\n} as const;\nexport type ${parsedSchema.name} = (typeof ${parsedSchema.name})[keyof typeof ${parsedSchema.name}];`,
43+
};
44+
}
45+
46+
// Fallback: classic TypeScript enum
3247
return {
3348
...parsedSchema,
3449
$content: parsedSchema.content,

templates/base/data-contracts.ejs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ const dataContractTemplates = {
2525
type: (contract) => {
2626
return `type ${contract.name}${buildGenerics(contract)} = ${contract.content}`;
2727
},
28+
'const': (contract) => {
29+
return `const ${contract.name}${buildGenerics(contract)} = ${contract.content}`;
30+
},
2831
}
2932
%>
3033

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`basic > --generate-const-object-enums 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+
/**
17+
* FooBar
18+
* @format int32
19+
*/
20+
export const IntEnumWithNames = {
21+
Unknown: 0,
22+
String: 1,
23+
Int32: 2,
24+
Int64: 3,
25+
Double: 4,
26+
DateTime: 5,
27+
Test2: 6,
28+
Test23: 7,
29+
Tess44: 8,
30+
BooFar: 9,
31+
} as const;
32+
export type IntEnumWithNames =
33+
(typeof IntEnumWithNames)[keyof typeof IntEnumWithNames];
34+
35+
export const BooleanEnum = {
36+
True: true,
37+
False: false,
38+
} as const;
39+
export type BooleanEnum = (typeof BooleanEnum)[keyof typeof BooleanEnum];
40+
41+
export const NumberEnum = {
42+
Value1: 1,
43+
Value2: 2,
44+
Value3: 3,
45+
Value4: 4,
46+
} as const;
47+
export type NumberEnum = (typeof NumberEnum)[keyof typeof NumberEnum];
48+
49+
export const StringEnum = {
50+
String1: "String1",
51+
String2: "String2",
52+
String3: "String3",
53+
String4: "String4",
54+
} as const;
55+
export type StringEnum = (typeof StringEnum)[keyof typeof StringEnum];
56+
57+
export type QueryParamsType = Record<string | number, any>;
58+
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
59+
60+
export interface FullRequestParams extends Omit<RequestInit, "body"> {
61+
/** set parameter to \`true\` for call \`securityWorker\` for this request */
62+
secure?: boolean;
63+
/** request path */
64+
path: string;
65+
/** content type of request body */
66+
type?: ContentType;
67+
/** query params */
68+
query?: QueryParamsType;
69+
/** format of response (i.e. response.json() -> format: "json") */
70+
format?: ResponseFormat;
71+
/** request body */
72+
body?: unknown;
73+
/** base url */
74+
baseUrl?: string;
75+
/** request cancellation token */
76+
cancelToken?: CancelToken;
77+
}
78+
79+
export type RequestParams = Omit<
80+
FullRequestParams,
81+
"body" | "method" | "query" | "path"
82+
>;
83+
84+
export interface ApiConfig<SecurityDataType = unknown> {
85+
baseUrl?: string;
86+
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
87+
securityWorker?: (
88+
securityData: SecurityDataType | null,
89+
) => Promise<RequestParams | void> | RequestParams | void;
90+
customFetch?: typeof fetch;
91+
}
92+
93+
export interface HttpResponse<D extends unknown, E extends unknown = unknown>
94+
extends Response {
95+
data: D;
96+
error: E;
97+
}
98+
99+
type CancelToken = Symbol | string | number;
100+
101+
export enum ContentType {
102+
Json = "application/json",
103+
JsonApi = "application/vnd.api+json",
104+
FormData = "multipart/form-data",
105+
UrlEncoded = "application/x-www-form-urlencoded",
106+
Text = "text/plain",
107+
}
108+
109+
export class HttpClient<SecurityDataType = unknown> {
110+
public baseUrl: string = "http://localhost:8080/api/v1";
111+
private securityData: SecurityDataType | null = null;
112+
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
113+
private abortControllers = new Map<CancelToken, AbortController>();
114+
private customFetch = (...fetchParams: Parameters<typeof fetch>) =>
115+
fetch(...fetchParams);
116+
117+
private baseApiParams: RequestParams = {
118+
credentials: "same-origin",
119+
headers: {},
120+
redirect: "follow",
121+
referrerPolicy: "no-referrer",
122+
};
123+
124+
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
125+
Object.assign(this, apiConfig);
126+
}
127+
128+
public setSecurityData = (data: SecurityDataType | null) => {
129+
this.securityData = data;
130+
};
131+
132+
protected encodeQueryParam(key: string, value: any) {
133+
const encodedKey = encodeURIComponent(key);
134+
return \`\${encodedKey}=\${encodeURIComponent(typeof value === "number" ? value : \`\${value}\`)}\`;
135+
}
136+
137+
protected addQueryParam(query: QueryParamsType, key: string) {
138+
return this.encodeQueryParam(key, query[key]);
139+
}
140+
141+
protected addArrayQueryParam(query: QueryParamsType, key: string) {
142+
const value = query[key];
143+
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
144+
}
145+
146+
protected toQueryString(rawQuery?: QueryParamsType): string {
147+
const query = rawQuery || {};
148+
const keys = Object.keys(query).filter(
149+
(key) => "undefined" !== typeof query[key],
150+
);
151+
return keys
152+
.map((key) =>
153+
Array.isArray(query[key])
154+
? this.addArrayQueryParam(query, key)
155+
: this.addQueryParam(query, key),
156+
)
157+
.join("&");
158+
}
159+
160+
protected addQueryParams(rawQuery?: QueryParamsType): string {
161+
const queryString = this.toQueryString(rawQuery);
162+
return queryString ? \`?\${queryString}\` : "";
163+
}
164+
165+
private contentFormatters: Record<ContentType, (input: any) => any> = {
166+
[ContentType.Json]: (input: any) =>
167+
input !== null && (typeof input === "object" || typeof input === "string")
168+
? JSON.stringify(input)
169+
: input,
170+
[ContentType.JsonApi]: (input: any) =>
171+
input !== null && (typeof input === "object" || typeof input === "string")
172+
? JSON.stringify(input)
173+
: input,
174+
[ContentType.Text]: (input: any) =>
175+
input !== null && typeof input !== "string"
176+
? JSON.stringify(input)
177+
: input,
178+
[ContentType.FormData]: (input: any) =>
179+
Object.keys(input || {}).reduce((formData, key) => {
180+
const property = input[key];
181+
formData.append(
182+
key,
183+
property instanceof Blob
184+
? property
185+
: typeof property === "object" && property !== null
186+
? JSON.stringify(property)
187+
: \`\${property}\`,
188+
);
189+
return formData;
190+
}, new FormData()),
191+
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
192+
};
193+
194+
protected mergeRequestParams(
195+
params1: RequestParams,
196+
params2?: RequestParams,
197+
): RequestParams {
198+
return {
199+
...this.baseApiParams,
200+
...params1,
201+
...(params2 || {}),
202+
headers: {
203+
...(this.baseApiParams.headers || {}),
204+
...(params1.headers || {}),
205+
...((params2 && params2.headers) || {}),
206+
},
207+
};
208+
}
209+
210+
protected createAbortSignal = (
211+
cancelToken: CancelToken,
212+
): AbortSignal | undefined => {
213+
if (this.abortControllers.has(cancelToken)) {
214+
const abortController = this.abortControllers.get(cancelToken);
215+
if (abortController) {
216+
return abortController.signal;
217+
}
218+
return void 0;
219+
}
220+
221+
const abortController = new AbortController();
222+
this.abortControllers.set(cancelToken, abortController);
223+
return abortController.signal;
224+
};
225+
226+
public abortRequest = (cancelToken: CancelToken) => {
227+
const abortController = this.abortControllers.get(cancelToken);
228+
229+
if (abortController) {
230+
abortController.abort();
231+
this.abortControllers.delete(cancelToken);
232+
}
233+
};
234+
235+
public request = async <T = any, E = any>({
236+
body,
237+
secure,
238+
path,
239+
type,
240+
query,
241+
format,
242+
baseUrl,
243+
cancelToken,
244+
...params
245+
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
246+
const secureParams =
247+
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
248+
this.securityWorker &&
249+
(await this.securityWorker(this.securityData))) ||
250+
{};
251+
const requestParams = this.mergeRequestParams(params, secureParams);
252+
const queryString = query && this.toQueryString(query);
253+
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
254+
const responseFormat = format || requestParams.format;
255+
256+
return this.customFetch(
257+
\`\${baseUrl || this.baseUrl || ""}\${path}\${queryString ? \`?\${queryString}\` : ""}\`,
258+
{
259+
...requestParams,
260+
headers: {
261+
...(requestParams.headers || {}),
262+
...(type && type !== ContentType.FormData
263+
? { "Content-Type": type }
264+
: {}),
265+
},
266+
signal:
267+
(cancelToken
268+
? this.createAbortSignal(cancelToken)
269+
: requestParams.signal) || null,
270+
body:
271+
typeof body === "undefined" || body === null
272+
? null
273+
: payloadFormatter(body),
274+
},
275+
).then(async (response) => {
276+
const r = response.clone() as HttpResponse<T, E>;
277+
r.data = null as unknown as T;
278+
r.error = null as unknown as E;
279+
280+
const data = !responseFormat
281+
? r
282+
: await response[responseFormat]()
283+
.then((data) => {
284+
if (r.ok) {
285+
r.data = data;
286+
} else {
287+
r.error = data;
288+
}
289+
return r;
290+
})
291+
.catch((e) => {
292+
r.error = e;
293+
return r;
294+
});
295+
296+
if (cancelToken) {
297+
this.abortControllers.delete(cancelToken);
298+
}
299+
300+
if (!response.ok) throw data;
301+
return data;
302+
});
303+
};
304+
}
305+
306+
/**
307+
* @title No title
308+
* @baseUrl http://localhost:8080/api/v1
309+
*/
310+
export class Api<
311+
SecurityDataType extends unknown,
312+
> extends HttpClient<SecurityDataType> {}
313+
"
314+
`;

0 commit comments

Comments
 (0)