├── .gitignore ├── doc ├── logo.png └── test.png ├── test ├── tests_bg.png ├── rpcSuite.js ├── server │ └── server.js ├── test.html ├── restSuite.js └── vendor │ └── nodeunit.js ├── package.json ├── README.md └── lib └── Porter.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /doc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heapwolf/Porter/HEAD/doc/logo.png -------------------------------------------------------------------------------- /doc/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heapwolf/Porter/HEAD/doc/test.png -------------------------------------------------------------------------------- /test/tests_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heapwolf/Porter/HEAD/test/tests_bg.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Porter", 3 | "description": "...", 4 | "version": "0.0.1", 5 | 6 | "private": true, 7 | 8 | "devDependencies": { 9 | "node-static": "0.5.x", 10 | "colors": "0.5.x", 11 | "eyes": "0.1.x" 12 | }, 13 | 14 | "scripts": { 15 | "test": "node test/server/server.js" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/rpcSuite.js: -------------------------------------------------------------------------------- 1 | 2 | this.rpcSuite = { 3 | 'test one': function (test) { 4 | test.ok(true, 'everythings ok'); 5 | setTimeout(function () { 6 | test.done(); 7 | }, 10); 8 | }, 9 | 'apples and oranges': function (test) { 10 | test.equal('apples', 'oranges', 'comparing apples and oranges'); 11 | test.done(); 12 | } 13 | }; -------------------------------------------------------------------------------- /test/server/server.js: -------------------------------------------------------------------------------- 1 | 2 | require.paths.unshift(require('path').join('..')); 3 | 4 | var sys = require('sys'), 5 | fs = require('fs'), 6 | qs = require('querystring'), 7 | eyes = require('eyes'), 8 | colors = require('colors'), 9 | http = require('http'), 10 | url = require('url'), 11 | nstatic = require('node-static'); 12 | 13 | var files = new nstatic.Server(__dirname + '/../../'), 14 | server = http.createServer(function (request, response) { 15 | 16 | if (request.url.match(/^\/(test1|test2|deep\/url)/)) { 17 | response.end('{ data: "ok" }'); 18 | } 19 | else if (request.url.match(/^\/\d+/)) { 20 | response.writeHead(+request.url.match(/\d+/)[0]); 21 | response.end(''); 22 | } 23 | else { 24 | 25 | request 26 | .addListener('end', function () { 27 | console.log('['.grey+'served'.yellow+'] '.grey + request.url.grey); 28 | files.serve(request, response); 29 | }); 30 | } 31 | }); 32 | 33 | try { 34 | 35 | server.listen(8080); 36 | console.log('Porter test server strted on http://127.0.0.1:8080 - Node.js ' + process.version.red); 37 | console.log('Use http://127.0.0.1:8080/test/test.html'); 38 | 39 | } 40 | catch(ex) { 41 | 42 | console.log('Could not start the test server. ' + ex.message); 43 | return; 44 | } 45 | 46 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Porter unit tests. 4 | 5 | 49 | 50 | 51 | 52 | 59 | 60 | 61 | logo 62 |

