Introduce plugin.restApi(), deprecate other REST helper methods

Implement REST-related methods on Gerrit (get, post, etc).

REST-related methods on plugin now take plugin URL space into account,
to match GWT UI plugin JS API.

Example:

``` js
Gerrit.install(plugin => {
  // deprecated:
  plugin.get('/foo', json => {
    // work work
  });
  Gerrit.post('/bar', {bar: 'space'}, json => {
    // post succeeds
  });

  // recommended:
  const pluginRestApi = plugin.restApi(plugin.url());
  plugin.get('/foo').then(json => {
    // work work
  });
  plugin.restApi().post('/bar', {bar: 'space'}).then(json => {
    // post succeeds
  });
});
```

Change-Id: I6f537507d76bddec1cac9159cebe1b720ab5caf8
This commit is contained in:
Viktar Donich
2017-10-27 16:33:33 -07:00
parent b6dc470818
commit e5a2f5c5cd
7 changed files with 340 additions and 61 deletions

View File

@@ -31,5 +31,6 @@ limitations under the License.
<script src="gr-js-api-interface.js"></script>
<script src="gr-plugin-endpoints.js"></script>
<script src="gr-plugin-action-context.js"></script>
<script src="gr-plugin-rest-api.js"></script>
<script src="gr-public-js-api.js"></script>
</dom-module>

View File

@@ -96,7 +96,8 @@ limitations under the License.
const response = {foo: 'foo'};
getResponseObjectStub.returns(Promise.resolve(response));
return plugin.get('/url', r => {
assert.isTrue(sendStub.calledWith('GET', '/url'));
assert.isTrue(sendStub.calledWith(
'GET', 'http://test.com/plugins/testplugin/url'));
assert.strictEqual(r, response);
});
});
@@ -105,7 +106,8 @@ limitations under the License.
const response = {foo: 'foo'};
getResponseObjectStub.returns(Promise.resolve(response));
return plugin.get('/url', r => 'rubbish').then(r => {
assert.isTrue(sendStub.calledWith('GET', '/url'));
assert.isTrue(sendStub.calledWith(
'GET', 'http://test.com/plugins/testplugin/url'));
assert.strictEqual(r, response);
});
});
@@ -115,7 +117,8 @@ limitations under the License.
const response = {bar: 'bar'};
getResponseObjectStub.returns(Promise.resolve(response));
return plugin.post('/url', payload, r => {
assert.isTrue(sendStub.calledWith('POST', '/url', payload));
assert.isTrue(sendStub.calledWith(
'POST', 'http://test.com/plugins/testplugin/url', payload));
assert.strictEqual(r, response);
});
});
@@ -125,7 +128,8 @@ limitations under the License.
const response = {bar: 'bar'};
getResponseObjectStub.returns(Promise.resolve(response));
return plugin.put('/url', payload, r => {
assert.isTrue(sendStub.calledWith('PUT', '/url', payload));
assert.isTrue(sendStub.calledWith(
'PUT', 'http://test.com/plugins/testplugin/url', payload));
assert.strictEqual(r, response);
});
});
@@ -134,7 +138,8 @@ limitations under the License.
const response = {status: 204};
sendStub.returns(Promise.resolve(response));
return plugin.delete('/url', r => {
assert.isTrue(sendStub.calledWithExactly('DELETE', '/url'));
assert.isTrue(sendStub.calledWithExactly(
'DELETE', 'http://test.com/plugins/testplugin/url'));
assert.strictEqual(r, response);
});
});
@@ -145,7 +150,8 @@ limitations under the License.
return plugin.delete('/url', r => {
throw new Error('Should not resolve');
}).catch(err => {
assert.isTrue(sendStub.calledWith('DELETE', '/url'));
assert.isTrue(sendStub.calledWith(
'DELETE', 'http://test.com/plugins/testplugin/url'));
assert.equal('text', err);
});
});

View File

