Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: introduce interceptors #979

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

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