diff --git a/lib/common/service-object.js b/lib/common/service-object.js index dd8013db9b65..06b8ba8e01e3 100644 --- a/lib/common/service-object.js +++ b/lib/common/service-object.js @@ -125,7 +125,7 @@ ServiceObject.prototype.create = function(options, callback) { args.push(onCreate); - this.createMethod.apply(null, args); + return this.createMethod.apply(null, args); }; /** @@ -146,9 +146,7 @@ ServiceObject.prototype.delete = function(callback) { // The `request` method may have been overridden to hold any special behavior. // Ensure we call the original `request` method. - ServiceObject.prototype.request.call(this, reqOpts, function(err, resp) { - callback(err, resp); - }); + return ServiceObject.prototype.request.call(this, reqOpts, callback); }; /** @@ -200,7 +198,7 @@ ServiceObject.prototype.get = function(config, callback) { var autoCreate = config.autoCreate && is.fn(this.create); delete config.autoCreate; - this.getMetadata(function(err, metadata) { + return this.getMetadata(function(err, metadata) { if (err) { if (err.code === 404 && autoCreate) { var args = [callback]; @@ -240,16 +238,11 @@ ServiceObject.prototype.getMetadata = function(callback) { // The `request` method may have been overridden to hold any special behavior. // Ensure we call the original `request` method. - ServiceObject.prototype.request.call(this, reqOpts, function(err, resp) { - if (err) { - callback(err, null, resp); - return; - } - + return ServiceObject.prototype.request.call(this, reqOpts).then(function(resp) { self.metadata = resp; - callback(null, self.metadata, resp); - }); + return [self.metadata, resp]; + }).asCallback(callback, { spread: true }); }; /** @@ -264,8 +257,6 @@ ServiceObject.prototype.getMetadata = function(callback) { ServiceObject.prototype.setMetadata = function(metadata, callback) { var self = this; - callback = callback || util.noop; - var methodConfig = this.methods.setMetadata || {}; var reqOpts = extend(true, { @@ -276,16 +267,11 @@ ServiceObject.prototype.setMetadata = function(metadata, callback) { // The `request` method may have been overridden to hold any special behavior. // Ensure we call the original `request` method. - ServiceObject.prototype.request.call(this, reqOpts, function(err, resp) { - if (err) { - callback(err, resp); - return; - } - + return ServiceObject.prototype.request.call(this, reqOpts).then(function(resp) { self.metadata = resp; - callback(null, resp); - }); + return [resp]; + }).asCallback(callback, { spread: true }); }; /** diff --git a/lib/common/service.js b/lib/common/service.js index a2ab5c86d37e..f32aab2e273a 100644 --- a/lib/common/service.js +++ b/lib/common/service.js @@ -22,6 +22,7 @@ var arrify = require('arrify'); var extend = require('extend'); +var Promise = require('bluebird'); /** * @type {module:common/util} @@ -68,42 +69,49 @@ function Service(config, options) { * @param {function} callback - The callback function passed to `request`. */ Service.prototype.request = function(reqOpts, callback) { - var uriComponents = [ - this.baseUrl - ]; - - if (this.projectIdRequired) { - uriComponents.push('projects'); - uriComponents.push(this.projectId); - } - - uriComponents.push(reqOpts.uri); - - reqOpts.uri = uriComponents - .map(function(uriComponent) { - var trimSlashesRegex = /^\/*|\/*$/g; - return uriComponent.replace(trimSlashesRegex, ''); - }) - .join('/') - // Some URIs have colon separators. - // Bad: https://.../projects/:list - // Good: https://.../projects:list - .replace(/\/:/g, ':'); - - // Interceptors should be called in the order they were assigned. - var combinedInterceptors = [].slice.call(this.globalInterceptors) - .concat(this.interceptors) - .concat(arrify(reqOpts.interceptors_)); - - var interceptor; - - while ((interceptor = combinedInterceptors.shift()) && interceptor.request) { - reqOpts = interceptor.request(reqOpts); - } - - delete reqOpts.interceptors_; - - return this.makeAuthenticatedRequest(reqOpts, callback); + var self = this; + return new Promise(function (resolve, reject) { + var uriComponents = [ + self.baseUrl + ]; + + if (self.projectIdRequired) { + uriComponents.push('projects'); + uriComponents.push(self.projectId); + } + + uriComponents.push(reqOpts.uri); + + reqOpts.uri = uriComponents + .map(function(uriComponent) { + var trimSlashesRegex = /^\/*|\/*$/g; + return uriComponent.replace(trimSlashesRegex, ''); + }) + .join('/') + // Some URIs have colon separators. + // Bad: https://.../projects/:list + // Good: https://.../projects:list + .replace(/\/:/g, ':'); + + // Interceptors should be called in the order they were assigned. + var combinedInterceptors = [].slice.call(self.globalInterceptors) + .concat(self.interceptors) + .concat(arrify(reqOpts.interceptors_)); + + var interceptor; + + while ((interceptor = combinedInterceptors.shift()) && interceptor.request) { + reqOpts = interceptor.request(reqOpts); + } + + delete reqOpts.interceptors_; + return self.makeAuthenticatedRequest(reqOpts, function (err, resp) { + if (err) { + err.response = resp; + } + return err ? reject(err) : resolve(resp); + }); + }).asCallback(callback, { spread: true }); }; module.exports = Service; diff --git a/lib/prediction/index.js b/lib/prediction/index.js index 1be462a4438b..3b0ab9a55651 100644 --- a/lib/prediction/index.js +++ b/lib/prediction/index.js @@ -25,6 +25,7 @@ var extend = require('extend'); var format = require('string-format-obj'); var is = require('is'); var nodeutil = require('util'); +var Promise = require('bluebird'); /** * @type {module:prediction/model} @@ -183,21 +184,16 @@ Prediction.prototype.createModel = function(id, options, callback) { delete body.type; } - this.request({ + return self.request({ method: 'POST', uri: '/trainedmodels', json: body - }, function(err, resp) { - if (err) { - callback(err, null, resp); - return; - } - + }).then(function(resp) { var model = self.model(resp.id); model.metadata = resp; - callback(null, model, resp); - }); + return [model, resp]; + }).asCallback(callback, { spread: true }); }; /** @@ -270,20 +266,15 @@ Prediction.prototype.createModel = function(id, options, callback) { Prediction.prototype.getModels = function(query, callback) { var self = this; - if (is.fn(query)) { + if (is.fn(query) || !query) { callback = query; query = {}; } - this.request({ + return self.request({ uri: '/trainedmodels/list', qs: query - }, function(err, resp) { - if (err) { - callback(err, null, null, resp); - return; - } - + }).then(function(resp) { var models = arrify(resp.items).map(function(model) { var modelInstance = self.model(model.id); modelInstance.metadata = model; @@ -298,8 +289,8 @@ Prediction.prototype.getModels = function(query, callback) { }); } - callback(null, models, nextQuery, resp); - }); + return [models, nextQuery, resp]; + }).asCallback(callback, { spread: true }); }; /** diff --git a/lib/prediction/model.js b/lib/prediction/model.js index ebb6bc90e776..c915e5542b8a 100644 --- a/lib/prediction/model.js +++ b/lib/prediction/model.js @@ -208,21 +208,16 @@ nodeutil.inherits(Model, ServiceObject); * }); */ Model.prototype.analyze = function(callback) { - this.request({ + return this.request({ uri: '/analyze' - }, function(err, resp) { - if (err) { - callback(err, null, resp); - return; - } - + }).then(function(resp) { var analysis = { data: resp.dataDescription || {}, model: resp.modelDescription || {} }; - callback(null, analysis, resp); - }); + return [analysis, resp]; + }).asCallback(callback, { spread: true }); }; /** @@ -331,7 +326,7 @@ Model.prototype.createWriteStream = function(label) { * }); */ Model.prototype.query = function(input, callback) { - this.request({ + return this.request({ method: 'POST', uri: '/predict', json: { @@ -339,12 +334,7 @@ Model.prototype.query = function(input, callback) { csvInstance: arrify(input) } } - }, function(err, resp) { - if (err) { - callback(err, null, resp); - return; - } - + }).then(function(resp) { var results = { winner: resp.outputLabel || resp.outputValue, scores: resp.outputMulti @@ -357,8 +347,8 @@ Model.prototype.query = function(input, callback) { }) }; - callback(null, results, resp); - }); + return [results, resp]; + }).asCallback(callback, { spread: true }); }; /** @@ -381,7 +371,7 @@ Model.prototype.query = function(input, callback) { * }); */ Model.prototype.train = function(label, input, callback) { - this.setMetadata({ + return this.setMetadata({ output: label, csvInstance: arrify(input) }, callback); diff --git a/package.json b/package.json index 5112483a29c5..c0ba69392bbf 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "array-uniq": "^1.0.2", "arrify": "^1.0.0", "async": "^1.4.2", + "bluebird": "^3.3.3", "camelize": "^1.0.0", "concat-stream": "^1.5.0", "create-error-class": "^2.0.1", @@ -135,7 +136,8 @@ "scripts": { "docs": "node ./scripts/docs.js", "lint": "jshint lib/ system-test/ test/ && jscs lib/ system-test/ test/", - "test": "npm run docs && mocha test/*/*.js test/index.js test/docs.js", + "mocha": "mocha test/*/*.js test/index.js test/docs.js", + "test": "npm run docs && npm run mocha", "system-test": "mocha system-test/* --timeout 60s --bail", "cover": "istanbul cover -x 'system-test/*' _mocha -- --timeout 60s test/*/*.js test/docs.js system-test/*", "coveralls": "istanbul cover -x 'system-test/*' _mocha --report lcovonly -- --timeout 60s test/*/*.js test/docs.js system-test/* -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" diff --git a/test/common/service-object.js b/test/common/service-object.js index 58efc44d0ce8..c627bb9c7d57 100644 --- a/test/common/service-object.js +++ b/test/common/service-object.js @@ -18,6 +18,7 @@ var assert = require('assert'); var extend = require('extend'); +var Promise = require('bluebird'); var ServiceObject = require('../../lib/common/service-object.js'); var util = require('../../lib/common/util.js'); @@ -411,17 +412,27 @@ describe('ServiceObject', function() { }); describe('getMetadata', function() { - it('should make the correct request', function(done) { + it('should make the correct request', function() { ServiceObject.prototype.request = function(reqOpts) { assert.strictEqual(this, serviceObject); assert.strictEqual(reqOpts.uri, ''); - done(); + return Promise.resolve(); }; - serviceObject.getMetadata(); + return serviceObject.getMetadata(); }); - it('should extend the request options with defaults', function(done) { + it('should make the correct request (callback)', function(done) { + ServiceObject.prototype.request = function(reqOpts) { + assert.strictEqual(this, serviceObject); + assert.strictEqual(reqOpts.uri, ''); + return Promise.resolve(); + }; + + serviceObject.getMetadata(done); + }); + + it('should extend the request options with defaults', function() { var method = { reqOpts: { method: 'override', @@ -434,35 +445,85 @@ describe('ServiceObject', function() { ServiceObject.prototype.request = function(reqOpts_) { assert.strictEqual(reqOpts_.method, method.reqOpts.method); assert.deepEqual(reqOpts_.qs, method.reqOpts.qs); - done(); + return Promise.resolve(); }; var serviceObject = new ServiceObject(CONFIG); serviceObject.methods.getMetadata = method; - serviceObject.getMetadata(); + return serviceObject.getMetadata(); }); - it('should execute callback with error & apiResponse', function(done) { + it('should extend the request options with defaults (callback)', function(done) { + var method = { + reqOpts: { + method: 'override', + qs: { + custom: true + } + } + }; + + ServiceObject.prototype.request = function(reqOpts_) { + assert.strictEqual(reqOpts_.method, method.reqOpts.method); + assert.deepEqual(reqOpts_.qs, method.reqOpts.qs); + return Promise.resolve(); + }; + + var serviceObject = new ServiceObject(CONFIG); + serviceObject.methods.getMetadata = method; + serviceObject.getMetadata(done); + }); + + it('should execute callback with error & apiResponse', function() { var error = new Error('Error.'); var apiResponse = {}; + error.response = apiResponse; - ServiceObject.prototype.request = function(reqOpts, callback) { - callback(error, apiResponse); + ServiceObject.prototype.request = function() { + return Promise.reject(error); }; - serviceObject.getMetadata(function(err, metadata, apiResponse_) { + return serviceObject.getMetadata().then(function () { + throw new Error('should have failed!'); + }, function(err) { assert.strictEqual(err, error); - assert.strictEqual(metadata, null); - assert.strictEqual(apiResponse_, apiResponse); + assert.strictEqual(err.response, apiResponse); + }); + }); + + it('should execute callback with error & apiResponse (callback)', function(done) { + var error = new Error('Error.'); + var apiResponse = {}; + error.response = apiResponse; + + ServiceObject.prototype.request = function() { + return Promise.reject(error); + }; + + serviceObject.getMetadata(function(err) { + assert.strictEqual(err, error); + assert.strictEqual(err.response, apiResponse); done(); }); }); - it('should update metadata', function(done) { + it('should update metadata', function() { var apiResponse = {}; - ServiceObject.prototype.request = function(reqOpts, callback) { - callback(null, apiResponse); + ServiceObject.prototype.request = function() { + return Promise.resolve(apiResponse); + }; + + return serviceObject.getMetadata().spread(function() { + assert.strictEqual(serviceObject.metadata, apiResponse); + }); + }); + + it('should update metadata (callback)', function(done) { + var apiResponse = {}; + + ServiceObject.prototype.request = function() { + return Promise.resolve(apiResponse); }; serviceObject.getMetadata(function(err) { @@ -472,11 +533,24 @@ describe('ServiceObject', function() { }); }); - it('should execute callback with metadata & API response', function(done) { + it('should execute callback with metadata & API response', function() { var apiResponse = {}; - ServiceObject.prototype.request = function(reqOpts, callback) { - callback(null, apiResponse); + ServiceObject.prototype.request = function() { + return Promise.resolve(apiResponse); + }; + + return serviceObject.getMetadata().spread(function(metadata, apiResponse_) { + assert.strictEqual(metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + }); + }); + + it('should execute callback with metadata & API response (callback)', function(done) { + var apiResponse = {}; + + ServiceObject.prototype.request = function() { + return Promise.resolve(apiResponse); }; serviceObject.getMetadata(function(err, metadata, apiResponse_) { @@ -489,7 +563,7 @@ describe('ServiceObject', function() { }); describe('setMetadata', function() { - it('should make the correct request', function(done) { + it('should make the correct request', function() { var metadata = {}; ServiceObject.prototype.request = function(reqOpts) { @@ -497,13 +571,27 @@ describe('ServiceObject', function() { assert.strictEqual(reqOpts.method, 'PATCH'); assert.strictEqual(reqOpts.uri, ''); assert.strictEqual(reqOpts.json, metadata); - done(); + return Promise.resolve({}); }; - serviceObject.setMetadata(metadata); + return serviceObject.setMetadata(metadata); }); - it('should extend the request options with defaults', function(done) { + it('should make the correct request (callback)', function(done) { + var metadata = {}; + + ServiceObject.prototype.request = function(reqOpts) { + assert.strictEqual(this, serviceObject); + assert.strictEqual(reqOpts.method, 'PATCH'); + assert.strictEqual(reqOpts.uri, ''); + assert.strictEqual(reqOpts.json, metadata); + return Promise.resolve({}); + }; + + serviceObject.setMetadata(metadata, done); + }); + + it('should extend the request options with defaults (callback)', function(done) { var metadataDefault = { a: 'b' }; @@ -528,34 +616,64 @@ describe('ServiceObject', function() { assert.strictEqual(reqOpts_.method, method.reqOpts.method); assert.deepEqual(reqOpts_.qs, method.reqOpts.qs); assert.deepEqual(reqOpts_.json, expectedJson); - done(); + return Promise.resolve(); }; var serviceObject = new ServiceObject(CONFIG); serviceObject.methods.setMetadata = method; - serviceObject.setMetadata(metadata); + serviceObject.setMetadata(metadata, done); }); - it('should execute callback with error & apiResponse', function(done) { + it('should execute callback with error & apiResponse', function() { var error = new Error('Error.'); var apiResponse = {}; + error.response = apiResponse; - ServiceObject.prototype.request = function(reqOpts, callback) { - callback(error, apiResponse); + ServiceObject.prototype.request = function() { + return Promise.reject(error); }; - serviceObject.setMetadata({}, function(err, apiResponse_) { + return serviceObject.setMetadata({}).then(function () { + throw new Error('should have failed'); + }, function(err) { assert.strictEqual(err, error); - assert.strictEqual(apiResponse_, apiResponse); + assert.strictEqual(err.response, apiResponse); + }); + }); + + it('should execute callback with error & apiResponse (callback)', function(done) { + var error = new Error('Error.'); + var apiResponse = {}; + error.response = apiResponse; + + ServiceObject.prototype.request = function() { + return Promise.reject(error); + }; + + serviceObject.setMetadata({}, function(err) { + assert.strictEqual(err, error); + assert.strictEqual(err.response, apiResponse); done(); }); }); - it('should update metadata', function(done) { + it('should update metadata', function() { var apiResponse = {}; - ServiceObject.prototype.request = function(reqOpts, callback) { - callback(null, apiResponse); + ServiceObject.prototype.request = function() { + return Promise.resolve(apiResponse); + }; + + return serviceObject.setMetadata({}).spread(function() { + assert.strictEqual(serviceObject.metadata, apiResponse); + }); + }); + + it('should update metadata (callback)', function(done) { + var apiResponse = {}; + + ServiceObject.prototype.request = function() { + return Promise.resolve(apiResponse); }; serviceObject.setMetadata({}, function(err) { @@ -565,11 +683,23 @@ describe('ServiceObject', function() { }); }); - it('should execute callback with metadata & API response', function(done) { + it('should execute callback with metadata & API response', function() { var apiResponse = {}; - ServiceObject.prototype.request = function(reqOpts, callback) { - callback(null, apiResponse); + ServiceObject.prototype.request = function() { + return Promise.resolve(apiResponse); + }; + + return serviceObject.setMetadata({}).spread(function(apiResponse_) { + assert.strictEqual(apiResponse_, apiResponse); + }); + }); + + it('should execute callback with metadata & API response (callback)', function(done) { + var apiResponse = {}; + + ServiceObject.prototype.request = function() { + return Promise.resolve(apiResponse); }; serviceObject.setMetadata({}, function(err, apiResponse_) { diff --git a/test/prediction/index.js b/test/prediction/index.js index ba94d91c50fa..c5cc6e9dbd81 100644 --- a/test/prediction/index.js +++ b/test/prediction/index.js @@ -21,6 +21,7 @@ var assert = require('assert'); var extend = require('extend'); var mockery = require('mockery-next'); var nodeutil = require('util'); +var Promise = require('bluebird'); var Service = require('../../lib/common/service.js'); var util = require('../../lib/common/util.js'); @@ -135,7 +136,7 @@ describe('Prediction', function() { }, /A model ID is required/); }); - it('should make the correct API request', function(done) { + it('should make the correct API request', function() { prediction.request = function(reqOpts) { assert.strictEqual(reqOpts.method, 'POST'); assert.strictEqual(reqOpts.uri, '/trainedmodels'); @@ -145,20 +146,52 @@ describe('Prediction', function() { }); assert.deepEqual(reqOpts.json, expectedBody); - done(); + return Promise.resolve(reqOpts.json); }; - prediction.createModel(ID, OPTIONS, assert.ifError); + return prediction.createModel(ID, OPTIONS); + }); + + it('should make the correct API request (callback)', function(done) { + prediction.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'POST'); + assert.strictEqual(reqOpts.uri, '/trainedmodels'); + + var expectedBody = extend({}, OPTIONS, { + id: ID + }); + assert.deepEqual(reqOpts.json, expectedBody); + + return Promise.resolve(reqOpts.json); + }; + + prediction.createModel(ID, OPTIONS, function (err, model, resp) { + if (err) { + assert.ifError(err); + } + assert.ok(model); + assert.ok(resp); + done(); + }); }); it('should not require any options', function() { + prediction.request = function () { + return Promise.resolve({ id: ID }); + }; + return prediction.createModel(ID); + }); + + it('should not require any options (callback)', function(done) { assert.doesNotThrow(function() { - prediction.request = util.noop; - prediction.createModel(ID, assert.ifError); + prediction.request = function () { + return Promise.resolve({ id: ID }); + }; + prediction.createModel(ID, done); }); }); - it('should accept a File for input data source', function(done) { + it('should accept a File for input data source', function() { var file = { name: 'file-name', parent: { @@ -170,43 +203,86 @@ describe('Prediction', function() { var expectedLocation = file.parent.name + '/' + file.name; assert.strictEqual(reqOpts.json.storageDataLocation, expectedLocation); assert.strictEqual(reqOpts.json.data, undefined); - done(); + return Promise.resolve(reqOpts.json); + }; + + return prediction.createModel(ID, { + data: file + }); + }); + + it('should accept a File for input data source (callback)', function(done) { + var file = { + name: 'file-name', + parent: { + name: 'bucket-name' + } + }; + + prediction.request = function(reqOpts) { + var expectedLocation = file.parent.name + '/' + file.name; + assert.strictEqual(reqOpts.json.storageDataLocation, expectedLocation); + assert.strictEqual(reqOpts.json.data, undefined); + return Promise.resolve(reqOpts.json); }; prediction.createModel(ID, { data: file - }, assert.ifError); + }, done); }); - it('should accept a model type', function(done) { + it('should accept a model type', function() { var type = 'classification'; prediction.request = function(reqOpts) { assert.strictEqual(reqOpts.json.modelType, type.toUpperCase()); assert.strictEqual(reqOpts.json.type, undefined); - done(); + return Promise.resolve(reqOpts.json); + }; + + return prediction.createModel(ID, { + type: type + }); + }); + + it('should accept a model type (callback)', function(done) { + var type = 'classification'; + + prediction.request = function(reqOpts) { + assert.strictEqual(reqOpts.json.modelType, type.toUpperCase()); + assert.strictEqual(reqOpts.json.type, undefined); + return Promise.resolve(reqOpts.json); }; prediction.createModel(ID, { type: type - }, assert.ifError); + }, done); }); describe('error', function() { var error = new Error('Error.'); var apiResponse = {}; + error.response = apiResponse; beforeEach(function() { - prediction.request = function(reqOpts, callback) { - callback(error, apiResponse); + prediction.request = function() { + return Promise.reject(error); }; }); - it('should execute callback with error and API response', function(done) { - prediction.createModel(ID, OPTIONS, function(err, model, apiResponse_) { + it('should execute callback with error and API response', function() { + return prediction.createModel(ID, OPTIONS).then(function () { + throw new Error('should have failed!'); + }, function(err) { assert.strictEqual(err, error); - assert.strictEqual(model, null); - assert.strictEqual(apiResponse_, apiResponse); + assert.strictEqual(err.response, apiResponse); + }); + }); + + it('should execute callback with error and API response (callback)', function(done) { + prediction.createModel(ID, OPTIONS, function(err) { + assert.strictEqual(err, error); + assert.strictEqual(err.response, apiResponse); done(); }); }); @@ -220,7 +296,7 @@ describe('Prediction', function() { beforeEach(function() { prediction.request = function(reqOpts, callback) { - callback(null, apiResponse); + return Promise.resolve(apiResponse); }; prediction.model = function() { @@ -228,14 +304,22 @@ describe('Prediction', function() { }; }); - it('should create a model from the response', function(done) { + it('should create a model from the response', function() { prediction.model = function(id) { assert.strictEqual(id, apiResponse.id); - setImmediate(done); return model; }; - prediction.createModel(ID, OPTIONS, assert.ifError); + return prediction.createModel(ID, OPTIONS); + }); + + it('should create a model from the response (callback)', function(done) { + prediction.model = function(id) { + assert.strictEqual(id, apiResponse.id); + return model; + }; + + prediction.createModel(ID, OPTIONS, done); }); it('should execute callback with model and API response', function(done) { @@ -259,44 +343,74 @@ describe('Prediction', function() { }); describe('getModels', function() { - it('should make the correct request', function(done) { + it('should make the correct request', function() { var query = {}; prediction.request = function(reqOpts) { assert.strictEqual(reqOpts.uri, '/trainedmodels/list'); assert.strictEqual(reqOpts.qs, query); - done(); + return Promise.resolve({ items: [] }); }; - prediction.getModels(query, assert.ifError); + return prediction.getModels(query); }); - it('should use an empty query if one was not provided', function(done) { + it('should make the correct request (callback)', function(done) { + var query = {}; + + prediction.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, '/trainedmodels/list'); + assert.strictEqual(reqOpts.qs, query); + + return Promise.resolve({ items: [] }); + }; + + prediction.getModels(query, done); + }); + + it('should use an empty query if one was not provided', function() { prediction.request = function(reqOpts) { assert.equal(Object.keys(reqOpts.qs).length, 0); - done(); + return Promise.resolve({ items: [] }); + }; + + return prediction.getModels(); + }); + + it('should use an empty query if one was not provided (callback)', function(done) { + prediction.request = function(reqOpts) { + assert.equal(Object.keys(reqOpts.qs).length, 0); + return Promise.resolve({ items: [] }); }; - prediction.getModels(assert.ifError); + prediction.getModels(done); }); describe('error', function() { var error = new Error('Error.'); var apiResponse = {}; + error.response = apiResponse; beforeEach(function() { - prediction.request = function(reqOpts, callback) { - callback(error, apiResponse); + prediction.request = function() { + return Promise.reject(error); }; }); - it('should execute callback with error and API response', function(done) { - prediction.getModels({}, function(err, models, nextQ, apiResponse_) { + it('should execute callback with error and API response', function() { + return prediction.getModels({}).then(function () { + throw new Error('should have failed!'); + }, function(err) { assert.strictEqual(err, error); - assert.strictEqual(models, null); - assert.strictEqual(nextQ, null); - assert.strictEqual(apiResponse_, apiResponse); + assert.strictEqual(err.response, apiResponse); + }); + }); + + it('should execute callback with error and API response (callback)', function(done) { + prediction.getModels({}, function(err) { + assert.strictEqual(err, error); + assert.strictEqual(err.response, apiResponse); done(); }); @@ -313,7 +427,7 @@ describe('Prediction', function() { beforeEach(function() { prediction.request = function(reqOpts, callback) { - callback(null, apiResponse); + return Promise.resolve(apiResponse); }; prediction.model = function() { @@ -321,7 +435,16 @@ describe('Prediction', function() { }; }); - it('should create Models from the response', function(done) { + it('should create Models from the response', function() { + prediction.model = function(id) { + assert.strictEqual(id, MODEL.id); + return MODEL; + }; + + return prediction.getModels({}); + }); + + it('should create Models from the response (callback)', function(done) { prediction.model = function(id) { assert.strictEqual(id, MODEL.id); setImmediate(done); @@ -331,7 +454,29 @@ describe('Prediction', function() { prediction.getModels({}, assert.ifError); }); - it('should set a nextQuery if necessary', function(done) { + it('should set a nextQuery if necessary', function() { + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: 'next-page-token' + }); + + var query = { a: 'b', c: 'd' }; + var originalQuery = extend({}, query); + + prediction.request = function(reqOpts, callback) { + return Promise.resolve(apiResponseWithNextPageToken); + }; + + return prediction.getModels(query).spread(function(models, nextQuery) { + // Check the original query wasn't modified. + assert.deepEqual(query, originalQuery); + + assert.deepEqual(nextQuery, extend({}, query, { + pageToken: apiResponseWithNextPageToken.nextPageToken + })); + }); + }); + + it('should set a nextQuery if necessary (callback)', function(done) { var apiResponseWithNextPageToken = extend({}, apiResponse, { nextPageToken: 'next-page-token' }); @@ -340,7 +485,7 @@ describe('Prediction', function() { var originalQuery = extend({}, query); prediction.request = function(reqOpts, callback) { - callback(null, apiResponseWithNextPageToken); + return Promise.resolve(apiResponseWithNextPageToken); }; prediction.getModels(query, function(err, models, nextQuery) { diff --git a/test/prediction/model.js b/test/prediction/model.js index 6e65b324171e..0e781072599a 100644 --- a/test/prediction/model.js +++ b/test/prediction/model.js @@ -22,6 +22,7 @@ var extend = require('extend'); var mockery = require('mockery-next'); var nodeutil = require('util'); var through = require('through2'); +var Promise = require('bluebird'); var ServiceObject = require('../../lib/common/service-object.js'); var util = require('../../lib/common/util.js'); @@ -115,30 +116,48 @@ describe('Index', function() { }); describe('analyze', function() { - it('should make the correct request', function(done) { + it('should make the correct request', function() { model.request = function(reqOpts) { assert.strictEqual(reqOpts.uri, '/analyze'); - done(); + return Promise.resolve({}); }; - model.analyze(assert.ifError); + return model.analyze(); + }); + + it('should make the correct request (callback)', function(done) { + model.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, '/analyze'); + return Promise.resolve({}); + }; + + model.analyze(done); }); describe('error', function() { var error = new Error('Error.'); var apiResponse = {}; + error.response = apiResponse; beforeEach(function() { - model.request = function(reqOpts, callback) { - callback(error, apiResponse); + model.request = function() { + return Promise.reject(error); }; }); - it('should exec callback with the error & API response', function(done) { - model.analyze(function(err, analysis, apiResponse_) { + it('should exec callback with the error & API response', function() { + return model.analyze().then(function () { + throw new Error('should have failed!'); + }, function(err) { assert.strictEqual(err, error); - assert.strictEqual(analysis, null); - assert.strictEqual(apiResponse_, apiResponse); + assert.strictEqual(err.response, apiResponse); + }); + }); + + it('should exec callback with the error & API response (callback)', function(done) { + model.analyze(function(err) { + assert.strictEqual(err, error); + assert.strictEqual(err.response, apiResponse); done(); }); }); @@ -151,12 +170,22 @@ describe('Index', function() { }; beforeEach(function() { - model.request = function(reqOpts, callback) { - callback(null, apiResponse); + model.request = function() { + return Promise.resolve(apiResponse); }; }); - it('should exec callback with analysis & API response', function(done) { + it('should exec callback with analysis & API response', function() { + return model.analyze().spread(function(analysis, apiResponse_) { + assert.deepEqual(analysis, { + data: apiResponse.dataDescription, + model: apiResponse.modelDescription + }); + assert.strictEqual(apiResponse_, apiResponse); + }); + }); + + it('should exec callback with analysis & API response (callback)', function(done) { model.analyze(function(err, analysis, apiResponse_) { assert.ifError(err); assert.deepEqual(analysis, { @@ -168,11 +197,24 @@ describe('Index', function() { }); }); - it('should default to empty analysis objects', function(done) { + it('should default to empty analysis objects', function() { + var apiResponseWithoutData = {}; + + model.request = function() { + return Promise.resolve(apiResponseWithoutData); + }; + + return model.analyze().spread(function(analysis) { + assert.deepEqual(analysis.data, {}); + assert.deepEqual(analysis.model, {}); + }); + }); + + it('should default to empty analysis objects (callback)', function(done) { var apiResponseWithoutData = {}; - model.request = function(reqOpts, callback) { - callback(null, apiResponseWithoutData); + model.request = function() { + return Promise.resolve(apiResponseWithoutData); }; model.analyze(function(err, analysis) { @@ -382,7 +424,7 @@ describe('Index', function() { }); describe('query', function() { - it('should make the correct request', function(done) { + it('should make the correct request', function() { var input = 'input'; model.request = function(reqOpts) { @@ -393,27 +435,53 @@ describe('Index', function() { csvInstance: [input] } }); - done(); + return Promise.resolve({ outputMulti: [] }); + }; + + return model.query(input); + }); + + it('should make the correct request (callback)', function(done) { + var input = 'input'; + + model.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'POST'); + assert.strictEqual(reqOpts.uri, '/predict'); + assert.deepEqual(reqOpts.json, { + input: { + csvInstance: [input] + } + }); + return Promise.resolve({ outputMulti: [] }); }; - model.query(input, assert.ifError); + model.query(input, done); }); describe('error', function() { var error = new Error('Error.'); var apiResponse = {}; + error.response = apiResponse; beforeEach(function() { - model.request = function(reqOpts, callback) { - callback(error, apiResponse); + model.request = function() { + return Promise.reject(error); }; }); - it('should exec callback with error & API response', function(done) { - model.query('input', function(err, results, apiResponse_) { + it('should exec callback with error & API response', function() { + return model.query('input').then(function () { + throw new Error('should have failed!'); + }, function(err) { assert.strictEqual(err, error); - assert.strictEqual(results, null); - assert.strictEqual(apiResponse_, apiResponse); + assert.strictEqual(err.response, apiResponse); + }); + }); + + it('should exec callback with error & API response (callback)', function(done) { + model.query('input', function(err) { + assert.strictEqual(err, error); + assert.strictEqual(err.response, apiResponse); done(); }); }); @@ -433,12 +501,23 @@ describe('Index', function() { }; beforeEach(function() { - model.request = function(reqOpts, callback) { - callback(null, apiResponse); + model.request = function() { + return Promise.resolve(apiResponse); }; }); - it('should return the results sorted by score', function(done) { + it('should return the results sorted by score', function() { + return model.query('input').spread(function(results) { + assert.strictEqual(results.winner, apiResponse.outputLabel); + + assert.strictEqual(results.scores.length, 2); + + assert.strictEqual(results.scores[0].score, 1); + assert.strictEqual(results.scores[1].score, 0); + }); + }); + + it('should return the results sorted by score (callback)', function(done) { model.query('input', function(err, results) { assert.ifError(err); @@ -453,15 +532,31 @@ describe('Index', function() { }); }); - it('should return the outputValue as the winner', function(done) { + it('should return the outputValue as the winner', function() { + var apiResponseWithValue = extend({}, apiResponse, { + outputValue: 44 + }); + + delete apiResponseWithValue.outputLabel; + + model.request = function() { + return Promise.resolve(apiResponseWithValue); + }; + + return model.query('input').spread(function(results) { + assert.strictEqual(results.winner, apiResponseWithValue.outputValue); + }); + }); + + it('should return the outputValue as the winner (callback)', function(done) { var apiResponseWithValue = extend({}, apiResponse, { outputValue: 44 }); delete apiResponseWithValue.outputLabel; - model.request = function(reqOpts, callback) { - callback(null, apiResponseWithValue); + model.request = function() { + return Promise.resolve(apiResponseWithValue); }; model.query('input', function(err, results) {