From 33835457c9e3ab711285b08a712e8f0cbeb9cd58 Mon Sep 17 00:00:00 2001 From: Murat Corlu Date: Mon, 2 Sep 2019 17:39:16 +0200 Subject: [PATCH] feat: Response.format method implemented --- README.md | 1 + src/lambda-wrapper.js | 6 +- src/response.js | 62 ++++++++++++++++++++ src/response.spec.js | 128 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 196 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e95587..7b28853 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ Methods: | Method | Notes | |-------------|-------| | [get()](https://expressjs.com/en/4x/api.html#res.get) | - | +| [format()](https://expressjs.com/en/4x/api.html#res.format) | Doesn't support shorthand mime-types | | [set()](https://expressjs.com/en/4x/api.html#res.set) | Only supports `key, value` parameters | | [send()](https://expressjs.com/en/4x/api.html#res.send) | Only supports string values | | [status()](https://expressjs.com/en/4x/api.html#res.status) | - | diff --git a/src/lambda-wrapper.js b/src/lambda-wrapper.js index 7ab811f..c6e8e7e 100644 --- a/src/lambda-wrapper.js +++ b/src/lambda-wrapper.js @@ -5,11 +5,15 @@ exports.use = (...handlers) => { return (event, context, callback) => { const request = new Request(event); const response = new Response(request, callback); + request.res = response; handlers = [].concat(...handlers); handlers.reduce((chain, handler) => chain.then( - () => new Promise((resolve) => handler(request, response, resolve)) + () => new Promise((resolve) => { + request.next = resolve; + return handler(request, response, resolve); + }) ).catch(callback), Promise.resolve()); } } diff --git a/src/response.js b/src/response.js index 4796f4a..0f0d4d3 100644 --- a/src/response.js +++ b/src/response.js @@ -34,6 +34,68 @@ class Response { return this.responseObj.headers[key.toLowerCase()]; } + /** + * Performs content-negotiation on the Accept HTTP header + * on the request object, when present. It uses `req.accepts()` + * to select a handler for the request, based on the acceptable + * types ordered by their quality values. If the header is not + * specified, the first callback is invoked. When no match is + * found, the server responds with 406 “Not Acceptable”, or invokes + * the `default` callback. + * + * The `Content-Type` response header is set when a callback is + * selected. However, you may alter this within the callback using + * methods such as `res.set()` or `res.type()`. + * + * The following example would respond with `{ "message": "hey" }` + * when the `Accept` header field is set to “application/json” + * or “*\/json” (however if it is “*\/*”, then the response will + * be “hey”). + * + * res.format({ + * 'text/plain': function(){ + * res.send('hey'); + * }, + * + * 'text/html': function(){ + * res.send('

hey

'); + * }, + * + * 'appliation/json': function(){ + * res.send({ message: 'hey' }); + * } + * }); + * + * By default it passes an `Error` + * with a `.status` of 406 to `next(err)` + * if a match is not made. If you provide + * a `.default` callback it will be invoked + * instead. + * + * @param {Object} obj + * @return {ServerResponse} for chaining + * @public + */ + format(obj) { + const defaultFn = obj.default; + const types = Object.keys(obj); + const chosenType = this.req.accepts(types); + + if (chosenType) { + this.type(chosenType); + obj[chosenType](this.req, this, this.req.next); + } else if (defaultFn) { + return defaultFn(this.req, this, this.req.next); + } else { + var err = new Error('Not Acceptable'); + err.status = err.statusCode = 406; + err.types = types; + this.req.next(err); + } + + return this; + } + /** * Sends a JSON response. This method sends a response (with the correct content-type) that is the parameter converted to a JSON string using JSON.stringify(). * diff --git a/src/response.spec.js b/src/response.spec.js index a8254fb..1b5e1a4 100644 --- a/src/response.spec.js +++ b/src/response.spec.js @@ -1,4 +1,5 @@ const { Response } = require('./response'); +const { Request } = require('./request'); describe('Response object', () => { it('set response status properly', () => { @@ -84,5 +85,132 @@ describe('Response object', () => { response.type('text/xml').end(); }); + + describe('should send correct response via accept header', () => { + it('with regular header', () => { + + const event = { + headers: { + 'Accept': 'text/xml', + 'Content-Length': 0 + }, + multiValueHeaders: {}, + httpMethod: 'POST', + isBase64Encoded: false, + path: '/path', + pathParameters: { }, + queryStringParameters: { }, + multiValueQueryStringParameters: { }, + stageVariables: { }, + requestContext: {}, + resource: '' + }; + + const req = new Request(event); + req.next = (error) => { + + }; + + const cb = (err, response) => { + expect(response.statusCode).toBe(200); + expect(response.headers['content-type']).toBe('text/xml'); + expect(response.body).toBe(''); + }; + + const response = new Response(req, cb); + + response.format({ + 'application/json': (req, res, next) => { + res.json({a: 1}); + }, + 'text/xml': (req, res, next) => { + res.send(''); + } + }); + }); + + it('without regular header', () => { + + const event = { + headers: { + 'Accept': 'text/html', + 'Content-Length': 0 + }, + multiValueHeaders: {}, + httpMethod: 'POST', + isBase64Encoded: false, + path: '/path', + pathParameters: { }, + queryStringParameters: { }, + multiValueQueryStringParameters: { }, + stageVariables: { }, + requestContext: {}, + resource: '' + }; + + const req = new Request(event); + req.next = (error) => { + + }; + + const cb = (err, response) => { + expect(response.statusCode).toBe(200); + expect(response.headers['content-type']).toBe('text/html'); + expect(response.body).toBe('

hi

'); + }; + + const response = new Response(req, cb); + + response.format({ + 'application/json': (req, res, next) => { + res.json({a: 1}); + }, + 'text/xml': (req, res, next) => { + res.send(''); + }, + 'default': (req, res, next) => { + res.type('text/html').send('

hi

'); + } + }); + }); + + + it('with non acceptable accept header', () => { + + const event = { + headers: { + 'Accept': 'image/jpeg', + 'Content-Length': 0 + }, + multiValueHeaders: {}, + httpMethod: 'POST', + isBase64Encoded: false, + path: '/path', + pathParameters: { }, + queryStringParameters: { }, + multiValueQueryStringParameters: { }, + stageVariables: { }, + requestContext: {}, + resource: '' + }; + + const req = new Request(event); + req.next = (error) => { + expect(error.status).toBe(406); + expect(error).toEqual(Error('Not Acceptable')); + }; + + const cb = (err, response) => { + }; + + const response = new Response(req, cb); + + response.format({ + 'application/json': (req, res, next) => { + res.json({a: 1}); + } + }); + }); + }) });