├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bower.json
├── example
├── api.json
├── api
│ ├── coffee.json
│ ├── orders.php
│ └── orders
│ │ └── 1.json
├── atom
│ ├── archive-1.xml
│ ├── entry-1.xml
│ └── recent.xml
├── buy_coffee.html
├── hal
│ ├── api.json
│ ├── coffee.json
│ ├── order1.json
│ └── orders.php
├── hal_coffee.html
├── hal_local.php
├── leave_the_maze.html
├── local.php
├── maze
│ ├── 1x1.json
│ ├── 1x2.json
│ ├── 1x3.json
│ ├── 2x1.json
│ ├── 2x2.json
│ ├── 2x3.json
│ └── finish.json
├── proxy.php
└── show_atom_archive.html
├── hateoas-client.js
├── package.json
└── test
├── hal-test.js
├── init-test.js
├── invalid-media-type-test.js
├── json-hc-test.js
└── server-test.js
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '0.10'
4 | - '0.11'
5 | - '0.12'
6 | before_deploy:
7 | - npm install json
8 | - node_modules/.bin/json -E "this.version='$TRAVIS_TAG'" -f package.json -I
9 | deploy:
10 | provider: npm
11 | email: JanS@DracoBlue.de
12 | api_key:
13 | secure: L5zAYgQJJjVwBsPMXIkgStaXHc2zeK/g1czu7cuj4uE/n7p1B7U2RI28kGeTeoyIKRS2haKEBkJubhEEbjPVl1/FGhJ9Pc6RskmJTuSJpSpsbFqJzDDtI0aewFdZ7rFRmMI6YjxCS5Dvml9ZXRTmrP64e6nwYNLaQlOPs0aw12k=
14 | on:
15 | tags: true
16 | repo: DracoBlue/hateoas-client-js
17 | branch: master
18 | node: 0.12
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | hateoas-client.js Changelog
2 | =======================
3 |
4 | ## dev
5 |
6 | - added experimental support for JSON HC
7 | - added tests for hal in `test/hal-test.js`
8 |
9 | ## 0.4.0 (2015/04/21)
10 |
11 | - added `BaseHttpResponse#getAllHeaders`
12 | - added `BaseHttpResponse#getStatusCode`
13 | - added `HttpAgent#getUrl`
14 | - added `HttpLink` as subclass of HttpAgent with `getRel`, `getType` and `getTitle`
15 | - added `head` request
16 | - added `NoContentResponse`
17 | - added `options.ajaxOptions` to override `jQuery.ajax` options
18 |
19 | ## 0.3.1 (2015/04/20)
20 |
21 | - fixed multiple links in hal
22 |
23 | ## 0.3.0 (2015/04/06)
24 |
25 | - in case of unsupported media type return `UnsupportedMediaTypeHttpResponse`
26 | - added server test
27 | - inject agent also in subresponses of json, hal and atom response
28 | - handle `Location`-redirects with `/` at the beginning on 201 Status Code
29 | - added `HttpAgent#logDebug` and `HttpAgent#logTrace` (enable it with `HttpAgent.enableLogging=true;` )
30 |
31 | ## 0.2.0 (2015/04/04)
32 |
33 | - added mocha test infrastructure
34 | - renamed to hateoas-client.js
35 | - handle relative paths in links (by asking HttpAgent for the base url)
36 | - Added FIXME method for getLinks on HTML/XML objects
37 | - added nodejs support with domino, jquery and xmlhttprequest for nodejs
38 | - added `HttpAgent#getBaseUrl`
39 |
40 | ## 0.1.0
41 |
42 | - added definition for requirejs
43 | - added JsonHalHttpResponse for HAL hyper media type
44 | - fixes JsonHttpResponse xhr variable on sub values
45 | - added AtomXmlHttpResponse and example for atom feed retrieval
46 | - added filtering * + object to breadth first search for anything until it matches a filter object
47 | - added handler for 201 Created response
48 | - added handling for status code 200
49 | - added * as indicator for breadth first search in HttpAgent#navigate
50 | - added function as filter object
51 | - example files added
52 | - initial commit
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | hateoas-client.js is licensed under the terms of MIT License.
2 |
3 | Copyright (c) 2011-2014 by DracoBlue (JanS@DracoBlue.de)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | hateoas-client.js README
2 | =======================
3 |
4 | * Latest Release: [](https://github.com/DracoBlue/hateoas-client-js/releases)
5 | * Build-Status: [](https://travis-ci.org/DracoBlue/hateoas-client-js)
6 | * Official Site:
7 |
8 | hateoas-client.js is copyright 2011-2015 by DracoBlue
9 |
10 | What is hateoas-client.js?
11 | -----------------------
12 |
13 | hateoas-client.js is a library (for browser+nodejs) to communicate with RESTful services. It uses
14 | jQuery as ajax library. It's aim is to provide a very simple API to follow
15 | the `links` defined in a request response, thus achieving
16 | level 3 in `Richardson Maturity Model`.
17 |
18 | Requirements:
19 |
20 | * jQuery 1.5+
21 |
22 | Installation
23 | ------------
24 |
25 | * On the browser: `$ bower install hateoas-client`
26 | * In nodejs: `$ npm install hateoas-client`
27 |
28 | How does it work?
29 | -----------------
30 |
31 | ### Example with JSON (in the browser)
32 |
33 | If you include `hateoas-client.js` after your `jQuery.js`, you'll have the ability
34 | to make such requests:
35 |
36 | var a = new HttpAgent('/api');
37 | a.navigate(['coffee', {'name': 'Small'}, "buy"]);
38 | a.post(function(response) {
39 | // response.getValue() contains what the POST /api/orders?product_id=5 returned
40 | });
41 |
42 | This example assumes that the responses contain
43 |
44 | GET /api
45 | {
46 | "links": [ { "rel": "coffee", "href": "/api/coffee" } ]
47 | }
48 | GET /api/coffee
49 | [
50 | {
51 | "id": 5,
52 | "name": "Small",
53 | "links": [ { "rel": "buy", "href": "/api/orders?product_id=5" } ]
54 | }
55 | ]
56 |
57 | ### Remote Example with Atom-Feed
58 |
59 | This example retrieves the most viewed videos from youtube, navigates 2x next, chooses the
60 | first element (because of empty filter`{}`), navigates to it's `self` link and finally:
61 | returns the `` element's text.
62 |
63 | var a = new HttpAgent('http://gdata.youtube.com/feeds/api/standardfeeds/most_viewed?max-results=5', {}, {
64 | 'proxy_script': 'proxy.php?url='
65 | });
66 |
67 | a.navigate(['next', 'next', {}, 'self']);
68 | a.get(function(response) {
69 | var title = jQuery(response.getValue()).find('title').text();
70 | });
71 |
72 | That example also shows, how one can use the `proxy_script`-option to use a
73 | `.php`-script to retrieve contents from a remote site.
74 |
75 | Usage with require.js
76 | ---------------------
77 |
78 | If you want to retrieve the HttpAgent in your require.js script use (ensure that `hateoas-client` maps on `hateoas-client.js`
79 | in your requirejs config file):
80 |
81 | ``` javascript
82 | require('hateoas-client', function(hateoasClient) {
83 | var a = new hateoasClient.HttpAgent('/api');
84 | });
85 | ```
86 |
87 | Supported Media Types
88 | ---------------------
89 |
90 | * `application/json` (detected by `JsonHttpResponse`), supported features:
91 | - `getValue()`: Returns the entire response as json object
92 | - `getValues()`: If the entire response was an array, it will return each element as `JsonHttpResponse[]`.
93 | - `getMatchingValue()`: Filters on `getValues()`
94 | - `getLinks()`: Expects a `links`-property as array with `{rel: REL, "href": URL}` elements and returns them as `HttpLink[]`
95 | * `application/atom+xml` (detected by `AtomXmlHttpResponse`), supported features:
96 | - `getValue()`: Returns the entire response as xml object
97 | - `getValues()`: Returns all `` children as `AtomXmlHttpResponse[]`.
98 | - `getMatchingValue()`: Filters on `getValues()`
99 | - `getLinks()`: Returns all `` children as `HttpLink[]`
100 | * `application/hal+json` (detected by `JsonHalHttpResponse`), supported features:
101 | - `getValue()`: Returns the entire response as json object
102 | - `getValues()`: Returns all `_embedded` objects as `JsonHalHttpResponse[]`.
103 | - `getMatchingValue()`: Filters on `getValues()`
104 | - `getLinks()`: Returns all `_links` and `_embedded` links as `HttpLink[]` according to HAL specification
105 | * `application/hc+json` (detected by `JsonHcHttpResponse`), supported features:
106 | - `getValue()`: Returns the entire response as json object
107 | - `getValues()`: Returns all embedded objects on root level with `self`-link as `JsonHalHttpResponse[]`.
108 | - `getMatchingValue()`: Filters on `getValues()`
109 | - `getLinks()`: Returns all embedded objects (with `self`-link), obvious links (with iana registration) or properties starting with `http:`/`https:` on root level as `HttpLink[]`
110 | * `application/xml` (detected by `XmlHttpResponse`), supported features:
111 | - `getValue()`: Returns the entire response as xml object
112 |
113 | Todos
114 | -----
115 |
116 | * test and extend support for other responses (xml, maybe a generic converter system or usage of the one from jQuery)
117 | * handle status codes other then 200 (currently only 200 is `JsonHttpResponse#isOk() == true` and 201 is interpreted)
118 | * add documentation for `HttpAgent`, `JsonHttpResponse`, `AtomXmlHttpResponse` and `XmlHttpResponse`
119 | * ... more as soon as I get to that!
120 |
121 | Changelog
122 | ---------
123 |
124 | See CHANGLOG.md for more information.
125 |
126 | License
127 | --------
128 |
129 | hateoas-client.js is licensed under the terms of MIT. See LICENSE for more information.
130 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hateoas-client",
3 | "main": "hateoas-client.js",
4 | "homepage": "https://github.com/DracoBlue/hateoas-client-js",
5 | "authors": [
6 | "DracoBlue "
7 | ],
8 | "description": "hateoas-client.js is a library to communicate with RESTful services. It uses jQuery as ajax library. It's aim is to provide a very simple API to follow the links defined in a request response, thus achieving level 3 in Richardson Maturity Model.",
9 | "moduleType": [
10 | "amd",
11 | "globals"
12 | ],
13 | "keywords": [
14 | "rest",
15 | "client",
16 | "rmm3",
17 | "jquery",
18 | "api",
19 | "hal",
20 | "atom",
21 | "json",
22 | "xml"
23 | ],
24 | "license": "MIT",
25 | "ignore": [
26 | "**/.*",
27 | "node_modules",
28 | "bower_components",
29 | "test",
30 | "tests"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/example/api.json:
--------------------------------------------------------------------------------
1 | {
2 | "links": [ { "rel": "coffee", "href": "api/coffee.json" }]
3 | }
--------------------------------------------------------------------------------
/example/api/coffee.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 5,
4 | "name": "Small",
5 | "links": [ { "rel": "buy", "href": "api/orders.php?product_id=5" } ]
6 | }
7 | ]
--------------------------------------------------------------------------------
/example/api/orders.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | Example Feed
4 | A subtitle.
5 |
6 |
7 |
8 | urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6
9 | 2003-12-13T18:30:02Z
10 |
11 | John Doe
12 | johndoe@example.com
13 |
14 |
15 | Atom-Powered Robots Run Amok
16 |
17 |
18 |
19 | urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a
20 | 2003-12-13T18:30:02Z
21 | Some text.
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/example/atom/entry-1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Atom-Powered Robots Run Amok
4 |
5 |
6 |
7 | urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a
8 | 2003-12-13T18:30:02Z
9 | Some text.
10 |
11 |
--------------------------------------------------------------------------------
/example/atom/recent.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Example Feed
4 | A subtitle.
5 |
6 |
7 |
8 | urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6
9 | 2003-12-13T18:30:02Z
10 |
11 | John Doe
12 | johndoe@example.com
13 |
14 |
15 | Atom-Powered Robots Run Amok
16 |
17 |
18 |
19 | urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a
20 | 2003-12-13T18:30:02Z
21 | Some text.
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/example/buy_coffee.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Please see your browsers console.log for output!
9 |
10 |
62 |
63 |
--------------------------------------------------------------------------------
/example/hal/api.json:
--------------------------------------------------------------------------------
1 | {
2 | "_links": {
3 | "coffee": { "href": "hal_local.php?file=coffee.json" }
4 | }
5 | }
--------------------------------------------------------------------------------
/example/hal/coffee.json:
--------------------------------------------------------------------------------
1 | {
2 | "_embedded": {
3 | "product": {
4 | "id": 5,
5 | "name": "Small",
6 | "_links": {
7 | "buy": { "rel": "buy", "href": "hal/orders.php?product_id=5" }
8 | }
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/example/hal/order1.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 1,
3 | "status": "pending"
4 | }
5 |
--------------------------------------------------------------------------------
/example/hal/orders.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Please see your browsers console.log for output!
9 |
10 |
64 |
65 |
--------------------------------------------------------------------------------
/example/hal_local.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Please see your browsers console.log for output!
9 |
10 |
41 |
42 |
--------------------------------------------------------------------------------
/example/local.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Please see your browsers console.log for output!
9 |
10 |
49 |
50 |
--------------------------------------------------------------------------------
/hateoas-client.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2011-2015 DracoBlue, http://dracoblue.net/
3 | *
4 | * Licensed under the terms of MIT License. For the full copyright and license
5 | * information, please see the LICENSE file in the root folder.
6 | */
7 |
8 | if (typeof window === 'undefined') { /* Running in NodeJS */
9 | var domino = require('domino');
10 | var $ = require('jquery')(domino.createWindow());
11 | var jQuery = $;
12 | var XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;
13 | $.support.cors=true; // cross domain
14 | $.ajaxSettings.xhr = function() {
15 | return new XMLHttpRequest();
16 | };
17 | }
18 |
19 | NotOkHttpResponse = function() {
20 |
21 | };
22 |
23 | NotOkHttpResponse.prototype.isOk = function() {
24 | return false;
25 | };
26 |
27 | HttpAgent = function(url, headers, options) {
28 | this.url = url;
29 | this.default_headers = headers || {};
30 | this.navigation_steps = [];
31 | this.options = options || {};
32 | this.logDebug('initialized', url);
33 | };
34 |
35 | HttpAgent.prototype.clone = function() {
36 | var clone = new HttpAgent(this.url);
37 | clone.default_headers = jQuery.extend(true, {}, this.default_headers);
38 | clone.navigation_steps = jQuery.extend(true, [], this.navigation_steps);
39 | return clone;
40 | };
41 |
42 | HttpAgent.enableLogging = false;
43 |
44 | HttpAgent.prototype.logDebug = function() {
45 | if (HttpAgent.enableLogging)
46 | {
47 | if (typeof this.uniqueId === "undefined") {
48 | this.uniqueId = (global.httpAgentNextUniqueLoggingId || 1);
49 | global.httpAgentNextUniqueLoggingId = this.uniqueId + 1;
50 | }
51 | var parameters = Array.prototype.slice.apply(arguments, [0]);
52 | parameters.unshift('[HttpAgent#' + this.uniqueId + ']');
53 | console.log.apply(console, parameters);
54 | }
55 | };
56 |
57 | HttpAgent.prototype.logTrace = function() {
58 | if (HttpAgent.enableLogging)
59 | {
60 | if (typeof this.uniqueId === "undefined") {
61 | this.uniqueId = (global.httpAgentNextUniqueLoggingId || 1);
62 | global.httpAgentNextUniqueLoggingId = this.uniqueId + 1;
63 | }
64 | var parameters = Array.prototype.slice.apply(arguments, [0]);
65 | var methodName = parameters.shift();
66 | parameters.unshift('[HttpAgent.' + methodName + '#' + this.uniqueId + ']');
67 | console.log.apply(console, parameters);
68 | }
69 | };
70 |
71 | HttpAgent.prototype.getUrl = function()
72 | {
73 | return this.url;
74 | };
75 |
76 | HttpAgent.prototype.getBaseUrl = function()
77 | {
78 | var match = this.url.match(/^(.+?[\/]+?.+?)\//);
79 | if (match)
80 | {
81 | return match[1];
82 | }
83 |
84 | return '';
85 | };
86 |
87 | HttpAgent.prototype.rawCall = function(cb, verb, params, headers) {
88 | var that = this;
89 |
90 | headers = headers || {};
91 | var url = that.url;
92 |
93 | if (this.options.proxy_script) {
94 | url = this.options.proxy_script + encodeURIComponent(url);
95 | }
96 |
97 | this.logTrace('rawCall', verb, that.url, params);
98 |
99 | jQuery.ajax(jQuery.extend((this.options.ajaxOptions || {}), {
100 | beforeSend: function(xhrObj){
101 | for (var header in that.default_headers)
102 | xhrObj.setRequestHeader(header, that.default_headers[header]);
103 | for (var header in headers)
104 | xhrObj.setRequestHeader(header, headers[header]);
105 | return xhrObj;
106 | },
107 | url: url,
108 | dataType: 'text',
109 | type: verb,
110 | data: params || {},
111 | complete: function(response) {
112 | that.logDebug('status', response.status);
113 | that.logDebug('response', (response.responseText || '').substr(0, 255));
114 | if (response.status === 201) {
115 | var absolute_href = response.getResponseHeader('Location');
116 | if (absolute_href.substr(0, 1) == '/')
117 | {
118 | absolute_href = that.getBaseUrl() + absolute_href;
119 | }
120 | that.url = absolute_href;
121 | that.rawCall(cb, 'GET', {}, headers);
122 | } else if (response.status === 204) {
123 | cb(new NoContentResponse(response, null, that));
124 | } else {
125 | cb(HttpAgent.getHttpResponseByRawResponse(response, that));
126 | }
127 | }
128 | }));
129 | };
130 |
131 | HttpAgent.prototype.rawNavigate = function(cb) {
132 | var that = this;
133 | var step_position = 0;
134 | var last_response = null;
135 |
136 | var performNextStep = function() {
137 | if (step_position === that.navigation_steps.length) {
138 | that.navigation_steps = [];
139 | cb(last_response);
140 | return ;
141 | }
142 |
143 | that.rawCall(function(current_response) {
144 | last_response = current_response;
145 | if (!current_response.isOk()) {
146 | cb(current_response);
147 | return ;
148 | }
149 |
150 | var next_step = that.navigation_steps[step_position];
151 |
152 | if (typeof next_step !== 'string') {
153 | step_position += 1;
154 | current_response = current_response.getMatchingValue(next_step);
155 | }
156 |
157 | var link_name = that.navigation_steps[step_position];
158 |
159 | if (link_name === '*') {
160 | step_position++;
161 | var filter_object = that.navigation_steps[step_position];
162 |
163 | that.rawBreadthFirstSearch(function(next_step_entry_point, last_search_response) {
164 | last_response = last_search_response;
165 | if (!next_step_entry_point) {
166 | cb(new NotOkHttpResponse());
167 | return ;
168 | }
169 | that.url = next_step_entry_point.url;
170 | step_position++;
171 | performNextStep();
172 | }, filter_object);
173 | } else {
174 | var next_step_entry_point = null;
175 | try {
176 | next_step_entry_point = current_response.getLink(link_name);
177 | } catch (error) {
178 | cb(new NotOkHttpResponse());
179 | return;
180 | }
181 | that.url = next_step_entry_point.url;
182 |
183 | step_position++;
184 | performNextStep();
185 | }
186 | }, 'GET');
187 | };
188 |
189 | performNextStep();
190 | };
191 |
192 | HttpAgent.prototype.rawBreadthFirstSearch = function(cb, filter_object) {
193 | var url_was_in_frontier = {};
194 | url_was_in_frontier[this.url] = true;
195 |
196 | var frontier = [this.url];
197 | var frontier_length = frontier.length;
198 | var tmp_entry_point = new HttpAgent(this.url);
199 |
200 | var new_frontier = [];
201 |
202 | var step_position = 0;
203 |
204 | var performIteration = function() {
205 | if (step_position === frontier_length && new_frontier.length === 0) {
206 | cb(null, new NotOkHttpResponse());
207 | return ;
208 | }
209 |
210 | if (step_position === frontier_length) {
211 | step_position = 0;
212 | frontier = new_frontier;
213 | frontier_length = frontier.length;
214 | new_frontier = [];
215 | }
216 |
217 | tmp_entry_point.url = frontier[step_position];
218 | tmp_entry_point.rawCall(function(response) {
219 | /*
220 | * Was this response ok?
221 | */
222 | if (response.isOk()) {
223 | var links = response.getLinks();
224 |
225 | if (typeof filter_object === "string" && typeof links[filter_object] !== 'undefined') {
226 | /*
227 | * YES!
228 | */
229 | cb(links[filter_object][0], response);
230 | return ;
231 | }
232 |
233 | if (typeof filter_object === "function") {
234 | if (filter_object(response))
235 | {
236 | cb(tmp_entry_point.clone(), response);
237 | return ;
238 | }
239 | }
240 |
241 | if (typeof filter_object !== "string") {
242 | try {
243 | var matching_value = response.getMatchingValue(filter_object);
244 | cb(tmp_entry_point.clone(), response);
245 | return ;
246 | } catch (error) {
247 | /*
248 | * We'll continue, if that object didn't match
249 | */
250 | }
251 | }
252 |
253 | jQuery.each(links, function(pos, link_targets) {
254 | jQuery.each(link_targets, function(sub_pos, link) {
255 | if (!url_was_in_frontier[link.url]) {
256 | new_frontier.push(link.url);
257 | url_was_in_frontier[link.url] = true;
258 | }
259 | });
260 | });
261 | }
262 |
263 | /*
264 | * Continue with next element in the frontier!
265 | */
266 | step_position++;
267 | performIteration();
268 | }, 'GET');
269 | };
270 |
271 | performIteration();
272 | };
273 |
274 |
275 | HttpAgent.prototype.call = function(cb, verb, params, headers) {
276 | var that = this;
277 | if (this.navigation_steps.length === 0) {
278 | this.rawCall(cb, verb, params, headers);
279 | } else {
280 | this.rawNavigate(function(navigation_response) {
281 | if (navigation_response.isOk()) {
282 | that.rawCall(cb, verb, params, headers);
283 | } else {
284 | cb(navigation_response);
285 | }
286 | });
287 | }
288 | };
289 |
290 | HttpAgent.prototype.get = function(cb, params, headers) {
291 | this.call(cb, 'GET', params, headers || {});
292 | };
293 |
294 | HttpAgent.prototype.post = function(cb, params, headers) {
295 | this.call(cb, 'POST', params, headers || {});
296 | };
297 |
298 | HttpAgent.prototype['delete'] = function(cb, params, headers) {
299 | this.call(cb, 'DELETE', params, headers || {});
300 | };
301 |
302 | HttpAgent.prototype.put = function(cb, params, headers) {
303 | this.call(cb, 'PUT', params, headers || {});
304 | };
305 |
306 | HttpAgent.prototype.patch = function(cb, params, headers) {
307 | this.call(cb, 'PATCH', params, headers || {});
308 | };
309 |
310 | HttpAgent.prototype.head = function(cb, params, headers) {
311 | this.call(cb, 'HEAD', params, headers || {});
312 | };
313 |
314 | HttpAgent.prototype.navigate = function(steps) {
315 | if (typeof steps === 'string') {
316 | this.addNavigationStep(steps);
317 | } else {
318 | var steps_length = steps.length;
319 | for (var i = 0; i < steps_length; i++) {
320 | this.addNavigationStep(steps[i]);
321 | }
322 | }
323 | return this;
324 | };
325 |
326 | HttpAgent.prototype.addNavigationStep = function(step) {
327 | this.navigation_steps.push(step);
328 | };
329 |
330 | HttpAgent.response_content_types = [];
331 |
332 | HttpAgent.registerResponseContentTypes = function(content_types, converter_class) {
333 | this.response_content_types.push([content_types, converter_class]);
334 | };
335 |
336 | HttpAgent.relations = [];
337 |
338 | HttpAgent.registerRelation = function(relation) {
339 | this.relations.push(relation);
340 | };
341 |
342 | HttpAgent.isRegisteredRelation = function(relation) {
343 | return this.relations.indexOf(relation) === -1 ? false : true;
344 | };
345 |
346 | HttpAgent.getHttpResponseByRawResponse = function(raw_response, agent) {
347 | agent.logTrace('getHttpResponseByRawResponse', raw_response, agent);
348 | var content_type = (raw_response.getResponseHeader('content-type') || '').toLowerCase().split(';')[0];
349 | agent.logDebug('content type', content_type);
350 | var response_content_types = this.response_content_types;
351 | var response_content_types_length = response_content_types.length;
352 | for (var i = 0; i < response_content_types_length; i++) {
353 | if (response_content_types[i][0].indexOf(content_type) !== -1) {
354 | var converter_class = response_content_types[i][1];
355 | agent.logDebug('converter', converter_class);
356 | return new converter_class(raw_response, null, agent);
357 | }
358 | }
359 |
360 | return new UnsupportedMediaTypeHttpResponse();
361 | };
362 |
363 | HttpLink = function(link_data, url, headers, options) {
364 | this.url = url;
365 | this.default_headers = headers || {};
366 | this.navigation_steps = [];
367 | this.options = options || {};
368 | this.link_data = link_data;
369 | };
370 |
371 | jQuery.extend(HttpLink.prototype, HttpAgent.prototype);
372 |
373 | HttpLink.prototype.getRel = function() {
374 | return this.link_data['rel'];
375 | };
376 |
377 | HttpLink.prototype.getTitle = function() {
378 | return this.link_data['title'];
379 | };
380 |
381 | HttpLink.prototype.getType = function() {
382 | return this.link_data['type'];
383 | };
384 |
385 | BaseHttpResponse = function() {
386 |
387 | };
388 |
389 | BaseHttpResponse.prototype.isOk = function() {
390 | return 200 === this.getStatusCode() ? true : false;
391 | };
392 |
393 | BaseHttpResponse.prototype.getStatusCode = function() {
394 | return this.xhr.status;
395 | };
396 |
397 | BaseHttpResponse.prototype.getHeader = function(name, default_value) {
398 | return this.xhr.getResponseHeader(name) || default_value;
399 | };
400 |
401 | BaseHttpResponse.prototype.getAllHeaders = function() {
402 | var headers = {};
403 | var rawHeaders = this.xhr.getAllResponseHeaders();
404 | var extractHeadersRegExp = /^([^:]+):\s+(.+)$/mg;
405 | var match;
406 |
407 | while ((match = extractHeadersRegExp.exec(rawHeaders)) !== null) {
408 | headers[match[1].trim()] = match[2].trim();
409 | }
410 |
411 | return headers;
412 | };
413 |
414 | BaseHttpResponse.prototype.getLink = function(link_name) {
415 | var links = this.getLinks();
416 | if (typeof links[link_name] === 'undefined') {
417 | throw new Error('Cannot find link with name: ' + link_name);
418 | }
419 | return links[link_name][0];
420 | };
421 |
422 | NoContentResponse = function(xhr, value, agent) {
423 | this.xhr = xhr;
424 | this.agent = agent;
425 | this.value = value || null;
426 | this.values = null;
427 | this.links_map = null;
428 | };
429 |
430 | jQuery.extend(NoContentResponse.prototype, BaseHttpResponse.prototype);
431 |
432 |
433 | NoContentResponse.prototype.isOk = function() {
434 | return true;
435 | };
436 |
437 | NoContentResponse.prototype.getLinks = function() {
438 | return {};
439 | };
440 |
441 | NoContentResponse.prototype.getValue = function() {
442 | return ;
443 | };
444 |
445 | JsonHttpResponse = function(xhr, value, agent) {
446 | this.xhr = xhr;
447 | this.agent = agent;
448 | this.value = value || null;
449 | this.values = null;
450 | this.links_map = null;
451 | agent.logDebug('initialized JsonHttpResponse');
452 | };
453 |
454 | jQuery.extend(JsonHttpResponse.prototype, BaseHttpResponse.prototype);
455 |
456 | JsonHttpResponse.prototype.getValue = function() {
457 | if (!this.value) {
458 | this.value = jQuery.parseJSON(this.xhr.responseText);
459 | }
460 |
461 | return this.value;
462 | };
463 |
464 | JsonHttpResponse.prototype.at = function(pos) {
465 | var values = this.getValues();
466 | return values[pos];
467 | };
468 |
469 | JsonHttpResponse.prototype.getValues = function() {
470 | if (!this.values) {
471 | var value_entry_points = [];
472 |
473 | var values = this.getValue(this.xhr);
474 | var values_length = values.length;
475 |
476 | for (var i = 0; i < values_length; i++) {
477 | value_entry_points.push(new JsonHttpResponse(this.xhr, values[i], this.agent));
478 | }
479 |
480 | this.values = value_entry_points;
481 | }
482 |
483 | return this.values;
484 | };
485 |
486 | JsonHttpResponse.prototype.getMatchingValue = function(filter_object) {
487 | var values = this.getValue();
488 |
489 | /*
490 | * FIXME: An old man's check, whether what we've got here is an array or not
491 | */
492 | if (typeof values !== "object" || typeof values.join !== "function") {
493 | values = [values];
494 | }
495 |
496 | var values_length = values.length;
497 |
498 | for (var i = 0; i < values_length; i++) {
499 | var value = values[i];
500 | var is_match = true;
501 | if (typeof filter_object === 'function') {
502 | is_match = filter_object(value);
503 | } else {
504 | for (key in filter_object) {
505 | if (filter_object.hasOwnProperty(key) && filter_object[key] !== value[key]) {
506 | is_match = false;
507 | }
508 | }
509 | }
510 |
511 | if (is_match) {
512 | return new JsonHttpResponse(this.xhr, value, this.agent);
513 | }
514 | }
515 |
516 | throw new Error('No matching value found for filter object');
517 | };
518 |
519 | JsonHttpResponse.prototype.getLinks = function() {
520 | if (this.links_map) {
521 | return this.links_map;
522 | }
523 |
524 | var value = this.getValue();
525 | var links = [];
526 |
527 | if (value.hasOwnProperty('links')) {
528 | links = jQuery.extend(true, links, value['links']);
529 | }
530 |
531 | if (value.hasOwnProperty('link')) {
532 | links.push(value['link']);
533 | }
534 |
535 | var links_map = {};
536 | var links_length = links.length;
537 |
538 | for (var i = 0; i < links_length; i++) {
539 | var link = links[i];
540 | var headers = {};
541 | if (link.type) {
542 | headers['Content-Type'] = link.type;
543 | }
544 | links_map[link.rel] = links_map[link.rel] || [];
545 | var absolute_href = link.href;
546 | if (absolute_href.substr(0, 1) == '/')
547 | {
548 | absolute_href = this.agent.getBaseUrl() + absolute_href;
549 | }
550 | links_map[link.rel].push(new HttpLink({"rel": link.rel, "type": link.type},absolute_href, headers));
551 | }
552 |
553 | this.links_map = links_map;
554 | return this.links_map;
555 | };
556 |
557 | HttpAgent.registerResponseContentTypes(['application/json'], JsonHttpResponse);
558 |
559 | JsonHcHttpResponse = function(xhr, value, agent) {
560 | this.xhr = xhr;
561 | this.agent = agent;
562 | this.value = value || null;
563 | this.values = null;
564 | this.links_map = null;
565 | agent.logDebug('initialized JsonHcHttpResponse');
566 | };
567 |
568 | jQuery.extend(JsonHcHttpResponse.prototype, BaseHttpResponse.prototype);
569 |
570 | JsonHcHttpResponse.prototype.getValue = function() {
571 | if (!this.value) {
572 | this.value = jQuery.parseJSON(this.xhr.responseText);
573 | }
574 |
575 | return this.value;
576 | };
577 |
578 | JsonHcHttpResponse.prototype.at = function(pos) {
579 | var values = this.getValues();
580 | return values[pos];
581 | };
582 |
583 | JsonHcHttpResponse.prototype.getValues = function() {
584 | var that = this;
585 | if (!this.values) {
586 | var value_entry_points = [];
587 |
588 | var value = this.getValue(this.xhr);
589 |
590 | var parseLinkArrayOrProperty = function(value, key) {
591 | if (value.hasOwnProperty("self") && typeof value.self === "string") {
592 | /*
593 | * We have "key": {"self": "/some/url"}, so an embedded object. So we generate the
594 | * link for that object.
595 | */
596 | value_entry_points.push(new JsonHcHttpResponse(that.xhr, value, this.agent));
597 | } else if (typeof value == "object" && Array.isArray(value)) {
598 | value.forEach(function(array_item) {
599 | parseLinkArrayOrProperty(array_item, key);
600 | });
601 | }
602 | };
603 |
604 | for (var key in value) {
605 | if (value.hasOwnProperty(key)) {
606 | parseLinkArrayOrProperty(value[key], key);
607 | }
608 | }
609 | this.values = value_entry_points;
610 | }
611 |
612 | return this.values;
613 | };
614 |
615 | JsonHcHttpResponse.prototype.getMatchingValue = function(filter_object) {
616 | var values = this.getValue();
617 |
618 | /*
619 | * FIXME: An old man's check, whether what we've got here is an array or not
620 | */
621 | if (typeof values !== "object" || typeof values.join !== "function") {
622 | values = [values];
623 | }
624 |
625 | var values_length = values.length;
626 |
627 | for (var i = 0; i < values_length; i++) {
628 | var value = values[i];
629 | var is_match = true;
630 | if (typeof filter_object === 'function') {
631 | is_match = filter_object(value);
632 | } else {
633 | for (var key in filter_object) {
634 | if (filter_object.hasOwnProperty(key) && filter_object[key] !== value[key]) {
635 | is_match = false;
636 | }
637 | }
638 | }
639 |
640 | if (is_match) {
641 | return new JsonHcHttpResponse(this.xhr, value, this.agent);
642 | }
643 | }
644 |
645 | throw new Error('No matching value found for filter object');
646 | };
647 |
648 | JsonHcHttpResponse.prototype.getLinks = function() {
649 | var that = this;
650 | if (this.links_map) {
651 | return this.links_map;
652 | }
653 |
654 | var value = this.getValue();
655 |
656 | var isValidLinkKeyAndValue = function(key, value) {
657 | if (HttpAgent.isRegisteredRelation(key)) {
658 | return true;
659 | }
660 |
661 | if (key.substr(0, 5) == 'http:' || key.substr(0, 6) == 'https:') {
662 | return true;
663 | }
664 |
665 | //if (typeof value === "string" && value.substr(0, 1) == '/') {
666 | // return true;
667 | //}
668 |
669 | return false;
670 | };
671 |
672 | var links_map = {};
673 |
674 | var parseLinkArrayOrProperty = function(value, key) {
675 | if (typeof value == "string") {
676 | /*
677 | * We have "key": "/relative/url" or "key": "http://example.org/absolute/url"
678 | */
679 | var absolute_href = value;
680 | if (absolute_href.substr(0, 1) == '/')
681 | {
682 | absolute_href = that.agent.getBaseUrl() + absolute_href;
683 | }
684 | links_map[key] = links_map[key] || [];
685 | links_map[key].push(new HttpLink({"rel": key}, absolute_href, {}));
686 | } else if (value.hasOwnProperty("self") && typeof value.self === "string") {
687 | /*
688 | * We have "key": {"self": "/some/url"}, so an embedded object. So we generate the
689 | * link for that object.
690 | */
691 | links_map[key] = links_map[key] || [];
692 | links_map[key].push(new HttpLink({"rel": key}, value.self, {}));
693 | } else if (typeof value == "object" && Array.isArray(value)) {
694 | value.forEach(function(array_item) {
695 | parseLinkArrayOrProperty(array_item, key);
696 | });
697 | }
698 | };
699 |
700 | for (var key in value) {
701 | if (value.hasOwnProperty(key) && isValidLinkKeyAndValue(key, value[key])) {
702 | parseLinkArrayOrProperty(value[key], key);
703 | }
704 | }
705 |
706 | this.links_map = links_map;
707 | return this.links_map;
708 | };
709 |
710 | HttpAgent.registerResponseContentTypes(['application/hc+json'], JsonHcHttpResponse);
711 |
712 | JsonHalHttpResponse = function(xhr, value, agent) {
713 | this.xhr = xhr;
714 | this.agent = agent;
715 | this.value = value || null;
716 | this.values = null;
717 | this.links_map = null;
718 | };
719 |
720 | jQuery.extend(JsonHalHttpResponse.prototype, BaseHttpResponse.prototype);
721 |
722 | JsonHalHttpResponse.prototype.getValue = function() {
723 | if (!this.value) {
724 | this.value = jQuery.parseJSON(this.xhr.responseText);
725 | }
726 |
727 | return this.value;
728 | };
729 |
730 | JsonHalHttpResponse.prototype.at = function(pos) {
731 | var values = this.getValues();
732 | return values[pos];
733 | };
734 |
735 | JsonHalHttpResponse.prototype.getValues = function() {
736 | if (!this.values) {
737 | var value_entry_points = [];
738 |
739 | var value = this.getValue(this.xhr);
740 |
741 | var embedded_objects_map = {};
742 |
743 | if (value.hasOwnProperty('_embedded')) {
744 | embedded_objects_map = jQuery.extend(true, {}, value['_embedded']);
745 | }
746 |
747 | for (var rel in embedded_objects_map)
748 | {
749 | if (embedded_objects_map.hasOwnProperty(rel))
750 | {
751 | var embedded_objects = embedded_objects_map[rel];
752 |
753 | /*
754 | * FIXME: An old man's check, whether what we've got here is an array or not
755 | */
756 | if (typeof embedded_objects !== "object" || typeof embedded_objects.join !== "function") {
757 | embedded_objects = [embedded_objects];
758 | }
759 |
760 | var embedded_objects_length = embedded_objects.length;
761 |
762 | for (var i = 0; i < embedded_objects_length; i++)
763 | {
764 | value_entry_points.push(new JsonHalHttpResponse(this.xhr, embedded_objects[i], this.agent));
765 | }
766 | }
767 | }
768 |
769 | this.values = value_entry_points;
770 | }
771 |
772 | return this.values;
773 | };
774 |
775 | JsonHalHttpResponse.prototype.getMatchingValue = function(filter_object) {
776 | var values = this.getValues();
777 |
778 | var values_length = values.length;
779 |
780 | for (var i = 0; i < values_length; i++) {
781 | var value = values[i].getValue();
782 | var is_match = true;
783 | if (typeof filter_object === 'function') {
784 | is_match = filter_object(value);
785 | } else {
786 | for (key in filter_object) {
787 | if (filter_object.hasOwnProperty(key) && filter_object[key] !== value[key]) {
788 | is_match = false;
789 | }
790 | }
791 | }
792 |
793 | if (is_match) {
794 | return new JsonHalHttpResponse(this.xhr, value, this.agent);
795 | }
796 | }
797 |
798 | throw new Error('No matching value found for filter object');
799 | };
800 |
801 | JsonHalHttpResponse.prototype.getLinks = function() {
802 | if (this.links_map) {
803 | return this.links_map;
804 | }
805 |
806 | var value = this.getValue();
807 | var links_map = {};
808 | var raw_links_map = {};
809 |
810 | if (value.hasOwnProperty('_links')) {
811 | raw_links_map = jQuery.extend(true, raw_links_map, value['_links']);
812 | }
813 |
814 | for (var rel in raw_links_map)
815 | {
816 | if (raw_links_map.hasOwnProperty(rel))
817 | {
818 | var raw_links = raw_links_map[rel];
819 |
820 | /*
821 | * FIXME: An old man's check, whether what we've got here is an array or not
822 | */
823 | if (typeof raw_links !== "object" || typeof raw_links.join !== "function") {
824 | raw_links = [raw_links];
825 | }
826 |
827 | var raw_links_length = raw_links.length;
828 |
829 | for (var i = 0; i < raw_links_length; i++)
830 | {
831 | var link = raw_links[i];
832 | var headers = {};
833 | /* FIXME: is `type` allowed in HAL? */
834 | if (link.type) {
835 | headers['Content-Type'] = link.type;
836 | }
837 | links_map[rel] = links_map[rel] || [];
838 | var absolute_href = link.href;
839 | if (absolute_href.substr(0, 1) == '/')
840 | {
841 | absolute_href = this.agent.getBaseUrl() + absolute_href;
842 | }
843 | links_map[rel].push(new HttpLink({"rel": rel, "type": link.type, "title": link.title}, absolute_href, headers));
844 | }
845 | }
846 | }
847 |
848 | this.links_map = links_map;
849 | return this.links_map;
850 | };
851 |
852 | HttpAgent.registerResponseContentTypes(['application/hal+json'], JsonHalHttpResponse);
853 |
854 |
855 | AtomXmlHttpResponse = function(xhr, value, agent) {
856 | this.xhr = xhr;
857 | this.agent = agent;
858 | this.value = value || null;
859 | this.values = null;
860 | this.links_map = null;
861 | };
862 |
863 | jQuery.extend(AtomXmlHttpResponse.prototype, BaseHttpResponse.prototype);
864 |
865 | AtomXmlHttpResponse.prototype.getValue = function() {
866 | if (!this.value) {
867 | this.value = this.xhr.responseXML.childNodes[0];
868 | }
869 | return this.value;
870 | };
871 |
872 | AtomXmlHttpResponse.prototype.getValues = function() {
873 | var that = this;
874 |
875 | if (!this.values) {
876 | var value = this.getValue();
877 |
878 | this.values = [];
879 |
880 | var entries = jQuery(this.getValue()).children('entry');
881 | jQuery.each(entries, function(pos, raw_entry) {
882 | that.values.push(new AtomXmlHttpResponse(that.xhr, raw_entry, that.agent));
883 | });
884 | }
885 |
886 | return this.values;
887 | };
888 |
889 | AtomXmlHttpResponse.prototype.getMatchingValue = function(filter_object) {
890 | var value_entry_points = [];
891 |
892 | var entries = jQuery(this.getValue()).children('entry');
893 | var entries_length = entries.length;
894 |
895 | for (var i = 0; i < entries_length; i++) {
896 | var raw_entry = entries[i];
897 | var value = new AtomXmlHttpResponse(this.xhr, raw_entry, this.agent);
898 | var is_match = true;
899 | if (typeof filter_object === 'function') {
900 | is_match = filter_object(value);
901 | } else {
902 | for (key in filter_object) {
903 | if (filter_object.hasOwnProperty(key) && jQuery(raw_entry).find(key).text() != filter_object[key]) {
904 | is_match = false;
905 | }
906 | }
907 | }
908 |
909 | if (is_match) {
910 | return value;
911 | }
912 | }
913 |
914 | throw new Error('No matching value found for filter object');
915 | };
916 |
917 | AtomXmlHttpResponse.prototype.getLinks = function() {
918 | if (this.links_map) {
919 | return this.links_map;
920 | }
921 |
922 | var links_map = {};
923 | var links = jQuery(this.getValue()).children('link');
924 | jQuery.each(links, function(pos, raw_link) {
925 | var headers = {};
926 | var link = jQuery(raw_link);
927 | var rel = link.attr('rel');
928 | if (link.attr('type')) {
929 | headers['Content-Type'] = link.attr('type');
930 | }
931 | links_map[rel] = links_map[rel] || [];
932 | var absolute_href = link.attr('href');
933 | if (absolute_href.substr(0, 1) == '/')
934 | {
935 | absolute_href = this.agent.getBaseUrl() + absolute_href;
936 | }
937 | links_map[rel].push(new HttpLink({"rel": rel, "type": link.attr('type')}, absolute_href, headers));
938 | });
939 |
940 | this.links_map = links_map;
941 | return this.links_map;
942 | };
943 |
944 | HttpAgent.registerResponseContentTypes(['application/atom+xml'], AtomXmlHttpResponse);
945 |
946 | XmlHttpResponse = function(xhr, value, agent) {
947 | this.xhr = xhr;
948 | this.agent = agent;
949 | this.value = value || null;
950 | this.values = null;
951 | this.links_map = null;
952 | };
953 |
954 | jQuery.extend(XmlHttpResponse.prototype, BaseHttpResponse.prototype);
955 |
956 | XmlHttpResponse.prototype.getValue = function() {
957 | if (!this.value) {
958 | this.value = this.xhr.responseXML;
959 | }
960 |
961 | return this.value;
962 | };
963 |
964 | XmlHttpResponse.prototype.getLinks = function() {
965 | /* FIXME: not yet implemented */
966 | return {};
967 | };
968 |
969 | UnsupportedMediaTypeHttpResponse = function() {
970 |
971 | };
972 |
973 | UnsupportedMediaTypeHttpResponse.prototype.isOk = function() {
974 | return false;
975 | };
976 |
977 | HttpAgent.registerResponseContentTypes(['text/html', 'application/xml'], XmlHttpResponse);
978 |
979 | /*
980 | * The registered relations are auto-generated like this (last update 2015/07/12):
981 | * curl -sS http://www.iana.org/assignments/link-relations/link-relations.xml | grep '' | tr -s ' ' | cut -f '2' -d '>' | cut -f '1' -d '<' | while read line; do echo "HttpAgent.registerRelation('$line');"; done
982 | */
983 | HttpAgent.registerRelation('about');
984 | HttpAgent.registerRelation('alternate');
985 | HttpAgent.registerRelation('appendix');
986 | HttpAgent.registerRelation('archives');
987 | HttpAgent.registerRelation('author');
988 | HttpAgent.registerRelation('bookmark');
989 | HttpAgent.registerRelation('canonical');
990 | HttpAgent.registerRelation('chapter');
991 | HttpAgent.registerRelation('collection');
992 | HttpAgent.registerRelation('contents');
993 | HttpAgent.registerRelation('copyright');
994 | HttpAgent.registerRelation('create-form');
995 | HttpAgent.registerRelation('current');
996 | HttpAgent.registerRelation('derivedfrom');
997 | HttpAgent.registerRelation('describedby');
998 | HttpAgent.registerRelation('describes');
999 | HttpAgent.registerRelation('disclosure');
1000 | HttpAgent.registerRelation('duplicate');
1001 | HttpAgent.registerRelation('edit');
1002 | HttpAgent.registerRelation('edit-form');
1003 | HttpAgent.registerRelation('edit-media');
1004 | HttpAgent.registerRelation('enclosure');
1005 | HttpAgent.registerRelation('first');
1006 | HttpAgent.registerRelation('glossary');
1007 | HttpAgent.registerRelation('help');
1008 | HttpAgent.registerRelation('hosts');
1009 | HttpAgent.registerRelation('hub');
1010 | HttpAgent.registerRelation('icon');
1011 | HttpAgent.registerRelation('index');
1012 | HttpAgent.registerRelation('item');
1013 | HttpAgent.registerRelation('last');
1014 | HttpAgent.registerRelation('latest-version');
1015 | HttpAgent.registerRelation('license');
1016 | HttpAgent.registerRelation('lrdd');
1017 | HttpAgent.registerRelation('memento');
1018 | HttpAgent.registerRelation('monitor');
1019 | HttpAgent.registerRelation('monitor-group');
1020 | HttpAgent.registerRelation('next');
1021 | HttpAgent.registerRelation('next-archive');
1022 | HttpAgent.registerRelation('nofollow');
1023 | HttpAgent.registerRelation('noreferrer');
1024 | HttpAgent.registerRelation('original');
1025 | HttpAgent.registerRelation('payment');
1026 | HttpAgent.registerRelation('predecessor-version');
1027 | HttpAgent.registerRelation('prefetch');
1028 | HttpAgent.registerRelation('prev');
1029 | HttpAgent.registerRelation('preview');
1030 | HttpAgent.registerRelation('previous');
1031 | HttpAgent.registerRelation('prev-archive');
1032 | HttpAgent.registerRelation('privacy-policy');
1033 | HttpAgent.registerRelation('profile');
1034 | HttpAgent.registerRelation('related');
1035 | HttpAgent.registerRelation('replies');
1036 | HttpAgent.registerRelation('search');
1037 | HttpAgent.registerRelation('section');
1038 | HttpAgent.registerRelation('self');
1039 | HttpAgent.registerRelation('service');
1040 | HttpAgent.registerRelation('start');
1041 | HttpAgent.registerRelation('stylesheet');
1042 | HttpAgent.registerRelation('subsection');
1043 | HttpAgent.registerRelation('successor-version');
1044 | HttpAgent.registerRelation('tag');
1045 | HttpAgent.registerRelation('terms-of-service');
1046 | HttpAgent.registerRelation('timegate');
1047 | HttpAgent.registerRelation('timemap');
1048 | HttpAgent.registerRelation('type');
1049 | HttpAgent.registerRelation('up');
1050 | HttpAgent.registerRelation('version-history');
1051 | HttpAgent.registerRelation('via');
1052 | HttpAgent.registerRelation('working-copy');
1053 | HttpAgent.registerRelation('working-copy-of');
1054 |
1055 | if (typeof define !== "undefined")
1056 | {
1057 | define('hateoas-client-js', [], function () {
1058 | return {
1059 | "HttpAgent": HttpAgent
1060 | };
1061 | });
1062 | }
1063 | else if (typeof exports !== "undefined")
1064 | {
1065 | exports.HttpAgent = HttpAgent;
1066 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hateoas-client",
3 | "description": "A library to communicate with RESTful services. It's aim is to provide a very simple API to follow the links defined in a request response, thus achieving level 3 in Richardson Maturity Model.",
4 | "main": "hateoas-client.js",
5 | "directories": {
6 | "example": "example"
7 | },
8 | "scripts": {
9 | "test": "mocha"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/DracoBlue/hateoas-client-js.git"
14 | },
15 | "keywords": [
16 | "rest",
17 | "hateoas",
18 | "api",
19 | "json",
20 | "hal",
21 | "atom",
22 | "xml"
23 | ],
24 | "author": "DracoBlue ",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/DracoBlue/hateoas-client-js/issues"
28 | },
29 | "homepage": "https://github.com/DracoBlue/hateoas-client-js",
30 | "dependencies": {
31 | "domino": "^1.0.18",
32 | "jquery": "^2.1.3",
33 | "xmlhttprequest": "^1.7.0"
34 | },
35 | "devDependencies": {
36 | "express": "^4.12.3",
37 | "mocha": "*"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/test/hal-test.js:
--------------------------------------------------------------------------------
1 | var assert = require("assert")
2 | var hateoasClient = require('./../hateoas-client.js');
3 |
4 | describe('HAL Test', function(){
5 | var express = require('express');
6 | var app = express();
7 |
8 | //hateoasClient.HttpAgent.enableLogging = true;
9 |
10 | app.get('/api.json', function(req, res) {
11 | res.set('Content-Type', 'application/hal+json');
12 | res.send(JSON.stringify({
13 | "_links": { "coffee": { "href": "/api/coffee.json" } }
14 | }));
15 | });
16 | app.get('/api/product/5', function(req, res) {
17 | res.set('Content-Type', 'application/hal+json');
18 | res.send(JSON.stringify(
19 | {
20 | "id": 5,
21 | "name": "Small",
22 | "_links": {"self": {"href": "/api/product/5"}, "buy": {"href": "/api/submitted-orders?product_id=5"}}
23 | }
24 | ));
25 | });
26 | app.get('/api/coffee.json', function(req, res) {
27 | res.set('Content-Type', 'application/hal+json');
28 | res.send(JSON.stringify({
29 | "title": "Coffee Menu",
30 | "_links": {
31 | "product": [{"title": "Small", "href": "/api/product/5"}]
32 | },
33 | "_embedded": {
34 | "product": [
35 | {
36 | "id": 5,
37 | "name": "Small",
38 | "_links": {"self": {"href": "/api/product/5"}, "buy": {"href": "/api/submitted-orders?product_id=5"}}
39 | }
40 | ]
41 | }
42 | }));
43 | });
44 | app.post('/api/submitted-orders', function(req, res) {
45 | res.redirect(201, '/api/orders/1');
46 | });
47 | app.get('/api/orders/1', function(req, res) {
48 | res.set('Content-Type', 'application/hal+json');
49 | res.send(JSON.stringify(
50 | {
51 | "id": 1,
52 | "status": "pending"
53 | }
54 | ));
55 | });
56 |
57 | describe('new HttpAgent()', function(){
58 |
59 | var server = null;
60 |
61 | before(function() {
62 | server = app.listen(3000, '127.0.0.1');
63 | });
64 |
65 | after(function(done){
66 | server.close(function() {
67 | done();
68 | });
69 | });
70 |
71 | it('should give a link', function(done){
72 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
73 |
74 | agent.get(function(response) {
75 | assert.equal(true, response.isOk(), 'It was not possible to navigate!');
76 | var links = response.getLinks();
77 | assert.ok(links["coffee"]);
78 | assert.equal(1, links["coffee"].length);
79 | var coffee_link = response.getLink('coffee');
80 | assert.equal('http://127.0.0.1:3000/api/coffee.json', coffee_link.getUrl());
81 | assert.equal('coffee', coffee_link.getRel());
82 | assert.equal('Express', response.getHeader('x-powered-by'));
83 | assert.equal('Express', response.getAllHeaders()['x-powered-by']);
84 | done();
85 | });
86 | });
87 |
88 | it('should navigate with filter object', function(done){
89 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
90 |
91 | agent.navigate(['coffee', {
92 | "name" : 'Small'
93 | }, 'buy']);
94 |
95 | agent.post(function(response) {
96 | assert.equal(true, response.isOk(), 'It was not possible to navigate!');
97 | assert.equal('pending', response.getValue().status, 'Cannot find status after navigation');
98 | done();
99 | });
100 | });
101 |
102 | it('should navigate without anything', function(done){
103 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
104 |
105 | agent.navigate('coffee');
106 |
107 | agent.get(function(response) {
108 | assert.equal(true, response.isOk(), 'It was not possible to navigate!');
109 | assert.equal('Coffee Menu', response.getValue().title, 'Cannot find title of the coffee menu');
110 | done();
111 | });
112 | });
113 |
114 | it('should navigate with filter function', function(done){
115 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
116 |
117 | agent.navigate(['coffee', function(value) {
118 | return value.name === 'Small';
119 | }, 'buy']);
120 |
121 | agent.post(function(response) {
122 | assert.equal(true, response.isOk(), 'It was not possible to navigate!');
123 | assert.equal('pending', response.getValue().status, 'Cannot find status after navigation');
124 | done();
125 | });
126 | });
127 |
128 | it('should return isOk = false, in case of broken link', function(done){
129 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
130 |
131 | agent.navigate(['coffee', 'invalid_link', 'buy']);
132 |
133 | agent.get(function(response) {
134 | assert.equal(false, response.isOk(), 'It was possible to navigate!');
135 | done();
136 | });
137 | });
138 | })
139 | })
--------------------------------------------------------------------------------
/test/init-test.js:
--------------------------------------------------------------------------------
1 | var assert = require("assert")
2 | var hateoasClient = require('./../hateoas-client.js');
3 |
4 | describe('InitializeHttpAgent', function(){
5 | describe('new HttpAgent()', function(){
6 |
7 | it('should initialize an agent', function(){
8 | var agent = new hateoasClient.HttpAgent();
9 | })
10 | })
11 | })
--------------------------------------------------------------------------------
/test/invalid-media-type-test.js:
--------------------------------------------------------------------------------
1 | var assert = require("assert")
2 | var hateoasClient = require('./../hateoas-client.js');
3 |
4 | describe('Invalid Media Type Test', function(){
5 | var express = require('express');
6 | var app = express();
7 |
8 | //hateoasClient.HttpAgent.enableLogging = true;
9 |
10 | app.get('/api.json', function(req, res) {
11 | res.set('Content-Type', 'invalid/media-type-test');
12 | res.send('lalalala');
13 | });
14 |
15 | describe('new HttpAgent()', function(){
16 |
17 | var server = null;
18 |
19 | before(function() {
20 | server = app.listen(3000, '127.0.0.1');
21 | });
22 |
23 | after(function(done){
24 | server.close(function() {
25 | done();
26 | });
27 | });
28 |
29 | it('should not throw an error', function(done){
30 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
31 |
32 | agent.get(function(response) {
33 | assert.equal(false, response.isOk(), 'It was possible to navigate!');
34 | done();
35 | });
36 | });
37 | })
38 | })
--------------------------------------------------------------------------------
/test/json-hc-test.js:
--------------------------------------------------------------------------------
1 | var assert = require("assert")
2 | var hateoasClient = require('./../hateoas-client.js');
3 |
4 | describe('JSON HC Test', function(){
5 | var express = require('express');
6 | var app = express();
7 |
8 | //hateoasClient.HttpAgent.enableLogging = true;
9 |
10 | app.get('/api.json', function(req, res) {
11 | res.set('Content-Type', 'application/hc+json');
12 | res.send(JSON.stringify({
13 | "http://example.org/rels/coffee": "/api/coffee.json",
14 | "http://example.org/rels/orders": "/api/orders"
15 | }));
16 | });
17 | app.get('/api/coffee.json', function(req, res) {
18 | res.set('Content-Type', 'application/hc+json');
19 | res.send(JSON.stringify([
20 | {
21 | "id": 5,
22 | "name": "Small",
23 | "http://example.org/rels/buy": "/api/submitted-orders?product_id=5"
24 | }
25 | ]));
26 | });
27 | app.post('/api/submitted-orders', function(req, res) {
28 | res.redirect(201, '/api/orders/1');
29 | });
30 | app.get('/api/orders/1', function(req, res) {
31 | res.set('Content-Type', 'application/hc+json');
32 | res.send(JSON.stringify(
33 | {
34 | "self": "/api/orders/1",
35 | "profile": "http://example.org/rels/order",
36 | "id": 1,
37 | "status": "pending"
38 | }
39 | ));
40 | });
41 |
42 | app.get('/api/orders/2', function(req, res) {
43 | res.set('Content-Type', 'application/hc+json');
44 | res.send(JSON.stringify(
45 | {
46 | "self": "/api/orders/2",
47 | "profile": "http://example.org/rels/order",
48 | "id": 2,
49 | "status": "finished"
50 | }
51 | ));
52 | });
53 |
54 | app.get('/api/orders', function(req, res) {
55 | res.set('Content-Type', 'application/hc+json');
56 | res.send(JSON.stringify({
57 | "next": "/api/orders?page=2",
58 | "first": "/api/orders",
59 | "http://example.org/rels/orders-item": [
60 | {
61 | "self": "/api/orders/1",
62 | "profile": "http://example.org/rels/order",
63 | "id" : 1,
64 | "status": "pending"
65 | },
66 | {
67 | "self": "/api/orders/2",
68 | "profile": "http://example.org/rels/order",
69 | "id" : 2,
70 | "status": "pending"
71 | }
72 | ]
73 | }
74 | ));
75 | });
76 |
77 | describe('new HttpAgent()', function(){
78 |
79 | var server = null;
80 |
81 | before(function() {
82 | server = app.listen(3000, '127.0.0.1');
83 | });
84 |
85 | after(function(done){
86 | server.close(function() {
87 | done();
88 | });
89 | });
90 |
91 | it('should give a link', function(done){
92 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
93 |
94 | agent.get(function(response) {
95 | assert.equal(true, response.isOk(), 'It was not possible to navigate!');
96 | var links = response.getLinks();
97 | assert.ok(links["http://example.org/rels/coffee"]);
98 | assert.equal(1, links["http://example.org/rels/coffee"].length);
99 | var coffee_link = response.getLink('http://example.org/rels/coffee');
100 | assert.equal('http://127.0.0.1:3000/api/coffee.json', coffee_link.getUrl());
101 | assert.equal('http://example.org/rels/coffee', coffee_link.getRel());
102 | assert.equal('Express', response.getHeader('x-powered-by'));
103 | assert.equal('Express', response.getAllHeaders()['x-powered-by']);
104 | done();
105 | });
106 | });
107 |
108 | it('should navigate with filter object', function(done){
109 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
110 |
111 | agent.navigate(['http://example.org/rels/coffee', {
112 | "name" : 'Small'
113 | }, 'http://example.org/rels/buy']);
114 |
115 | agent.post(function(response) {
116 | assert.equal(true, response.isOk(), 'It was not possible to navigate!');
117 | assert.equal('pending', response.getValue().status, 'Cannot find status after navigation');
118 | done();
119 | });
120 | });
121 |
122 | it('should navigate without anything', function(done){
123 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
124 |
125 | agent.navigate('http://example.org/rels/coffee');
126 |
127 | agent.get(function(response) {
128 | assert.equal(true, response.isOk(), 'It was not possible to navigate!');
129 | assert.equal('Small', response.getValue()[0].name, 'Cannot find name of first coffee');
130 | done();
131 | });
132 | });
133 |
134 | it('should navigate with filter function', function(done){
135 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
136 |
137 | agent.navigate(['http://example.org/rels/coffee', function(value) {
138 | return value.name === 'Small';
139 | }, 'http://example.org/rels/buy']);
140 |
141 | agent.post(function(response) {
142 | assert.equal(true, response.isOk(), 'It was not possible to navigate!');
143 | assert.equal('pending', response.getValue().status, 'Cannot find status after navigation');
144 | done();
145 | });
146 | });
147 |
148 | it('should return isOk = false, in case of broken link', function(done){
149 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
150 |
151 | agent.navigate(['http://example.org/rels/coffee', 'http://example.org/rels/invalid-link', 'http://example.org/rels/buy']);
152 |
153 | agent.get(function(response) {
154 | assert.equal(false, response.isOk(), 'It was possible to navigate!');
155 | done();
156 | });
157 | });
158 |
159 | it('should work with embedded links', function(done){
160 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
161 | agent.navigate('http://example.org/rels/orders');
162 | agent.get(function(response) {
163 | assert.equal(true, response.isOk(), 'It was not possible to navigate!');
164 | var links = response.getLinks();
165 |
166 | assert.ok(links["next"]);
167 | assert.ok(links["first"]);
168 | assert.ok(links["http://example.org/rels/orders-item"]);
169 | assert.equal(2, links["http://example.org/rels/orders-item"].length);
170 |
171 | //assert.equal(1, links["http://example.org/rels/coffee"].length);
172 | //var coffee_link = response.getLink('http://example.org/rels/coffee');
173 | //assert.equal('http://127.0.0.1:3000/api/coffee.json', coffee_link.getUrl());
174 | //assert.equal('http://example.org/rels/coffee', coffee_link.getRel());
175 | //assert.equal('Express', response.getHeader('x-powered-by'));
176 | //assert.equal('Express', response.getAllHeaders()['x-powered-by']);
177 | done();
178 | });
179 | });
180 | })
181 | });
--------------------------------------------------------------------------------
/test/server-test.js:
--------------------------------------------------------------------------------
1 | var assert = require("assert")
2 | var hateoasClient = require('./../hateoas-client.js');
3 |
4 | describe('ServerTest', function(){
5 | var express = require('express');
6 | var app = express();
7 |
8 | //hateoasClient.HttpAgent.enableLogging = true;
9 |
10 | app.get('/api.json', function(req, res) {
11 | res.set('Content-Type', 'application/json');
12 | res.send(JSON.stringify({
13 | "links": [ { "rel": "coffee", "href": "/api/coffee.json" }]
14 | }));
15 | });
16 | app.get('/api/coffee.json', function(req, res) {
17 | res.set('Content-Type', 'application/json');
18 | res.send(JSON.stringify([
19 | {
20 | "id": 5,
21 | "name": "Small",
22 | "links": [ { "rel": "buy", "href": "/api/submitted-orders?product_id=5" } ]
23 | }
24 | ]));
25 | });
26 | app.post('/api/submitted-orders', function(req, res) {
27 | res.redirect(201, '/api/orders/1');
28 | });
29 | app.get('/api/orders/1', function(req, res) {
30 | res.set('Content-Type', 'application/json');
31 | res.send(JSON.stringify(
32 | {
33 | "id": 1,
34 | "status": "pending"
35 | }
36 | ));
37 | });
38 |
39 | describe('new HttpAgent()', function(){
40 |
41 | var server = null;
42 |
43 | before(function() {
44 | server = app.listen(3000, '127.0.0.1');
45 | });
46 |
47 | after(function(done){
48 | server.close(function() {
49 | done();
50 | });
51 | });
52 |
53 | it('should give a link', function(done){
54 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
55 |
56 | agent.get(function(response) {
57 | assert.equal(true, response.isOk(), 'It was not possible to navigate!');
58 | var links = response.getLinks();
59 | assert.ok(links["coffee"]);
60 | assert.equal(1, links["coffee"].length);
61 | var coffee_link = response.getLink('coffee');
62 | assert.equal('http://127.0.0.1:3000/api/coffee.json', coffee_link.getUrl());
63 | assert.equal('coffee', coffee_link.getRel());
64 | assert.equal('Express', response.getHeader('x-powered-by'));
65 | assert.equal('Express', response.getAllHeaders()['x-powered-by']);
66 | done();
67 | });
68 | });
69 |
70 | it('should navigate with filter object', function(done){
71 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
72 |
73 | agent.navigate(['coffee', {
74 | "name" : 'Small'
75 | }, 'buy']);
76 |
77 | agent.post(function(response) {
78 | assert.equal(true, response.isOk(), 'It was not possible to navigate!');
79 | assert.equal('pending', response.getValue().status, 'Cannot find status after navigation');
80 | done();
81 | });
82 | });
83 |
84 | it('should navigate without anything', function(done){
85 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
86 |
87 | agent.navigate('coffee');
88 |
89 | agent.get(function(response) {
90 | assert.equal(true, response.isOk(), 'It was not possible to navigate!');
91 | assert.equal('Small', response.getValue()[0].name, 'Cannot find name of first coffee');
92 | done();
93 | });
94 | });
95 |
96 | it('should navigate with filter function', function(done){
97 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
98 |
99 | agent.navigate(['coffee', function(value) {
100 | return value.name === 'Small';
101 | }, 'buy']);
102 |
103 | agent.post(function(response) {
104 | assert.equal(true, response.isOk(), 'It was not possible to navigate!');
105 | assert.equal('pending', response.getValue().status, 'Cannot find status after navigation');
106 | done();
107 | });
108 | });
109 |
110 | it('should return isOk = false, in case of broken link', function(done){
111 | var agent = new hateoasClient.HttpAgent('http://127.0.0.1:3000/api.json');
112 |
113 | agent.navigate(['coffee', 'invalid_link', 'buy']);
114 |
115 | agent.get(function(response) {
116 | assert.equal(false, response.isOk(), 'It was possible to navigate!');
117 | done();
118 | });
119 | });
120 | })
121 | })
--------------------------------------------------------------------------------