forked from YahooArchive/mojito
-
Notifications
You must be signed in to change notification settings - Fork 1
/
route-maker.common.js
446 lines (369 loc) · 13.2 KB
/
route-maker.common.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
/*
* Copyright (c) 2011-2012, Yahoo! Inc. All rights reserved.
* Copyrights licensed under the New BSD License.
* See the accompanying LICENSE file for terms.
*/
/*jslint anon:true, sloppy:true, regexp: true, nomen:true*/
/*global YUI*/
YUI.add('mojito-route-maker', function(Y, NAME) {
var doCallReplacement,
copy;
function wild(it) {
// if {it}, then it is a wildcard
if (it.indexOf('{') === 0) {
// so return the true value without the {}
return it.substring(1, it.length - 1);
}
}
function resolveParams(route, params) {
// console.log('============= resolving params for route ' +
// route.name);
// console.log(params);
// console.log('requires: ' + Y.JSON.stringify(route.requires));
var tester = [];
// we don't need to do anything if this route requires no params
if (Y.Object.size(route.requires) === 0) {
return route;
}
Y.Object.each(params, function(pval, pname) {
if (route.requires && route.requires[pname]) {
tester.push(pname + '=' + pval);
}
});
if (tester.length) {
tester.sort();
if (new RegExp(route.int_match).test(tester.join('&'))) {
Y.Object.each(params, function(pval, pname) {
route.query[pname] = pval;
});
return route;
}
}
}
function buildRoute(name, route) {
var i,
verbObj,
path,
matches,
build,
segment,
key;
if (!route.name) {
route.name = name;
}
if (!route.verbs) {
route.verbs = ['GET'];
}
// Checking route.verbs is changed from an array to an object by the
// building process, so routes that have already been computed are
// not recomputed.
if (route.verbs.length && route.path && route.call) {
// FUTURE: [Issue 73] allow object params, not just string
if (!route.params) {
route.params = '';
}
if (!route.regex) {
route.regex = {};
}
if (!route.query) {
route.query = {};
}
/*
* Here we convert the verb array to a map for easy use later on
**/
verbObj = {};
for (i in route.verbs) {
if (route.verbs.hasOwnProperty(i)) {
verbObj[route.verbs[i].toUpperCase()] = true;
}
}
route.verbs = verbObj;
path = route.path.split('/');
/*
* Here we build the matching regex for external URI's
*/
for (segment in path) {
if (path.hasOwnProperty(segment)) {
if (path[segment][0] === ':') {
key = path[segment].substr(1);
route.query[key] = '';
path[segment] = route.regex[key] ?
'(' + route.regex[key] + ')' :
'([^\/]+)';
}
if (path[segment][0] === '*') {
path[segment] = '(.*)';
}
}
}
/*
* Here we build the matching regex for internal URI's
*/
route.requires = {};
matches = route.path.match(/:([^\/]+)/g);
for (i in matches) {
if (matches.hasOwnProperty(i)) {
route.requires[matches[i].substr(1)] = '[^&]+';
}
}
for (i in route.regex) {
if (route.regex.hasOwnProperty(i)) {
route.requires[i] = route.regex[i];
}
}
if (typeof route.params !== 'object') {
route.params = Y.QueryString.parse(String(route.params));
}
build = [];
for (i in route.requires) {
if (route.requires.hasOwnProperty(i)) {
build.push(i + '=' + route.requires[i]);
}
}
build.sort();
/*
* We are done so lets store the regex's for the route.
*/
// TODO: [Issue 74] These Regexes are recreated on
// every request because they need to be serialized and sent to the
// client, need to figure out a way to prevent that
route.ext_match = '^' + path.join('\/') + '$';
route.int_match = '^' + build.join('&') + '$';
}
return route;
}
doCallReplacement = function(route, uri) {
var uriParts = uri.substr(1).split('\/'),
pathParts = route.path.substr(1).split('\/'),
template = {},
cnt = 0;
pathParts.forEach(function(pathPart) {
var key,
val,
regex;
// process only those keyed by ':'
if (pathPart.indexOf(':') === 0) {
key = pathPart.substr(1);
val = uriParts[cnt];
template[key] = val;
regex = new RegExp('{' + key + '}', 'g');
if (regex.test(route.call)) {
route.call = route.call.replace(regex, template[key]);
} else {
route.params[key] = val;
}
}
cnt += 1;
});
return route;
};
copy = function(obj) {
var temp = null, key = '';
if (!obj || typeof obj !== 'object') {
return obj;
}
temp = new obj.constructor();
for (key in obj) {
if (obj.hasOwnProperty(key)) {
temp[key] = copy(obj[key]);
}
}
return temp;
};
/*
* The route maker for reverse URL lookup.
* @class Maker
* @namespace Y.mojito
* @param {Object} routes key value store of all routes in the system
*/
function Maker(routes) {
var name;
this._routes = {};
// TODO: [Issue 75] Cache these computed routes so we
// don't have to do this on each request.
for (name in routes) {
if (routes.hasOwnProperty(name)) {
this._routes[name] = buildRoute(name, routes[name]);
}
}
}
Maker.prototype = {
/*
* Generates a URL from a route query
* @method make
* @param {String} query string to convert to a URL
* @param {String} verb http method
*/
make: function(query, verb) {
// Y.log('make(' + query + ', ' + verb + ')', 'debug', NAME);
var parts = query.split('?'),
call = parts[0],
params = {},
route,
uri;
// TODO: don't assign to a parameter.
verb = verb || 'GET';
if (parts[1]) {
params = Y.QueryString.parse(parts[1]);
}
route = this._matchToExternal(call, params, verb, this._routes);
if (!route) {
throw new Error(
"No route match found for '" + query + "' (" + verb + ')'
);
}
uri = route.path;
Y.Object.each(route.query, function(v, k) {
uri = uri.replace(':' + k, v);
delete params[k];
});
if (!Y.Object.isEmpty(params)) {
uri += '?' + Y.QueryString.stringify(params);
}
return uri;
},
/**
* Finds a route for a given method+URL
* @method find
* @param {string} url the URL to find a route for.
* @param {string} verb the HTTP method.
*/
find: function(uri, verb) {
// logger.log('[UriRouter] find( ' + uri + ', ' + verb + ' )');
var route,
match,
ret,
i,
id;
// TODO: don't assign to parameter.
verb = verb || 'GET';
route = this._matchToInternal(uri, verb, this._routes);
if (!route) {
return null;
}
// logger.log('[UriRouter] found route: ' + Y.JSON.stringify(route));
match = copy(route);
// Add the extracted URI params to the query obj
ret = new RegExp(route.ext_match).exec(uri);
i = 1;
for (id in match.query) {
if (match.query.hasOwnProperty(id)) {
match.query[id] = ret[i];
i += 1;
}
}
// Add the fixed params to a query obj if they are not there
for (i in match.params) {
if (match.params.hasOwnProperty(i) && !match.query[i]) {
match.query[i] = match.params[i];
}
}
return match;
},
/**
* For optimization. Call this to get the computed routes that can be
* passed to the constructor to avoid recomputing the routes.
* @method getComputedRoutes
* @return {object} computed routes.
*/
getComputedRoutes: function() {
return this._routes;
},
/**
* Returns a matching route for the given URI
* @method _matchToInternal
* @param {string} uri The uri to find a route for.
* @param {string} verb. The HTTP verb for the route.
* @private
*/
_matchToInternal: function(uri, verb, routes) {
var name;
// TODO: don't assign to a parameter.
if (!verb) {
verb = 'GET';
}
verb = verb.toUpperCase();
// logger.log('[UriRouter] Start Matching ...');
for (name in routes) {
if (routes.hasOwnProperty(name)) {
// logger.log('[UriRouter] testing ' + name);
// TODO: [Issue 74] See comment elsewhere
// about regexes being created... we need to stash these
// objects somewhere instead of creating them on every
// request
if (new RegExp(routes[name].ext_match).test(uri) &&
routes[name].verbs &&
routes[name].verbs.hasOwnProperty(verb)) {
// TODO: [Issue 74] Prevent more Regex creations.
return doCallReplacement(routes[name], uri);
}
// logger.log('[UriRouter] ' + verb + ' ' + uri + ' ' +
// routes[name].ext_match);
}
}
return false;
},
/*
* @method _matchToExternal
* @private
*/
_matchToExternal: function(call, params, verb, routes) {
var match,
callParts = call.split('.'),
callId = callParts[0],
callAction = callParts[1];
Y.Object.some(routes, function(route) {
var routeCall,
routeId,
routeAction,
wildId,
wildAction;
// it might be an exact match
if (call === route.call && route.verbs[verb]) {
match = resolveParams(route, params);
if (match) {
return true;
}
}
// if we have a wild card try a match
if ('*.*' === route.call && route.verbs[verb]) {
params.module = callId;
params.action = callAction;
match = resolveParams(route, params);
if (match) {
return true;
}
}
routeCall = route.call.split('.');
routeId = routeCall[0];
routeAction = routeCall[1];
wildId = wild(routeId);
if (wildId) {
params[wildId] = callId;
}
wildAction = wild(routeAction);
if (wildAction) {
params[wildAction] = callAction;
}
// if action is wild, or action matches
if ((wildAction || (callAction === routeAction)) &&
// and if id is wild, or id matches
((wildId || (callId === routeId))) &&
// and if the verb is correct
route.verbs[verb]) {
// then we can try a param match
match = resolveParams(route, params);
if (match) {
return true;
}
}
});
return match;
}
};
Y.namespace('mojito').RouteMaker = Maker;
}, '0.1.0', { requires: [
'querystring-stringify-simple',
'querystring-parse',
'mojito-util'
]});