├── .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: [![GitHub version](https://badge.fury.io/gh/DracoBlue%2Fhateoas-client-js.png)](https://github.com/DracoBlue/hateoas-client-js/releases) 5 | * Build-Status: [![Build Status](https://travis-ci.org/DracoBlue/hateoas-client-js.png?branch=master)](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 `<entry>` children as `AtomXmlHttpResponse[]`. 98 | - `getMatchingValue()`: Filters on `getValues()` 99 | - `getLinks()`: Returns all `<link>` 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 <JanS@DracoBlue.de>" 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 | <?php 2 | 3 | header('Location: api/orders/1.json', false, 201); -------------------------------------------------------------------------------- /example/api/orders/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "status": "pending" 4 | } 5 | -------------------------------------------------------------------------------- /example/atom/archive-1.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <feed xmlns="http://www.w3.org/2005/Atom"> 3 | <title>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 | }) --------------------------------------------------------------------------------