http_router 0.8.11 → 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +32 -3
- data/js/lib/http_router.coffee +368 -0
- data/js/lib/http_router.js +668 -0
- data/js/package.json +10 -0
- data/js/test/test.coffee +136 -0
- data/js/test/test.js +229 -0
- data/lib/http_router/node/arbitrary.rb +1 -1
- data/lib/http_router/node/free_regex.rb +3 -2
- data/lib/http_router/node/glob.rb +4 -3
- data/lib/http_router/node/glob_regex.rb +3 -2
- data/lib/http_router/node/lookup.rb +2 -3
- data/lib/http_router/node/path.rb +62 -0
- data/lib/http_router/node/request.rb +13 -2
- data/lib/http_router/node/root.rb +29 -2
- data/lib/http_router/node/spanning_regex.rb +6 -5
- data/lib/http_router/node.rb +7 -12
- data/lib/http_router/rack/builder.rb +0 -8
- data/lib/http_router/regex_route.rb +13 -0
- data/lib/http_router/route.rb +57 -42
- data/lib/http_router/util.rb +41 -0
- data/lib/http_router/version.rb +1 -1
- data/lib/http_router.rb +17 -22
- data/test/common/generate.txt +8 -2
- data/test/common/http_recognize.txt +58 -0
- data/test/common/recognize.txt +12 -65
- data/test/generation.rb +5 -102
- data/test/generic.rb +111 -0
- data/test/recognition.rb +6 -100
- data/test/test_misc.rb +26 -2
- data/test/test_mounting.rb +4 -4
- data/test/test_recognition.rb +18 -0
- metadata +94 -34
- data/lib/http_router/node/destination.rb +0 -45
- data/lib/http_router/path.rb +0 -58
data/Rakefile
CHANGED
@@ -1,11 +1,33 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require 'bundler'
|
3
3
|
Bundler::GemHelper.install_tasks
|
4
|
+
# Rake::Task['release'].enhance([:test, :release_js]) FIXME, this just doesn't work.
|
4
5
|
|
5
|
-
|
6
|
-
|
6
|
+
task :release_js do
|
7
|
+
$: << 'lib'
|
8
|
+
require 'http_router/version'
|
9
|
+
File.open('js/package.json', 'w') do |f|
|
10
|
+
f << <<-EOT
|
11
|
+
{
|
12
|
+
"name": "http_router",
|
13
|
+
"description": "URL routing and generation in js",
|
14
|
+
"author": "Joshua Hull <joshbuddy@gmail.com>",
|
15
|
+
"version": "#{HttpRouter::VERSION}",
|
16
|
+
"directories": {
|
17
|
+
"lib" : "./lib/http_router"
|
18
|
+
},
|
19
|
+
"main": "lib/http_router"
|
20
|
+
}
|
21
|
+
EOT
|
22
|
+
end
|
23
|
+
sh "cd js && npm publish"
|
24
|
+
`git commit js/package.json -m'bumped js version'`
|
25
|
+
end
|
7
26
|
|
8
|
-
|
27
|
+
test_tasks = ['test:generation', 'test:recognition', 'test:integration', 'test:examples', 'test:rdoc_examples']
|
28
|
+
#test_tasks << 'test:js' if `which coffee && which node` && $?.success?
|
29
|
+
desc "Run all tests"
|
30
|
+
task :test => test_tasks
|
9
31
|
|
10
32
|
desc "Clean things"
|
11
33
|
task :clean do
|
@@ -22,6 +44,13 @@ namespace :test do
|
|
22
44
|
Dir['./test/**/test_*.rb'].each { |test| require test }
|
23
45
|
end
|
24
46
|
|
47
|
+
desc "Run js tests"
|
48
|
+
task :js do
|
49
|
+
sh "coffee -c js/test/test.coffee"
|
50
|
+
sh "coffee -c js/lib/http_router.coffee"
|
51
|
+
sh "node js/test/test.js"
|
52
|
+
end
|
53
|
+
|
25
54
|
desc "Run generic recognition tests"
|
26
55
|
task :recognition do
|
27
56
|
$: << 'lib'
|
@@ -0,0 +1,368 @@
|
|
1
|
+
root.Sherpa = class Sherpa
|
2
|
+
constructor: (@callback) ->
|
3
|
+
@root = new Node()
|
4
|
+
@routes = {}
|
5
|
+
match: (httpRequest, httpResponse) ->
|
6
|
+
request = if (httpRequest.url?) then new Request(httpRequest) else new PathRequest(httpRequest)
|
7
|
+
@root.match(request)
|
8
|
+
if request.destinations.length > 0
|
9
|
+
new Response(request, httpResponse).invoke()
|
10
|
+
else if @callback?
|
11
|
+
@callback(request.underlyingRequest)
|
12
|
+
findSubparts: (part) ->
|
13
|
+
subparts = []
|
14
|
+
while match = part.match(/\\.|[:*][a-z0-9_]+|[^:*\\]+/)
|
15
|
+
part = part.slice(match.index, part.length)
|
16
|
+
subparts.push part.slice(0, match[0].length)
|
17
|
+
part = part.slice(match[0].length, part.length)
|
18
|
+
subparts
|
19
|
+
generatePaths: (path) ->
|
20
|
+
[paths, chars, startIndex, endIndex] = [[''], path.split(''), 0, 1]
|
21
|
+
for charIndex in [0...chars.length]
|
22
|
+
c = chars[charIndex]
|
23
|
+
switch c
|
24
|
+
when '\\'
|
25
|
+
# do nothing ...
|
26
|
+
charIndex++
|
27
|
+
add = if chars[charIndex] == ')' or chars[charIndex] == '('
|
28
|
+
chars[charIndex]
|
29
|
+
else
|
30
|
+
"\\#{chars[charIndex]}"
|
31
|
+
paths[pathIndex] += add for pathIndex in [startIndex...endIndex]
|
32
|
+
when '('
|
33
|
+
# over current working set, double paths
|
34
|
+
paths.push(paths[pathIndex]) for pathIndex in [startIndex...endIndex]
|
35
|
+
# move working set to newly copied paths
|
36
|
+
startIndex = endIndex
|
37
|
+
endIndex = paths.length
|
38
|
+
when ')'
|
39
|
+
startIndex -= endIndex - startIndex
|
40
|
+
else
|
41
|
+
paths[pathIndex] += c for pathIndex in [startIndex...endIndex]
|
42
|
+
paths.reverse()
|
43
|
+
paths
|
44
|
+
url: (name, params) ->
|
45
|
+
@routes[name]?.url(params)
|
46
|
+
addComplexPart: (subparts, compiledPath, matchesWith, variableNames) ->
|
47
|
+
escapeRegexp = (str) -> str.replace(/([\.*+?^=!:${}()|[\]\/\\])/g, '\\$1')
|
48
|
+
[capturingIndicies, splittingIndicies, captures, spans] = [[], [], 0, false]
|
49
|
+
regexSubparts = for part in subparts
|
50
|
+
switch part[0]
|
51
|
+
when '\\'
|
52
|
+
compiledPath.push "'#{part[1]}'"
|
53
|
+
escapeRegexp(part[1])
|
54
|
+
when ':', '*'
|
55
|
+
spans = true if part[0] == '*'
|
56
|
+
captures += 1
|
57
|
+
name = part.slice(1, part.length)
|
58
|
+
variableNames.push(name)
|
59
|
+
if part[0] == '*'
|
60
|
+
splittingIndicies.push(captures)
|
61
|
+
compiledPath.push "params['#{name}'].join('/')"
|
62
|
+
else
|
63
|
+
capturingIndicies.push(captures)
|
64
|
+
compiledPath.push "params['#{name}']"
|
65
|
+
if spans
|
66
|
+
if matchesWith[name]? then "((?:#{matchesWith[name].source}\\/?)+)" else '(.*?)'
|
67
|
+
else
|
68
|
+
"(#{(matchesWith[name]?.source || '[^/]*?')})"
|
69
|
+
else
|
70
|
+
compiledPath.push "'#{part}'"
|
71
|
+
escapeRegexp(part)
|
72
|
+
regexp = new RegExp("#{regexSubparts.join('')}$")
|
73
|
+
if spans
|
74
|
+
new SpanningRegexMatcher(regexp, capturingIndicies, splittingIndicies)
|
75
|
+
else
|
76
|
+
new RegexMatcher(regexp, capturingIndicies, splittingIndicies)
|
77
|
+
addSimplePart: (subparts, compiledPath, matchesWith, variableNames) ->
|
78
|
+
part = subparts[0]
|
79
|
+
switch part[0]
|
80
|
+
when ':'
|
81
|
+
variableName = part.slice(1, part.length)
|
82
|
+
compiledPath.push "params['#{variableName}']"
|
83
|
+
variableNames.push(variableName)
|
84
|
+
if matchesWith[variableName]? then new SpanningRegexMatcher(matchesWith[variableName], [0], []) else new Variable()
|
85
|
+
when '*'
|
86
|
+
compiledPath.push "params['#{variableName}'].join('/')"
|
87
|
+
variableName = part.slice(1, part.length)
|
88
|
+
variableNames.push(variableName)
|
89
|
+
new Glob(matchesWith[variableName])
|
90
|
+
else
|
91
|
+
compiledPath.push "'#{part}'"
|
92
|
+
new Lookup(part)
|
93
|
+
add: (rawPath, opts) ->
|
94
|
+
matchesWith = opts?.matchesWith || {}
|
95
|
+
defaults = opts?.default || {}
|
96
|
+
routeName = opts?.name
|
97
|
+
partiallyMatch = false
|
98
|
+
route = if rawPath.exec?
|
99
|
+
new Route([@root.add(new RegexPath(@root, rawPath))])
|
100
|
+
else
|
101
|
+
if rawPath.substring(rawPath.length - 1) == '*'
|
102
|
+
rawPath = rawPath.substring(0, rawPath.length - 1)
|
103
|
+
partiallyMatch = true
|
104
|
+
pathSet = for path in @generatePaths(rawPath)
|
105
|
+
node = @root
|
106
|
+
variableNames = []
|
107
|
+
parts = path.split('/')
|
108
|
+
compiledPath = []
|
109
|
+
for part in parts
|
110
|
+
unless part == ''
|
111
|
+
compiledPath.push "'/'"
|
112
|
+
subparts = @findSubparts(part)
|
113
|
+
nextNodeFn = if subparts.length == 1 then @addSimplePart else @addComplexPart
|
114
|
+
node = node.add(nextNodeFn(subparts, compiledPath, matchesWith, variableNames))
|
115
|
+
if opts?.conditions?
|
116
|
+
node = node.add(new RequestMatcher(opts.conditions))
|
117
|
+
path = new Path(node, variableNames)
|
118
|
+
path.partial = partiallyMatch
|
119
|
+
path.compiled = if compiledPath.length == 0 then "'/'" else compiledPath.join('+')
|
120
|
+
path
|
121
|
+
new Route(pathSet, matchesWith)
|
122
|
+
route.default = defaults
|
123
|
+
route.name = routeName
|
124
|
+
@routes[routeName] = route if routeName?
|
125
|
+
route
|
126
|
+
|
127
|
+
class Response
|
128
|
+
constructor: (@request, @httpResponse, @position) ->
|
129
|
+
@position ||= 0
|
130
|
+
next: ->
|
131
|
+
if @position == @destinations.length - 1
|
132
|
+
false
|
133
|
+
else
|
134
|
+
new Response(@request, @httpResponse, @position + 1).invoke()
|
135
|
+
invoke: ->
|
136
|
+
req = if typeof(@request.underlyingRequest) == 'string' then {} else @request.underlyingRequest
|
137
|
+
req.params = @request.destinations[@position].params
|
138
|
+
req.route = @request.destinations[@position].route
|
139
|
+
req.pathInfo = @request.destinations[@position].pathInfo
|
140
|
+
@request.destinations[@position].route.destination(req, @httpResponse)
|
141
|
+
|
142
|
+
class Node
|
143
|
+
constructor: ->
|
144
|
+
@type ||= 'node'
|
145
|
+
@matchers = []
|
146
|
+
add: (n) ->
|
147
|
+
@matchers.push(n) if !@matchers[@matchers.length - 1]?.usable(n)
|
148
|
+
@matchers[@matchers.length - 1].use(n)
|
149
|
+
usable: (n) -> n.type == @type
|
150
|
+
match: (request) ->
|
151
|
+
m.match(request) for m in @matchers
|
152
|
+
superMatch: Node::match
|
153
|
+
use: (n) -> this
|
154
|
+
|
155
|
+
class Lookup extends Node
|
156
|
+
constructor: (part) ->
|
157
|
+
@part = part
|
158
|
+
@type = 'lookup'
|
159
|
+
@map = {}
|
160
|
+
super
|
161
|
+
match: (request) ->
|
162
|
+
if @map[request.path[0]]?
|
163
|
+
request = request.clone()
|
164
|
+
part = request.path.shift()
|
165
|
+
@map[part].match(request)
|
166
|
+
use: (n) ->
|
167
|
+
@map[n.part] ||= new Node()
|
168
|
+
@map[n.part]
|
169
|
+
|
170
|
+
class Variable extends Node
|
171
|
+
constructor: ->
|
172
|
+
@type ||= 'variable'
|
173
|
+
super
|
174
|
+
match: (request) ->
|
175
|
+
if request.path.length > 0
|
176
|
+
request = request.clone()
|
177
|
+
request.variables.push(request.path.shift())
|
178
|
+
super(request)
|
179
|
+
|
180
|
+
class Glob extends Variable
|
181
|
+
constructor: (@regexp) ->
|
182
|
+
@type = 'glob'
|
183
|
+
super
|
184
|
+
match: (request) ->
|
185
|
+
if request.path.length > 0
|
186
|
+
original_request = request
|
187
|
+
cloned_path = request.path.slice(0, request.path)
|
188
|
+
for i in [1..original_request.path.length]
|
189
|
+
request = original_request.clone()
|
190
|
+
match = request.path[i - 1].match(@regexp) if @regexp?
|
191
|
+
return if @regexp? and (!match? or match[0].length != request.path[i - 1].length)
|
192
|
+
request.variables.push(request.path.slice(0, i))
|
193
|
+
request.path = request.path.slice(i, request.path.length)
|
194
|
+
@superMatch(request)
|
195
|
+
|
196
|
+
class RegexMatcher extends Node
|
197
|
+
constructor: (@regexp, @capturingIndicies, @splittingIndicies) ->
|
198
|
+
@type ||= 'regex'
|
199
|
+
@varIndicies = []
|
200
|
+
@varIndicies[i] = [i, 'split'] for i in @splittingIndicies
|
201
|
+
@varIndicies[i] = [i, 'capture'] for i in @capturingIndicies
|
202
|
+
@varIndicies.sort (a, b) -> a[0] - b[0]
|
203
|
+
super
|
204
|
+
match: (request) ->
|
205
|
+
if request.path[0]? and match = request.path[0].match(@regexp)
|
206
|
+
return unless match[0].length == request.path[0].length
|
207
|
+
request = request.clone()
|
208
|
+
@addVariables(request, match)
|
209
|
+
request.path.shift()
|
210
|
+
super(request)
|
211
|
+
addVariables: (request, match) ->
|
212
|
+
for v in @varIndicies when v?
|
213
|
+
idx = v[0]
|
214
|
+
type = v[1]
|
215
|
+
switch type
|
216
|
+
when 'split' then request.variables.push match[idx].split('/')
|
217
|
+
when 'capture' then request.variables.push match[idx]
|
218
|
+
usable: (n) ->
|
219
|
+
n.type == @type && n.regexp == @regexp && n.capturingIndicies == @capturingIndicies && n.splittingIndicies == @splittingIndicies
|
220
|
+
|
221
|
+
class SpanningRegexMatcher extends RegexMatcher
|
222
|
+
constructor: (@regexp, @capturingIndicies, @splittingIndicies) ->
|
223
|
+
@type = 'spanning'
|
224
|
+
super
|
225
|
+
match: (request) ->
|
226
|
+
if request.path.length > 0
|
227
|
+
wholePath = request.wholePath()
|
228
|
+
if match = wholePath.match(@regexp)
|
229
|
+
return unless match.index == 0
|
230
|
+
request = request.clone()
|
231
|
+
@addVariables(request, match)
|
232
|
+
request.path = request.splitPath(wholePath.slice(match.index + match[0].length, wholePath.length))
|
233
|
+
@superMatch(request)
|
234
|
+
|
235
|
+
class RequestMatcher extends Node
|
236
|
+
constructor: (@conditions) ->
|
237
|
+
@type = 'request'
|
238
|
+
super
|
239
|
+
match: (request) ->
|
240
|
+
conditionCount = 0
|
241
|
+
satisfiedConditionCount = 0
|
242
|
+
for type, matcher of @conditions
|
243
|
+
val = request.underlyingRequest[type]
|
244
|
+
conditionCount++
|
245
|
+
v = if matcher instanceof Array
|
246
|
+
matching = ->
|
247
|
+
for cond in matcher
|
248
|
+
if cond.exec?
|
249
|
+
return true if matcher.exec(val)
|
250
|
+
else
|
251
|
+
return true if cond == val
|
252
|
+
false
|
253
|
+
matching()
|
254
|
+
else
|
255
|
+
if matcher.exec? then matcher.exec(val) else matcher == val
|
256
|
+
satisfiedConditionCount++ if v
|
257
|
+
if conditionCount == satisfiedConditionCount
|
258
|
+
super(request)
|
259
|
+
usable: (n) ->
|
260
|
+
n.type == @type && n.conditions == @conditions
|
261
|
+
|
262
|
+
class Path extends Node
|
263
|
+
constructor: (@parent, @variableNames) ->
|
264
|
+
@type = 'path'
|
265
|
+
@partial = false
|
266
|
+
addDestination: (request) -> request.destinations.push({route: @route, request: request, params: @constructParams(request)})
|
267
|
+
match: (request) ->
|
268
|
+
if @partial or request.path.length == 0
|
269
|
+
@addDestination(request)
|
270
|
+
if @partial
|
271
|
+
request.destinations[request.destinations.length - 1].pathInfo = "/#{request.wholePath()}"
|
272
|
+
constructParams: (request) ->
|
273
|
+
params = {}
|
274
|
+
for i in [0...@variableNames.length]
|
275
|
+
params[@variableNames[i]] = request.variables[i]
|
276
|
+
params
|
277
|
+
url: (rawParams) ->
|
278
|
+
rawParams = {} unless rawParams?
|
279
|
+
params = {}
|
280
|
+
for key in @variableNames
|
281
|
+
params[key] = if @route.default? then rawParams[key] || @route.default[key] else rawParams[key]
|
282
|
+
return undefined if !params[key]?
|
283
|
+
for name in @variableNames
|
284
|
+
if @route.matchesWith[name]?
|
285
|
+
match = params[name].match(@route.matchesWith[name])
|
286
|
+
return undefined unless match? && match[0].length == params[name].length
|
287
|
+
path = if @compiled == '' then '' else eval(@compiled)
|
288
|
+
if path?
|
289
|
+
delete rawParams[name] for name in @variableNames
|
290
|
+
path
|
291
|
+
|
292
|
+
class RegexPath extends Path
|
293
|
+
constructor: (@parent, @regexp) ->
|
294
|
+
@type = 'regexp_route'
|
295
|
+
super
|
296
|
+
match: (request) ->
|
297
|
+
request.regexpRouteMatch = @regexp.exec(request.decodedPath())
|
298
|
+
if request.regexpRouteMatch? && request.regexpRouteMatch[0].length == request.decodedPath().length
|
299
|
+
request = request.clone()
|
300
|
+
request.path = []
|
301
|
+
super(request)
|
302
|
+
constructParams: (request) -> request.regexpRouteMatch
|
303
|
+
url: (rawParams) -> throw("This route cannot be generated")
|
304
|
+
|
305
|
+
class Route
|
306
|
+
constructor: (@pathSet, @matchesWith) ->
|
307
|
+
path.route = this for path in @pathSet
|
308
|
+
to: (@destination) ->
|
309
|
+
path.parent.add(path) for path in @pathSet
|
310
|
+
generateQuery: (params, base, query) ->
|
311
|
+
query = ""
|
312
|
+
base ||= ""
|
313
|
+
if params?
|
314
|
+
if params instanceof Array
|
315
|
+
for idx in [0...(params.length)]
|
316
|
+
query += @generateQuery(params[idx], "#{base}[]")
|
317
|
+
else if params instanceof Object
|
318
|
+
for k,v of params
|
319
|
+
query += @generateQuery(v, if base == '' then k else "#{base}[#{k}]")
|
320
|
+
else
|
321
|
+
query += encodeURIComponent(base).replace(/%20/g, '+')
|
322
|
+
query += '='
|
323
|
+
query += encodeURIComponent(params).replace(/%20/g, '+')
|
324
|
+
query += '&'
|
325
|
+
query
|
326
|
+
url: (params) ->
|
327
|
+
path = undefined
|
328
|
+
for pathObj in @pathSet
|
329
|
+
path = pathObj.url(params)
|
330
|
+
break if path?
|
331
|
+
if path?
|
332
|
+
query = @generateQuery(params)
|
333
|
+
joiner = if query != '' then '?' else ''
|
334
|
+
"#{encodeURI(path)}#{joiner}#{query.substr(0, query.length - 1)}"
|
335
|
+
else
|
336
|
+
undefined
|
337
|
+
|
338
|
+
class Request
|
339
|
+
constructor: (@underlyingRequest, @callback) ->
|
340
|
+
@variables = []
|
341
|
+
@destinations = []
|
342
|
+
if @underlyingRequest?
|
343
|
+
@path = @splitPath()
|
344
|
+
toString: -> "<Request path: /#{@path.join('/') } #{@path.length}>"
|
345
|
+
wholePath: -> @path.join('/')
|
346
|
+
decodedPath: (path) ->
|
347
|
+
unless path?
|
348
|
+
path = require('url').parse(@underlyingRequest.url).pathname
|
349
|
+
decodeURI(path)
|
350
|
+
splitPath: (path) ->
|
351
|
+
decodedPath = @decodedPath(path)
|
352
|
+
splitPath = if decodedPath == '/' then [] else decodedPath.split('/')
|
353
|
+
splitPath.shift() if splitPath[0] == ''
|
354
|
+
splitPath
|
355
|
+
clone: ->
|
356
|
+
c = new Request()
|
357
|
+
c.path = @path.slice(0, @path.length)
|
358
|
+
c.variables = @variables.slice(0, @variables.length)
|
359
|
+
c.underlyingRequest = @underlyingRequest
|
360
|
+
c.callback = @callback
|
361
|
+
c.destinations = @destinations
|
362
|
+
c
|
363
|
+
|
364
|
+
class PathRequest extends Request
|
365
|
+
decodedPath: (path) ->
|
366
|
+
unless path?
|
367
|
+
path = @underlyingRequest
|
368
|
+
decodeURI(path)
|