Skip to content

Commit

Permalink
impr: better handle response types for fetch
Browse files Browse the repository at this point in the history
fix: type error in generated code for urlencoded content type
  • Loading branch information
yhnavein committed Jul 5, 2024
1 parent e9f538b commit 9c64e39
Show file tree
Hide file tree
Showing 16 changed files with 188 additions and 140 deletions.
54 changes: 12 additions & 42 deletions src/gen/genOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { camel } from 'case';
import type { OpenAPIV3 as OA3 } from 'openapi-types';

import { getParameterType } from '../swagger';
import { groupOperationsByGroupName, getBestResponse, orderBy, renderFile } from '../utils';
import {
groupOperationsByGroupName,
getBestResponse,
orderBy,
renderFile,
type MyContentType,
getBestContentType,
} from '../utils';
import { generateBarrelFile } from './createBarrel';
import type { ApiOperation, ClientOptions } from '../types';
import { escapeReservedWords } from '../utils';
Expand Down Expand Up @@ -62,8 +69,8 @@ export function prepareOperations(
const ops = fixDuplicateOperations(operations);

return ops.map((op) => {
const responseObject = getBestResponse(op);
const returnType = getParameterType(responseObject, options);
const [respObject, responseContentType] = getBestResponse(op);
const returnType = getParameterType(respObject, options);

const body = getRequestBody(op.requestBody);
const queryParams = getParams(op.parameters as OA3.ParameterObject[], options, ['query']);
Expand All @@ -80,6 +87,7 @@ export function prepareOperations(

return {
returnType,
responseContentType,
method: op.method.toUpperCase(),
name: getOperationName(op.operationId, op.group),
url: op.path,
Expand Down Expand Up @@ -186,44 +194,6 @@ function getRequestBody(reqBody: OA3.ReferenceObject | OA3.RequestBodyObject): I
return null;
}

const orderedContentTypes = [
'application/json',
'text/json',
'text/plain',
'application/x-www-form-urlencoded',
'multipart/form-data',
];
function getBestContentType(reqBody: OA3.RequestBodyObject): [OA3.MediaTypeObject, MyContentType] {
const contentTypes = Object.keys(reqBody.content);
if (contentTypes.length === 0) {
return [null, null];
}

const firstContentType = orderedContentTypes.find((ct) => contentTypes.includes(ct));
if (firstContentType) {
const typeObject = reqBody.content[firstContentType];
const type = getContentType(firstContentType);
return [typeObject, type];
}

const typeObject = reqBody.content[contentTypes[0]];
const type = getContentType(contentTypes[0]);
return [typeObject, type];
}

function getContentType(type: string) {
if (type === 'application/x-www-form-urlencoded') {
return 'urlencoded';
}
if (type === 'multipart/form-data') {
return 'form-data';
}
if (type === 'application/octet-stream') {
return 'binary';
}
return 'json';
}

interface ClientData {
clientName: string;
camelCaseName: string;
Expand All @@ -233,6 +203,7 @@ interface ClientData {

interface IOperation {
returnType: string;
responseContentType: string;
method: string;
name: string;
url: string;
Expand All @@ -254,4 +225,3 @@ interface IOperationParam {
interface IBodyParam extends IOperationParam {
contentType?: MyContentType;
}
type MyContentType = 'json' | 'urlencoded' | 'form-data' | 'binary';
45 changes: 28 additions & 17 deletions src/utils/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ describe('getBestResponse', () => {
responses: {},
};

const res = getBestResponse(op);
const [res] = getBestResponse(op);

expect(res).to.be.equal(null);
});
Expand All @@ -271,7 +271,7 @@ describe('getBestResponse', () => {
},
};

const res = getBestResponse(op);
const [res] = getBestResponse(op);

expect(res).to.be.eql({
schema: {
Expand All @@ -280,25 +280,36 @@ describe('getBestResponse', () => {
});
});

it('handles 201 response with unsupported media type', () => {
const op: OA3.OperationObject = {
responses: {
'201': {
description: 'Success',
content: {
'application/octet-stream': {
schema: {
$ref: '#/components/schemas/TestObject',
describe('different response content types', () => {
const sampleSchema = { $ref: '#/components/schemas/TestObject' };
const testCases = [
{ contentType: 'application/json', schema: sampleSchema, expected: 'json' },
{ contentType: 'text/json', schema: sampleSchema, expected: 'json' },
{ contentType: 'application/octet-stream', schema: sampleSchema, expected: 'binary' },
{ contentType: 'text/plain', schema: sampleSchema, expected: 'text' },
{ contentType: 'something/wrong', schema: sampleSchema, expected: 'json' },
];

for (const { contentType, schema, expected } of testCases) {
it(`handles 201 ${contentType} response`, () => {
const op: OA3.OperationObject = {
responses: {
'201': {
description: 'Success',
content: {
[contentType]: {
schema,
},
},
},
},
},
},
};
};

const res = getBestResponse(op);
const [, respContentType] = getBestResponse(op);

expect(res).to.be.eql(null);
expect(respContentType).to.deep.equal(expected);
});
}
});

it('handles multiple responses', () => {
Expand Down Expand Up @@ -327,7 +338,7 @@ describe('getBestResponse', () => {
},
};

const res = getBestResponse(op);
const [res] = getBestResponse(op);

expect(res).to.be.eql({
schema: {
Expand Down
56 changes: 48 additions & 8 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,21 +109,16 @@ export function groupOperationsByGroupName(operations: ApiOperation[]) {
* Other media types are not supported at this time.
* @returns Response or reference of the success response
*/
export function getBestResponse(op: OA3.OperationObject) {
export function getBestResponse(op: OA3.OperationObject): [OA3.MediaTypeObject, MyContentType] {
const NOT_FOUND = 100000;
const lowestCode = Object.keys(op.responses).sort().shift() ?? NOT_FOUND;

const resp = lowestCode === NOT_FOUND ? op.responses[0] : op.responses[lowestCode.toString()];

if (resp && 'content' in resp) {
return (
resp.content['application/json'] ??
resp.content['text/json'] ??
resp.content['text/plain'] ??
null
);
return getBestContentType(resp);
}
return null;
return [null, null];
}

/** This method tries to fix potentially wrong out parameter given from commandline */
Expand All @@ -150,3 +145,48 @@ export function orderBy<T>(arr: T[] | null | undefined, key: string) {
}

const sortByKey = (key: string) => (a, b) => a[key] > b[key] ? 1 : b[key] > a[key] ? -1 : 0;

const orderedContentTypes = [
'application/json',
'text/json',
'text/plain',
'application/x-www-form-urlencoded',
'multipart/form-data',
];
export function getBestContentType(
reqBody: OA3.RequestBodyObject | OA3.ResponseObject
): [OA3.MediaTypeObject, MyContentType] {
const contentTypes = Object.keys(reqBody.content);
if (contentTypes.length === 0) {
return [null, null];
}

const firstContentType = orderedContentTypes.find((ct) => contentTypes.includes(ct));
if (firstContentType) {
const typeObject = reqBody.content[firstContentType];
const type = getContentType(firstContentType);
return [typeObject, type];
}

const typeObject = reqBody.content[contentTypes[0]];
const type = getContentType(contentTypes[0]);
return [typeObject, type];
}

function getContentType(type: string) {
if (type === 'application/x-www-form-urlencoded') {
return 'urlencoded';
}
if (type === 'multipart/form-data') {
return 'form-data';
}
if (type === 'application/octet-stream') {
return 'binary';
}
if (type === 'text/plain') {
return 'text';
}
return 'json';
}

export type MyContentType = 'json' | 'urlencoded' | 'form-data' | 'binary' | 'text';
2 changes: 1 addition & 1 deletion templates/axios/operation.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ $config?: AxiosRequestConfig
method: '<%= it.method %>',
<% if(it.body) { %>
<% if(it.body.contentType === 'urlencoded') { %>
data: new URLSearchParams(<%= it.body.name %>),
data: new URLSearchParams(<%= it.body.name %> as any),
<% } else { %>
data: <%= it.body.name %>,
<% } %>
Expand Down
15 changes: 11 additions & 4 deletions templates/fetch/operation.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
<% }); %>
$config?: RequestInit
): Promise<<%~ it.returnType %>> {
let url = defaults.baseUrl + '<%= it.url %>?';
let url = `${defaults.baseUrl}<%= it.url %>?`;
<% if(it.pathParams && it.pathParams.length > 0) {
it.pathParams.forEach((parameter) => { %>
url = url.replace('{<%= parameter.name %>}', encodeURIComponent("" + <%= parameter.name %>));
url = url.replace('{<%= parameter.name %>}', encodeURIComponent(<%= parameter.name %>));
<% });
} %>
<% if(it.query && it.query.length > 0) { %>
Expand All @@ -33,7 +33,7 @@ $config?: RequestInit
<% if(it.body.contentType === 'binary') { %>
body: <%= it.body.name %>,
<% } else if(it.body.contentType === 'urlencoded') { %>
body: new URLSearchParams(<%= it.body.name %>),
body: new URLSearchParams(<%= it.body.name %> as any),
<% } else { %>
body: JSON.stringify(<%= it.body.name %>),
<% } %>
Expand All @@ -46,5 +46,12 @@ $config?: RequestInit
},
<% } %>
...$config,
}).then((response) => response.json() as Promise<<%~ it.returnType %>>);
})
<% if(it.responseContentType === 'binary') { %>
.then((response) => response.blob() as Promise<<%~ it.returnType %>>);
<% } else if(it.responseContentType === 'text') { %>
.then((response) => response.text() as Promise<<%~ it.returnType %>>);
<% } else { %>
.then((response) => response.json() as Promise<<%~ it.returnType %>>);
<% } %>
},
2 changes: 1 addition & 1 deletion templates/ng1/operation.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
url,
<% if(['POST', 'PUT', 'PATCH'].includes(it.method)) { %>
<% if(it.body) { %>
<%= it.body.contentType === 'urlencoded' ? 'new URLSearchParams(' + it.body.name + ')' : it.body.name %>,
<%= it.body.contentType === 'urlencoded' ? 'new URLSearchParams(' + it.body.name + ' as any)' : it.body.name %>,
<% } else { %>
null,
<% } %>
Expand Down
2 changes: 1 addition & 1 deletion templates/ng2/operation.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ config?: any
url,
<% if(['POST', 'PUT', 'PATCH'].includes(it.method)) { %>
<% if(it.body) { %>
<%= it.body.contentType === 'urlencoded' ? 'new URLSearchParams(' + it.body.name + ')' : it.body.name %>,
<%= it.body.contentType === 'urlencoded' ? 'new URLSearchParams(' + it.body.name + ' as any)' : it.body.name %>,
<% } else { %>
null,
<% } %>
Expand Down
2 changes: 1 addition & 1 deletion templates/swr-axios/operation.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
method: '<%= it.method %>',
<% if(it.body) { %>
<% if(it.body.contentType === 'urlencoded') { %>
data: new URLSearchParams(<%= it.body.name %>),
data: new URLSearchParams(<%= it.body.name %> as any),
<% } else { %>
data: <%= it.body.name %>,
<% } %>
Expand Down
2 changes: 1 addition & 1 deletion templates/xior/operation.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ $config?: XiorRequestConfig
method: '<%= it.method %>',
<% if(it.body) { %>
<% if(it.body.contentType === 'urlencoded') { %>
data: new URLSearchParams(<%= it.body.name %>),
data: new URLSearchParams(<%= it.body.name %> as any),
<% } else { %>
data: <%= it.body.name %>,
<% } %>
Expand Down
5 changes: 3 additions & 2 deletions test/petstore-v3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,10 @@ paths:
'200':
description: successful operation
content:
application/json:
application/octet-stream:
schema:
$ref: '#/components/schemas/ApiResponse'
type: string
format: binary
security:
- petstore_auth:
- 'write:pets'
Expand Down
6 changes: 3 additions & 3 deletions test/snapshots/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export const petClient = {
return axios.request<Pet>({
url: url,
method: 'PUT',
data: new URLSearchParams(body),
data: new URLSearchParams(body as any),
...$config,
});
},
Expand Down Expand Up @@ -154,11 +154,11 @@ export const petClient = {
petId: number ,
additionalMetadata: string | null | undefined,
$config?: AxiosRequestConfig
): AxiosPromise<ApiResponse> {
): AxiosPromise<File> {
let url = '/pet/{petId}/uploadImage';
url = url.replace('{petId}', encodeURIComponent("" + petId));

return axios.request<ApiResponse>({
return axios.request<File>({
url: url,
method: 'POST',
data: body,
Expand Down
Loading

0 comments on commit 9c64e39

Please sign in to comment.