Skip to content

Commit 2d0ca4e

Browse files
committed
feat: new hooks (onPreBuildRoutePath, onBuildRoutePath, onInsertPathParam)
1 parent 5280658 commit 2d0ca4e

File tree

9 files changed

+627
-9
lines changed

9 files changed

+627
-9
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# next release
22

3+
new hooks:
4+
```ts
5+
/** calls before parse\process route path */
6+
onPreBuildRoutePath: (routePath: string) => string | void;
7+
/** calls after parse\process route path */
8+
onBuildRoutePath: (data: BuildRoutePath) => BuildRoutePath | void;
9+
/** calls before insert path param name into string path interpolation */
10+
onInsertPathParam: (paramName: string, index: number, arr: BuildRouteParam[], resultRoute: string) => string | void;
11+
```
12+
feature: ability to modify route path params before insert them into string (request url, #446, with using hook `onInsertPathParam`)
13+
314
# 11.1.3
415

516
fix: problems with `text/*` content types (axios, fetch http clients) (thanks @JochenDiekenbrock, #312, #443)

index.d.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,35 @@ interface GenerateApiParamsFromSpecLiteral extends GenerateApiParamsBase {
212212

213213
export type GenerateApiParams = GenerateApiParamsFromPath | GenerateApiParamsFromUrl | GenerateApiParamsFromSpecLiteral;
214214

215+
type BuildRouteParam = {
216+
/** {bar} */
217+
$match: string;
218+
name: string;
219+
required: boolean;
220+
type: "string";
221+
description: string;
222+
schema: {
223+
type: string;
224+
};
225+
in: "path" | "query";
226+
};
227+
228+
type BuildRoutePath = {
229+
/** /foo/{bar}/baz */
230+
originalRoute: string;
231+
/** /foo/${bar}/baz */
232+
route: string;
233+
pathParams: BuildRouteParam[];
234+
queryParams: BuildRouteParam[];
235+
};
236+
215237
export interface Hooks {
238+
/** calls before parse\process route path */
239+
onPreBuildRoutePath: (routePath: string) => string | void;
240+
/** calls after parse\process route path */
241+
onBuildRoutePath: (data: BuildRoutePath) => BuildRoutePath | void;
242+
/** calls before insert path param name into string path interpolation */
243+
onInsertPathParam: (paramName: string, index: number, arr: BuildRouteParam[], resultRoute: string) => string | void;
216244
/** calls after parse schema component */
217245
onCreateComponent: (component: SchemaComponent) => SchemaComponent | void;
218246
/** calls after parse any kind of schema */

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@
5757
"test:nullableRefTest2.0": "node tests/spec/nullable-2.0/test.js",
5858
"test:additionalProperties2.0": "node tests/spec/additional-properties-2.0/test.js",
5959
"test:enums2.0": "node tests/spec/enums-2.0/test.js",
60-
"test:another-query-params": "node tests/spec/another-query-params/test.js"
60+
"test:another-query-params": "node tests/spec/another-query-params/test.js",
61+
"test:on-insert-path-param": "node tests/spec/on-insert-path-param/test.js"
6162
},
6263
"author": "acacode",
6364
"license": "MIT",

src/configuration.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ class CodeGenConfig {
8181
routeNameDuplicatesMap = new Map();
8282
prettierOptions = { ...CONSTANTS.PRETTIER_OPTIONS };
8383
hooks = {
84+
onPreBuildRoutePath: (routePath) => void 0,
85+
onBuildRoutePath: (routeData) => void 0,
86+
onInsertPathParam: (pathParam) => void 0,
8487
onCreateComponent: (schema) => schema,
8588
onParseSchema: (originalSchema, parsedSchema) => parsedSchema,
8689
onCreateRoute: (routeData) => routeData,

src/schema-parser/schema-routes.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ class SchemaRoutes {
8787
);
8888
};
8989

90-
parseRouteName = (routeName) => {
90+
parseRouteName = (originalRouteName) => {
91+
const routeName = this.config.hooks.onPreBuildRoutePath(originalRouteName) || originalRouteName;
92+
9193
const pathParamMatches = (routeName || "").match(
9294
/({(([a-zA-Z]-?_?\.?){1,})([0-9]{1,})?})|(:(([a-zA-Z]-?_?\.?){1,})([0-9]{1,})?:?)/g,
9395
);
@@ -123,8 +125,9 @@ class SchemaRoutes {
123125

124126
let fixedRoute = _.reduce(
125127
pathParams,
126-
(fixedRoute, pathParam) => {
127-
return _.replace(fixedRoute, pathParam.$match, `\${${pathParam.name}}`);
128+
(fixedRoute, pathParam, i, arr) => {
129+
const insertion = this.config.hooks.onInsertPathParam(pathParam.name, i, arr, fixedRoute) || pathParam.name;
130+
return _.replace(fixedRoute, pathParam.$match, `\${${insertion}}`);
128131
},
129132
routeName || "",
130133
);
@@ -161,12 +164,14 @@ class SchemaRoutes {
161164
});
162165
}
163166

164-
return {
165-
originalRoute: routeName || "",
167+
const result = {
168+
originalRoute: originalRouteName || "",
166169
route: fixedRoute,
167170
pathParams,
168171
queryParams,
169172
};
173+
174+
return this.config.hooks.onBuildRoutePath(result) || result;
170175
};
171176

172177
getRouteParams = (routeInfo, pathParamsFromRouteName, queryParamsFromRouteName) => {
@@ -266,9 +271,7 @@ class SchemaRoutes {
266271
return CONTENT_KIND.IMAGE;
267272
}
268273

269-
if (
270-
_.some(contentTypes, (contentType) => _.startsWith(contentType, "text/"))
271-
) {
274+
if (_.some(contentTypes, (contentType) => _.startsWith(contentType, "text/"))) {
272275
return CONTENT_KIND.TEXT;
273276
}
274277

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
/* eslint-disable */
2+
/* tslint:disable */
3+
/*
4+
* ---------------------------------------------------------------
5+
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
6+
* ## ##
7+
* ## AUTHOR: acacode ##
8+
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
9+
* ---------------------------------------------------------------
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<RequestInit, "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+
/** base url */
29+
baseUrl?: string;
30+
/** request cancellation token */
31+
cancelToken?: CancelToken;
32+
}
33+
34+
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
35+
36+
export interface ApiConfig<SecurityDataType = unknown> {
37+
baseUrl?: string;
38+
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
39+
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
40+
customFetch?: typeof fetch;
41+
}
42+
43+
export interface HttpResponse<D extends unknown, E extends unknown = unknown> extends Response {
44+
data: D;
45+
error: E;
46+
}
47+
48+
type CancelToken = Symbol | string | number;
49+
50+
export enum ContentType {
51+
Json = "application/json",
52+
FormData = "multipart/form-data",
53+
UrlEncoded = "application/x-www-form-urlencoded",
54+
Text = "text/plain",
55+
}
56+
57+
export class HttpClient<SecurityDataType = unknown> {
58+
public baseUrl: string = "http://petstore.swagger.io/api";
59+
private securityData: SecurityDataType | null = null;
60+
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
61+
private abortControllers = new Map<CancelToken, AbortController>();
62+
private customFetch = (...fetchParams: Parameters<typeof fetch>) => fetch(...fetchParams);
63+
64+
private baseApiParams: RequestParams = {
65+
credentials: "same-origin",
66+
headers: {},
67+
redirect: "follow",
68+
referrerPolicy: "no-referrer",
69+
};
70+
71+
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
72+
Object.assign(this, apiConfig);
73+
}
74+
75+
public setSecurityData = (data: SecurityDataType | null) => {
76+
this.securityData = data;
77+
};
78+
79+
protected encodeQueryParam(key: string, value: any) {
80+
const encodedKey = encodeURIComponent(key);
81+
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
82+
}
83+
84+
protected addQueryParam(query: QueryParamsType, key: string) {
85+
return this.encodeQueryParam(key, query[key]);
86+
}
87+
88+
protected addArrayQueryParam(query: QueryParamsType, key: string) {
89+
const value = query[key];
90+
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
91+
}
92+
93+
protected toQueryString(rawQuery?: QueryParamsType): string {
94+
const query = rawQuery || {};
95+
const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
96+
return keys
97+
.map((key) => (Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key)))
98+
.join("&");
99+
}
100+
101+
protected addQueryParams(rawQuery?: QueryParamsType): string {
102+
const queryString = this.toQueryString(rawQuery);
103+
return queryString ? `?${queryString}` : "";
104+
}
105+
106+
private contentFormatters: Record<ContentType, (input: any) => any> = {
107+
[ContentType.Json]: (input: any) =>
108+
input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
109+
[ContentType.Text]: (input: any) => (input !== null && typeof input !== "string" ? JSON.stringify(input) : input),
110+
[ContentType.FormData]: (input: any) =>
111+
Object.keys(input || {}).reduce((formData, key) => {
112+
const property = input[key];
113+
formData.append(
114+
key,
115+
property instanceof Blob
116+
? property
117+
: typeof property === "object" && property !== null
118+
? JSON.stringify(property)
119+
: `${property}`,
120+
);
121+
return formData;
122+
}, new FormData()),
123+
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
124+
};
125+
126+
protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
127+
return {
128+
...this.baseApiParams,
129+
...params1,
130+
...(params2 || {}),
131+
headers: {
132+
...(this.baseApiParams.headers || {}),
133+
...(params1.headers || {}),
134+
...((params2 && params2.headers) || {}),
135+
},
136+
};
137+
}
138+
139+
protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
140+
if (this.abortControllers.has(cancelToken)) {
141+
const abortController = this.abortControllers.get(cancelToken);
142+
if (abortController) {
143+
return abortController.signal;
144+
}
145+
return void 0;
146+
}
147+
148+
const abortController = new AbortController();
149+
this.abortControllers.set(cancelToken, abortController);
150+
return abortController.signal;
151+
};
152+
153+
public abortRequest = (cancelToken: CancelToken) => {
154+
const abortController = this.abortControllers.get(cancelToken);
155+
156+
if (abortController) {
157+
abortController.abort();
158+
this.abortControllers.delete(cancelToken);
159+
}
160+
};
161+
162+
public request = async <T = any, E = any>({
163+
body,
164+
secure,
165+
path,
166+
type,
167+
query,
168+
format,
169+
baseUrl,
170+
cancelToken,
171+
...params
172+
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
173+
const secureParams =
174+
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
175+
this.securityWorker &&
176+
(await this.securityWorker(this.securityData))) ||
177+
{};
178+
const requestParams = this.mergeRequestParams(params, secureParams);
179+
const queryString = query && this.toQueryString(query);
180+
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
181+
const responseFormat = format || requestParams.format;
182+
183+
return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, {
184+
...requestParams,
185+
headers: {
186+
...(requestParams.headers || {}),
187+
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
188+
},
189+
signal: cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal,
190+
body: typeof body === "undefined" || body === null ? null : payloadFormatter(body),
191+
}).then(async (response) => {
192+
const r = response as HttpResponse<T, E>;
193+
r.data = null as unknown as T;
194+
r.error = null as unknown as E;
195+
196+
const data = !responseFormat
197+
? r
198+
: await response[responseFormat]()
199+
.then((data) => {
200+
if (r.ok) {
201+
r.data = data;
202+
} else {
203+
r.error = data;
204+
}
205+
return r;
206+
})
207+
.catch((e) => {
208+
r.error = e;
209+
return r;
210+
});
211+
212+
if (cancelToken) {
213+
this.abortControllers.delete(cancelToken);
214+
}
215+
216+
if (!response.ok) throw data;
217+
return data;
218+
});
219+
};
220+
}
221+
222+
/**
223+
* @title Swagger Petstore
224+
* @version 1.0.0
225+
* @license MIT
226+
* @termsOfService http://swagger.io/terms/
227+
* @baseUrl http://petstore.swagger.io/api
228+
* @contact Swagger API Team
229+
*
230+
* A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification
231+
*/
232+
export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDataType> {
233+
wrongPathParams1 = {
234+
/**
235+
* @description DDD
236+
*
237+
* @tags key, delete
238+
* @name WrongPathParams1
239+
* @request DELETE:/wrong-path-params1/{pathParam1}/{path_param2}/{path_param3}/:pathParam4
240+
*/
241+
wrongPathParams1: (
242+
pathParam1: string,
243+
pathParam2: string,
244+
pathParam3: string,
245+
pathParam4: string,
246+
params: RequestParams = {},
247+
) =>
248+
this.request<void, any>({
249+
path: `/wrong-path-params1/${encodeURIComponent(pathParam1)}/${encodeURIComponent(
250+
pathParam2,
251+
)}/${encodeURIComponent(pathParam3)}/${encodeURIComponent(pathParam4)}`,
252+
method: "DELETE",
253+
...params,
254+
}),
255+
};
256+
}

0 commit comments

Comments
 (0)