63 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /test/restSuite.js: -------------------------------------------------------------------------------- 1 | var restSuite = this.restSuite = {}; 2 | !function() { 3 | mock('GET to /test1'); 4 | mock('PUT to /test1 with payload'); 5 | mock('PUT to /test1 with payload and with validation'); 6 | mock('GET to /test2'); 7 | mock('POST to /test2/:id with url token replacement and with payload'); 8 | mock('POST to /test2/:id/:name with url token replacement and with payload'); 9 | mock('GET to /deep/url'); 10 | mock('GET to /404 with status handlers', function(test) { 11 | return { 12 | '404': emptyResponse(test) 13 | }; 14 | }); 15 | mock('GET to /404 with default status handler', function(test) { 16 | return { 17 | '200': function() {}, 18 | 'default': emptyResponse(test) 19 | }; 20 | }); 21 | mock('GET to /403 with global handler', function(test) { 22 | io.on({ 23 | '403': emptyResponse(test) 24 | }); 25 | return {}; 26 | }); 27 | mock('GET to /402 with global and local handler', function(test) { 28 | io.on({ 29 | '402': emptyResponse(test) 30 | }); 31 | return { 32 | '402': emptyResponse(test, false) 33 | }; 34 | }); 35 | mock('GET to /401 with global and local handler stopping propagation', 36 | function(test) { 37 | io.on({ 38 | '401': unexpectedResponse(test) 39 | }); 40 | return { 41 | '401': propagatedResponse(test) 42 | }; 43 | }); 44 | 45 | 46 | // Internal 47 | function mock(text, callback) { 48 | var match = /(GET|POST|PUT|DELETE) to ([^\s]+)/.exec(text); 49 | if (match === null) return; 50 | 51 | var method = match[1].toLowerCase(), 52 | url = match[2], 53 | validation = /with validation/i.test(text); 54 | 55 | function search(conf, match) { 56 | var result = false; 57 | 58 | if (Array.isArray(conf)) { 59 | if (conf[0] === method && conf[1] === url && 60 | conf.length === (validation ? 3 : 2)) { 61 | result = match; 62 | } 63 | } else { 64 | Object.keys(conf).some(function (key) { 65 | return result = search(conf[key], match.concat(key)); 66 | }); 67 | } 68 | 69 | return result; 70 | }; 71 | 72 | var selector = search(PorterConfig, []), 73 | args = []; 74 | if (!selector || selector.length <= 0) return; 75 | 76 | if (/with url token replacement/i.test(text)) { 77 | args.push({ 78 | id: 1, 79 | name: 'hij1nx' 80 | }); 81 | } 82 | 83 | if (/with payload/i.test(text)) { 84 | args.push({ 85 | value: 1 86 | }); 87 | } 88 | 89 | function call(callback) { 90 | var fn = selector.slice(0, -1).reduce(function(acc, curr) { 91 | return acc[curr]; 92 | }, io); 93 | 94 | return fn[selector.slice(-1)].apply(fn, args.concat(callback)); 95 | }; 96 | 97 | restSuite[text] = function(test) { 98 | call(callback ? callback(test) : function(err, response) { 99 | if (validation) { 100 | if (/should fail/i.test(text)) { 101 | test.ok(err); 102 | } else { 103 | test.ok(!err); 104 | } 105 | } 106 | test.ok(response, 'received data from test server'); 107 | test.done(); 108 | }); 109 | }; 110 | } 111 | 112 | function emptyResponse(test, finish) { 113 | return function(err, response) { 114 | test.equal(err, null); 115 | test.equal(response, ''); 116 | 117 | if (finish !== false) { 118 | test.done(); 119 | } 120 | }; 121 | }; 122 | 123 | function propagatedResponse(test) { 124 | return function(err, response) { 125 | test.equal(err, null); 126 | test.equal(response, ''); 127 | 128 | setTimeout(function() { 129 | test.done(); 130 | }, 10); 131 | 132 | return false; 133 | }; 134 | }; 135 | 136 | function unexpectedResponse(test) { 137 | return function(err, response) { 138 | test.ok(false, 'Unexpected that handler\'s call'); 139 | }; 140 | }; 141 | ; 142 | }(); 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Alt text](https://github.com/hij1nx/Porter/raw/master/doc/logo.png)
2 | 3 | ### porter is a lightweight, resourced oriented, abstraction layer for JSON-REST. It will generate methods needed to access resources based on a JSON configuration. It will balance your code's signal to noise ratio by simplifying the communication interfaces. 4 | 5 | ```javascript 6 | var porter = Porter({ 7 | 8 | users: { 9 | list: ['get', '/api/users/:partialname'], 10 | update: ['post', '/api/apps/:username'] 11 | }, 12 | 13 | apps: { 14 | list: ['get', '/api/apps/:username'], 15 | create: ['post', '/api/apps/:username/:appname'] 16 | } 17 | 18 | }); 19 | ``` 20 | 21 | Produces the following methods. 22 | 23 | ```javascript 24 | porter.users.list(/* ... */); 25 | porter.users.update(/* ... */); 26 | porter.apps.list(/* ... */); 27 | porter.apps.create(/* ... */); 28 | ``` 29 | 30 | The Porter constructor takes a single object literal containing members grouped by resource. Resources are then expressed as arrays. In the case of defining a REST call, there is a verb and a path, where each path can have tokens in it that will get supplanted when used. Here is the above definition put in use... 31 | 32 | ### Payload and Parameters 33 | 34 | ```javascript 35 | porter.users.list( 36 | 37 | { partialname: 'bill' }, // replaces the ':partialname' token in the 'list' resource's URI. 38 | { foo: 10, bar: 20 }, // appends '?foobar=10&bar=20' to the URL when the method is a GET, adds as a message body for a POST. 39 | function(error, response) { 40 | // do something... 41 | } 42 | 43 | ); 44 | ``` 45 | 46 | The `list` function was generated from its definition in the `users` group. We pass it 1) an object literal that supplants the token in the request url and 2) a callback function that will process when the request is done. 47 | 48 | ### Adding inbound and outbound data validation, and more complex resource organization. 49 | 50 | ```javascript 51 | function hasData(data) { // a simple data validator. 52 | if(typeof data !== 'undefined') { 53 | return true; 54 | } 55 | } 56 | 57 | var porter = Porter({ 58 | 59 | admin: { 60 | users: { 61 | list: ['get', '/api/users/:partialname', { outbound: hasData, inbound: hasData }], 62 | update: ['post', '/api/apps/:username'] 63 | }, 64 | 65 | apps: { 66 | list: ['get', '/api/apps/:username'], 67 | create: ['post', '/api/apps/:username/:appname'] 68 | } 69 | } 70 | }); 71 | ``` 72 | Any arbitrary function can be applied to assert the inbound and outbound data of a request, as seen above. If a validating function returns anything other than true, it is considered invalid and the callback for the resource will will have its 'error' parameter populated with either the exception or the return value of the validator. 73 | 74 | ### Specifying settings that apply to all calls that get made. 75 | 76 | ```javascript 77 | var porter = Porter({ 78 | 79 | users: { 80 | list: ['get', '/api/users/:partialname', { outbound: hasData, inbound: hasData }], 81 | update: ['post', '/api/apps/:username', { inbound: hasData }] 82 | }, 83 | 84 | apps: { 85 | list: ['get', '/api/apps/:username', { inbound: hasData }], 86 | create: ['post', '/api/apps/:username/:appname', { inbound: hasData }] 87 | } 88 | 89 | }).use({ 90 | port: 8080, 91 | inbound: hasData, 92 | outbound: hasData, 93 | headers: { 'Accept': 'application/json' } 94 | }); 95 | ``` 96 | 97 | The `use` function sets the defaults for all calls that get made. It accepts an object literal containing the following members... 98 | 99 | `port` Number - The port of the server that will accept the requests.
100 | `inbound` Object - A JSONSchema object that will validate against every incoming request.
101 | `outbound` Object - A JSONSchema object that will validate against every outgoing request.
102 | `host` String - An IP address of the host server that will accept the requests.
103 | `headers` Object - An object literal of HTTP request headers that will be attached to each request.
104 | `protocol` String - The protocol to be used for all requests, ie 'http', 'https'.
105 | `lib` Object - If you want to use a more full featured, cross-browser friendly ajax library ****add this back!****.
106 | 107 | And here is the above code in use... 108 | 109 | ```javascript 110 | porter.headers['Authorization'] = 'Basic ' + encodeBase64('username:password'); 111 | 112 | porter.users.update( 113 | 114 | { partialname: 'bill' }, 115 | { address: '555 Mockingbird Ln' }, 116 | 117 | function(error, response) { 118 | // do something... 119 | } 120 | ); 121 | ``` 122 | 123 | The `update` function was generated from its definition in the `users` group. We pass it a payload object, some data to replace the url tokens with and a callback function for when the request has finished processing. The app object will also expose the headers collection, this is simply an object literal that contains the headers to be used for the request. 124 | 125 | ### Specifying what to do with the response. 126 | 127 | ```javascript 128 | var porter = Porter({ 129 | 130 | users: { 131 | list: ['get', '/api/users/:partialname'] 132 | } 133 | 134 | }).use({ 135 | port: 8080, 136 | host: 'google.com' 137 | }).on({ 138 | '500': function(err, response) { 139 | // do something... 140 | }, 141 | '404': function(err, response) { 142 | // do something... 143 | } 144 | }); 145 | ``` 146 | 147 | In a lot of cases you'll want to handle http responses based on their response code. using the `on` method will allow you to associate methods with these response codes. In some cases you'll want to explicitly override these http response code handlers. you can do this by replacing the regular callback method with an object literal containing the items to overwrite. 148 | 149 | ```javascript 150 | porter.users.update( 151 | 152 | { partialname: 'bill' }, 153 | { address: '555 Mockingbird Ln' }, 154 | 155 | { 156 | '404': function(err, response) { 157 | // do something... 158 | }, 159 | '500': function(err, response) { 160 | // do something... 161 | } 162 | } 163 | ); 164 | ``` 165 | 166 | 167 | ### Testing and debugging. 168 | 169 | Porter provides a simple Node.js server to complement it's test suite. 170 | You may find this a useful starting point for your own test suite. 171 | Running `npm install && npm test` from root folder of this project will start 172 | development server that will be used for serving tests. 173 | ![Alt text](https://github.com/hij1nx/Porter/raw/master/doc/test.png)
174 | 175 | 176 | ## Credits 177 | 178 | Author: @hij1nx 179 | 180 | Contributors: @indexzero, @marak, @indutny 181 | 182 | ## Licence 183 | 184 | (The MIT License) 185 | 186 | Copyright (c) 2011 hij1nx 187 | 188 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 189 | 190 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 191 | 192 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 193 | -------------------------------------------------------------------------------- /lib/Porter.js: -------------------------------------------------------------------------------- 1 | 2 | ;(function(window, undefined) { 3 | 4 | window.Porter = function Porter(resources) { 5 | 6 | if(!(this instanceof Porter)) return new Porter(resources); 7 | 8 | var self = this; 9 | var cache = this.cache = {}; 10 | var dloc = document.location; 11 | var noop = function() {}; 12 | var dnode = (function() { 13 | var scripts = document.getElementsByTagName('script'); 14 | for (var i = scripts.length - 1; i >= 0; i--){ 15 | if(scripts[i].src === '/dnode') { 16 | return true; 17 | } 18 | } 19 | return false; 20 | }()); 21 | 22 | function isJSON(str) { 23 | if (str.length == 0) return false; 24 | str = str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'); 25 | str = str.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'); 26 | str = str.replace(/(?:^|:|,)(?:\s*\[)+/g, ''); 27 | return (/^[\],:{}\s]*$/).test(str); 28 | } 29 | 30 | this.options = { 31 | protocol: 'http', 32 | ip: dloc.hostname, 33 | host: dloc.hostname, 34 | port: dloc.port === '' ? 80 : dloc.port, 35 | headers: { 36 | 'Accept': 'application/json', 37 | 'Content-Type': 'application/json' 38 | }, 39 | delay: false 40 | }; 41 | this._locks = {}; 42 | 43 | this.headers = this.options.headers; 44 | this._listeners = {}; 45 | 46 | function addListener(self, action, listener) { 47 | var listeners = self._listeners[action]; 48 | listeners ? 49 | listeners.push(listener) 50 | : 51 | (self._listeners[action] = [listener]); 52 | }; 53 | 54 | function executeListener(self, action) { 55 | var args = Array.prototype.slice.call(arguments, 2), 56 | listeners = self._listeners[action]; 57 | 58 | if (listeners) { 59 | for (var i = 0, l = listeners.length; i < l; i++) { 60 | // Returning false will stop propagation 61 | if (listeners[i].apply(null, args) === false) break; 62 | } 63 | return true; 64 | } else { 65 | return false; 66 | } 67 | 68 | }; 69 | 70 | this.on = function(listeners) { 71 | for (var listener in listeners) { 72 | if (listeners.hasOwnProperty(listener)) { 73 | addListener(self, listener, listeners[listener]); 74 | } 75 | } 76 | return self; 77 | }; 78 | 79 | this.use = function(options) { 80 | for (var option in options) { 81 | if (options.hasOwnProperty(option)) { 82 | self.options[option] = options[option]; 83 | } 84 | } 85 | return self; 86 | }; 87 | 88 | this.ajax = function(conf) { 89 | 90 | var ajax = this; 91 | ajax._listeners = {}; 92 | 93 | ajax.xhr = (function() { 94 | if(typeof XMLHttpRequest != "undefined") { 95 | return new XMLHttpRequest(); 96 | } 97 | else { 98 | try { 99 | return new ActiveXObject("Msxml2.XMLHTTP"); 100 | } catch (e) { 101 | try { 102 | return new ActiveXObject("Microsoft.XMLHTTP"); 103 | } catch (err) { 104 | throw new Error('Browser too old.'); 105 | } 106 | } 107 | } 108 | })(); 109 | 110 | ajax.run = function(callbacks, validators) { 111 | 112 | // Setup locks 113 | if (self.options.delay !== false && conf.type !== 'get') { 114 | if (!conf.ignoreLocks && self._locks[conf.url]) { 115 | self._locks[conf.url].push({ 116 | conf: conf, 117 | callbacks: callbacks, 118 | validators: validators 119 | }); 120 | return; 121 | } else { 122 | self._locks[conf.url] = []; 123 | } 124 | } 125 | 126 | // is callbacks actually plural? if so, merge. 127 | if(!(typeof callbacks === 'function')) { 128 | for(c in callbacks) { 129 | if(callbacks.hasOwnProperty(c)) { 130 | addListener(ajax, c, callbacks[c]); 131 | } 132 | } 133 | } 134 | else { 135 | addListener(ajax, 'default', callbacks); 136 | } 137 | 138 | // merge the global _listeners into this xhr. 139 | for(l in self._listeners) { 140 | if(self._listeners.hasOwnProperty(l)) { 141 | for (var i = 0; i < self._listeners[l].length; i++) { 142 | addListener(ajax, l, self._listeners[l][i]); 143 | } 144 | } 145 | } 146 | 147 | ajax.xhr.open(conf.type, conf.url, true); 148 | 149 | var headers = self.options.headers; 150 | 151 | // pull from the default headers. 152 | for(var header in headers) { 153 | if (headers.hasOwnProperty(header)) { 154 | ajax.xhr.setRequestHeader(header, headers[header]); 155 | } 156 | } 157 | 158 | if(conf.data) { 159 | 160 | try { 161 | 162 | // if there is data, and there is also an outgoing data validator, 163 | // verify that the value returned is true, otherwise provide the 164 | // returned value as the 'err' to the listener/callback. 165 | 166 | var validation = true, validator; 167 | 168 | if((validators && validators.outbound) || self.outbound) { 169 | validator = validators || self; 170 | validation = validator.outbound(conf.data); 171 | if(validation !== true) { 172 | throw new Error(validation); 173 | } 174 | } 175 | 176 | ajax.xhr.send(JSON.stringify(conf.data)); 177 | 178 | } 179 | catch(ex) { executeListener(ajax, 'default', ex, null, null, null); } 180 | } 181 | else { 182 | ajax.xhr.send(); 183 | } 184 | 185 | ajax.xhr.onreadystatechange = function() { 186 | 187 | switch (ajax.xhr.readyState) { 188 | case 1: 189 | executeListener(ajax, 'loading'); 190 | break; 191 | case 2: 192 | executeListener(ajax, 'loaded'); 193 | break; 194 | case 3: 195 | executeListener(ajax, 'interactive'); 196 | break; 197 | case 4: 198 | 199 | // Release locks or run delayed requests 200 | if (!conf.ignoreLocks && conf.type !== 'get' && 201 | self.options.delay !== false) { 202 | 203 | setTimeout(function() { 204 | var data = self._locks[conf.url].shift(); 205 | 206 | // If we have any request queued 207 | if (data) { 208 | var _locks = self._locks; 209 | 210 | // Temporally clear locks 211 | self._locks = {}; 212 | 213 | // Do unlocked request 214 | var req = new self.ajax(data.conf); 215 | req.run(data.callbacks, data.validators); 216 | 217 | // Restore locks 218 | self._locks = _locks; 219 | } else { 220 | // Remove lock 221 | self._locks[conf.url] = null; 222 | } 223 | }, self.options.delay); 224 | } 225 | 226 | var response = ajax.xhr.responseText.trim(), 227 | status = ajax.xhr.status, 228 | statusText = ajax.xhr.statusText, 229 | validator, 230 | validation = true; 231 | 232 | try { 233 | response = isJSON(response) ? JSON.parse(response) : response; 234 | } 235 | catch(ex) { 236 | executeListener(ajax, 'default', ex, null, null, null); 237 | } 238 | 239 | // if there is validation, and it returns anything other 240 | // than true, it will be considered an error and that 241 | // value will be returned as the 'err' of the listener/callback. 242 | 243 | if((validators && validators['inbound']) || self.inbound) { 244 | validator = validators || self; 245 | validation = validator['inbound'](response); 246 | } 247 | 248 | var codeAction = executeListener(ajax, status, 249 | validation ? null : validation, 250 | response, statusText, self.xhr); 251 | if (!codeAction) { 252 | executeListener(ajax, 'default', validation ? null : validation, 253 | response, statusText, self.xhr); 254 | } 255 | 256 | cache[conf.url] = response; 257 | 258 | break; 259 | } 260 | }; 261 | }; 262 | return this; 263 | }; 264 | 265 | this.clear = function() { 266 | for(var d in self.cache) { 267 | if(self.cache.hasOwnProperty(d)) { delete self.cache[d]; } 268 | } 269 | } 270 | 271 | function normalizePath(path, keys) { 272 | // normalizePath by TJ Holowaychuk (https://github.com/visionmedia) 273 | path = path 274 | .concat('/?') 275 | .replace(/\/\(/g, '(?:/') 276 | .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){ 277 | keys.push(key); 278 | slash = slash || ''; 279 | return [ 280 | (optional ? '' : slash), 281 | '(?:', 282 | (optional ? slash : ''), 283 | (format || '') + (capture || '([^/]+?)') + ')', 284 | (optional || '')].join(''); 285 | }) 286 | .replace(/([\/.])/g, '\\$1') 287 | .replace(/\*/g, '(.+)'); 288 | return new RegExp('^' + path + '$', 'i'); 289 | } 290 | 291 | function req(url, method, replace, data, callbacks, validators, resource) { 292 | 293 | if(method === 'rpc') { 294 | 295 | if(!dnode) { 296 | var script = document.createElement('script'); 297 | script.src = '/dnode.js'; 298 | document.body.appendChild(script); 299 | } 300 | 301 | var args = ([].slice.call(arguments)).splice(2, arguments.length); 302 | 303 | DNode.connect(function (remote) { 304 | remote[resource](args, callbacks); 305 | }); 306 | 307 | } 308 | else { 309 | 310 | var params = '', pcount = 0; 311 | 312 | if(method === 'get') { 313 | for(var d in data) { 314 | if(data.hasOwnProperty(d)) { 315 | if(pcount===0) { params += '?'; } 316 | params += encodeURIComponent(d) + "=" + encodeURIComponent(data[d]) + "&"; 317 | pcount++; 318 | } 319 | } 320 | } 321 | 322 | return new self.ajax({ 323 | 324 | // merege in the options and build the url to request. 325 | url: [self.options.protocol, '://', self.options.ip || self.options.host, ':', self.options.port, url, params].join(''), 326 | type: method, 327 | data: data || {} 328 | 329 | }).run(callbacks, validators ? validators.inbound : null); 330 | } 331 | 332 | } 333 | 334 | function buildResources(resources, ns) { 335 | for (var resource in resources) { 336 | if (resources.hasOwnProperty(resource)) { 337 | 338 | if(Object.prototype.toString.call(resources[resource]) == "[object Array]") { 339 | 340 | ns[resource] = (function(request, resource) { 341 | 342 | return function() { 343 | 344 | var keys = [], 345 | method = request[0], 346 | url = request[1], 347 | validators = request[2], 348 | matcher = normalizePath(url, keys), 349 | 350 | args = Array.prototype.slice.call(arguments), 351 | alen = args.length, 352 | klen = keys.length, 353 | key = null; 354 | 355 | if(alen === 1 && args[0] === true) { 356 | return args[alen](self.cache[url]); 357 | } 358 | 359 | url = url.replace(/{([^{}]*)}/g, function (a, b) { 360 | var r = args[0][b]; 361 | return typeof r === 'string' || typeof r === 'number' ? escape(r) : a; 362 | }); 363 | 364 | if (klen > 0) { 365 | 366 | // If we have keys, then we need at least two arguments 367 | // and first argument in a two-argument pair can be assumed 368 | // to be the replacement map. 369 | 370 | if (alen === 1) { 371 | args.splice(0, -1, null); 372 | args.splice(0, -1, null); 373 | } 374 | 375 | if (alen === 2) { 376 | args.splice(1, -1, null); 377 | } 378 | 379 | if (typeof args[0] === 'string') { 380 | if (klen > 1) { 381 | throw new Error('Wrong number of keys in replacement. Expected ' + klen + ' got 1.'); 382 | } 383 | 384 | key = keys[0]; 385 | url = url.replace(':' + key, args[0]); 386 | 387 | } 388 | else { 389 | 390 | while (klen--) { 391 | key = keys[klen]; 392 | var val = args && args[0] ? args[0][key] : '', 393 | replace = (val === '') ? new RegExp('/?:' + key) : ':' + key; 394 | url = url.replace(replace, val); 395 | 396 | } 397 | } 398 | } 399 | else { 400 | // If we don't have keys, then we need at least one argument 401 | // and the first argument in a two-argument pair can be assumed 402 | // to be the data for the request. 403 | 404 | if (alen < 1 || alen > 2) { 405 | throw new Error('Cannot execute request ' + request + ' with ' + alen + ' arguments.'); 406 | } 407 | else if (alen === 1) { 408 | args.splice(0, -1, null); 409 | } 410 | 411 | args.splice(0, -1, null); 412 | } 413 | 414 | args = [url, method].concat(args, validators, resource); 415 | 416 | req.apply(null, args); 417 | } 418 | })(resources[resource], resource); 419 | 420 | } 421 | else { 422 | if(!ns[resource]) { 423 | ns[resource] = {}; 424 | } 425 | buildResources(resources[resource], ns[resource]); 426 | } 427 | } 428 | } 429 | } 430 | 431 | buildResources(resources, self); 432 | 433 | return self; 434 | } 435 | 436 | }(window)); 437 | -------------------------------------------------------------------------------- /test/vendor/nodeunit.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Nodeunit 3 | * https://github.com/caolan/nodeunit 4 | * Copyright (c) 2010 Caolan McMahon 5 | * MIT Licensed 6 | * 7 | * json2.js 8 | * http://www.JSON.org/json2.js 9 | * Public Domain. 10 | * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 11 | */ 12 | nodeunit = (function(){ 13 | /* 14 | http://www.JSON.org/json2.js 15 | 2010-11-17 16 | 17 | Public Domain. 18 | 19 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 20 | 21 | See http://www.JSON.org/js.html 22 | 23 | 24 | This code should be minified before deployment. 25 | See http://javascript.crockford.com/jsmin.html 26 | 27 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 28 | NOT CONTROL. 29 | 30 | 31 | This file creates a global JSON object containing two methods: stringify 32 | and parse. 33 | 34 | JSON.stringify(value, replacer, space) 35 | value any JavaScript value, usually an object or array. 36 | 37 | replacer an optional parameter that determines how object 38 | values are stringified for objects. It can be a 39 | function or an array of strings. 40 | 41 | space an optional parameter that specifies the indentation 42 | of nested structures. If it is omitted, the text will 43 | be packed without extra whitespace. If it is a number, 44 | it will specify the number of spaces to indent at each 45 | level. If it is a string (such as '\t' or ' '), 46 | it contains the characters used to indent at each level. 47 | 48 | This method produces a JSON text from a JavaScript value. 49 | 50 | When an object value is found, if the object contains a toJSON 51 | method, its toJSON method will be called and the result will be 52 | stringified. A toJSON method does not serialize: it returns the 53 | value represented by the name/value pair that should be serialized, 54 | or undefined if nothing should be serialized. The toJSON method 55 | will be passed the key associated with the value, and this will be 56 | bound to the value 57 | 58 | For example, this would serialize Dates as ISO strings. 59 | 60 | Date.prototype.toJSON = function (key) { 61 | function f(n) { 62 | // Format integers to have at least two digits. 63 | return n < 10 ? '0' + n : n; 64 | } 65 | 66 | return this.getUTCFullYear() + '-' + 67 | f(this.getUTCMonth() + 1) + '-' + 68 | f(this.getUTCDate()) + 'T' + 69 | f(this.getUTCHours()) + ':' + 70 | f(this.getUTCMinutes()) + ':' + 71 | f(this.getUTCSeconds()) + 'Z'; 72 | }; 73 | 74 | You can provide an optional replacer method. It will be passed the 75 | key and value of each member, with this bound to the containing 76 | object. The value that is returned from your method will be 77 | serialized. If your method returns undefined, then the member will 78 | be excluded from the serialization. 79 | 80 | If the replacer parameter is an array of strings, then it will be 81 | used to select the members to be serialized. It filters the results 82 | such that only members with keys listed in the replacer array are 83 | stringified. 84 | 85 | Values that do not have JSON representations, such as undefined or 86 | functions, will not be serialized. Such values in objects will be 87 | dropped; in arrays they will be replaced with null. You can use 88 | a replacer function to replace those with JSON values. 89 | JSON.stringify(undefined) returns undefined. 90 | 91 | The optional space parameter produces a stringification of the 92 | value that is filled with line breaks and indentation to make it 93 | easier to read. 94 | 95 | If the space parameter is a non-empty string, then that string will 96 | be used for indentation. If the space parameter is a number, then 97 | the indentation will be that many spaces. 98 | 99 | Example: 100 | 101 | text = JSON.stringify(['e', {pluribus: 'unum'}]); 102 | // text is '["e",{"pluribus":"unum"}]' 103 | 104 | 105 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); 106 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' 107 | 108 | text = JSON.stringify([new Date()], function (key, value) { 109 | return this[key] instanceof Date ? 110 | 'Date(' + this[key] + ')' : value; 111 | }); 112 | // text is '["Date(---current time---)"]' 113 | 114 | 115 | JSON.parse(text, reviver) 116 | This method parses a JSON text to produce an object or array. 117 | It can throw a SyntaxError exception. 118 | 119 | The optional reviver parameter is a function that can filter and 120 | transform the results. It receives each of the keys and values, 121 | and its return value is used instead of the original value. 122 | If it returns what it received, then the structure is not modified. 123 | If it returns undefined then the member is deleted. 124 | 125 | Example: 126 | 127 | // Parse the text. Values that look like ISO date strings will 128 | // be converted to Date objects. 129 | 130 | myData = JSON.parse(text, function (key, value) { 131 | var a; 132 | if (typeof value === 'string') { 133 | a = 134 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); 135 | if (a) { 136 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], 137 | +a[5], +a[6])); 138 | } 139 | } 140 | return value; 141 | }); 142 | 143 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { 144 | var d; 145 | if (typeof value === 'string' && 146 | value.slice(0, 5) === 'Date(' && 147 | value.slice(-1) === ')') { 148 | d = new Date(value.slice(5, -1)); 149 | if (d) { 150 | return d; 151 | } 152 | } 153 | return value; 154 | }); 155 | 156 | 157 | This is a reference implementation. You are free to copy, modify, or 158 | redistribute. 159 | */ 160 | 161 | /*jslint evil: true, strict: false, regexp: false */ 162 | 163 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, 164 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, 165 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, 166 | lastIndex, length, parse, prototype, push, replace, slice, stringify, 167 | test, toJSON, toString, valueOf 168 | */ 169 | 170 | 171 | // Create a JSON object only if one does not already exist. We create the 172 | // methods in a closure to avoid creating global variables. 173 | 174 | if (!this.JSON) { 175 | this.JSON = {}; 176 | } 177 | 178 | (function () { 179 | "use strict"; 180 | 181 | function f(n) { 182 | // Format integers to have at least two digits. 183 | return n < 10 ? '0' + n : n; 184 | } 185 | 186 | if (typeof Date.prototype.toJSON !== 'function') { 187 | 188 | Date.prototype.toJSON = function (key) { 189 | 190 | return isFinite(this.valueOf()) ? 191 | this.getUTCFullYear() + '-' + 192 | f(this.getUTCMonth() + 1) + '-' + 193 | f(this.getUTCDate()) + 'T' + 194 | f(this.getUTCHours()) + ':' + 195 | f(this.getUTCMinutes()) + ':' + 196 | f(this.getUTCSeconds()) + 'Z' : null; 197 | }; 198 | 199 | String.prototype.toJSON = 200 | Number.prototype.toJSON = 201 | Boolean.prototype.toJSON = function (key) { 202 | return this.valueOf(); 203 | }; 204 | } 205 | 206 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 207 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 208 | gap, 209 | indent, 210 | meta = { // table of character substitutions 211 | '\b': '\\b', 212 | '\t': '\\t', 213 | '\n': '\\n', 214 | '\f': '\\f', 215 | '\r': '\\r', 216 | '"' : '\\"', 217 | '\\': '\\\\' 218 | }, 219 | rep; 220 | 221 | 222 | function quote(string) { 223 | 224 | // If the string contains no control characters, no quote characters, and no 225 | // backslash characters, then we can safely slap some quotes around it. 226 | // Otherwise we must also replace the offending characters with safe escape 227 | // sequences. 228 | 229 | escapable.lastIndex = 0; 230 | return escapable.test(string) ? 231 | '"' + string.replace(escapable, function (a) { 232 | var c = meta[a]; 233 | return typeof c === 'string' ? c : 234 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 235 | }) + '"' : 236 | '"' + string + '"'; 237 | } 238 | 239 | 240 | function str(key, holder) { 241 | 242 | // Produce a string from holder[key]. 243 | 244 | var i, // The loop counter. 245 | k, // The member key. 246 | v, // The member value. 247 | length, 248 | mind = gap, 249 | partial, 250 | value = holder[key]; 251 | 252 | // If the value has a toJSON method, call it to obtain a replacement value. 253 | 254 | if (value && typeof value === 'object' && 255 | typeof value.toJSON === 'function') { 256 | value = value.toJSON(key); 257 | } 258 | 259 | // If we were called with a replacer function, then call the replacer to 260 | // obtain a replacement value. 261 | 262 | if (typeof rep === 'function') { 263 | value = rep.call(holder, key, value); 264 | } 265 | 266 | // What happens next depends on the value's type. 267 | 268 | switch (typeof value) { 269 | case 'string': 270 | return quote(value); 271 | 272 | case 'number': 273 | 274 | // JSON numbers must be finite. Encode non-finite numbers as null. 275 | 276 | return isFinite(value) ? String(value) : 'null'; 277 | 278 | case 'boolean': 279 | case 'null': 280 | 281 | // If the value is a boolean or null, convert it to a string. Note: 282 | // typeof null does not produce 'null'. The case is included here in 283 | // the remote chance that this gets fixed someday. 284 | 285 | return String(value); 286 | 287 | // If the type is 'object', we might be dealing with an object or an array or 288 | // null. 289 | 290 | case 'object': 291 | 292 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 293 | // so watch out for that case. 294 | 295 | if (!value) { 296 | return 'null'; 297 | } 298 | 299 | // Make an array to hold the partial results of stringifying this object value. 300 | 301 | gap += indent; 302 | partial = []; 303 | 304 | // Is the value an array? 305 | 306 | if (Object.prototype.toString.apply(value) === '[object Array]') { 307 | 308 | // The value is an array. Stringify every element. Use null as a placeholder 309 | // for non-JSON values. 310 | 311 | length = value.length; 312 | for (i = 0; i < length; i += 1) { 313 | partial[i] = str(i, value) || 'null'; 314 | } 315 | 316 | // Join all of the elements together, separated with commas, and wrap them in 317 | // brackets. 318 | 319 | v = partial.length === 0 ? '[]' : 320 | gap ? '[\n' + gap + 321 | partial.join(',\n' + gap) + '\n' + 322 | mind + ']' : 323 | '[' + partial.join(',') + ']'; 324 | gap = mind; 325 | return v; 326 | } 327 | 328 | // If the replacer is an array, use it to select the members to be stringified. 329 | 330 | if (rep && typeof rep === 'object') { 331 | length = rep.length; 332 | for (i = 0; i < length; i += 1) { 333 | k = rep[i]; 334 | if (typeof k === 'string') { 335 | v = str(k, value); 336 | if (v) { 337 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 338 | } 339 | } 340 | } 341 | } else { 342 | 343 | // Otherwise, iterate through all of the keys in the object. 344 | 345 | for (k in value) { 346 | if (Object.hasOwnProperty.call(value, k)) { 347 | v = str(k, value); 348 | if (v) { 349 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 350 | } 351 | } 352 | } 353 | } 354 | 355 | // Join all of the member texts together, separated with commas, 356 | // and wrap them in braces. 357 | 358 | v = partial.length === 0 ? '{}' : 359 | gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + 360 | mind + '}' : '{' + partial.join(',') + '}'; 361 | gap = mind; 362 | return v; 363 | } 364 | } 365 | 366 | // If the JSON object does not yet have a stringify method, give it one. 367 | 368 | if (typeof JSON.stringify !== 'function') { 369 | JSON.stringify = function (value, replacer, space) { 370 | 371 | // The stringify method takes a value and an optional replacer, and an optional 372 | // space parameter, and returns a JSON text. The replacer can be a function 373 | // that can replace values, or an array of strings that will select the keys. 374 | // A default replacer method can be provided. Use of the space parameter can 375 | // produce text that is more easily readable. 376 | 377 | var i; 378 | gap = ''; 379 | indent = ''; 380 | 381 | // If the space parameter is a number, make an indent string containing that 382 | // many spaces. 383 | 384 | if (typeof space === 'number') { 385 | for (i = 0; i < space; i += 1) { 386 | indent += ' '; 387 | } 388 | 389 | // If the space parameter is a string, it will be used as the indent string. 390 | 391 | } else if (typeof space === 'string') { 392 | indent = space; 393 | } 394 | 395 | // If there is a replacer, it must be a function or an array. 396 | // Otherwise, throw an error. 397 | 398 | rep = replacer; 399 | if (replacer && typeof replacer !== 'function' && 400 | (typeof replacer !== 'object' || 401 | typeof replacer.length !== 'number')) { 402 | throw new Error('JSON.stringify'); 403 | } 404 | 405 | // Make a fake root object containing our value under the key of ''. 406 | // Return the result of stringifying the value. 407 | 408 | return str('', {'': value}); 409 | }; 410 | } 411 | 412 | 413 | // If the JSON object does not yet have a parse method, give it one. 414 | 415 | if (typeof JSON.parse !== 'function') { 416 | JSON.parse = function (text, reviver) { 417 | 418 | // The parse method takes a text and an optional reviver function, and returns 419 | // a JavaScript value if the text is a valid JSON text. 420 | 421 | var j; 422 | 423 | function walk(holder, key) { 424 | 425 | // The walk method is used to recursively walk the resulting structure so 426 | // that modifications can be made. 427 | 428 | var k, v, value = holder[key]; 429 | if (value && typeof value === 'object') { 430 | for (k in value) { 431 | if (Object.hasOwnProperty.call(value, k)) { 432 | v = walk(value, k); 433 | if (v !== undefined) { 434 | value[k] = v; 435 | } else { 436 | delete value[k]; 437 | } 438 | } 439 | } 440 | } 441 | return reviver.call(holder, key, value); 442 | } 443 | 444 | 445 | // Parsing happens in four stages. In the first stage, we replace certain 446 | // Unicode characters with escape sequences. JavaScript handles many characters 447 | // incorrectly, either silently deleting them, or treating them as line endings. 448 | 449 | text = String(text); 450 | cx.lastIndex = 0; 451 | if (cx.test(text)) { 452 | text = text.replace(cx, function (a) { 453 | return '\\u' + 454 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 455 | }); 456 | } 457 | 458 | // In the second stage, we run the text against regular expressions that look 459 | // for non-JSON patterns. We are especially concerned with '()' and 'new' 460 | // because they can cause invocation, and '=' because it can cause mutation. 461 | // But just to be safe, we want to reject all unexpected forms. 462 | 463 | // We split the second stage into 4 regexp operations in order to work around 464 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 465 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we 466 | // replace all simple value tokens with ']' characters. Third, we delete all 467 | // open brackets that follow a colon or comma or that begin the text. Finally, 468 | // we look to see that the remaining characters are only whitespace or ']' or 469 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. 470 | 471 | if (/^[\],:{}\s]*$/ 472 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') 473 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') 474 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 475 | 476 | // In the third stage we use the eval function to compile the text into a 477 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity 478 | // in JavaScript: it can begin a block or an object literal. We wrap the text 479 | // in parens to eliminate the ambiguity. 480 | 481 | j = eval('(' + text + ')'); 482 | 483 | // In the optional fourth stage, we recursively walk the new structure, passing 484 | // each name/value pair to a reviver function for possible transformation. 485 | 486 | return typeof reviver === 'function' ? 487 | walk({'': j}, '') : j; 488 | } 489 | 490 | // If the text is not JSON parseable, then a SyntaxError is thrown. 491 | 492 | throw new SyntaxError('JSON.parse'); 493 | }; 494 | } 495 | }()); 496 | var assert = {}; 497 | var types = {}; 498 | var core = {}; 499 | var nodeunit = {}; 500 | var reporter = {}; 501 | (function(){ 502 | 503 | var async = {}; 504 | 505 | // global on the server, window in the browser 506 | var root = this; 507 | var previous_async = root.async; 508 | 509 | if(typeof module !== 'undefined' && module.exports) module.exports = async; 510 | else root.async = async; 511 | 512 | async.noConflict = function(){ 513 | root.async = previous_async; 514 | return async; 515 | }; 516 | 517 | //// cross-browser compatiblity functions //// 518 | 519 | var _forEach = function(arr, iterator){ 520 | if(arr.forEach) return arr.forEach(iterator); 521 | for(var i=0; i b ? 1 : 0; 764 | }), function(x){return x.value;})); 765 | }) 766 | }; 767 | 768 | async.auto = function(tasks, callback){ 769 | callback = callback || function(){}; 770 | var keys = _keys(tasks); 771 | if(!keys.length) return callback(null); 772 | 773 | var completed = []; 774 | 775 | var listeners = []; 776 | var addListener = function(fn){ 777 | listeners.unshift(fn); 778 | }; 779 | var removeListener = function(fn){ 780 | for(var i=0; i 965 | // 966 | // Permission is hereby granted, free of charge, to any person obtaining a copy 967 | // of this software and associated documentation files (the 'Software'), to 968 | // deal in the Software without restriction, including without limitation the 969 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 970 | // sell copies of the Software, and to permit persons to whom the Software is 971 | // furnished to do so, subject to the following conditions: 972 | // 973 | // The above copyright notice and this permission notice shall be included in 974 | // all copies or substantial portions of the Software. 975 | // 976 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 977 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 978 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 979 | // AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 980 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 981 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 982 | 983 | 984 | var pSlice = Array.prototype.slice; 985 | 986 | // 1. The assert module provides functions that throw 987 | // AssertionError's when particular conditions are not met. The 988 | // assert module must conform to the following interface. 989 | 990 | var assert = exports; 991 | 992 | // 2. The AssertionError is defined in assert. 993 | // new assert.AssertionError({message: message, actual: actual, expected: expected}) 994 | 995 | assert.AssertionError = function AssertionError (options) { 996 | this.name = "AssertionError"; 997 | this.message = options.message; 998 | this.actual = options.actual; 999 | this.expected = options.expected; 1000 | this.operator = options.operator; 1001 | var stackStartFunction = options.stackStartFunction || fail; 1002 | 1003 | if (Error.captureStackTrace) { 1004 | Error.captureStackTrace(this, stackStartFunction); 1005 | } 1006 | }; 1007 | // code from util.inherits in node 1008 | assert.AssertionError.super_ = Error; 1009 | 1010 | 1011 | // EDITED FOR BROWSER COMPATIBILITY: replaced Object.create call 1012 | // TODO: test what effect this may have 1013 | var ctor = function () { this.constructor = assert.AssertionError; }; 1014 | ctor.prototype = Error.prototype; 1015 | assert.AssertionError.prototype = new ctor(); 1016 | 1017 | 1018 | assert.AssertionError.prototype.toString = function() { 1019 | if (this.message) { 1020 | return [this.name+":", this.message].join(' '); 1021 | } else { 1022 | return [ this.name+":" 1023 | , JSON.stringify(this.expected ) 1024 | , this.operator 1025 | , JSON.stringify(this.actual) 1026 | ].join(" "); 1027 | } 1028 | }; 1029 | 1030 | // assert.AssertionError instanceof Error 1031 | 1032 | assert.AssertionError.__proto__ = Error.prototype; 1033 | 1034 | // At present only the three keys mentioned above are used and 1035 | // understood by the spec. Implementations or sub modules can pass 1036 | // other keys to the AssertionError's constructor - they will be 1037 | // ignored. 1038 | 1039 | // 3. All of the following functions must throw an AssertionError 1040 | // when a corresponding condition is not met, with a message that 1041 | // may be undefined if not provided. All assertion methods provide 1042 | // both the actual and expected values to the assertion error for 1043 | // display purposes. 1044 | 1045 | function fail(actual, expected, message, operator, stackStartFunction) { 1046 | throw new assert.AssertionError({ 1047 | message: message, 1048 | actual: actual, 1049 | expected: expected, 1050 | operator: operator, 1051 | stackStartFunction: stackStartFunction 1052 | }); 1053 | } 1054 | 1055 | // EXTENSION! allows for well behaved errors defined elsewhere. 1056 | assert.fail = fail; 1057 | 1058 | // 4. Pure assertion tests whether a value is truthy, as determined 1059 | // by !!guard. 1060 | // assert.ok(guard, message_opt); 1061 | // This statement is equivalent to assert.equal(true, guard, 1062 | // message_opt);. To test strictly for the value true, use 1063 | // assert.strictEqual(true, guard, message_opt);. 1064 | 1065 | assert.ok = function ok(value, message) { 1066 | if (!!!value) fail(value, true, message, "==", assert.ok); 1067 | }; 1068 | 1069 | // 5. The equality assertion tests shallow, coercive equality with 1070 | // ==. 1071 | // assert.equal(actual, expected, message_opt); 1072 | 1073 | assert.equal = function equal(actual, expected, message) { 1074 | if (actual != expected) fail(actual, expected, message, "==", assert.equal); 1075 | }; 1076 | 1077 | // 6. The non-equality assertion tests for whether two objects are not equal 1078 | // with != assert.notEqual(actual, expected, message_opt); 1079 | 1080 | assert.notEqual = function notEqual(actual, expected, message) { 1081 | if (actual == expected) { 1082 | fail(actual, expected, message, "!=", assert.notEqual); 1083 | } 1084 | }; 1085 | 1086 | // 7. The equivalence assertion tests a deep equality relation. 1087 | // assert.deepEqual(actual, expected, message_opt); 1088 | 1089 | assert.deepEqual = function deepEqual(actual, expected, message) { 1090 | if (!_deepEqual(actual, expected)) { 1091 | fail(actual, expected, message, "deepEqual", assert.deepEqual); 1092 | } 1093 | }; 1094 | 1095 | function _deepEqual(actual, expected) { 1096 | // 7.1. All identical values are equivalent, as determined by ===. 1097 | if (actual === expected) { 1098 | return true; 1099 | 1100 | } else if (Buffer.isBuffer(actual) && Buffer.isBuffer(expected)) { 1101 | if (actual.length != expected.length) return false; 1102 | 1103 | for (var i = 0; i < actual.length; i++) { 1104 | if (actual[i] !== expected[i]) return false; 1105 | } 1106 | 1107 | return true; 1108 | 1109 | // 7.2. If the expected value is a Date object, the actual value is 1110 | // equivalent if it is also a Date object that refers to the same time. 1111 | } else if (actual instanceof Date && expected instanceof Date) { 1112 | return actual.getTime() === expected.getTime(); 1113 | 1114 | // 7.3. Other pairs that do not both pass typeof value == "object", 1115 | // equivalence is determined by ==. 1116 | } else if (typeof actual != 'object' && typeof expected != 'object') { 1117 | return actual == expected; 1118 | 1119 | // 7.4. For all other Object pairs, including Array objects, equivalence is 1120 | // determined by having the same number of owned properties (as verified 1121 | // with Object.prototype.hasOwnProperty.call), the same set of keys 1122 | // (although not necessarily the same order), equivalent values for every 1123 | // corresponding key, and an identical "prototype" property. Note: this 1124 | // accounts for both named and indexed properties on Arrays. 1125 | } else { 1126 | return objEquiv(actual, expected); 1127 | } 1128 | } 1129 | 1130 | function isUndefinedOrNull (value) { 1131 | return value === null || value === undefined; 1132 | } 1133 | 1134 | function isArguments (object) { 1135 | return Object.prototype.toString.call(object) == '[object Arguments]'; 1136 | } 1137 | 1138 | function objEquiv (a, b) { 1139 | if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) 1140 | return false; 1141 | // an identical "prototype" property. 1142 | if (a.prototype !== b.prototype) return false; 1143 | //~~~I've managed to break Object.keys through screwy arguments passing. 1144 | // Converting to array solves the problem. 1145 | if (isArguments(a)) { 1146 | if (!isArguments(b)) { 1147 | return false; 1148 | } 1149 | a = pSlice.call(a); 1150 | b = pSlice.call(b); 1151 | return _deepEqual(a, b); 1152 | } 1153 | try{ 1154 | var ka = _keys(a), 1155 | kb = _keys(b), 1156 | key, i; 1157 | } catch (e) {//happens when one is a string literal and the other isn't 1158 | return false; 1159 | } 1160 | // having the same number of owned properties (keys incorporates hasOwnProperty) 1161 | if (ka.length != kb.length) 1162 | return false; 1163 | //the same set of keys (although not necessarily the same order), 1164 | ka.sort(); 1165 | kb.sort(); 1166 | //~~~cheap key test 1167 | for (i = ka.length - 1; i >= 0; i--) { 1168 | if (ka[i] != kb[i]) 1169 | return false; 1170 | } 1171 | //equivalent values for every corresponding key, and 1172 | //~~~possibly expensive deep test 1173 | for (i = ka.length - 1; i >= 0; i--) { 1174 | key = ka[i]; 1175 | if (!_deepEqual(a[key], b[key] )) 1176 | return false; 1177 | } 1178 | return true; 1179 | } 1180 | 1181 | // 8. The non-equivalence assertion tests for any deep inequality. 1182 | // assert.notDeepEqual(actual, expected, message_opt); 1183 | 1184 | assert.notDeepEqual = function notDeepEqual(actual, expected, message) { 1185 | if (_deepEqual(actual, expected)) { 1186 | fail(actual, expected, message, "notDeepEqual", assert.notDeepEqual); 1187 | } 1188 | }; 1189 | 1190 | // 9. The strict equality assertion tests strict equality, as determined by ===. 1191 | // assert.strictEqual(actual, expected, message_opt); 1192 | 1193 | assert.strictEqual = function strictEqual(actual, expected, message) { 1194 | if (actual !== expected) { 1195 | fail(actual, expected, message, "===", assert.strictEqual); 1196 | } 1197 | }; 1198 | 1199 | // 10. The strict non-equality assertion tests for strict inequality, as determined by !==. 1200 | // assert.notStrictEqual(actual, expected, message_opt); 1201 | 1202 | assert.notStrictEqual = function notStrictEqual(actual, expected, message) { 1203 | if (actual === expected) { 1204 | fail(actual, expected, message, "!==", assert.notStrictEqual); 1205 | } 1206 | }; 1207 | 1208 | function _throws (shouldThrow, block, err, message) { 1209 | var exception = null, 1210 | threw = false, 1211 | typematters = true; 1212 | 1213 | message = message || ""; 1214 | 1215 | //handle optional arguments 1216 | if (arguments.length == 3) { 1217 | if (typeof(err) == "string") { 1218 | message = err; 1219 | typematters = false; 1220 | } 1221 | } else if (arguments.length == 2) { 1222 | typematters = false; 1223 | } 1224 | 1225 | try { 1226 | block(); 1227 | } catch (e) { 1228 | threw = true; 1229 | exception = e; 1230 | } 1231 | 1232 | if (shouldThrow && !threw) { 1233 | fail( "Missing expected exception" 1234 | + (err && err.name ? " ("+err.name+")." : '.') 1235 | + (message ? " " + message : "") 1236 | ); 1237 | } 1238 | if (!shouldThrow && threw && typematters && exception instanceof err) { 1239 | fail( "Got unwanted exception" 1240 | + (err && err.name ? " ("+err.name+")." : '.') 1241 | + (message ? " " + message : "") 1242 | ); 1243 | } 1244 | if ((shouldThrow && threw && typematters && !(exception instanceof err)) || 1245 | (!shouldThrow && threw)) { 1246 | throw exception; 1247 | } 1248 | }; 1249 | 1250 | // 11. Expected to throw an error: 1251 | // assert.throws(block, Error_opt, message_opt); 1252 | 1253 | assert.throws = function(block, /*optional*/error, /*optional*/message) { 1254 | _throws.apply(this, [true].concat(pSlice.call(arguments))); 1255 | }; 1256 | 1257 | // EXTENSION! This is annoying to write outside this module. 1258 | assert.doesNotThrow = function(block, /*optional*/error, /*optional*/message) { 1259 | _throws.apply(this, [false].concat(pSlice.call(arguments))); 1260 | }; 1261 | 1262 | assert.ifError = function (err) { if (err) {throw err;}}; 1263 | })(assert); 1264 | (function(exports){ 1265 | /*! 1266 | * Nodeunit 1267 | * Copyright (c) 2010 Caolan McMahon 1268 | * MIT Licensed 1269 | * 1270 | * THIS FILE SHOULD BE BROWSER-COMPATIBLE JS! 1271 | * Only code on that line will be removed, its mostly to avoid requiring code 1272 | * that is node specific 1273 | */ 1274 | 1275 | /** 1276 | * Module dependencies 1277 | */ 1278 | 1279 | 1280 | 1281 | /** 1282 | * Creates assertion objects representing the result of an assert call. 1283 | * Accepts an object or AssertionError as its argument. 1284 | * 1285 | * @param {object} obj 1286 | * @api public 1287 | */ 1288 | 1289 | exports.assertion = function (obj) { 1290 | return { 1291 | method: obj.method || '', 1292 | message: obj.message || (obj.error && obj.error.message) || '', 1293 | error: obj.error, 1294 | passed: function () { 1295 | return !this.error; 1296 | }, 1297 | failed: function () { 1298 | return Boolean(this.error); 1299 | } 1300 | }; 1301 | }; 1302 | 1303 | /** 1304 | * Creates an assertion list object representing a group of assertions. 1305 | * Accepts an array of assertion objects. 1306 | * 1307 | * @param {Array} arr 1308 | * @param {Number} duration 1309 | * @api public 1310 | */ 1311 | 1312 | exports.assertionList = function (arr, duration) { 1313 | var that = arr || []; 1314 | that.failures = function () { 1315 | var failures = 0; 1316 | for (var i=0; i'; 1691 | }; 1692 | 1693 | 1694 | /** 1695 | * Run all tests within each module, reporting the results 1696 | * 1697 | * @param {Array} files 1698 | * @api public 1699 | */ 1700 | 1701 | exports.run = function (modules, options) { 1702 | var start = new Date().getTime(); 1703 | exports.addStyles(); 1704 | 1705 | var html = ''; 1706 | nodeunit.runModules(modules, { 1707 | moduleStart: function (name) { 1708 | html += '

' + name + '

'; 1709 | html += '
    '; 1710 | }, 1711 | testDone: function (name, assertions) { 1712 | if (!assertions.failures()) { 1713 | html += '
  1. ' + name + '
  2. '; 1714 | } 1715 | else { 1716 | html += '
  3. ' + name; 1717 | for (var i=0; i'; 1724 | } 1725 | html += '
    ';
    1726 |                         html += a.error.stack || a.error;
    1727 |                         html += '
    '; 1728 | } 1729 | }; 1730 | html += '
  4. '; 1731 | } 1732 | }, 1733 | moduleDone: function () { 1734 | html += '
'; 1735 | }, 1736 | done: function (assertions) { 1737 | var end = new Date().getTime(); 1738 | var duration = end - start; 1739 | if (assertions.failures()) { 1740 | html += '

FAILURES: ' + assertions.failures() + 1741 | '/' + assertions.length + ' assertions failed (' + 1742 | assertions.duration + 'ms)

'; 1743 | } 1744 | else { 1745 | html += '

OK: ' + assertions.length + 1746 | ' assertions (' + assertions.duration + 'ms)

'; 1747 | } 1748 | document.body.innerHTML += html; 1749 | } 1750 | }); 1751 | }; 1752 | })(reporter); 1753 | nodeunit = core; 1754 | nodeunit.assert = assert; 1755 | nodeunit.reporter = reporter; 1756 | nodeunit.run = reporter.run; 1757 | return nodeunit; })(); --------------------------------------------------------------------------------