Skip to content

Commit

Permalink
feat(openapi-v3): add sugar decorators for file requestBody/response
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Mar 16, 2020
1 parent eafc9b9 commit e8c8f38
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 10 deletions.
Expand Up @@ -4,7 +4,7 @@
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';
import {getControllerSpec, requestBody, post} from '../../../../';
import {getControllerSpec, post, requestBody} from '../../../../';

describe('requestBody decorator - shortcuts', () => {
context('array', () => {
Expand All @@ -13,10 +13,7 @@ describe('requestBody decorator - shortcuts', () => {
class MyController {
@post('/greeting')
greet(
@requestBody.array(
{type: 'string'},
{description: description, required: false},
)
@requestBody.array({type: 'string'}, {description, required: false})
name: string[],
) {}
}
Expand All @@ -33,10 +30,41 @@ describe('requestBody decorator - shortcuts', () => {

const requestBodySpec = actualSpec.paths['/greeting']['post'].requestBody;
expect(requestBodySpec).to.have.properties({
description: description,
description,
required: false,
content: expectedContent,
});
});
});

context('file', () => {
it('generates the correct schema spec for a file argument', () => {
const description = 'a picture';
class MyController {
@post('/pictures')
upload(
@requestBody.file({description, required: true})
request: unknown, // It should be `Request` from `@loopback/rest`
) {}
}

const actualSpec = getControllerSpec(MyController);
const expectedContent = {
'multipart/form-data': {
'x-parser': 'stream',
schema: {
type: 'object',
properties: {file: {type: 'string', format: 'binary'}},
},
},
};

const requestBodySpec = actualSpec.paths['/pictures']['post'].requestBody;
expect(requestBodySpec).to.have.properties({
description,
required: true,
content: expectedContent,
});
});
});
});
Expand Up @@ -7,7 +7,7 @@ import {Model, model, property} from '@loopback/repository';
import {expect} from '@loopback/testlab';
import * as httpStatus from 'http-status';
import {ResponseObject} from 'openapi3-ts';
import {get, getControllerSpec, oas} from '../../..';
import {get, getControllerSpec, oas, param} from '../../..';

describe('@oas.response decorator', () => {
it('allows a class to not be decorated with @oas.response at all', () => {
Expand Down Expand Up @@ -201,4 +201,53 @@ describe('@oas.response decorator', () => {
).to.eql({$ref: '#/components/schemas/SuccessModel'});
});
});

context('@oas.response.file', () => {
it('allows @oas.response.file with media types', () => {
class MyController {
@get('/files/{filename}')
@oas.response.file('image/jpeg', 'image/png')
download(@param.path.string('filename') fileName: string) {
// use response.download(...);
}
}

const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/files/{filename}'].get.responses['200']).to.eql(
{
description: 'The file content',
content: {
'image/jpeg': {
schema: {type: 'string', format: 'binary'},
},
'image/png': {
schema: {type: 'string', format: 'binary'},
},
},
},
);
});

it('allows @oas.response.file without media types', () => {
class MyController {
@get('/files/{filename}')
@oas.response.file()
download(@param.path.string('filename') filename: string) {
// use response.download(...);
}
}

const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/files/{filename}'].get.responses['200']).to.eql(
{
description: 'The file content',
content: {
'application/octet-stream': {
schema: {type: 'string', format: 'binary'},
},
},
},
);
});
});
});
67 changes: 65 additions & 2 deletions packages/openapi-v3/src/decorators/request-body.decorator.ts
Expand Up @@ -144,10 +144,10 @@ export namespace requestBody {
* @param properties - The requestBody properties other than `content`
* @param itemSpec - the full item object
*/
export const array = function(
export const array = (
itemSpec: SchemaObject | ReferenceObject,
properties?: {description?: string; required?: boolean},
) {
) => {
return requestBody({
...properties,
content: {
Expand All @@ -157,4 +157,67 @@ export namespace requestBody {
},
});
};

/**
* Define a requestBody of `file` type. This is used to support
* multipart/form-data based file upload. Use `@requestBody` for other content
* types.
*
* {@link https://swagger.io/docs/specification/describing-request-body/file-upload | OpenAPI file upload}
*
* @example
* import {Request} from '@loopback/rest';
*
* ```ts
* class MyController {
* @post('/pictures')
* upload(
* @requestBody.file()
* request: Request,
* ) {
* // ...
* }
* }
* ```
*
* @param properties - Optional description and required flag
*/
export const file = (properties?: {
description?: string;
required?: boolean;
}) => {
return requestBody({
description: 'Request body for multipart/form-data based file upload',
required: true,
content: {
// Media type for file upload
'multipart/form-data': {
// Skip body parsing
'x-parser': 'stream',
schema: {
type: 'object',
properties: {
file: {
type: 'string',
// This is required by OpenAPI spec 3.x for file upload
format: 'binary',
},
// Multiple file upload is not working with swagger-ui
// https://github.com/swagger-api/swagger-ui/issues/4600
/*
files: {
type: 'array',
items: {
type: 'string',
format: 'binary',
},
},
*/
},
},
},
},
...properties,
});
};
}
46 changes: 45 additions & 1 deletion packages/openapi-v3/src/decorators/response.decorator.ts
Expand Up @@ -41,7 +41,7 @@ function buildDecoratorReducer(
| SchemaObject
| ReferenceObject,
contentType: ct,
description,
description: m.description ?? description,
});
});
} else {
Expand Down Expand Up @@ -81,3 +81,47 @@ export function response(
{decoratorName: '@response', allowInheritance: false},
);
}

export namespace response {
/**
* Decorate the response as a file
*
* @example
* ```ts
* import {oas, get, param} from '@loopback/openapi-v3';
* import {RestBindings, Response} from '@loopback/rest';
*
* class MyController {
* @get('/files/{filename}')
* @oas.response.file('image/jpeg', 'image/png')
* download(
* @param.path.string('filename') fileName: string,
* @inject(RestBindings.Http.RESPONSE) response: Response,
* ) {
* // use response.download(...);
* }
* }
* ```
* @param mediaTypes - A list of media types for the file response. It's
* default to `['application/octet-stream']`.
*/
export const file = (...mediaTypes: string[]) => {
if (mediaTypes.length === 0) {
mediaTypes = ['application/octet-stream'];
}
const responseWithContent: ResponseWithContent = {
content: {},
description: 'The file content',
};
for (const t of mediaTypes) {
responseWithContent.content[t] = {
schema: {
type: 'string',
format: 'binary', // This is required by OpenAPI spec 3.x
},
};
}

return response(200, responseWithContent);
};
}

0 comments on commit e8c8f38

Please sign in to comment.