Skip to content

Commit

Permalink
Finalizing HTTP clients refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
etki committed Feb 26, 2017
1 parent ad0572e commit 03a8689
Show file tree
Hide file tree
Showing 17 changed files with 335 additions and 135 deletions.
117 changes: 67 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
This repository contains simple SDK to ease development of VoxImplant
scenarios.

Currently it consists of an advanced promise-based HTTP client (relatively to
raw `Net` namespace) and SLF4J-alike logger.
Currently it consists of promise-based HTTP client (a little bit more
advanced than raw `Net.httpRequestAsync`), REST client, which is a
simple sugar wrapper for HTTP client, and SLF4J-alike logger.

It may be installed via classic npm call

Expand Down Expand Up @@ -47,12 +48,12 @@ filtering.
Full logger constructor signature looks like this:

```js
new Slf4j(name, level, writer);
new Slf4j(name, threshold, writer);
```

where name will be printed before real content (to distinguish
different loggers), level is `sdk.logger.Level` enum instance, and
writer is anything that has `.write` method accepting string.
different loggers), threshold is `sdk.logger.Level` enum instance, and
writer is anything that has `.write` method accepting a string.

There is also factory to simplify new logger creation:

Expand All @@ -71,10 +72,15 @@ This client is a simple wrapper around `Net.httpRequestAsync` primitive

```js
var sdk = require('@ama-team/voxengine-sdk'),
client = new sdk.http.basic.Client(Net.httpRequestAsync, {retries: 9, throwOnServerError: true});
options = {
url: 'http://my.backend.com',
retries: 9,
throwOnServerError: true
},
client = new sdk.http.basic.Client(options);

var call = client
.get('http://my.backend.com/magic-phone-number.txt')
.get('/magic-phone-number.txt')
.then(function(response) {
return VoxEngine.callPSTN(response.payload);
});
Expand All @@ -93,19 +99,24 @@ Basic client provides you with following methods:
with following rules:

- Query is an object where every key is associated with a
string value or array of string values
- The same applies to headers object
- Payload may be a string only
- There are no smart actions applied on url, so it will be passed
through as-is (don't forget to specify host)
- Method is a string (surprise!) and can be set using `sdk.http.Method`
enum
string value or array of string values.
- The same applies to headers object.
- Payload may be a string only.
- URL is built by simple concatenation of `options.url` and `url`
method argument, so it would be
`http://my.backend.com/magic-phone-number.txt` in the example. In case
you need to use same client to talk to different hosts, just don't
specify url in options - it would be set to empty string.
- Method is a string and can be set using `sdk.http.Method` enum.

Every method returns a promise that either resolves with response or
rejects with `sdk.http.NetworkException`, `sdk.http.HttpException`, one
of their children or `sdk.http.InvalidConfigurationException`. Whether
an error will be thrown and how many retries will be made is defined in
client settings passed as the second constructor argument:
of their children or `sdk.http.InvalidConfigurationException`.
In case of reject, received exception should have `.name`, `.message`,
`.code`, `.request` and sometimes `.response` fields.

Whether an error will be thrown and how many retries will be made is
defined in client settings passed as first constructor argument:

```js
var settings = {
Expand All @@ -126,58 +137,64 @@ var settings = {
headers: {},
// maximum amount of request retries
retries: 4,
// alternative logger
logger: new sdk.logger.slf4j.Slf4j('scenario.http.client-a')
// alternative logger factory to set another writer/debug level
loggerFactory: new sdk.logger.slf4j.Factory(sdk.logger.Level.Error)
};
```

Tou can tune client as you want to throw exceptions or return responses
You can tune client as you want to throw exceptions or return responses
on certain outcomes. Network errors always result in exception,
however, you may enforce several retries to be made.

## REST client

Provided client exploits `Net` inhabitant capabilities to provide more
fresh interface:
REST client is a wrapper around basic HTTP client that operates over
entities rather than HTTP requests/responses. It adds automatic
serialization/deserialization support, provides `exists`, `get`,
`create`, `set`, `modify` and `delete` methods and adds following rules:

- Any client error results in corresponding exception
- Any server error also results in exception, but retries for specified
amount of times
- `.exists()` method is a wrapper around HEAD-request and returns boolean
in promise, treating 404 as false and any 2xx as true
- `.get()` method is a wrapper around GET-request and returns null on 404
- Non-safe data-changing methods (all others) treat 404 as an error and
trigger `http.NotFoundException`
- There are fallback methods `.request()` and `.execute()` in case you have
some logic depending on response codes.

So you may talk to your backend like that:

```js
var rest = new sdk.http.rest.Client(),
user = rest
.get('/user', {phone: number, size: 1})
.then(function (response) {
return response ? response.content[0] : rest.create('/user', {phone: number});
});
```

REST client is configured very similarly to HTTP client:

```js
var options = {
baseUrl: 'http://backend/api/v1',
// following stuff is purely optional
attempts: 5,
url: 'http://backend/api/v1',
retries: 5,
methodOverrideHeader: 'X-HTTP-Method-Override',
logger: logger,
loggerFactory: new sdk.logger.slf4j.Factory(sdk.logger.Level.Error),
// that's pretty much the default
serializer: {
serialize: function (object) {
return JSON.stringify(object);
},
deserialize: function (string) {
return JSON.parse(string);
}
serialize: JSON.stringify,
deserialize: JSON.parse(string)
},
fixedHeaders: {
headers: {
'Content-Type': 'application/json'
}
},
client = new sdk.http.rest.RestClient(Net.asyncHttpRequest, options);

client.put('/conversation/12345/finished', {timestamp: new Date().getTime()}, {'X-Entity-Version': '12'})
.then(function (response) {
logger.info('Received response: {}', response);
}, function (error) {
logger.error('Failed to perform request: {}', error);
VoxEngine.terminate();
});
client = new sdk.http.rest.Client(options);
```

