diff --git a/lib/api/routes.js b/lib/api/routes.js index f2bdc31..bdc91fe 100644 --- a/lib/api/routes.js +++ b/lib/api/routes.js @@ -60,9 +60,8 @@ router.get("/commands", function(req, res) { }); router.post("/commands/:command", function(req, res) { - var command = req.params.command; - var result = Cylon.commands[command].apply(Cylon, req.commandParams); - res.json({ result: result }); + var command = Cylon.commands[req.params.command]; + router.runCommand(req, res, Cylon, command); }); router.get("/robots", function(req, res) { @@ -79,8 +78,7 @@ router.get("/robots/:robot/commands", load, function(req, res) { router.all("/robots/:robot/commands/:command", load, function(req, res) { var command = req.robot.commands[req.params.command]; - var result = command.apply(req.robot, req.commandParams); - res.json({ result: result }); + router.runCommand(req, res, req.robot, command); }); router.get("/robots/:robot/devices", load, function(req, res) { @@ -117,8 +115,7 @@ router.get("/robots/:robot/devices/:device/commands", load, function(req, res) { router.all("/robots/:robot/devices/:device/commands/:command", load, function(req, res) { var command = req.device.commands[req.params.command]; - var result = command.apply(req.device, req.commandParams); - res.json({ result: result }); + router.runCommand(req, res, req.device, command); }); router.get("/robots/:robot/connections", load, function(req, res) { @@ -128,3 +125,17 @@ router.get("/robots/:robot/connections", load, function(req, res) { router.get("/robots/:robot/connections/:connection", load, function(req, res) { res.json({ connection: req.connection }); }); + +// Run an MCP, Robot, or Device command. Process the results immediately, +// or asynchronously if the result is a Promise. +router.runCommand = function(req, res, my, command) { + var result = command.apply(my, req.commandParams); + if (typeof result.then === "function") { + result + .then(function(result) {return res.json({result: result});}) + .catch(function(err) {return res.status(500).json({error: err});}); + } + else { + res.json({ result: result }); + } +}; diff --git a/spec/lib/api/routes.spec.js b/spec/lib/api/routes.spec.js index 641fb00..7d44dc3 100644 --- a/spec/lib/api/routes.spec.js +++ b/spec/lib/api/routes.spec.js @@ -21,6 +21,33 @@ function findFinalHandler(path) { return handlers[handlers.length - 1]; } +function MockPromise() { + var my = this; + my.resolve = function() { + var args = arguments; + process.nextTick(function() { + my.thenCallback.apply(null, args); + }); + return my; + }; + my.reject = function() { + var args = arguments; + process.nextTick(function() { + my.errorCallback.apply(null, args); + }); + return my; + }; + my.then = function(thenCallback) { + my.thenCallback = thenCallback; + return my; + }; + my.catch = function(errorCallback) { + my.errorCallback = errorCallback; + return my; + }; + my.deferred = my; +} + describe("API routes", function() { var routes = [ ["GET", "/"], @@ -55,18 +82,40 @@ describe("API commands", function() { var req, res; beforeEach(function() { Cylon.commands.ping = function() { return "pong"; }; + Cylon.commands.pingAsync = function() { + var promise = new MockPromise(); + return promise.resolve("immediate pong"); + }; req = new MockRequest(); res = new MockResponse(); + res.status = function(statusCode) { + res.statusCode = statusCode; + return res; + }; req.device = { name: "testDevice", commands: { - announce: function(){return "im here";} + announce: function(){return "im here";}, + announceAsync: function() { + var promise = new MockPromise(); + process.nextTick(function(){ + return promise.reject("sorry, sore throat"); + }); + return promise.deferred; + } } }; req.robot = { name: "fred", commands: { - speak: function(){return "ahem";} + speak: function(){return "ahem";}, + speakAsync: function() { + var promise = new MockPromise(); + process.nextTick(function(){ + return promise.resolve("see ya in another cycle"); + }); + return promise.deferred; + } }, devices: { testDevice: req.device @@ -75,12 +124,13 @@ describe("API commands", function() { }); afterEach(function() { delete Cylon.commands.ping; + delete Cylon.commands.pingAsync; }); it("returns the list of MCP commands", function() { res.json = function(obj){ expect(obj.commands).to.exist(); - expect(obj.commands.length).to.equal(1); + expect(obj.commands.length).to.equal(2); expect(obj.commands[0]).to.equal("ping"); }; findFinalHandler("/commands")(req, res); @@ -94,11 +144,19 @@ describe("API commands", function() { findFinalHandler("/commands/ping")(req, res); }); + it("returns an immediate MCP async command", function() { + req.params = {command:"pingAsync"}; + res.json = function(obj){ + expect(obj.result).to.equal("immediate pong"); + }; + findFinalHandler("/commands/pingAsync")(req, res); + }); + it("returns the list of robot commands", function() { req.params = {robot: "fred"}; res.json = function(obj){ expect(obj.commands).to.exist(); - expect(obj.commands.length).to.equal(1); + expect(obj.commands.length).to.equal(2); expect(obj.commands[0]).to.equal("speak"); }; findFinalHandler("/robots/fred/commands")(req, res); @@ -112,11 +170,19 @@ describe("API commands", function() { findFinalHandler("/robots/fred/commands/speak")(req, res); }); + it("invokes an asynchronous robot command", function() { + req.params = {robot: "fred", command:"speakAsync"}; + res.json = function(obj){ + expect(obj.result).to.equal("see ya in another cycle"); + }; + findFinalHandler("/robots/fred/commands/speakAsync")(req, res); + }); + it("returns the list of device commands", function() { req.params = {robot: "fred", device: "testDevice" }; res.json = function(obj){ expect(obj.commands).to.exist(); - expect(obj.commands.length).to.equal(1); + expect(obj.commands.length).to.equal(2); expect(obj.commands[0]).to.equal("announce"); }; var path = "/robots/fred/devices/testDevice/commands"; @@ -128,7 +194,17 @@ describe("API commands", function() { res.json = function(obj){ expect(obj.result).to.equal("im here"); }; - var path = "/robots/fred/devices/testDevice/commands/speak"; + var path = "/robots/fred/devices/testDevice/commands/announce"; + findFinalHandler(path)(req, res); + }); + + it("returns correctly for an async device command that errored", function() { + req.params = {robot: "fred", device: "testDevice", command:"announceAsync"}; + res.json = function(obj){ + expect(obj.error).to.equal("sorry, sore throat"); + expect(res.statusCode).to.equal(500); + }; + var path = "/robots/fred/devices/testDevice/commands/announceAsync"; findFinalHandler(path)(req, res); });