UNPKG

9.11 kBJavaScriptView Raw
1"use strict";
2
3(function() {
4 var root = this;
5
6 if (typeof Promise === 'undefined') {
7 if (typeof require === 'function') {
8 var Promise = require('promise/lib/es6-extensions');
9 }
10 else {
11 throw new Error('Portals requires "Promise" support');
12 }
13 }
14
15 /**
16 * Merges multiple objects together into a single object and
17 * returns said object.
18 *
19 * @param {Object} out
20 * @return {Object}
21 */
22 var deepExtend = function (out) {
23 out = out || {};
24
25 for (var i = 1; i < arguments.length; i++) {
26 var obj = arguments[i];
27
28 if (!obj) continue;
29
30 for (var key in obj) {
31 if (obj.hasOwnProperty(key)) {
32 if (typeof obj[key] === 'object') {
33 out[key] = out[key] || {};
34 deepExtend(out[key], obj[key]);
35 }
36 else {
37 out[key] = obj[key];
38 }
39 }
40 }
41 }
42
43 return out;
44 }
45
46 /**
47 * Validates request options and ensures URL and method are provided.
48 *
49 * @param {Object} options
50 * @return {Object}
51 */
52 var validateRequestInterceptor = function (options) {
53 // ensure method is present and a string
54 if (typeof options.method !== 'string') {
55 throw new Error('Invalid method provided');
56 }
57
58 // ensure url is present and a string
59 if (typeof options.url !== 'string') {
60 throw new Error('Invalid url provided');
61 }
62
63 // ensure that "headers" is an object
64 if (typeof options.headers !== 'object') {
65 options.headers = {};
66 }
67
68 return options
69 }
70
71 /**
72 * Merges globals into each request.
73 *
74 * @param {Object} options
75 * @return {Object}
76 */
77 var mergeGlobalsRequestInterceptor = function (options) {
78 options = deepExtend({}, this.globals, options);
79
80 return options;
81 }
82
83 /**
84 * Builds up the URL using the hostname, params and query.
85 *
86 * @param {Object} options
87 * @return {Object}
88 */
89 var buildUrlRequestInterceptor = function (options) {
90 // if "http" is not present in url, add the hostname
91 if (options.url.indexOf('http') !== 0) {
92 options.url = (options.hostname || '') + options.url;
93 }
94
95 return options;
96 }
97
98 /**
99 * Encodes the request body as JSON.
100 *
101 * @param {Object} options
102 * @return {Object}
103 */
104 var encodeJsonRequestInterceptor = function (options) {
105 if ((options.data || options.body)
106 && typeof options.headers['Content-Type'] === 'string'
107 && options.headers['Content-Type'].indexOf('json') !== -1) {
108 options.body = JSON.stringify(options.data || options.body);
109 }
110
111 return options;
112 }
113
114 /**
115 * Parses the response if JSON.
116 *
117 * @param {Object} response
118 * @return {Object}
119 */
120 var parseJsonResponseInterceptor = function (response) {
121 if (response.headers['Content-Type']
122 && response.headers['Content-Type'].indexOf('json') !== -1) {
123 response.body = JSON.parse(response.body);
124 }
125
126 return response;
127 }
128
129 /**
130 * Constructor: Generate a new "Portal" instance.
131 */
132 var Portal = function () {
133 this.globals = {
134 hostname: '',
135 method: 'GET',
136 headers: {
137 'Accept': 'application/json',
138 'Content-Type': 'application/json'
139 }
140 };
141
142 this._requestInterceptors = [
143 validateRequestInterceptor
144 ];
145
146 this._responseInterceptors = [];
147 }
148
149 /**
150 * Send a request to the server using the given details in the
151 * options object. Inherits base object from the set globals.
152 *
153 * @param {Object} options
154 * @return {Promise}
155 */
156 Portal.prototype.send = function (opts) {
157 // ensure that an object is given
158 if (typeof opts !== 'object') {
159 throw new Error('Options must be an object!');
160 }
161
162 // loop through the request interceptors
163 for (var i = 0; i < this._requestInterceptors.length; i++) {
164 opts = this._requestInterceptors[i].call(this, opts)
165
166 // make sure that the options object is still an object
167 if (typeof opts !== 'object') {
168 throw new Error('Options object is no longer an object after interception!');
169 }
170 }
171
172 // prepare the request
173 var self = this;
174 var xhr = new XMLHttpRequest();
175
176 if (!('withCredentials' in xhr)
177 && typeof XDomainRequest !== 'undefined') {
178 xhr = new XDomainRequest();
179 }
180
181 xhr.open(opts.method.toUpperCase(), opts.url, true);
182 xhr.withCredentials = opts.withCredentials || false;
183
184 var promise = new Promise(function (resolve, reject) {
185 xhr.onload = function (e) {
186
187 var response = {
188 status: this.status,
189 headers: {
190 'Content-Type': this.getResponseHeader('Content-Type'),
191 'Cache-Control': this.getResponseHeader('Cache-Control'),
192 'Expires': this.getResponseHeader('Expires')
193 },
194 type: this.responseType,
195 body: this.responseText,
196 xhr: this
197 };
198
199 // loop through the response interceptors
200 for (var i = 0; i < self._responseInterceptors.length; i++) {
201 response = self._responseInterceptors[i].call(self, response)
202
203 // make sure that the options object is still an object
204 if (typeof response !== 'object') {
205 throw new Error('Response object is no longer an object after interception!');
206 }
207 }
208
209 if (this.status === 200) resolve(response);
210 else reject(response);
211 };
212
213 // send connection errors to catch()
214 xhr.onerror = function () {
215 reject({
216 status: 0,
217 headers: {
218 'Content-Type': 'text/plain'
219 },
220 type: 'error',
221 body: 'Connection Error',
222 xhr: this
223 });
224 };
225
226 });
227
228 // add the headers to the request
229 for (var key in opts.headers) {
230 xhr.setRequestHeader(key, opts.headers[key]);
231 }
232
233 // ensure that body is a string
234 if (opts.body && typeof opts.body !== 'string') {
235 opts.body = JSON.stringify(opts.body);
236 }
237
238 xhr.send(opts.body || null);
239
240 return promise;
241 }
242
243 /**
244 * Helper for making GET requests.
245 *
246 * @param {String} url
247 * @return {Promise}
248 */
249 Portal.prototype.get = function (url) {
250 return this.send({
251 method: 'GET',
252 url: url
253 });
254 }
255
256 /**
257 * Helper for making POST requests.
258 *
259 * @param {String} url
260 * @param {Object} data
261 * @return {Promise}
262 */
263 Portal.prototype.post = function (url, data) {
264 return this.send({
265 method: 'POST',
266 url: url,
267 data: data
268 });
269 }
270
271 /**
272 * Helper for making PUT requests.
273 *
274 * @param {String} url
275 * @param {Object} data
276 * @return {Promise}
277 */
278 Portal.prototype.put = function (url, data) {
279 return this.send({
280 method: 'PUT',
281 url: url,
282 data: data
283 });
284 }
285
286 /**
287 * Helper for making DELETE requests.
288 *
289 * @param {String} url
290 * @return {Promise}
291 */
292 Portal.prototype.del = function (url) {
293 return this.send({
294 method: 'DELETE',
295 url: url
296 });
297 }
298
299 /**
300 * Adds all the default interceptors to the instance.
301 *
302 * @return {this}
303 */
304 Portal.prototype.useDefaultInterceptors = function () {
305 return this
306 .onRequest(mergeGlobalsRequestInterceptor)
307 .onRequest(buildUrlRequestInterceptor)
308 .onRequest(encodeJsonRequestInterceptor)
309 .onResponse(parseJsonResponseInterceptor);
310 }
311
312 /**
313 * Add new request interceptor.
314 *
315 * @param {Function} fn
316 * @return {this}
317 */
318 Portal.prototype.onRequest = function (fn) {
319 // ensure that interceptor is function
320 if (typeof fn !== 'function') {
321 throw new Error('Interceptor must be a function!');
322 }
323
324 // add the interceptor to the list
325 this._requestInterceptors.push(fn);
326
327 return this;
328 }
329
330 /**
331 * Add new response interceptor.
332 *
333 * @param {Function} fn
334 * @return {this}
335 */
336 Portal.prototype.onResponse = function (fn) {
337 // ensure that interceptor is function
338 if (typeof fn !== 'function') {
339 throw new Error('Interceptor must be a function!');
340 }
341
342 // add the interceptor to the list
343 this._responseInterceptors.push(fn);
344
345 return this;
346 }
347
348 /**
349 * Factory method that creates a new Portal instance with the
350 * default interceptors.
351 *
352 * @return {Portal}
353 */
354 var factory = function () {
355 var port = new Portal();
356
357 port.useDefaultInterceptors();
358
359 return port;
360 }
361
362 /**
363 * EXPORT
364 * Export via CommonJS or attach it to the window.
365 */
366 var portals = {
367 create: factory,
368 Portal: Portal,
369 interceptors: {
370 validateRequest: validateRequestInterceptor,
371 mergeGlobalsRequest: mergeGlobalsRequestInterceptor,
372 buildUrlRequest: buildUrlRequestInterceptor,
373 encodeJsonRequest: encodeJsonRequestInterceptor,
374 parseJsonResponse: parseJsonResponseInterceptor
375 }
376 };
377
378 if (typeof exports !== 'undefined') {
379 if (typeof module !== 'undefined' && module.exports) {
380 exports = module.exports = portals
381 }
382 exports.portals = portals
383 }
384 else {
385 root.portals = portals
386 }
387
388}).call(this)