├── .gitignore ├── test ├── api │ ├── .htaccess │ ├── cors.php │ ├── test.json │ └── twitter.json ├── test.js ├── test.html ├── unit.html ├── setup.js └── unit.js ├── README.md ├── package.json ├── CHANGELOG.md ├── lib ├── main.js └── apiconnect.js ├── demo ├── sh_custom.css ├── sh_javascript.min.js ├── sh_main.min.js └── index.html ├── vendor ├── qunit │ ├── qunit.css │ └── qunit.js └── json2.js └── docs ├── generator.rb └── methods.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /test/api/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | # Parse json files as PHP 3 | AddType application/x-httpd-php .json 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # APIConnect 2 | 3 | A simplified Javascript interface for working with APIs. 4 | Detailed documentation [here](http://andrewplummer.github.com/APIConnect/docs.html). 5 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var APIConnect = require('../lib/main'); 4 | 5 | 6 | var api = new APIConnect('localhost:4000'); 7 | 8 | api.connect('PUT APIConnect/test/api/test.json'); 9 | 10 | 11 | api.contentType('json'); 12 | 13 | api.updateTest({ foo: 'bar' }).always(function() { 14 | console.info('aahhahaha', arguments); 15 | }); 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/api/cors.php: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "APIConnect", 3 | "version": "0.6.0", 4 | "description": "A simplified Javascript interface for working with APIs.", 5 | "keywords": ["api", "rest", "routing","cross-domain","ajax"], 6 | "homepage": "http://andrewplummer.github.com/APIConnect/", 7 | "author": "Andrew Plummer", 8 | "main": "./lib/main", 9 | "directories" : {"lib" : "./lib"}, 10 | "repository" : {"type" : "git", "url": "https://github.com/andrewplummer/APIConnect"}, 11 | "dependencies": { 12 | "request": ">= 2.9.153", 13 | "jquery-deferred": ">= 0.2.0" 14 | }, 15 | "engines" : {"node" : ">= 0.6.0"} 16 | } 17 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/unit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

QUnit example

8 |

9 |
10 |

