Skip to content

Commit

Permalink
feat: add --ky option
Browse files Browse the repository at this point in the history
Generate a client that uses this library: https://github.com/sindresorhus/ky
  • Loading branch information
aleclarson committed Mar 5, 2024
1 parent 2b6db23 commit 8be147d
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 2 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Options:
--disableStrictSSL disabled strict SSL (default: false)
--disableProxy disabled proxy (default: false)
--axios generate axios http client (default: false)
--ky generate ky http client (default: false)
--unwrap-response-data unwrap the data item from the response (default: false)
--disable-throw-on-error Do not throw an error when response.ok is not true (default: false)
--single-http-client Ability to send HttpClient instance to Api constructor (default: false)
Expand Down Expand Up @@ -124,7 +125,7 @@ generateApi({
// ...
},
templates: path.resolve(process.cwd(), './api-templates'),
httpClientType: "axios", // or "fetch"
httpClientType: "axios", // or "fetch" or "ky"
defaultResponseAsSuccess: false,
generateClient: true,
generateRouteTypes: false,
Expand Down
7 changes: 6 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ const program = cli({
description: 'generate axios http client',
default: codeGenBaseConfig.httpClientType === HTTP_CLIENT.AXIOS,
},
{
flags: '--ky',
description: 'generate axios http client',
default: codeGenBaseConfig.httpClientType === HTTP_CLIENT.KY,
},
{
flags: '--unwrap-response-data',
description: 'unwrap the data item from the response',
Expand Down Expand Up @@ -324,7 +329,7 @@ const main = async () => {
url: options.path,
generateRouteTypes: options.routeTypes,
generateClient: !!(options.axios || options.client),
httpClientType: options.axios ? HTTP_CLIENT.AXIOS : HTTP_CLIENT.FETCH,
httpClientType: options.axios ? HTTP_CLIENT.AXIOS : options.ky ? HTTP_CLIENT.KY : HTTP_CLIENT.FETCH,
input: resolve(process.cwd(), options.path),
output: resolve(process.cwd(), options.output || '.'),
...customConfig,
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"dotenv": "^16.3.1",
"git-diff": "^2.0.6",
"husky": "^8.0.3",
"ky": "^1.2.2",
"pretty-quick": "^3.1.3",
"rimraf": "^5.0.1"
},
Expand Down
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const SCHEMA_TYPES = {
const HTTP_CLIENT = {
FETCH: 'fetch',
AXIOS: 'axios',
KY: 'ky',
};

const PROJECT_VERSION = packageJson.version;
Expand Down
136 changes: 136 additions & 0 deletions templates/base/http-clients/ky-http-client.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<%
const { apiConfig, generateResponses, config } = it;
%>

import type { KyInstance, Options as KyOptions, KyResponse } from "ky";
import ky from "ky";

export type QueryParamsType = Record<string | number, any>;
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;

export interface FullRequestParams extends Omit<KyOptions, "json" | "body"> {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean;
/** request path */
path: string;
/** content type of request body */
type?: ContentType;
/** query params */
query?: QueryParamsType;
/** format of response (i.e. response.json() -> format: "json") */
format?: ResponseFormat;
/** request body */
body?: unknown;
}

export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;

export interface ApiConfig<SecurityDataType = unknown> extends Omit<KyOptions, "data" | "cancelToken"> {
securityWorker?: (securityData: SecurityDataType | null) => Promise<KyOptions | void> | KyOptions | void;
secure?: boolean;
format?: ResponseType;
}

export enum ContentType {
Json = "application/json",
FormData = "multipart/form-data",
UrlEncoded = "application/x-www-form-urlencoded",
Text = "text/plain",
}

export class HttpClient<SecurityDataType = unknown> {
public instance: KyInstance;
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
private secure?: boolean;
private format?: ResponseType;

constructor({ securityWorker, secure, format, ...options }: ApiConfig<SecurityDataType> = {}) {
this.instance = axios.create({ ...options, prefixUrl: options.prefixUrl || "<%~ apiConfig.baseUrl %>" })
this.secure = secure;
this.format = format;
this.securityWorker = securityWorker;
}

public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data
}

protected mergeRequestParams(params1: KyOptions, params2?: KyOptions): KyOptions {
return {
...params1,
...params2,
headers: {
...(params1.headers),
...(params2 && params2.headers),
},
};
}

protected stringifyFormItem(formItem: unknown) {
if (typeof formItem === "object" && formItem !== null) {
return JSON.stringify(formItem);
} else {
return `${formItem}`;
}
}

protected createFormData(input: Record<string, unknown>): FormData {
return Object.keys(input || {}).reduce((formData, key) => {
const property = input[key];
const propertyContent: any[] = (property instanceof Array) ? property : [property]

for (const formItem of propertyContent) {
const isFileType = formItem instanceof Blob || formItem instanceof File;
formData.append(
key,
isFileType ? formItem : this.stringifyFormItem(formItem)
);
}

return formData;
}, new FormData());
}

public request = async <T = any, _E = any>({
secure,
path,
type,
query,
format,
body,
...params
<% if (config.unwrapResponseData) { %>
}: FullRequestParams): Promise<T> => {
<% } else { %>
}: FullRequestParams): KyResponse<T> => {
<% } %>
const secureParams = ((typeof secure === 'boolean' ? secure : this.secure) && this.securityWorker && (await this.securityWorker(this.securityData))) || {};
const requestParams = this.mergeRequestParams(params, secureParams);
const responseFormat = (format || this.format) || undefined;

if (type === ContentType.FormData && body && body !== null && typeof body === "object") {
body = this.createFormData(body as Record<string, unknown>);
}

if (type === ContentType.Text && body && body !== null && typeof body !== "string") {
body = JSON.stringify(body);
}

return this.instance.request({
...requestParams,
headers: {
...(requestParams.headers || {}),
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
},
params: query,
responseType: responseFormat,
data: body,
url: path,
<% if (config.unwrapResponseData) { %>
}).json();
<% } else { %>
});
<% } %>
};
}
2 changes: 2 additions & 0 deletions templates/default/api.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const descriptionLines = _.compact([

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

<% if (config.httpClientType === config.constants.HTTP_CLIENT.KY) { %> import type { KyResponse } from "ky"; <% } %>

<% if (descriptionLines.length) { %>
/**
<% descriptionLines.forEach((descriptionLine) => { %>
Expand Down
3 changes: 3 additions & 0 deletions templates/default/procedure-call.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ const describeReturnType = () => {
case HTTP_CLIENT.AXIOS: {
return `Promise<AxiosResponse<${type}>>`
}
case HTTP_CLIENT.KY: {
return `KyResponse<${type}>`
}
default: {
return `Promise<HttpResponse<${type}, ${errorType}>`
}
Expand Down
2 changes: 2 additions & 0 deletions templates/modular/api.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const dataContracts = _.map(modelTypes, "name");

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

<% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { KyResponse } from "ky"; <% } %>

import { HttpClient, RequestParams, ContentType, HttpResponse } from "./<%~ config.fileNames.httpClient %>";
<% if (dataContracts.length) { %>
import { <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %>"
Expand Down
3 changes: 3 additions & 0 deletions templates/modular/procedure-call.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ const describeReturnType = () => {
case HTTP_CLIENT.AXIOS: {
return `Promise<AxiosResponse<${type}>>`
}
case HTTP_CLIENT.KY: {
return `KyResponse<${type}>`
}
default: {
return `Promise<HttpResponse<${type}, ${errorType}>`
}
Expand Down

0 comments on commit 8be147d

Please sign in to comment.