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 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)