Skip to content

Commit

Permalink
feat: support form-data and urlencoded content types
Browse files Browse the repository at this point in the history
  • Loading branch information
yhnavein committed Jul 4, 2024
1 parent 1990727 commit e9f538b
Show file tree
Hide file tree
Showing 19 changed files with 288 additions and 177 deletions.
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.1/schema.json",
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"formatter": {
"enabled": true,
"formatWithErrors": false,
Expand Down
1 change: 1 addition & 0 deletions src/gen/createBarrel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { camel } from 'case';

import type { ApiOperation, ClientOptions } from '../types';
import { renderFile } from '../utils';

Expand Down
128 changes: 126 additions & 2 deletions src/gen/genOperations.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { expect } from 'chai';
import type { OpenAPIV3 as OA3 } from 'openapi-types';

import { prepareOperations, fixDuplicateOperations, getOperationName } from './genOperations';
import type { ApiOperation } from '../types';
import { getClientOptions } from '../utils';
import type { OpenAPIV3 as OA3 } from 'openapi-types';

describe('prepareOperations', () => {
const opts = getClientOptions();
Expand Down Expand Up @@ -105,7 +105,7 @@ describe('prepareOperations', () => {
expect(op2.parameters).to.deep.equal([]);
});

describe('requestBody', () => {
describe('requestBody (JSON)', () => {
it('should handle requestBody with ref type', () => {
const ops: ApiOperation[] = [
{
Expand All @@ -129,6 +129,7 @@ describe('prepareOperations', () => {

const [op1] = prepareOperations(ops, opts);
const expectedBodyParam = {
contentType: 'json',
name: 'body',
optional: false,
originalName: 'body',
Expand Down Expand Up @@ -205,6 +206,7 @@ describe('prepareOperations', () => {

const [op1] = prepareOperations(ops, opts);
const expectedBodyParam = {
contentType: 'json',
name: 'body',
optional: true,
originalName: 'body',
Expand Down Expand Up @@ -251,6 +253,7 @@ describe('prepareOperations', () => {

const [op1] = prepareOperations(ops, opts);
const expectedBodyParam = {
contentType: 'json',
name: 'petBody',
optional: false,
originalName: 'pet-body',
Expand Down Expand Up @@ -334,6 +337,127 @@ describe('prepareOperations', () => {
]);
});
});

describe('requestBody (x-www-form-urlencoded)', () => {
it('should handle requestBody with ref type', () => {
const ops: ApiOperation[] = [
{
operationId: 'createPet',
method: 'post',
path: '/pet',
requestBody: {
required: true,
content: {
'application/x-www-form-urlencoded': {
schema: {
$ref: '#/components/schemas/Pet',
},
},
},
},
responses: {},
group: null,
},
];

const [op1] = prepareOperations(ops, opts);
const expectedBodyParam = {
contentType: 'urlencoded',
name: 'body',
optional: false,
originalName: 'body',
type: 'Pet',
original: ops[0].requestBody,
};

expect(op1.body).to.deep.equal(expectedBodyParam);
expect(op1.parameters).to.deep.equal([expectedBodyParam]);
});
});

describe('requestBody (application/octet-stream)', () => {
it('should handle File request body', () => {
const ops: ApiOperation[] = [
{
operationId: 'createPet',
method: 'post',
path: '/pet',
requestBody: {
required: true,
content: {
'application/octet-stream': {
schema: {
type: 'string',
format: 'binary',
},
},
},
},
responses: {},
group: null,
},
];

const [op1] = prepareOperations(ops, opts);
const expectedBodyParam = {
contentType: 'binary',
name: 'body',
optional: false,
originalName: 'body',
type: 'File',
original: ops[0].requestBody,
};

expect(op1.body).to.deep.equal(expectedBodyParam);
expect(op1.parameters).to.deep.equal([expectedBodyParam]);
});
});

describe('requestBody (multipart/form-data)', () => {
it('should handle form data', () => {
const ops: ApiOperation[] = [
{
operationId: 'createPet',
method: 'post',
path: '/pet',
requestBody: {
required: true,
content: {
'multipart/form-data': {
schema: {
type: 'object',
properties: {
name: {
type: 'string',
},
file: {
type: 'string',
format: 'binary',
},
},
},
},
},
},
responses: {},
group: null,
},
];

const [op1] = prepareOperations(ops, opts);
const expectedBodyParam = {
contentType: 'form-data',
name: 'body',
optional: false,
originalName: 'body',
type: 'FormData',
original: ops[0].requestBody,
};

expect(op1.body).to.deep.equal(expectedBodyParam);
expect(op1.parameters).to.deep.equal([expectedBodyParam]);
});
});
});
});

Expand Down
68 changes: 49 additions & 19 deletions src/gen/genOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,6 @@ function getParams(
}));
}

export function renderOperationGroup(
group: any[],
func: any,
spec: OA3.Document,
options: ClientOptions
): string[] {
return group.map((op) => func.call(this, spec, op, options)).reduce((a, b) => a.concat(b));
}

/**
* Escapes param names to more safe form
*/
Expand All @@ -176,29 +167,63 @@ export function getParamName(name: string): string {
);
}

function getRequestBody(
reqBody: OA3.ReferenceObject | OA3.RequestBodyObject
): IOperationParam | null {
function getRequestBody(reqBody: OA3.ReferenceObject | OA3.RequestBodyObject): IBodyParam | null {
if (reqBody && 'content' in reqBody) {
const bodyContent =
reqBody.content['application/json'] ??
reqBody.content['text/json'] ??
reqBody.content['text/plain'] ??
null;
const [bodyContent, contentType] = getBestContentType(reqBody);
const isFormData = contentType === 'form-data';

if (bodyContent) {
return {
originalName: reqBody['x-name'] ?? 'body',
name: getParamName(reqBody['x-name'] ?? 'body'),
type: getParameterType(bodyContent, {}),
type: isFormData ? 'FormData' : getParameterType(bodyContent, {}),
optional: !reqBody.required,
original: reqBody,
contentType,
};
}
}
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 @@ -214,7 +239,7 @@ interface IOperation {
parameters: IOperationParam[];
query: IOperationParam[];
pathParams: IOperationParam[];
body: IOperationParam;
body: IBodyParam;
headers: IOperationParam[];
}

Expand All @@ -225,3 +250,8 @@ interface IOperationParam {
optional: boolean;
original: OA3.ParameterObject | OA3.RequestBodyObject;
}

interface IBodyParam extends IOperationParam {
contentType?: MyContentType;
}
type MyContentType = 'json' | 'urlencoded' | 'form-data' | 'binary';
Loading

0 comments on commit e9f538b

Please sign in to comment.