Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ofetch http client #696

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat: add ofetch http client
samydoesit committed Nov 11, 2024
commit 0d1742318656cb918ff40cf85f2db413d5b76a65
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -40,6 +40,7 @@ Options:
--module-name-index <number> determines which path index should be used for routes separation (example: GET:/fruits/getFruit -> index:0 -> moduleName -> fruits) (default: 0)
--module-name-first-tag splits routes based on the first tag (default: false)
--axios generate axios http client (default: false)
--ofetch generate ofetch 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)
@@ -62,7 +63,7 @@ Commands:
generate-templates Generate ".ejs" templates needed for generate api
-o, --output <string> output path of generated templates
-m, --modular generate templates needed to separate files for http client, data contracts, and routes (default: false)
--http-client <string> http client type (possible values: "fetch", "axios") (default: "fetch")
--http-client <string> http client type (possible values: "fetch", "axios", "ofetch") (default: "fetch")
-c, --clean-output clean output folder before generate template. WARNING: May cause data loss (default: false)
-r, --rewrite rewrite content in existing templates (default: false)
--silent Output only errors to console (default: false)
9 changes: 8 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
@@ -191,6 +191,11 @@ const generateCommand = defineCommand({
description: "generate axios http client",
default: false,
},
ofetch: {
type: "boolean",
description: "generate ofetch http client",
default: false,
},
"unwrap-response-data": {
type: "boolean",
description: "unwrap the data item from the response",
@@ -318,7 +323,9 @@ const generateCommand = defineCommand({
httpClientType:
args["http-client"] || args.axios
? HTTP_CLIENT.AXIOS
: HTTP_CLIENT.FETCH,
: args.ofetch
? HTTP_CLIENT.OFETCH
: HTTP_CLIENT.FETCH,
input: path.resolve(process.cwd(), args.path as string),
modular: args.modular,
moduleNameFirstTag: args["module-name-first-tag"],
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ export const FILE_PREFIX = `/* eslint-disable */

export const HTTP_CLIENT = {
FETCH: "fetch",
OFETCH: "ofetch",
AXIOS: "axios",
} as const;

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

import type { $Fetch, FetchOptions } from 'ofetch'
import { $fetch } from 'ofetch'

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

export interface CustomFetchOptions extends FetchOptions {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean
}

export type RequestParams = Omit<CustomFetchOptions, 'body' | 'method'>

export interface ApiConfig<SecurityDataType = unknown> {
baseURL?: string;
basePath?: string;
baseApiParams?: Omit<RequestParams, "baseURL" | "cancelToken" | "signal">;
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
customFetch?: $Fetch;
}

type CancelToken = Symbol | string | number;

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

export class HttpClient<SecurityDataType = unknown> {
public baseURL: string = "<%~ apiConfig.baseUrl %>";
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
private abortControllers = new Map<CancelToken, AbortController>();
private customFetch = (url: string, fetchParams: FetchOptions) => $fetch(url, fetchParams)

private baseApiParams: RequestParams = {
credentials: 'same-origin',
headers: {},
redirect: 'follow',
referrerPolicy: 'no-referrer',
}

constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
Object.assign(this, apiConfig);
}

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

protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
return {
...this.baseApiParams,
...params1,
...(params2 || {}),
headers: {
...(this.baseApiParams.headers || {}),
...(params1.headers || {}),
...((params2 && params2.headers) || {}),
},
};
}

protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
if (this.abortControllers.has(cancelToken)) {
const abortController = this.abortControllers.get(cancelToken);
if (abortController) {
return abortController.signal;
}
return void 0;
}

const abortController = new AbortController();
this.abortControllers.set(cancelToken, abortController);
return abortController.signal;
}

public abortRequest = (cancelToken: CancelToken) => {
const abortController = this.abortControllers.get(cancelToken)

if (abortController) {
abortController.abort();
this.abortControllers.delete(cancelToken);
}
}

public request = async <T = any>(url: string, {
body,
secure,
method,
baseURL,
signal,
params,
...options
<% if (config.unwrapResponseData) { %>
}: CustomFetchOptions): Promise<T> => {
<% } else { %>
}: CustomFetchOptions): Promise<T> => {
<% } %>
const secureParams = ((typeof secure === 'boolean' ? secure : this.baseApiParams.secure) && this.securityWorker && await this.securityWorker(this.securityData)) || {};
const requestOptions = this.mergeRequestParams(options, secureParams)

return this.customFetch(
`${baseURL || this.baseURL || ""}${this.basePath ? `${this.basePath}` : ''}${url}`,
{
params,
method,
...requestOptions,
signal,
body,
}
<% if (config.unwrapResponseData) { %>
).then((response: T) => response.data)
<% } else { %>
).then((response: T) => response)
<% } %>
};
}
7 changes: 7 additions & 0 deletions templates/default/procedure-call.ejs
Original file line number Diff line number Diff line change
@@ -88,13 +88,20 @@ const describeReturnType = () => {

*/
<%~ route.routeName.usage %><%~ route.namespace ? ': ' : ' = ' %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> =>
<% if (config.httpClientType === config.constants.HTTP_CLIENT.OFETCH) { %>
<%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>>(`<%~ path %>`, {
<% } %>
<% if (config.httpClientType !== config.constants.HTTP_CLIENT.OFETCH) { %>
<%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>, <%~ errorType %>>({
path: `<%~ path %>`,
<% } %>
method: '<%~ _.upperCase(method) %>',
<%~ queryTmpl ? `query: ${queryTmpl},` : '' %>
<%~ bodyTmpl ? `body: ${bodyTmpl},` : '' %>
<%~ securityTmpl ? `secure: ${securityTmpl},` : '' %>
<% if (config.httpClientType !== config.constants.HTTP_CLIENT.OFETCH) { %>
<%~ bodyContentKindTmpl ? `type: ${bodyContentKindTmpl},` : '' %>
<%~ responseFormatTmpl ? `format: ${responseFormatTmpl},` : '' %>
<% } %>
...<%~ _.get(requestConfigParam, "name") %>,
})<%~ route.namespace ? ',' : '' %>
5,706 changes: 5,706 additions & 0 deletions tests/spec/ofetch/__snapshots__/basic.test.ts.snap

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions tests/spec/ofetch/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";

import { afterAll, beforeAll, describe, expect, test } from "vitest";

import { generateApi } from "../../../src/index.js";

describe("basic", async () => {
let tmpdir = "";

beforeAll(async () => {
tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api"));
});

afterAll(async () => {
await fs.rm(tmpdir, { recursive: true });
});

test("--ofetch option", async () => {
await generateApi({
fileName: "schema",
input: path.resolve(import.meta.dirname, "schema.json"),
output: tmpdir,
silent: true,
generateClient: true,
httpClientType: "ofetch",
});

const content = await fs.readFile(path.join(tmpdir, "schema.ts"), {
encoding: "utf8",
});

expect(content).toMatchSnapshot();
});
});
23,828 changes: 23,828 additions & 0 deletions tests/spec/ofetch/schema.json

Large diffs are not rendered by default.

5,712 changes: 5,712 additions & 0 deletions tests/spec/ofetchSingleHttpClient/__snapshots__/basic.test.ts.snap

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions tests/spec/ofetchSingleHttpClient/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";

import { afterAll, beforeAll, describe, expect, test } from "vitest";

import { generateApi } from "../../../src/index.js";

describe("basic", async () => {
let tmpdir = "";

beforeAll(async () => {
tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api"));
});

afterAll(async () => {
await fs.rm(tmpdir, { recursive: true });
});

test("--ofetch --single-http-client", async () => {
await generateApi({
fileName: "schema",
input: path.resolve(import.meta.dirname, "schema.json"),
output: tmpdir,
silent: true,
generateClient: true,
httpClientType: "ofetch",
singleHttpClient: true,
});

const content = await fs.readFile(path.join(tmpdir, "schema.ts"), {
encoding: "utf8",
});

expect(content).toMatchSnapshot();
});
});
23,828 changes: 23,828 additions & 0 deletions tests/spec/ofetchSingleHttpClient/schema.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions types/index.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import type { ComponentTypeNameResolver } from "../src/component-type-name-resol
import type { MonoSchemaParser } from "../src/schema-parser/mono-schema-parser.js";
import type { Translator } from "../src/translators/translator.js";

type HttpClientType = "axios" | "fetch";
type HttpClientType = "axios" | "fetch" | "ofetch";

interface GenerateApiParamsBase {
/**
@@ -638,7 +638,7 @@ export interface GenerateApiConfiguration {
debug: boolean;
anotherArrayType: boolean;
extractRequestBody: boolean;
httpClientType: "axios" | "fetch";
httpClientType: "axios" | "fetch" | "ofetch";
addReadonly: boolean;
extractResponseBody: boolean;
extractResponseError: boolean;