├── .gitignore ├── Makefile ├── Readme.md ├── ajaxapi.js ├── index.js ├── package.json └── test ├── setup.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | browserify := ./node_modules/.bin/browserify 2 | uglify := ./node_modules/.bin/uglifyjs 3 | 4 | ajaxapi.js: index.js 5 | $(browserify) -s ajaxapi $< | $(uglify) -m > $@ 6 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # ajaxapi 2 | 3 | minimal ajax library. 4 | 5 | - optimized for consuming restful api's 6 | - small and compact (< 6kb gzipped) 7 | - uses promises 8 | - supports node.js and the browser 9 | 10 | ```js 11 | var ajaxapi = require('ajaxapi'); 12 | 13 | var API = ajaxapi('https://api.github.com'); 14 | 15 | API.get('/repo/iojs/io.js') 16 | .then(function (data) { 17 | alert("Stars: " + data.stargazers_count); 18 | }) 19 | 20 | API.put('/user', { name: 'John Constantine' }) 21 | .then(function (data) { 22 | alert("User data was saved"); 23 | }); 24 | ``` 25 | 26 |
27 | 28 | ## Customization 29 | 30 | ### Before hooks 31 | 32 | Hooks before its sent 33 | 34 | ```js 35 | var API = ajaxapi('https://api.github.com'); 36 | 37 | API.before(function (ctx) { 38 | ctx.headers['X-Access-Token'] = '...'; 39 | 40 | ctx.headers //=> {} 41 | ctx.method //=> "GET" 42 | ctx.url //=> "https://api.github.com/foo/bar" 43 | ctx.data //=> {} 44 | }); 45 | ``` 46 | 47 | ### After hooks 48 | 49 | Promise stuff -- to be appended to the chain via `.then()` after the body is 50 | parsed. 51 | 52 | These hooks will be chaining each other. 53 | 54 | ```js 55 | var API = ajaxapi('https://api.github.com'); 56 | 57 | API.after(function (data) { 58 | // do stuff 59 | API.response.headers 60 | API.response.statusCode 61 | 62 | return data; 63 | }); 64 | ``` 65 | -------------------------------------------------------------------------------- /ajaxapi.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;"undefined"!=typeof window?t=window:"undefined"!=typeof global?t=global:"undefined"!=typeof self&&(t=self),t.ajaxapi=e()}}(function(){var e,t,r;return function n(e,t,r){function i(s,f){if(!t[s]){if(!e[s]){var a=typeof require=="function"&&require;if(!f&&a)return a(s,!0);if(o)return o(s,!0);var u=new Error("Cannot find module '"+s+"'");throw u.code="MODULE_NOT_FOUND",u}var c=t[s]={exports:{}};e[s][0].call(c.exports,function(t){var r=e[s][1][t];return i(r?r:t)},c,c.exports,n,e,t,r)}return t[s].exports}var o=typeof require=="function"&&require;for(var s=0;s1){e[r[0].toLowerCase()]=r.slice(1).join(":").trim()}});n(new i(a.status,e,a.responseText))}};a.open(e,t,true);for(var p in r.headers){a.setRequestHeader(p.toLowerCase(),r.headers[p])}a.send(r.body?r.body:null)});f.getBody=function(){return f.then(function(e){return e.getBody()})};return f.nodeify(s)}},{"./lib/handle-qs.js":4,"http-response-object":5,promise:6}],4:[function(e,t,r){"use strict";var n=e("qs").parse;var i=e("qs").stringify;t.exports=o;function o(e,t){e=e.split("?");var r=e[0];var o=(e[1]||"").split("#")[0];var s=e[1]&&e[1].split("#").length>1?"#"+e[1].split("#")[1]:"";var f=n(o);for(var a in t){f[a]=t[a]}o=i(f);if(o!==""){o="?"+o}return r+o+s}},{qs:12}],5:[function(e,t,r){"use strict";t.exports=n;function n(e,t,r){if(typeof e!=="number"){throw new TypeError("statusCode must be a number but was "+typeof e)}if(t===null){throw new TypeError("headers cannot be null")}if(typeof t!=="object"){throw new TypeError("headers must be an object but was "+typeof t)}this.statusCode=e;this.headers={};for(var n in t){this.headers[n.toLowerCase()]=t[n]}this.body=r}n.prototype.getBody=function(e){if(this.statusCode>=300){var t=new Error("Server responded with status code "+this.statusCode+":\n"+this.body.toString());t.statusCode=this.statusCode;t.headers=this.headers;t.body=this.body;throw t}return e?this.body.toString(e):this.body}},{}],6:[function(e,t,r){"use strict";t.exports=e("./lib/core.js");e("./lib/done.js");e("./lib/es6-extensions.js");e("./lib/node-extensions.js")},{"./lib/core.js":7,"./lib/done.js":8,"./lib/es6-extensions.js":9,"./lib/node-extensions.js":10}],7:[function(e,t,r){"use strict";var n=e("asap");t.exports=i;function i(e){if(typeof this!=="object")throw new TypeError("Promises must be constructed via new");if(typeof e!=="function")throw new TypeError("not a function");var t=null;var r=null;var i=[];var f=this;this.then=function(e,t){return new f.constructor(function(r,n){a(new o(e,t,r,n))})};function a(e){if(t===null){i.push(e);return}n(function(){var n=t?e.onFulfilled:e.onRejected;if(n===null){(t?e.resolve:e.reject)(r);return}var i;try{i=n(r)}catch(o){e.reject(o);return}e.resolve(i)})}function u(e){try{if(e===f)throw new TypeError("A promise cannot be resolved with itself.");if(e&&(typeof e==="object"||typeof e==="function")){var n=e.then;if(typeof n==="function"){s(n.bind(e),u,c);return}}t=true;r=e;p()}catch(i){c(i)}}function c(e){t=false;r=e;p()}function p(){for(var e=0,t=i.length;et){i.pop()}i.push(function(e,t){if(e)o(e);else n(t)});var s=e.apply(r,i);if(s&&(typeof s==="object"||typeof s==="function")&&typeof s.then==="function"){n(s)}})}};n.nodeify=function(e){return function(){var t=Array.prototype.slice.call(arguments);var r=typeof t[t.length-1]==="function"?t.pop():null;var o=this;try{return e.apply(this,arguments).nodeify(r,o)}catch(s){if(r===null||typeof r=="undefined"){return new n(function(e,t){t(s)})}else{i(function(){r.call(o,s)})}}}};n.prototype.nodeify=function(e,t){if(typeof e!="function")return this;this.then(function(r){i(function(){e.call(t,null,r)})},function(r){i(function(){e.call(t,r)})})}},{"./core.js":7,asap:11}],11:[function(e,t,r){(function(e){var r={task:void 0,next:null};var n=r;var i=false;var o=void 0;var s=false;function f(){while(r.next){r=r.next;var e=r.task;r.task=void 0;var t=r.domain;if(t){r.domain=void 0;t.enter()}try{e()}catch(n){if(s){if(t){t.exit()}setTimeout(f,0);if(t){t.enter()}throw n}else{setTimeout(function(){throw n},0)}}if(t){t.exit()}}i=false}if(typeof e!=="undefined"&&e.nextTick){s=true;o=function(){e.nextTick(f)}}else if(typeof setImmediate==="function"){if(typeof window!=="undefined"){o=setImmediate.bind(window,f)}else{o=function(){setImmediate(f)}}}else if(typeof MessageChannel!=="undefined"){var a=new MessageChannel;a.port1.onmessage=f;o=function(){a.port2.postMessage(0)}}else{o=function(){setTimeout(f,0)}}function u(t){n=n.next={task:t,domain:s&&e.domain,next:null};if(!i){i=true;o()}}t.exports=u}).call(this,e("_process"))},{_process:2}],12:[function(e,t,r){t.exports=e("./lib/")},{"./lib/":13}],13:[function(e,t,r){var n=e("./stringify");var i=e("./parse");var o={};t.exports={stringify:n,parse:i}},{"./parse":14,"./stringify":15}],14:[function(e,t,r){var n=e("./utils");var i={delimiter:"&",depth:5,arrayLimit:20,parameterLimit:1e3};i.parseValues=function(e,t){var r={};var i=e.split(t.delimiter,t.parameterLimit===Infinity?undefined:t.parameterLimit);for(var o=0,s=i.length;o=0&&f<=r.arrayLimit){o=[];o[f]=i.parseObject(e,t,r)}else{o[s]=i.parseObject(e,t,r)}}return o};i.parseKeys=function(e,t,r){if(!e){return}var n=/^([^\[\]]*)/;var o=/(\[[^\[\]]*\])/g;var s=n.exec(e);if(Object.prototype.hasOwnProperty(s[1])){return}var f=[];if(s[1]){f.push(s[1])}var a=0;while((s=o.exec(e))!==null&&a "/get/john" 31 | */ 32 | 33 | /* 34 | * request() : request(method, url, [data]) 35 | * Performs a request. 36 | */ 37 | 38 | Api.prototype.request = function (method, url, data) { 39 | var options = { headers: {}, qs: {}, json: (data || {}) }; 40 | 41 | var context = { 42 | method: method, 43 | url: this.prefix(url), 44 | data: data, 45 | headers: options.headers, 46 | options: options 47 | }; 48 | 49 | // apply before hooks (custom) 50 | this._before.forEach(function (fn) { fn.call(this, context); }); 51 | 52 | // promise 53 | var pro = Api.request( 54 | context.method, 55 | context.url, 56 | context.options); 57 | 58 | // apply after hooks (defaults) 59 | pro = pro 60 | .then(this.catchCorsError.bind(this)) 61 | .then(this.saveResponse.bind(this)) 62 | .then(this.parseBody.bind(this)); 63 | 64 | // apply after hooks (custom) 65 | this._after.forEach(function (callbacks) { 66 | pro = pro.then(proxy(callbacks[0]), proxy(callbacks[1])); 67 | }); 68 | 69 | function proxy (fn) { 70 | return fn ? fn.bind(this) : null; 71 | } 72 | 73 | return pro; 74 | }; 75 | 76 | /* 77 | * aliases 78 | */ 79 | 80 | Api.prototype.get = buildAlias('GET'); 81 | Api.prototype.put = buildAlias('PUT'); 82 | Api.prototype.del = buildAlias('DELETE'); 83 | Api.prototype.post = buildAlias('POST'); 84 | Api.prototype.patch = buildAlias('PATCH'); 85 | 86 | /* 87 | * currying helper to make alias methods 88 | */ 89 | 90 | function buildAlias (method) { 91 | return function () { 92 | return Api.prototype.request.apply(this, 93 | [method].concat([].slice.call(arguments))); 94 | }; 95 | } 96 | 97 | /* 98 | * add before hook 99 | */ 100 | 101 | Api.prototype.before = function (fn) { 102 | this._before.push(fn); 103 | return this; 104 | }; 105 | 106 | /* 107 | * add after hook 108 | */ 109 | 110 | Api.prototype.after = function (okFn, errFn) { 111 | this._after.push([ okFn, errFn ]); 112 | return this; 113 | }; 114 | 115 | /* 116 | * parses the body 117 | * 118 | * parseBody({ 119 | * body: '{"name":"Joe"}', 120 | * headers: { 'content-type': 'application/json' } 121 | * }) 122 | * => { name: "Joe" } 123 | */ 124 | 125 | Api.prototype.parseBody = function (res) { 126 | var 127 | body = res.getBody(), 128 | type = res.headers['content-type']; 129 | 130 | if (type && type.match(/^application\/json/)) 131 | return JSON.parse(body); 132 | else 133 | return body; 134 | }; 135 | 136 | /* 137 | * prefixes a url with the base 138 | */ 139 | 140 | Api.prototype.prefix = function (url) { 141 | if (url[0] === '/') 142 | return (this.base || '') + url; 143 | else 144 | return url; 145 | }; 146 | 147 | /* 148 | * (internal) saves responses 149 | */ 150 | 151 | Api.prototype.saveResponse = function (res) { 152 | this.response = this.res = res; 153 | return res; 154 | }; 155 | 156 | /* 157 | * (internal) catches cross origin request errors 158 | */ 159 | 160 | Api.prototype.catchCorsError = function (res) { 161 | if (res && res.statusCode === 0) 162 | throw new Error("API failed due to cross-origin error"); 163 | return res; 164 | }; 165 | 166 | /* 167 | * export 168 | */ 169 | 170 | module.exports = Api; 171 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ajaxapi", 3 | "version": "0.0.0-pre5", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "author": "Rico Sta. Cruz ", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/rstacruz/ajaxapi.git" 14 | }, 15 | "dependencies": { 16 | "then-request": "2.0.3", 17 | "uri-template-lite": "0.1.10" 18 | }, 19 | "devDependencies": { 20 | "browserify": "^8.1.1", 21 | "chai": "^1.10.0", 22 | "mocha": "^2.1.0", 23 | "nock": "^0.57.0", 24 | "uglify-js": "^2.4.16" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | global.expect = require('chai').expect; 2 | global.nock = require('nock'); 3 | global.Ajax = require('../index'); 4 | global.Api = null; 5 | 6 | beforeEach(function () { 7 | Api = Ajax('http://api.github.com'); 8 | }); 9 | 10 | afterEach(function () { 11 | nock.cleanAll(); 12 | }); 13 | 14 | global.mockApi = function() { 15 | return nock('http://api.github.com'); 16 | }; 17 | 18 | global.errExpected = function () { 19 | throw new Error("Error was expected out of a promise"); 20 | }; 21 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | require('./setup'); 2 | 3 | describe('200', function () { 4 | var data; 5 | 6 | beforeEach(function () { 7 | mockApi() 8 | .get('/repos/iojs/io.js') 9 | .reply(200, { name: 'io.js' }); 10 | }); 11 | 12 | beforeEach(function (next) { 13 | return Api.get('/repos/iojs/io.js') 14 | .then(function (d) { data = d; next(); }); 15 | }); 16 | 17 | it('works', function () { 18 | expect(data.name).eql('io.js'); 19 | }); 20 | }); 21 | 22 | describe('404', function () { 23 | beforeEach(function () { 24 | mockApi() 25 | .get('/repos/iojs/io.js') 26 | .reply(404); 27 | }); 28 | 29 | it('has statuscode', function () { 30 | return Api.get('/repos/iojs/io.js') 31 | .then(errExpected, function (err) { 32 | expect(err.statusCode).eql(404); 33 | }); 34 | }); 35 | }); 36 | --------------------------------------------------------------------------------