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 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
- desc "Run all tests"
6
- task :test => ['test:generation', 'test:recognition', 'test:integration', 'test:examples', 'test:rdoc_examples']
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
- require 'pp'
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)