@@ -0,0 +1,104 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
(function(window) {
'use strict';
function GrPluginRestApi(opt_prefix) {
this.opt_prefix = opt_prefix || '';
this._restApi = document.createElement('gr-rest-api-interface');
}
GrPluginRestApi.prototype.getLoggedIn = function() {
return this._restApi.getLoggedIn();
};
/**
* Fetch and return native browser REST API Response.
* @param {string} method HTTP Method (GET, POST, etc)
* @param {string} url URL without base path or plugin prefix
* @param {Object=} payload Respected for POST and PUT only.
* @return {!Promise}
*/
GrPluginRestApi.prototype.fetch = function(method, url, opt_payload) {
return this._restApi.send(method, this.opt_prefix + url, opt_payload);
};
/**
* Fetch and parse REST API response, if request succeeds.
* @param {string} method HTTP Method (GET, POST, etc)
* @param {string} url URL without base path or plugin prefix
* @param {Object=} payload Respected for POST and PUT only.
* @return {!Promise} resolves on success, rejects on error.
*/
GrPluginRestApi.prototype.send = function(method, url, opt_payload) {
return this.fetch(method, url, opt_payload).then(response => {
if (response.status < 200 || response.status >= 300) {
return response.text().then(text => {
if (text) {
return Promise.reject(text);
} else {
return Promise.reject(response.status);
}
});
} else {
return this._restApi.getResponseObject(response);
}
});
};
/**
* @param {string} url URL without base path or plugin prefix
* @return {!Promise} resolves on success, rejects on error.
*/
GrPluginRestApi.prototype.get = function(url) {
return this.send('GET', url);
};
/**
* @param {string} url URL without base path or plugin prefix
* @return {!Promise} resolves on success, rejects on error.
*/
GrPluginRestApi.prototype.post = function(url, opt_payload) {
return this.send('POST', url, opt_payload);
};
/**
* @param {string} url URL without base path or plugin prefix
* @return {!Promise} resolves on success, rejects on error.
*/
GrPluginRestApi.prototype.put = function(url, opt_payload) {
return this.send('PUT', url, opt_payload);
};
/**
* @param {string} url URL without base path or plugin prefix
* @return {!Promise} resolves on 204, rejects on error.
*/
GrPluginRestApi.prototype.delete = function(url) {
return this.fetch('DELETE', url).then(response => {
if (response.status !== 204) {
return response.text().then(text => {
if (text) {
return Promise.reject(text);
} else {
return Promise.reject(response.status);
}
});
}
return response;
});
};
window.GrPluginRestApi = GrPluginRestApi;
})(window);

View File

@@ -0,0 +1,124 @@
<!DOCTYPE html>
<!--
Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-rest-api</title>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-js-api-interface.html"/>
<script>
suite('gr-plugin-rest-api tests', () => {
let instance;
let sandbox;
let getResponseObjectStub;
let sendStub;
setup(() => {
sandbox = sinon.sandbox.create();
getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
stub('gr-rest-api-interface', {
getAccount() {
return Promise.resolve({name: 'Judy Hopps'});
},
getResponseObject: getResponseObjectStub,
send(...args) {
return sendStub(...args);
},
});
Gerrit._setPluginsCount(1);
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
instance = new GrPluginRestApi();
});
teardown(() => {
sandbox.restore();
});
test('fetch', () => {
const payload = {foo: 'foo'};
return instance.fetch('HTTP_METHOD', '/url', payload).then(r => {
assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload));
assert.equal(r.status, 200);
assert.isFalse(getResponseObjectStub.called);
});
});
test('send', () => {
const payload = {foo: 'foo'};
const response = {bar: 'bar'};
getResponseObjectStub.returns(Promise.resolve(response));
return instance.send('HTTP_METHOD', '/url', payload).then(r => {
assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload));
assert.strictEqual(r, response);
});
});
test('get', () => {
const response = {foo: 'foo'};
getResponseObjectStub.returns(Promise.resolve(response));
return instance.get('/url').then(r => {
assert.isTrue(sendStub.calledWith('GET', '/url'));
assert.strictEqual(r, response);
});
});
test('post', () => {
const payload = {foo: 'foo'};
const response = {bar: 'bar'};
getResponseObjectStub.returns(Promise.resolve(response));
return instance.post('/url', payload).then(r => {
assert.isTrue(sendStub.calledWith('POST', '/url', payload));
assert.strictEqual(r, response);
});
});
test('put', () => {
const payload = {foo: 'foo'};
const response = {bar: 'bar'};
getResponseObjectStub.returns(Promise.resolve(response));
return instance.put('/url', payload).then(r => {
assert.isTrue(sendStub.calledWith('PUT', '/url', payload));
assert.strictEqual(r, response);
});
});
test('delete works', () => {
const response = {status: 204};
sendStub.returns(Promise.resolve(response));
return instance.delete('/url').then(r => {
assert.isTrue(sendStub.calledWith('DELETE', '/url'));
assert.strictEqual(r, response);
});
});
test('delete fails', () => {
sendStub.returns(Promise.resolve(
{status: 400, text() { return Promise.resolve('text'); }}));
return instance.delete('/url').then(r => {
throw new Error('Should not resolve');
}).catch(err => {
assert.isTrue(sendStub.calledWith('DELETE', '/url'));
assert.equal('text', err);
});
});
});
</script>

View File