11 |
    12 |
    test markup, will be hidden
    13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | v0.6 3 | ====== 4 | 5 | - Released for npm. 6 | - APIs on the same domain will not use JSONP. 7 | - Constructor now accepts a single string as the domain (may include port). 8 | - Params can be burned directly when connecting routes by adding ?key=value. 9 | - Routes can accept a single function as a shortcut for a success callback. 10 | - Data format can now differ from append format. 11 | - Fix for POST being misinterpreted in "posts" 12 | 13 | 14 | v0.5 15 | ====== 16 | 17 | - Adding the code, starting here. 18 | - Allowed a callback in place of params/options if defined as a shortcut for .then(). 19 | - Allowed the domain to be set in the constructor via a single string. 20 | - Allowed the burn in of params via normal syntax ?key=value, etc. 21 | -------------------------------------------------------------------------------- /test/api/test.json: -------------------------------------------------------------------------------- 1 | 48 | -------------------------------------------------------------------------------- /test/api/twitter.json: -------------------------------------------------------------------------------- 1 | 0)) 24 | { 25 | $api_url .= '?' . http_build_query($options); 26 | } 27 | 28 | curl_setopt($curl_handle, CURLOPT_URL, $api_url); 29 | 30 | if ($require_credentials) 31 | { 32 | curl_setopt($curl_handle, CURLOPT_USERPWD, $credentials); 33 | } 34 | if ($http_method == 'post') 35 | { 36 | curl_setopt($curl_handle, CURLOPT_POST, true); 37 | curl_setopt($curl_handle, CURLOPT_POSTFIELDS, http_build_query($options)); 38 | } 39 | 40 | curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, TRUE); 41 | curl_setopt($curl_handle, CURLOPT_HTTPHEADER, array('Expect:')); 42 | 43 | $twitter_data = curl_exec($curl_handle); 44 | 45 | header('HTTP/1.1 200 OK'); 46 | curl_close($curl_handle); 47 | 48 | return $twitter_data; 49 | } 50 | 51 | // Gets the 20 most recent statuses 52 | $f = apiCall('foobar', 'get', 'json', array(), false); 53 | 54 | echo $f; 55 | 56 | ?> 57 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | 2 | var url = require('url'); 3 | var request = require('request'); 4 | var deferred = require('jquery-deferred'); 5 | var APIConnect = require('./apiconnect'); 6 | 7 | 8 | 9 | APIConnect.util.Deferred = function() { 10 | return new deferred.Deferred; 11 | } 12 | APIConnect.util.supportsCORS = function() { 13 | return true; 14 | } 15 | APIConnect.util.isArray = function(arr) { 16 | return Array.isArray(arr); 17 | } 18 | APIConnect.util.ajax = function(context, method, url, params, options) { 19 | var deferred = this.Deferred(), 20 | isGET = method == 'GET', 21 | isJSON = options.contentType == 'json'; 22 | request({ 23 | url: url, 24 | method: method, 25 | qs: isGET ? params : null, 26 | form: !isGET && !isJSON ? params : null, 27 | json: !isGET && isJSON ? params : null 28 | }, function (error, response, body) { 29 | if(!error) { 30 | try { 31 | var data = options.dataFormat == 'json' ? JSON.parse(body) : body; 32 | if(options.complete) options.complete.call(context, data); 33 | if(options.success) options.success.call(context, data); 34 | return deferred.resolve(data); 35 | } catch(e) {}; 36 | } 37 | if(options.error) options.error.call(context, body); 38 | deferred.reject(body); 39 | }); 40 | return deferred; 41 | } 42 | APIConnect.util.getLocationValue = function() { 43 | return null; 44 | } 45 | APIConnect.util.getLocationValue = function() { 46 | return null; 47 | } 48 | APIConnect.util.when = function() { 49 | return deferred.when.apply(this, arguments); 50 | } 51 | APIConnect.util.getFullURL = function(base, params) { 52 | var obj = url.parse(base); 53 | obj.query = params; 54 | return url.format(obj); 55 | } 56 | 57 | module.exports = APIConnect; 58 | -------------------------------------------------------------------------------- /demo/sh_custom.css: -------------------------------------------------------------------------------- 1 | pre { 2 | color: #eeeeee; 3 | font-weight: normal; 4 | font-style: normal; 5 | } 6 | 7 | .sh_keyword { 8 | color: #b62; 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | 13 | .sh_predef_func { 14 | color: #a7c; 15 | font-weight: normal; 16 | font-style: normal; 17 | } 18 | 19 | .sh_type { 20 | color: #00ff00; 21 | font-weight: normal; 22 | font-style: normal; 23 | } 24 | 25 | .sh_string { 26 | color: #8b0; 27 | font-weight: normal; 28 | font-style: normal; 29 | } 30 | 31 | .sh_cbracket { 32 | color: #fa3; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | .sh_regexp { 38 | color: #e5e; 39 | font-weight: normal; 40 | font-style: normal; 41 | } 42 | 43 | .sh_specialchar { 44 | color: #ff22ff; 45 | font-weight: normal; 46 | font-style: normal; 47 | } 48 | 49 | .sh_comment { 50 | color: #888; 51 | font-weight: normal; 52 | font-style: normal; 53 | } 54 | 55 | .sh_number { 56 | color: #f93; 57 | font-weight: normal; 58 | font-style: normal; 59 | } 60 | 61 | .sh_preproc { 62 | color: #ff22ff; 63 | font-weight: normal; 64 | font-style: normal; 65 | } 66 | 67 | .sh_function { 68 | color: #eee; 69 | font-weight: normal; 70 | font-style: normal; 71 | } 72 | 73 | .sh_url { 74 | color: #ff0000; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | .sh_date { 80 | color: #B26818; 81 | font-weight: normal; 82 | font-style: normal; 83 | } 84 | 85 | .sh_time { 86 | color: #B26818; 87 | font-weight: normal; 88 | font-style: normal; 89 | } 90 | 91 | .sh_file { 92 | color: #B26818; 93 | font-weight: normal; 94 | font-style: normal; 95 | } 96 | 97 | .sh_ip { 98 | color: #ff0000; 99 | font-weight: normal; 100 | font-style: normal; 101 | } 102 | 103 | .sh_name { 104 | color: #ff0000; 105 | font-weight: normal; 106 | font-style: normal; 107 | } 108 | 109 | .sh_variable { 110 | color: #B26818; 111 | font-weight: normal; 112 | font-style: normal; 113 | } 114 | 115 | .sh_oldfile { 116 | color: #ff22ff; 117 | font-weight: normal; 118 | font-style: normal; 119 | } 120 | 121 | .sh_newfile { 122 | color: #ff0000; 123 | font-weight: normal; 124 | font-style: normal; 125 | } 126 | 127 | .sh_difflines { 128 | color: #B26818; 129 | font-weight: normal; 130 | font-style: normal; 131 | } 132 | 133 | .sh_selector { 134 | color: #B26818; 135 | font-weight: normal; 136 | font-style: normal; 137 | } 138 | 139 | .sh_property { 140 | color: #B26818; 141 | font-weight: normal; 142 | font-style: normal; 143 | } 144 | 145 | .sh_value { 146 | color: #ff0000; 147 | font-weight: normal; 148 | font-style: normal; 149 | } 150 | 151 | -------------------------------------------------------------------------------- /demo/sh_javascript.min.js: -------------------------------------------------------------------------------- 1 | if(!this.sh_languages){this.sh_languages={}}sh_languages.javascript=[[[/\/\/\//g,"sh_comment",1],[/\/\//g,"sh_comment",7],[/\/\*\*/g,"sh_comment",8],[/\/\*/g,"sh_comment",9],[/\b(?:abstract|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|final|finally|for|function|goto|if|implements|in|instanceof|interface|native|new|null|private|protected|prototype|public|return|static|super|switch|synchronized|throw|throws|this|transient|true|try|typeof|var|volatile|while|with)\b/g,"sh_keyword",-1],[/(\+\+|--|\)|\])(\s*)(\/=?(?![*\/]))/g,["sh_symbol","sh_normal","sh_symbol"],-1],[/(0x[A-Fa-f0-9]+|(?:[\d]*\.)?[\d]+(?:[eE][+-]?[\d]+)?)(\s*)(\/(?![*\/]))/g,["sh_number","sh_normal","sh_symbol"],-1],[/([A-Za-z$_][A-Za-z0-9$_]*\s*)(\/=?(?![*\/]))/g,["sh_normal","sh_symbol"],-1],[/\/(?:\\.|[^*\\\/])(?:\\.|[^\\\/])*\/[gim]*/g,"sh_regexp",-1],[/\b[+-]?(?:(?:0x[A-Fa-f0-9]+)|(?:(?:[\d]*\.)?[\d]+(?:[eE][+-]?[\d]+)?))u?(?:(?:int(?:8|16|32|64))|L)?\b/g,"sh_number",-1],[/"/g,"sh_string",10],[/'/g,"sh_string",11],[/~|!|%|\^|\*|\(|\)|-|\+|=|\[|\]|\\|:|;|,|\.|\/|\?|&|<|>|\|/g,"sh_symbol",-1],[/\{|\}/g,"sh_cbracket",-1],[/\b(?:Math|Infinity|NaN|undefined|arguments)\b/g,"sh_predef_var",-1],[/\b(?:Array|Boolean|Date|Error|EvalError|Function|Number|Object|RangeError|ReferenceError|RegExp|String|SyntaxError|TypeError|URIError|decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|eval|isFinite|isNaN|parseFloat|parseInt)\b/g,"sh_predef_func",-1],[/(?:[A-Za-z]|_)[A-Za-z0-9_]*(?=[ \t]*\()/g,"sh_function",-1]],[[/$/g,null,-2],[/(?:?)|(?:?)/g,"sh_url",-1],[/<\?xml/g,"sh_preproc",2,1],[//g,"sh_keyword",-1],[/<(?:\/)?[A-Za-z](?:[A-Za-z0-9_:.-]*)/g,"sh_keyword",6,1],[/&(?:[A-Za-z0-9]+);/g,"sh_preproc",-1],[/<(?:\/)?[A-Za-z][A-Za-z0-9]*(?:\/)?>/g,"sh_keyword",-1],[/<(?:\/)?[A-Za-z][A-Za-z0-9]*/g,"sh_keyword",6,1],[/@[A-Za-z]+/g,"sh_type",-1],[/(?:TODO|FIXME|BUG)(?:[:]?)/g,"sh_todo",-1]],[[/\?>/g,"sh_preproc",-2],[/([^=" \t>]+)([ \t]*)(=?)/g,["sh_type","sh_normal","sh_symbol"],-1],[/"/g,"sh_string",3]],[[/\\(?:\\|")/g,null,-1],[/"/g,"sh_string",-2]],[[/>/g,"sh_preproc",-2],[/([^=" \t>]+)([ \t]*)(=?)/g,["sh_type","sh_normal","sh_symbol"],-1],[/"/g,"sh_string",3]],[[/-->/g,"sh_comment",-2],[/ 379 | 380 | 381 | 438 | 439 | 440 | -------------------------------------------------------------------------------- /vendor/json2.js: -------------------------------------------------------------------------------- 1 | /* 2 | http://www.JSON.org/json2.js 3 | 2011-10-19 4 | 5 | Public Domain. 6 | 7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 8 | 9 | See http://www.JSON.org/js.html 10 | 11 | 12 | This code should be minified before deployment. 13 | See http://javascript.crockford.com/jsmin.html 14 | 15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 16 | NOT CONTROL. 17 | 18 | 19 | This file creates a global JSON object containing two methods: stringify 20 | and parse. 21 | 22 | JSON.stringify(value, replacer, space) 23 | value any JavaScript value, usually an object or array. 24 | 25 | replacer an optional parameter that determines how object 26 | values are stringified for objects. It can be a 27 | function or an array of strings. 28 | 29 | space an optional parameter that specifies the indentation 30 | of nested structures. If it is omitted, the text will 31 | be packed without extra whitespace. If it is a number, 32 | it will specify the number of spaces to indent at each 33 | level. If it is a string (such as '\t' or ' '), 34 | it contains the characters used to indent at each level. 35 | 36 | This method produces a JSON text from a JavaScript value. 37 | 38 | When an object value is found, if the object contains a toJSON 39 | method, its toJSON method will be called and the result will be 40 | stringified. A toJSON method does not serialize: it returns the 41 | value represented by the name/value pair that should be serialized, 42 | or undefined if nothing should be serialized. The toJSON method 43 | will be passed the key associated with the value, and this will be 44 | bound to the value 45 | 46 | For example, this would serialize Dates as ISO strings. 47 | 48 | Date.prototype.toJSON = function (key) { 49 | function f(n) { 50 | // Format integers to have at least two digits. 51 | return n < 10 ? '0' + n : n; 52 | } 53 | 54 | return this.getUTCFullYear() + '-' + 55 | f(this.getUTCMonth() + 1) + '-' + 56 | f(this.getUTCDate()) + 'T' + 57 | f(this.getUTCHours()) + ':' + 58 | f(this.getUTCMinutes()) + ':' + 59 | f(this.getUTCSeconds()) + 'Z'; 60 | }; 61 | 62 | You can provide an optional replacer method. It will be passed the 63 | key and value of each member, with this bound to the containing 64 | object. The value that is returned from your method will be 65 | serialized. If your method returns undefined, then the member will 66 | be excluded from the serialization. 67 | 68 | If the replacer parameter is an array of strings, then it will be 69 | used to select the members to be serialized. It filters the results 70 | such that only members with keys listed in the replacer array are 71 | stringified. 72 | 73 | Values that do not have JSON representations, such as undefined or 74 | functions, will not be serialized. Such values in objects will be 75 | dropped; in arrays they will be replaced with null. You can use 76 | a replacer function to replace those with JSON values. 77 | JSON.stringify(undefined) returns undefined. 78 | 79 | The optional space parameter produces a stringification of the 80 | value that is filled with line breaks and indentation to make it 81 | easier to read. 82 | 83 | If the space parameter is a non-empty string, then that string will 84 | be used for indentation. If the space parameter is a number, then 85 | the indentation will be that many spaces. 86 | 87 | Example: 88 | 89 | text = JSON.stringify(['e', {pluribus: 'unum'}]); 90 | // text is '["e",{"pluribus":"unum"}]' 91 | 92 | 93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); 94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' 95 | 96 | text = JSON.stringify([new Date()], function (key, value) { 97 | return this[key] instanceof Date ? 98 | 'Date(' + this[key] + ')' : value; 99 | }); 100 | // text is '["Date(---current time---)"]' 101 | 102 | 103 | JSON.parse(text, reviver) 104 | This method parses a JSON text to produce an object or array. 105 | It can throw a SyntaxError exception. 106 | 107 | The optional reviver parameter is a function that can filter and 108 | transform the results. It receives each of the keys and values, 109 | and its return value is used instead of the original value. 110 | If it returns what it received, then the structure is not modified. 111 | If it returns undefined then the member is deleted. 112 | 113 | Example: 114 | 115 | // Parse the text. Values that look like ISO date strings will 116 | // be converted to Date objects. 117 | 118 | myData = JSON.parse(text, function (key, value) { 119 | var a; 120 | if (typeof value === 'string') { 121 | a = 122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); 123 | if (a) { 124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], 125 | +a[5], +a[6])); 126 | } 127 | } 128 | return value; 129 | }); 130 | 131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { 132 | var d; 133 | if (typeof value === 'string' && 134 | value.slice(0, 5) === 'Date(' && 135 | value.slice(-1) === ')') { 136 | d = new Date(value.slice(5, -1)); 137 | if (d) { 138 | return d; 139 | } 140 | } 141 | return value; 142 | }); 143 | 144 | 145 | This is a reference implementation. You are free to copy, modify, or 146 | redistribute. 147 | */ 148 | 149 | /*jslint evil: true, regexp: true */ 150 | 151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, 152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, 153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, 154 | lastIndex, length, parse, prototype, push, replace, slice, stringify, 155 | test, toJSON, toString, valueOf 156 | */ 157 | 158 | 159 | // Create a JSON object only if one does not already exist. We create the 160 | // methods in a closure to avoid creating global variables. 161 | 162 | var JSON; 163 | if (!JSON) { 164 | JSON = {}; 165 | } 166 | 167 | (function () { 168 | 'use strict'; 169 | 170 | function f(n) { 171 | // Format integers to have at least two digits. 172 | return n < 10 ? '0' + n : n; 173 | } 174 | 175 | if (typeof Date.prototype.toJSON !== 'function') { 176 | 177 | Date.prototype.toJSON = function (key) { 178 | 179 | return isFinite(this.valueOf()) 180 | ? this.getUTCFullYear() + '-' + 181 | f(this.getUTCMonth() + 1) + '-' + 182 | f(this.getUTCDate()) + 'T' + 183 | f(this.getUTCHours()) + ':' + 184 | f(this.getUTCMinutes()) + ':' + 185 | f(this.getUTCSeconds()) + 'Z' 186 | : null; 187 | }; 188 | 189 | String.prototype.toJSON = 190 | Number.prototype.toJSON = 191 | Boolean.prototype.toJSON = function (key) { 192 | return this.valueOf(); 193 | }; 194 | } 195 | 196 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 197 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 198 | gap, 199 | indent, 200 | meta = { // table of character substitutions 201 | '\b': '\\b', 202 | '\t': '\\t', 203 | '\n': '\\n', 204 | '\f': '\\f', 205 | '\r': '\\r', 206 | '"' : '\\"', 207 | '\\': '\\\\' 208 | }, 209 | rep; 210 | 211 | 212 | function quote(string) { 213 | 214 | // If the string contains no control characters, no quote characters, and no 215 | // backslash characters, then we can safely slap some quotes around it. 216 | // Otherwise we must also replace the offending characters with safe escape 217 | // sequences. 218 | 219 | escapable.lastIndex = 0; 220 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) { 221 | var c = meta[a]; 222 | return typeof c === 'string' 223 | ? c 224 | : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 225 | }) + '"' : '"' + string + '"'; 226 | } 227 | 228 | 229 | function str(key, holder) { 230 | 231 | // Produce a string from holder[key]. 232 | 233 | var i, // The loop counter. 234 | k, // The member key. 235 | v, // The member value. 236 | length, 237 | mind = gap, 238 | partial, 239 | value = holder[key]; 240 | 241 | // If the value has a toJSON method, call it to obtain a replacement value. 242 | 243 | if (value && typeof value === 'object' && 244 | typeof value.toJSON === 'function') { 245 | value = value.toJSON(key); 246 | } 247 | 248 | // If we were called with a replacer function, then call the replacer to 249 | // obtain a replacement value. 250 | 251 | if (typeof rep === 'function') { 252 | value = rep.call(holder, key, value); 253 | } 254 | 255 | // What happens next depends on the value's type. 256 | 257 | switch (typeof value) { 258 | case 'string': 259 | return quote(value); 260 | 261 | case 'number': 262 | 263 | // JSON numbers must be finite. Encode non-finite numbers as null. 264 | 265 | return isFinite(value) ? String(value) : 'null'; 266 | 267 | case 'boolean': 268 | case 'null': 269 | 270 | // If the value is a boolean or null, convert it to a string. Note: 271 | // typeof null does not produce 'null'. The case is included here in 272 | // the remote chance that this gets fixed someday. 273 | 274 | return String(value); 275 | 276 | // If the type is 'object', we might be dealing with an object or an array or 277 | // null. 278 | 279 | case 'object': 280 | 281 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 282 | // so watch out for that case. 283 | 284 | if (!value) { 285 | return 'null'; 286 | } 287 | 288 | // Make an array to hold the partial results of stringifying this object value. 289 | 290 | gap += indent; 291 | partial = []; 292 | 293 | // Is the value an array? 294 | 295 | if (Object.prototype.toString.apply(value) === '[object Array]') { 296 | 297 | // The value is an array. Stringify every element. Use null as a placeholder 298 | // for non-JSON values. 299 | 300 | length = value.length; 301 | for (i = 0; i < length; i += 1) { 302 | partial[i] = str(i, value) || 'null'; 303 | } 304 | 305 | // Join all of the elements together, separated with commas, and wrap them in 306 | // brackets. 307 | 308 | v = partial.length === 0 309 | ? '[]' 310 | : gap 311 | ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' 312 | : '[' + partial.join(',') + ']'; 313 | gap = mind; 314 | return v; 315 | } 316 | 317 | // If the replacer is an array, use it to select the members to be stringified. 318 | 319 | if (rep && typeof rep === 'object') { 320 | length = rep.length; 321 | for (i = 0; i < length; i += 1) { 322 | if (typeof rep[i] === 'string') { 323 | k = rep[i]; 324 | v = str(k, value); 325 | if (v) { 326 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 327 | } 328 | } 329 | } 330 | } else { 331 | 332 | // Otherwise, iterate through all of the keys in the object. 333 | 334 | for (k in value) { 335 | if (Object.prototype.hasOwnProperty.call(value, k)) { 336 | v = str(k, value); 337 | if (v) { 338 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 339 | } 340 | } 341 | } 342 | } 343 | 344 | // Join all of the member texts together, separated with commas, 345 | // and wrap them in braces. 346 | 347 | v = partial.length === 0 348 | ? '{}' 349 | : gap 350 | ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' 351 | : '{' + partial.join(',') + '}'; 352 | gap = mind; 353 | return v; 354 | } 355 | } 356 | 357 | // If the JSON object does not yet have a stringify method, give it one. 358 | 359 | if (typeof JSON.stringify !== 'function') { 360 | JSON.stringify = function (value, replacer, space) { 361 | 362 | // The stringify method takes a value and an optional replacer, and an optional 363 | // space parameter, and returns a JSON text. The replacer can be a function 364 | // that can replace values, or an array of strings that will select the keys. 365 | // A default replacer method can be provided. Use of the space parameter can 366 | // produce text that is more easily readable. 367 | 368 | var i; 369 | gap = ''; 370 | indent = ''; 371 | 372 | // If the space parameter is a number, make an indent string containing that 373 | // many spaces. 374 | 375 | if (typeof space === 'number') { 376 | for (i = 0; i < space; i += 1) { 377 | indent += ' '; 378 | } 379 | 380 | // If the space parameter is a string, it will be used as the indent string. 381 | 382 | } else if (typeof space === 'string') { 383 | indent = space; 384 | } 385 | 386 | // If there is a replacer, it must be a function or an array. 387 | // Otherwise, throw an error. 388 | 389 | rep = replacer; 390 | if (replacer && typeof replacer !== 'function' && 391 | (typeof replacer !== 'object' || 392 | typeof replacer.length !== 'number')) { 393 | throw new Error('JSON.stringify'); 394 | } 395 | 396 | // Make a fake root object containing our value under the key of ''. 397 | // Return the result of stringifying the value. 398 | 399 | return str('', {'': value}); 400 | }; 401 | } 402 | 403 | 404 | // If the JSON object does not yet have a parse method, give it one. 405 | 406 | if (typeof JSON.parse !== 'function') { 407 | JSON.parse = function (text, reviver) { 408 | 409 | // The parse method takes a text and an optional reviver function, and returns 410 | // a JavaScript value if the text is a valid JSON text. 411 | 412 | var j; 413 | 414 | function walk(holder, key) { 415 | 416 | // The walk method is used to recursively walk the resulting structure so 417 | // that modifications can be made. 418 | 419 | var k, v, value = holder[key]; 420 | if (value && typeof value === 'object') { 421 | for (k in value) { 422 | if (Object.prototype.hasOwnProperty.call(value, k)) { 423 | v = walk(value, k); 424 | if (v !== undefined) { 425 | value[k] = v; 426 | } else { 427 | delete value[k]; 428 | } 429 | } 430 | } 431 | } 432 | return reviver.call(holder, key, value); 433 | } 434 | 435 | 436 | // Parsing happens in four stages. In the first stage, we replace certain 437 | // Unicode characters with escape sequences. JavaScript handles many characters 438 | // incorrectly, either silently deleting them, or treating them as line endings. 439 | 440 | text = String(text); 441 | cx.lastIndex = 0; 442 | if (cx.test(text)) { 443 | text = text.replace(cx, function (a) { 444 | return '\\u' + 445 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 446 | }); 447 | } 448 | 449 | // In the second stage, we run the text against regular expressions that look 450 | // for non-JSON patterns. We are especially concerned with '()' and 'new' 451 | // because they can cause invocation, and '=' because it can cause mutation. 452 | // But just to be safe, we want to reject all unexpected forms. 453 | 454 | // We split the second stage into 4 regexp operations in order to work around 455 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 456 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we 457 | // replace all simple value tokens with ']' characters. Third, we delete all 458 | // open brackets that follow a colon or comma or that begin the text. Finally, 459 | // we look to see that the remaining characters are only whitespace or ']' or 460 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. 461 | 462 | if (/^[\],:{}\s]*$/ 463 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') 464 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') 465 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 466 | 467 | // In the third stage we use the eval function to compile the text into a 468 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity 469 | // in JavaScript: it can begin a block or an object literal. We wrap the text 470 | // in parens to eliminate the ambiguity. 471 | 472 | j = eval('(' + text + ')'); 473 | 474 | // In the optional fourth stage, we recursively walk the new structure, passing 475 | // each name/value pair to a reviver function for possible transformation. 476 | 477 | return typeof reviver === 'function' 478 | ? walk({'': j}, '') 479 | : j; 480 | } 481 | 482 | // If the text is not JSON parseable, then a SyntaxError is thrown. 483 | 484 | throw new SyntaxError('JSON.parse'); 485 | }; 486 | } 487 | }()); 488 | -------------------------------------------------------------------------------- /docs/methods.html: -------------------------------------------------------------------------------- 1 |
    2 | 22 |
    23 | 24 |
    25 |

    connect(route, options)

    26 |

    27 | Connects the route. 28 | This is the main workhorse of APIConnect. route is the route to be called, it can have the 4 HTTP verbs GET, POST, PUT, or DELETE preceding it, and it can also contain the format (.json, etc). options is an object that contains the options for the route. Any options here will be merged into the options passed when calling the route. Additionally there are 2 special options: params will "burn in" any parameters so that they will always be passed when the route is called. as will override the resulting method name, which by default is a camel-case form of the human-readable method (get, create, update, or destroy) plus the last fragment in the route. For example, connect('GET friends/statuses') will result in the method name getStatuses. 29 |

    30 |

    31 | Returns: 32 | Instance 33 |

    34 |

    api.connect('friends')

    35 |

    api.connect('POST friends')

    36 |

    api.connect('PUT friends')

    37 |

    api.connect('DELETE friends')

    38 |

    api.connect('friends', { as 'fetchPeople' })

    39 |

    api.connect('search', { params: { complete: true }, as: 'findCompleted' })

    40 |
    41 | 42 |
    43 |

    contentType(setting)

    44 |

    45 | Gets or sets the contentType. Default is form. 46 | If the server requires a special contentType for parameter data, this can be specified here. Currently only form and json are supported. If the content type is "form", the standard key=value parameter serialization format will be used. If the contentType is json, the mime-type will be changed to application/json. In this case params need to be passed as a string, otherwise APIConnect will attempt to stringify them using JSON.stringify, which will fail in older browsers if not available. If you need this functionality a proper JSON shim should be used such as https://github.com/douglascrockford/JSON-js. 47 |

    48 |

    49 | Returns: 50 | Setting or Instance 51 |

    52 |

    api.contentType('json')

    53 |

    api.contentType()

    54 |
    55 | 56 |
    57 |

    context(name, callback)

    58 |

    59 | Allows a callback inside which the context name will be prepended. 60 | If you have multiple routes with the same context, use this so that it doesn't have to be added to every connect statement. 61 |

    62 |

    63 | Returns: 64 | Nothing 65 |

    66 |

    api.context('friends', function() { 67 | api.connect('posts'); 68 | api.connect('photos'); 69 | api.connect('events'); 70 | });

    71 |
    72 | 73 |
    74 |

    cors(setting)

    75 |

    76 | Gets or sets the CORS (cross-origin resource sharing) setting. Default is true. 77 | When active, APIConnect will attempt to make cross-browser ajax requests. JSONP will be used instead if the API is not on the same domain and the browser does not support CORS. Setting this to false will turn off CORS support for all browsers. Note that CORS needs to be enabled on the server-side as well. If the API you're trying to connect to does not support this feature it should be turned off. 78 |

    79 |

    80 | Returns: 81 | Setting or Instance 82 |

    83 |

    api.cors(false)

    84 |

    api.cors()

    85 |
    86 | 87 |
    88 |

    domain(setting)

    89 |

    90 | Gets or sets the domain. 91 | The domain can also be set when creating a new instance of APIConnect by passing a string as the first argument to the constructor. 92 |

    93 |

    94 | Returns: 95 | Setting or Instance 96 |

    97 |

    api.domain('graph.facebook.com')

    98 |

    api.domain()

    99 |
    100 | 101 |
    102 |

    format(dataFormat, appendFormat)

    103 |

    104 | Gets or sets the format. 105 | dataFormat is the format passed to the AJAX library. By default this is JSON, so the result will be parsed as JSON. appendFormat is the format appended to each route. By default this is false, so no format will be appended. If true it will use the dataFormat, and if anything else it will use it as the appended format instead. 106 |

    107 |

    108 | Returns: 109 | Setting or Instance 110 |

    111 |

    api.format()

    112 |

    api.format('xml', 'php')

    113 |
    114 | 115 |
    116 |

    getOverride(setting)

    117 |

    118 | Gets or sets the "getOverride" option. 119 | This option turns all POST, PUT, and DELETE requests into GET and instead sets a _method parameter representing the true method. setting can be always, which always adds _method, jsonp, which only overrides when using JSONP, always-except-get, or jsonp-except-get, which do the same except do not override GET methods, which typically do not require a _method parameter. 120 |

    121 |

    122 | Returns: 123 | Setting or Instance 124 |

    125 |

    api.getOverride()

    126 |

    api.getOverride('always')

    127 |
    128 | 129 |
    130 |

    jsonp(setting)

    131 |

    132 | Gets or sets the JSONP setting. Default is true. 133 | When active, APIConnect will attempt to make JSONP requests if CORS is disabled or not available, and if the API is not the same domain. Note that JSONP needs to be enabled on the server-side as well. If the API you're trying to connect to does not support this feature it should be turned off. 134 |

    135 |

    136 | Returns: 137 | Setting or Instance 138 |

    139 |

    api.jsonp(false)

    140 |

    api.jsonp()

    141 |
    142 | 143 |
    144 |

    option(name, value)

    145 |

    146 | Gets or sets a default option. 147 | Default options are merged into the local options of all routes when called, and apply to all connected routes. Instance-wide options are always merged in last, ie. local options will always override them, so they can also be thought of as defaults. 148 |

    149 |

    150 | Returns: 151 | Setting or Instance 152 |

    153 |

    api.option('port', 3000)

    154 |

    api.option('port')

    155 |
    156 | 157 |
    158 |

    options(obj)

    159 |

    160 | Gets or sets the default options. 161 | Calls option for each key/value pair in obj. When called without any arguments returns all default options. 162 |

    163 |

    164 | Returns: 165 | Setting or Instance 166 |

    167 |

    api.options({ port: 3000 })

    168 |

    api.options()

    169 |
    170 | 171 |
    172 |

    param(name, value)

    173 |

    174 | Gets or sets a default parameter. 175 | Default parameters are added to all routes. This is useful for things like API keys or access tokens that need to be used in every API call. 176 |

    177 |

    178 | Returns: 179 | Setting or Instance 180 |

    181 |

    api.param('api_key', 'foo')

    182 |

    api.param('api_key')

    183 |
    184 | 185 |
    186 |

    params(obj)

    187 |

    188 | Gets or sets the default parameters. 189 | Calls param for each key/value pair in obj. When called without any arguments returns all default params. 190 |

    191 |

    192 | Returns: 193 | Setting or Instance 194 |

    195 |

    api.param({ api_key: 'foo' })

    196 |

    api.param()

    197 |
    198 | 199 |
    200 |

    port(setting)

    201 |

    202 | Gets or sets the port. Default is null. 203 | 204 |

    205 |

    206 | Returns: 207 | Setting or Instance 208 |

    209 |

    api.port(3000)

    210 |

    api.port()

    211 |
    212 | 213 |
    214 |

    postOverride(setting)

    215 |

    216 | Gets or sets the "postOverride" option. 217 | Some APIs require a POST method to stand in for PUT and DELETE. This option turns all PUT and DELETE requests into POST with a _method parameter representing the true method. setting can be true or false. 218 |

    219 |

    220 | Returns: 221 | Setting or Instance 222 |

    223 |

    api.postOverride()

    224 |

    api.postOverride(true)

    225 |
    226 | 227 |
    228 |

    protocol(setting)

    229 |

    230 | Gets or sets the default protocol. Default is "auto". 231 | When set to "auto", https will be used if the current page is using https, or if a param like "token", "access_token", or "oauth_token" is found in the params. Otherwise will use http. 232 |

    233 |

    234 | Returns: 235 | Setting or Instance 236 |

    237 |

    api.protocol('https')

    238 |

    api.protocol()

    239 |
    240 | 241 |
    242 |

    resource(name, options)

    243 |

    244 | Shortcut for connecting multiple routes for the same resource. 245 | For singular resources it will connect 4 routes, GET, POST, UPDATE, and DELETE as getName, createName, updateName, and destroyName. For plural routes it will add /:id to the URL and singularize the method name for the above routes, and add an fifth "index" route without the /:id fragment using non-singular method name. Singular/plural routes will attempt to be intelligently detected by finding the singularized form of name. For non-countable or irregular nouns, instead pass { collection: true } in options to force plural routes. Additionally, only and except are allowed in options to limit the routes connected. Both can be either an array or comma-delimited string. Note that when limiting connected routes, index and show are used instead of get to differentiate between singular and plural get routes to be connected. This is only applicable to the resource method. 246 |

    247 |

    248 | Returns: 249 | Instance 250 |

    251 |

    api.resource('tweet')

    252 |

    api.resource('tweets')

    253 |

    api.resource('tweets', { only: 'show,create' })

    254 |

    api.resource('tweets', { except: 'show,create' })

    255 |

    api.resource('equipment', { collection: true })

    256 |
    257 | 258 |
    259 |

    timeout(setting)

    260 |

    261 | Gets or sets the timeout. Default is 30000. 262 | This option will be merged into the params passed to jQuery $.ajax, and tells it when to timeout an ajax request. 263 |

    264 |

    265 | Returns: 266 | Setting or Instance 267 |

    268 |

    api.timeout(10000)

    269 |

    api.timeout()

    270 |
    271 | -------------------------------------------------------------------------------- /lib/apiconnect.js: -------------------------------------------------------------------------------- 1 | /* 2 | * APIConnect v0.6 3 | * 4 | * Freely distributable and licensed under the MIT-style license. 5 | * Copyright (c) 2012 Andrew Plummer 6 | * http://andrewplummer.github.com/APIConnect/ 7 | * 8 | * ---------------------------- */ 9 | (function(context) { 10 | 11 | 12 | // Constants 13 | 14 | var HUMANIZED_HTTP_VERBS = { 15 | 'GET': 'get', 16 | 'POST': 'create', 17 | 'PUT': 'update', 18 | 'DELETE': 'destroy' 19 | }; 20 | 21 | var RESOURCE_ROUTES = [ 22 | { name: 'index', method: 'GET', has_id: false }, 23 | { name: 'show', method: 'GET', has_id: true }, 24 | { name: 'create', method: 'POST', has_id: false }, 25 | { name: 'update', method: 'PUT', has_id: true }, 26 | { name: 'destroy', method: 'DELETE', has_id: true } 27 | ]; 28 | 29 | var ACCESS_TOKEN_PARAMS = [ 30 | 'token', 31 | 'access_token', 32 | 'oauth_token' 33 | ]; 34 | 35 | var MAX_SIZE_FOR_JSONP_REQUESTS = 4091; 36 | var DEFAULT_OPTIONS = ['protocol', 'domain', 'port', 'timeout', 'cors', 'jsonp', 'contentType']; 37 | var JSON_STRINGIFY_ERROR = 'JSON.stringify failed! Shim may be required: https://github.com/douglascrockford/JSON-js'; 38 | 39 | var InstanceMethods = {}; 40 | 41 | var APIConnect = function(obj) { 42 | if(typeof obj === 'string') { 43 | obj = { domain: obj }; 44 | } 45 | this.defaultOptions = { 46 | protocol: 'auto', 47 | getOverride: 'jsonp-except-get', 48 | dataFormat: 'json', 49 | appendFormat: false, 50 | timeout: 30000, 51 | domain: '', 52 | port: '', 53 | jsonp: true, 54 | cors: true 55 | }; 56 | this.base = []; 57 | this.cache = {}; 58 | this.defaultParams = {}; 59 | mergeDefaults(this, obj || {}); 60 | }; 61 | 62 | /*** 63 | * @method protocol([setting]) 64 | * @returns Setting or Instance 65 | * @short Gets or sets the default protocol. Default is "auto". 66 | * @extra When set to "auto", https will be used if the current page is using https, or if a param like "token", "access_token", or "oauth_token" is found in the params. Otherwise will use http. 67 | * 68 | * @example 69 | * 70 | * api.protocol('https') -> api 71 | * api.protocol() -> 'https' 72 | * 73 | *** 74 | * @method domain([setting]) 75 | * @returns Setting or Instance 76 | * @short Gets or sets the domain. 77 | * @extra The domain can also be set when creating a new instance of APIConnect by passing a string as the first argument to the constructor. 78 | * 79 | * @example 80 | * 81 | * api.domain('graph.facebook.com') -> api 82 | * api.domain() -> 'graph.facebook.com' 83 | * 84 | *** 85 | * @method port([setting]) 86 | * @returns Setting or Instance 87 | * @short Gets or sets the port. Default is %null%. 88 | * 89 | * @example 90 | * 91 | * api.port(3000) -> api 92 | * api.port() -> 3000 93 | * 94 | *** 95 | * @method timeout([setting]) 96 | * @returns Setting or Instance 97 | * @short Gets or sets the timeout. Default is %30000%. 98 | * @extra This option will be merged into the params passed to jQuery $.ajax, and tells it when to timeout an ajax request. 99 | * 100 | * @example 101 | * 102 | * api.timeout(10000) -> api 103 | * api.timeout() -> 10000 104 | * 105 | *** 106 | * @method cors([setting]) 107 | * @returns Setting or Instance 108 | * @short Gets or sets the CORS (cross-origin resource sharing) setting. Default is %true%. 109 | * @extra When active, APIConnect will attempt to make cross-browser ajax requests. JSONP will be used instead if the API is not on the same domain and the browser does not support CORS. Setting this to %false% will turn off CORS support for all browsers. Note that CORS needs to be enabled on the server-side as well. If the API you're trying to connect to does not support this feature it should be turned off. 110 | * 111 | * @example 112 | * 113 | * api.cors(false) -> api 114 | * api.cors() -> false 115 | * 116 | *** 117 | * @method jsonp([setting]) 118 | * @returns Setting or Instance 119 | * @short Gets or sets the JSONP setting. Default is %true%. 120 | * @extra When active, APIConnect will attempt to make JSONP requests if CORS is disabled or not available, and if the API is not the same domain. Note that JSONP needs to be enabled on the server-side as well. If the API you're trying to connect to does not support this feature it should be turned off. 121 | * 122 | * @example 123 | * 124 | * api.jsonp(false) -> api 125 | * api.jsonp() -> false 126 | * 127 | *** 128 | * @method contentType([setting]) 129 | * @returns Setting or Instance 130 | * @short Gets or sets the contentType. Default is %form%. 131 | * @extra If the server requires a special contentType for parameter data, this can be specified here. Currently only %form% and %json% are supported. If the content type is "form", the standard %key=value% parameter serialization format will be used. If the contentType is %json%, the mime-type will be changed to %application/json%. In this case params need to be passed as a string, otherwise APIConnect will attempt to stringify them using %JSON.stringify%, which will fail in older browsers if not available. If you need this functionality a proper JSON shim should be used such as https://github.com/douglascrockford/JSON-js. 132 | * 133 | * @example 134 | * 135 | * api.contentType('json') -> api 136 | * api.contentType() -> 'json' 137 | * 138 | ***/ 139 | arrayEach(DEFAULT_OPTIONS, function(name) { 140 | InstanceMethods[name] = getterOrSetter('options', name); 141 | }); 142 | 143 | /*** 144 | * @method param(, [value]) 145 | * @returns Setting or Instance 146 | * @short Gets or sets a default parameter. 147 | * @extra Default parameters are added to all routes. This is useful for things like API keys or access tokens that need to be used in every API call. 148 | * @example 149 | * 150 | * api.param('api_key', 'foo') -> api 151 | * api.param('api_key') -> 'foo' 152 | * 153 | *** 154 | * @method params([obj]) 155 | * @returns Setting or Instance 156 | * @short Gets or sets the default parameters. 157 | * @extra Calls %param% for each key/value pair in [obj]. When called without any arguments returns all default params. 158 | * @example 159 | * 160 | * api.param({ api_key: 'foo' }) -> api 161 | * api.param() -> { api_key: 'foo' } 162 | * 163 | *** 164 | * @method option(, [value]) 165 | * @returns Setting or Instance 166 | * @short Gets or sets a default option. 167 | * @extra Default options are merged into the local options of all routes when called, and apply to all connected routes. Instance-wide options are always merged in last, ie. local options will always override them, so they can also be thought of as defaults. 168 | * @example 169 | * 170 | * api.option('port', 3000) -> api 171 | * api.option('port') -> 3000 172 | * 173 | *** 174 | * @method options([obj]) 175 | * @returns Setting or Instance 176 | * @short Gets or sets the default options. 177 | * @extra Calls %option% for each key/value pair in [obj]. When called without any arguments returns all default options. 178 | * @example 179 | * 180 | * api.options({ port: 3000 }) -> api 181 | * api.options() -> { port: 3000, ... } 182 | * 183 | ***/ 184 | arrayEach(['param', 'option'], function(name) { 185 | var plural = name + 's'; 186 | InstanceMethods[name] = function(prop, set) { 187 | return getOrSet(this, arguments, plural, prop, set); 188 | } 189 | InstanceMethods[plural] = function(obj) { 190 | var context = this; 191 | if(arguments.length === 0) { 192 | return getDefaultObject(context, plural); 193 | } else { 194 | objectEach(obj, function(prop, set) { 195 | context[name](prop, set); 196 | }); 197 | return context; 198 | } 199 | } 200 | }); 201 | 202 | /*** 203 | * @method getOverride([setting] = 'jsonp-except-get') 204 | * @returns Setting or Instance 205 | * @short Gets or sets the "getOverride" option. 206 | * @extra This option turns all POST, PUT, and DELETE requests into GET and instead sets a %_method% parameter representing the true method. can be %always%, which always adds %_method%, %jsonp%, which only overrides when using JSONP, %always-except-get%, or %jsonp-except-get%, which do the same except do not override GET methods, which typically do not require a %_method% parameter. 207 | * @example 208 | * 209 | * api.getOverride() -> 'jsonp-except-get' 210 | * api.getOverride('always') -> api 211 | * 212 | ***/ 213 | InstanceMethods['getOverride'] = getterOrSetter('options', 'getOverride'); 214 | 215 | /*** 216 | * @method postOverride( = false) 217 | * @returns Setting or Instance 218 | * @short Gets or sets the "postOverride" option. 219 | * @extra Some APIs require a POST method to stand in for PUT and DELETE. This option turns all PUT and DELETE requests into POST with a %_method% parameter representing the true method. can be %true% or %false%. 220 | * @example 221 | * 222 | * api.postOverride() -> false 223 | * api.postOverride(true) -> api 224 | * 225 | ***/ 226 | InstanceMethods['postOverride'] = getterOrSetter('options', 'postOverride'); 227 | 228 | /*** 229 | * @method format( = 'json', [appendFormat] = false) 230 | * @returns Setting or Instance 231 | * @short Gets or sets the format. 232 | * @extra is the format passed to the AJAX library. By default this is JSON, so the result will be parsed as JSON. [appendFormat] is the format appended to each route. By default this is %false%, so no format will be appended. If %true% it will use the , and if anything else it will use it as the appended format instead. 233 | * @example 234 | * 235 | * api.format() -> { dataFormat: 'json', appendFormat: false } 236 | * api.format('xml', 'php') -> api 237 | * 238 | ***/ 239 | InstanceMethods['format'] = function(dataFormat, appendFormat) { 240 | if(arguments.length > 0) { 241 | this.defaultOptions.dataFormat = dataFormat; 242 | this.defaultOptions.appendFormat = appendFormat; 243 | return this; 244 | } else { 245 | return { dataFormat: this.defaultOptions.format, appendFormat: this.defaultOptions.appendFormat }; 246 | } 247 | } 248 | 249 | /*** 250 | * @method context(, ) 251 | * @returns Nothing 252 | * @short Allows a callback inside which the context will be prepended. 253 | * @extra If you have multiple routes with the same context, use this so that it doesn't have to be added to every %connect% statement. 254 | * @example 255 | * 256 | + api.context('friends', function() { 257 | * api.connect('posts'); 258 | * api.connect('photos'); 259 | * api.connect('events'); 260 | * }); 261 | * 262 | ***/ 263 | InstanceMethods['context'] = function(name, fn) { 264 | var prev = this.base; 265 | this.base = getRouteObject(this, name, {}); 266 | fn.call(this); 267 | this.base = prev; 268 | } 269 | 270 | /*** 271 | * @method connect(, [options]) 272 | * @returns Instance 273 | * @short Connects the route. 274 | * @extra This is the main workhorse of APIConnect. is the route to be called, it can have the 4 HTTP verbs GET, POST, PUT, or DELETE preceding it, and it can also contain the format (.json, etc). [options] is an object that contains the options for the route. Any options here will be merged into the options passed when calling the route. Additionally there are 2 special options: %params% will "burn in" any parameters so that they will always be passed when the route is called. %as% will override the resulting method name, which by default is a camel-case form of the human-readable method (get, create, update, or destroy) plus the last fragment in the route. For example, %connect('GET friends/statuses')% will result in the method name %getStatuses%. 275 | * @example 276 | * 277 | + api.connect('friends') -> Connects method api.getFriends() 278 | + api.connect('POST friends') -> Connects method api.createFriends() 279 | + api.connect('PUT friends') -> Connects method api.updateFriends() 280 | + api.connect('DELETE friends') -> Connects method api.updateFriends() 281 | + api.connect('friends', { as 'fetchPeople' }) -> Connects method api.fetchPeople() 282 | + api.connect('search', { params: { complete: true }, as: 'findCompleted' }) -> Connects method api.findCompleted() with permanent params "complete= true" 283 | * 284 | ***/ 285 | InstanceMethods['connect'] = connectRoute; 286 | 287 | /*** 288 | * @method resource(, [options]) 289 | * @returns Instance 290 | * @short Shortcut for connecting multiple routes for the same resource. 291 | * @extra For singular resources it will connect 4 routes, GET, POST, UPDATE, and DELETE as get, create, update, and destroy. For plural routes it will add %/:id% to the URL and singularize the method name for the above routes, and add an fifth "index" route without the %/:id% fragment using non-singular method name. Singular/plural routes will attempt to be intelligently detected by finding the singularized form of . For non-countable or irregular nouns, instead pass %{ collection: true }% in [options] to force plural routes. Additionally, %only% and %except% are allowed in [options] to limit the routes connected. Both can be either an array or comma-delimited string. Note that when limiting connected routes, %index% and %show% are used instead of %get% to differentiate between singular and plural %get% routes to be connected. This is only applicable to the %resource% method. 292 | * @example 293 | * 294 | + api.resource('tweet') -> Connects get, create, update, and destroy routes 295 | + api.resource('tweets') -> Connects index, get, create, update, and destroy routes 296 | + api.resource('tweets', { only: 'show,create' }) -> Connects get and create routes 297 | + api.resource('tweets', { except: 'show,create' }) -> Connects index, update, and delete routes 298 | + api.resource('equipment', { collection: true }) -> Connects index, get, create, update, and destroy routes 299 | * 300 | ***/ 301 | InstanceMethods['resource'] = function(name, options) { 302 | options = options || {}; 303 | var context = this, 304 | match = name.match(/(.*?)([^/]+?)(\.\w+)?$/), 305 | prefix = match[1] || '', 306 | name = match[2], 307 | format = match[3] || '', 308 | singular = singularize(name), 309 | isPlural = singular != name, 310 | actions = RESOURCE_ROUTES.concat(), 311 | isCollection = options.collection || isPlural; 312 | 313 | arrayEach(['only','except'], function(opt) { 314 | actions = restrictActions(actions, options[opt], opt == 'only'); 315 | }); 316 | 317 | arrayEach(actions, function(action) { 318 | var methodName, 319 | route = prefix + name, 320 | isIndex = action.name == 'index'; 321 | if(isCollection && action.has_id) { 322 | route += '/:id'; 323 | } 324 | // Names like "getEquipment" will collide, so only set them up once. Passing a forced "collection" 325 | // property will allow an optional context later to handle both "index" and "show" actions in these edge cases. 326 | if(!isIndex || isPlural) { 327 | methodName = HUMANIZED_HTTP_VERBS[action.method] + camelize(isIndex ? name : singular); 328 | context.connect(action.method + ' ' + route + format, { as: methodName, collection: options.collection }); 329 | } 330 | }); 331 | 332 | return this; 333 | } 334 | 335 | // Not documenting authorize methods for now. 336 | 337 | InstanceMethods['authorize'] = function(url, params) { 338 | 339 | var authParams, openerFields = [], openerFields, context = this, popup = false; 340 | 341 | authParams = { 342 | response_type: 'token', 343 | redirect_uri: util.getLocationValue('href'), 344 | _: new Date().getTime() 345 | } 346 | 347 | if(params.popup) { 348 | popup = true; 349 | delete params.popup; 350 | } 351 | 352 | objectEach(URL_OPENER_DEFAULTS, function(name, value) { 353 | if(name in params) { 354 | value = params[name]; 355 | delete params[name]; 356 | } 357 | if(name == 'left' && value == 'center') { 358 | value = Math.round(window.innerWidth - (params.width || URL_OPENER_DEFAULTS.width)); 359 | } else if(name == 'top' && value == 'center') { 360 | value = Math.round(window.innerHeight - (params.height || URL_OPENER_DEFAULTS.height)); 361 | } 362 | openerFields.push(name + '=' + value); 363 | }); 364 | 365 | params = merge(authParams, params); 366 | 367 | url = util.getFullURL(url, params); 368 | 369 | return openExternalURL(context, url, popup, openerFields); 370 | 371 | }; 372 | 373 | 374 | // Instance helpers 375 | 376 | function mergeDefaults(context, obj) { 377 | arrayEach(obj.routes, function(r) { 378 | context.connect(r); 379 | }); 380 | arrayEach(obj.resources, function(r) { 381 | var match = r.toLowerCase().match(/\s*(\S+)\s*(?:only (.+))?\s*(?:except (.+))?/), 382 | resource = match[1], 383 | opt = { only: match[2], except: match[3] }; 384 | context.resource(resource, opt); 385 | }); 386 | delete obj.routes; 387 | delete obj.resources; 388 | objectEach(obj, function(key, value) { 389 | if(context[key]) { 390 | context[key](value); 391 | } else { 392 | context.defaultParams[key] = value; 393 | } 394 | }) 395 | } 396 | 397 | function getterOrSetter(type, prop) { 398 | return function(set) { 399 | return getOrSet(this, arguments, type, prop, set); 400 | } 401 | } 402 | 403 | function getOrSet(context, args, type, prop, set) { 404 | var hash = getDefaultObject(context, type); 405 | if(args.length === 0) { 406 | return hash[prop]; 407 | } else { 408 | hash[prop] = set; 409 | return context; 410 | } 411 | } 412 | 413 | function getDefaultObject(context, type) { 414 | return context['default' + type.slice(0,1).toUpperCase() + type.slice(1)]; 415 | } 416 | 417 | // Route connect helpers 418 | 419 | function connectRoute(str, routeOptions) { 420 | 421 | var context = this, match, route, method, routeObject, routeParams, as; 422 | 423 | routeOptions = routeOptions || {}; 424 | match = str.match(/\s*(?:(get|post|put|delete)\s)?\/?([^\s?]+)(?:\?(\S+))?\s*(?:as\s+(\S+))?/i); 425 | method = match[1] ? match[1].toUpperCase() : 'GET'; 426 | route = match[2]; 427 | routeObject = getRouteObject(context, route, routeOptions, method); 428 | routeParams = getParamsFromString(match[3]) || routeOptions.params; 429 | as = match[4] || routeOptions.as || getMethodName(method, routeObject); 430 | 431 | if(context[as]) { 432 | // Method exists so merge its route object to allow it a new context. 433 | mergeRoutes(context[as].routeObject, routeObject); 434 | } else { 435 | context[as] = function(params, options) { 436 | var url, key, callback, deferred; 437 | 438 | if(typeof params == 'string') { 439 | arrayEach(routeObject, function(fragment) { 440 | if(fragment.param) { 441 | var tmp = {}; 442 | tmp[fragment.text] = params; 443 | params = tmp; 444 | return false; 445 | } 446 | }); 447 | } 448 | 449 | arrayEach(arguments, function(arg) { 450 | if(typeof arg === 'function') { 451 | callback = arg; 452 | } 453 | }); 454 | 455 | params = merge(context.defaultParams, routeParams, params); 456 | options = merge(context.defaultOptions, routeOptions, options); 457 | 458 | url = resolveURL(context, routeObject, params, options); 459 | 460 | deferred = initiateRequest(context, url, method, params, options); 461 | if(callback) deferred.always(callback); 462 | 463 | return deferred; 464 | } 465 | 466 | // Store a reference to the route object so 467 | // that it can be merged later if needed. 468 | context[as].routeObject = routeObject; 469 | 470 | return context; 471 | } 472 | } 473 | 474 | function getRouteObject(context, route, routeOptions, method) { 475 | 476 | var previous, 477 | result = context.base.concat(); 478 | 479 | route = route.replace(/\.(\w+)$/, function(match, format) { 480 | routeOptions.appendFormat = format; 481 | return ''; 482 | }); 483 | 484 | arrayEach(route.split('/'), function(str, i, arr) { 485 | var fragment, 486 | match = str.match(/^:(.+)$/), 487 | param = match && match[1], 488 | singular = singularize(str), 489 | last = i == arr.length - 1; 490 | 491 | function setFragmentAndPrevious(property, set) { 492 | fragment[property] = set; 493 | // If the route has an explicit collection, then the previous fragment may occur without 494 | // an :id, so don't explicitly enforce the same expectations. 495 | // ex. GET /tweets/:tweet_id 496 | // ex. GET /tweets 497 | // Both may be allowed for a single method call, but only if "collection" is explicitly 498 | // true, allowing for things like uncountable resources that would otherwise have a method collision. 499 | if(!method || method != 'GET' || !routeOptions.collection) { 500 | previous[property] = set; 501 | } 502 | } 503 | 504 | fragment = { 505 | text: str.replace(/^:/, ''), 506 | singular: singular, 507 | param: !!param, 508 | required: true 509 | } 510 | 511 | if(param) { 512 | if(param == 'id' && previous && !previous.param) { 513 | // ex. /users/:id should expect either :id or :user_id 514 | // ex. tweets/:user/:id should not follow this pattern, however. 515 | setFragmentAndPrevious('expected', previous.singular + '_id'); 516 | setFragmentAndPrevious('id_expected', true); 517 | previous.name = previous.singular; 518 | } else if(previous && param.replace(/_id$/, '') == previous.singular) { 519 | // ex. /users/:user_id should expect only :user_id 520 | setFragmentAndPrevious('expected', param); 521 | previous.name = previous.singular; 522 | } else { 523 | fragment.expected = param; 524 | } 525 | } 526 | result.push(fragment); 527 | previous = fragment; 528 | }); 529 | return result; 530 | } 531 | 532 | function getMethodName(method, routeObject) { 533 | var name = HUMANIZED_HTTP_VERBS[method], reversed, obj; 534 | reversed = routeObject.concat().reverse(); 535 | arrayEach(reversed, function(el, i, arr) { 536 | if(!el.param) { 537 | obj = el; 538 | return false; 539 | } 540 | }); 541 | if(!obj) { 542 | obj = reversed[0]; 543 | } 544 | name += sanitize(obj.name || obj.text); 545 | return name; 546 | } 547 | 548 | function mergeRoutes(routes1, routes2) { 549 | var fragmentsToMerge = [], 550 | startIndex; 551 | 552 | arrayEach(routes2.slice(0,-1), function(fragment, i) { 553 | if(!routes1[i] || routes1[i].text != fragment.text) { 554 | fragmentsToMerge.push(fragment); 555 | if(startIndex === undefined) startIndex = i; 556 | } else if(startIndex !== undefined) { 557 | return false; 558 | } 559 | }); 560 | Array.prototype.splice.apply(routes1, [startIndex, 0].concat(fragmentsToMerge)); 561 | } 562 | 563 | 564 | // Resource helpers 565 | 566 | function restrictActions(actions, restrictedActions, only) { 567 | var result = [], match, matchedIndex; 568 | if(!restrictedActions) return actions; 569 | arrayEach(restrictedActions, function(action, i, arr) { 570 | match = null; 571 | arrayEach(actions, function(el, i) { 572 | if(el.name == action) { 573 | match = el; 574 | matchedIndex = i; 575 | return false; 576 | } 577 | }); 578 | if(match && only) { 579 | result.push(match); 580 | } else if(match && !only) { 581 | actions.splice(matchedIndex, 1); 582 | } 583 | }); 584 | return only ? result : actions; 585 | } 586 | 587 | 588 | // URL Helpers 589 | 590 | function resolveURL(context, routeObject, params, options) { 591 | var url = getURLBase(params, options); 592 | 593 | arrayEach(routeObject, function(fragment) { 594 | var paramsExpected, found; 595 | 596 | paramsExpected = fragment.expected || fragment.id_expected; 597 | if(fragment.expected && params[fragment.expected]) { 598 | found = fragment.expected; 599 | } else if(fragment.id_expected && params['id']) { 600 | found = 'id'; 601 | } 602 | 603 | // Add the fragment if it was either not expected a matching param, 604 | // or if it was exepecting one and it was found. 605 | if((paramsExpected && found) || (!paramsExpected && !fragment.param)) { 606 | url += '/' + (fragment.param ? params[found] : fragment.text); 607 | } 608 | 609 | // Delete the param if found so as not to pass it along to the query string. 610 | if(found && fragment.param) { 611 | delete params[found]; 612 | } 613 | 614 | }); 615 | 616 | if(options.appendFormat) { 617 | url += '.' + (typeof options.appendFormat == 'string' ? options.appendFormat : options.dataFormat); 618 | } 619 | return url; 620 | } 621 | 622 | function getProtocol(setting, params) { 623 | var protocol = setting; 624 | if(setting == 'auto') { 625 | protocol = util.getLocationValue('protocol') == 'https:' || tokenExists(params) ? 'https' : 'http'; 626 | } 627 | return protocol.replace(/:?$/, ''); 628 | } 629 | 630 | function tokenExists(params) { 631 | return arrayAny(ACCESS_TOKEN_PARAMS, function(p) { 632 | return params[p]; 633 | }); 634 | } 635 | 636 | function getURLBase(params, options) { 637 | var base = getProtocol(options.protocol, params); 638 | base += '://'; 639 | base += options.domain; 640 | if(options.port) { 641 | base += ':' + options.port; 642 | } 643 | return base; 644 | } 645 | 646 | // Request helpers 647 | 648 | function initiateRequest(context, url, method, params, options) { 649 | var deferred, cached, fullURL, split, intendedMethod = method; 650 | 651 | // If either this API or this browser does not support CORS and does support JSONP instead, use it. 652 | if(switchToJSONP(context, options)) { 653 | options.dataType = 'jsonp'; 654 | // Delete the "jsonp" property as it will override the ajax callback otherwise. 655 | delete options.jsonp; 656 | } else { 657 | options.dataType = options.dataFormat; 658 | } 659 | 660 | if(allowGetOverride(options.getOverride, method, options.dataType == 'jsonp')) { 661 | params._method = method; 662 | method = 'GET'; 663 | } else if(allowPostOverride(options.postOverride, method)) { 664 | params._method = method; 665 | method = 'POST'; 666 | } 667 | 668 | fullURL = util.getFullURL(url, params); 669 | 670 | if(cached = cacheRetrieve(context, fullURL, options)) { 671 | callOptionalCallback(context, options.complete, cached); 672 | callOptionalCallback(context, options.success, cached); 673 | return util.Deferred().resolve(cached); 674 | } 675 | 676 | if(util.splitRequest(fullURL, options)) { 677 | if(options.sizeError) { 678 | 679 | var callbacks = {}; 680 | 681 | arrayEach(['error','complete','success'], function(type) { 682 | callbacks[type] = options[type]; 683 | delete options[type]; 684 | }); 685 | 686 | // Splitting requests, deferred is now an array. 687 | deferred = []; 688 | split = options.sizeError.call(context, url, params); 689 | 690 | 691 | // If the callback has returned an array it is an attempt to split the calls. 692 | if(util.isArray(split)) { 693 | 694 | arrayEach(split, function(request) { 695 | deferred.push(initiateRequest(context, request.url, method, request.params, options)); 696 | }); 697 | 698 | return util.when.apply(deferred).fail(function(data) { 699 | callOptionalCallback(context, callbacks.error, data); 700 | }).done(function(data) { 701 | callOptionalCallback(context, callbacks.complete, data); 702 | callOptionalCallback(context, callbacks.success, data); 703 | }); 704 | 705 | } 706 | } 707 | callOptionalCallback(context, options.error, {}); 708 | return util.Deferred().reject('Error: Max URL length exceeded!'); 709 | } 710 | 711 | deferred = util.ajax(context, method, url, params, options); 712 | cacheCapture(context, intendedMethod, deferred, fullURL, options); 713 | 714 | return deferred; 715 | } 716 | 717 | function cacheRetrieve(context, fullURL, options) { 718 | return options.cache && context.cache[fullURL]; 719 | } 720 | 721 | function cacheCapture(context, method, deferred, fullURL, options) { 722 | if(options.cache && method === 'GET') { 723 | deferred.done(function(data) { 724 | context.cache[fullURL] = data; 725 | }); 726 | } 727 | } 728 | 729 | function callOptionalCallback(context, callback, data) { 730 | if(callback) { 731 | callback.call(context, data); 732 | } 733 | } 734 | 735 | function switchToJSONP(context, options) { 736 | return ((!options.cors || !util.supportsCORS()) && options.jsonp && options.dataFormat == 'json' && !isSameDomain(context)); 737 | } 738 | 739 | function isSameDomain(context) { 740 | return util.getLocationValue('hostname') == context.domain() && util.getLocationValue('port') == context.port(); 741 | } 742 | 743 | function allowGetOverride(setting, method, jsonp) { 744 | return (setting == 'always') || 745 | (setting == 'jsonp' && jsonp) || 746 | (setting == 'always-except-get' && method != 'GET') || 747 | (setting == 'jsonp-except-get' && jsonp && method != 'GET'); 748 | } 749 | 750 | function allowPostOverride(setting, method, jsonp) { 751 | return setting === true && (method == 'PUT' || method == 'DELETE'); 752 | } 753 | 754 | function getParamsFromString(str) { 755 | if(!str) return null; 756 | var result = {}, split; 757 | split = str.split('&'); 758 | arrayEach(split, function(p) { 759 | p = p.split('='); 760 | result[p[0]] = p[1]; 761 | }); 762 | return result; 763 | } 764 | 765 | 766 | // Authorization helpers 767 | 768 | var URL_OPENER_DEFAULTS = { 769 | directories: 'no', 770 | height: '600', 771 | width: '900', 772 | left: 'center', 773 | top: 'center', 774 | location: 'no', 775 | menubar: 'no', 776 | resizable: 'no', 777 | scrollbars: 'no', 778 | status: 'no', 779 | titlebar: 'no', 780 | toolbar: 'no' 781 | }; 782 | 783 | function openExternalURL(context, url, popup, openerFields) { 784 | var deferred = util.Deferred(); 785 | if(popup) { 786 | var child = window.open(url, null, openerFields.join(',')), interval; 787 | interval = setInterval(function() { 788 | if(child.closed) { 789 | deferred.reject(); 790 | clearInterval(interval); 791 | } else { 792 | getAccessToken(context, child, function(token, expires) { 793 | if(token) { 794 | clearInterval(interval); 795 | deferred.resolve(token, expires); 796 | child.close(); 797 | } 798 | }); 799 | } 800 | }, 500) 801 | } else { 802 | $('