REST client exposes main `.request(http.Method.*, route, payload, [query], [headers])`
method, as well as shortcuts `.get(route, [query], [headers])`,
`.create(route, [payload], [headers])`,
`.set(route, [payload], [headers])`, and
`.delete(route, [payload], [headers])`. `http.Method.*` is a single
string map, so you can use any HTTP method you may invent via
`.request()` method.

## How do i require this stuff in VoxEngine?

If you don't already know, VoxImplant scripts are basically just a
Expand Down
10 changes: 5 additions & 5 deletions circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ test:
post:
- 'npm run test:report'
# in case tag is pushed, coveralls will try to find current branch and will end on master
# this should be reworked some day, since it is not necessary that tag will end on master branch
- git fetch origin master && git checkout master && git checkout $CIRCLE_SHA1
- 'npm run test:report:publish'
- '[ -z "$CIRCLE_BRANCH" ] || npm run test:report:publish'
- npm run doc
- mkdir -p $CIRCLE_TEST_REPORTS/junit
- cp build/data/xunit/xunit.xml $CIRCLE_TEST_REPORTS/junit/xunit.xml
- cp build/data/xunit/junit.xml $CIRCLE_TEST_REPORTS/junit/junit.xml
- cp -r build/* $CIRCLE_ARTIFACTS/
deployment:
npm-release:
tag: /^\d+\.\d+\.\d+$/
owner: ama-team
commands:
- echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > ~/.npmrc
- npm publish --access public
- npm publish --access public
32 changes: 18 additions & 14 deletions lib/http/_common.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* @module http/_common
*/

var Schema = require('./_schema');

/**
Expand Down Expand Up @@ -32,51 +36,51 @@ var index = {
/**
* @class {IllegalUrlException}
*
* @param {string} message
* @param {int} code
* @property {string} message
* @property {int} code
*/

/**
* @class {MissingHostException}
*
* @param {string} message
* @param {int} code
* @property {string} message
* @property {int} code
*/

/**
* @class {ConnectionErrorException}
*
* @param {string} message
* @param {int} code
* @property {string} message
* @property {int} code
*/

/**
* @class {RedirectVortexException}
*
* @param {string} message
* @param {int} code
* @property {string} message
* @property {int} code
*/

/**
* @class {NetworkErrorException}
*
* @param {string} message
* @param {int} code
* @property {string} message
* @property {int} code
*/

/**
* @class {TimeoutException}
*
* @param {string} message
* @param {int} code
* @property {string} message
* @property {int} code
*/


/**
* @class {VoxEngineErrorException}
*
* @param {string} message
* @param {int} code
* @property {string} message
* @property {int} code
*/

/**
Expand Down
24 changes: 19 additions & 5 deletions lib/http/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function getDefaults() {
throwOnNotFound: false,
retryOnNotFound: false,
retries: 4,
loggerFactory: slf4j.factory()
loggerFactory: new slf4j.Factory()
};
}

Expand Down Expand Up @@ -49,20 +49,22 @@ var ResponseStatus = {
* @property {string} methodOverrideHeader
* @property {Headers} headers Headers to be used on every request.
* @property {int} retries Maximum number of retries allowed for request.
* @property {object} loggerFactory Factory to produce a logger.
* @property {VarArgLoggerFactory} loggerFactory Factory to produce a logger.
*/

/**
* @class
*
* @param {netHttpRequestAsync} [transport]
* @implements IHttpClient
*
* @param {BasicHttpClientSettings|object} [settings]
* @param {netHttpRequestAsync} [transport]
*/
function BasicHttpClient(transport, settings) {
function BasicHttpClient(settings, transport) {
transport = transport || Net.httpRequestAsync;
settings = settings || {};
var defaults = getDefaults(),
logger = setting('loggerFactory')('ama-team.voxengine-sdk.http.basic'),
logger = setting('loggerFactory').create('ama-team.voxengine-sdk.http.basic'),
self = this;

function computeStatus(code) {
Expand Down Expand Up @@ -215,6 +217,18 @@ function BasicHttpClient(transport, settings) {
* @return {HttpResponsePromise}
*/

/**
* Perform PATCH request.
*
* @function BasicHttpClient#patch
*
* @param {string} url
* @param {string} [payload]
* @param {Headers} [headers]
*
* @return {HttpResponsePromise}
*/

/**
* Perform DELETE request.
*
Expand Down
13 changes: 13 additions & 0 deletions lib/http/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,17 @@ Object.keys(_).forEach(function (k) {
exports.basic = require('./basic');
exports.rest = require('./rest');

/**
* @namespace
*
* @property {Function} NetworkException
* @property {Function} IllegalUrlException
* @property {Function} MissingHostException
* @property {Function} ConnectionErrorException
* @property {Function} RedirectVortexException
* @property {Function} NetworkErrorException
* @property {Function} TimeoutException
* @property {Function} VoxEngineErrorException
* @property {Function} HttpException
*/
module.exports = exports;
Loading

0 comments on commit 03a8689

Please sign in to comment.