From 2a8feb5eaf7331bbddb2d3dc5044c30b0d5c4294 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Wed, 2 Jul 2014 10:23:32 -0700 Subject: [PATCH 01/22] Added support for automatically converting to XML, if requested via Accept --- .gitignore | 1 + ext/swagger.js | 3 ++- lib/http-context.js | 14 +++++++++++++- package.json | 3 ++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 80a584a5..69c0dd23 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +.idea/ *.seed *.log *.csv diff --git a/ext/swagger.js b/ext/swagger.js index a11d2486..b1f2602b 100644 --- a/ext/swagger.js +++ b/ext/swagger.js @@ -173,7 +173,8 @@ function routeToAPI(route) { parameters: route.accepts ? route.accepts.map(acceptToParameter(route)) : [], errorResponses: [], // TODO(schoon) - We don't have descriptions for this yet. summary: route.description, // TODO(schoon) - Excerpt? - notes: '' // TODO(schoon) - `description` metadata? + notes: '', // TODO(schoon) - `description` metadata? + produces: ['application/json', 'application/xml'] }] }; } diff --git a/lib/http-context.js b/lib/http-context.js index a0a09430..6aa7f847 100644 --- a/lib/http-context.js +++ b/lib/http-context.js @@ -14,7 +14,8 @@ var EventEmitter = require('events').EventEmitter , inherits = util.inherits , assert = require('assert') , Dynamic = require('./dynamic') - , SUPPORTED_TYPES = ['json', 'application/javascript', 'text/javascript']; + , xml2js = require('xml2js') + , SUPPORTED_TYPES = ['*/*', 'json', 'application/javascript', 'text/javascript', 'xml']; /** * Create a new `HttpContext` with the given `options`. @@ -258,6 +259,7 @@ HttpContext.prototype.done = function () { if(dataExists) { switch(accepts) { + case '*/*': case 'json': res.json(data); break; @@ -265,6 +267,16 @@ HttpContext.prototype.done = function () { case 'text/javascript': res.jsonp(data); break; + case 'xml': + res.header('Content-Type', 'text/xml'); + if(data === null) { + res.header('Content-Length', '7'); + res.end(''); + } else { + var builder = new xml2js.Builder(); + res.send(builder.buildObject(data)); + } + break; default: // not acceptable res.send(406); diff --git a/package.json b/package.json index 3ea9613e..f923103e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "traverse": "~0.6.6", "request": "~2.36.0", "browser-request": "~0.3.1", - "qs": "~0.6.6" + "qs": "~0.6.6", + "xml2js": "~0.4.4" }, "devDependencies": { "mocha": "~1.19.0", From 5f472be5b6bbf5e70c10377449385cf7fbad7be8 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Tue, 8 Jul 2014 05:58:04 -0700 Subject: [PATCH 02/22] Better support for alternate Content-Types --- ext/swagger.js | 4 ++-- lib/http-context.js | 18 +++++++++++++++--- test/swagger.test.js | 23 ++++++++++++++++++++--- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/ext/swagger.js b/ext/swagger.js index b1f2602b..5847dcbc 100644 --- a/ext/swagger.js +++ b/ext/swagger.js @@ -16,7 +16,7 @@ function Swagger(remotes, options, models) { // Unfold options. var _options = options || {}; var name = _options.name || 'swagger'; - var version = _options.version; + var version = _options.version || '1'; var basePath = _options.basePath; // We need a temporary REST adapter to discover our available routes. @@ -174,7 +174,7 @@ function routeToAPI(route) { errorResponses: [], // TODO(schoon) - We don't have descriptions for this yet. summary: route.description, // TODO(schoon) - Excerpt? notes: '', // TODO(schoon) - `description` metadata? - produces: ['application/json', 'application/xml'] + produces: ['application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml'] }] }; } diff --git a/lib/http-context.js b/lib/http-context.js index 6aa7f847..9e94da4f 100644 --- a/lib/http-context.js +++ b/lib/http-context.js @@ -15,7 +15,12 @@ var EventEmitter = require('events').EventEmitter , assert = require('assert') , Dynamic = require('./dynamic') , xml2js = require('xml2js') - , SUPPORTED_TYPES = ['*/*', 'json', 'application/javascript', 'text/javascript', 'xml']; + , SUPPORTED_TYPES = [ + 'application/json', 'application/javascript', 'application/xml', + 'text/javascript', 'text/xml', + 'json', 'xml', + '*/*' + ]; /** * Create a new `HttpContext` with the given `options`. @@ -260,6 +265,7 @@ HttpContext.prototype.done = function () { if(dataExists) { switch(accepts) { case '*/*': + case 'application/json': case 'json': res.json(data); break; @@ -267,9 +273,15 @@ HttpContext.prototype.done = function () { case 'text/javascript': res.jsonp(data); break; + case 'application/xml': + case 'text/xml': case 'xml': - res.header('Content-Type', 'text/xml'); - if(data === null) { + if (accepts == 'application/xml') { + res.header('Content-Type', 'application/xml'); + } else { + res.header('Content-Type', 'text/xml'); + } + if (data === null) { res.header('Content-Length', '7'); res.end(''); } else { diff --git a/test/swagger.test.js b/test/swagger.test.js index 9234f4f2..66637555 100644 --- a/test/swagger.test.js +++ b/test/swagger.test.js @@ -78,13 +78,30 @@ describe('swagger definition', function() { }); }); - function getSwaggerResources(restPath) { + describe('should honor Accept:', function() { + CONTENT_TYPES = ['application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml']; + + for (var contentType in CONTENT_TYPES) { + it(CONTENT_TYPES[contentType], function (done) { + swagger(objects); + + var getReq = getSwaggerResources(null, CONTENT_TYPES[contentType]); + getReq.end(function (err, res) { + if (err) return done(err); + done(); + }); + }); + } + }); + + function getSwaggerResources(restPath, contentType) { var app = createRestApiApp(restPath); var prefix = restPath || ''; + contentType = contentType || 'application/json'; return request(app) .get(prefix + '/swagger/resources') - .set('Accept', 'application/json') - .expect('Content-Type', /json/) + .set('Accept', contentType) + .expect('Content-Type', new RegExp('^' + contentType + '.*')) .expect(200); } From ff07add39c46cccc6af2458e01a6641f929dbab4 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Tue, 8 Jul 2014 06:17:04 -0700 Subject: [PATCH 03/22] Switch from xml2js to easyxml in order to avoid bugs --- lib/http-context.js | 5 ++--- package.json | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/http-context.js b/lib/http-context.js index 9e94da4f..5015f830 100644 --- a/lib/http-context.js +++ b/lib/http-context.js @@ -14,7 +14,7 @@ var EventEmitter = require('events').EventEmitter , inherits = util.inherits , assert = require('assert') , Dynamic = require('./dynamic') - , xml2js = require('xml2js') + , easyXml = require('easyxml') , SUPPORTED_TYPES = [ 'application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml', @@ -285,8 +285,7 @@ HttpContext.prototype.done = function () { res.header('Content-Length', '7'); res.end(''); } else { - var builder = new xml2js.Builder(); - res.send(builder.buildObject(data)); + res.send(easyXml.render(data)); } break; default: diff --git a/package.json b/package.json index f923103e..88ee95c2 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,13 @@ "Remoting", "REST" ], - "version": "1.5.0", + "version": "1.5.1", "scripts": { "test": "mocha" }, "dependencies": { "debug": "~0.8.1", + "easyxml": "~0.0.5", "express": "~3.5.0", "eventemitter2": "~0.4.13", "cors": "~2.3.1", @@ -21,8 +22,7 @@ "traverse": "~0.6.6", "request": "~2.36.0", "browser-request": "~0.3.1", - "qs": "~0.6.6", - "xml2js": "~0.4.4" + "qs": "~0.6.6" }, "devDependencies": { "mocha": "~1.19.0", From 632a8ffd82e3106cceb0fd08f52ad4390118f7c1 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Tue, 8 Jul 2014 06:34:12 -0700 Subject: [PATCH 04/22] Switch from easyxml to js2xmlparser in order to avoid bugs --- lib/http-context.js | 4 ++-- package.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/http-context.js b/lib/http-context.js index 5015f830..4bd33772 100644 --- a/lib/http-context.js +++ b/lib/http-context.js @@ -14,7 +14,7 @@ var EventEmitter = require('events').EventEmitter , inherits = util.inherits , assert = require('assert') , Dynamic = require('./dynamic') - , easyXml = require('easyxml') + , js2xmlparser = require("js2xmlparser") , SUPPORTED_TYPES = [ 'application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml', @@ -285,7 +285,7 @@ HttpContext.prototype.done = function () { res.header('Content-Length', '7'); res.end(''); } else { - res.send(easyXml.render(data)); + res.send(js2xmlparser('response', data)); } break; default: diff --git a/package.json b/package.json index 88ee95c2..b461dfc3 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,12 @@ "Remoting", "REST" ], - "version": "1.5.1", + "version": "1.5.2", "scripts": { "test": "mocha" }, "dependencies": { "debug": "~0.8.1", - "easyxml": "~0.0.5", "express": "~3.5.0", "eventemitter2": "~0.4.13", "cors": "~2.3.1", @@ -22,7 +21,8 @@ "traverse": "~0.6.6", "request": "~2.36.0", "browser-request": "~0.3.1", - "qs": "~0.6.6" + "qs": "~0.6.6", + "js2xmlparser": "~0.1.3" }, "devDependencies": { "mocha": "~1.19.0", From d75d4694b7ad0228849392d8feec20a4dc8e42a2 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Tue, 8 Jul 2014 06:48:30 -0700 Subject: [PATCH 05/22] Configured pretty-printing and date conversion to ISO for XML responses --- lib/http-context.js | 11 ++++++++++- package.json | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/http-context.js b/lib/http-context.js index 4bd33772..bc8a22b7 100644 --- a/lib/http-context.js +++ b/lib/http-context.js @@ -285,7 +285,16 @@ HttpContext.prototype.done = function () { res.header('Content-Length', '7'); res.end(''); } else { - res.send(js2xmlparser('response', data)); + res.send(js2xmlparser('response', data, { + prettyPrinting: { + indentString: ' ' + }, + convertMap: { + "[object Date]": function(date) { + return date.toISOString(); + } + } + })); } break; default: diff --git a/package.json b/package.json index b461dfc3..2bec10a5 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "Remoting", "REST" ], - "version": "1.5.2", + "version": "1.5.3", "scripts": { "test": "mocha" }, From 8c448bebe438418c0b913191804f4e9a290435ea Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Tue, 8 Jul 2014 06:59:22 -0700 Subject: [PATCH 06/22] Avoid adding empty description for each API --- ext/swagger.js | 11 +++++++---- package.json | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ext/swagger.js b/ext/swagger.js index 5847dcbc..35147732 100644 --- a/ext/swagger.js +++ b/ext/swagger.js @@ -36,10 +36,13 @@ function Swagger(remotes, options, models) { }; classes.forEach(function (item) { - resourceDoc.apis.push({ - path: '/' + name + item.http.path, - description: item.ctor.sharedCtor && item.ctor.sharedCtor.description - }); + var api = { + path: '/' + name + item.http.path + }; + if (item.ctor.sharedCtor && item.ctor.sharedCtor.description) { + api.description = item.ctor.sharedCtor.description; + } + resourceDoc.apis.push(api); apiDocs[item.name] = { apiVersion: resourceDoc.apiVersion, diff --git a/package.json b/package.json index 2bec10a5..115ee25d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "Remoting", "REST" ], - "version": "1.5.3", + "version": "1.5.4", "scripts": { "test": "mocha" }, From 9fbb27d6a3fdc5150504e956beeded864fad2653 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Tue, 8 Jul 2014 07:04:24 -0700 Subject: [PATCH 07/22] REVERT: Avoid adding empty description for each API --- ext/swagger.js | 11 ++++------- package.json | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/ext/swagger.js b/ext/swagger.js index 35147732..5847dcbc 100644 --- a/ext/swagger.js +++ b/ext/swagger.js @@ -36,13 +36,10 @@ function Swagger(remotes, options, models) { }; classes.forEach(function (item) { - var api = { - path: '/' + name + item.http.path - }; - if (item.ctor.sharedCtor && item.ctor.sharedCtor.description) { - api.description = item.ctor.sharedCtor.description; - } - resourceDoc.apis.push(api); + resourceDoc.apis.push({ + path: '/' + name + item.http.path, + description: item.ctor.sharedCtor && item.ctor.sharedCtor.description + }); apiDocs[item.name] = { apiVersion: resourceDoc.apiVersion, diff --git a/package.json b/package.json index 115ee25d..2bec10a5 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "Remoting", "REST" ], - "version": "1.5.4", + "version": "1.5.3", "scripts": { "test": "mocha" }, From 4e81d278374372aa118f022514ba1e002a0aea4d Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Sat, 12 Jul 2014 20:15:09 -0700 Subject: [PATCH 08/22] Upgraded to generate specs based on Swagger 1.2 --- .gitignore | 2 +- ext/swagger.js | 41 +++++++++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 9462333e..9638f241 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.log *.csv *.dat +*.iml *.out *.pid *.swp @@ -11,4 +12,3 @@ node_modules/ /coverage/ dist -StrongRemoting.iml diff --git a/ext/swagger.js b/ext/swagger.js index 5847dcbc..b40405c5 100644 --- a/ext/swagger.js +++ b/ext/swagger.js @@ -30,8 +30,10 @@ function Swagger(remotes, options, models) { var apiDocs = {}; var resourceDoc = { apiVersion: version, - swaggerVersion: '1.1', + swaggerVersion: '1.2', basePath: basePath, + consumes: ['application/json', 'application/xml', 'text/xml'], + produces: ['application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml'], apis: [] }; @@ -163,18 +165,29 @@ function addDynamicBasePathGetter(remotes, path, obj) { function routeToAPI(route) { var returnDesc = route.returns && route.returns[0]; + var responseMessages = [ + { + code: 200, + message: null, + responseModel: returnDesc ? returnDesc.model || prepareDataType(returnDesc.type) : 'void' + } + ]; + if (route.errors) { + responseMessages.push.apply(responseMessages, route.errors); + } return { path: convertPathFragments(route.path), operations: [{ - httpMethod: convertVerb(route.verb), + method: convertVerb(route.verb), + deprecated: route.deprecated, nickname: route.method.replace(/\./g, '_'), // [rfeng] Swagger UI doesn't escape '.' for jQuery selector - responseClass: returnDesc ? returnDesc.model || prepareDataType(returnDesc.type) : 'void', - parameters: route.accepts ? route.accepts.map(acceptToParameter(route)) : [], - errorResponses: [], // TODO(schoon) - We don't have descriptions for this yet. summary: route.description, // TODO(schoon) - Excerpt? notes: '', // TODO(schoon) - `description` metadata? - produces: ['application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml'] + parameters: route.accepts ? route.accepts.map(acceptToParameter(route)) : [], + consumes: ['application/json', 'application/xml', 'text/xml'], + produces: ['application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml'], + responseMessages: responseMessages }] }; } @@ -227,12 +240,20 @@ function acceptToParameter(route) { } return { - paramType: paramType || type, name: name, - description: accepts.description, - dataType: accepts.model || prepareDataType(accepts.type), required: !!accepts.required, - allowMultiple: false + paramType: paramType || type, + type: prepareDataType(accepts.type), + $ref: accepts.model, + items: accepts.items, + format: accepts.format, + defaultValue: accepts.defaultValue, + enum: accepts.enum, + minimum: accepts.minimum, + maximum: accepts.maximum, + uniqueItems: accepts.uniqueItems, + allowMultiple: accepts.allowMultiple, + description: accepts.description }; }; } From aa1c4ce9b074d59b91d205c8b2ae07cdf38f6352 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Mon, 14 Jul 2014 13:03:19 -0700 Subject: [PATCH 09/22] Added support for errors property to specify possible errors that can be returned --- lib/exports-helper.js | 2 ++ lib/http-context.js | 1 + lib/http-invocation.js | 2 ++ lib/jsonrpc-adapter.js | 3 ++- lib/remote-objects.js | 3 ++- lib/rest-adapter.js | 4 +++- lib/shared-class.js | 1 + lib/shared-method.js | 6 ++++++ lib/socket-io-context.js | 1 + test/rest-adapter.test.js | 5 +++++ test/rest.test.js | 5 +++-- 11 files changed, 28 insertions(+), 5 deletions(-) diff --git a/lib/exports-helper.js b/lib/exports-helper.js index 4199ee1d..bac8134a 100644 --- a/lib/exports-helper.js +++ b/lib/exports-helper.js @@ -112,6 +112,7 @@ function method(fn, options) { var path = options.path || options.name || fn.name || null; var accepts = options.accepts || null; var returns = options.returns || null; + var errors = options.errors || null; if (!path) { // TODO: Error. @@ -122,6 +123,7 @@ function method(fn, options) { fn.shared = true; fn.accepts = accepts; fn.returns = returns; + fn.errors = errors; PASSTHROUGH_OPTIONS.forEach(function (key) { if (options[key]) { diff --git a/lib/http-context.js b/lib/http-context.js index bc8a22b7..c80adb6a 100644 --- a/lib/http-context.js +++ b/lib/http-context.js @@ -53,6 +53,7 @@ HttpContext.prototype.buildArgs = function (method) { var ctx = this; var accepts = method.accepts; var returns = method.returns; + var errors = method.errors; // build arguments from req and method options accepts.forEach(function (o) { diff --git a/lib/http-invocation.js b/lib/http-invocation.js index 081baa38..c59df7aa 100644 --- a/lib/http-invocation.js +++ b/lib/http-invocation.js @@ -81,6 +81,7 @@ HttpInvocation.prototype.createRequest = function () { var req = {json: true, method: verb || 'GET'}; var accepts = method.accepts; var returns = method.returns; + var errors = method.errors; var query; // initial url is the format @@ -176,6 +177,7 @@ HttpInvocation.prototype.transformResponse = function(res, body, callback) { var callbackArgs = [null]; // null => placeholder for err var method = this.method; var returns = method.returns; + var errors = method.errors; var isObject = typeof body === 'object'; var err; var hasError = res.statusCode >= 400; diff --git a/lib/jsonrpc-adapter.js b/lib/jsonrpc-adapter.js index e68bf651..985c100c 100644 --- a/lib/jsonrpc-adapter.js +++ b/lib/jsonrpc-adapter.js @@ -206,7 +206,8 @@ JsonRpcAdapter.prototype.allRoutes = function () { description: method.description, method: method.stringName, accepts: (method.accepts && method.accepts.length) ? method.accepts : undefined, - returns: (method.returns && method.returns.length) ? method.returns : undefined + returns: (method.returns && method.returns.length) ? method.returns : undefined, + errors: (method.errors && method.errors.length) ? method.errors : undefined }); } }; diff --git a/lib/remote-objects.js b/lib/remote-objects.js index 2ac629ca..541958a9 100644 --- a/lib/remote-objects.js +++ b/lib/remote-objects.js @@ -205,7 +205,8 @@ RemoteObjects.prototype.toJSON = function () { result[sharedMethod.stringName] = { http: sharedMethod.fn && sharedMethod.fn.http, accepts: sharedMethod.accepts, - returns: sharedMethod.returns + returns: sharedMethod.returns, + errors: sharedMethod.errors }; }); diff --git a/lib/rest-adapter.js b/lib/rest-adapter.js index 639749bd..ea2c1e54 100644 --- a/lib/rest-adapter.js +++ b/lib/rest-adapter.js @@ -368,7 +368,8 @@ RestAdapter.prototype.allRoutes = function () { description: method.description, method: method.stringName, accepts: (method.accepts && method.accepts.length) ? method.accepts : undefined, - returns: (method.returns && method.returns.length) ? method.returns : undefined + returns: (method.returns && method.returns.length) ? method.returns : undefined, + errors: (method.errors && method.errors.length) ? method.errors : undefined }); } } @@ -419,6 +420,7 @@ function RestMethod(restClass, sharedMethod) { this.accepts = sharedMethod.accepts; this.returns = sharedMethod.returns; + this.errors = sharedMethod.errors; this.description = sharedMethod.description; var methodRoutes = getRoutes(sharedMethod); diff --git a/lib/shared-class.js b/lib/shared-class.js index 4c05ee76..5649fef4 100644 --- a/lib/shared-class.js +++ b/lib/shared-class.js @@ -126,6 +126,7 @@ function define(methods, name, options, fn) { * define('myMethod', { * accepts: {arg: 'str', type: 'string'}, * returns: {arg: 'str', type: 'string'} + * errors: [ { code: 404, message: 'Not Found', responseModel: 'Error' } ] * }, myMethod); * }); * function myMethod(str, cb) { diff --git a/lib/shared-method.js b/lib/shared-method.js index 7bc7c27e..36282263 100644 --- a/lib/shared-method.js +++ b/lib/shared-method.js @@ -38,6 +38,7 @@ function SharedMethod(fn, name, sc, options) { var isStatic = this.isStatic = options.isStatic || false; this.accepts = options.accepts || fn.accepts || []; this.returns = options.returns || fn.returns || []; + this.errors = options.errors || fn.errors || []; this.description = options.description || fn.description; this.http = options.http || fn.http || {}; this.rest = options.rest || fn.rest || {}; @@ -64,6 +65,9 @@ function SharedMethod(fn, name, sc, options) { if(this.returns && !Array.isArray(this.returns)) { this.returns = [this.returns]; } + if(this.errors && !Array.isArray(this.errors)) { + this.errors = [this.errors]; + } this.stringName = (sc ? sc.name : '') + (isStatic ? '.' : '.prototype.') + name; } @@ -83,6 +87,7 @@ SharedMethod.fromFunction = function(fn, name, sharedClass, isStatic) { isStatic: isStatic, accepts: fn.accepts, returns: fn.returns, + errors: fn.errors, description: fn.description, http: fn.http, rest: fn.rest @@ -99,6 +104,7 @@ SharedMethod.fromFunction = function(fn, name, sharedClass, isStatic) { SharedMethod.prototype.invoke = function (scope, args, fn) { var accepts = this.accepts; var returns = this.returns; + var errors = this.errors; var method = this.getFunction(); var sharedMethod = this; var formattedArgs = []; diff --git a/lib/socket-io-context.js b/lib/socket-io-context.js index 09deb6b0..ccbe36c7 100644 --- a/lib/socket-io-context.js +++ b/lib/socket-io-context.js @@ -85,6 +85,7 @@ SocketIOContext.prototype.invoke = function (scope, method, fn) { var args = method.isSharedCtor ? this.ctorArgs : this.args; var accepts = method.accepts; var returns = method.returns; + var errors = method.errors; var scope; var result; diff --git a/test/rest-adapter.test.js b/test/rest-adapter.test.js index a01c6121..a2208700 100644 --- a/test/rest-adapter.test.js +++ b/test/rest-adapter.test.js @@ -105,6 +105,11 @@ describe('RestAdapter', function() { expect(method.returns).to.eql([anArg]); }); + it('has `errors`', function() { + var method = givenRestStaticMethod({ errors: anArg }); + expect(method.errors).to.eql([anArg]); + }); + it('has `description`', function() { var method = givenRestStaticMethod({ description: 'a-desc' }); expect(method.description).to.equal('a-desc'); diff --git a/test/rest.test.js b/test/rest.test.js index a10f4125..0c5ba112 100644 --- a/test/rest.test.js +++ b/test/rest.test.js @@ -618,6 +618,7 @@ describe('strong-remoting-rest', function(){ { arg: 'a', type: 'number', http: {source: 'path'} } ], returns: { arg: 'n', type: 'number', root: true}, + errors: [], http: { path: '/:a' } } ); @@ -625,7 +626,7 @@ describe('strong-remoting-rest', function(){ request(app)['get'](method.classUrl + '/1?callback=boo') .set('Accept', 'application/javascript') .expect('Content-Type', /javascript/) - .expect('typeof boo === \'function\' && boo(1);', done); + .expect('/**/ typeof boo === \'function\' && boo(1);', done); }); it('should allow jsonp requests with null response', function (done) { @@ -645,7 +646,7 @@ describe('strong-remoting-rest', function(){ request(app)['get'](method.classUrl + '/1?callback=boo') .set('Accept', 'application/javascript') .expect('Content-Type', /javascript/) - .expect('typeof boo === \'function\' && boo(null);', done); + .expect('/**/ typeof boo === \'function\' && boo(null);', done); }); it('should allow arguments in the query', function(done) { From fe02b783f14d7f814a9dbc118e31914d009c0ec5 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Mon, 14 Jul 2014 14:55:26 -0700 Subject: [PATCH 10/22] Added description for 200 responseMessage --- ext/swagger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/swagger.js b/ext/swagger.js index b40405c5..0ec8eac7 100644 --- a/ext/swagger.js +++ b/ext/swagger.js @@ -168,7 +168,7 @@ function routeToAPI(route) { var responseMessages = [ { code: 200, - message: null, + message: 'Request was successful', responseModel: returnDesc ? returnDesc.model || prepareDataType(returnDesc.type) : 'void' } ]; From 2c1a042a21464c9a232026d712d01d37a408cb74 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Wed, 16 Jul 2014 14:17:49 -0700 Subject: [PATCH 11/22] Added error-handling when unable to generate XML --- lib/http-context.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/http-context.js b/lib/http-context.js index c80adb6a..ef1fd866 100644 --- a/lib/http-context.js +++ b/lib/http-context.js @@ -286,16 +286,21 @@ HttpContext.prototype.done = function () { res.header('Content-Length', '7'); res.end(''); } else { - res.send(js2xmlparser('response', data, { - prettyPrinting: { - indentString: ' ' - }, - convertMap: { - "[object Date]": function(date) { - return date.toISOString(); + try { + var xml = js2xmlparser('response', data, { + prettyPrinting: { + indentString: ' ' + }, + convertMap: { + "[object Date]": function (date) { + return date.toISOString(); + } } - } - })); + }); + res.send(xml); + } catch(e) { + res.send(500, e + "\n" + data); + } } break; default: From c049f134214e0c42ce65c77483feeb15df79b9d9 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Fri, 18 Jul 2014 20:23:14 -0700 Subject: [PATCH 12/22] Corrected addDynamicBasePathGetter() to honor x-forwarded-proto in order to support reverse-proxy from https to http --- ext/swagger.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/swagger.js b/ext/swagger.js index 0ec8eac7..dfb000fe 100644 --- a/ext/swagger.js +++ b/ext/swagger.js @@ -136,8 +136,9 @@ function addDynamicBasePathGetter(remotes, path, obj) { remotes.before(path, function (ctx, next) { var headers = ctx.req.headers; var host = headers.Host || headers.host; + var protocol = headers['x-forwarded-proto'] || headers['X-Forwarded-Proto'] || ctx.req.protocol - basePath = ctx.req.protocol + '://' + host + initialPath; + basePath = protocol + '://' + host + initialPath; next(); }); From ecb7a07004f8e4ad97c836341d3fc2b482ad0a9f Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Wed, 6 Aug 2014 22:10:11 -0700 Subject: [PATCH 13/22] Added support for notes --- lib/exports-helper.js | 2 +- lib/jsonrpc-adapter.js | 1 + lib/rest-adapter.js | 2 ++ lib/shared-method.js | 3 +++ test/rest-adapter.test.js | 5 +++++ 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/exports-helper.js b/lib/exports-helper.js index bac8134a..65bd40e5 100644 --- a/lib/exports-helper.js +++ b/lib/exports-helper.js @@ -12,7 +12,7 @@ var debug = require('debug')('strong-remoting:exports-helper'); /*! * Constants */ -var PASSTHROUGH_OPTIONS = ['http', 'description']; +var PASSTHROUGH_OPTIONS = ['http', 'description', 'notes']; /** * @class A wrapper to make manipulating the exports object easier. diff --git a/lib/jsonrpc-adapter.js b/lib/jsonrpc-adapter.js index 62f1e098..65bceb9d 100644 --- a/lib/jsonrpc-adapter.js +++ b/lib/jsonrpc-adapter.js @@ -209,6 +209,7 @@ JsonRpcAdapter.prototype.allRoutes = function () { verb: verb, path: path, description: method.description, + notes: method.notes, method: method.stringName, accepts: (method.accepts && method.accepts.length) ? method.accepts : undefined, returns: (method.returns && method.returns.length) ? method.returns : undefined, diff --git a/lib/rest-adapter.js b/lib/rest-adapter.js index 65f75c4b..2dec7cfa 100644 --- a/lib/rest-adapter.js +++ b/lib/rest-adapter.js @@ -437,6 +437,7 @@ RestAdapter.prototype.allRoutes = function () { verb: verb, path: path, description: method.description, + notes: method.notes, method: method.stringName, accepts: (method.accepts && method.accepts.length) ? method.accepts : undefined, returns: (method.returns && method.returns.length) ? method.returns : undefined, @@ -483,6 +484,7 @@ function RestMethod(restClass, sharedMethod) { this.returns = sharedMethod.returns; this.errors = sharedMethod.errors; this.description = sharedMethod.description; + this.notes = sharedMethod.notes; var methodRoutes = getRoutes(sharedMethod); if (sharedMethod.isStatic || !restClass.ctor) { diff --git a/lib/shared-method.js b/lib/shared-method.js index 0eef471f..3d041cbf 100644 --- a/lib/shared-method.js +++ b/lib/shared-method.js @@ -51,6 +51,7 @@ var EventEmitter = require('events').EventEmitter * @property {Array|Object} accepts See `options.accepts` * @property {Array|Object} returns See `options.returns` * @property {String} description + * @property {String} notes * @property {String} http * @property {String} rest * @property {String} shared @@ -72,6 +73,7 @@ function SharedMethod(fn, name, sc, options) { this.returns = options.returns || fn.returns || []; this.errors = options.errors || fn.errors || []; this.description = options.description || fn.description; + this.notes = options.notes || fn.notes; this.http = options.http || fn.http || {}; this.rest = options.rest || fn.rest || {}; this.shared = options.shared; @@ -121,6 +123,7 @@ SharedMethod.fromFunction = function(fn, name, sharedClass, isStatic) { returns: fn.returns, errors: fn.errors, description: fn.description, + notes: fn.notes, http: fn.http, rest: fn.rest }); diff --git a/test/rest-adapter.test.js b/test/rest-adapter.test.js index fe4bb16d..8b5f7e23 100644 --- a/test/rest-adapter.test.js +++ b/test/rest-adapter.test.js @@ -115,6 +115,11 @@ describe('RestAdapter', function() { expect(method.description).to.equal('a-desc'); }); + it('has `notes`', function() { + var method = givenRestStaticMethod({ notes: 'some-notes' }); + expect(method.notes).to.equal('some-notes'); + }); + describe('isReturningArray()', function() { it('returns true when there is single root Array arg', function() { var method = givenRestStaticMethod({ From a9510bd839a70f8cc3ff9c51a07c52863d826532 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Wed, 6 Aug 2014 22:40:46 -0700 Subject: [PATCH 14/22] Added support for return types as Arrays instead of Strings --- lib/http-context.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/http-context.js b/lib/http-context.js index ef1fd866..e516577c 100644 --- a/lib/http-context.js +++ b/lib/http-context.js @@ -106,7 +106,11 @@ HttpContext.prototype.buildArgs = function (method) { // cast booleans and numbers var dynamic; var type = typeof val; - var otype = o.type && o.type.toLowerCase(); + if (Array.isArray(o.type)) { + otype = '[' + o.type.join(',') + ']'; + } else { + otype = o.type && o.type.toLowerCase(); + } if(Dynamic.canConvert(otype)) { dynamic = new Dynamic(val, ctx); From 007e7810ff06ba8f5b4c2cbcb1b8e7e205559c09 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Wed, 6 Aug 2014 22:51:30 -0700 Subject: [PATCH 15/22] Corrected XML handling when root is an Array --- lib/http-context.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/http-context.js b/lib/http-context.js index e516577c..cfb64790 100644 --- a/lib/http-context.js +++ b/lib/http-context.js @@ -291,7 +291,11 @@ HttpContext.prototype.done = function () { res.end(''); } else { try { - var xml = js2xmlparser('response', data, { + var input = data; + if (Object.prototype.toString.call(input) === "[object Array]") { + input = { result: data }; + } + var xml = js2xmlparser('response', input, { prettyPrinting: { indentString: ' ' }, From ea52c91e31935e60889c662c91ab73484230aa50 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Wed, 6 Aug 2014 23:02:42 -0700 Subject: [PATCH 16/22] Added missing docs for errors and notes --- lib/shared-method.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/shared-method.js b/lib/shared-method.js index 3d041cbf..c56b13ee 100644 --- a/lib/shared-method.js +++ b/lib/shared-method.js @@ -44,12 +44,14 @@ var EventEmitter = require('events').EventEmitter * @param {Object} [options.accepts.rest] The REST mapping / settings for the * argument. * @param {Array|Object} [options.returns] An `Array` of argument definitions + * @param {Array|Object} [options.errors] An `Array` of error definitions * The same options are available as `options.accepts`. * @property {String} name The method name * @property {String[]} aliases An array of method aliases * @property {Array|Object} isStatic * @property {Array|Object} accepts See `options.accepts` * @property {Array|Object} returns See `options.returns` + * @property {Array|Object} errors See `options.errors` * @property {String} description * @property {String} notes * @property {String} http From 4ec15e45f6883e105b25bfd12ad7f1c0add5d16b Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Wed, 6 Aug 2014 23:29:25 -0700 Subject: [PATCH 17/22] Re-added tests for XML conversion, and added tests for handling returns as array with root=true --- lib/http-context.js | 1 + package.json | 2 +- test/rest.test.js | 57 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/lib/http-context.js b/lib/http-context.js index cfb64790..d5651cde 100644 --- a/lib/http-context.js +++ b/lib/http-context.js @@ -106,6 +106,7 @@ HttpContext.prototype.buildArgs = function (method) { // cast booleans and numbers var dynamic; var type = typeof val; + var otype = null; if (Array.isArray(o.type)) { otype = '[' + o.type.join(',') + ']'; } else { diff --git a/package.json b/package.json index e23b35d8..12e69e2d 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "request": "~2.37.0", "browser-request": "~0.3.2", "qs": "~0.6.6", - "js2xmlparser": "~0.1.3" + "js2xmlparser": "~0.1.4" }, "devDependencies": { "supertest": "~0.13.0", diff --git a/test/rest.test.js b/test/rest.test.js index 505e5002..4c80f727 100644 --- a/test/rest.test.js +++ b/test/rest.test.js @@ -48,6 +48,18 @@ describe('strong-remoting-rest', function(){ .expect('Content-Type', /json/); } + function xml(method, url) { + if (url === undefined) { + url = method; + method = 'get'; + } + + return request(app)[method](url) + .set('Accept', 'text/xml') + .set('Content-Type', 'text/xml') + .expect('Content-Type', /xml/); + } + describe('remoting options', function(){ // The 1kb limit is set by RemoteObjects.create({json: {limit: '1kb'}}); it('should reject json payload larger than 1kb', function(done) { @@ -124,6 +136,51 @@ describe('strong-remoting-rest', function(){ .expect(200, { msg: 'hello' }, done); }); + it('should honor Accept: header', function(done) { + var method = givenSharedStaticMethod( + function greet2(msg, cb) { + cb(null, msg); + }, + { + accepts: { arg: 'person', type: 'string' }, + returns: { arg: 'msg', type: 'string' } + } + ); + + xml(method.url + '?person=hello') + .expect(200, '\n\n hello\n', done); + }); + + it('should handle returns of array', function(done) { + var method = givenSharedStaticMethod( + function greet3(msg, cb) { + cb(null, [msg]); + }, + { + accepts: { arg: 'person', type: ['string'] }, + returns: { arg: 'msg', type: 'string' } + } + ); + + xml(method.url + '?person=hello') + .expect(200, '\n\n hello\n', done); + }); + + it('should handle returns of array to XML', function(done) { + var method = givenSharedStaticMethod( + function greet4(msg, cb) { + cb(null, [msg]); + }, + { + accepts: { arg: 'person', type: ['string'] }, + returns: { arg: 'msg', type: ['string'], root: true } + } + ); + + xml(method.url + '?person=hello') + .expect(200, '\n\n hello\n', done); + }); + it('should allow arguments in the path', function(done) { var method = givenSharedStaticMethod( function bar(a, b, cb) { From 56d3d197328b338292df4a96344c721753e34713 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Fri, 5 Sep 2014 21:29:28 -0700 Subject: [PATCH 18/22] Changed errorHandler() to honor options.remoting.errorHandler.handler in order to replace restErrorHandler --- lib/rest-adapter.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/rest-adapter.js b/lib/rest-adapter.js index 2dec7cfa..d848a17a 100644 --- a/lib/rest-adapter.js +++ b/lib/rest-adapter.js @@ -286,6 +286,10 @@ RestAdapter.urlNotFoundHandler = function() { RestAdapter.errorHandler = function(options) { options = options || {}; return function restErrorHandler(err, req, res, next) { + if (options.handler) { + return options.handler(err, req, res, next) + } + if(typeof err === 'string') { err = new Error(err); err.status = err.statusCode = 500; From a34132d2b6ab1833fa0c7c8b5d986db202e00311 Mon Sep 17 00:00:00 2001 From: Joel Taylor Date: Wed, 17 Sep 2014 11:38:16 -0700 Subject: [PATCH 19/22] Number validation --- lib/shared-method.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/shared-method.js b/lib/shared-method.js index c56b13ee..e0d70118 100644 --- a/lib/shared-method.js +++ b/lib/shared-method.js @@ -169,6 +169,12 @@ SharedMethod.prototype.invoke = function (scope, args, fn) { } } + if(actualType === 'number' && Number.isNaN(uarg)) { + var err = new Error(name + ' must be a number'); + err.statusCode = 400; + return fn(err); + } + // convert strings if(actualType === 'string' && desc.type !== 'any' && actualType !== desc.type) { switch(desc.type) { From 1a2f528fff194ada8ee71b424527393e26c249b1 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Tue, 7 Oct 2014 17:16:20 -0700 Subject: [PATCH 20/22] Corrected params for ['string'] --- test/rest.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/rest.test.js b/test/rest.test.js index 6c7fcb29..16fed2a0 100644 --- a/test/rest.test.js +++ b/test/rest.test.js @@ -244,7 +244,7 @@ describe('strong-remoting-rest', function(){ } ); - xml(method.url + '?person=hello') + xml(method.url + '?person=["hello"]') .expect(200, '\n\n hello\n', done); }); @@ -259,7 +259,7 @@ describe('strong-remoting-rest', function(){ } ); - xml(method.url + '?person=hello') + xml(method.url + '?person=["hello"]') .expect(200, '\n\n hello\n', done); }); From 03d4e9dd9e4c5f1da53fb0f4aa5a1df11b9a40f0 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Tue, 7 Oct 2014 17:58:50 -0700 Subject: [PATCH 21/22] Changed to automatically handle splitting arrays of boolean, date, number, and string when delimited with , or | --- lib/shared-method.js | 32 ++++++++++++++++++++++++++++---- test/rest.test.js | 6 +++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/shared-method.js b/lib/shared-method.js index ba4ca2ed..dfeaecab 100644 --- a/lib/shared-method.js +++ b/lib/shared-method.js @@ -177,22 +177,46 @@ SharedMethod.prototype.invoke = function (scope, args, fn) { // convert strings if(actualType === 'string' && desc.type !== 'any' && actualType !== desc.type) { - switch(desc.type) { + targetType = desc.type + if (Array.isArray(desc.type) && targetType.length == 1) { + uarg = uarg.split(/[,|]/) + targetType = targetType[0] + } + switch(targetType) { case 'string': break; case 'date': - uarg = new Date(uarg); + if (Array.isArray(uarg)) { + for (i in uarg) { + uarg[i] = new Date(uarg[i]); + } + } else { + uarg = new Date(uarg); + } break; case 'number': - uarg = Number(uarg); + if (Array.isArray(uarg)) { + for (i in uarg) { + uarg[i] = Number(uarg[i]); + } + } else { + uarg = Number(uarg); + } break; case 'boolean': - uarg = Boolean(uarg); + if (Array.isArray(uarg)) { + for (i in uarg) { + uarg[i] = Boolean(uarg[i]); + } + } else { + uarg = Boolean(uarg); + } break; // Other types such as 'object', 'array', // ModelClass, ['string'], or [ModelClass] default: try { + console.log({type:desc.type, uarg:uarg}) uarg = JSON.parse(uarg); } catch(err) { debug('- %s - invalid value for argument \'%s\' of type \'%s\': %s', diff --git a/test/rest.test.js b/test/rest.test.js index 16fed2a0..02ca837d 100644 --- a/test/rest.test.js +++ b/test/rest.test.js @@ -244,7 +244,7 @@ describe('strong-remoting-rest', function(){ } ); - xml(method.url + '?person=["hello"]') + xml(method.url + '?person=hello') .expect(200, '\n\n hello\n', done); }); @@ -259,7 +259,7 @@ describe('strong-remoting-rest', function(){ } ); - xml(method.url + '?person=["hello"]') + xml(method.url + '?person=hello') .expect(200, '\n\n hello\n', done); }); @@ -336,7 +336,7 @@ describe('strong-remoting-rest', function(){ } ); - json(method.classUrl +'/?a=z&b=["x", "y"]') + json(method.classUrl +'/?a=z&b=x|y') .expect({ n: 'xyz' }, done); }); From 3a3da6ba042c29f914194df45957da45f3f26fe4 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Tue, 7 Oct 2014 18:02:51 -0700 Subject: [PATCH 22/22] Removed obsolete debug print --- lib/shared-method.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/shared-method.js b/lib/shared-method.js index dfeaecab..d8146533 100644 --- a/lib/shared-method.js +++ b/lib/shared-method.js @@ -216,7 +216,6 @@ SharedMethod.prototype.invoke = function (scope, args, fn) { // ModelClass, ['string'], or [ModelClass] default: try { - console.log({type:desc.type, uarg:uarg}) uarg = JSON.parse(uarg); } catch(err) { debug('- %s - invalid value for argument \'%s\' of type \'%s\': %s',