@@ -32,6 +32,28 @@
return _restAPI;
};
// TODO (viktard): deprecate in favor of GrPluginRestApi.
function send(method, url, opt_callback, opt_payload) {
return getRestAPI().send(method, url, opt_payload).then(response => {
if (response.status < 200 || response.status >= 300) {
return response.text().then(text => {
if (text) {
return Promise.reject(text);
} else {
return Promise.reject(response.status);
}
});
} else {
return getRestAPI().getResponseObject(response);
}
}).then(response => {
if (opt_callback) {
opt_callback(response);
}
return response;
});
}
const API_VERSION = '0.1';
/**
@@ -131,55 +153,27 @@
};
Plugin.prototype._send = function(method, url, opt_callback, opt_payload) {
return getRestAPI().send(method, url, opt_payload).then(response => {
if (response.status < 200 || response.status >= 300) {
return response.text().then(text => {
if (text) {
return Promise.reject(text);
} else {
return Promise.reject(response.status);
}
});
} else {
return getRestAPI().getResponseObject(response);
}
}).then(response => {
if (opt_callback) {
opt_callback(response);
}
return response;
});
return send(method, this.url(url), opt_callback, opt_payload);
};
Plugin.prototype.get = function(url, opt_callback) {
console.warn('.get() is deprecated! Use .restApi().get()');
return this._send('GET', url, opt_callback);
},
};
Plugin.prototype.post = function(url, payload, opt_callback) {
console.warn('.post() is deprecated! Use .restApi().post()');
return this._send('POST', url, opt_callback, payload);
},
};
Plugin.prototype.put = function(url, payload, opt_callback) {
console.warn('.put() is deprecated! Use .restApi().put()');
return this._send('PUT', url, opt_callback, payload);
},
};
Plugin.prototype.delete = function(url, opt_callback) {
return getRestAPI().send('DELETE', url).then(response => {
if (response.status !== 204) {
return response.text().then(text => {
if (text) {
return Promise.reject(text);
} else {
return Promise.reject(response.status);
}
});
}
if (opt_callback) {
opt_callback(response);
}
return response;
});
},
return Gerrit.delete(this.url(url), opt_callback);
};
Plugin.prototype.changeActions = function() {
return new GrChangeActionsInterface(this,
@@ -205,6 +199,17 @@
return new GrProjectApi(this);
};
/**
* To make REST requests for plugin-provided endpoints, use
* @example
* const pluginRestApi = plugin.restApi(plugin.url());
*
* @param {string} Base url for subsequent .get(), .post() etc requests.
*/
Plugin.prototype.restApi = function(opt_prefix) {
return new GrPluginRestApi(opt_prefix);
};
Plugin.prototype.attributeHelper = function(element) {
return new GrAttributeHelper(element);
};
@@ -272,7 +277,7 @@
Gerrit.getPluginName = function() {
console.warn('Gerrit.getPluginName is not supported in PolyGerrit.',
'Please use self.getPluginName() instead.');
'Please use plugin.getPluginName() instead.');
};
Gerrit.css = function(rulesStr) {
@@ -310,9 +315,45 @@
};
Gerrit.getLoggedIn = function() {
console.warn('Gerrit.getLoggedIn() is deprecated! ' +
'Use plugin.restApi().getLoggedIn()');
return document.createElement('gr-rest-api-interface').getLoggedIn();
};
Gerrit.get = function(url, callback) {
console.warn('.get() is deprecated! Use plugin.restApi().get()');
send('GET', url, callback);
};
Gerrit.post = function(url, payload, callback) {
console.warn('.post() is deprecated! Use plugin.restApi().post()');
send('POST', url, callback, payload);
};
Gerrit.put = function(url, payload, callback) {
console.warn('.put() is deprecated! Use plugin.restApi().put()');
send('PUT', url, callback, payload);
};
Gerrit.delete = function(url, opt_callback) {
console.warn('.delete() is deprecated! Use plugin.restApi().delete()');
return getRestAPI().send('DELETE', url).then(response => {
if (response.status !== 204) {
return response.text().then(text => {
if (text) {
return Promise.reject(text);
} else {
return Promise.reject(response.status);
}
});
}
if (opt_callback) {
opt_callback(response);
}
return response;
});
};
/**
* Polyfill GWT API dependencies to avoid runtime exceptions when loading
* GWT-compiled plugins.

View File

@@ -1372,23 +1372,25 @@
options.headers.set(header, opt_headers[header]);
}
}
return this._auth.fetch(this.getBaseUrl() + url, options)
.then(response => {
if (!response.ok) {
if (opt_errFn) {
return opt_errFn.call(opt_ctx || null, response);
}
this.fire('server-error', {response});
}
return response;
}).catch(err => {
this.fire('network-error', {error: err});
if (opt_errFn) {
return opt_errFn.call(opt_ctx, null, err);
} else {
throw err;
}
});
if (!url.startsWith('http')) {
url = this.getBaseUrl() + url;
}
return this._auth.fetch(url, options).then(response => {
if (!response.ok) {
if (opt_errFn) {
return opt_errFn.call(opt_ctx || null, response);
}
this.fire('server-error', {response});
}
return response;
}).catch(err => {
this.fire('network-error', {error: err});
if (opt_errFn) {
return opt_errFn.call(opt_ctx, null, err);
} else {
throw err;
}
});
},
/**

View File

@@ -147,6 +147,7 @@ limitations under the License.
'shared/gr-js-api-interface/gr-change-actions-js-api_test.html',
'shared/gr-js-api-interface/gr-change-reply-js-api_test.html',
'shared/gr-js-api-interface/gr-js-api-interface_test.html',
'shared/gr-js-api-interface/gr-plugin-rest-api_test.html',
'shared/gr-limited-text/gr-limited-text_test.html',
'shared/gr-linked-chip/gr-linked-chip_test.html',
'shared/gr-linked-text/gr-linked-text_test.html',