Skip to content

Commit dc80241

Browse files
author
svolkov
committed
BREAKING_CHANGE: fully refactored http-client.eta template; feat: image content kind; feat: request content type auto substitution
1 parent 0f6a69c commit dc80241

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+12365
-5805
lines changed

CHANGELOG.md

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

3+
Fixes:
4+
- Request content types auto substitution
5+
i.e. if request body is form data, then request body content type will be `multipart/form-data`
6+
Features:
7+
- Ability to provide custom formatting `fetch` response
8+
- `"IMAGE"` content kind for response\request data objects
9+
10+
BREAKING_CHANGES:
11+
- Fully refactored `http-client.eta` template, make it more flexible and simpler.
12+
13+
Internal:
14+
- Changed templates:
15+
- `http-client.eta`
16+
- `procedure-call.eta`
17+
18+
This version works with previous templates.
19+
320
# 4.4.0
421

522
Fixes:

src/routes.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,14 @@ const getRequestInfoTypes = (requestInfos, parsedSchemas, operationId) =>
8383
_.reduce(
8484
requestInfos,
8585
(acc, requestInfo, status) => {
86+
const contentTypes = getContentTypes([requestInfo]);
87+
8688
return [
8789
...acc,
8890
{
8991
...(requestInfo || {}),
90-
contentTypes: getContentTypes([requestInfo]),
92+
contentTypes: contentTypes,
93+
contentKind: getContentKind(contentTypes),
9194
type: getTypeFromRequestInfo(requestInfo, parsedSchemas, operationId),
9295
description: formatDescription(requestInfo.description || "", true),
9396
status: _.isNaN(+status) ? status : +status,
@@ -280,6 +283,7 @@ const CONTENT_KIND = {
280283
JSON: "JSON",
281284
URL_ENCODED: "URL_ENCODED",
282285
FORM_DATA: "FORM_DATA",
286+
IMAGE: "IMAGE",
283287
OTHER: "OTHER",
284288
};
285289

@@ -296,6 +300,10 @@ const getContentKind = (contentTypes) => {
296300
return CONTENT_KIND.FORM_DATA;
297301
}
298302

303+
if (_.some(contentTypes, (contentType) => _.includes(contentType, "image/"))) {
304+
return CONTENT_KIND.IMAGE;
305+
}
306+
299307
return CONTENT_KIND.OTHER;
300308
};
301309

@@ -464,6 +472,7 @@ const parseRoutes = ({ usageSchema, parsedSchemas, moduleNameIndex, extractReque
464472
: "params",
465473
optional: true,
466474
type: "RequestParams",
475+
defaultValue: "{}",
467476
},
468477
};
469478

templates/default/http-client.eta

Lines changed: 96 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,32 @@
22
const { apiConfig, generateResponses } = it;
33
%>
44

5-
export type RequestParams = Omit<RequestInit, "body" | "method"> & {
6-
secure?: boolean;
5+
export type QueryParamsType = Record<string | number, any>;
6+
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
7+
8+
export interface FullRequestParams extends Omit<RequestInit, "body"> {
9+
/** set parameter to `true` for call `securityWorker` for this request */
10+
secure?: boolean;
11+
/** request path */
12+
path: string;
13+
/** content type of request body */
14+
type?: ContentType;
15+
/** query params */
16+
query?: QueryParamsType;
17+
/** format of response (i.e. response.json() -> format: "json") */
18+
format?: keyof Omit<Body, "body" | "bodyUsed">;
19+
/** request body */
20+
body?: unknown;
21+
/** base url */
22+
baseUrl?: string;
723
}
824

9-
export type RequestQueryParamsType = Record<string | number, any>;
25+
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">
1026

1127

1228
interface ApiConfig<<%~ apiConfig.generic.map(g => g.name).join(', ') %>> {
1329
baseUrl?: string;
14-
baseApiParams?: RequestParams;
30+
baseApiParams?: Omit<RequestParams, "baseUrl">;
1531
securityWorker?: (securityData: SecurityDataType) => RequestParams;
1632
}
1733

@@ -28,10 +44,10 @@ interface HttpResponse<D extends unknown, E extends unknown = unknown> extends R
2844
error: E;
2945
}
3046

31-
enum BodyType {
32-
Json,
33-
FormData,
34-
UrlEncoded,
47+
export enum ContentType {
48+
Json = "application/json",
49+
FormData = "multipart/form-data",
50+
UrlEncoded = "application/x-www-form-urlencoded",
3551
}
3652

3753
export class HttpClient<<%~ apiConfig.generic.map(g => `${g.name} = unknown`).join(', ') %>> {
@@ -42,7 +58,7 @@ export class HttpClient<<%~ apiConfig.generic.map(g => `${g.name} = unknown`).jo
4258
private baseApiParams: RequestParams = {
4359
credentials: 'same-origin',
4460
headers: {
45-
'Content-Type': 'application/json'
61+
'Content-Type': ContentType.Json
4662
},
4763
redirect: 'follow',
4864
referrerPolicy: 'no-referrer',
@@ -56,91 +72,103 @@ export class HttpClient<<%~ apiConfig.generic.map(g => `${g.name} = unknown`).jo
5672
this.securityData = data
5773
}
5874

59-
private addQueryParam(query: RequestQueryParamsType, key: string) {
60-
return encodeURIComponent(key) + "=" + encodeURIComponent(Array.isArray(query[key]) ? query[key].join(",") : query[key])
75+
private addQueryParam(query: QueryParamsType, key: string) {
76+
const value = query[key];
77+
78+
return (
79+
encodeURIComponent(key) + "=" + encodeURIComponent(
80+
Array.isArray(value) ? value.join(",") :
81+
typeof value === "number" ? value :
82+
`${value}`
83+
)
84+
);
6185
}
6286

63-
protected toQueryString(rawQuery?: RequestQueryParamsType): string {
87+
protected toQueryString(rawQuery?: QueryParamsType): string {
6488
const query = rawQuery || {};
6589
const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
6690
return keys
6791
.map((key) =>
6892
typeof query[key] === "object" && !Array.isArray(query[key])
69-
? this.toQueryString(query[key] as object)
93+
? this.toQueryString(query[key] as QueryParamsType)
7094
: this.addQueryParam(query, key),
7195
)
7296
.join("&");
7397
}
7498

75-
protected addQueryParams(rawQuery?: RequestQueryParamsType): string {
99+
protected addQueryParams(rawQuery?: QueryParamsType): string {
76100
const queryString = this.toQueryString(rawQuery);
77101
return queryString ? `?${queryString}` : "";
78102
}
79103

80-
private bodyFormatters: Record<BodyType, (input: any) => any> = {
81-
[BodyType.Json]: JSON.stringify,
82-
[BodyType.FormData]: (input: any) =>
104+
private contentFormatters: Record<ContentType, (input: any) => any> = {
105+
[ContentType.Json]: JSON.stringify,
106+
[ContentType.FormData]: (input: any) =>
83107
Object.keys(input).reduce((data, key) => {
84108
data.append(key, input[key]);
85109
return data;
86110
}, new FormData()),
87-
[BodyType.UrlEncoded]: (input: any) => this.toQueryString(input),
111+
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
88112
}
89113

90-
private mergeRequestOptions(params: RequestParams, securityParams?: RequestParams): RequestParams {
114+
private mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
91115
return {
92116
...this.baseApiParams,
93-
...params,
94-
...(securityParams || {}),
117+
...params1,
118+
...(params2 || {}),
95119
headers: {
96120
...(this.baseApiParams.headers || {}),
97-
...(params.headers || {}),
98-
...((securityParams && securityParams.headers) || {})
99-
}
100-
}
121+
...(params1.headers || {}),
122+
...((params2 && params2.headers) || {}),
123+
},
124+
};
101125
}
102-
103-
private safeParseResponse = <T = any, E = any>(response: Response): Promise<HttpResponse<T, E>> => {
104-
const r = response as HttpResponse<T, E>;
105-
r.data = null as unknown as T;
106-
r.error = null as unknown as E;
107-
108-
return response
109-
.json()
110-
.then((data) => {
111-
if (r.ok) {
112-
r.data = data;
113-
} else {
114-
r.error = data;
115-
}
116-
return r;
117-
})
118-
.catch((e) => {
119-
r.error = e;
120-
return r;
121-
});
122-
}
123-
124-
public request = <T = any, E = any>(
125-
path: string,
126-
method: string,
127-
{ secure, ...params }: RequestParams = {},
128-
body?: any,
129-
bodyType?: BodyType,
130-
secureByDefault?: boolean,
131-
): <% if (generateResponses) { %>TPromise<HttpResponse<T, E>><% } else { %>Promise<HttpResponse<T>><% } %> => {
132-
const requestUrl = `${this.baseUrl}${path}`;
133-
const secureOptions = (secureByDefault || secure) && this.securityWorker ? this.securityWorker(this.securityData) : {};
134-
const requestOptions = {
135-
...this.mergeRequestOptions(params, secureOptions),
136-
method,
137-
body: body ? this.bodyFormatters[bodyType || BodyType.Json](body) : null,
126+
127+
public request = <T = any, E = any>({
128+
body,
129+
secure,
130+
path,
131+
type = ContentType.Json,
132+
query,
133+
format = "json",
134+
baseUrl,
135+
...params
136+
}: FullRequestParams): <% if (generateResponses) { %>TPromise<HttpResponse<T, E>><% } else { %>Promise<HttpResponse<T, E>><% } %> => {
137+
const secureParams = (secure && this.securityWorker) ? this.securityWorker(this.securityData) : {};
138+
const requestParams = this.mergeRequestParams(params, secureParams);
139+
const queryString = query && this.toQueryString(query);
140+
141+
return fetch(
142+
`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`,
143+
{
144+
headers: {
145+
"Content-Type": type,
146+
...(requestParams.headers || {}),
147+
},
148+
...requestParams,
149+
body: body ? (this.contentFormatters[type] ? this.contentFormatters[type](body) : body) : null,
138150
}
139-
140-
return fetch(requestUrl, requestOptions).then(async response => {
141-
const data = await this.safeParseResponse<T, E>(response);
142-
if (!response.ok) throw data
143-
return data
144-
})
145-
}
151+
).then(async (response) => {
152+
const r = response as HttpResponse<T, E>;
153+
r.data = (null as unknown) as T;
154+
r.error = (null as unknown) as E;
155+
156+
const data = await response[format]()
157+
.then((data) => {
158+
if (r.ok) {
159+
r.data = data;
160+
} else {
161+
r.error = data;
162+
}
163+
return r;
164+
})
165+
.catch((e) => {
166+
r.error = e;
167+
return r;
168+
});
169+
170+
if (!response.ok) throw data;
171+
return data;
172+
});
173+
};
146174
}

templates/default/procedure-call.eta

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
<%
22
const { utils, route, config } = it;
3-
const { requestBodyInfo } = route;
3+
const { requestBodyInfo, responseBodyInfo } = route;
44
const { _, getInlineParseContent, getParseContent, parseSchema, getComponentByRef, require } = utils;
55
const { parameters, path, method, payload, params, query, formData, security, requestParams } = route.request;
66
const { type, errorType, contentTypes } = route.response;
77
const routeDocs = includeFile("./route-docs", { config, route, utils });
88
const queryName = (query && query.name) || "query";
99
const pathParams = _.values(parameters);
1010

11-
const argToTmpl = ({ name, optional, type }) => `${name}${optional ? '?' : ''}: ${type}`;
11+
const argToTmpl = ({ name, optional, type, defaultValue }) => `${name}${!defaultValue && optional ? '?' : ''}: ${type}${defaultValue ? ` = ${defaultValue}` : ''}`;
1212

1313
const rawWrapperArgs = config.extractRequestParams ?
1414
_.compact([
@@ -34,26 +34,25 @@ const wrapperArgs = _
3434
.map(argToTmpl)
3535
.join(', ')
3636

37+
// RequestParams["type"]
3738
const requestContentKind = {
38-
"JSON": "BodyType.Json",
39-
"URL_ENCODED": "BodyType.UrlEncoded",
40-
"FORM_DATA": "BodyType.FormData"
39+
"JSON": "ContentType.Json",
40+
"URL_ENCODED": "ContentType.UrlEncoded",
41+
"FORM_DATA": "ContentType.FormData",
42+
}
43+
// RequestParams["format"]
44+
const responseContentKind = {
45+
"JSON": '"json"',
46+
"IMAGE": '"blob"',
47+
"FORM_DATA": '"formData"'
4148
}
4249

43-
const bodyModeTmpl = requestContentKind[requestBodyInfo.contentKind] || (security && requestContentKind.JSON) || null
44-
const securityTmpl = security ? 'true' : null
45-
const pathTmpl = query != null
46-
? `\`${path}\${this.addQueryParams(${queryName})}\``
47-
: `\`${path}\``
48-
const requestArgs = [pathTmpl, `'${_.upperCase(method)}'`, _.get(params, "name"), _.get(payload, "name"), bodyModeTmpl, securityTmpl]
49-
.reverse()
50-
.reduce((args, arg) => {
51-
if (args.length === 0 && !arg) return args
52-
args.push(arg ? arg : 'null')
53-
return args
54-
}, [])
55-
.reverse()
56-
.join(', ')
50+
const bodyTmpl = _.get(payload, "name") || null;
51+
const queryTmpl = (query != null && queryName) || null;
52+
const bodyContentKindTmpl = requestContentKind[requestBodyInfo.contentKind] || null;
53+
const responseFormatTmpl = responseContentKind[responseBodyInfo.success && responseBodyInfo.success.schema && responseBodyInfo.success.schema.contentKind] || null;
54+
const securityTmpl = security ? 'true' : null;
55+
5756
%>
5857
/**
5958
<%~ routeDocs.description %>
@@ -64,4 +63,13 @@ const requestArgs = [pathTmpl, `'${_.upperCase(method)}'`, _.get(params, "name")
6463

6564
*/
6665
<%~ route.routeName.usage %><%~ route.namespace ? ': ' : ' = ' %>(<%~ wrapperArgs %>) =>
67-
this.request<<%~ type %>, <%~ errorType %>>(<%~ requestArgs %>)<%~ route.namespace ? ',' : '' %>
66+
this.request<<%~ type %>, <%~ errorType %>>({
67+
path: `<%~ path %>`,
68+
method: '<%~ _.upperCase(method) %>',
69+
<%~ queryTmpl ? `query: ${queryTmpl},` : '' %>
70+
<%~ bodyTmpl ? `body: ${bodyTmpl},` : '' %>
71+
<%~ securityTmpl ? `secure: ${securityTmpl},` : '' %>
72+
<%~ bodyContentKindTmpl ? `type: ${bodyContentKindTmpl},` : '' %>
73+
<%~ responseFormatTmpl ? `format: ${responseFormatTmpl},` : '' %>
74+
...<%~ _.get(params, "name") %>,
75+
})<%~ route.namespace ? ',' : '' %>

templates/modular/api.eta

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const apiClassName = classNameCase(route.moduleName);
55
const routes = route.routes;
66
const dataContracts = _.map(modelTypes, "name");
77
%>
8-
import { HttpClient, RequestParams, BodyType } from "./<%~ config.fileNames.httpClient %>";
8+
import { HttpClient, RequestParams, ContentType } from "./<%~ config.fileNames.httpClient %>";
99
<% if (dataContracts.length) { %>
1010
import { <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %>"
1111
<% } %>

0 commit comments

Comments
 (0)