Skip to content

Commit

Permalink
Add support for "file" return args
Browse files Browse the repository at this point in the history
Allow users to specify { type: 'file', root: true } for the single
argument that will be sent directly as the response body.

The following values are supported for file the file argument:
 - String
 - Buffer
 - ReadableStream (anything that exposes .pipe() method)
  • Loading branch information
bajtos committed Feb 26, 2016
1 parent 588740b commit a85068e
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 4 deletions.
20 changes: 18 additions & 2 deletions lib/http-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ var assert = require('assert');
var ContextBase = require('./context-base');
var Dynamic = require('./dynamic');
var js2xmlparser = require('js2xmlparser');
var SharedMethod = require('./shared-method');

var DEFAULT_SUPPORTED_TYPES = [
'application/json', 'application/javascript', 'application/xml',
'text/javascript', 'text/xml',
Expand Down Expand Up @@ -405,6 +407,9 @@ HttpContext.prototype.setReturnArgByName = function(name, value) {
}

if (returnDesc.root) {
// TODO(bajtos) call SharedMethod's convertToBasicRemotingType here?
this.resultType = typeof returnDesc.type === 'string' ?
returnDesc.type.toLowerCase() : returnDesc.type;
return;
}

Expand Down Expand Up @@ -637,13 +642,24 @@ HttpContext.prototype.done = function(cb) {
res.header('Content-Type', operationResults.contentType);
}
if (dataExists) {
operationResults.sendBody(res, data, method);
if (this.resultType !== 'file') {
operationResults.sendBody(res, data, method);
res.end();
} else if (Buffer.isBuffer(data) || typeof(data) === 'string') {
res.end(data);
} else if (data.pipe) {
data.pipe(res);
} else {
var valueType = SharedMethod.getType(data);
var msg = 'Cannot create a file response from "' + valueType + '"';
return cb(new Error(msg));
}
} else {
if (res.statusCode === undefined || res.statusCode === 200) {
res.statusCode = 204;
}
res.end();
}

res.end();
cb();
};
17 changes: 15 additions & 2 deletions lib/shared-method.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ var assert = require('assert');
* either a single argument as an object or an ordered set of arguments as an array.
* The `err` argument is assumed; do not specify. NOTE: Can have the same properties as
* `accepts`, except for `http.target`.
*
* Additionally, one of the callback arguments can have `type: 'file'` and
* `root:true`, in which case this argument is sent in the raw form as
* a response body. Allowed values: `String`, `Buffer` or `ReadableStream`
* @property {Boolean} [shared] Whether the method is shared. Default is `true`.
* @property {Number} [status] The default status code.
* @end
Expand Down Expand Up @@ -390,6 +394,7 @@ function convertToBasicRemotingType(type) {
case 'boolean':
case 'buffer':
case 'object':
case 'file':
case 'any':
return type;
case 'array':
Expand Down Expand Up @@ -531,16 +536,24 @@ SharedMethod.toResult = function(returns, raw, ctx) {
}

if (item.root) {
result = convert(raw[index]);
var isFile = convertToBasicRemotingType(item.type) === 'file';
result = isFile ? raw[index] : convert(raw[index]);
return false;
}

return true;
});

returns.forEach(function(item, index) {
var value = convert(raw[index]);
var name = item.name || item.arg;
if (convertToBasicRemotingType(item.type) === 'file') {
console.warn('%s: discarded non-root return argument %s of type "file"',
this.stringName,
name);
return;
}

var value = convert(raw[index]);
result[name] = value;
});

Expand Down
67 changes: 67 additions & 0 deletions test/rest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var request = require('supertest');
var expect = require('chai').expect;
var factory = require('./helpers/shared-objects-factory.js');
var Promise = global.Promise || require('bluebird');
var Readable = require('stream').Readable;

var ACCEPT_XML_OR_ANY = 'application/xml,*/*;q=0.8';
var TEST_ERROR = new Error('expected test error');
Expand Down Expand Up @@ -2108,6 +2109,72 @@ describe('strong-remoting-rest', function() {
});
});

describe('returns type "file"', function() {
var METHOD_SIGNATURE = {
returns: [
{ arg: 'body', type: 'file', root: true },
{ arg: 'Content-Type', type: 'string', http: { target: 'header' } },
],
};

it('should send back Buffer body', function(done) {
var method = givenSharedStaticMethod(
function(cb) { cb(null, new Buffer('some-text'), 'text/plain'); },
METHOD_SIGNATURE);

return request(app).get(method.url)
.expect(200)
.expect('Content-Type', /^text\/plain/)
.expect('some-text')
.end(done);
});

it('should send back String body', function(done) {
var method = givenSharedStaticMethod(
function(cb) { cb(null, 'some-text', 'text/plain'); },
METHOD_SIGNATURE);

return request(app).get(method.url)
.expect(200)
.expect('Content-Type', /^text\/plain/)
.expect('some-text')
.end(done);
});

it('should send back Stream body', function(done) {
var method = givenSharedStaticMethod(
function(cb) {
var stream = new Readable();
stream.push('some-text');
stream.push(null); // EOF
cb(null, stream, 'text/plain');
},
METHOD_SIGNATURE);

return request(app).get(method.url)
.expect(200)
.expect('Content-Type', /^text\/plain/)
.expect('some-text')
.end(done);
});

it('should fail for unsupported value type', function(done) {
var method = givenSharedStaticMethod(
function(cb) { cb(null, [1, 2]); },
METHOD_SIGNATURE);

return request(app).get(method.url)
.expect(500)
.expect('Content-Type', /json/)
.end(function(err, res) {
if (err) return done(err);
expect(res.body).to.have.property('error');
expect(res.body.error.message).to.match(/array/);
done();
});
});
});

it('returns correct error response body', function(done) {
function TestError() {
Error.captureStackTrace(this, TestError);
Expand Down

0 comments on commit a85068e

Please sign in to comment.