Skip to content

Commit

Permalink
Merge pull request #979 from stephenplusplus/spp--core-interceptors
Browse files Browse the repository at this point in the history
core: introduce interceptors
  • Loading branch information
callmehiphop committed Dec 1, 2015
2 parents dc131d2 + e9a8eeb commit 5016ca7
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 78 deletions.
3 changes: 3 additions & 0 deletions lib/common/service-object.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ function ServiceObject(config) {
this.id = config.id; // Name or ID (e.g. dataset ID, bucket name, etc.)
this.createMethod = config.createMethod;
this.methods = config.methods || {};
this.interceptors = [];

if (config.methods) {
var allMethodNames = Object.keys(ServiceObject.prototype);
Expand Down Expand Up @@ -312,6 +313,8 @@ ServiceObject.prototype.request = function(reqOpts, callback) {
})
.join('/');

reqOpts.interceptors_ = [].slice.call(this.interceptors);

this.parent.request(reqOpts, callback);
};

Expand Down
17 changes: 17 additions & 0 deletions lib/common/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

'use strict';

var arrify = require('arrify');

/**
* @type {module:common/util}
* @private
Expand Down Expand Up @@ -48,6 +50,8 @@ function Service(config, options) {
this.authClient = this.makeAuthenticatedRequest.authClient;
this.baseUrl = config.baseUrl;
this.getCredentials = this.makeAuthenticatedRequest.getCredentials;
this.globalInterceptors = arrify(options.interceptors_);
this.interceptors = [];
this.projectId = options.projectId;
this.projectIdRequired = config.projectIdRequired !== false;
}
Expand Down Expand Up @@ -84,6 +88,19 @@ Service.prototype.request = function(reqOpts, callback) {
// 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_;

this.makeAuthenticatedRequest(reqOpts, callback);
};

Expand Down
118 changes: 56 additions & 62 deletions lib/common/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,58 +50,6 @@ var missingProjectIdError = new Error([

util.missingProjectIdError = missingProjectIdError;

/**
* Extend a global configuration object with user options provided at the time
* of sub-module instantiation.
*
* Connection details currently come in two ways: `credentials` or
* `keyFilename`. Because of this, we have a special exception when overriding a
* global configuration object. If a user provides either to the global
* configuration, then provides another at submodule instantiation-time, the
* latter is preferred.
*
* @param {object} globalConfig - The global configuration object.
* @param {object=} overrides - The instantiation-time configuration object.
* @return {object}
*
* @example
* // globalConfig = {
* // credentials: {...}
* // }
* Datastore.prototype.dataset = function(options) {
* // options = {
* // keyFilename: 'keyfile.json'
* // }
* return extendGlobalConfig(this.config, options);
* // returns:
* // {
* // keyFilename: 'keyfile.json'
* // }
* };
*/
function extendGlobalConfig(globalConfig, overrides) {
var options = extend({}, globalConfig);
var hasGlobalConnection = options.credentials || options.keyFilename;

overrides = overrides || {};
var isOverridingConnection = overrides.credentials || overrides.keyFilename;

if (hasGlobalConnection && isOverridingConnection) {
delete options.credentials;
delete options.keyFilename;
}

var defaults = {};

if (process.env.GCLOUD_PROJECT) {
defaults.projectId = process.env.GCLOUD_PROJECT;
}

return extend(true, defaults, options, overrides);
}

util.extendGlobalConfig = extendGlobalConfig;

