diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..79d3d85
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,6 @@
+{
+ "extends": "airbnb/base",
+ "rules": {
+ "indent": [2, 4]
+ }
+}
diff --git a/.gitignore b/.gitignore
index d27938e..3c3629e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1 @@
node_modules
-bower_components
-dist/restful.js
diff --git a/.jshintrc b/.jshintrc
deleted file mode 100644
index 6951bad..0000000
--- a/.jshintrc
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "curly": true,
- "eqeqeq": true,
- "undef": true,
- "esnext": true,
- "jasmine": true
-}
diff --git a/.travis.yml b/.travis.yml
index 88728d7..bdad513 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,3 +5,5 @@ sudo: false
branches:
only:
- master
+before_script:
+ - npm install request
diff --git a/Makefile b/Makefile
index b90985e..ef720d9 100644
--- a/Makefile
+++ b/Makefile
@@ -1,18 +1,23 @@
-.PHONY: build test
+PATH := ${CURDIR}/node_modules/.bin:${PATH}
+
+.PHONY: build es5 test
install:
npm install
- bower install
+ npm install whatwg-fetch
+ npm install request
-build: jshint
- ${CURDIR}/node_modules/.bin/webpack --optimize-minimize --output-file=restful.min.js
+build:
+ NODE_ENV=production webpack
-watch:
- ${CURDIR}/node_modules/.bin/webpack --watch
+build-dev:
+ webpack
-jshint:
- ./node_modules/jshint/bin/jshint src/**/*.js
- ./node_modules/jshint/bin/jshint test/**/*.js
+es5:
+ ${CURDIR}/node_modules/.bin/babel --out-dir=dist/es5 --stage=0 src
+
+watch:
+ webpack -d --watch
-test: build
- CHROME_BIN=`which chromium-browser` ${CURDIR}/node_modules/karma/bin/karma start test/karma.conf.js --single-run
+test:
+ NODE_ENV=test mocha --compilers js:babel/register --colors --reporter=spec --timeout=10000 test/{**,**/**,**/**/**}/*.js
diff --git a/README.md b/README.md
index 805073a..34fa012 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
A pure JS client for interacting with server-side RESTful resources. Think Restangular without Angular.
+*Note*: All examples written in this README use the ES6 specification.
+
## Installation
It is available with bower or npm:
@@ -11,45 +13,50 @@ bower install restful.js
npm install restful.js
```
-Include `restful.min.js` to the HTML, and the `restful` object is now available in the global scope:
-
-```html
-
-```
+The `dist` folder contains two built versions which you can use to include either restful.js or a standalone version.
+Standalone version already embeds `fetch`.
-Alternately, you can use [RequireJS](http://requirejs.org/) or [Browserify](http://browserify.org/) to avoid global scoping.
+Alternately, you can use a module loader like [webpack](http://webpack.github.io/).
```js
-var restful = require('restful.js');
+import restful from 'restful.js';
```
## Usage
### Create a resource targeting your API
-Start by defining the base endpoint for an API, for instance `http://api.example.com`.
+Restful.js needs an HTTP backend in order to perform queries. Two http backend are currently available:
+* [fetch](https://github.com/github/fetch): For using restful.js in a browser.
+* [request](https://github.com/request/request): For using restful.js in Node.js.
+There are defined as optional dependencies and therefore you must install them either with `npm` or `bower` depending your package manager.
+
+Start by defining the base endpoint for an API, for instance `http://api.example.com` with the good http backend.
+For a browser build :
```js
-var api = restful('api.example.com');
+import whatwg-fetch;
+import restful, { fetchBackend } from 'restful.js';
+
+const api = restful('http://api.example.com', fetchBackend(fetch));
```
-You can add headers, port, custom protocol, or even a prefix (like a version number):
+For a node build :
+```js
+import request from 'request';
+import restful, { requestBackend } from 'restful.js';
-```javascript
-var api = restful('api.example.com')
- .header('AuthToken', 'test') // set global header
- .prefixUrl('v1')
- .protocol('https')
- .port(8080);
-// resource now targets `https://api.example.com:8080/v1`
+const api = restful('http://api.example.com', requestBackend(request));
```
+For those who prefers a ready-to-go version, pre built version of restful.js with fetch are available into the `dist` folder.
+
### Collections and Members endpoints
A *collection* is an API endpoint for a list of entities, for instance `http://api.example.com/articles`. Create it using the `all(name)` syntax:
```js
-var articlesCollection = api.all('articles'); // http://api.example.com/articles
+const articlesCollection = api.all('articles'); // http://api.example.com/articles
```
`articlesCollection` is just the description of the collection, the API wasn't fetched yet.
@@ -57,7 +64,7 @@ var articlesCollection = api.all('articles'); // http://api.example.com/article
A *member* is an API endpoint for a single entity, for instance `http://api.example.com/articles/1`. Create it using the `one(name, id)` syntax:
```js
-var articleMember = api.one('articles', 1); // http://api.example.com/articles/1
+const articleMember = api.one('articles', 1); // http://api.example.com/articles/1
```
Just like above, `articleMember` is a description, not an entity.
@@ -65,41 +72,44 @@ Just like above, `articleMember` is a description, not an entity.
You can chain `one()` and `all()` to target the required collection or member:
```js
-var articleMember = api.one('articles', 1); // http://api.example.com/articles/1
-var commentsCollection = articleMember.all('comments'); // http://api.example.com/articles/1/comments
+const articleMember = api.one('articles', 1); // http://api.example.com/articles/1
+const commentsCollection = articleMember.all('comments'); // http://api.example.com/articles/1/comments
```
#### Custom endpoint URL
-In case you need to set a custom endpoint URL, you can use `oneUrl` or `allUrl` methods.
+In case you need to set a custom endpoint URL, you can use `custom` methods.
```js
-var articleMember = api.oneUrl('articles', 'http://custom.url/article?id=1'); // http://custom.url/article?id=1
-```
+const articleCustom = api.custom('articles/beta'); // http://api.example.com/articles/beta
-```js
-var articlesCollection = api.allUrl('articles', 'http://custom.url/article/list'); // http://custom.url/article/list
+// you can add an absolute url
+const articleCustom = api.custom('http://custom.url/articles/beta', false); // http://custom.url/articles/beta
```
+A custom endpoint acts like a member, and therefore you can use `one` and `all` to chain other endpoint with it.
+
#### Entities
-Once you have collections and members endpoints, fetch them to get *entities*. Restful.js exposes `get()` and `getAll()` methods for fetching endpoints. Since these methods are asynchronous, they return a Promise ([based on the ES6 Promise specification](https://github.com/jakearchibald/es6-promise)) for response.
+Once you have collections and members endpoints, fetch them to get *entities*. Restful.js exposes `get()` and `getAll()` methods for fetching endpoints. Since these methods are asynchronous, they return a native Promise for response.
+
+If your application does not support native Promise, you can use a [polyfill](https://github.com/jakearchibald/es6-promise).
```js
-var articleMember = api.one('articles', 1); // http://api.example.com/articles/1
-articleMember.get().then(function(response) {
- var articleEntity = response.body();
+const articleMember = api.one('articles', 1); // http://api.example.com/articles/1
+articleMember.get().then((response) => {
+ const articleEntity = response.body();
- var article = articleEntity.data();
+ const article = articleEntity.data();
console.log(article.title); // hello, world!
});
-var commentsCollection = articleMember.all('comments'); // http://api.example.com/articles/1/comments
-commentsCollection.getAll().then(function(response) {
- var commentEntities = response.body();
+const commentsCollection = articleMember.all('comments'); // http://api.example.com/articles/1/comments
+commentsCollection.getAll().then((response) => {
+ const commentEntities = response.body();
- commentEntities.forEach(function(commentEntity) {
- var comment = commentEntity.data();
+ commentEntities.forEach((commentEntity) => {
+ const comment = commentEntity.data();
console.log(comment.body);
})
});
@@ -109,42 +119,42 @@ commentsCollection.getAll().then(function(response) {
```js
// fetch http://api.example.com/articles/1/comments/4
-var articleMember = api.one('articles', 1);
-var commentMember = articleMember.one('comments', 4);
-commentMember.get().then(function(response) {
+const articleMember = api.one('articles', 1);
+const commentMember = articleMember.one('comments', 4);
+commentMember.get().then((response) => {
//
});
-// equivalent to
-var commentsCollection = articleMember.all('comments');
-commentsCollection.get(4).then(function(response) {
+// equivalent to
+const commentsCollection = articleMember.all('comments');
+commentsCollection.get(4).then((response) => {
//
});
```
### Response
-A response is made from the HTTP response fetched from the endpoint. It exposes `status()`, `headers()`, and `body()` methods. For a `GET` request, the `body` method will return one or a an array of entities. Therefore you can disable this hydration by calling `body(false)`.
+A response is made from the HTTP response fetched from the endpoint. It exposes `statusCode()`, `headers()`, and `body()` methods. For a `GET` request, the `body` method will return one or an array of entities. Therefore you can disable this hydration by calling `body(false)`.
### Entity Data
An entity is made from the HTTP response data fetched from the endpoint. It exposes a `data()` method:
```js
-var articleCollection = api.all('articles'); // http://api.example.com/articles
+const articleCollection = api.all('articles'); // http://api.example.com/articles
// http://api.example.com/articles/1
-api.one('articles', 1).get().then(function(response) {
- var articleEntity = response.body();
+api.one('articles', 1).get().then((response) => {
+ const articleEntity = response.body();
// if the server response was { id: 1, title: 'test', body: 'hello' }
- var article = articleEntity.data();
+ const article = articleEntity.data();
article.title; // returns `test`
article.body; // returns `hello`
// You can also edit it
article.title = 'test2';
// Finally you can easily update it or delete it
articleEntity.save(); // will perform a PUT request
- articleEntity.remove(); // will perform a DELETE request
-}, function(response) {
+ articleEntity.delete(); // will perform a DELETE request
+}, (response) => {
// The reponse code is not >= 200 and < 400
throw new Error('Invalid response');
});
@@ -152,39 +162,50 @@ api.one('articles', 1).get().then(function(response) {
You can also use the entity to continue exploring the API. Entities expose several other methods to chain calls:
-* `entity.one(name, id)`: Query a member child of the entity.
-* `entity.all(name)`: Query a collection child of the entity.
-* `entity.url()`: Get the entity url.
-* `entity.save()`: Save the entity modifications by performing a POST request.
-* `entity.remove()`: Remove the entity by performing a DELETE request.
-* `entity.id()`: Get the id of the entity.
+* `entity.one ( name, id )`: Query a member child of the entity.
+* `entity.all ( name )`: Query a collection child of the entity.
+* `entity.url ()`: Get the entity url.
+* `entity.save ( [, data [, params [, headers ]]] )`: Save the entity modifications by performing a POST request.
+* `entity.delete ( [, data [, params [, headers ]]] )`: Remove the entity by performing a DELETE request.
+* `entity.id ()`: Get the id of the entity.
```js
-var articleMember = api.one('articles', 1); // http://api.example.com/articles/1
-var commentMember = articleMember.one('comments', 3); // http://api.example.com/articles/1/comments/3
+const articleMember = api.one('articles', 1); // http://api.example.com/articles/1
+const commentMember = articleMember.one('comments', 3); // http://api.example.com/articles/1/comments/3
commentMember.get()
- .then(function(response) {
- var commentEntity = response.body();
+ .then((response) => {
+ const commentEntity = response.body();
// You can also call `all` and `one` on an entity
return comment.all('authors').getAll(); // http://api.example.com/articles/1/comments/3/authors
- }).then(function(response) {
- var authorEntities = response.body();
+ }).then((response) => {
+ const authorEntities = response.body();
- authorEntities.forEach(function(authorEntity) {
- var author = authorEntity.data();
+ authorEntities.forEach((authorEntity) => {
+ const author = authorEntity.data();
console.log(author.name);
});
});
```
-Restful.js uses an inheritance pattern when collections or members are chained. That means that when you configure a collection or a member, it will configure all the collection an members chained afterwards.
+`entity.id()` will get the id from its data regarding of the `identifier` of its endpoint. If you are using another name than `id` you can modify it by calling `identifier()` on the endpoint.
+
+```js
+const articleCollection = api.all('articles'); // http://api.example.com/articles
+articleCollection.identifier('_id'); // We use _id as id field
+
+const articleMember = api.one('articles', 1); // http://api.example.com/articles/1
+articleMember.identifier('_id'); // We use _id as id field
+```
+
+Restful.js uses an inheritance pattern when collections or members are chained. That means that when you configure a collection or a member, it will configure all the collection on members chained afterwards.
```js
// configure the api
api.header('AuthToken', 'test');
+api.identifier('_id');
-var articlesCollection = api.all('articles');
+const articlesCollection = api.all('articles');
articlesCollection.get(1); // will send the `AuthToken` header
// You can configure articlesCollection, too
articlesCollection.header('foo', 'bar');
@@ -197,123 +218,132 @@ Restful.js exposes similar methods on collections, members and entities. The nam
### Collection methods
+* `addErrorInterceptor ( interceptor )`: Add an error interceptor. You can alter the whole error.
+* `addRequestInterceptor ( interceptor )`: Add a request interceptor. You can alter the whole request.
+* `addResponseInterceptor ( interceptor )`: Add a response interceptor. You can alter the whole response.
+* `custom ( name [, isRelative = true ] )`: Target a child member with a custom url.
+* `delete ( id [, data [, params [, headers ]]] )`: Delete a member in a collection. Returns a promise with the response.
* `getAll ( [ params [, headers ]] )`: Get a full collection. Returns a promise with an array of entities.
* `get ( id [, params [, headers ]] )`: Get a member in a collection. Returns a promise with an entity.
-* `post ( data [, headers ] )`: Create a member in a collection. Returns a promise with the response.
-* `put ( id, data [, headers ] )`: Update a member in a collection. Returns a promise with the response.
-* `delete ( id [, data, headers ] )`: Delete a member in a collection. Returns a promise with the response.
-* `patch ( id, data [, headers ] )`: Patch a member in a collection. Returns a promise with the response.
-* `head ( id, [, headers ] )`: Perform a HEAD request on a member in a collection. Returns a promise with the response.
-* `url ()`: Get the collection url.
-* `addResponseInterceptor ( interceptor )`: Add a response interceptor. You can only alter data and headers.
-* `addRequestInterceptor ( interceptor )`: Add a request interceptor.
-* `addFullResponseInterceptor ( interceptor )`: Add a full response interceptor. You can alter data and headers.
-* `addFullRequestInterceptor ( interceptor )`: Add a full request interceptor. You can alter params, headers, data, method and url.
+* `head ( id [, params [, headers ]] )`: Perform a HEAD request on a member in a collection. Returns a promise with the response.
* `header ( name, value )`: Add a header.
+* `headers ()`: Get all headers added to the collection.
+* `on ( event, listener )`: Add an event listener on the collection.
+* `once ( event, listener )`: Add an event listener on the collection which will be triggered only once.
+* `patch ( id [, data [, params [, headers ]]] )`: Patch a member in a collection. Returns a promise with the response.
+* `post ( [ data [, params [, headers ]]] )`: Create a member in a collection. Returns a promise with the response.
+* `put ( id [, data [, params [, headers ]]] )`: Update a member in a collection. Returns a promise with the response.
+* `url ()`: Get the collection url.
```js
// http://api.example.com/articles/1/comments/2/authors
-var authorsCollection = api.one('articles', 1).one('comments', 2).all('authors');
-authorsCollection.getAll().then(function(authorEntities) { /* */ });
-authorsCollection.get(1).then(function(authorEntity) { /* */ });
+const authorsCollection = api.one('articles', 1).one('comments', 2).all('authors');
+authorsCollection.getAll().then((authorEntities) => { /* */ });
+authorsCollection.get(1).then((authorEntity) => { /* */ });
```
### Member methods
+* `addErrorInterceptor ( interceptor )`: Add an error interceptor. You can alter the whole error.
+* `addRequestInterceptor ( interceptor )`: Add a request interceptor. You can alter the whole request.
+* `addResponseInterceptor ( interceptor )`: Add a response interceptor. You can alter the whole response.
+* `all ( name )`: Target a child collection `name`.
+* `custom ( name [, isRelative = true ] )`: Target a child member with a custom url.
+* `delete ( [ data [, params [, headers ]]] )`: Delete a member. Returns a promise with the response.
* `get ( [ params [, headers ]] )`: Get a member. Returns a promise with an entity.
-* `put ( data [, headers ] )`: Update a member. Returns a promise with the response.
-* `delete ( [ data, headers ] )`: Delete a member. Returns a promise with the response.
-* `patch ( data [, headers ] )`: Patch a member. Returns a promise with the response.
-* `head ( [ headers ] )`: Perform a HEAD request on a member. Returns a promise with the response.
+* `head ( [ params [, headers ]] )`: Perform a HEAD request on a member. Returns a promise with the response.
+* `header ( name, value )`: Add a header.
+* `headers ()`: Get all headers added to the member.
+* `on ( event, listener )`: Add an event listener on the member.
+* `once ( event, listener )`: Add an event listener on the member which will be triggered only once.
* `one ( name, id )`: Target a child member in a collection `name`.
-* `all ( name )`: Target a child collection `name`.
+* `patch ( [ data [, params [, headers ]]] )`: Patch a member. Returns a promise with the response.
+* `post ( [ data [, params [, headers ]]] )`: Create a member. Returns a promise with the response.
+* `put ( [ data [, params [, headers ]]] )`: Update a member. Returns a promise with the response.
* `url ()`: Get the member url.
-* `addResponseInterceptor ( interceptor )`: Add a response interceptor.
-* `addRequestInterceptor ( interceptor )`: Add a request interceptor. You can only alter data and headers.
-* `addFullResponseInterceptor ( interceptor )`: Add a full response interceptor. You can alter data and headers.
-* `addFullRequestInterceptor ( interceptor )`: Add a full request interceptor. You can alter params, headers, data, method and url.
-* `header ( name, value )`: Add a header.
```js
// http://api.example.com/articles/1/comments/2
-var commentMember = api.one('articles', 1).one('comments', 2);
-commentMember.get().then(function(commentEntity) { /* */ });
-commentMember.delete().then(function(data) { /* */ });
+const commentMember = api.one('articles', 1).one('comments', 2);
+commentMember.get().then((commentEntity) => { /* */ });
+commentMember.delete().then((data) => { /* */ });
```
### Interceptors
-A response or request interceptor is a callback which looks like this:
+An error, response or request interceptor is a callback which looks like this:
```js
-resource.addRequestInterceptor(function(data, headers, method, url) {
- // to edit the headers, just edit the headers object
+resource.addRequestInterceptor((config) => {
+ const { data, headers, method, params, url } = config;
+ // all args had been modified
+ return {
+ data,
+ params,
+ headers,
+ method,
+ url,
+ };
- // You always must return the data object
- return data;
+ // just return modified arguments
+ return {
+ data,
+ headers,
+ };
});
-```
-
-A full request interceptor is a callback which looks like this:
-
-```js
-resource.addFullRequestInterceptor(function(params, headers, data, method, url) {
- //...
+resource.addResponseInterceptor((response, config) => {
+ const { data, headers, statusCode } = response;
// all args had been modified
return {
- params: params,
- headers: headers,
- data: data,
- method: method,
- url: url
+ data,
+ headers,
+ statusCode
};
// just return modified arguments
return {
- headers: headers,
- data: data
+ data,
+ headers,
};
});
-```
-
-A full response interceptor is a callback which looks like this:
-```js
-resource.addFullResponseInterceptor(function(data, headers, method, url) {
- // all args had been modified (method and url is read only)
+resource.addErrorInterceptor((error, config) => {
+ const { message, response } = error;
+ // all args had been modified
return {
- headers: headers,
- data: data
+ message,
+ response,
};
// just return modified arguments
return {
- headers: headers
+ message,
};
});
```
### Response methods
-* `response.status()`: Get the HTTP status code of the response
-* `response.headers()`: Get the HTTP headers of the response
-* `response.body()`: Get the HTTP body of the response. If it is a `GET` request, it will hydrate some entities. To get the raw body call it with `false` as argument.
+* `body ()`: Get the HTTP body of the response. If it is a `GET` request, it will hydrate some entities. To get the raw body call it with `false` as argument.
+* `headers ()`: Get the HTTP headers of the response.
+* `statusCode ()`: Get the HTTP status code of the response.
### Entity methods
-* `entity.data()` : Get the JS object unserialized from the response body (which must be in JSON)
-* `entity.one(name, id)`: Query a member child of the entity.
-* `entity.all(name)`: Query a collection child of the entity.
-* `entity.url()`: Get the entity url.
-* `entity.id()`: Get the id of the entity.
-* `entity.save ( [ headers ] )`: Update the member link to the entity. Returns a promise with the response.
-* `entity.remove ( [ headers ] )`: Delete the member link to the entity. Returns a promise with the response.
+* `all ( name )`: Query a collection child of the entity.
+* `custom ( name [, isRelative = true ] )`: Target a child member with a custom url.
+* `data ()` : Get the JS object unserialized from the response body (which must be in JSON)
+* `id ()`: Get the id of the entity.
+* `one ( name, id )`: Query a member child of the entity.
+* `delete ( [, data [, params [, headers ]]] )`: Delete the member link to the entity. Returns a promise with the response.
+* `save ( [, data [, params [, headers ]]] )`: Update the member link to the entity. Returns a promise with the response.
+* `url ()`: Get the entity url.
```js
// http://api.example.com/articles/1/comments/2
-var commentMember = api.one('articles', 1).one('comments', 2);
-commentMember.get().then(function(commentEntity) {
+const commentMember = api.one('articles', 1).one('comments', 2);
+commentMember.get().then((commentEntity) => {
commentEntity.save();
commentEntity.remove();
});
@@ -321,18 +351,42 @@ commentMember.get().then(function(commentEntity) {
### Error Handling
-To deal with errors, you must use the `catch` method on any of the returned promises:
+To deal with errors, you can either use error interceptors, error callbacks on promise or error events.
```js
-var commentMember = resource.one('articles', 1).one('comments', 2);
+const commentMember = resource.one('articles', 1).one('comments', 2);
commentMember
.get()
- .then(function(commentEntity) { /* */ })
- .catch(function(err) {
+ .then((commentEntity) => { /* */ })
+ .catch((err) => {
// deal with the error
});
+
+commentMember.on('error', (error, config) => {
+ // deal with the error
+});
+```
+
+### Events
+
+Any endpoint (collection or member) is an event emitter. It emits `request`, `response` and `error` events. When it emits an event, it is propagated to all its parents. This way you can listen to all errors, requests and response on your restful instance by listening on your root endpoint.
+
+```js
+api.on('error', (error, config) => {
+ // deal with the error
+});
+
+api.on('request', (config) => {
+ // deal with the request
+});
+
+api.on('response', (config) => {
+ // deal with the response
+});
```
+You can also use `once` method to add a one shot event listener.
+
## Development
Install dependencies:
@@ -341,12 +395,20 @@ Install dependencies:
make install
```
-### Build
+### Development Build
-To rebuild the minified JavaScript you must run: `make build`.
+To rebuild the JavaScript you must run: `make build-dev`.
During development you can run `make watch` to trigger a build at each change.
+### Production build
+
+To build for production (minified files) you must run: `make build`.
+
+### ES5 build
+
+To build the ES5 files you must run: `make es5`.
+
### Tests
```sh
diff --git a/UPGRADE-0.9.md b/UPGRADE-0.9.md
new file mode 100644
index 0000000..69b6cba
--- /dev/null
+++ b/UPGRADE-0.9.md
@@ -0,0 +1,29 @@
+# Upgrade to 0.9
+
+For any change, refer to the README for more details.
+
+## Initialization
+
+Initialization of a restful object has changed.
+
+## Native promise
+
+The polyfill for native promise is no longer included in restful.js, you must include it on your own if needed.
+
+## Targeting a custom url
+
+All methods `customUrl`, `allUrl` and `oneUrl` are replaced by a `custom` method.
+
+## Entities
+
+The `remove` method of an entity is now named `delete`.
+
+``` diff
+- entity.remove()
++ entity.delete()
+```
+
+## HTTP methods
+
+All HTTP methods have now normalized parameters in this order `data, params, headers`.
+`data` depends on the method.
diff --git a/bower.json b/bower.json
index 2064f1c..6e8fe1e 100644
--- a/bower.json
+++ b/bower.json
@@ -1,6 +1,6 @@
{
"name": "restful.js",
- "version": "0.6.1",
+ "version": "0.9.0",
"homepage": "https://github.com/marmelab/restful.js",
"authors": [
"Robin Bressan "
diff --git a/build/restful.fetch.js b/build/restful.fetch.js
new file mode 100644
index 0000000..863fcf9
--- /dev/null
+++ b/build/restful.fetch.js
@@ -0,0 +1,7 @@
+import restful from '../src';
+import fetchBackend from '../src/http/fetch';
+import 'whatwg-fetch';
+
+export default function(baseUrl, httpBackend = fetchBackend(fetch)) {
+ return restful(baseUrl, httpBackend);
+}
diff --git a/build/restful.standalone.js b/build/restful.standalone.js
new file mode 100644
index 0000000..4aa5cb9
--- /dev/null
+++ b/build/restful.standalone.js
@@ -0,0 +1,3 @@
+import restful from '../src';
+
+export default restful;
diff --git a/dist/restful.min.js b/dist/restful.min.js
deleted file mode 100644
index ce40a0f..0000000
--- a/dist/restful.min.js
+++ /dev/null
@@ -1,8 +0,0 @@
-!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):"object"==typeof exports?exports.restful=e():t.restful=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){var n={baseUrl:t,port:e||80,prefixUrl:"",protocol:"http"},r=function(){var t={_http:_["default"](v["default"]),headers:{},fullRequestInterceptors:[],fullResponseInterceptors:[],requestInterceptors:[],responseInterceptors:[]},e={url:function r(){var r=n.protocol+"://"+n.baseUrl;return 80!==n.port&&(r+=":"+n.port),""!==n.prefixUrl&&(r+="/"+n.prefixUrl),r}};return a["default"](e,t),i["default"](function(){return t._http},e)}(),o={_url:null,customUrl:function(t){return"undefined"==typeof t?this._url:(this._url=t,this)},url:function(){return r.url()},one:function(t,e){return p["default"](t,e,o)},oneUrl:function(t,e){return this.customUrl(e),this.one(t,null)},all:function(t){return f["default"](t,o)},allUrl:function(t,e){return this.customUrl(e),this.all(t)}};return o=i["default"](h["default"](r),o),a["default"](o,n),o}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=o;var u=n(1),i=r(u),s=n(2),a=r(s),c=n(3),f=r(c),l=n(8),p=r(l),d=n(9),h=r(d),m=n(10),v=r(m),y=n(30),_=r(y);t.exports=e["default"]},function(t,e){"use strict";function n(t){if(null==t)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(t)}function r(t){var e=Object.getOwnPropertyNames(t);return Object.getOwnPropertySymbols&&(e=e.concat(Object.getOwnPropertySymbols(t))),e.filter(function(e){return o.call(t,e)})}var o=Object.prototype.propertyIsEnumerable;t.exports=Object.assign||function(t,e){for(var o,u,i=n(t),s=1;s=200&&400>o?n(u["default"](t,e)):void r(u["default"](t))})},t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){var n={status:function(){return t.status},body:function(){var n=void 0===arguments[0]?!0:arguments[0];return n&&e?"[object Array]"===Object.prototype.toString.call(t.data)?t.data.map(function(t){return a["default"](t.id,t,e(t.id))}):a["default"](t.data.id,t.data,e(t.data.id)):t.data},headers:function(){return t.headers},config:function(){return t.config}};return i["default"](function(){return t},n)}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=o;var u=n(1),i=r(u),s=n(7),a=r(s);t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t,e,n){var r={_url:null,customUrl:function(t){return"undefined"==typeof t?this._url:(this._url=t,this)},one:function(t,e){return n.one(t,e)},oneUrl:function(t,e){return this.customUrl(e),this.one(t,null)},all:function(t){return n.all(t)},allUrl:function(t,e){return this.customUrl(e),this.all(t)},save:function(t){return n.put(e,t)},remove:function(t){return n["delete"]({},t)},url:function(){return n.url()},id:function(){return t},data:function(){return e}};return i["default"](function(){return e},r)}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=o;var u=n(1),i=r(u);t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t,e,n){var r=n.customUrl&&n.customUrl()?n.customUrl():[n.url(),t,e].join("/"),u=f["default"](r,n()),s={_url:null,customUrl:function(t){return"undefined"==typeof t?this._url:(this._url=t,this)},get:function(t,e){return u.get(t,e).then(function(t){return p["default"](t,function(){return s})})},put:function(t,e){return u.put(t,e).then(function(t){return p["default"](t)})},patch:function(t,e){return u.patch(t,e).then(function(t){return p["default"](t)})},head:function(t,e){return u.head(t,e).then(function(t){return p["default"](t)})},"delete":function(t,e){return u["delete"](t,e).then(function(t){return p["default"](t)})},one:function(t,e){return o(t,e,s)},oneUrl:function(t,e){return this.customUrl(e),this.one(t,null)},all:function(t){return a["default"](t,s)},allUrl:function(t,e){return this.customUrl(e),this.all(t)},url:function(){return r}};return s=i["default"](h["default"](u),s)}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=o;var u=n(1),i=r(u),s=n(3),a=r(s),c=n(4),f=r(c),l=n(5),p=r(l),d=n(9),h=r(d);t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t){function e(){return t}var n=i["default"](e,{addFullRequestInterceptor:function(e){return t.fullRequestInterceptors().push(e),n},fullRequestInterceptors:function(){return t.fullRequestInterceptors()},addFullResponseInterceptor:function(e){return t.fullResponseInterceptors().push(e),n},fullResponseInterceptors:function(){return t.fullResponseInterceptors()},addRequestInterceptor:function(e){return t.requestInterceptors().push(e),n},requestInterceptors:function(){return t.requestInterceptors()},addResponseInterceptor:function(e){return t.responseInterceptors().push(e),n},responseInterceptors:function(){return t.responseInterceptors()},header:function(e,r){return t.headers()[e]=r,n},headers:function(){return t.headers()}});return n}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=o;var u=n(1),i=r(u);t.exports=e["default"]},function(t,e,n){t.exports=n(11)},function(t,e,n){"use strict";var r=n(12),o=n(13),u=n(14),i=n(15),s=n(23);!function(){var t=n(24);t&&"function"==typeof t.polyfill&&t.polyfill()}();var a=t.exports=function c(t){t=o.merge({method:"get",headers:{},transformRequest:r.transformRequest,transformResponse:r.transformResponse},t),t.withCredentials=t.withCredentials||r.withCredentials;var e=[i,void 0],n=Promise.resolve(t);for(c.interceptors.request.forEach(function(t){e.unshift(t.fulfilled,t.rejected)}),c.interceptors.response.forEach(function(t){e.push(t.fulfilled,t.rejected)});e.length;)n=n.then(e.shift(),e.shift());return n.success=function(t){return u("success","then","https://github.com/mzabriskie/axios/blob/master/README.md#response-api"),n.then(function(e){t(e.data,e.status,e.headers,e.config)}),n},n.error=function(t){return u("error","catch","https://github.com/mzabriskie/axios/blob/master/README.md#response-api"),n.then(null,function(e){t(e.data,e.status,e.headers,e.config)}),n},n};a.defaults=r,a.all=function(t){return Promise.all(t)},a.spread=n(29),a.interceptors={request:new s,response:new s},function(){function t(){o.forEach(arguments,function(t){a[t]=function(e,n){return a(o.merge(n||{},{method:t,url:e}))}})}function e(){o.forEach(arguments,function(t){a[t]=function(e,n,r){return a(o.merge(r||{},{method:t,url:e,data:n}))}})}t("delete","get","head"),e("post","put","patch")}()},function(t,e,n){"use strict";var r=n(13),o=/^\)\]\}',?\n/,u={"Content-Type":"application/x-www-form-urlencoded"};t.exports={transformRequest:[function(t,e){return r.isFormData(t)?t:r.isArrayBuffer(t)?t:r.isArrayBufferView(t)?t.buffer:!r.isObject(t)||r.isFile(t)||r.isBlob(t)?t:(!r.isUndefined(e)&&r.isUndefined(e["Content-Type"])&&(e["Content-Type"]="application/json;charset=utf-8"),JSON.stringify(t))}],transformResponse:[function(t){if("string"==typeof t){t=t.replace(o,"");try{t=JSON.parse(t)}catch(e){}}return t}],headers:{common:{Accept:"application/json, text/plain, */*"},patch:r.merge(u),post:r.merge(u),put:r.merge(u)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN"}},function(t,e){"use strict";function n(t){return"[object Array]"===v.call(t)}function r(t){return"[object ArrayBuffer]"===v.call(t)}function o(t){return"[object FormData]"===v.call(t)}function u(t){return"undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(t):t&&t.buffer&&t.buffer instanceof ArrayBuffer}function i(t){return"string"==typeof t}function s(t){return"number"==typeof t}function a(t){return"undefined"==typeof t}function c(t){return null!==t&&"object"==typeof t}function f(t){return"[object Date]"===v.call(t)}function l(t){return"[object File]"===v.call(t)}function p(t){return"[object Blob]"===v.call(t)}function d(t){return t.replace(/^\s*/,"").replace(/\s*$/,"")}function h(t,e){if(null!==t&&"undefined"!=typeof t){var r=n(t)||"object"==typeof t&&!isNaN(t.length);if("object"==typeof t||r||(t=[t]),r)for(var o=0,u=t.length;u>o;o++)e.call(null,t[o],o,t);else for(var i in t)t.hasOwnProperty(i)&&e.call(null,t[i],i,t)}}function m(){var t={};return h(arguments,function(e){h(e,function(e,n){t[n]=e})}),t}var v=Object.prototype.toString;t.exports={isArray:n,isArrayBuffer:r,isFormData:o,isArrayBufferView:u,isString:i,isNumber:s,isObject:c,isUndefined:a,isDate:f,isFile:l,isBlob:p,forEach:h,merge:m,trim:d}},function(t,e){"use strict";t.exports=function(t,e,n){try{console.warn("DEPRECATED method `"+t+"`."+(e?" Use `"+e+"` instead.":"")+" This method will be removed in a future release."),n&&console.warn("For more information about usage see "+n)}catch(r){}}},function(t,e,n){(function(e){"use strict";t.exports=function(t){return new Promise(function(r,o){try{"undefined"!=typeof window?n(17)(r,o,t):"undefined"!=typeof e&&n(17)(r,o,t)}catch(u){o(u)}})}}).call(e,n(16))},function(t,e){function n(){c=!1,i.length?a=i.concat(a):f=-1,a.length&&r()}function r(){if(!c){var t=setTimeout(n);c=!0;for(var e=a.length;e;){for(i=a,a=[];++f1)for(var n=1;n=200&&p.status<300?t:e)(u),p=null}};var d=c(n.url)?i.read(n.xsrfCookieName||r.xsrfCookieName):void 0;if(d&&(l[n.xsrfHeaderName||r.xsrfHeaderName]=d),o.forEach(l,function(t,e){f||"content-type"!==e.toLowerCase()?p.setRequestHeader(e,t):delete l[e]}),n.withCredentials&&(p.withCredentials=!0),n.responseType)try{p.responseType=n.responseType}catch(h){if("json"!==p.responseType)throw h}o.isArrayBuffer(f)&&(f=new DataView(f)),p.send(f)}},function(t,e,n){"use strict";function r(t){return encodeURIComponent(t).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+")}var o=n(13);t.exports=function(t,e){if(!e)return t;var n=[];return o.forEach(e,function(t,e){null!==t&&"undefined"!=typeof t&&(o.isArray(t)||(t=[t]),o.forEach(t,function(t){o.isDate(t)?t=t.toISOString():o.isObject(t)&&(t=JSON.stringify(t)),n.push(r(e)+"="+r(t))}))}),n.length>0&&(t+=(-1===t.indexOf("?")?"?":"&")+n.join("&")),t}},function(t,e,n){"use strict";var r=n(13);t.exports={write:function(t,e,n,o,u,i){var s=[];s.push(t+"="+encodeURIComponent(e)),r.isNumber(n)&&s.push("expires="+new Date(n).toGMTString()),r.isString(o)&&s.push("path="+o),r.isString(u)&&s.push("domain="+u),i===!0&&s.push("secure"),document.cookie=s.join("; ")},read:function(t){var e=document.cookie.match(new RegExp("(^|;\\s*)("+t+")=([^;]*)"));return e?decodeURIComponent(e[3]):null},remove:function(t){this.write(t,"",Date.now()-864e5)}}},function(t,e,n){"use strict";var r=n(13);t.exports=function(t){var e,n,o,u={};return t?(r.forEach(t.split("\n"),function(t){o=t.indexOf(":"),e=r.trim(t.substr(0,o)).toLowerCase(),n=r.trim(t.substr(o+1)),e&&(u[e]=u[e]?u[e]+", "+n:n)}),u):u}},function(t,e,n){"use strict";var r=n(13);t.exports=function(t,e,n){return r.forEach(n,function(n){t=n(t,e)}),t}},function(t,e,n){"use strict";function r(t){var e=t;return i&&(s.setAttribute("href",e),e=s.href),s.setAttribute("href",e),{href:s.href,protocol:s.protocol?s.protocol.replace(/:$/,""):"",host:s.host,search:s.search?s.search.replace(/^\?/,""):"",hash:s.hash?s.hash.replace(/^#/,""):"",hostname:s.hostname,port:s.port,pathname:"/"===s.pathname.charAt(0)?s.pathname:"/"+s.pathname}}var o,u=n(13),i=/(msie|trident)/i.test(navigator.userAgent),s=document.createElement("a");o=r(window.location.href),t.exports=function(t){var e=u.isString(t)?r(t):t;return e.protocol===o.protocol&&e.host===o.host}},function(t,e,n){"use strict";function r(){this.handlers=[]}var o=n(13);r.prototype.use=function(t,e){return this.handlers.push({fulfilled:t,rejected:e}),this.handlers.length-1},r.prototype.eject=function(t){this.handlers[t]&&(this.handlers[t]=null)},r.prototype.forEach=function(t){o.forEach(this.handlers,function(e){null!==e&&t(e)})},t.exports=r},function(t,e,n){var r;(function(t,o,u,i){/*!
- * @overview es6-promise - a tiny implementation of Promises/A+.
- * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
- * @license Licensed under MIT license
- * See https://raw.githubusercontent.com/jakearchibald/es6-promise/master/LICENSE
- * @version 2.3.0
- */
-(function(){"use strict";function s(t){return"function"==typeof t||"object"==typeof t&&null!==t}function a(t){return"function"==typeof t}function c(t){return"object"==typeof t&&null!==t}function f(t){Y=t}function l(t){Q=t}function p(){var e=t.nextTick,n=t.versions.node.match(/^(?:(\d+)\.)?(?:(\d+)\.)?(\*|\d+)$/);return Array.isArray(n)&&"0"===n[1]&&"10"===n[2]&&(e=o),function(){e(y)}}function d(){return function(){K(y)}}function h(){var t=0,e=new et(y),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2}}function m(){var t=new MessageChannel;return t.port1.onmessage=y,function(){t.port2.postMessage(0)}}function v(){return function(){setTimeout(y,1)}}function y(){for(var t=0;W>t;t+=2){var e=ot[t],n=ot[t+1];e(n),ot[t]=void 0,ot[t+1]=void 0}W=0}function _(){try{var t=n(27);return K=t.runOnLoop||t.runOnContext,d()}catch(e){return v()}}function g(){}function b(){return new TypeError("You cannot resolve a promise with itself")}function w(){return new TypeError("A promises callback cannot return that same promise.")}function x(t){try{return t.then}catch(e){return at.error=e,at}}function I(t,e,n,r){try{t.call(e,n,r)}catch(o){return o}}function j(t,e,n){Q(function(t){var r=!1,o=I(n,e,function(n){r||(r=!0,e!==n?O(t,n):R(t,n))},function(e){r||(r=!0,q(t,e))},"Settle: "+(t._label||" unknown promise"));!r&&o&&(r=!0,q(t,o))},t)}function T(t,e){e._state===it?R(t,e._result):e._state===st?q(t,e._result):U(e,void 0,function(e){O(t,e)},function(e){q(t,e)})}function A(t,e){if(e.constructor===t.constructor)T(t,e);else{var n=x(e);n===at?q(t,at.error):void 0===n?R(t,e):a(n)?j(t,e,n):R(t,e)}}function O(t,e){t===e?q(t,b()):s(e)?A(t,e):R(t,e)}function E(t){t._onerror&&t._onerror(t._result),C(t)}function R(t,e){t._state===ut&&(t._result=e,t._state=it,0!==t._subscribers.length&&Q(C,t))}function q(t,e){t._state===ut&&(t._state=st,t._result=e,Q(E,t))}function U(t,e,n,r){var o=t._subscribers,u=o.length;t._onerror=null,o[u]=e,o[u+it]=n,o[u+st]=r,0===u&&t._state&&Q(C,t)}function C(t){var e=t._subscribers,n=t._state;if(0!==e.length){for(var r,o,u=t._result,i=0;ii;i++)U(r.resolve(t[i]),void 0,e,n);return o}function B(t){var e=this;if(t&&"object"==typeof t&&t.constructor===e)return t;var n=new e(g);return O(n,t),n}function L(t){var e=this,n=new e(g);return q(n,t),n}function H(){throw new TypeError("You must pass a resolver function as the first argument to the promise constructor")}function X(){throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.")}function J(t){this._id=mt++,this._state=void 0,this._result=void 0,this._subscribers=[],g!==t&&(a(t)||H(),this instanceof J||X(),F(this,t))}function V(){var t;if("undefined"!=typeof u)t=u;else if("undefined"!=typeof self)t=self;else try{t=Function("return this")()}catch(e){throw new Error("polyfill failed because global object is unavailable in this environment")}var n=t.Promise;(!n||"[object Promise]"!==Object.prototype.toString.call(n.resolve())||n.cast)&&(t.Promise=vt)}var $;$=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)};var K,Y,z,G=$,W=0,Q=({}.toString,function(t,e){ot[W]=t,ot[W+1]=e,W+=2,2===W&&(Y?Y(y):z())}),Z="undefined"!=typeof window?window:void 0,tt=Z||{},et=tt.MutationObserver||tt.WebKitMutationObserver,nt="undefined"!=typeof t&&"[object process]"==={}.toString.call(t),rt="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel,ot=new Array(1e3);z=nt?p():et?h():rt?m():void 0===Z?_():v();var ut=void 0,it=1,st=2,at=new M,ct=new M;k.prototype._validateInput=function(t){return G(t)},k.prototype._validationError=function(){return new Error("Array Methods must be provided an Array")},k.prototype._init=function(){this._result=new Array(this.length)};var ft=k;k.prototype._enumerate=function(){for(var t=this,e=t.length,n=t.promise,r=t._input,o=0;n._state===ut&&e>o;o++)t._eachEntry(r[o],o)},k.prototype._eachEntry=function(t,e){var n=this,r=n._instanceConstructor;c(t)?t.constructor===r&&t._state!==ut?(t._onerror=null,n._settledAt(t._state,e,t._result)):n._willSettleAt(r.resolve(t),e):(n._remaining--,n._result[e]=t)},k.prototype._settledAt=function(t,e,n){var r=this,o=r.promise;o._state===ut&&(r._remaining--,t===st?q(o,n):r._result[e]=n),0===r._remaining&&R(o,r._result)},k.prototype._willSettleAt=function(t,e){var n=this;U(t,void 0,function(t){n._settledAt(it,e,t)},function(t){n._settledAt(st,e,t)})};var lt=N,pt=D,dt=B,ht=L,mt=0,vt=J;J.all=lt,J.race=pt,J.resolve=dt,J.reject=ht,J._setScheduler=f,J._setAsap=l,J._asap=Q,J.prototype={constructor:J,then:function(t,e){var n=this,r=n._state;if(r===it&&!t||r===st&&!e)return this;var o=new this.constructor(g),u=n._result;if(r){var i=arguments[r-1];Q(function(){S(r,o,i,u)})}else U(n,o,t,e);return o},"catch":function(t){return this.then(null,t)}};var yt=V,_t={Promise:vt,polyfill:yt};n(28).amd?(r=function(){return _t}.call(e,n,e,i),!(void 0!==r&&(i.exports=r))):"undefined"!=typeof i&&i.exports?i.exports=_t:"undefined"!=typeof this&&(this.ES6Promise=_t),yt()}).call(this)}).call(e,n(16),n(25).setImmediate,function(){return this}(),n(26)(t))},function(t,e,n){(function(t,r){function o(t,e){this._id=t,this._clearFn=e}var u=n(16).nextTick,i=Function.prototype.apply,s=Array.prototype.slice,a={},c=0;e.setTimeout=function(){return new o(i.call(setTimeout,window,arguments),clearTimeout)},e.setInterval=function(){return new o(i.call(setInterval,window,arguments),clearInterval)},e.clearTimeout=e.clearInterval=function(t){t.close()},o.prototype.unref=o.prototype.ref=function(){},o.prototype.close=function(){this._clearFn.call(window,this._id)},e.enroll=function(t,e){clearTimeout(t._idleTimeoutId),t._idleTimeout=e},e.unenroll=function(t){clearTimeout(t._idleTimeoutId),t._idleTimeout=-1},e._unrefActive=e.active=function(t){clearTimeout(t._idleTimeoutId);var e=t._idleTimeout;e>=0&&(t._idleTimeoutId=setTimeout(function(){t._onTimeout&&t._onTimeout()},e))},e.setImmediate="function"==typeof t?t:function(t){var n=c++,r=arguments.length<2?!1:s.call(arguments,1);return a[n]=!0,u(function(){a[n]&&(r?t.apply(null,r):t.call(null),e.clearImmediate(n))}),n},e.clearImmediate="function"==typeof r?r:function(t){delete a[t]}}).call(e,n(25).setImmediate,n(25).clearImmediate)},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children=[],t.webpackPolyfill=1),t}},function(t,e){},function(t,e){t.exports=function(){throw new Error("define cannot be used indirect")}},function(t,e){"use strict";t.exports=function(t){return function(e){t.apply(null,e)}}},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t,e,n,r){return r=void 0!==r?!!r:!1,function(o,u){if(r)try{o=JSON.parse(o)}catch(i){}for(var s in t)o=t[s](o,u,e,n);if(!r)try{o=JSON.stringify(o)}catch(i){}return o}}function u(t){var e={backend:t,setBackend:function(t){return this.backend=t,s["default"](function(){return t},this)},request:function(t,e){return-1!==["post","put","patch"].indexOf(e.method)&&(e.transformRequest=[o(e.requestInterceptors||[],e.method,e.url)],delete e.requestInterceptors),e.transformResponse=[o(e.responseInterceptors||[],e.method,e.url,!0)],delete e.responseInterceptors,this.backend(e).then(function(t){var n=e.fullResponseInterceptors;for(var r in n){var o=n[r](t.data,t.headers,e.method,e.url);o.data&&(t.data=o.data),o.headers&&(t.headers=o.headers)}return t})}};return s["default"](function(){return t},e)}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=u;var i=n(1),s=r(i);t.exports=e["default"]}])});
\ No newline at end of file
diff --git a/package.json b/package.json
index bb598dd..bb8e1e1 100644
--- a/package.json
+++ b/package.json
@@ -1,25 +1,35 @@
{
"name": "restful.js",
- "version": "0.6.2",
+ "version": "0.9.0",
"repository": "https://github.com/marmelab/restful.js",
+ "bugs": {
+ "url": "https://github.com/marmelab/restful.js/issues"
+ },
"description": "A pure JS client for interacting with server-side RESTful resources. Think Restangular without Angular.",
"scripts": {
"test": "make test"
},
- "main": "dist/restful.min.js",
+ "main": "dist/es5",
"author": "Robin Bressan ",
"license": "MIT",
+ "dependencies": {
+ "babel": "~5.8.23",
+ "babel-loader": "~5.3.2",
+ "immutable": "~3.7.5",
+ "object-assign": "~2.0.0"
+ },
"devDependencies": {
- "axios": "^0.5.2",
- "babel-core": "^5.5.8",
- "babel-loader": "^5.1.4",
- "jshint": "^2.8.0",
- "karma": "0.12.14",
- "karma-chrome-launcher": "0.1.3",
- "karma-jasmine": "0.1.5",
- "karma-phantomjs-launcher": "0.1.4",
- "karma-spec-reporter": "0.0.16",
- "object-assign": "^3.0.0",
- "webpack": "^1.9.11"
+ "babel-eslint": "~4.0.10",
+ "chai": "~3.2.0",
+ "eslint": "~1.2.1",
+ "eslint-config-airbnb": "~0.0.7",
+ "mocha": "~2.3.0",
+ "nock": "~2.12.0",
+ "sinon": "~1.16.1",
+ "webpack": "~1.9.11"
+ },
+ "optionnalDependencies": {
+ "request": "~2.62.0",
+ "whatwg-fetch": "~0.9.0"
}
}
diff --git a/src/http/fetch.js b/src/http/fetch.js
new file mode 100644
index 0000000..c938917
--- /dev/null
+++ b/src/http/fetch.js
@@ -0,0 +1,36 @@
+export default function(fetch) {
+ return (config) => {
+ const url = config.url;
+ delete config.url;
+
+ if (config.data) {
+ config.body = /application\/json/.test(config.headers['Content-Type']) ? JSON.stringify(config.data) : config.data;
+ delete config.data;
+ }
+
+ return fetch(url, config)
+ .then((response) => {
+ return response.json().then((json) => {
+ const headers = {};
+
+ response.headers.forEach((value, name) => {
+ headers[name] = value;
+ });
+
+ const responsePayload = {
+ data: json,
+ headers: headers,
+ statusCode: response.status,
+ };
+
+ if (response.status >= 200 && response.status < 300) {
+ return responsePayload;
+ }
+
+ const error = new Error(response.statusText);
+ error.response = responsePayload;
+ throw error;
+ });
+ });
+ };
+}
diff --git a/src/http/request.js b/src/http/request.js
new file mode 100644
index 0000000..19907f9
--- /dev/null
+++ b/src/http/request.js
@@ -0,0 +1,44 @@
+export default function(request) {
+ return (config) => {
+ if (config.data) {
+ config.form = /application\/json/.test(config.headers['Content-Type']) ? JSON.stringify(config.data) : config.data;
+ delete config.data;
+ }
+
+ if (config.params) {
+ config.qs = config.params;
+ delete config.params;
+ }
+
+ return new Promise((resolve, reject) => {
+ request(config, (err, response, body) => {
+ if (err) {
+ throw err;
+ }
+
+ let data;
+
+ try {
+ data = JSON.parse(body);
+ } catch (e) {
+ data = body;
+ }
+
+ const responsePayload = {
+ data,
+ headers: response.headers,
+ statusCode: response.statusCode,
+ };
+
+ if (response.statusCode >= 200 && response.statusCode < 300) {
+ return resolve(responsePayload);
+ }
+
+ const error = new Error(response.statusMessage);
+ error.response = responsePayload;
+
+ reject(error);
+ });
+ });
+ };
+}
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..c768f60
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,21 @@
+import endpoint from './model/endpoint';
+import fetchBackend from './http/fetch';
+import http from './service/http';
+import { member } from './model/decorator';
+import requestBackend from './http/request';
+import scope from './model/scope';
+
+export default function(baseUrl, httpBackend) {
+ const rootScope = scope();
+ rootScope.assign('config', 'entityIdentifier', 'id');
+ rootScope.set('debug', false);
+ if (!baseUrl && typeof(window) !== undefined && window.location) {
+ rootScope.set('url', `${window.location.protocol}//${window.location.host}`);
+ } else {
+ rootScope.set('url', baseUrl);
+ }
+
+ return member(endpoint(http(httpBackend))(rootScope));
+}
+
+export { fetchBackend, requestBackend };
diff --git a/src/model/collection.js b/src/model/collection.js
deleted file mode 100644
index d61c021..0000000
--- a/src/model/collection.js
+++ /dev/null
@@ -1,101 +0,0 @@
-import assign from 'object-assign';
-import endpoint from 'model/endpoint';
-import responseBuilder from 'service/responseBuilder';
-import member from 'model/member';
-import resource from 'model/resource';
-
-export default function collection(name, parent) {
- var url = parent.customUrl && parent.customUrl() ? parent.customUrl() : [parent.url(), name].join('/');
-
- var refEndpoint = endpoint(url, parent());
-
- function refEndpointFactory(id) {
- var _endpoint = endpoint(url + '/' + id, parent());
-
- // Configure the endpoint
- // We do it this way because the request must have an endpoint which inherits from this collection config
- _endpoint
- .headers(refEndpoint.headers())
- .responseInterceptors(refEndpoint.responseInterceptors())
- .requestInterceptors(refEndpoint.requestInterceptors());
-
- return _endpoint;
- }
-
- function memberFactory(id) {
- var _member = member(name, id, parent);
-
- // Configure the endpoint
- // We do it this way because the response must have a member which inherits from this collection config
- _member()
- .headers(refEndpoint.headers())
- .responseInterceptors(refEndpoint.responseInterceptors())
- .requestInterceptors(refEndpoint.requestInterceptors());
-
- return _member;
- }
-
- var model = {
- get(id, params, headers) {
- return refEndpointFactory(id)
- .get(params, headers)
- .then(function(serverResponse) {
- return responseBuilder(serverResponse, memberFactory);
- });
- },
-
- getAll(params, headers) {
- return refEndpoint
- .getAll(params, headers)
- .then(function(serverResponse) {
- return responseBuilder(serverResponse, memberFactory);
- });
- },
-
- post(data, headers) {
- return refEndpoint
- .post(data, headers)
- .then(function(serverResponse) {
- return responseBuilder(serverResponse);
- });
- },
-
- put(id, data, headers) {
- return refEndpointFactory(id)
- .put(data, headers)
- .then(function(serverResponse) {
- return responseBuilder(serverResponse);
- });
- },
-
- patch(id, data, headers) {
- return refEndpointFactory(id)
- .patch(data, headers)
- .then(function(serverResponse) {
- return responseBuilder(serverResponse);
- });
- },
-
- head(id, data, headers) {
- return refEndpointFactory(id)
- .head(data, headers)
- .then(function(serverResponse) {
- return responseBuilder(serverResponse);
- });
- },
-
- delete(id, data, headers) {
- return refEndpointFactory(id)
- .delete(data, headers)
- .then(function(serverResponse) {
- return responseBuilder(serverResponse);
- });
- },
-
- url() {
- return url;
- },
- };
-
- return assign(resource(refEndpoint), model);
-}
diff --git a/src/model/decorator.js b/src/model/decorator.js
new file mode 100644
index 0000000..fa4efd5
--- /dev/null
+++ b/src/model/decorator.js
@@ -0,0 +1,39 @@
+import assign from 'object-assign';
+
+export function custom(endpoint) {
+ return (name, relative = true) => {
+ if (relative) {
+ return member(endpoint.new(`${endpoint.url()}/${name}`)); // eslint-disable-line no-use-before-define
+ }
+
+ return member(endpoint.new(name)); // eslint-disable-line no-use-before-define
+ };
+}
+
+export function collection(endpoint) {
+ function _bindHttpMethod(method) {
+ return (...args) => {
+ const id = args.shift();
+ return endpoint.new(`${endpoint.url()}/${id}`)[method](...args);
+ };
+ }
+
+ return assign(endpoint, {
+ custom: custom(endpoint),
+ delete: _bindHttpMethod('delete'),
+ getAll: endpoint.get,
+ get: _bindHttpMethod('get'),
+ head: _bindHttpMethod('head'),
+ one: (name, id) => member(endpoint.new(`${endpoint.url()}/${name}/${id}`)), // eslint-disable-line no-use-before-define
+ patch: _bindHttpMethod('patch'),
+ put: _bindHttpMethod('put'),
+ });
+}
+
+export function member(endpoint) {
+ return assign(endpoint, {
+ all: (name) => collection(endpoint.new(`${endpoint.url()}/${name}`)),
+ custom: custom(endpoint),
+ one: (name, id) => member(endpoint.new(`${endpoint.url()}/${name}/${id}`)),
+ });
+}
diff --git a/src/model/endpoint.js b/src/model/endpoint.js
index e7dec42..6dcd0bb 100644
--- a/src/model/endpoint.js
+++ b/src/model/endpoint.js
@@ -1,232 +1,109 @@
import assign from 'object-assign';
-import configurable from 'util/configurable';
-
-export default function endpoint(url, parent) {
- var config = {
- _parent: parent,
- headers: {},
- fullRequestInterceptors: [],
- fullResponseInterceptors: [],
- requestInterceptors: [],
- responseInterceptors:[],
- };
-
- /**
- * Merge the local full request interceptors and the parent's ones
- * @private
- * @return {array} full request interceptors
- */
- function _getFullRequestInterceptors() {
- var current = model,
- fullRequestInterceptors = [];
-
- while (current) {
- fullRequestInterceptors = fullRequestInterceptors.concat(current.fullRequestInterceptors());
-
- current = current._parent ? current._parent() : null;
- }
-
- return fullRequestInterceptors;
- }
-
- /**
- * Merge the local full response interceptors and the parent's ones
- * @private
- * @return {array} full response interceptors
- */
- function _getFullResponseInterceptors() {
- var current = model,
- fullResponseInterceptors = [];
-
- while (current) {
- fullResponseInterceptors = fullResponseInterceptors.concat(current.fullResponseInterceptors());
-
- current = current._parent ? current._parent() : null;
- }
-
- return fullResponseInterceptors;
- }
-
- /**
- * Merge the local request interceptors and the parent's ones
- * @private
- * @return {array} request interceptors
- */
- function _getRequestInterceptors() {
- var current = model,
- requestInterceptors = [];
-
- while (current) {
- requestInterceptors = requestInterceptors.concat(current.requestInterceptors());
-
- current = current._parent ? current._parent() : null;
- }
-
- return requestInterceptors;
- }
-
- /**
- * Merge the local response interceptors and the parent's ones
- * @private
- * @return {array} response interceptors
- */
- function _getResponseInterceptors() {
- var current = model,
- responseInterceptors = [];
+import responseFactory from './response';
+import { fromJS, List, Map } from 'immutable';
+import serialize from '../util/serialize';
+
+/* eslint-disable new-cap */
+export default function(request) {
+ return function endpointFactory(scope) {
+ scope.on('error', () => true); // Add a default error listener to prevent unwanted exception
+ const endpoint = {}; // Persists reference
+
+ function _generateRequestConfig(method, data, params, headers) {
+ let config = Map({
+ errorInterceptors: List(scope.get('errorInterceptors')),
+ headers: Map(scope.get('headers')).mergeDeep(Map(headers)),
+ method,
+ params,
+ requestInterceptors: List(scope.get('requestInterceptors')),
+ responseInterceptors: List(scope.get('responseInterceptors')),
+ url: scope.get('url'),
+ });
+
+ if (data) {
+ if (!config.hasIn(['headers', 'Content-Type'])) {
+ config = config.setIn(['headers', 'Content-Type'], 'application/json;charset=UTF-8');
+ }
+
+ config = config.set('data', fromJS(data));
+ }
- while (current) {
- responseInterceptors = responseInterceptors.concat(current.responseInterceptors());
+ scope.emit('request', serialize(config));
- current = current._parent ? current._parent() : null;
+ return config;
}
- return responseInterceptors;
- }
-
- /**
- * Merge the local headers and the parent's ones
- * @private
- * @return {array} headers
- */
- function _getHeaders() {
- var current = model,
- headers = {};
-
- while (current) {
- assign(headers, current.headers());
-
- current = current._parent ? current._parent() : null;
+ function _onResponse(config, rawResponse) {
+ const response = responseFactory(rawResponse, endpoint);
+ scope.emit('response', response, serialize(config));
+ return response;
}
- return headers;
- }
-
- function _generateRequestConfig(method, url, params = {}, headers = {}, data = null) {
- var config = {
- method: method,
- url: url,
- params: params || {},
- headers: assign({}, _getHeaders(), headers || {}),
- responseInterceptors: _getResponseInterceptors(),
- fullResponseInterceptors: _getFullResponseInterceptors(),
- };
-
- if (data) {
- config.data = data;
- config.requestInterceptors = _getRequestInterceptors();
+ function _onError(config, error) {
+ scope.emit('error', error, serialize(config));
+ throw error;
}
- var interceptors = _getFullRequestInterceptors();
- for (let i in interceptors) {
- let intercepted = interceptors[i](params, headers, data, method, url);
-
- if (intercepted.method) {
- config.method = intercepted.method;
+ function _httpMethodFactory(method, expectData = true) {
+ if (expectData) {
+ return (data, params = null, headers = null) => {
+ const config = _generateRequestConfig(method, data, params, headers);
+ return request(config).then(
+ (rawResponse) => _onResponse(config, rawResponse),
+ (rawResponse) => _onError(config, rawResponse)
+ );
+ };
}
- if (intercepted.url) {
- config.url = intercepted.url;
- }
-
- if (intercepted.params) {
- config.params = intercepted.params;
- }
-
- if (intercepted.headers) {
- config.headers = intercepted.headers;
- }
-
- if (intercepted.data) {
- config.data = intercepted.data;
- }
+ return (params = null, headers = null) => {
+ const config = _generateRequestConfig(method, null, params, headers);
+ return request(config).then(
+ (rawResponse) => _onResponse(config, rawResponse),
+ (error) => _onError(config, error)
+ );
+ };
}
- return config;
- }
-
- var model = {
- get(params, headers) {
- var nextConfig = _generateRequestConfig('get', url, params, headers);
-
- return config._parent().request(
- nextConfig.method,
- nextConfig
- );
- },
-
- getAll(params, headers) {
- var nextConfig = _generateRequestConfig('get', url, params, headers);
-
- return config._parent().request(
- nextConfig.method,
- nextConfig
- );
- },
-
- post(data, headers) {
- headers = headers || {};
- if (!headers['Content-Type']) {
- headers['Content-Type'] = 'application/json;charset=UTF-8';
- }
- var nextConfig = _generateRequestConfig('post', url, {}, headers, data);
-
- return config._parent().request(
- nextConfig.method,
- nextConfig
- );
- },
-
- put(data, headers) {
- headers = headers || {};
- if (!headers['Content-Type']) {
- headers['Content-Type'] = 'application/json;charset=UTF-8';
- }
- var nextConfig = _generateRequestConfig('put', url, {}, headers, data);
-
- return config._parent().request(
- nextConfig.method,
- nextConfig
- );
- },
-
- patch(data, headers) {
- headers = headers || {};
- if (!headers['Content-Type']) {
- headers['Content-Type'] = 'application/json;charset=UTF-8';
- }
- var nextConfig = _generateRequestConfig('patch', url, {}, headers, data);
-
- return config._parent().request(
- nextConfig.method,
- nextConfig
- );
- },
-
- delete(data, headers) {
- var nextConfig = _generateRequestConfig('delete', url, {}, headers, data);
-
- return config._parent().request(
- nextConfig.method,
- nextConfig
- );
- },
+ function addInterceptor(type) {
+ return (interceptor) => {
+ scope.push(`${type}Interceptors`, interceptor);
- head(headers) {
- var nextConfig = _generateRequestConfig('head', url, {}, headers);
-
- return config._parent().request(
- nextConfig.method,
- nextConfig
- );
- },
+ return endpoint;
+ };
+ }
+ assign(endpoint, {
+ addErrorInterceptor: addInterceptor('error'),
+ addRequestInterceptor: addInterceptor('request'),
+ addResponseInterceptor: addInterceptor('response'),
+ delete: _httpMethodFactory('delete'),
+ identifier: newIdentifier => {
+ if (newIdentifier === undefined) {
+ return scope.get('config').get('entityIdentifier');
+ }
+
+ scope.assign('config', 'entityIdentifier', newIdentifier);
+
+ return endpoint;
+ },
+ get: _httpMethodFactory('get', false),
+ head: _httpMethodFactory('head', false),
+ header: (key, value) => scope.assign('headers', key, value),
+ headers: () => scope.get('headers'),
+ new: (url) => {
+ const childScope = scope.new();
+ childScope.set('url', url);
+
+ return endpointFactory(childScope);
+ },
+ on: scope.on,
+ once: scope.once,
+ patch: _httpMethodFactory('patch'),
+ post: _httpMethodFactory('post'),
+ put: _httpMethodFactory('put'),
+ url: () => scope.get('url'),
+ });
+
+ return endpoint;
};
-
- model = assign(function() {
- return config._parent();
- }, model);
-
- configurable(model, config);
-
- return model;
}
diff --git a/src/model/entity.js b/src/model/entity.js
index 329a7e0..32273b2 100644
--- a/src/model/entity.js
+++ b/src/model/entity.js
@@ -1,61 +1,18 @@
-import assign from 'object-assign';
-
-export default function entity(id, data, member) {
- var model = {
- _url: null,
-
- customUrl(url) {
- if (typeof url === 'undefined') {
- return this._url;
- }
-
- this._url = url;
-
- return this;
- },
-
- one(name, id) {
- return member.one(name, id);
- },
-
- oneUrl(name, url) {
- this.customUrl(url);
-
- return this.one(name, null);
- },
-
- all(name) {
- return member.all(name);
- },
-
- allUrl(name, url) {
- this.customUrl(url);
-
- return this.all(name);
- },
-
- save(headers) {
- return member.put(data, headers);
- },
-
- remove(headers) {
- return member.delete({}, headers);
- },
-
- url() {
- return member.url();
+export default function(data, endpoint) {
+ return {
+ all: endpoint.all,
+ custom: endpoint.custom,
+ data() {
+ return data;
},
-
+ delete: endpoint.delete,
id() {
- return id;
+ return data[endpoint.identifier()];
},
-
- data() {
- return data;
- }
+ one: endpoint.one,
+ save(...args) {
+ return endpoint.put(data, ...args);
+ },
+ url: endpoint.url,
};
-
- return assign(function () {
- return data;
- }, model);
}
diff --git a/src/model/member.js b/src/model/member.js
deleted file mode 100644
index 925994c..0000000
--- a/src/model/member.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import assign from 'object-assign';
-import collection from 'model/collection';
-import endpoint from 'model/endpoint';
-import responseBuilder from 'service/responseBuilder';
-import resource from 'model/resource';
-
-export default function member(name, id, parent) {
- var url = parent.customUrl && parent.customUrl() ? parent.customUrl() : [parent.url(), name, id].join('/');
-
- var refEndpoint = endpoint(url, parent());
-
- var model = {
- _url: null,
-
- customUrl(url) {
- if (typeof url === 'undefined') {
- return this._url;
- }
-
- this._url = url;
-
- return this;
- },
-
- get(params, headers) {
- return refEndpoint
- .get(params, headers)
- .then(function(serverResponse) {
- return responseBuilder(serverResponse, function() {
- return model;
- });
- });
- },
-
- put(data, headers) {
- return refEndpoint.put(data, headers)
- .then(function(serverResponse) {
- return responseBuilder(serverResponse);
- });
- },
-
- patch(data, headers) {
- return refEndpoint.patch(data, headers)
- .then(function(serverResponse) {
- return responseBuilder(serverResponse);
- });
- },
-
- head(data, headers) {
- return refEndpoint.head(data, headers)
- .then(function(serverResponse) {
- return responseBuilder(serverResponse);
- });
- },
-
- delete(data, headers) {
- return refEndpoint.delete(data, headers)
- .then(function(serverResponse) {
- return responseBuilder(serverResponse);
- });
- },
-
- one(name, id) {
- return member(name, id, model);
- },
-
- oneUrl(name, url) {
- this.customUrl(url);
-
- return this.one(name, null);
- },
-
- all(name) {
- return collection(name, model);
- },
-
- allUrl(name, url) {
- this.customUrl(url);
-
- return this.all(name);
- },
-
- url() {
- return url;
- },
- };
-
- // We override model because one and all need it as a closure
- model = assign(resource(refEndpoint), model);
-
- return model;
-}
diff --git a/src/model/resource.js b/src/model/resource.js
deleted file mode 100644
index 0fca7a9..0000000
--- a/src/model/resource.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import assign from 'object-assign';
-
-export default function resource(refEndpoint) {
- function modelFunc() {
- return refEndpoint;
- }
-
- var model = assign(modelFunc, {
- addFullRequestInterceptor(interceptor) {
- refEndpoint.fullRequestInterceptors().push(interceptor);
-
- return model;
- },
-
- fullRequestInterceptors() {
- return refEndpoint.fullRequestInterceptors();
- },
-
- addFullResponseInterceptor(interceptor) {
- refEndpoint.fullResponseInterceptors().push(interceptor);
-
- return model;
- },
-
- fullResponseInterceptors() {
- return refEndpoint.fullResponseInterceptors();
- },
-
- addRequestInterceptor(interceptor) {
- refEndpoint.requestInterceptors().push(interceptor);
-
- return model;
- },
-
- requestInterceptors() {
- return refEndpoint.requestInterceptors();
- },
-
- addResponseInterceptor(interceptor) {
- refEndpoint.responseInterceptors().push(interceptor);
-
- return model;
- },
-
- responseInterceptors() {
- return refEndpoint.responseInterceptors();
- },
-
- header(name, value) {
- refEndpoint.headers()[name] = value;
-
- return model;
- },
-
- headers() {
- return refEndpoint.headers();
- }
- });
-
- return model;
-}
diff --git a/src/model/response.js b/src/model/response.js
index c43f357..168560f 100644
--- a/src/model/response.js
+++ b/src/model/response.js
@@ -1,40 +1,41 @@
-import assign from 'object-assign';
-import entity from 'model/entity';
+import entity from './entity';
+import { List } from 'immutable';
+import serialize from '../util/serialize';
-export default function response(serverResponse, memberFactory) {
- var model = {
- status() {
- return serverResponse.status;
- },
+/* eslint-disable new-cap */
+export default function(response, decoratedEndpoint) {
+ const identifier = decoratedEndpoint.identifier();
+ return {
+ statusCode() {
+ return serialize(response.get('statusCode'));
+ },
body(hydrate = true) {
- if (!hydrate || !memberFactory) {
- return serverResponse.data;
+ const data = response.get('data');
+
+ if (!hydrate) {
+ return serialize(data);
}
- if (Object.prototype.toString.call(serverResponse.data) === '[object Array]') {
- return serverResponse.data.map(function(datum) {
- return entity(datum.id, datum, memberFactory(datum.id));
- });
+ if (List.isList(data)) {
+ if (decoratedEndpoint.all) {
+ throw new Error('Unexpected array as response, you should use all method for that');
+ }
+
+ return serialize(data.map((datum) => {
+ const id = datum.get(identifier);
+ return entity(serialize(datum), decoratedEndpoint.custom(`${id}`));
+ }));
}
- return entity(
- serverResponse.data.id,
- serverResponse.data,
- memberFactory(serverResponse.data.id)
- );
- },
+ if (!decoratedEndpoint.all) {
+ throw new Error('Expected array as response, you should use one method for that');
+ }
+ return entity(serialize(data), decoratedEndpoint);
+ },
headers() {
- return serverResponse.headers;
+ return serialize(response.get('headers'));
},
-
- config() {
- return serverResponse.config;
- }
};
-
- return assign(function () {
- return serverResponse;
- }, model);
}
diff --git a/src/model/scope.js b/src/model/scope.js
new file mode 100644
index 0000000..0edb869
--- /dev/null
+++ b/src/model/scope.js
@@ -0,0 +1,69 @@
+import { EventEmitter } from 'events';
+import { List, Map, Iterable } from 'immutable';
+
+/* eslint-disable new-cap */
+export default function scopeFactory(parentScope) {
+ let _data = Map();
+ const _emitter = new EventEmitter();
+
+ const scope = {
+ assign(key, subKey, value) {
+ if (!scope.has(key)) {
+ scope.set(key, Map());
+ }
+
+ _data = _data.setIn([key, subKey], value);
+ return scope;
+ },
+ emit(...args) {
+ _emitter.emit(...args);
+
+ if (parentScope) {
+ parentScope.emit(...args);
+ }
+ },
+ get(key) {
+ const datum = _data.get(key);
+
+ if ((scope.has(key) && !Iterable.isIterable(datum)) || !parentScope) {
+ return datum;
+ } else if (!scope.has(key) && parentScope) {
+ return parentScope.get(key);
+ }
+
+ const parentDatum = parentScope.get(key);
+
+ if (!parentDatum) {
+ return datum;
+ }
+
+ if (List.isList(parentDatum)) {
+ return parentDatum.concat(datum);
+ }
+
+ return parentDatum.mergeDeep(datum);
+ },
+ has(key) {
+ return _data.has(key);
+ },
+ new() {
+ return scopeFactory(scope);
+ },
+ on: _emitter.on.bind(_emitter),
+ once: _emitter.once.bind(_emitter),
+ push(key, value) {
+ if (!scope.has(key)) {
+ scope.set(key, List());
+ }
+
+ _data = _data.update(key, (list) => list.push(value));
+ return scope;
+ },
+ set(key, value) {
+ _data = _data.set(key, value);
+ return scope;
+ },
+ };
+
+ return scope;
+}
diff --git a/src/restful.js b/src/restful.js
deleted file mode 100644
index 3a1ed75..0000000
--- a/src/restful.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import assign from 'object-assign';
-import configurable from 'util/configurable';
-import collection from 'model/collection';
-import member from 'model/member';
-import resource from 'model/resource';
-import axios from 'axios';
-import http from 'service/http';
-
-export default function restful(baseUrl, port) {
- var config = {
- baseUrl: baseUrl,
- port: port || 80,
- prefixUrl: '',
- protocol: 'http',
- };
-
- var fakeEndpoint = (function() {
- var _config = {
- _http: http(axios),
- headers: {},
- fullRequestInterceptors: [],
- fullResponseInterceptors: [],
- requestInterceptors: [],
- responseInterceptors: [],
- };
-
- var model = {
- url() {
- var url = config.protocol + '://' + config.baseUrl;
-
- if (config.port !== 80) {
- url += ':' + config.port;
- }
-
- if (config.prefixUrl !== '') {
- url += '/' + config.prefixUrl;
- }
-
- return url;
- }
- };
-
- configurable(model, _config);
-
- return assign(function() {
- return _config._http;
- }, model);
- }());
-
- var model = {
- _url: null,
-
- customUrl(url) {
- if (typeof url === 'undefined') {
- return this._url;
- }
-
- this._url = url;
-
- return this;
- },
-
- url() {
- return fakeEndpoint.url();
- },
-
- one(name, id) {
- return member(name, id, model);
- },
-
- oneUrl(name, url) {
- this.customUrl(url);
-
- return this.one(name, null);
- },
-
- all(name) {
- return collection(name, model);
- },
-
- allUrl(name, url) {
- this.customUrl(url);
-
- return this.all(name);
- }
- };
-
- // We override model because one and all need it as a closure
- model = assign(resource(fakeEndpoint), model);
-
- configurable(model, config);
-
- return model;
-}
diff --git a/src/service/http.js b/src/service/http.js
index 916bd7f..4000da2 100644
--- a/src/service/http.js
+++ b/src/service/http.js
@@ -1,70 +1,42 @@
import assign from 'object-assign';
-
-function interceptorCallback(interceptors, method, url, isResponseInterceptor) {
- isResponseInterceptor = isResponseInterceptor !== undefined ? !!isResponseInterceptor : false;
-
- return function(data, headers) {
- if (isResponseInterceptor) {
- try {
- data = JSON.parse(data);
- } catch (e) {}
- }
-
- for (var i in interceptors) {
- data = interceptors[i](data, headers, method, url);
- }
-
- if (!isResponseInterceptor) {
- try {
- data = JSON.stringify(data);
- } catch (e) {}
- }
-
- return data;
- };
-}
-
-export default function http(httpBackend) {
- var model = {
- backend: httpBackend,
-
- setBackend(httpBackend) {
- this.backend = httpBackend;
-
- return assign(function() {
- return httpBackend;
- }, this);
- },
-
- request(method, config) {
- if (['post', 'put', 'patch'].indexOf(config.method) !== -1) {
- config.transformRequest = [interceptorCallback(config.requestInterceptors || [], config.method, config.url)];
- delete config.requestInterceptors;
- }
-
- config.transformResponse = [interceptorCallback(config.responseInterceptors || [], config.method, config.url, true)];
- delete config.responseInterceptors;
-
- return this.backend(config).then(function (response) {
- const interceptors = config.fullResponseInterceptors;
- for (let i in interceptors) {
- let intercepted = interceptors[i](response.data, response.headers, config.method, config.url);
-
- if (intercepted.data) {
- response.data = intercepted.data;
+import { fromJS, List, Iterable } from 'immutable';
+import serialize from '../util/serialize';
+
+/* eslint-disable new-cap */
+function reducePromiseList(list, initialValue, params = []) {
+ return list.reduce((promise, nextItem) => {
+ return promise.then(currentValue => {
+ return Promise.resolve(nextItem(serialize(currentValue), ...params))
+ .then((nextValue) => {
+ if (!Iterable.isIterable(currentValue)) {
+ return assign({}, currentValue, nextValue);
}
- if (intercepted.headers) {
- response.headers = intercepted.headers;
- }
- }
+ return currentValue.mergeDeep(nextValue);
+ });
+ });
+ }, Promise.resolve(initialValue));
+}
- return response;
+export default function(httpBackend) {
+ return (config) => {
+ const errorInterceptors = List(config.get('errorInterceptors'));
+ const requestInterceptors = List(config.get('requestInterceptors'));
+ const responseInterceptors = List(config.get('responseInterceptors'));
+ const currentConfig = config
+ .delete('errorInterceptors')
+ .delete('requestInterceptors')
+ .delete('responseInterceptors');
+
+ return reducePromiseList(requestInterceptors, currentConfig)
+ .then((transformedConfig) => {
+ return httpBackend(serialize(transformedConfig)).then((response) => {
+ return reducePromiseList(responseInterceptors, fromJS(response), [serialize(transformedConfig)]);
+ });
+ })
+ .then(null, (error) => {
+ return reducePromiseList(errorInterceptors, error, [serialize(currentConfig)])
+ .then((transformedError) => Promise.reject(transformedError));
});
- }
};
-
- return assign(function() {
- return httpBackend;
- }, model);
}
diff --git a/src/service/responseBuilder.js b/src/service/responseBuilder.js
deleted file mode 100644
index c59c5a2..0000000
--- a/src/service/responseBuilder.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import response from 'model/response';
-
-export default function(serverResponse, memberFactory) {
- return new Promise(function(resolve, reject) {
- var status = serverResponse.status;
-
- if (status >= 200 && status < 400) {
- return resolve(response(
- serverResponse,
- memberFactory
- ));
- }
-
- reject(response(serverResponse));
- });
-}
diff --git a/src/util/configurable.js b/src/util/configurable.js
deleted file mode 100644
index 6d95f59..0000000
--- a/src/util/configurable.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Make a function configurable
- *
- * Configurable functions modify their configuration. The configuration
- * must be an object defined in the function closure.
- *
- * var config = {
- * foo: 1
- * };
- * function bar() {
- * console.log('foo value is', config.foo);
- * }
- * bar(); // 'foo value is 1'
- * config.foo = 3;
- * bar(); // 'foo value is 3'
- *
- * The configurable behavior modifies the function object, adding one
- * method for each item in the configuration.
- *
- * The added methods use the same name as the configuration items. The
- * added methods are both setters and getters, which means that they return
- * the config value when called without argument, and they modify the
- * config value when called with an argument.
- *
- * configurable(bar, config);
- * // Now bar has a foo() method
- *
- * bar.foo(2)
- * bar(); // 'foo value is 2'
- * bar.foo(); // 2
- *
- * @param {Function} targetFunction - The functions to make configurable
- * @param {Object} config - The configuration object with the default config values
- */
-export default function configurable(targetFunction, config) {
- 'use strict';
-
- function configure(item) {
- targetFunction[item] = function(value) {
- if (!arguments.length) {
- return config[item];
- }
- config[item] = value;
- return targetFunction;
- };
- }
-
- for (var item in config) {
- if (!config.hasOwnProperty(item)) {
- continue;
- }
- configure(item);
- }
-}
diff --git a/src/util/serialize.js b/src/util/serialize.js
new file mode 100644
index 0000000..247ab68
--- /dev/null
+++ b/src/util/serialize.js
@@ -0,0 +1,9 @@
+import { Iterable } from 'immutable';
+
+export default function(value) {
+ if (Iterable.isIterable(value)) {
+ return value.toJS();
+ }
+
+ return value;
+}
diff --git a/test/fixture/articles.js b/test/fixture/articles.js
new file mode 100644
index 0000000..6fe67e4
--- /dev/null
+++ b/test/fixture/articles.js
@@ -0,0 +1,37 @@
+export default [
+ {
+ author: 'Pedro',
+ title: 'Beauty Is A Kaleidoscope of Colour',
+ _id: '1',
+ },
+ {
+ author: 'Pedro',
+ title: 'The 5 Most Ridiculously Badass Protesters Ever Photographed',
+ _id: '2',
+ },
+ {
+ author: 'Tuco',
+ title: "What 'The Shining' Would Be Terrifying in The Company’s Histor",
+ _id: '3',
+ },
+ {
+ author: 'Tuco',
+ title: 'The Admirable Reason Why He Took The Right Path',
+ _id: '4',
+ },
+ {
+ author: 'Guillermo',
+ title: 'Looking At You From Seeing Bad Reviews',
+ _id: '5',
+ },
+ {
+ author: 'Alphonso',
+ title: "Fathers, Sons and The World About Palm Oil. Here's What It's Like My Worst Nightmare, and It’s Our Fault",
+ _id: '6',
+ },
+ {
+ author: 'Juan',
+ title: '6 People Who Love Fast Food Restaurants Are Rushing to Offer',
+ _id: '7',
+ },
+];
diff --git a/test/fixture/comments.js b/test/fixture/comments.js
new file mode 100644
index 0000000..27f4237
--- /dev/null
+++ b/test/fixture/comments.js
@@ -0,0 +1,44 @@
+export default {
+ '1': [
+ {
+ content: 'Overly elegant illustration mate',
+ id: '1',
+ },
+ {
+ content: "It's delightful not just fabulous!",
+ id: '2',
+ },
+ {
+ content: 'I want to learn this kind of type! Teach me.',
+ id: '3',
+ },
+ ],
+ '2': [
+ {
+ content: 'Slick. I approve the use of background and pattern!',
+ id: '1',
+ },
+ {
+ content: "I think I'm crying. It's that simple.",
+ id: '2',
+ },
+ {
+ content: 'Very thought out! Designgasmed all over this!',
+ id: '3',
+ },
+ {
+ content: 'Nice use of ivory in this shapes, friend.',
+ id: '4',
+ },
+ ],
+ '5': [
+ {
+ content: 'Such shot, many navigation, so alluring',
+ id: '1',
+ },
+ {
+ content: 'Let me take a nap... great design, anyway.',
+ id: '2',
+ },
+ ],
+};
diff --git a/test/karma.conf.js b/test/karma.conf.js
deleted file mode 100644
index f7bef8c..0000000
--- a/test/karma.conf.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/* global module,process */
-
-module.exports = function(config) {
- 'use strict';
-
- config.set({
- basePath: '../',
- browsers: [process.env.CI ? 'PhantomJS' : 'Chrome'],
- files: [
- {pattern: 'dist/restful.min.js', included: true},
-
- {pattern: 'test/promise-mock.js', included: true},
-
- // test files
- {pattern: 'test/src/**/*.js', included: true},
- ],
- reporters: ['spec'],
- frameworks: ['jasmine'],
- });
-};
diff --git a/test/mock/api.js b/test/mock/api.js
new file mode 100644
index 0000000..951a570
--- /dev/null
+++ b/test/mock/api.js
@@ -0,0 +1,50 @@
+import articles from '../fixture/articles';
+import comments from '../fixture/comments';
+
+export default function(nock) {
+ const server = nock('http://localhost');
+
+ server
+ .get('/articles')
+ .reply(200, articles);
+
+ server
+ .post('/articles')
+ .reply((uri, requestBody) => {
+ return [201, requestBody.toString()];
+ });
+
+ articles.forEach((article) => {
+ server
+ .get(`/articles/${article._id}`)
+ .reply(200, article);
+
+ server
+ .delete(`/articles/${article._id}`)
+ .reply((uri, requestBody) => {
+ return [200, requestBody.toString()];
+ });
+
+ if (!comments[article._id]) {
+ return;
+ }
+
+ server
+ .get(`/articles/${article._id}/comments`)
+ .reply(200, comments[article._id]);
+
+ comments[article._id].forEach((comment) => {
+ server
+ .get(`/articles/${article._id}/comments/${comment.id}`)
+ .reply(200, comment);
+
+ server
+ .put(`/articles/${article._id}/comments/${comment.id}`)
+ .reply((uri, requestBody) => {
+ return [200, requestBody.toString()];
+ });
+ });
+ });
+
+ return server;
+}
diff --git a/test/promise-mock.js b/test/promise-mock.js
deleted file mode 100644
index 84cc876..0000000
--- a/test/promise-mock.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/* global window */
-
-(function() {
- window.Promise = function(cb) {
- var self = this;
- this._state = 'pending';
-
- cb(
- function(result) {
- self._state = 'fullfiled';
- self.result = result;
- },
-
- function(error) {
- self._state = 'rejected';
- self.error = error;
- }
- );
- };
-
- window.Promise.prototype.then = function(successCallback, errorCallback) {
- var self = this;
-
- return new Promise(function(resolve, reject) {
- if (self._state === 'pending') {
- return;
- }
-
- var nextResult;
-
- if (self._state === 'rejected') {
- if (!errorCallback) {
- return;
- }
-
- nextResult = errorCallback(self.error);
-
- if (nextResult && nextResult.then) {
- return nextResult.then(function() {
- reject(nextResult);
- });
- }
-
- return reject(nextResult);
- }
-
- if (!successCallback) {
- return;
- }
-
- nextResult = successCallback(self.result);
-
- if (nextResult && nextResult.then) {
- return nextResult.then(function(result) {
- resolve(result);
- });
- }
-
- resolve(nextResult);
- });
- };
-}());
diff --git a/test/src/http/fetchSpec.js b/test/src/http/fetchSpec.js
new file mode 100644
index 0000000..2641da0
--- /dev/null
+++ b/test/src/http/fetchSpec.js
@@ -0,0 +1,137 @@
+import { expect } from 'chai';
+import fetchBackend from '../../../src/http/fetch';
+import sinon from 'sinon';
+
+describe('Fetch HTTP Backend', () => {
+ let httpBackend;
+ let fetch;
+ let response;
+
+ beforeEach(() => {
+ response = {
+ headers: {
+ forEach: (cb) => {
+ cb('here', 'test');
+ },
+ },
+ json: () => Promise.resolve({ content: 'Yes' }),
+ status: 200,
+ };
+ fetch = sinon.stub().returns(Promise.resolve(response));
+ httpBackend = fetchBackend(fetch);
+ });
+
+ it('should map config to be compatible with fetch package', () => {
+ httpBackend({
+ data: {
+ me: 'you',
+ },
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ params: {
+ asc: 1,
+ },
+ url: '/url',
+ });
+
+ expect(fetch.getCall(0).args).to.deep.equal([
+ '/url',
+ {
+ body: '{"me":"you"}',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ params: {
+ asc: 1,
+ },
+ },
+ ]);
+
+ httpBackend({
+ data: {
+ me: 'you',
+ },
+ headers: {},
+ params: {
+ asc: 1,
+ },
+ url: '/url',
+ });
+
+ expect(fetch.getCall(1).args).to.deep.equal([
+ '/url',
+ {
+ body: {
+ me: 'you',
+ },
+ headers: {},
+ params: {
+ asc: 1,
+ },
+ },
+ ]);
+ });
+
+ it('should correctly format the response when it succeed', (done) => {
+ httpBackend({
+ data: {
+ me: 'you',
+ },
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ params: {
+ asc: 1,
+ },
+ url: '/url',
+ })
+ .then((response) => { // eslint-disable-line no-shadow
+ expect(response).to.deep.equal({
+ data: {
+ content: 'Yes',
+ },
+ headers: {
+ test: 'here',
+ },
+ statusCode: 200,
+ });
+
+ done();
+ })
+ .catch(done);
+ });
+
+ it('should correctly format the error when it fails', (done) => {
+ response.status = 404;
+ response.statusText = 'Not Found';
+
+ httpBackend({
+ data: {
+ me: 'you',
+ },
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ params: {
+ asc: 1,
+ },
+ url: '/url',
+ })
+ .then(done.bind(done, ['It should throw an error']), (error) => {
+ expect(error.message).to.equal('Not Found');
+ expect(error.response).to.deep.equal({
+ data: {
+ content: 'Yes',
+ },
+ headers: {
+ test: 'here',
+ },
+ statusCode: 404,
+ });
+
+ done();
+ })
+ .catch(done);
+ });
+});
diff --git a/test/src/http/requestSpec.js b/test/src/http/requestSpec.js
new file mode 100644
index 0000000..c872409
--- /dev/null
+++ b/test/src/http/requestSpec.js
@@ -0,0 +1,135 @@
+import { expect } from 'chai';
+import requestBackend from '../../../src/http/request';
+import sinon from 'sinon';
+
+describe('Request HTTP Backend', () => {
+ let httpBackend;
+ let request;
+
+ beforeEach(() => {
+ request = sinon.spy();
+ httpBackend = requestBackend(request);
+ });
+
+ it('should map config to be compatible with request package', () => {
+ httpBackend({
+ data: {
+ me: 'you',
+ },
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ params: {
+ asc: 1,
+ },
+ url: '/url',
+ });
+
+ expect(request.getCall(0).args[0]).to.deep.equal({
+ form: '{"me":"you"}',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ qs: {
+ asc: 1,
+ },
+ url: '/url',
+ });
+
+ httpBackend({
+ data: {
+ me: 'you',
+ },
+ headers: {},
+ params: {
+ asc: 1,
+ },
+ url: '/url',
+ });
+
+ expect(request.getCall(1).args[0]).to.deep.equal({
+ form: {
+ me: 'you',
+ },
+ headers: {},
+ qs: {
+ asc: 1,
+ },
+ url: '/url',
+ });
+ });
+
+ it('should correctly format the response when it succeed', (done) => {
+ httpBackend({
+ data: {
+ me: 'you',
+ },
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ params: {
+ asc: 1,
+ },
+ url: '/url',
+ })
+ .then((response) => {
+ expect(response).to.deep.equal({
+ data: {
+ content: 'Yes',
+ },
+ headers: {
+ test: 'here',
+ },
+ statusCode: 200,
+ });
+
+ done();
+ })
+ .catch(done);
+
+ request.getCall(0).args[1](null, {
+ headers: {
+ test: 'here',
+ },
+ statusCode: 200,
+ }, '{"content":"Yes"}');
+ });
+
+ it('should correctly format the error when it fails', (done) => {
+ httpBackend({
+ data: {
+ me: 'you',
+ },
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ params: {
+ asc: 1,
+ },
+ url: '/url',
+ })
+ .then(done.bind(done, ['It should throw an error']), (error) => {
+ expect(error.message).to.equal('Not Found');
+ expect(error.response).to.deep.equal({
+ data: {
+ content: 'Yes',
+ },
+ headers: {
+ test: 'here',
+ },
+ statusCode: 404,
+ });
+
+ done();
+ })
+ .catch(done);
+
+ request.getCall(0).args[1](null, {
+ headers: {
+ test: 'here',
+ },
+ statusCode: 404,
+ statusMessage: 'Not Found',
+ }, '{"content":"Yes"}');
+ });
+});
diff --git a/test/src/model/decoratorSpec.js b/test/src/model/decoratorSpec.js
new file mode 100644
index 0000000..c58f723
--- /dev/null
+++ b/test/src/model/decoratorSpec.js
@@ -0,0 +1,157 @@
+import { expect } from 'chai';
+import * as decorators from '../../../src/model/decorator';
+import sinon from 'sinon';
+
+describe('Decorator Model', () => {
+ let endpoint;
+
+ beforeEach(() => {
+ endpoint = {
+ new: sinon.stub().returns({ fake: true }),
+ url: sinon.stub().returns('/url'),
+ };
+ });
+
+ describe('Member Decorator', () => {
+ it('should add all, custom and one methods to an endpoint', () => {
+ const member = decorators.member(endpoint);
+
+ expect(typeof(member.all)).to.equal('function');
+ expect(typeof(member.custom)).to.equal('function');
+ expect(typeof(member.one)).to.equal('function');
+ });
+
+ it('should create a new endpoint with correct url when one is called', () => {
+ const member = decorators.member(endpoint);
+
+ const childMember = member.one('articles', 1);
+
+ expect(endpoint.new.getCall(0).args).to.deep.equal([
+ '/url/articles/1',
+ ]);
+ expect(childMember.fake).to.be.true;
+ expect(typeof(childMember.all)).to.equal('function');
+ expect(typeof(childMember.custom)).to.equal('function');
+ expect(typeof(childMember.one)).to.equal('function');
+ });
+
+ it('should create a new endpoint with correct url when all is called', () => {
+ const member = decorators.member(endpoint);
+
+ const articles = member.all('articles');
+
+ expect(endpoint.new.getCall(0).args).to.deep.equal([
+ '/url/articles',
+ ]);
+
+ expect(typeof(articles.one)).to.equal('function');
+ expect(articles.all).to.be.undefined;
+ expect(typeof(articles.custom)).to.equal('function');
+ });
+
+ it('should create a new endpoint with correct url when custom is called', () => {
+ const member = decorators.member(endpoint);
+
+ const test = member.custom('test/me');
+
+ expect(endpoint.new.getCall(0).args).to.deep.equal([
+ '/url/test/me',
+ ]);
+
+ expect(typeof(test.one)).to.equal('function');
+ expect(typeof(test.all)).to.equal('function');
+ expect(typeof(test.custom)).to.equal('function');
+
+ const testAbsolute = member.custom('/test/me', false);
+
+ expect(endpoint.new.getCall(1).args).to.deep.equal([
+ '/test/me',
+ ]);
+
+ expect(typeof(testAbsolute.one)).to.equal('function');
+ expect(typeof(testAbsolute.all)).to.equal('function');
+ expect(typeof(testAbsolute.custom)).to.equal('function');
+ });
+ });
+
+ describe('Collection Decorator', () => {
+ it('should add custom and one methods to an endpoint', () => {
+ const collection = decorators.collection(endpoint);
+
+ expect(collection.all).to.be.undefined;
+ expect(typeof(collection.custom)).to.equal('function');
+ expect(typeof(collection.one)).to.equal('function');
+ });
+
+ it('should create a new endpoint with correct url when one is called', () => {
+ const collection = decorators.collection(endpoint);
+
+ const childMember = collection.one('articles', 1);
+
+ expect(endpoint.new.getCall(0).args).to.deep.equal([
+ '/url/articles/1',
+ ]);
+ expect(childMember.fake).to.be.true;
+ expect(typeof(childMember.all)).to.equal('function');
+ expect(typeof(childMember.custom)).to.equal('function');
+ expect(typeof(childMember.one)).to.equal('function');
+ });
+
+ it('should create a new endpoint with correct url when custom is called', () => {
+ const collection = decorators.collection(endpoint);
+
+ const test = collection.custom('test/me');
+
+ expect(endpoint.new.getCall(0).args).to.deep.equal([
+ '/url/test/me',
+ ]);
+
+ expect(typeof(test.one)).to.equal('function');
+ expect(typeof(test.all)).to.equal('function');
+ expect(typeof(test.custom)).to.equal('function');
+
+ const testAbsolute = collection.custom('/test/me', false);
+
+ expect(endpoint.new.getCall(1).args).to.deep.equal([
+ '/test/me',
+ ]);
+
+ expect(typeof(testAbsolute.one)).to.equal('function');
+ expect(typeof(testAbsolute.all)).to.equal('function');
+ expect(typeof(testAbsolute.custom)).to.equal('function');
+ });
+
+ it('should create a new endpoint with correct url on the fly when an http method is called', () => {
+ const collection = decorators.collection(endpoint);
+ endpoint.post = sinon.stub().returns({ hello2: 'world2' });
+ const get = sinon.stub().returns({ hello: 'world' });
+
+ endpoint.new.returns({
+ get,
+ });
+
+ expect(collection.get(1, { test: 1 }, { here: 2 })).to.deep.equal({ hello: 'world' });
+ expect(get.getCall(0).args).to.deep.equal([
+ { test: 1 },
+ { here: 2 },
+ ]);
+ expect(endpoint.new.getCall(0).args).to.deep.equal([
+ '/url/1',
+ ]);
+
+ expect(collection.post({ data: true }, { test: 1 }, { here: 2 })).to.deep.equal({ hello2: 'world2' });
+ expect(endpoint.post.getCall(0).args).to.deep.equal([
+ { data: true },
+ { test: 1 },
+ { here: 2 },
+ ]);
+ });
+
+ it('should call get on the endpoint when getAll is called', () => {
+ endpoint.get = { do: 1 };
+
+ const collection = decorators.collection(endpoint);
+ expect(collection.getAll).to.deep.equal({ do: 1 });
+ });
+ });
+});
diff --git a/test/src/model/endpointSpec.js b/test/src/model/endpointSpec.js
new file mode 100644
index 0000000..11ca0c0
--- /dev/null
+++ b/test/src/model/endpointSpec.js
@@ -0,0 +1,531 @@
+import { expect } from 'chai';
+import endpointModel from '../../../src/model/endpoint';
+import { Map } from 'immutable';
+import scopeModel from '../../../src/model/scope';
+import sinon from 'sinon';
+
+/* eslint-disable new-cap */
+describe('Endpoint model', () => {
+ let endpoint;
+ let request;
+ let scope;
+
+ beforeEach(() => {
+ request = sinon.stub().returns(Promise.resolve(Map({
+ data: { result: true },
+ })));
+
+ scope = scopeModel();
+ scope.set('url', '/url');
+ scope.assign('config', 'entityIdentifier', 'id');
+ sinon.spy(scope, 'on');
+ endpoint = endpointModel(request)(scope);
+ });
+
+ describe('get', () => {
+ it('should call request with correct config when called with no argument', () => {
+ endpoint.get();
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ errorInterceptors: [],
+ headers: {},
+ method: 'get',
+ params: null,
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+
+ it('should call request with correct config when called with params and headers', () => {
+ endpoint.get({ filter: 'asc' }, { Authorization: 'Token xxxx' });
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ errorInterceptors: [],
+ headers: {
+ Authorization: 'Token xxxx',
+ },
+ method: 'get',
+ params: {
+ filter: 'asc',
+ },
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+ });
+
+ describe('post', () => {
+ it('should call request with correct config when called with only data', () => {
+ endpoint.post([
+ 'request',
+ 'data',
+ ]);
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ data: [
+ 'request',
+ 'data',
+ ],
+ errorInterceptors: [],
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ method: 'post',
+ params: null,
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+
+ it('should call request with correct config when called with data and headers', () => {
+ endpoint.post([
+ 'request',
+ 'data',
+ ], {
+ goodbye: 'planet',
+ }, {
+ hello: 'world',
+ });
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ data: [
+ 'request',
+ 'data',
+ ],
+ errorInterceptors: [],
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ hello: 'world',
+ },
+ method: 'post',
+ params: {
+ goodbye: 'planet',
+ },
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+ });
+
+ describe('put', () => {
+ it('should call request with correct config when called with only data', () => {
+ endpoint.put([
+ 'request',
+ 'data',
+ ]);
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ data: [
+ 'request',
+ 'data',
+ ],
+ errorInterceptors: [],
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ method: 'put',
+ params: null,
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+
+ it('should call request with correct config when called with data and headers', () => {
+ endpoint.put([
+ 'request',
+ 'data',
+ ], {
+ goodbye: 'planet',
+ }, {
+ hello: 'world',
+ });
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ data: [
+ 'request',
+ 'data',
+ ],
+ errorInterceptors: [],
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ hello: 'world',
+ },
+ method: 'put',
+ params: {
+ goodbye: 'planet',
+ },
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+ });
+
+ describe('patch', () => {
+ it('should call request with correct config when called with only data', () => {
+ endpoint.patch([
+ 'request',
+ 'data',
+ ]);
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ data: [
+ 'request',
+ 'data',
+ ],
+ errorInterceptors: [],
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ method: 'patch',
+ params: null,
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+
+ it('should call request with correct config when called with data and headers', () => {
+ endpoint.patch([
+ 'request',
+ 'data',
+ ], {
+ goodbye: 'planet',
+ }, {
+ hello: 'world',
+ });
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ data: [
+ 'request',
+ 'data',
+ ],
+ errorInterceptors: [],
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ hello: 'world',
+ },
+ method: 'patch',
+ params: {
+ goodbye: 'planet',
+ },
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+ });
+
+ describe('delete', () => {
+ it('should call request with correct config when called with only data', () => {
+ endpoint.delete([
+ 'request',
+ 'data',
+ ]);
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ data: [
+ 'request',
+ 'data',
+ ],
+ errorInterceptors: [],
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ method: 'delete',
+ params: null,
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+
+ it('should call request with correct config when called with data and headers', () => {
+ endpoint.delete([
+ 'request',
+ 'data',
+ ], {
+ goodbye: 'planet',
+ }, {
+ hello: 'world',
+ });
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ data: [
+ 'request',
+ 'data',
+ ],
+ errorInterceptors: [],
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ hello: 'world',
+ },
+ method: 'delete',
+ params: {
+ goodbye: 'planet',
+ },
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+ });
+
+ describe('head', () => {
+ it('should call request with correct config when called with no argument', () => {
+ endpoint.head();
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ errorInterceptors: [],
+ headers: {},
+ method: 'head',
+ params: null,
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+
+ it('should call request with correct config when called with params and headers', () => {
+ endpoint.head({ filter: 'asc' }, { Authorization: 'Token xxxx' });
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ errorInterceptors: [],
+ headers: {
+ Authorization: 'Token xxxx',
+ },
+ method: 'head',
+ params: {
+ filter: 'asc',
+ },
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+ });
+
+ describe('interceptors', () => {
+ it('should add a request interceptor and pass it to the request callback when one request is performed', () => {
+ endpoint.addRequestInterceptor({ hello: 'world' });
+
+ endpoint.get();
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ errorInterceptors: [],
+ headers: {},
+ method: 'get',
+ params: null,
+ requestInterceptors: [{ hello: 'world' }],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+
+ it('should add a response interceptor and pass it to the request callback when one request is performed', () => {
+ endpoint.addResponseInterceptor({ hello2: 'world2' });
+
+ endpoint.get();
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ errorInterceptors: [],
+ headers: {},
+ method: 'get',
+ params: null,
+ requestInterceptors: [],
+ responseInterceptors: [{ hello2: 'world2' }],
+ url: '/url',
+ });
+ });
+
+ it('should add a error interceptor and pass it to the request callback when one request is performed', () => {
+ endpoint.addErrorInterceptor({ hello3: 'world3' });
+
+ endpoint.get();
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ errorInterceptors: [{ hello3: 'world3' }],
+ headers: {},
+ method: 'get',
+ params: null,
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+ });
+
+ describe('headers', () => {
+ it('should add an header to all request done by the endpoint when header is called', () => {
+ endpoint.header('Authorization', 'xxxx');
+ endpoint.get(null, { hello: 'world' });
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ errorInterceptors: [],
+ headers: {
+ Authorization: 'xxxx',
+ hello: 'world',
+ },
+ method: 'get',
+ params: null,
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+
+ it('should override existing headers when performing a request with the same header name', () => {
+ endpoint.header('Authorization', 'xxxx');
+ endpoint.get(null, { Authorization: 'yyyy' });
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ errorInterceptors: [],
+ headers: {
+ Authorization: 'yyyy',
+ },
+ method: 'get',
+ params: null,
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ });
+ });
+
+ it('should return the endpoint url when url is called', () => {
+ expect(endpoint.url()).to.equal('/url');
+ });
+
+ it('should create a child endpoint when new is called with a child scope', () => {
+ const childEndpoint = endpoint.new('/url2');
+
+ expect(childEndpoint.url()).to.equal('/url2');
+
+ endpoint.header('Authorization', 'xxxx');
+ endpoint.header('hello', 'world');
+ endpoint.addRequestInterceptor({ alpha: 'beta' });
+
+ childEndpoint.header('hello', 'planet');
+ childEndpoint.addResponseInterceptor({ omega: 'gamma' });
+
+ childEndpoint.post({ content: 'test' });
+
+ expect(request.getCall(0).args[0].toJS()).to.deep.equal({
+ data: { content: 'test' },
+ errorInterceptors: [],
+ headers: {
+ Authorization: 'xxxx',
+ 'Content-Type': 'application/json;charset=UTF-8',
+ hello: 'planet',
+ },
+ method: 'post',
+ params: null,
+ requestInterceptors: [
+ { alpha: 'beta' },
+ ],
+ responseInterceptors: [
+ { omega: 'gamma' },
+ ],
+ url: '/url2',
+ });
+ });
+
+ it('should emit a request event when a request is made', () => {
+ const listener = sinon.spy();
+ endpoint.on('request', listener);
+
+ endpoint.get();
+
+ expect(listener.getCall(0).args).to.deep.equal([{
+ errorInterceptors: [],
+ headers: {},
+ method: 'get',
+ params: null,
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ }]);
+ });
+
+ it('should emit a response event when a response is received', (done) => {
+ const listener = sinon.spy();
+ endpoint.on('response', listener);
+
+ endpoint.get().then((response) => {
+ expect(listener.getCall(0).args[0].body(false)).to.deep.equal({
+ result: true,
+ });
+ expect(listener.getCall(0).args[1]).to.deep.equal({
+ errorInterceptors: [],
+ headers: {},
+ method: 'get',
+ params: null,
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ });
+ expect(response.body(false)).to.deep.equal({
+ result: true,
+ });
+
+ done();
+ }).catch(done);
+ });
+
+ it('should emit a error event when an error response is received', (done) => {
+ const listener = sinon.spy();
+ endpoint.on('error', listener);
+ request.returns(Promise.reject(new Error('Oops')));
+
+ endpoint.get().then(done.bind(done, ['It should throw an error']), (error) => {
+ expect(listener.getCall(0).args).to.deep.equal([
+ new Error('Oops'),
+ {
+ errorInterceptors: [],
+ headers: {},
+ method: 'get',
+ params: null,
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/url',
+ },
+ ]);
+ expect(error.message).to.equal('Oops');
+ done();
+ }).catch(done);
+ });
+
+ it('should emit event across parent endpoints', (done) => {
+ const listener = sinon.spy();
+ const childEndpoint = endpoint.new('/child');
+ endpoint.on('error', listener);
+
+ request.returns(Promise.reject(new Error('Oops')));
+
+ childEndpoint.get().then(done.bind(done, ['It should throw an error']), (error) => {
+ expect(listener.getCall(0).args).to.deep.equal([
+ new Error('Oops'),
+ {
+ errorInterceptors: [],
+ headers: {},
+ method: 'get',
+ params: null,
+ requestInterceptors: [],
+ responseInterceptors: [],
+ url: '/child',
+ },
+ ]);
+ expect(error.message).to.equal('Oops');
+ done();
+ }).catch(done);
+ });
+
+ it('should register a default error listener', () => {
+ expect(scope.on.getCall(0).args[0]).to.equal('error');
+ });
+});
diff --git a/test/src/model/entitySpec.js b/test/src/model/entitySpec.js
new file mode 100644
index 0000000..f0e31de
--- /dev/null
+++ b/test/src/model/entitySpec.js
@@ -0,0 +1,81 @@
+import { expect } from 'chai';
+import entityModel from '../../../src/model/entity';
+import sinon from 'sinon';
+
+describe('Entity model', () => {
+ let endpoint;
+ let entity;
+
+ beforeEach(() => {
+ endpoint = {
+ all: sinon.spy(),
+ custom: sinon.spy(),
+ delete: sinon.spy(),
+ identifier: sinon.stub().returns('hello'),
+ one: sinon.spy(),
+ put: sinon.spy(),
+ url: sinon.stub().returns('/url'),
+ };
+
+ entity = entityModel({
+ hello: 'world',
+ here: 'again',
+ }, endpoint);
+ });
+
+ it('should return its id based on the endpoint identifier', () => {
+ expect(entity.id()).to.equal('world');
+ expect(endpoint.identifier.callCount).to.equal(1);
+ });
+
+ it('should return its data when data is called', () => {
+ expect(entity.data()).to.deep.equal({
+ hello: 'world',
+ here: 'again',
+ });
+ });
+
+ it('should call endpoint.all when all is called', () => {
+ entity.all('test', 'me');
+ expect(endpoint.all.getCall(0).args).to.deep.equal([
+ 'test',
+ 'me',
+ ]);
+ });
+
+ it('should call endpoint.one when one is called', () => {
+ entity.one('test', 'me');
+ expect(endpoint.one.getCall(0).args).to.deep.equal([
+ 'test',
+ 'me',
+ ]);
+ });
+
+ it('should call endpoint.delete when delete is called', () => {
+ entity.delete('test', 'me');
+ expect(endpoint.delete.getCall(0).args).to.deep.equal([
+ 'test',
+ 'me',
+ ]);
+ });
+
+ it('should call endpoint.put with entity data when save is called', () => {
+ entity.save('test', 'me');
+ expect(endpoint.put.getCall(0).args).to.deep.equal([
+ {
+ hello: 'world',
+ here: 'again',
+ },
+ 'test',
+ 'me',
+ ]);
+ });
+
+ it('should call endpoint.url when url is called', () => {
+ expect(entity.url('test', 'me')).to.equal('/url');
+ expect(endpoint.url.getCall(0).args).to.deep.equal([
+ 'test',
+ 'me',
+ ]);
+ });
+});
diff --git a/test/src/model/responseSpec.js b/test/src/model/responseSpec.js
new file mode 100644
index 0000000..d7f2e9a
--- /dev/null
+++ b/test/src/model/responseSpec.js
@@ -0,0 +1,93 @@
+import { expect } from 'chai';
+import { fromJS } from 'immutable';
+import responseModel from '../../../src/model/response';
+import sinon from 'sinon';
+
+describe('Response model', () => {
+ let endpoint;
+ let response;
+
+ beforeEach(() => {
+ endpoint = {
+ all: sinon.spy(),
+ custom: sinon.stub().returns({
+ url: sinon.stub().returns('/url/id'),
+ }),
+ identifier: sinon.stub().returns('hello'),
+ url: sinon.stub().returns('/url'),
+ };
+
+ response = responseModel(fromJS({
+ data: {
+ hello: 'world',
+ here: 'again',
+ },
+ headers: {
+ hello: 'world',
+ },
+ statusCode: 200,
+ }), endpoint);
+ });
+
+ it('should return its statusCode when statusCode is called', () => {
+ expect(response.statusCode()).to.equal(200);
+ });
+
+ it('should return its headers when headers is called', () => {
+ expect(response.headers()).to.deep.equal({
+ hello: 'world',
+ });
+ });
+
+ it('should return its raw data when body is called with false as argument', () => {
+ expect(response.body(false)).to.deep.equal({
+ hello: 'world',
+ here: 'again',
+ });
+ });
+
+ it('should return one entity when it raw data is not an array and body is called without argument', () => {
+ const entity = response.body();
+
+ expect(entity.data()).to.deep.equal({
+ hello: 'world',
+ here: 'again',
+ });
+
+ // Ensure the same endpoint is given to our entity
+ expect(entity.url()).to.equal('/url');
+ expect(endpoint.url.getCall(0).args).to.deep.equal([]);
+ });
+
+ it('should return an array of entities when it raw data is an array and body is called without argument', () => {
+ delete endpoint.all; // We simulate a collection endpoint;
+
+ const entities = responseModel(fromJS({
+ data: [
+ {
+ hello: 'world',
+ here: 'again',
+ },
+ {
+ hello: 'world2',
+ here: 'again2',
+ },
+ ],
+ }), endpoint).body();
+
+ expect(entities[0].data()).to.deep.equal({
+ hello: 'world',
+ here: 'again',
+ });
+ expect(entities[1].data()).to.deep.equal({
+ hello: 'world2',
+ here: 'again2',
+ });
+
+ expect(entities[0].url()).to.equal('/url/id');
+ expect(entities[1].url()).to.equal('/url/id');
+
+ expect(endpoint.custom.getCall(0).args).to.deep.equal(['world']);
+ expect(endpoint.custom.getCall(1).args).to.deep.equal(['world2']);
+ });
+});
diff --git a/test/src/model/scopeSpec.js b/test/src/model/scopeSpec.js
new file mode 100644
index 0000000..b951efa
--- /dev/null
+++ b/test/src/model/scopeSpec.js
@@ -0,0 +1,175 @@
+import { expect } from 'chai';
+import scope from '../../../src/model/scope';
+import sinon from 'sinon';
+
+describe('Scope Model', () => {
+ it('should expose methods to deal with simple value', () => {
+ const rootScope = scope();
+
+ rootScope.set('hello', 'world');
+ expect(rootScope.get('hello')).to.equal('world');
+ });
+
+ it('should expose methods to deal with object', () => {
+ const rootScope = scope();
+
+ rootScope.assign('obj', 'hello', 'again');
+ expect(rootScope.get('obj').toJS()).to.deep.equal({
+ hello: 'again',
+ });
+
+ rootScope.assign('obj', 'goodbye', 'again again');
+ expect(rootScope.get('obj').toJS()).to.deep.equal({
+ goodbye: 'again again',
+ hello: 'again',
+ });
+ });
+
+ it('should expose methods to deal with array', () => {
+ const rootScope = scope();
+
+ rootScope.push('arr', 'here');
+ expect(rootScope.get('arr').toJS()).to.deep.equal([
+ 'here',
+ ]);
+
+ rootScope.push('arr', 'there');
+ expect(rootScope.get('arr').toJS()).to.deep.equal([
+ 'here',
+ 'there',
+ ]);
+ });
+
+ it('should return simple value from current scope or parent', () => {
+ const rootScope = scope();
+ rootScope.set('hello', 'world');
+ rootScope.set('hello2', 'world2');
+ rootScope.set('hello3', 'oops');
+
+ const childScope = rootScope.new();
+ childScope.set('hello3', 'world3');
+
+
+ expect(rootScope.get('hello')).to.equal('world');
+ expect(rootScope.get('hello3')).to.equal('oops');
+ expect(childScope.get('hello2')).to.equal('world2');
+ expect(childScope.get('hello3')).to.equal('world3');
+ });
+
+ it('should merge object values with parent scope', () => {
+ const rootScope = scope();
+ rootScope.assign('obj', 'hello', 'again');
+ rootScope.assign('obj', 'goodbye', 'again again');
+
+ const childScope = rootScope.new();
+ childScope.assign('obj', 'good', 'bad');
+ childScope.assign('obj', 'goodbye', 'again again again');
+
+ const grandChildScope = childScope.new();
+ grandChildScope.assign('obj', 'well', 'fine');
+
+ expect(rootScope.get('obj').toJS()).to.deep.equal({
+ goodbye: 'again again',
+ hello: 'again',
+ });
+
+ expect(childScope.get('obj').toJS()).to.deep.equal({
+ good: 'bad',
+ goodbye: 'again again again',
+ hello: 'again',
+ });
+
+ expect(grandChildScope.get('obj').toJS()).to.deep.equal({
+ good: 'bad',
+ goodbye: 'again again again',
+ hello: 'again',
+ well: 'fine',
+ });
+ });
+
+ it('should merge array values with parent scope', () => {
+ const rootScope = scope();
+ rootScope.push('arr', 'here');
+ rootScope.push('arr', 'there');
+
+ const childScope = rootScope.new();
+ childScope.push('arr', 'location');
+
+ const grandChildScope = childScope.new();
+ grandChildScope.push('arr', 'place');
+
+ expect(rootScope.get('arr').toJS()).to.deep.equal([
+ 'here',
+ 'there',
+ ]);
+
+ expect(childScope.get('arr').toJS()).to.deep.equal([
+ 'here',
+ 'there',
+ 'location',
+ ]);
+
+ expect(grandChildScope.get('arr').toJS()).to.deep.equal([
+ 'here',
+ 'there',
+ 'location',
+ 'place',
+ ]);
+ });
+
+ it('should expose a has method to test if a key exists', () => {
+ const rootScope = scope();
+ rootScope.set('hello', 'world');
+
+ expect(rootScope.has('hello')).to.be.true;
+ expect(rootScope.has('hello2')).to.be.false;
+ });
+
+ it('should expose emit/on/once methods to deal with events', () => {
+ const rootScope = scope();
+ const listener1 = sinon.spy();
+ rootScope.on('test', listener1);
+
+ const childScope = rootScope.new();
+ const listener2 = sinon.spy();
+
+ childScope.on('test', listener2);
+
+ const grandChildScope = childScope.new();
+ const listener3 = sinon.spy();
+ grandChildScope.once('test', listener3);
+
+ rootScope.emit('test', 'hello');
+ expect(listener1.getCall(0).args).to.deep.equal([
+ 'hello',
+ ]);
+
+ childScope.emit('test', 'hola');
+ expect(listener1.getCall(1).args).to.deep.equal([
+ 'hola',
+ ]);
+ expect(listener2.getCall(0).args).to.deep.equal([
+ 'hola',
+ ]);
+
+ grandChildScope.emit('test', 'hi');
+ expect(listener1.getCall(2).args).to.deep.equal([
+ 'hi',
+ ]);
+ expect(listener2.getCall(1).args).to.deep.equal([
+ 'hi',
+ ]);
+ expect(listener3.getCall(0).args).to.deep.equal([
+ 'hi',
+ ]);
+
+ grandChildScope.emit('test', 'hi again');
+ expect(listener1.getCall(3).args).to.deep.equal([
+ 'hi again',
+ ]);
+ expect(listener2.getCall(2).args).to.deep.equal([
+ 'hi again',
+ ]);
+ expect(listener3.callCount).to.equal(1);
+ });
+});
diff --git a/test/src/restful-spec.js b/test/src/restful-spec.js
deleted file mode 100644
index d37414e..0000000
--- a/test/src/restful-spec.js
+++ /dev/null
@@ -1,1060 +0,0 @@
-/*global restful,spyOn */
-
-(function() {
- 'use strict';
-
- var httpBackend,
- http,
- resource;
-
- function q(result) {
- return new Promise(function(resolve) {
- resolve(result);
- });
- }
-
- describe('restful', function() {
- beforeEach(function() {
- httpBackend = {
- get: function(config) {
- if (config.url.substr(config.url.length - 1) !== 's') {
- return q({
- // `data` is the response that was provided by the server
- data: config.transformResponse[0]({
- id: 1,
- title: 'test',
- body: 'Hello, I am a test',
- published_at: '2015-01-03'
- }),
-
- // `status` is the HTTP status code from the server response
- status: 200,
-
- // `headers` the headers that the server responded with
- headers: {},
-
- // `config` is the config that was provided to `axios` for the request
- config: {}
- });
- } else {
- return q({
- // `data` is the response that was provided by the server
- data: [
- {
- id: 1,
- title: 'test',
- body: 'Hello, I am a test'
- },
- {
- id: 2,
- title: 'test2',
- body: 'Hello, I am a test2'
- }
- ],
-
- // `status` is the HTTP status code from the server response
- status: 200,
-
- // `headers` the headers that the server responded with
- headers: {},
-
- // `config` is the config that was provided to `axios` for the request
- config: {}
- });
- }
- },
-
- 'custom-get': function(config) {
- return this.get(config);
- },
-
- put: function(config) {
- return q({
- // `data` is the response that was provided by the server
- data: {
- result: 2
- },
-
- // `status` is the HTTP status code from the server response
- status: 200,
-
- // `headers` the headers that the server responded with
- headers: {},
-
- // `config` is the config that was provided to `axios` for the request
- config: {}
- });
- },
-
- delete: function(config) {
- return q({
- // `data` is the response that was provided by the server
- data: {
- result: 1
- },
-
- // `status` is the HTTP status code from the server response
- status: 200,
-
- // `headers` the headers that the server responded with
- headers: {},
-
- // `config` is the config that was provided to `axios` for the request
- config: {}
- });
- },
-
- post: function(config) {
- return q({
- // `data` is the response that was provided by the server
- data: {
- result: 0
- },
-
- // `status` is the HTTP status code from the server response
- status: 200,
-
- // `headers` the headers that the server responded with
- headers: {},
-
- // `config` is the config that was provided to `axios` for the request
- config: {}
- });
- },
-
- patch: function(config) {
- return q({
- // `data` is the response that was provided by the server
- data: {
- result: 4
- },
-
- // `status` is the HTTP status code from the server response
- status: 200,
-
- // `headers` the headers that the server responded with
- headers: {},
-
- // `config` is the config that was provided to `axios` for the request
- config: {}
- });
- },
-
- head: function(config) {
- return q({
- // `data` is the response that was provided by the server
- data: {
- result: 5
- },
-
- // `status` is the HTTP status code from the server response
- status: 200,
-
- // `headers` the headers that the server responded with
- headers: {},
-
- // `config` is the config that was provided to `axios` for the request
- config: {}
- });
- }
- };
-
- resource = restful('localhost')
- .port(3000)
- .prefixUrl('v1')
- .protocol('https');
-
- resource()._http(resource()._http().setBackend(function (config) {
- return httpBackend[config.method](config);
- }));
- });
-
- it('should provide a configured resource', function() {
- expect(resource.baseUrl()).toBe('localhost');
- expect(resource.port()).toBe(3000);
- expect(resource.prefixUrl()).toBe('v1');
- expect(resource.protocol()).toBe('https');
-
- expect(resource.url()).toBe('https://localhost:3000/v1');
- });
-
- it('should provide a configured collection when resource.all is called', function() {
- var articles = resource.all('articles');
-
- expect(articles.url()).toBe('https://localhost:3000/v1/articles');
- expect(articles()._parent()).toBe(resource());
- });
-
- it('should provide a configured collection/member when calls are chained', function() {
- var article = resource.one('articles', 3),
- comment = article.one('comments', 5);
-
- expect(comment.url()).toBe('https://localhost:3000/v1/articles/3/comments/5');
- expect(comment()._parent()).toBe(article());
-
- var comments = article.all('comments');
-
- expect(comments.url()).toBe('https://localhost:3000/v1/articles/3/comments');
- });
-
- it('should call http.get with correct parameters when get is called on a member', function() {
- var article = resource.one('articles', 3),
- comment = article.one('comments', 5);
-
- spyOn(httpBackend, 'get').andCallThrough();
-
- comment.get().then(function(response) {
- var entity = response.body();
-
- // As we use a promesse mock, this is always called synchronously
- expect(entity.data().title).toBe('test');
- expect(entity.data().body).toBe('Hello, I am a test');
- });
-
- expect(httpBackend.get).toHaveBeenCalledWith({
- method: 'get',
- url: 'https://localhost:3000/v1/articles/3/comments/5',
- params: {},
- headers: {},
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
-
- httpBackend.get.reset();
-
- comment.get({ test: 'test3' }, { bar: 'foo' }).then(function(response) {
- var entity = response.body();
-
- // As we use a promesse mock, this is always called synchronously
- expect(entity.data().title).toBe('test');
- expect(entity.data().body).toBe('Hello, I am a test');
- });
-
- expect(httpBackend.get).toHaveBeenCalledWith({
- method: 'get',
- url: 'https://localhost:3000/v1/articles/3/comments/5',
- params: { test: 'test3' },
- headers: { bar: 'foo' },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
- });
-
- it('should call http.put with correct parameters when put is called on member', function() {
- var article = resource.one('articles', 3),
- comment = article.one('comments', 2);
-
- spyOn(httpBackend, 'put').andCallThrough();
-
- comment.put({ body: 'I am a new comment' }).then(function(response) {
- // As we use a promesse mock, this is always called synchronously
- expect(response()).toEqual({
- data: {
- result: 2
- },
- status: 200,
- headers: {},
- config: {}
- });
- });
-
- expect(httpBackend.put).toHaveBeenCalledWith({
- method: 'put',
- url: 'https://localhost:3000/v1/articles/3/comments/2',
- params: {},
- data: {
- body: 'I am a new comment'
- },
- headers: {
- 'Content-Type': 'application/json;charset=UTF-8'
- },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)],
- transformRequest: [jasmine.any(Function)]
- });
- });
-
- it('should call http.delete with correct parameters when delete is called on member', function() {
- var article = resource.one('articles', 3),
- comment = article.one('comments', 2);
-
- spyOn(httpBackend, 'delete').andCallThrough();
-
- comment.delete({}, { foo: 'bar' }).then(function(response) {
- // As we use a promesse mock, this is always called synchronously
- expect(response()).toEqual({
- data: {
- result: 1
- },
- status: 200,
- headers: {},
- config: {}
- });
- });
-
- expect(httpBackend.delete).toHaveBeenCalledWith({
- method: 'delete',
- url: 'https://localhost:3000/v1/articles/3/comments/2',
- params: {},
- headers: { foo: 'bar' },
- fullResponseInterceptors: [],
- data: {},
- requestInterceptors : [ ],
- transformResponse: [jasmine.any(Function)]
- });
- });
-
- it('should call http.head with correct parameters when head is called on a member', function() {
- var article = resource.one('articles', 3),
- comment = article.one('comments', 5);
-
- spyOn(httpBackend, 'head').andCallThrough();
-
- comment.head({ bar: 'foo' }).then(function(response) {
- // As we use a promesse mock, this is always called synchronously
- expect(response()).toEqual({
- // `data` is the response that was provided by the server
- data: {
- result: 5
- },
-
- // `status` is the HTTP status code from the server response
- status: 200,
-
- // `headers` the headers that the server responded with
- headers: {},
-
- // `config` is the config that was provided to `axios` for the request
- config: {}
- });
- });
-
- expect(httpBackend.head).toHaveBeenCalledWith({
- method: 'head',
- url: 'https://localhost:3000/v1/articles/3/comments/5',
- params: {},
- headers: { bar: 'foo' },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
- });
-
- it('should call http.patch with correct parameters when patch is called on member', function() {
- var article = resource.one('articles', 3),
- comment = article.one('comments', 2);
-
- spyOn(httpBackend, 'patch').andCallThrough();
-
- comment.patch({ body: 'I am a new comment' }, { foo: 'bar' }).then(function(response) {
- // As we use a promesse mock, this is always called synchronously
- expect(response()).toEqual({
- // `data` is the response that was provided by the server
- data: {
- result: 4
- },
-
- // `status` is the HTTP status code from the server response
- status: 200,
-
- // `headers` the headers that the server responded with
- headers: {},
-
- // `config` is the config that was provided to `axios` for the request
- config: {}
- });
- });
-
- expect(httpBackend.patch).toHaveBeenCalledWith({
- method: 'patch',
- url: 'https://localhost:3000/v1/articles/3/comments/2',
- params: {},
- data: {
- body: 'I am a new comment'
- },
- headers: {
- 'Content-Type': 'application/json;charset=UTF-8',
- foo: 'bar'
- },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)],
- transformRequest: [jasmine.any(Function)]
- });
- });
-
- it('should call http.put with correct parameters when save is called on an entity', function() {
- var articles = resource.one('articles', 3),
- comment = articles.one('comments', 5);
-
- spyOn(httpBackend, 'get').andCallThrough();
- spyOn(httpBackend, 'put').andCallThrough();
-
- comment.get().then(function(response) {
- var entity = response.body();
-
- // As we use a promesse mock, this is always called synchronously
- expect(entity.data().title).toBe('test');
- expect(entity.data().body).toBe('Hello, I am a test');
- expect(entity.data().published_at).toBe('2015-01-03');
-
- entity.data().body = 'Overriden';
- entity.data().published_at = '2015-01-06';
-
- entity.save();
- });
-
- expect(httpBackend.get).toHaveBeenCalledWith({
- method: 'get',
- url: 'https://localhost:3000/v1/articles/3/comments/5',
- params: {},
- headers: {},
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
-
- expect(httpBackend.put).toHaveBeenCalledWith({
- method: 'put',
- url: 'https://localhost:3000/v1/articles/3/comments/5',
- params: {},
- data: {
- id: 1,
- title: 'test',
- body: 'Overriden',
- published_at: '2015-01-06'
- },
- headers: {
- 'Content-Type': 'application/json;charset=UTF-8'
- },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)],
- transformRequest: [jasmine.any(Function)]
- });
-
- httpBackend.put.reset();
-
- comment.get().then(function(response) {
- var entity = response.body();
-
- // As we use a promesse mock, this is always called synchronously
- entity.save({ foo: 'bar' });
- });
-
- expect(httpBackend.put).toHaveBeenCalledWith({
- method: 'put',
- url: 'https://localhost:3000/v1/articles/3/comments/5',
- params: {},
- data: {
- id: 1,
- title: 'test',
- body: 'Hello, I am a test',
- published_at: '2015-01-03'
- },
- headers: {
- 'Content-Type': 'application/json;charset=UTF-8',
- foo: 'bar'
- },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)],
- transformRequest: [jasmine.any(Function)]
- });
- });
-
- it('should call http.delete with correct parameters when remove is called on an entity', function() {
- var article = resource.one('articles', 3),
- comment = article.one('comments', 5);
-
- spyOn(httpBackend, 'get').andCallThrough();
- spyOn(httpBackend, 'delete').andCallThrough();
-
- comment.get().then(function(response) {
- var entity = response.body();
-
- // As we use a promesse mock, this is always called synchronously
- expect(entity.data().title).toBe('test');
- expect(entity.data().body).toBe('Hello, I am a test');
-
- entity.remove();
- });
-
- expect(httpBackend.get).toHaveBeenCalledWith({
- method: 'get',
- url: 'https://localhost:3000/v1/articles/3/comments/5',
- params: {},
- headers: {},
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
-
- expect(httpBackend.delete).toHaveBeenCalledWith({
- method: 'delete',
- url: 'https://localhost:3000/v1/articles/3/comments/5',
- params: {},
- data: {},
- headers: {},
- fullResponseInterceptors: [],
- requestInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
-
- httpBackend.delete.reset();
-
- comment.get().then(function(response) {
- var entity = response.body();
-
- // As we use a promesse mock, this is always called synchronously
- entity.remove({ foo: 'bar' });
- });
-
- expect(httpBackend.delete).toHaveBeenCalledWith({
- method: 'delete',
- url: 'https://localhost:3000/v1/articles/3/comments/5',
- params: {},
- headers: { foo: 'bar' },
- data: {},
- fullResponseInterceptors: [],
- requestInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
- });
-
- it('should call http.get with correct parameters when get is called on collection', function() {
- var article = resource.one('articles', 3),
- comments = article.all('comments');
-
- spyOn(httpBackend, 'get').andCallThrough();
-
- comments.get(1, { page: 1 }, { foo: 'bar' }).then(function(response) {
- var entity = response.body();
-
- // As we use a promesse mock, this is always called synchronously
- expect(entity.data().id).toBe(1);
- expect(entity.data().title).toBe('test');
- expect(entity.data().body).toBe('Hello, I am a test');
- });
-
- expect(httpBackend.get).toHaveBeenCalledWith({
- method: 'get',
- url: 'https://localhost:3000/v1/articles/3/comments/1',
- params: { page: 1 },
- headers: { foo: 'bar' },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
- });
-
- it('should call http.get with correct parameters when getAll is called on collection', function() {
- var article = resource.one('articles', 3),
- comments = article.all('comments');
-
- spyOn(httpBackend, 'get').andCallThrough();
-
- comments.getAll({ page: 1 }, { foo: 'bar' }).then(function(response) {
- var entities = response.body();
-
- // As we use a promesse mock, this is always called synchronously
- expect(entities[0].data().id).toBe(1);
- expect(entities[0].data().title).toBe('test');
- expect(entities[0].data().body).toBe('Hello, I am a test');
-
- expect(entities[1].data().id).toBe(2);
- expect(entities[1].data().title).toBe('test2');
- expect(entities[1].data().body).toBe('Hello, I am a test2');
- });
-
- expect(httpBackend.get).toHaveBeenCalledWith({
- method: 'get',
- url: 'https://localhost:3000/v1/articles/3/comments',
- params: { page: 1 },
- headers: { foo: 'bar' },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
- });
-
- it('should call http.post with correct parameters when post is called on collection', function() {
- var article = resource.one('articles', 3),
- comments = article.all('comments');
-
- spyOn(httpBackend, 'post').andCallThrough();
-
- comments.post({ body: 'I am a new comment' }, { foo: 'bar' }).then(function(response) {
- // As we use a promesse mock, this is always called synchronously
- expect(response()).toEqual({
- // `data` is the response that was provided by the server
- data: {
- result: 0
- },
-
- // `status` is the HTTP status code from the server response
- status: 200,
-
- // `headers` the headers that the server responded with
- headers: {},
-
- // `config` is the config that was provided to `axios` for the request
- config: {}
- });
- });
-
- expect(httpBackend.post).toHaveBeenCalledWith({
- method: 'post',
- url: 'https://localhost:3000/v1/articles/3/comments',
- params: {},
- data: {
- body: 'I am a new comment'
- },
- headers: {
- 'Content-Type': 'application/json;charset=UTF-8',
- foo: 'bar'
- },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)],
- transformRequest: [jasmine.any(Function)]
- });
- });
-
- it('should call http.put with correct parameters when put is called on collection', function() {
- var article = resource.one('articles', 3),
- comments = article.all('comments');
-
- spyOn(httpBackend, 'put').andCallThrough();
-
- comments.put(2, { body: 'I am a new comment' }, { foo: 'bar' }).then(function(response) {
- // As we use a promesse mock, this is always called synchronously
- expect(response()).toEqual({
- // `data` is the response that was provided by the server
- data: {
- result: 2
- },
-
- // `status` is the HTTP status code from the server response
- status: 200,
-
- // `headers` the headers that the server responded with
- headers: {},
-
- // `config` is the config that was provided to `axios` for the request
- config: {}
- });
- });
-
- expect(httpBackend.put).toHaveBeenCalledWith({
- method: 'put',
- url: 'https://localhost:3000/v1/articles/3/comments/2',
- params: {},
- data: {
- body: 'I am a new comment'
- },
- headers: {
- 'Content-Type': 'application/json;charset=UTF-8',
- foo: 'bar'
- },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)],
- transformRequest: [jasmine.any(Function)]
- });
- });
-
- it('should call http.delete with correct parameters when delete is called on collection', function() {
- var article = resource.one('articles', 3),
- comments = article.all('comments');
-
- spyOn(httpBackend, 'delete').andCallThrough();
-
- comments.delete(2, {}, { foo: 'bar' }).then(function(response) {
- // As we use a promesse mock, this is always called synchronously
- expect(response()).toEqual({
- // `data` is the response that was provided by the server
- data: {
- result: 1
- },
-
- // `status` is the HTTP status code from the server response
- status: 200,
-
- // `headers` the headers that the server responded with
- headers: {},
-
- // `config` is the config that was provided to `axios` for the request
- config: {}
- });
- });
-
- expect(httpBackend.delete).toHaveBeenCalledWith({
- method: 'delete',
- url: 'https://localhost:3000/v1/articles/3/comments/2',
- params: {},
- data: {},
- headers: { foo: 'bar' },
- fullResponseInterceptors: [],
- requestInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
- });
-
- it('should call http.head with correct parameters when head is called on a collection', function() {
- var article = resource.one('articles', 3),
- comments = article.all('comments');
-
- spyOn(httpBackend, 'head').andCallThrough();
-
- comments.head(5, { bar: 'foo' }).then(function(response) {
- // As we use a promesse mock, this is always called synchronously
- expect(response()).toEqual({
- // `data` is the response that was provided by the server
- data: {
- result: 5
- },
-
- // `status` is the HTTP status code from the server response
- status: 200,
-
- // `headers` the headers that the server responded with
- headers: {},
-
- // `config` is the config that was provided to `axios` for the request
- config: {}
- });
- });
-
- expect(httpBackend.head).toHaveBeenCalledWith({
- method: 'head',
- url: 'https://localhost:3000/v1/articles/3/comments/5',
- params: {},
- headers: { bar: 'foo' },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
- });
-
- it('should call http.patch with correct parameters when patch is called on collection', function() {
- var article = resource.one('articles', 3),
- comments = article.all('comments');
-
- spyOn(httpBackend, 'patch').andCallThrough();
-
- comments.patch(2, { body: 'I am a new comment' }, { foo: 'bar' }).then(function(response) {
- // As we use a promesse mock, this is always called synchronously
- expect(response()).toEqual({
- // `data` is the response that was provided by the server
- data: {
- result: 4
- },
-
- // `status` is the HTTP status code from the server response
- status: 200,
-
- // `headers` the headers that the server responded with
- headers: {},
-
- // `config` is the config that was provided to `axios` for the request
- config: {}
- });
- });
-
- expect(httpBackend.patch).toHaveBeenCalledWith({
- method: 'patch',
- url: 'https://localhost:3000/v1/articles/3/comments/2',
- params: {},
- data: {
- body: 'I am a new comment'
- },
- headers: {
- 'Content-Type': 'application/json;charset=UTF-8',
- foo: 'bar'
- },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)],
- transformRequest: [jasmine.any(Function)]
- });
- });
-
- it('should merge global headers with headers argument', function() {
- var article = resource.one('articles', 3),
- comments = article.all('comments');
-
- // we define headers after calling one and all to ensure the propagation of configuration
- resource.header('foo2', 'bar2');
-
- spyOn(httpBackend, 'get').andCallThrough();
-
- comments.get(2, null, { foo: 'bar' });
-
- expect(httpBackend.get).toHaveBeenCalledWith({
- method: 'get',
- url: 'https://localhost:3000/v1/articles/3/comments/2',
- params: {},
- headers: { foo2: 'bar2', foo: 'bar' },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
-
- httpBackend.get.reset();
-
- comments.get(2, null, { foo2: 'bar' });
-
- expect(httpBackend.get).toHaveBeenCalledWith({
- method: 'get',
- url: 'https://localhost:3000/v1/articles/3/comments/2',
- params: {},
- headers: { foo2: 'bar' },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
-
- comments
- .header('foo3', 'bar3')
- .addResponseInterceptor(function(res) {
- res.title = 'Intercepted :)';
-
- return res;
- });
-
- comments.get(1).then(function(response) {
- var comment = response.body();
-
- expect(comment.data().title).toBe('Intercepted :)');
-
- // test inheritance pattern
- resource.one('articles', 1).get().then(function(response) {
- var article = response.body();
-
- expect(article.data().title).toBe('test');
- });
-
- httpBackend.get.reset();
-
- comment.one('authors', 1).get();
-
- expect(httpBackend.get).toHaveBeenCalledWith({
- method: 'get',
- url: 'https://localhost:3000/v1/articles/3/comments/1/authors/1',
- params: {},
- headers: { foo3: 'bar3', foo2: 'bar2' },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
- });
- });
-
- it('should call response interceptors on get response', function() {
- var interceptor1 = jasmine.createSpy('interceptor1').andReturn({
- id: 1,
- title: 'Intercepted',
- body: 'Hello, I am a test',
- published_at: '2015-01-03'
- });
-
- resource.addResponseInterceptor(interceptor1);
-
- var getArgs;
-
- spyOn(httpBackend, 'get').andCallFake(function(config) {
- getArgs = config;
-
- return q({
- data: {
- id: 1,
- title: 'test',
- body: 'Hello, I am a test',
- published_at: '2015-01-03'
- }
- });
- });
-
- resource.one('articles', 1).get().then(function(response) {
- var article = response.body();
-
- expect(getArgs).toEqual({
- url: 'https://localhost:3000/v1/articles/1',
- params : {},
- headers: {},
- //responseInterceptors: [interceptor1],
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)],
- method: 'patch'
- });
-
- var transformedData = getArgs.transformResponse[0](JSON.stringify({
- id: 1,
- title: 'test',
- body: 'Hello, I am a test',
- published_at: '2015-01-03'
- }));
-
- expect(interceptor1).toHaveBeenCalledWith(JSON.stringify({
- id: 1,
- title: 'test',
- body: 'Hello, I am a test',
- published_at: '2015-01-03'
- }));
-
- expect(transformedData.title).toBe('Intercepted');
- });
- });
-
- it('should correctly chain callback in restful object', function() {
- expect(resource.header('foo', 'bar')).toBe(resource);
- expect(resource.header('foo', 'bar').prefixUrl('v1')).toBe(resource);
-
- var articlesCollection = resource.header('foo', 'bar').prefixUrl('v1').all('articles');
-
- expect(articlesCollection.header('foo1', 'bar1')).toBe(articlesCollection);
-
- expect(articlesCollection.header('foo2', 'bar2')
- .addResponseInterceptor(jasmine.createSpy('interceptor'))).toBe(articlesCollection);
-
- var commentsMember = resource.one('articles', 2).one('comments', 1);
-
- expect(commentsMember.header('foo3', 'bar3')).toBe(commentsMember);
-
- expect(commentsMember.header('foo4', 'bar4')
- .addResponseInterceptor(jasmine.createSpy('interceptor'))).toBe(commentsMember);
- });
-
- it('should allow custom url in restful object', function() {
- var article = resource.oneUrl('articles', 'http://custom.url/article/1');
-
- expect(article.url()).toBe('http://custom.url/article/1');
-
- var articles = resource.allUrl('articles', 'http://custom.url/articles');
-
- expect(articles.url()).toBe('http://custom.url/articles');
-
- var comment = article.one('comment', 1);
-
- expect(comment.url()).toBe('http://custom.url/article/1/comment/1');
-
- var post = comment.oneUrl('comment', 'http://custom.url/post?article=1&comment=1');
-
- expect(post.url()).toBe('http://custom.url/post?article=1&comment=1');
- });
-
- it('should call http.get with correct url when get is called on collection', function() {
- var article = resource.one('articles', 3),
- comments = article.allUrl('comments', 'http://custom.url/comment');
-
- spyOn(httpBackend, 'get').andCallThrough();
-
- comments.get(1, { page: 1 }, { foo: 'bar' }).then(function(response) {
- var entity = response.body();
-
- // As we use a promesse mock, this is always called synchronously
- expect(entity.data().id).toBe(1);
- expect(entity.data().title).toBe('test');
- expect(entity.data().body).toBe('Hello, I am a test');
- });
-
- expect(httpBackend.get).toHaveBeenCalledWith({
- method: 'get',
- url: 'http://custom.url/comment/1',
- params: { page: 1 },
- headers: { foo: 'bar' },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
- });
-
- it('should call full request interceptors on collection getAll request', function() {
- resource.addFullRequestInterceptor(function (params, headers, data, method) {
- params._end = params.page * 20;
- params._start = params._end - 20;
- delete params.page;
-
- headers.custom = 'mine';
-
- return {
- method: 'custom-get',
- params: params,
- headers: headers
- };
- });
- var articles = resource.all('articles');
-
- spyOn(httpBackend, 'get').andCallThrough();
-
- articles.getAll({ page: 1 }, { foo: 'bar' });
-
- expect(httpBackend.get).toHaveBeenCalledWith({
- method: 'custom-get',
- url: 'https://localhost:3000/v1/articles',
- params: { _start: 0, _end: 20 },
- headers: { foo: 'bar', custom: 'mine' },
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
- });
-
- it('should call full reponse interceptors on collection getAll response', function() {
- var interceptor = function interceptor(data, headers) {
- headers['X-FROM'] = 'my_interceptor';
-
- data[0].title = 'Intercepted :)';
-
- return {
- data: data,
- headers: headers
- };
- };
- resource.addFullResponseInterceptor(interceptor);
- var articles = resource.all('articles');
-
- spyOn(httpBackend, 'get').andCallThrough();
-
- articles.getAll({ page: 1 }, { foo: 'bar' }).then(function(response) {
- var entities = response.body();
-
- // As we use a promesse mock, this is always called synchronously
- expect(entities[0].data().body).toBe('Hello, I am a test');
- expect(entities[0].data().title).toBe('Intercepted :)');
- expect(entities[1].data().body).toBe('Hello, I am a test2');
-
- expect(response().headers['X-FROM']).toBe('my_interceptor');
- });
-
- expect(httpBackend.get).toHaveBeenCalledWith({
- method: 'get',
- url: 'https://localhost:3000/v1/articles',
- params: { page: 1 },
- headers: { foo: 'bar' },
- fullResponseInterceptors: [interceptor],
- transformResponse: [jasmine.any(Function)]
- });
- });
-
- it('should call delete not only with header but even with data when deleting one entity', function() {
- var articles = resource.oneUrl('articles', 'https://localhost:3000/v1/articles');
- spyOn(httpBackend, 'delete').andCallThrough();
-
- articles.delete([1,2], {});
-
- expect(httpBackend.delete).toHaveBeenCalledWith({
- method: 'delete',
- url: 'https://localhost:3000/v1/articles',
- params: {},
- headers: { },
- data: [1,2],
- requestInterceptors: [],
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
- });
-
- it('should call delete not only with header but even with data when deleting a collection entity', function() {
- var articles = resource.allUrl('articles', 'https://localhost:3000/v1/articles');
- spyOn(httpBackend, 'delete').andCallThrough();
-
- articles.delete(1,{ key: 'value' }, {});
-
- expect(httpBackend.delete).toHaveBeenCalledWith({
- method: 'delete',
- url: 'https://localhost:3000/v1/articles/1',
- params: {},
- headers: { },
- data: { key: 'value' },
- requestInterceptors: [],
- fullResponseInterceptors: [],
- transformResponse: [jasmine.any(Function)]
- });
- });
- });
-})();
diff --git a/test/src/restfulSpec.js b/test/src/restfulSpec.js
new file mode 100644
index 0000000..ac2dc8f
--- /dev/null
+++ b/test/src/restfulSpec.js
@@ -0,0 +1,106 @@
+import api from '../mock/api';
+import { expect } from 'chai';
+import nock from 'nock';
+import request from 'request';
+import restful, { requestBackend } from '../../src';
+import sinon from 'sinon';
+
+describe('Restful', () => {
+ it('should create an endpoint with provided url when it is called', (done) => {
+ const client = restful('http://url', sinon.stub().returns(Promise.resolve({
+ data: { output: 1 },
+ })));
+
+ expect(client.url()).to.equal('http://url');
+
+ client.get().then((response) => {
+ expect(response.body(false)).to.deep.equal({
+ output: 1,
+ });
+
+ done();
+ }).catch(done);
+ });
+
+ it('should create an endpoint with current url if none provided and window.location exists', () => {
+ global.window = {
+ location: {
+ host: 'test.com',
+ protocol: 'https:',
+ },
+ };
+
+ const client = restful();
+
+ expect(client.url()).to.equal('https://test.com');
+
+ delete global.window;
+ });
+
+ it('should work with a real API', (done) => {
+ api(nock);
+ const client = restful('http://localhost', requestBackend(request));
+
+ client
+ .all('articles')
+ .identifier('_id')
+ .getAll()
+ .then((response) => {
+ const articles = response.body();
+
+ expect(articles[0].data()).to.deep.equal({
+ author: 'Pedro',
+ title: 'Beauty Is A Kaleidoscope of Colour',
+ _id: '1',
+ });
+ expect(articles[0].id()).to.equal('1');
+
+ expect(articles[3].data()).to.deep.equal({
+ author: 'Tuco',
+ title: 'The Admirable Reason Why He Took The Right Path',
+ _id: '4',
+ });
+ expect(articles[3].id()).to.equal('4');
+
+ return articles;
+ })
+ .then((articles) => {
+ return articles[4]
+ .one('comments', 2)
+ .identifier('id')
+ .get()
+ .then((response) => {
+ const comment = response.body();
+
+ expect(comment.id()).to.equal('2');
+ comment.data().content = 'Updated';
+ return comment.save();
+ })
+ .then((response) => {
+ expect(response.body(false)).to.deep.equal({
+ content: 'Updated',
+ id: '2',
+ });
+ return client.all('articles').post({
+ title: 'Hello',
+ });
+ })
+ .then((response) => {
+ expect(response.body(false)).to.deep.equal({
+ title: 'Hello',
+ });
+ expect(response.statusCode()).to.equal(201);
+
+ return client.one('articles', 1).delete({ cascade: '1' });
+ })
+ .then((response) => {
+ expect(response.body(false)).to.deep.equal({
+ cascade: '1',
+ });
+ expect(response.statusCode()).to.equal(200);
+ });
+ })
+ .then(() => done())
+ .catch(done);
+ });
+});
diff --git a/test/src/service/httpSpec.js b/test/src/service/httpSpec.js
new file mode 100644
index 0000000..c57289d
--- /dev/null
+++ b/test/src/service/httpSpec.js
@@ -0,0 +1,251 @@
+import { expect } from 'chai';
+import httpService from '../../../src/service/http';
+import { Map } from 'immutable';
+import sinon from 'sinon';
+
+/* eslint-disable new-cap */
+describe('HTTP Service', () => {
+ let http;
+ let httpBackend;
+
+ beforeEach(() => {
+ httpBackend = sinon.stub().returns(Promise.resolve({ output: 1 }));
+ http = httpService(httpBackend);
+ });
+
+ it('should execute request interceptors, then call the http backend and execute response interceptors before returning result', (done) => {
+ const requestInterceptor1 = sinon.stub().returns({ method: 'put' });
+ const requestInterceptor2 = sinon.stub().returns({ params: { asc: 1 } });
+ const requestInterceptor3 = sinon.stub().returns(new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({ url: '/updated' });
+ }, 100);
+ }));
+ const responseInterceptor1 = sinon.stub().returns({ status: 'yes' });
+
+ http(Map({
+ method: 'get',
+ form: {
+ test: 'test',
+ },
+ requestInterceptors: [requestInterceptor1, requestInterceptor2, requestInterceptor3],
+ responseInterceptors: [responseInterceptor1],
+ url: '/url',
+ })).then((response) => {
+ expect(requestInterceptor1.getCall(0).args).to.deep.equal([
+ {
+ method: 'get',
+ form: {
+ test: 'test',
+ },
+ url: '/url',
+ },
+ ]);
+ expect(requestInterceptor2.getCall(0).args).to.deep.equal([
+ {
+ method: 'put',
+ form: {
+ test: 'test',
+ },
+ url: '/url',
+ },
+ ]);
+ expect(requestInterceptor3.getCall(0).args).to.deep.equal([
+ {
+ method: 'put',
+ form: {
+ test: 'test',
+ },
+ params: {
+ asc: 1,
+ },
+ url: '/url',
+ },
+ ]);
+ expect(httpBackend.getCall(0).args).to.deep.equal([
+ {
+ method: 'put',
+ form: {
+ test: 'test',
+ },
+ params: {
+ asc: 1,
+ },
+ url: '/updated',
+ },
+ ]);
+ expect(responseInterceptor1.getCall(0).args).to.deep.equal([
+ {
+ output: 1,
+ },
+ {
+ method: 'put',
+ form: {
+ test: 'test',
+ },
+ params: {
+ asc: 1,
+ },
+ url: '/updated',
+ },
+ ]);
+ expect(response.toJS()).to.deep.equal({
+ output: 1,
+ status: 'yes',
+ });
+ done();
+ }).catch(done);
+ });
+
+ it('should execute error interceptors when an error occured in a request interceptor', (done) => {
+ const requestInterceptor1 = sinon.stub().returns({ method: 'put' });
+ const requestInterceptor2 = sinon.stub().returns(new Promise(() => {
+ throw new Error('Oops');
+ }));
+ const errorInterceptor1 = sinon.stub().returns({ status: 'yes' });
+
+ http(Map({
+ errorInterceptors: [errorInterceptor1],
+ method: 'get',
+ form: {
+ test: 'test',
+ },
+ requestInterceptors: [requestInterceptor1, requestInterceptor2],
+ responseInterceptors: [],
+ url: '/url',
+ })).then(done.bind(done, 'It should throw an error'), (error) => {
+ expect(requestInterceptor1.getCall(0).args).to.deep.equal([
+ {
+ method: 'get',
+ form: {
+ test: 'test',
+ },
+ url: '/url',
+ },
+ ]);
+ expect(requestInterceptor2.getCall(0).args).to.deep.equal([
+ {
+ method: 'put',
+ form: {
+ test: 'test',
+ },
+ url: '/url',
+ },
+ ]);
+ expect(httpBackend.callCount).to.equal(0);
+ expect(errorInterceptor1.getCall(0).args).to.deep.equal([
+ new Error('Oops'),
+ {
+ method: 'get',
+ form: {
+ test: 'test',
+ },
+ url: '/url',
+ },
+ ]);
+ expect(error).to.deep.equal({ status: 'yes' });
+ done();
+ }).catch(done);
+ });
+
+ it('should execute error interceptors when an error occured in a response interceptor', (done) => {
+ const responseInterceptor1 = sinon.stub().returns(new Promise(() => {
+ throw new Error('Oops');
+ }));
+ const errorInterceptor1 = sinon.stub().returns({ status: 'yes' });
+
+ http(Map({
+ errorInterceptors: [errorInterceptor1],
+ method: 'get',
+ form: {
+ test: 'test',
+ },
+ requestInterceptors: [],
+ responseInterceptors: [responseInterceptor1],
+ url: '/url',
+ })).then(done.bind(done, 'It should throw an error'), (error) => {
+ expect(responseInterceptor1.getCall(0).args).to.deep.equal([
+ {
+ output: 1,
+ },
+ {
+ method: 'get',
+ form: {
+ test: 'test',
+ },
+ url: '/url',
+ },
+ ]);
+ expect(httpBackend.getCall(0).args).to.deep.equal([
+ {
+ method: 'get',
+ form: {
+ test: 'test',
+ },
+ url: '/url',
+ },
+ ]);
+ expect(errorInterceptor1.getCall(0).args).to.deep.equal([
+ new Error('Oops'),
+ {
+ method: 'get',
+ form: {
+ test: 'test',
+ },
+ url: '/url',
+ },
+ ]);
+ expect(error).to.deep.equal({ status: 'yes' });
+ done();
+ }).catch(done);
+ });
+
+
+ it('should execute error interceptors when an error occured in httpBackend', (done) => {
+ const requestInterceptor1 = sinon.stub().returns({ method: 'put' });
+ const errorInterceptor1 = sinon.stub().returns({ status: 'yes' });
+ httpBackend.returns(Promise.reject(new Error('Oops')));
+
+ http(Map({
+ errorInterceptors: [errorInterceptor1],
+ method: 'get',
+ form: {
+ test: 'test',
+ },
+ requestInterceptors: [requestInterceptor1],
+ responseInterceptors: [],
+ url: '/url',
+ })).then(done.bind(done, 'It should throw an error'), (error) => {
+ expect(requestInterceptor1.getCall(0).args).to.deep.equal([
+ {
+ method: 'get',
+ form: {
+ test: 'test',
+ },
+ url: '/url',
+ },
+ ]);
+ expect(httpBackend.getCall(0).args).to.deep.equal([
+ {
+ method: 'put',
+ form: {
+ test: 'test',
+ },
+ url: '/url',
+ },
+ ]);
+ expect(errorInterceptor1.getCall(0).args).to.deep.equal([
+ new Error('Oops'),
+ {
+ method: 'get',
+ form: {
+ test: 'test',
+ },
+ url: '/url',
+ },
+ ]);
+ expect(error).to.deep.equal({ status: 'yes' });
+ done();
+ }).catch(done);
+ });
+});
diff --git a/test/src/util/serializeSpec.js b/test/src/util/serializeSpec.js
new file mode 100644
index 0000000..ebbe427
--- /dev/null
+++ b/test/src/util/serializeSpec.js
@@ -0,0 +1,20 @@
+import { expect } from 'chai';
+import serialize from '../../../src/util/serialize';
+import { Map } from 'immutable';
+
+/* eslint-disable new-cap */
+describe('Serialize util', () => {
+ it('should call to JS on an Iterable before returning it', () => {
+ expect(serialize(Map({ test: 'test' }))).to.deep.equal({
+ test: 'test',
+ });
+ });
+
+ it('should return a non Iterable value', () => {
+ expect(serialize('test')).to.equal('test');
+
+ expect(serialize({ test: 'test' })).to.deep.equal({
+ test: 'test',
+ });
+ });
+});
diff --git a/webpack.config.js b/webpack.config.js
index 371ebf5..cdd92ca 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,12 +1,11 @@
+/* eslint-disable no-var */
+var webpack = require('webpack');
+var production = process.env.NODE_ENV === 'production';
+
module.exports = {
entry: {
- restful: './src/restful.js',
- },
- resolve:{
- modulesDirectories: [
- 'node_modules',
- 'src'
- ]
+ restful: './build/restful.standalone.js',
+ 'restful.standalone': './build/restful.fetch.js',
},
module: {
loaders: [{
@@ -15,10 +14,18 @@ module.exports = {
loader: 'babel-loader',
}],
},
+ plugins: [
+ new webpack.DefinePlugin({
+ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
+ }),
+ ].concat(production ? [
+ new webpack.optimize.UglifyJsPlugin(),
+ new webpack.optimize.DedupePlugin(),
+ ] : []),
output: {
path: './dist',
- filename: '[name].js',
+ filename: production ? '[name].min.js' : '[name].js',
library: 'restful',
- libraryTarget: 'umd'
- }
+ libraryTarget: 'umd',
+ },
};