http_router 0.8.11 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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)
|