Skip to content

Commit 7c8567e

Browse files
committed
feat: add --ky option
Generate a client that uses this library: https://github.com/sindresorhus/ky
1 parent 2b6db23 commit 7c8567e

File tree

10 files changed

+161
-2
lines changed

10 files changed

+161
-2
lines changed

Diff for: README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Options:
6868
--disableStrictSSL disabled strict SSL (default: false)
6969
--disableProxy disabled proxy (default: false)
7070
--axios generate axios http client (default: false)
71+
--ky generate ky http client (default: false)
7172
--unwrap-response-data unwrap the data item from the response (default: false)
7273
--disable-throw-on-error Do not throw an error when response.ok is not true (default: false)
7374
--single-http-client Ability to send HttpClient instance to Api constructor (default: false)
@@ -124,7 +125,7 @@ generateApi({
124125
// ...
125126
},
126127
templates: path.resolve(process.cwd(), './api-templates'),
127-
httpClientType: "axios", // or "fetch"
128+
httpClientType: "axios", // or "fetch" or "ky"
128129
defaultResponseAsSuccess: false,
129130
generateClient: true,
130131
generateRouteTypes: false,

Diff for: index.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ const program = cli({
162162
description: 'generate axios http client',
163163
default: codeGenBaseConfig.httpClientType === HTTP_CLIENT.AXIOS,
164164
},
165+
{
166+
flags: '--ky',
167+
description: 'generate axios http client',
168+
default: codeGenBaseConfig.httpClientType === HTTP_CLIENT.KY,
169+
},
165170
{
166171
flags: '--unwrap-response-data',
167172
description: 'unwrap the data item from the response',
@@ -324,7 +329,7 @@ const main = async () => {
324329
url: options.path,
325330
generateRouteTypes: options.routeTypes,
326331
generateClient: !!(options.axios || options.client),
327-
httpClientType: options.axios ? HTTP_CLIENT.AXIOS : HTTP_CLIENT.FETCH,
332+
httpClientType: options.axios ? HTTP_CLIENT.AXIOS : options.ky ? HTTP_CLIENT.KY : HTTP_CLIENT.FETCH,
328333
input: resolve(process.cwd(), options.path),
329334
output: resolve(process.cwd(), options.output || '.'),
330335
...customConfig,

Diff for: package-lock.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"dotenv": "^16.3.1",
8686
"git-diff": "^2.0.6",
8787
"husky": "^8.0.3",
88+
"ky": "^1.2.2",
8889
"pretty-quick": "^3.1.3",
8990
"rimraf": "^5.0.1"
9091
},

Diff for: src/constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const SCHEMA_TYPES = {
2828
const HTTP_CLIENT = {
2929
FETCH: 'fetch',
3030
AXIOS: 'axios',
31+
KY: 'ky',
3132
};
3233

3334
const PROJECT_VERSION = packageJson.version;

Diff for: templates/base/http-clients/ky-http-client.ejs

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<%
2+
const { apiConfig, generateResponses, config } = it;
3+
%>
4+
5+
import type { KyInstance, Options as KyOptions } from "ky";
6+
import ky from "ky";
7+
8+
export type KyResponse<Data> = Response & {
9+
json<T extends Data = Data>(): Promise<T>;
10+
}
11+
12+
export type QueryParamsType = Record<string | number, any>;
13+
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
14+
15+
export interface FullRequestParams extends Omit<KyOptions, "json" | "body"> {
16+
/** set parameter to `true` for call `securityWorker` for this request */
17+
secure?: boolean;
18+
/** request path */
19+
path: string;
20+
/** content type of request body */
21+
type?: ContentType;
22+
/** query params */
23+
query?: QueryParamsType;
24+
/** format of response (i.e. response.json() -> format: "json") */
25+
format?: ResponseFormat;
26+
/** request body */
27+
body?: unknown;
28+
}
29+
30+
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
31+
32+
export interface ApiConfig<SecurityDataType = unknown> extends Omit<KyOptions, "data" | "cancelToken"> {
33+
securityWorker?: (securityData: SecurityDataType | null) => Promise<KyOptions | void> | KyOptions | void;
34+
secure?: boolean;
35+
format?: ResponseType;
36+
}
37+
38+
export enum ContentType {
39+
Json = "application/json",
40+
FormData = "multipart/form-data",
41+
UrlEncoded = "application/x-www-form-urlencoded",
42+
Text = "text/plain",
43+
}
44+
45+
export class HttpClient<SecurityDataType = unknown> {
46+
public instance: KyInstance;
47+
private securityData: SecurityDataType | null = null;
48+
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
49+
private secure?: boolean;
50+
private format?: ResponseType;
51+
52+
constructor({ securityWorker, secure, format, ...options }: ApiConfig<SecurityDataType> = {}) {
53+
this.instance = axios.create({ ...options, prefixUrl: options.prefixUrl || "<%~ apiConfig.baseUrl %>" })
54+
this.secure = secure;
55+
this.format = format;
56+
this.securityWorker = securityWorker;
57+
}
58+
59+
public setSecurityData = (data: SecurityDataType | null) => {
60+
this.securityData = data
61+
}
62+
63+
protected mergeRequestParams(params1: KyOptions, params2?: KyOptions): KyOptions {
64+
return {
65+
...params1,
66+
...params2,
67+
headers: {
68+
...(params1.headers),
69+
...(params2 && params2.headers),
70+
},
71+
};
72+
}
73+
74+
protected stringifyFormItem(formItem: unknown) {
75+
if (typeof formItem === "object" && formItem !== null) {
76+
return JSON.stringify(formItem);
77+
} else {
78+
return `${formItem}`;
79+
}
80+
}
81+
82+
protected createFormData(input: Record<string, unknown>): FormData {
83+
return Object.keys(input || {}).reduce((formData, key) => {
84+
const property = input[key];
85+
const propertyContent: any[] = (property instanceof Array) ? property : [property]
86+
87+
for (const formItem of propertyContent) {
88+
const isFileType = formItem instanceof Blob || formItem instanceof File;
89+
formData.append(
90+
key,
91+
isFileType ? formItem : this.stringifyFormItem(formItem)
92+
);
93+
}
94+
95+
return formData;
96+
}, new FormData());
97+
}
98+
99+
public request = async <T = any, _E = any>({
100+
secure,
101+
path,
102+
type,
103+
query,
104+
format,
105+
body,
106+
...params
107+
<% if (config.unwrapResponseData) { %>
108+
}: FullRequestParams): Promise<T> => {
109+
<% } else { %>
110+
}: FullRequestParams): KyResponse<T> => {
111+
<% } %>
112+
const secureParams = ((typeof secure === 'boolean' ? secure : this.secure) && this.securityWorker && (await this.securityWorker(this.securityData))) || {};
113+
const requestParams = this.mergeRequestParams(params, secureParams);
114+
const responseFormat = (format || this.format) || undefined;
115+
116+
if (type === ContentType.FormData && body && body !== null && typeof body === "object") {
117+
body = this.createFormData(body as Record<string, unknown>);
118+
}
119+
120+
if (type === ContentType.Text && body && body !== null && typeof body !== "string") {
121+
body = JSON.stringify(body);
122+
}
123+
124+
return this.instance.request({
125+
...requestParams,
126+
headers: {
127+
...(requestParams.headers || {}),
128+
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
129+
},
130+
params: query,
131+
responseType: responseFormat,
132+
data: body,
133+
url: path,
134+
<% if (config.unwrapResponseData) { %>
135+
}).json();
136+
<% } else { %>
137+
});
138+
<% } %>
139+
};
140+
}

Diff for: templates/default/api.ejs

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ const descriptionLines = _.compact([
2828

2929
<% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %>
3030

31+
<% if (config.httpClientType === config.constants.HTTP_CLIENT.KY) { %> import type { KyResponse } from "ky"; <% } %>
32+
3133
<% if (descriptionLines.length) { %>
3234
/**
3335
<% descriptionLines.forEach((descriptionLine) => { %>

Diff for: templates/default/procedure-call.ejs

+3
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ const describeReturnType = () => {
7272
case HTTP_CLIENT.AXIOS: {
7373
return `Promise<AxiosResponse<${type}>>`
7474
}
75+
case HTTP_CLIENT.KY: {
76+
return `KyResponse<${type}>`
77+
}
7578
default: {
7679
return `Promise<HttpResponse<${type}, ${errorType}>`
7780
}

Diff for: templates/modular/api.ejs

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const dataContracts = _.map(modelTypes, "name");
88

99
<% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %>
1010

11+
<% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { KyResponse } from "ky"; <% } %>
12+
1113
import { HttpClient, RequestParams, ContentType, HttpResponse } from "./<%~ config.fileNames.httpClient %>";
1214
<% if (dataContracts.length) { %>
1315
import { <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %>"

Diff for: templates/modular/procedure-call.ejs

+3
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ const describeReturnType = () => {
7272
case HTTP_CLIENT.AXIOS: {
7373
return `Promise<AxiosResponse<${type}>>`
7474
}
75+
case HTTP_CLIENT.KY: {
76+
return `KyResponse<${type}>`
77+
}
7578
default: {
7679
return `Promise<HttpResponse<${type}, ${errorType}>`
7780
}

0 commit comments

Comments
 (0)