Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Avatar for KuCalc : devops.
Download
50663 views
1
/*
2
* router.js: Base functionality for the router.
3
*
4
* (C) 2011, Nodejitsu Inc.
5
* MIT LICENSE
6
*
7
*/
8
9
//
10
// Helper function to turn flatten an array.
11
//
12
function _flatten (arr) {
13
var flat = [];
14
15
for (var i = 0, n = arr.length; i < n; i++) {
16
flat = flat.concat(arr[i]);
17
}
18
19
return flat;
20
}
21
22
//
23
// Helper function for wrapping Array.every
24
// in the browser.
25
//
26
function _every (arr, iterator) {
27
for (var i = 0; i < arr.length; i += 1) {
28
if (iterator(arr[i], i, arr) === false) {
29
return;
30
}
31
}
32
};
33
34
//
35
// Helper function for performing an asynchronous every
36
// in series in the browser and the server.
37
//
38
function _asyncEverySeries (arr, iterator, callback) {
39
if (!arr.length) {
40
return callback();
41
}
42
43
var completed = 0;
44
(function iterate() {
45
iterator(arr[completed], function (err) {
46
if (err || err === false) {
47
callback(err);
48
callback = function () {};
49
}
50
else {
51
completed += 1;
52
if (completed === arr.length) {
53
callback();
54
}
55
else {
56
iterate();
57
}
58
}
59
});
60
})();
61
};
62
63
//
64
// Helper function for expanding "named" matches
65
// (e.g. `:dog`, etc.) against the given set
66
// of params:
67
//
68
// {
69
// ':dog': function (str) {
70
// return str.replace(/:dog/, 'TARGET');
71
// }
72
// ...
73
// }
74
//
75
function paramifyString(str, params, mod) {
76
mod = str;
77
for (var param in params) {
78
if (params.hasOwnProperty(param)) {
79
mod = params[param](str);
80
if (mod !== str) { break }
81
}
82
}
83
84
return mod === str
85
? '([._a-zA-Z0-9-]+)'
86
: mod;
87
}
88
89
//
90
// Helper function for expanding wildcards (*) and
91
// "named" matches (:whatever)
92
//
93
function regifyString(str, params) {
94
if (~str.indexOf('*')) {
95
str = str.replace(/\*/g, '([_\.\(\)!\\ %@&a-zA-Z0-9-]+)');
96
}
97
98
var captures = str.match(/:([^\/]+)/ig),
99
length;
100
101
if (captures) {
102
length = captures.length;
103
for (var i = 0; i < length; i++) {
104
str = str.replace(captures[i], paramifyString(captures[i], params));
105
}
106
}
107
108
return str;
109
}
110
111
//
112
// ### function Router (routes)
113
// #### @routes {Object} **Optional** Routing table for this instance.
114
// Constuctor function for the Router object responsible for building
115
// and dispatching from a given routing table.
116
//
117
var Router = exports.Router = function (routes) {
118
this.params = {};
119
this.routes = {};
120
this.methods = ['on', 'after', 'before'];
121
this.scope = [];
122
this._methods = {};
123
124
this.configure();
125
this.mount(routes || {});
126
};
127
128
//
129
// ### function configure (options)
130
// #### @options {Object} **Optional** Options to configure this instance with
131
// Configures this instance with the specified `options`.
132
//
133
Router.prototype.configure = function (options) {
134
options = options || {};
135
136
for (var i = 0; i < this.methods.length; i++) {
137
this._methods[this.methods[i]] = true;
138
}
139
140
this.recurse = options.recurse || this.recurse || false;
141
this.async = options.async || false;
142
this.delimiter = options.delimiter || '\/';
143
this.strict = typeof options.strict === 'undefined' ? true : options.strict;
144
this.notfound = options.notfound;
145
this.resource = options.resource;
146
147
// Client only, but browser.js does not include a super implementation
148
this.history = (options.html5history && this.historySupport) || false;
149
this.run_in_init = (this.history === true && options.run_handler_in_init !== false);
150
151
//
152
// TODO: Global once
153
//
154
this.every = {
155
after: options.after || null,
156
before: options.before || null,
157
on: options.on || null
158
};
159
160
return this;
161
};
162
163
//
164
// ### function param (token, regex)
165
// #### @token {string} Token which to replace (e.g. `:dog`, 'cat')
166
// #### @matcher {string|RegExp} Target to replace the token with.
167
// Setups up a `params` function which replaces any instance of `token`,
168
// inside of a given `str` with `matcher`. This is very useful if you
169
// have a common regular expression throughout your code base which
170
// you wish to be more DRY.
171
//
172
Router.prototype.param = function (token, matcher) {
173
if (token[0] !== ':') {
174
token = ':' + token;
175
}
176
177
var compiled = new RegExp(token, 'g');
178
this.params[token] = function (str) {
179
return str.replace(compiled, matcher.source || matcher);
180
};
181
};
182
183
//
184
// ### function on (method, path, route)
185
// #### @method {string} **Optional** Method to use
186
// #### @path {Array|string} Path to set this route on.
187
// #### @route {Array|function} Handler for the specified method and path.
188
// Adds a new `route` to this instance for the specified `method`
189
// and `path`.
190
//
191
Router.prototype.on = Router.prototype.route = function (method, path, route) {
192
var self = this;
193
194
if (!route && typeof path == 'function') {
195
//
196
// If only two arguments are supplied then assume this
197
// `route` was meant to be a generic `on`.
198
//
199
route = path;
200
path = method;
201
method = 'on';
202
}
203
204
if (Array.isArray(path)) {
205
return path.forEach(function(p) {
206
self.on(method, p, route);
207
});
208
}
209
210
if (path.source) {
211
path = path.source.replace(/\\\//ig, '/');
212
}
213
214
if (Array.isArray(method)) {
215
return method.forEach(function (m) {
216
self.on(m.toLowerCase(), path, route);
217
});
218
}
219
220
this.insert(method, this.scope.concat(path.split(new RegExp(this.delimiter))), route);
221
};
222
223
//
224
// ### function path (path, routesFn)
225
// #### @path {string|RegExp} Nested scope in which to path
226
// #### @routesFn {function} Function to evaluate in the new scope
227
// Evalutes the `routesFn` in the given path scope.
228
//
229
Router.prototype.path = function (path, routesFn) {
230
var self = this,
231
length = this.scope.length;
232
233
if (path.source) {
234
path = path.source.replace(/\\\//ig, '/');
235
}
236
237
path = path.split(new RegExp(this.delimiter));
238
this.scope = this.scope.concat(path);
239
240
routesFn.call(this, this);
241
this.scope.splice(length, path.length);
242
};
243
244
//
245
// ### function dispatch (method, path[, callback])
246
// #### @method {string} Method to dispatch
247
// #### @path {string} Path to dispatch
248
// #### @callback {function} **Optional** Continuation to respond to for async scenarios.
249
// Finds a set of functions on the traversal towards
250
// `method` and `path` in the core routing table then
251
// invokes them based on settings in this instance.
252
//
253
Router.prototype.dispatch = function (method, path, callback) {
254
var self = this,
255
fns = this.traverse(method, path, this.routes, ''),
256
invoked = this._invoked,
257
after;
258
259
this._invoked = true;
260
if (!fns || fns.length === 0) {
261
this.last = [];
262
if (typeof this.notfound === 'function') {
263
this.invoke([this.notfound], { method: method, path: path }, callback);
264
}
265
266
return false;
267
}
268
269
if (this.recurse === 'forward') {
270
fns = fns.reverse();
271
}
272
273
function updateAndInvoke() {
274
self.last = fns.after;
275
self.invoke(self.runlist(fns), self, callback);
276
}
277
278
//
279
// Builds the list of functions to invoke from this call
280
// to dispatch conforming to the following order:
281
//
282
// 1. Global after (if any)
283
// 2. After functions from the last call to dispatch
284
// 3. Global before (if any)
285
// 4. Global on (if any)
286
// 5. Matched functions from routing table (`['before', 'on'], ['before', 'on`], ...]`)
287
//
288
after = this.every && this.every.after
289
? [this.every.after].concat(this.last)
290
: [this.last];
291
292
if (after && after.length > 0 && invoked) {
293
if (this.async) {
294
this.invoke(after, this, updateAndInvoke);
295
}
296
else {
297
this.invoke(after, this);
298
updateAndInvoke();
299
}
300
301
return true;
302
}
303
304
updateAndInvoke();
305
return true;
306
};
307
308
//
309
// ### function runlist (fns)
310
// #### @fns {Array} List of functions to include in the runlist
311
// Builds the list of functions to invoke from this call
312
// to dispatch conforming to the following order:
313
//
314
// 1. Global before (if any)
315
// 2. Global on (if any)
316
// 3. Matched functions from routing table (`['before', 'on'], ['before', 'on`], ...]`)
317
//
318
Router.prototype.runlist = function (fns) {
319
var runlist = this.every && this.every.before
320
? [this.every.before].concat(_flatten(fns))
321
: _flatten(fns);
322
323
if (this.every && this.every.on) {
324
runlist.push(this.every.on);
325
}
326
327
runlist.captures = fns.captures;
328
runlist.source = fns.source;
329
return runlist;
330
};
331
332
//
333
// ### function invoke (fns, thisArg)
334
// #### @fns {Array} Set of functions to invoke in order.
335
// #### @thisArg {Object} `thisArg` for each function.
336
// #### @callback {function} **Optional** Continuation to pass control to for async `fns`.
337
// Invokes the `fns` synchronously or asynchronously depending on the
338
// value of `this.async`. Each function must **not** return (or respond)
339
// with false, or evaluation will short circuit.
340
//
341
Router.prototype.invoke = function (fns, thisArg, callback) {
342
var self = this;
343
344
if (this.async) {
345
_asyncEverySeries(fns, function apply(fn, next) {
346
if (Array.isArray(fn)) {
347
return _asyncEverySeries(fn, apply, next);
348
}
349
else if (typeof fn == 'function') {
350
fn.apply(thisArg, fns.captures.concat(next));
351
}
352
}, function () {
353
//
354
// Ignore the response here. Let the routed take care
355
// of themselves and eagerly return true.
356
//
357
358
if (callback) {
359
callback.apply(thisArg, arguments);
360
}
361
});
362
}
363
else {
364
_every(fns, function apply(fn) {
365
if (Array.isArray(fn)) {
366
return _every(fn, apply);
367
}
368
else if (typeof fn === 'function') {
369
return fn.apply(thisArg, fns.captures || null);
370
}
371
else if (typeof fn === 'string' && self.resource) {
372
self.resource[fn].apply(thisArg, fns.captures || null)
373
}
374
});
375
}
376
};
377
378
//
379
// ### function traverse (method, path, routes, regexp)
380
// #### @method {string} Method to find in the `routes` table.
381
// #### @path {string} Path to find in the `routes` table.
382
// #### @routes {Object} Partial routing table to match against
383
// #### @regexp {string} Partial regexp representing the path to `routes`.
384
// Core routing logic for `director.Router`: traverses the
385
// specified `path` within `this.routes` looking for `method`
386
// returning any `fns` that are found.
387
//
388
Router.prototype.traverse = function (method, path, routes, regexp) {
389
var fns = [],
390
current,
391
exact,
392
match,
393
next,
394
that;
395
396
//
397
// Base Case #1:
398
// If we are dispatching from the root
399
// then only check if the method exists.
400
//
401
if (path === this.delimiter && routes[method]) {
402
next = [[routes.before, routes[method]].filter(Boolean)];
403
next.after = [routes.after].filter(Boolean);
404
next.matched = true;
405
next.captures = [];
406
return next;
407
}
408
409
for (var r in routes) {
410
//
411
// We dont have an exact match, lets explore the tree
412
// in a depth-first, recursive, in-order manner where
413
// order is defined as:
414
//
415
// ['before', 'on', '<method>', 'after']
416
//
417
// Remember to ignore keys (i.e. values of `r`) which
418
// are actual methods (e.g. `on`, `before`, etc), but
419
// which are not actual nested route (i.e. JSON literals).
420
//
421
if (routes.hasOwnProperty(r) && (!this._methods[r] ||
422
this._methods[r] && typeof routes[r] === 'object' && !Array.isArray(routes[r]))) {
423
//
424
// Attempt to make an exact match for the current route
425
// which is built from the `regexp` that has been built
426
// through recursive iteration.
427
//
428
current = exact = regexp + this.delimiter + r;
429
430
431
if (!this.strict) {
432
exact += '[' + this.delimiter + ']?';
433
}
434
435
match = path.match(new RegExp('^' + exact));
436
437
if (!match) {
438
//
439
// If there isn't a `match` then continue. Here, the
440
// `match` is a partial match. e.g.
441
//
442
// '/foo/bar/buzz'.match(/^\/foo/) // ['/foo']
443
// '/no-match/route'.match(/^\/foo/) // null
444
//
445
continue;
446
}
447
448
if (match[0] && match[0] == path && routes[r][method]) {
449
//
450
// ### Base case 2:
451
// If we had a `match` and the capture is the path itself,
452
// then we have completed our recursion.
453
//
454
next = [[routes[r].before, routes[r][method]].filter(Boolean)];
455
next.after = [routes[r].after].filter(Boolean);
456
next.matched = true;
457
next.captures = match.slice(1);
458
459
if (this.recurse && routes === this.routes) {
460
next.push([routes['before'], routes['on']].filter(Boolean));
461
next.after = next.after.concat([routes['after']].filter(Boolean))
462
}
463
464
return next;
465
}
466
467
//
468
// ### Recursive case:
469
// If we had a match, but it is not yet an exact match then
470
// attempt to continue matching against the next portion of the
471
// routing table.
472
//
473
next = this.traverse(method, path, routes[r], current);
474
475
//
476
// `next.matched` will be true if the depth-first search of the routing
477
// table from this position was successful.
478
//
479
if (next.matched) {
480
//
481
// Build the in-place tree structure representing the function
482
// in the correct order.
483
//
484
if (next.length > 0) {
485
fns = fns.concat(next);
486
}
487
488
if (this.recurse) {
489
fns.push([routes[r].before, routes[r].on].filter(Boolean));
490
next.after = next.after.concat([routes[r].after].filter(Boolean));
491
492
if (routes === this.routes) {
493
fns.push([routes['before'], routes['on']].filter(Boolean));
494
next.after = next.after.concat([routes['after']].filter(Boolean))
495
}
496
}
497
498
fns.matched = true;
499
fns.captures = next.captures;
500
fns.after = next.after;
501
502
//
503
// ### Base case 2:
504
// Continue passing the partial tree structure back up the stack.
505
// The caller for `dispatch()` will decide what to do with the functions.
506
//
507
return fns;
508
}
509
}
510
}
511
512
return false;
513
};
514
515
//
516
// ### function insert (method, path, route, context)
517
// #### @method {string} Method to insert the specific `route`.
518
// #### @path {Array} Parsed path to insert the `route` at.
519
// #### @route {Array|function} Route handlers to insert.
520
// #### @parent {Object} **Optional** Parent "routes" to insert into.
521
// Inserts the `route` for the `method` into the routing table for
522
// this instance at the specified `path` within the `context` provided.
523
// If no context is provided then `this.routes` will be used.
524
//
525
Router.prototype.insert = function (method, path, route, parent) {
526
var methodType,
527
parentType,
528
isArray,
529
nested,
530
part;
531
532
path = path.filter(function (p) {
533
return p && p.length > 0;
534
});
535
536
parent = parent || this.routes;
537
part = path.shift();
538
if (/\:|\*/.test(part) && !/\\d|\\w/.test(part)) {
539
part = regifyString(part, this.params);
540
}
541
542
if (path.length > 0) {
543
//
544
// If this is not the last part left in the `path`
545
// (e.g. `['cities', 'new-york']`) then recurse into that
546
// child
547
//
548
parent[part] = parent[part] || {};
549
return this.insert(method, path, route, parent[part]);
550
}
551
552
//
553
// If there is no part and the path has been exhausted
554
// and the parent is the root of the routing table,
555
// then we are inserting into the root and should
556
// only dive one level deep in the Routing Table.
557
//
558
if (!part && !path.length && parent === this.routes) {
559
methodType = typeof parent[method];
560
561
switch (methodType) {
562
case 'function':
563
parent[method] = [parent[method], route];
564
return;
565
case 'object':
566
parent[method].push(route)
567
return;
568
case 'undefined':
569
parent[method] = route;
570
return;
571
}
572
573
return;
574
}
575
576
//
577
// Otherwise, we are at the end of our insertion so we should
578
// insert the `route` based on the `method` after getting the
579
// `parent` of the last `part`.
580
//
581
parentType = typeof parent[part];
582
isArray = Array.isArray(parent[part]);
583
584
if (parent[part] && !isArray && parentType == 'object') {
585
methodType = typeof parent[part][method];
586
587
switch (methodType) {
588
case 'function':
589
parent[part][method] = [parent[part][method], route];
590
return;
591
case 'object':
592
parent[part][method].push(route)
593
return;
594
case 'undefined':
595
parent[part][method] = route;
596
return;
597
}
598
}
599
else if (parentType == 'undefined') {
600
nested = {};
601
nested[method] = route;
602
parent[part] = nested;
603
return;
604
}
605
606
throw new Error('Invalid route context: ' + parentType);
607
};
608
609
610
//
611
// ### function extend (methods)
612
// #### @methods {Array} List of method names to extend this instance with
613
// Extends this instance with simple helper methods to `this.on`
614
// for each of the specified `methods`
615
//
616
Router.prototype.extend = function(methods) {
617
var self = this,
618
len = methods.length,
619
i;
620
621
for (i = 0; i < len; i++) {
622
(function(method) {
623
self._methods[method] = true;
624
self[method] = function () {
625
var extra = arguments.length === 1
626
? [method, '']
627
: [method];
628
629
self.on.apply(self, extra.concat(Array.prototype.slice.call(arguments)));
630
};
631
})(methods[i]);
632
}
633
};
634
635
//
636
// ### function mount (routes, context)
637
// #### @routes {Object} Routes to mount onto this instance
638
// Mounts the sanitized `routes` onto the root context for this instance.
639
//
640
// e.g.
641
//
642
// new Router().mount({ '/foo': { '/bar': function foobar() {} } })
643
//
644
// yields
645
//
646
// { 'foo': 'bar': function foobar() {} } }
647
//
648
Router.prototype.mount = function(routes, path) {
649
if (!routes || typeof routes !== "object" || Array.isArray(routes)) {
650
return;
651
}
652
653
var self = this;
654
path = path || [];
655
if (!Array.isArray(path)) {
656
path = path.split(self.delimiter);
657
}
658
659
function insertOrMount(route, local) {
660
var rename = route,
661
parts = route.split(self.delimiter),
662
routeType = typeof routes[route],
663
isRoute = parts[0] === "" || !self._methods[parts[0]],
664
event = isRoute ? "on" : rename;
665
666
if (isRoute) {
667
rename = rename.slice((rename.match(new RegExp(self.delimiter)) || [''])[0].length);
668
parts.shift();
669
}
670
671
if (isRoute && routeType === 'object' && !Array.isArray(routes[route])) {
672
local = local.concat(parts);
673
self.mount(routes[route], local);
674
return;
675
}
676
677
if (isRoute) {
678
local = local.concat(rename.split(self.delimiter));
679
}
680
681
self.insert(event, local, routes[route]);
682
}
683
684
for (var route in routes) {
685
if (routes.hasOwnProperty(route)) {
686
insertOrMount(route, path.slice(0));
687
}
688
}
689
};
690
691
692