/**
* No op.
*
Expand Down Expand Up @@ -492,22 +440,68 @@ function decorateRequest(reqOpts) {
util.decorateRequest = decorateRequest;

/**
* Merges and validates API configurations
* Extend a global configuration object with user options provided at the time
* of sub-module instantiation.
*
* @throws {Error} If projectId is missing
* Connection details currently come in two ways: `credentials` or
* `keyFilename`. Because of this, we have a special exception when overriding a
* global configuration object. If a user provides either to the global
* configuration, then provides another at submodule instantiation-time, the
* latter is preferred.
*
* @param {?object} globalContext - api level context, this is where the
* gloabl configuration should live
* @param {?object} localConfig - api level configurations
* @return {object} config - merged and validated configurations
* @param {object} globalConfig - The global configuration object.
* @param {object=} overrides - The instantiation-time configuration object.
* @return {object}
*/
function normalizeArguments(globalContext, localConfig, options) {
var globalConfig = globalContext && globalContext.config_ || {};
var config = util.extendGlobalConfig(globalConfig, localConfig);
function extendGlobalConfig(globalConfig, overrides) {
globalConfig = globalConfig || {};
overrides = overrides || {};

var defaultConfig = {};

if (process.env.GCLOUD_PROJECT) {
defaultConfig.projectId = process.env.GCLOUD_PROJECT;
}

var options = extend({}, globalConfig);

var hasGlobalConnection = options.credentials || options.keyFilename;
var isOverridingConnection = overrides.credentials || overrides.keyFilename;

if (hasGlobalConnection && isOverridingConnection) {
delete options.credentials;
delete options.keyFilename;
}

var extendedConfig = extend(true, defaultConfig, options, overrides);

// Preserve the original (not cloned) interceptors.
extendedConfig.interceptors_ = globalConfig.interceptors_;

return extendedConfig;
}

util.extendGlobalConfig = extendGlobalConfig;

/**
* Merge and validate API configurations.
*
* @throws {Error} If a projectId is not specified.
*
* @param {object} globalContext - gcloud-level context.
* @param {object} globalContext.config_ - gcloud-level configuration.
* @param {object} localConfig - Service-level configurations.
* @param {object=} options - Configuration object.
* @param {boolean} options.projectIdRequired - Whether to throw if a project ID
* is required, but not provided by the user. (Default: true)
* @return {object} config - Merged and validated configuration.
*/
function normalizeArguments(globalContext, localConfig, options) {
options = options || {};

if (!config.projectId && options.projectIdRequired !== false) {
var config = util.extendGlobalConfig(globalContext.config_, localConfig);

if (options.projectIdRequired !== false && !config.projectId) {
throw util.missingProjectIdError;
}

Expand Down
63 changes: 62 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,17 @@ var scopedApis = {
* See our [Authentication Guide](#/authentication) for how to obtain the
* necessary credentials for connecting to your project.
*
* ### Advanced Usage
*
* #### Interceptors
*
* All of the returned modules hold a special `interceptors` array you can use
* to have control over the flow of the internal operations of this library. As
* of now, we support a request interceptor, allowing you to tweak all of the
* API request options before the HTTP request is sent.
*
* See the example below for more.
*
* @alias module:gcloud
* @constructor
*
Expand Down Expand Up @@ -284,9 +295,59 @@ var scopedApis = {
* //-
* // `gcs` and `otherGcs` will use their respective credentials for all future
* // API requests.
* //
* // <h4>Interceptors</h4>
* //
* // Use a `request` interceptor to set a custom HTTP header on your requests.
* //-
* gcloud.interceptors.push({
* request: function(requestOptions) {
* requestOptions.headers = requestOptions.headers || {};
* requestOptions.headers['X-Cloud-Trace-Context'] = 'I will be overridden';
* return requestOptions;
* }
* });
*
* //-
* // You can also set an interceptor on the service level, like a Storage
* // object.
* //-
* gcs.interceptors.push({
* request: function(requestOptions) {
* requestOptions.headers = requestOptions.headers || {};
* requestOptions.headers['X-Cloud-Trace-Context'] = 'I will be overridden';
* return requestOptions;
* }
* });
*
* //-
* // Additionally, set one on the service object level, such as a Bucket.
* //-
* bucket.interceptors.push({
* request: function(requestOptions) {
* requestOptions.headers = requestOptions.headers || {};
* requestOptions.headers['X-Cloud-Trace-Context'] = 'I win!';
* return requestOptions;
* }
* });
*
* //-
* // The following request will combine all of the headers, executed in the
* // order from when they were assigned, respecting the hierarchy:
* // global before service before service object.
* //-
* bucket.getMetadata(function() {
* // This HTTP request was sent with the 'I win!' header specified above.
* });
*/
function gcloud(config) {
config = extend(true, { interceptors_: [] }, config);

var gcloudExposedApi = {
config_: config,
interceptors: config.interceptors_
};

return Object.keys(apis).reduce(function(gcloudExposedApi, apiName) {
var Class = apis[apiName];

Expand All @@ -297,7 +358,7 @@ function gcloud(config) {
}

return gcloudExposedApi;
}, { config_: config });
}, gcloudExposedApi);
}

module.exports = extend(gcloud, apis);
18 changes: 18 additions & 0 deletions test/common/service-object.js
Original file line number Diff line number Diff line change
Expand Up @@ -650,5 +650,23 @@ describe('ServiceObject', function() {

serviceObject.request(reqOpts, assert.ifError);
});

it('should pass a clone of the interceptors', function(done) {
serviceObject.interceptors.push({
request: function(reqOpts) {
reqOpts.one = true;
return reqOpts;
}
});

serviceObject.parent.request = function(reqOpts) {
var serviceObjectInterceptors = serviceObject.interceptors;
assert.deepEqual(reqOpts.interceptors_, serviceObjectInterceptors);
assert.notStrictEqual(reqOpts.interceptors_, serviceObjectInterceptors);
done();
};

serviceObject.request({ uri: '' }, assert.ifError);
});
});
});
Loading

0 comments on commit 5016ca7

Please sign in to comment.