http_router 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -15,12 +15,12 @@ This is very new code. Lots of stuff probably doesn't work right. I will likely
15
15
  * Request condition support.
16
16
  * Partial matches.
17
17
  * Supports interstitial variables (e.g. /my-:variable-brings.all.the.boys/yard).
18
- * Very fast and small code base (~600 loc).
18
+ * Very fast and small code base (~1,000 loc).
19
19
  * Sinatra compatibility.
20
20
 
21
21
  == Usage
22
22
 
23
- Please see the examples directory for a bunch of awesome rackup file examples, with tonnes of commentary.
23
+ Please see the examples directory for a bunch of awesome rackup file examples, with tonnes of commentary. As well, the rdocs should provide a lot of useful specifics and exact usage.
24
24
 
25
25
  === <tt>HttpRouter.new</tt>
26
26
 
@@ -29,6 +29,7 @@ Takes the following options:
29
29
  * <tt>:default_app</tt> - The default #call made on non-matches. Defaults to a 404 generator.
30
30
  * <tt>:ignore_trailing_slash</tt> - Ignores the trailing slash when matching. Defaults to true.
31
31
  * <tt>:redirect_trailing_slash</tt> - Redirect on trailing slash matches to non-trailing slash paths. Defaults to false.
32
+ * <tt>:middleware</tt> - Perform matching without deferring to matched route. Defaults to false.
32
33
 
33
34
  === <tt>#add(name, options)</tt>
34
35
 
data/Rakefile CHANGED
@@ -15,3 +15,11 @@ begin
15
15
  CodeStats::Tasks.new
16
16
  rescue LoadError
17
17
  end
18
+
19
+ require 'rake/rdoctask'
20
+ desc "Generate documentation"
21
+ Rake::RDocTask.new do |rd|
22
+ rd.main = "README.rdoc"
23
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
24
+ rd.rdoc_dir = 'rdoc'
25
+ end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.4
1
+ 0.1.5
@@ -0,0 +1,46 @@
1
+ require 'http_router'
2
+
3
+ use(HttpRouter, :middleware => true) {
4
+ add('/test').name(:test)
5
+ add('/:variable').name(:var)
6
+ add('/more/*glob').name(:glob)
7
+ add('/get/:id').matching(:id => /\d+/).name(:get)
8
+ }
9
+
10
+ run proc {|env|
11
+ [
12
+ 200,
13
+ {'Content-type' => 'text/plain'},
14
+ [<<-HEREDOC
15
+ We matched? #{env['router.response'] && env['router.response'].matched? ? 'yes!' : 'no'}
16
+ Params are #{env['router.response'] && env['router.response'].matched? ? env['router.response'].params_as_hash.inspect : 'we had no params'}
17
+ That was fun
18
+ HEREDOC
19
+ ]
20
+ ]
21
+ }
22
+
23
+ # crapbook-pro:polleverywhere joshua$ curl http://127.0.0.1:3000/hi
24
+ # We matched? yes!
25
+ # Params are {:variable=>"hi"}
26
+ # That was fun
27
+ # crapbook-pro:polleverywhere joshua$ curl http://127.0.0.1:3000/test
28
+ # We matched? yes!
29
+ # Params are {}
30
+ # That was fun
31
+ # crapbook-pro:polleverywhere joshua$ curl http://127.0.0.1:3000/hey
32
+ # We matched? yes!
33
+ # Params are {:variable=>"hey"}
34
+ # That was fun
35
+ # crapbook-pro:polleverywhere joshua$ curl http://127.0.0.1:3000/more/fun/in/the/sun
36
+ # We matched? yes!
37
+ # Params are {:glob=>["fun", "in", "the", "sun"]}
38
+ # That was fun
39
+ # crapbook-pro:polleverywhere joshua$ curl http://127.0.0.1:3000/get/what
40
+ # We matched? no
41
+ # Params are we had no params
42
+ # That was fun
43
+ # crapbook-pro:polleverywhere joshua$ curl http://127.0.0.1:3000/get/123
44
+ # We matched? yes!
45
+ # Params are {:id=>"123"}
46
+ # That was fun
@@ -0,0 +1,22 @@
1
+ require 'http_router'
2
+ HttpRouter.override_rack_mapper!
3
+
4
+ map('/get/:id') { |env|
5
+ [200, {'Content-type' => 'text/plain'}, ["My id is #{env['router.params'][:id]}\n"]]
6
+ }
7
+
8
+ # you have post, get, head, put and delete.
9
+ post('/get/:id') { |env|
10
+ [200, {'Content-type' => 'text/plain'}, ["My id is #{env['router.params'][:id]} and you posted!\n"]]
11
+ }
12
+
13
+ map('/get/:id', :matching => {:id => /\d+/}) { |env|
14
+ [200, {'Content-type' => 'text/plain'}, ["My id is #{env['router.params'][:id]}, which is a number\n"]]
15
+ }
16
+
17
+ # crapbook-pro:~ joshua$ curl http://127.0.0.1:3000/get/foo
18
+ # My id is foo
19
+ # crapbook-pro:~ joshua$ curl -X POST http://127.0.0.1:3000/get/foo
20
+ # My id is foo and you posted!
21
+ # crapbook-pro:~ joshua$ curl -X POST http://127.0.0.1:3000/get/123
22
+ # My id is 123, which is a number
@@ -1,12 +1,13 @@
1
1
  class HttpRouter
2
2
  class Glob < Variable
3
- def matches(env, parts, whole_path)
4
- if @matches_with && match = @matches_with.match(parts.first)
3
+ def matches?(parts, whole_path)
4
+ @matches_with.nil? or (!parts.empty? and match = @matches_with.match(parts.first) and match.begin(0))
5
+ end
6
+
7
+ def consume(parts, whole_path)
8
+ if @matches_with
5
9
  params = [parts.shift]
6
- while !parts.empty? and match = @matches_with.match(parts.first)
7
- params << parts.shift
8
- end
9
- whole_path.replace(parts.join('/'))
10
+ params << parts.shift while matches?(parts, whole_path)
10
11
  params
11
12
  else
12
13
  params = parts.dup
@@ -3,19 +3,17 @@ class HttpRouter
3
3
  attr_accessor :value, :variable, :catchall
4
4
  attr_reader :linear, :lookup, :request_node, :arbitrary_node
5
5
 
6
- def initialize(base)
7
- @router = base
6
+ def initialize(router)
7
+ @router = router
8
8
  reset!
9
9
  end
10
10
 
11
11
  def reset!
12
- @linear = nil
13
- @lookup = nil
14
- @catchall = nil
12
+ @linear, @lookup, @catchall = nil, nil, nil
15
13
  end
16
14
 
17
15
  def add(val)
18
- if val.is_a?(Variable)
16
+ if val.respond_to?(:matches?)
19
17
  if val.matches_with
20
18
  add_to_linear(val)
21
19
  else
@@ -140,31 +138,30 @@ class HttpRouter
140
138
  end
141
139
 
142
140
  def find_on_parts(request, parts, params)
143
- if !parts.empty?
141
+ unless parts.empty?
142
+ whole_path = parts.join('/')
144
143
  if @linear && !@linear.empty?
145
- whole_path = parts.join('/')
144
+ response = nil
145
+ dupped_parts = nil
146
146
  next_node = @linear.find do |(tester, node)|
147
- if tester.is_a?(Regexp) and match = tester.match(whole_path) #and match.index == 0 TODO
148
- whole_path.slice!(0,match[0].size)
149
- parts.replace(router.split(whole_path))
150
- node
151
- elsif tester.respond_to?(:matches) and new_params = tester.matches(request.env, parts, whole_path)
152
- params << new_params
153
- node
147
+ if tester.respond_to?(:matches?) and tester.matches?(parts, whole_path)
148
+ dupped_parts = parts.dup
149
+ params << tester.consume(dupped_parts, whole_path)
150
+ parts.replace(dupped_parts) if response = node.find_on_parts(request, dupped_parts, params)
151
+ elsif tester.respond_to?(:match) and match = tester.match(whole_path) and match.begin(0) == 0
152
+ dupped_parts = router.split(whole_path[match[0].size, whole_path.size])
153
+ parts.replace(dupped_parts) if response = node.find_on_parts(request, dupped_parts, params)
154
154
  else
155
155
  nil
156
156
  end
157
157
  end
158
- if next_node and match = next_node.last.find_on_parts(request, parts, params)
159
- return match
160
- end
158
+ return response if response
161
159
  end
162
160
  if match = @lookup && @lookup[parts.first]
163
161
  parts.shift
164
162
  return match.find_on_parts(request, parts, params)
165
163
  elsif @catchall
166
- params << @catchall.variable.matches(request.env, parts, whole_path)
167
- parts.shift
164
+ params << @catchall.variable.consume(parts, whole_path)
168
165
  return @catchall.find_on_parts(request, parts, params)
169
166
  elsif parts.size == 1 && parts.first == '' && (value && value.route.trailing_slash_ignore? || router.ignore_trailing_slash?)
170
167
  parts.shift
@@ -0,0 +1,35 @@
1
+ class HttpRouter
2
+ class OptionalCompiler
3
+ attr_reader :paths
4
+ def initialize(path)
5
+ @start_index = 0
6
+ @end_index = 1
7
+ @paths = [""]
8
+ @chars = path.chars.to_a
9
+ while !@chars.empty?
10
+ case @chars.first
11
+ when '(' then @chars.shift and double_paths
12
+ when ')' then @chars.shift and half_paths
13
+ when '\\' then @chars.shift and add_to_current_set(@chars.shift)
14
+ else add_to_current_set(@chars.shift)
15
+ end
16
+ end
17
+ @paths
18
+ end
19
+
20
+ def add_to_current_set(c)
21
+ (@start_index...@end_index).each { |path_index| @paths[path_index] << c }
22
+ end
23
+
24
+ # over current working set, double @paths
25
+ def double_paths
26
+ (@start_index...@end_index).each { |path_index| @paths << @paths[path_index].dup }
27
+ @start_index = @end_index
28
+ @end_index = @paths.size
29
+ end
30
+
31
+ def half_paths
32
+ @start_index -= @end_index - @start_index
33
+ end
34
+ end
35
+ end
@@ -1,14 +1,23 @@
1
1
  require 'cgi'
2
2
  class HttpRouter
3
3
  class Path
4
- attr_reader :parts
5
- attr_accessor :route
6
- def initialize(path, parts)
7
- @path, @parts = path, parts
4
+ attr_reader :parts, :route
5
+ def initialize(route, path, parts)
6
+ @route, @path, @parts = route, path, parts
8
7
  if duplicate_variable_names = variable_names.dup.uniq!
9
8
  raise AmbiguousVariableException.new("You have duplicate variable name present: #{duplicate_variable_names.join(', ')}")
10
9
  end
11
10
 
11
+ @path_validation_regex = path.split(/([:\*][a-zA-Z0-9_]+)/).map{ |part|
12
+ case part[0]
13
+ when ?:, ?*
14
+ route.matches_with[part[1, part.size].to_sym] || '.*?'
15
+ else
16
+ Regexp.quote(part)
17
+ end
18
+ }.join
19
+ @path_validation_regex = Regexp.new("^#{@path_validation_regex}$")
20
+
12
21
  eval_path = path.gsub(/[:\*]([a-zA-Z0-9_]+)/) {"\#{args.shift || (options && options.delete(:#{$1})) || raise(MissingParameterException.new(\"missing parameter #{$1}\"))}" }
13
22
  instance_eval "
14
23
  def raw_url(args,options)
@@ -36,6 +45,7 @@ class HttpRouter
36
45
 
37
46
  def url(args, options)
38
47
  path = raw_url(args, options)
48
+ raise InvalidRouteException.new if path !~ @path_validation_regex
39
49
  raise TooManyParametersException.new unless args.empty?
40
50
  Rack::Utils.uri_escape!(path)
41
51
  generate_querystring(path, options)
@@ -1,10 +1,11 @@
1
1
  class HttpRouter
2
2
  class Route
3
- attr_reader :dest, :paths
4
- attr_accessor :trailing_slash_ignore, :partially_match, :default_values, :router
3
+ attr_reader :dest, :paths, :path, :matches_with
4
+ attr_accessor :trailing_slash_ignore, :partially_match, :default_values
5
5
 
6
- def initialize(base, path)
7
- @router = base
6
+ def initialize(router, path)
7
+ @router = router
8
+ path[0,0] = '/' unless path[0] == ?/
8
9
  @path = path
9
10
  @original_path = path.dup
10
11
  @partially_match = extract_partial_match(path)
@@ -15,15 +16,6 @@ class HttpRouter
15
16
  @default_values = {}
16
17
  end
17
18
 
18
- def significant_variable_names
19
- unless @significant_variable_names
20
- @significant_variable_names = @paths.map { |p| p.variable_names }
21
- @significant_variable_names.flatten!
22
- @significant_variable_names.uniq!
23
- end
24
- @significant_variable_names
25
- end
26
-
27
19
  def method_missing(method, *args, &block)
28
20
  if RequestNode::RequestMethods.include?(method)
29
21
  condition(method => args)
@@ -32,54 +24,89 @@ class HttpRouter
32
24
  end
33
25
  end
34
26
 
27
+ # Returns the options used to create this route.
28
+ def as_options
29
+ {:matching => @matches_with, :conditions => @conditions, :default_values => @default_values, :name => @name}
30
+ end
31
+
32
+ # Creates a deep uncompiled copy of this route.
33
+ def clone
34
+ Route.new(@router, @original_path.dup).with_options(as_options)
35
+ end
36
+
37
+ # Uses an option hash to apply conditions to a Route.
38
+ # The following keys are supported.
39
+ # *name -- Maps to #name method.
40
+ # *matching -- Maps to #matching method.
41
+ # *conditions -- Maps to #conditions method.
42
+ # *default_value -- Maps to #default_value method.
35
43
  def with_options(options)
36
- if options && options[:matching]
37
- default(options[:matching])
38
- end
39
- if options && options[:conditions]
40
- condition(options[:conditions])
41
- end
42
- if options && options[:default_values]
43
- default(options[:default_values])
44
- end
44
+ name(options[:name]) if options && options[:name]
45
+ matching(options[:matching]) if options && options[:matching]
46
+ condition(options[:conditions]) if options && options[:conditions]
47
+ default(options[:default_values]) if options && options[:default_values]
45
48
  self
46
49
  end
47
50
 
51
+ # Sets the name of the route
52
+ # Returns +self+.
48
53
  def name(name)
49
54
  @name = name
50
55
  router.named_routes[@name] = self if @name && compiled?
51
56
  self
52
57
  end
53
58
 
59
+ # Sets a default value for the route
60
+ # Returns +self+.
61
+ #
62
+ # Example
63
+ # router = HttpRouter.new
64
+ # router.add("/:test").default(:test => 'foo').name(:test).compile
65
+ # router.url(:test)
66
+ # # ==> "/foo"
67
+ # router.url(:test, 'override')
68
+ # # ==> "/override"
54
69
  def default(v)
55
70
  @default_values.merge!(v)
56
71
  self
57
72
  end
58
73
 
74
+ # Causes this route to recognize the GET and HEAD request methods. Returns +self+.
59
75
  def get
60
76
  request_method('GET', 'HEAD')
61
77
  end
62
78
 
79
+ # Causes this route to recognize the POST request method. Returns +self+.
63
80
  def post
64
81
  request_method('POST')
65
82
  end
66
83
 
84
+ # Causes this route to recognize the HEAD request method. Returns +self+.
67
85
  def head
68
86
  request_method('HEAD')
69
87
  end
70
88
 
89
+ # Causes this route to recognize the PUT request method. Returns +self+.
71
90
  def put
72
91
  request_method('PUT')
73
92
  end
74
93
 
94
+ # Causes this route to recognize the DELETE request method. Returns +self+.
75
95
  def delete
76
96
  request_method('DELETE')
77
97
  end
78
98
 
99
+ # Causes this route to recognize the GET request method. Returns +self+.
79
100
  def only_get
80
- request_method('DELETE')
101
+ request_method('GET')
81
102
  end
82
103
 
104
+ # Sets a request condition for the route
105
+ # Returns +self+.
106
+ #
107
+ # Example
108
+ # router = HttpRouter.new
109
+ # router.add("/:test").condition(:host => 'www.example.org').name(:test).compile
83
110
  def condition(conditions)
84
111
  guard_compiled
85
112
  conditions.each do |k,v|
@@ -91,45 +118,63 @@ class HttpRouter
91
118
  end
92
119
  alias_method :conditions, :condition
93
120
 
121
+ # Sets a regex matcher for a variable
122
+ # Returns +self+.
123
+ #
124
+ # Example
125
+ # router = HttpRouter.new
126
+ # router.add("/:test").matching(:test => /\d+/).name(:test).compile
94
127
  def matching(match)
95
128
  guard_compiled
96
129
  match.each do |var_name, matchers|
97
130
  matchers = Array(matchers)
98
131
  matchers.each do |m|
99
- @matches_with.key?(var_name) ?
100
- raise :
101
- @matches_with[var_name] = m
132
+ @matches_with.key?(var_name) ? raise : @matches_with[var_name] = m
102
133
  end
103
134
  end
104
135
  self
105
136
  end
106
137
 
138
+ # Returns the current route's name.
107
139
  def named
108
140
  @name
109
141
  end
110
142
 
143
+ # Sets the destination of the route. Receives either a block, or a proc.
144
+ # Returns +self+.
145
+ #
146
+ # Example
147
+ # router = HttpRouter.new
148
+ # router.add("/:test").matching(:test => /\d+/).name(:test).to(proc{ |env| Rack::Response.new("hi there").finish })
149
+ # Or
150
+ # router.add("/:test").matching(:test => /\d+/).name(:test).to { |env| Rack::Response.new("hi there").finish }
111
151
  def to(dest = nil, &block)
112
152
  compile
113
153
  @dest = dest || block
114
154
  self
115
155
  end
116
156
 
157
+ # Sets partial matching on this route. Defaults to +true+. Returns +self+.
117
158
  def partial(match = true)
118
159
  @partially_match = match
119
160
  self
120
161
  end
121
162
 
163
+ # Adds an arbitrary proc matcher to a Route. Receives either a block, or a proc. The proc will receive a Rack::Request object and must return true for the Route to be matched. Returns +self+.
122
164
  def arbitrary(proc = nil, &block)
123
165
  @arbitrary << (proc || block)
124
166
  self
125
167
  end
126
168
 
169
+ # Compile state for route. Returns +true+ or +false+.
127
170
  def compiled?
128
171
  !@paths.nil?
129
172
  end
130
173
 
131
- def compile(force = false)
132
- if force || @paths.nil?
174
+ # Compiles the route and inserts it into the tree. This is called automatically when you add a destination via #to to the route. Until a route
175
+ # is compiled, it will not be recognized.
176
+ def compile
177
+ if @paths.nil?
133
178
  router.named_routes[@name] = self if @name
134
179
  @paths = compile_paths
135
180
  @paths.each_with_index do |p1, i|
@@ -138,7 +183,6 @@ class HttpRouter
138
183
  end
139
184
  end
140
185
  @paths.each do |path|
141
- path.route = self
142
186
  current_node = router.root.add_path(path)
143
187
  working_set = current_node.add_request_methods(@conditions)
144
188
  working_set.map!{|node| node.add_arbitrary(@arbitrary)}
@@ -150,8 +194,8 @@ class HttpRouter
150
194
  self
151
195
  end
152
196
 
197
+ # Sets the destination of this route to redirect to an arbitrary URL.
153
198
  def redirect(path, status = 302)
154
- guard_compiled
155
199
  raise(ArgumentError, "Status has to be an integer between 300 and 399") unless (300..399).include?(status)
156
200
  to { |env|
157
201
  params = env['router.params']
@@ -162,9 +206,8 @@ class HttpRouter
162
206
  self
163
207
  end
164
208
 
209
+ # Sets the destination of this route to serve static files from either a directory or a single file.
165
210
  def static(root)
166
- guard_compiled
167
- raise AlreadyCompiledException.new if compiled?
168
211
  if File.directory?(root)
169
212
  partial.to ::Rack::File.new(root)
170
213
  else
@@ -173,14 +216,17 @@ class HttpRouter
173
216
  self
174
217
  end
175
218
 
219
+ # The current state of trailing / ignoring on this route. Returns +true+ or +false+.
176
220
  def trailing_slash_ignore?
177
221
  @trailing_slash_ignore
178
222
  end
179
223
 
224
+ # The current state of partial matching on this route. Returns +true+ or +false+.
180
225
  def partially_match?
181
226
  @partially_match
182
227
  end
183
228
 
229
+ # Generates a URL for this route. See HttpRouter#url for how the arguments for this are structured.
184
230
  def url(*args)
185
231
  options = args.last.is_a?(Hash) ? args.pop : nil
186
232
  options ||= {} if default_values
@@ -229,41 +275,12 @@ class HttpRouter
229
275
  path[-2, 2] == '/?' && path.slice!(-2, 2)
230
276
  end
231
277
 
232
- def compile_optionals(path)
233
- start_index = 0
234
- end_index = 1
235
-
236
- paths = [""]
237
- chars = path.split('')
238
-
239
- chars.each do |c|
240
- case c
241
- when '('
242
- # over current working set, double paths
243
- (start_index...end_index).each do |path_index|
244
- paths << paths[path_index].dup
245
- end
246
- start_index = end_index
247
- end_index = paths.size
248
- when ')'
249
- start_index -= end_index - start_index
250
- else
251
- (start_index...end_index).each do |path_index|
252
- paths[path_index] << c
253
- end
254
- end
255
- end
256
- paths
257
- end
258
-
259
278
  def compile_paths
260
- paths = compile_optionals(@path)
279
+ paths = HttpRouter::OptionalCompiler.new(@path).paths
261
280
  paths.map do |path|
262
281
  original_path = path.dup
263
- index = -1
264
282
  split_path = router.split(path)
265
283
  new_path = split_path.map do |part|
266
- index += 1
267
284
  case part
268
285
  when /^:([a-zA-Z_0-9]+)$/
269
286
  v_name = $1.to_sym
@@ -276,7 +293,7 @@ class HttpRouter
276
293
  end
277
294
  end
278
295
  new_path.flatten!
279
- Path.new(original_path, new_path)
296
+ Path.new(self, original_path, new_path)
280
297
  end
281
298
  end
282
299
 
@@ -309,5 +326,14 @@ class HttpRouter
309
326
  def guard_compiled
310
327
  raise AlreadyCompiledException.new if compiled?
311
328
  end
329
+
330
+ def significant_variable_names
331
+ unless @significant_variable_names
332
+ @significant_variable_names = @paths.map { |p| p.variable_names }
333
+ @significant_variable_names.flatten!
334
+ @significant_variable_names.uniq!
335
+ end
336
+ @significant_variable_names
337
+ end
312
338
  end
313
339
  end
@@ -2,19 +2,23 @@ class HttpRouter
2
2
  class Variable
3
3
  attr_reader :name, :matches_with
4
4
 
5
- def initialize(base, name, matches_with = nil)
6
- @router = base
5
+ def initialize(router, name, matches_with = nil)
6
+ @router = router
7
7
  @name = name
8
8
  @matches_with = matches_with
9
9
  end
10
10
 
11
- def matches(env, parts, whole_path)
12
- if @matches_with.nil?
13
- parts.first
14
- elsif @matches_with and match = @matches_with.match(whole_path)
15
- whole_path.slice!(0, match[0].size)
16
- parts.replace(router.split(whole_path))
11
+ def matches?(parts, whole_path)
12
+ @matches_with.nil? or (@matches_with and match = @matches_with.match(whole_path) and match.begin(0) == 0)
13
+ end
14
+
15
+ def consume(parts, whole_path)
16
+ if @matches_with
17
+ match = @matches_with.match(whole_path)
18
+ parts.replace(router.split(whole_path[match.end(0), whole_path.size]))
17
19
  match[0]
20
+ else
21
+ parts.shift
18
22
  end
19
23
  end
20
24
 
data/lib/http_router.rb CHANGED
@@ -3,93 +3,167 @@ require 'rack'
3
3
  require 'ext/rack/uri_escape'
4
4
 
5
5
  class HttpRouter
6
- autoload :Node, 'http_router/node'
7
- autoload :Root, 'http_router/root'
8
- autoload :Variable, 'http_router/variable'
9
- autoload :Glob, 'http_router/glob'
10
- autoload :Route, 'http_router/route'
11
- autoload :Response, 'http_router/response'
12
- autoload :Path, 'http_router/path'
6
+ autoload :Node, 'http_router/node'
7
+ autoload :Root, 'http_router/root'
8
+ autoload :Variable, 'http_router/variable'
9
+ autoload :Glob, 'http_router/glob'
10
+ autoload :Route, 'http_router/route'
11
+ autoload :Response, 'http_router/response'
12
+ autoload :Path, 'http_router/path'
13
+ autoload :OptionalCompiler, 'http_router/optional_compiler'
13
14
 
15
+ # Raised when a Route is not able to be generated.
14
16
  UngeneratableRouteException = Class.new(RuntimeError)
17
+ # Raised when a Route is not able to be generated due to a missing parameter.
15
18
  MissingParameterException = Class.new(RuntimeError)
19
+ # Raised when a Route is generated that isn't valid.
20
+ InvalidRouteException = Class.new(RuntimeError)
21
+ # Raised when a Route is not able to be generated due to too many parameters being passed in.
16
22
  TooManyParametersException = Class.new(RuntimeError)
23
+ # Raised when an already inserted Route has more conditions added.
17
24
  AlreadyCompiledException = Class.new(RuntimeError)
25
+ # Raised when an ambiguous Route is added. For example, this will be raised if you attempt to add "/foo(/:bar)(/:baz)".
18
26
  AmbiguousRouteException = Class.new(RuntimeError)
27
+ # Raised when a request condition is added that is not recognized.
19
28
  UnsupportedRequestConditionError = Class.new(RuntimeError)
29
+ # Raised when there is a potential conflict of variable names within your Route.
20
30
  AmbiguousVariableException = Class.new(RuntimeError)
21
31
 
22
32
  attr_reader :named_routes, :routes, :root
23
33
 
34
+ # Monkey-patches Rack::Builder to use HttpRouter.
35
+ # See examples/rack_mapper.rb
24
36
  def self.override_rack_mapper!
25
37
  require File.join('ext', 'rack', 'rack_mapper')
26
38
  end
27
39
 
28
- def initialize(options = nil, &block)
29
- @options = options
30
- @default_app = options && options[:default_app] || proc{|env| ::Rack::Response.new("Not Found", 404).finish }
40
+ # Creates a new HttpRouter.
41
+ # Can be called with either <tt>HttpRouter.new(proc{|env| ... }, { .. options .. })</tt> or with the first argument omitted.
42
+ # If there is a proc first, then it's used as the default app in the case of a non-match.
43
+ # Supported options are
44
+ # * :default_app -- Default application used if there is a non-match on #call. Defaults to 404 generator.
45
+ # * :ignore_trailing_slash -- Ignore a trailing / when attempting to match. Defaults to +true+.
46
+ # * :redirect_trailing_slash -- On trailing /, redirect to the same path without the /. Defaults to +false+.
47
+ # * :middleware -- On recognition, store the route Response in env['router.response'] and always call the default app. Defaults to +false+.
48
+ def initialize(*args, &block)
49
+ default_app, options = args.first.is_a?(Hash) ? [nil, args.first] : [args.first, args[1]]
50
+
51
+ @options = options
52
+ @default_app = default_app || options && options[:default_app] || proc{|env| Rack::Response.new("Not Found", 404).finish }
31
53
  @ignore_trailing_slash = options && options.key?(:ignore_trailing_slash) ? options[:ignore_trailing_slash] : true
32
54
  @redirect_trailing_slash = options && options.key?(:redirect_trailing_slash) ? options[:redirect_trailing_slash] : false
33
- @routes = []
34
- @named_routes = {}
35
- @init_block = block
55
+ @middleware = options && options.key?(:middleware) ? options[:middleware] : false
56
+ @routes = []
57
+ @named_routes = {}
58
+ @init_block = block
36
59
  reset!
37
- instance_eval(&block) if block
60
+ if block
61
+ instance_eval(&block)
62
+ @routes.each {|r| r.compile}
63
+ end
38
64
  end
39
65
 
66
+ # Ignore trailing slash feature enabled? See #initialize for details.
40
67
  def ignore_trailing_slash?
41
68
  @ignore_trailing_slash
42
69
  end
43
70
 
71
+ # Redirect trailing slash feature enabled? See #initialize for details.
44
72
  def redirect_trailing_slash?
45
73
  @redirect_trailing_slash
46
74
  end
47
75
 
76
+ # Resets the router to a clean state.
48
77
  def reset!
49
78
  @root = Root.new(self)
50
79
  @routes.clear
51
80
  @named_routes.clear
52
81
  end
53
82
 
83
+ # Assigns the default application.
54
84
  def default(app)
55
85
  @default_app = app
56
86
  end
57
87
 
58
- def split(path)
59
- (path[0] == ?/ ? path[1, path.size] : path).split('/')
88
+ # Adds a path to be recognized.
89
+ #
90
+ # To assign a part of the path to a specific variable, use :variable_name within the route.
91
+ # For example, <tt>add('/path/:id')</tt> would match <tt>/path/test</tt>, with the variable <tt>:id</tt> having the value <tt>"test"</tt>.
92
+ #
93
+ # You can receive mulitple parts into a single variable by using the glob syntax.
94
+ # For example, <tt>add('/path/*id')</tt> would match <tt>/path/123/456/789</tt>, with the variable <tt>:id</tt> having the value <tt>["123", "456", "789"]</tt>.
95
+ #
96
+ # As well, paths can end with two optional parts, <tt>*</tt> and <tt>/?</tt>. If it ends with a <tt>*</tt>, it will match partially, returning the part of the path unmatched in the PATH_INFO value of the env. The part matched to will be returned in the SCRIPT_NAME. If it ends with <tt>/?</tt>, then a trailing / on the path will be optionally matched for that specific route. As trailing /'s are ignored by default, you probably don't actually want to use this option that frequently.
97
+ #
98
+ # Routes can also contain optional parts. There are surrounded with <tt>( )</tt>'s. If you need to match on a bracket in the route itself, you can escape the parentheses with a backslash.
99
+ #
100
+ # The second argument, options, is an optional hash that can modify the route in further ways. See HttpRouter::Route#with_options for details. Typically, you want to add further options to the route by calling additional methods on it. See HttpRouter::Route for further details.
101
+ #
102
+ # Returns the route object.
103
+ def add(path, options = nil)
104
+ add_route Route.new(self, path.dup).with_options(options)
60
105
  end
61
106
 
62
- def add(path, options = nil)
63
- route = Route.new(self, path.dup).with_options(options)
107
+ # Adds a route to be recognized. This must be a HttpRouter::Route object. Returns the route just added.
108
+ def add_route(route)
64
109
  @routes << route
65
110
  route
66
111
  end
67
112
 
113
+ # Adds a path that only responds to the request methods +GET+ and +HEAD+.
114
+ #
115
+ # Returns the route object.
68
116
  def get(path, options = nil)
69
117
  add(path, options).get
70
118
  end
71
119
 
120
+ # Adds a path that only responds to the request method +POST+.
121
+ #
122
+ # Returns the route object.
72
123
  def post(path, options = nil)
73
124
  add(path, options).post
74
125
  end
75
126
 
127
+ # Adds a path that only responds to the request method +PUT+.
128
+ #
129
+ # Returns the route object.
76
130
  def put(path, options = nil)
77
131
  add(path, options).put
78
132
  end
79
133
 
134
+ # Adds a path that only responds to the request method +DELETE+.
135
+ #
136
+ # Returns the route object.
80
137
  def delete(path, options = nil)
81
138
  add(path, options).delete
82
139
  end
83
140
 
141
+ # Adds a path that only responds to the request method +GET+.
142
+ #
143
+ # Returns the route object.
84
144
  def only_get(path, options = nil)
85
145
  add(path, options).only_get
86
146
  end
87
147
 
148
+ # Returns the HttpRouter::Response object if the env is matched, otherwise, returns +nil+.
88
149
  def recognize(env)
89
150
  response = @root.find(env.is_a?(Hash) ? Rack::Request.new(env) : env)
90
151
  end
91
152
 
92
- # Generate a URL for a specified route.
153
+ # Generate a URL for a specified route. This will accept a list of variable values plus any other variable names named as a hash.
154
+ # This first value must be either the Route object or the name of the route.
155
+ #
156
+ # Example:
157
+ # router = HttpRouter.new
158
+ # router.add('/:foo.:format).name(:test).compile
159
+ # router.url(:test, 123, 'html')
160
+ # # ==> "/123.html"
161
+ # router.url(:test, 123, :format => 'html')
162
+ # # ==> "/123.html"
163
+ # router.url(:test, :foo => 123, :format => 'html')
164
+ # # ==> "/123.html"
165
+ # router.url(:test, :foo => 123, :format => 'html', :fun => 'inthesun')
166
+ # # ==> "/123.html?fun=inthesun"
93
167
  def url(route, *args)
94
168
  case route
95
169
  when Symbol
@@ -101,7 +175,10 @@ class HttpRouter
101
175
  end
102
176
  end
103
177
 
104
- # Allow the router to be called via Rake / Middleware.
178
+ # Rack compatible #call. If matching route is found, and +dest+ value responds to #call, processing will pass to the matched route. Otherwise,
179
+ # the default application will be called. The router will be available in the env under the key <tt>router</tt>. And parameters matched will
180
+ # be available under the key <tt>router.params</tt>. The HttpRouter::Response object will be available under the key <tt>router.response</tt> if
181
+ # a response is available.
105
182
  def call(env)
106
183
  request = Rack::Request.new(env)
107
184
  if redirect_trailing_slash? && (request.head? || request.get?) && request.path_info[-1] == ?/
@@ -110,7 +187,7 @@ class HttpRouter
110
187
  response.finish
111
188
  else
112
189
  env['router'] = self
113
- if response = recognize(request)
190
+ if response = recognize(request) and !@middleware
114
191
  if response.matched? && response.route.dest && response.route.dest.respond_to?(:call)
115
192
  process_params(env, response)
116
193
  consume_path!(request, response) if response.partial_match?
@@ -119,6 +196,7 @@ class HttpRouter
119
196
  return [response.status, response.headers, []]
120
197
  end
121
198
  end
199
+ env['router.response'] = response
122
200
  @default_app.call(env)
123
201
  end
124
202
  end
@@ -147,15 +225,18 @@ class HttpRouter
147
225
  Glob.new(self, *args)
148
226
  end
149
227
 
150
- def dup
151
- dup_router = HttpRouter.new(@options, &@init_block)
152
- @routes.each do |r|
153
- new_route = r.dup
154
- new_route.router = dup_router
155
- dup_router.routes << new_route
156
- new_route.compile(true)
228
+ # Creates a deep-copy of the router.
229
+ def clone
230
+ cloned_router = HttpRouter.new(@default_app, @options, &@init_block)
231
+ @routes.each do |route|
232
+ new_route = route.clone
233
+ new_route.instance_variable_set(:@router, cloned_router)
157
234
  end
158
- dup_router
235
+ cloned_router
236
+ end
237
+
238
+ def split(path)
239
+ (path[0] == ?/ ? path[1, path.size] : path).split('/')
159
240
  end
160
241
 
161
242
  private
@@ -129,5 +129,13 @@ describe "HttpRouter#generate" do
129
129
  @router.url(:test, 123).should == "/123?page=1"
130
130
  end
131
131
  end
132
+
133
+ context "with a matching" do
134
+ it "should raise an exception when the route is invalid" do
135
+ @router.add("/:var").matching(:var => /\d+/).name(:test).compile
136
+ proc{@router.url(:test, 'asd')}.should raise_error(HttpRouter::InvalidRouteException)
137
+ end
138
+ end
139
+
132
140
  end
133
141
  end
data/spec/misc_spec.rb CHANGED
@@ -5,12 +5,13 @@ describe "HttpRouter" do
5
5
 
6
6
  context "route adding" do
7
7
  it "should work with options too" do
8
- route = @router.add('/:test', :conditions => {:request_method => %w{HEAD GET}, :host => 'host1'}, :default_values => {:page => 1}, :matching => {:test => /^\d+/}).to :test
9
- @router.recognize(Rack::MockRequest.env_for('http://host2/variable', :method => 'POST')).matched?.should be_false
10
- @router.recognize(Rack::MockRequest.env_for('http://host1/variable', :method => 'POST')).matched?.should be_false
11
- @router.recognize(Rack::MockRequest.env_for('http://host2/123', :method => 'POST')).matched?.should be_false
12
- @router.recognize(Rack::MockRequest.env_for('http://host1/123', :method => 'POST')).matched?.should be_false
13
- @router.recognize(Rack::MockRequest.env_for('http://host1/123', :method => 'GET')).route.dest.should == :test
8
+ route = @router.add('/:test', :conditions => {:request_method => %w{HEAD GET}, :host => 'host1'}, :default_values => {:page => 1}, :matching => {:test => /\d+/}, :name => :foobar).to :test
9
+ @router.recognize(Rack::MockRequest.env_for('http://host2/variable', :method => 'POST')).should be_nil
10
+ @router.recognize(Rack::MockRequest.env_for('http://host1/variable', :method => 'POST')).should be_nil
11
+ @router.recognize(Rack::MockRequest.env_for('http://host2/123', :method => 'POST')).matched?.should be_false
12
+ @router.recognize(Rack::MockRequest.env_for('http://host1/123', :method => 'POST')).matched?.should be_false
13
+ @router.recognize(Rack::MockRequest.env_for('http://host1/123', :method => 'GET' )).route.dest.should == :test
14
+ @router.url(:foobar, '123').should == '/123?page=1'
14
15
  end
15
16
  end
16
17
 
@@ -34,24 +35,30 @@ describe "HttpRouter" do
34
35
  it "should raise on unsupported request methods" do
35
36
  proc {@router.add("/").condition(:flibberty => 'gibet').compile}.should raise_error(HttpRouter::UnsupportedRequestConditionError)
36
37
  end
37
-
38
38
  end
39
39
 
40
- context "dupping" do
41
- it "run the routes" do
42
- pending
40
+ context "cloning" do
41
+ it "clone the routes" do
43
42
  r1 = HttpRouter.new {
44
43
  add('/test').name(:test_route).to :test
45
44
  }
46
- r2 = r1.dup
47
- r2.add('/test2').to(:test2)
45
+ r2 = r1.clone
46
+
47
+ r2.add('/test2').name(:test).to(:test2)
48
+ r2.routes.size.should == 2
49
+
48
50
  r1.recognize(Rack::MockRequest.env_for('/test2')).should be_nil
49
51
  r2.recognize(Rack::MockRequest.env_for('/test2')).should_not be_nil
50
-
51
52
  r1.named_routes[:test_route].should == r1.routes.first
52
53
  r2.named_routes[:test_route].should == r2.routes.first
54
+
55
+ r1.add('/another').name(:test).to(:test2)
56
+
57
+ r1.routes.size.should == r2.routes.size
58
+ r1.url(:test).should == '/another'
59
+ r2.url(:test).should == '/test2'
60
+ r1.routes.first.dest.should == :test
61
+ r2.routes.first.dest.should == :test
53
62
  end
54
63
  end
55
-
56
-
57
64
  end
@@ -0,0 +1,20 @@
1
+ describe "HttpRouter as middleware" do
2
+ before(:each) do
3
+ @builder = Rack::Builder.new do
4
+ use(HttpRouter, :middleware => true) {
5
+ add('/test').name(:test).to(:test)
6
+ }
7
+ end
8
+ end
9
+
10
+ it "should always have the router" do
11
+ @builder.run proc{|env| [200, {}, [env['router'].url(:test)]]}
12
+ @builder.call(Rack::MockRequest.env_for('/some-path')).last.join.should == '/test'
13
+ end
14
+
15
+ it "should stash the match if it exists" do
16
+ @builder.run proc{|env| [200, {}, [env['router.response'].dest.to_s]]}
17
+ @builder.call(Rack::MockRequest.env_for('/test')).last.join.should == 'test'
18
+ end
19
+ end
20
+
@@ -3,14 +3,14 @@ describe "HttpRouter#recognize" do
3
3
  @router = HttpRouter.new
4
4
  end
5
5
 
6
- context("static paths") do
6
+ context("with static paths") do
7
7
  ['/', '/test', '/test/time', '/one/more/what', '/test.html'].each do |path|
8
8
  it "should recognize #{path.inspect}" do
9
9
  route = @router.add(path).to(path)
10
10
  @router.recognize(Rack::MockRequest.env_for(path)).route.should == route
11
11
  end
12
12
  end
13
-
13
+
14
14
  context("with optional parts") do
15
15
  it "work either way" do
16
16
  route = @router.add("/test(/optional)").to(:test)
@@ -19,7 +19,16 @@ describe "HttpRouter#recognize" do
19
19
  end
20
20
  end
21
21
 
22
- context("partial matching") do
22
+ context("with escaped ()'s") do
23
+ it "should recognize ()" do
24
+ route = @router.add('/test\(:variable\)').to(:test)
25
+ response = @router.recognize(Rack::MockRequest.env_for('/test(hello)'))
26
+ response.route.should == route
27
+ response.params.first.should == 'hello'
28
+ end
29
+ end
30
+
31
+ context("with partial matching") do
23
32
  it "should match partially or completely" do
24
33
  route = @router.add("/test*").to(:test)
25
34
  @router.recognize(Rack::MockRequest.env_for('/test')).route.should == route
@@ -29,8 +38,8 @@ describe "HttpRouter#recognize" do
29
38
  end
30
39
  end
31
40
 
32
- context("proc acceptance") do
33
- it "should match optionally with a proc" do
41
+ context("with proc acceptance") do
42
+ it "should match" do
34
43
  @router.add("/test").arbitrary(Proc.new{|req| req.host == 'hellodooly' }).to(:test1)
35
44
  @router.add("/test").arbitrary(Proc.new{|req| req.host == 'lovelove' }).arbitrary{|req| req.port == 80}.to(:test2)
36
45
  @router.add("/test").arbitrary(Proc.new{|req| req.host == 'lovelove' }).arbitrary{|req| req.port == 8080}.to(:test3)
@@ -47,7 +56,7 @@ describe "HttpRouter#recognize" do
47
56
  response.dest.should == :test4
48
57
  end
49
58
 
50
- it "should match optionally with a proc and request conditions" do
59
+ it "should match with request conditions" do
51
60
  @router.add("/test").get.arbitrary(Proc.new{|req| req.host == 'lovelove' }).arbitrary{|req| req.port == 80}.to(:test1)
52
61
  @router.add("/test").get.arbitrary(Proc.new{|req| req.host == 'lovelove' }).arbitrary{|req| req.port == 8080}.to(:test2)
53
62
  response = @router.recognize(Rack::MockRequest.env_for('http://lovelove:8080/test'))
@@ -64,33 +73,32 @@ describe "HttpRouter#recognize" do
64
73
 
65
74
  end
66
75
 
67
- context("trailing slashes") do
68
- it "should ignore a trailing slash" do
76
+ context("with trailing slashes") do
77
+ it "should ignore" do
69
78
  route = @router.add("/test").to(:test)
70
79
  @router.recognize(Rack::MockRequest.env_for('/test/')).route.should == route
71
80
  end
72
81
 
73
- it "should not recognize a trailing slash when used with the /? syntax and ignore_trailing_slash disabled" do
82
+ it "should not recognize when used with the /? syntax and ignore_trailing_slash disabled" do
74
83
  @router = HttpRouter.new(:ignore_trailing_slash => false)
75
84
  route = @router.add("/test/?").to(:test)
76
85
  @router.recognize(Rack::MockRequest.env_for('/test/')).route.should == route
77
86
  end
78
87
 
79
- it "should recognize a trailing slash when used with the /? syntax and ignore_trailing_slash enabled" do
88
+ it "should recognize when used with the /? syntax and ignore_trailing_slash enabled" do
80
89
  @router = HttpRouter.new(:ignore_trailing_slash => false)
81
90
  route = @router.add("/test").to(:test)
82
91
  @router.recognize(Rack::MockRequest.env_for('/test/')).should be_nil
83
92
  end
84
- it "should not capture the trailing slash in a variable normally" do
93
+ it "should not capture normally" do
85
94
  route = @router.add("/:test").to(:test)
86
95
  @router.recognize(Rack::MockRequest.env_for('/test/')).params.first.should == 'test'
87
96
  end
88
97
  end
89
-
90
98
  end
91
99
 
92
- context "variables" do
93
- it "should recognize a simple variable" do
100
+ context "with variables" do
101
+ it "should recognize" do
94
102
  @router.add("/foo").to(:test1)
95
103
  @router.add("/foo/:id").to(:test2)
96
104
  @router.recognize(Rack::MockRequest.env_for('/foo')).dest.should == :test1
@@ -98,15 +106,24 @@ describe "HttpRouter#recognize" do
98
106
  end
99
107
  end
100
108
 
101
- context "request methods" do
102
- it "should pick a specific request_method" do
109
+ context "with missing leading /" do
110
+ it "should recognize" do
111
+ @router.add("foo").to(:test1)
112
+ @router.add("foo.html").to(:test2)
113
+ @router.recognize(Rack::MockRequest.env_for('/foo')).dest.should == :test1
114
+ @router.recognize(Rack::MockRequest.env_for('/foo.html')).dest.should == :test2
115
+ end
116
+ end
117
+
118
+ context "with request methods" do
119
+ it "should recognize" do
103
120
  route = @router.post("/test").to(:test)
104
121
  @router.recognize(Rack::MockRequest.env_for('/test', :method => 'POST')).route.should == route
105
122
  @router.recognize(Rack::MockRequest.env_for('/test', :method => 'GET')).status.should == 405
106
123
  @router.recognize(Rack::MockRequest.env_for('/test', :method => 'GET')).headers['Allow'].should == "POST"
107
124
  end
108
125
 
109
- it "should pick a specific request_method with other paths all through it" do
126
+ it "should recognize deeply" do
110
127
  @router.post("/test").to(:test_post)
111
128
  @router.post("/test/post").to(:test_post_post)
112
129
  @router.get("/test").to(:test_get)
@@ -136,7 +153,7 @@ describe "HttpRouter#recognize" do
136
153
 
137
154
  end
138
155
 
139
- context("dynamic paths") do
156
+ context("with dynamic paths") do
140
157
  it "should recognize '/:variable'" do
141
158
  route = @router.add('/:variable').to(:test)
142
159
  response = @router.recognize(Rack::MockRequest.env_for('/value'))
@@ -183,15 +200,15 @@ describe "HttpRouter#recognize" do
183
200
  response.params_as_hash[:test].should == 'hey'
184
201
  end
185
202
 
186
- context "globs" do
187
- it "should recognize a glob" do
203
+ context "with globs" do
204
+ it "should recognize" do
188
205
  route = @router.add('/test/*variable').to(:test)
189
206
  response = @router.recognize(Rack::MockRequest.env_for('/test/one/two/three'))
190
207
  response.route.should == route
191
208
  response.params.should == [['one', 'two', 'three']]
192
209
  end
193
- it "should recognize a glob with a regexp" do
194
- route = @router.add('/test/*variable/anymore').matching(:variable => /^\d+$/).to(:test)
210
+ it "should recognize with a regexp" do
211
+ route = @router.add('/test/*variable/anymore').matching(:variable => /\d+/).to(:test)
195
212
  response = @router.recognize(Rack::MockRequest.env_for('/test/123/345/567/anymore'))
196
213
  response.route.should == route
197
214
  response.params.should == [['123', '345', '567']]
@@ -202,23 +219,23 @@ describe "HttpRouter#recognize" do
202
219
 
203
220
  end
204
221
 
205
- context("interstitial variables") do
206
- it "should recognize interstitial variables" do
222
+ context("with interstitial variables") do
223
+ it "should recognize" do
207
224
  route = @router.add('/one-:variable-time').to(:test)
208
225
  response = @router.recognize(Rack::MockRequest.env_for('/one-value-time'))
209
226
  response.route.should == route
210
227
  response.params_as_hash[:variable].should == 'value'
211
228
  end
212
229
 
213
- it "should recognize interstitial variables with a regex" do
214
- route = @router.add('/one-:variable-time').matching(:variable => /^\d+/).to(:test)
230
+ it "should recognize with a regex" do
231
+ route = @router.add('/one-:variable-time').matching(:variable => /\d+/).to(:test)
215
232
  @router.recognize(Rack::MockRequest.env_for('/one-value-time')).should be_nil
216
233
  response = @router.recognize(Rack::MockRequest.env_for('/one-123-time'))
217
234
  response.route.should == route
218
235
  response.params_as_hash[:variable].should == '123'
219
236
  end
220
237
 
221
- it "should recognize interstitial variable when there is an extension" do
238
+ it "should recognize when there is an extension" do
222
239
  route = @router.add('/hey.:greed.html').to(:test)
223
240
  response = @router.recognize(Rack::MockRequest.env_for('/hey.greedyboy.html'))
224
241
  response.route.should == route
@@ -227,8 +244,8 @@ describe "HttpRouter#recognize" do
227
244
 
228
245
  end
229
246
 
230
- context("dynamic greedy paths") do
231
- it "should recognize greedy variables" do
247
+ context("with dynamic greedy paths") do
248
+ it "should recognize" do
232
249
  route = @router.add('/:variable').matching(:variable => /\d+/).to(:test)
233
250
  response = @router.recognize(Rack::MockRequest.env_for('/123'))
234
251
  response.route.should == route
@@ -238,12 +255,23 @@ describe "HttpRouter#recognize" do
238
255
  response.should be_nil
239
256
  end
240
257
 
241
- it "should capture the trailing slash in a greedy variable" do
258
+ it "should continue on with normal if regex fails to match" do
259
+ @router.add("/:test/number").matching(:test => /\d+/).to(:test_number)
260
+ target = @router.add("/:test/anything").to(:test_anything)
261
+ @router.recognize(Rack::MockRequest.env_for('/123/anything')).route.should == target
262
+ end
263
+
264
+ it "should capture the trailing slash" do
242
265
  route = @router.add("/:test").matching(:test => /.*/).to(:test)
243
266
  @router.recognize(Rack::MockRequest.env_for('/test/')).params.first.should == 'test/'
244
267
  end
245
268
 
246
- it "should capture the extension in a greedy variable" do
269
+ it "should require the match to begin at the beginning" do
270
+ route = @router.add("/:test").matching(:test => /\d+/).to(:test)
271
+ @router.recognize(Rack::MockRequest.env_for('/a123')).should be_nil
272
+ end
273
+
274
+ it "should capture the extension" do
247
275
  route = @router.add("/:test").matching(:test => /.*/).to(:test)
248
276
  @router.recognize(Rack::MockRequest.env_for('/test.html')).params.first.should == 'test.html'
249
277
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http_router
3
3
  version: !ruby/object:Gem::Version
4
- hash: 19
4
+ hash: 17
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 4
10
- version: 0.1.4
9
+ - 5
10
+ version: 0.1.5
11
11
  platform: ruby
12
12
  authors:
13
13
  - Joshua Hull
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-05-30 00:00:00 -04:00
18
+ date: 2010-05-30 00:00:00 +09:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -54,8 +54,9 @@ files:
54
54
  - benchmarks/rec2.rb
55
55
  - benchmarks/recognition_bm.rb
56
56
  - examples/glob.ru
57
+ - examples/middleware.ru
58
+ - examples/rack_mapper.ru
57
59
  - examples/simple.ru
58
- - examples/simple_with_mapper.ru
59
60
  - examples/variable.ru
60
61
  - examples/variable_with_regex.ru
61
62
  - http_router.gemspec
@@ -65,6 +66,7 @@ files:
65
66
  - lib/http_router/glob.rb
66
67
  - lib/http_router/interface/sinatra.rb
67
68
  - lib/http_router/node.rb
69
+ - lib/http_router/optional_compiler.rb
68
70
  - lib/http_router/path.rb
69
71
  - lib/http_router/response.rb
70
72
  - lib/http_router/root.rb
@@ -74,6 +76,7 @@ files:
74
76
  - spec/misc_spec.rb
75
77
  - spec/rack/dispatch_spec.rb
76
78
  - spec/rack/generate_spec.rb
79
+ - spec/rack/middleware_spec.rb
77
80
  - spec/rack/route_spec.rb
78
81
  - spec/recognize_spec.rb
79
82
  - spec/sinatra/recognize_spec.rb
@@ -118,6 +121,7 @@ test_files:
118
121
  - spec/misc_spec.rb
119
122
  - spec/rack/dispatch_spec.rb
120
123
  - spec/rack/generate_spec.rb
124
+ - spec/rack/middleware_spec.rb
121
125
  - spec/rack/route_spec.rb
122
126
  - spec/recognize_spec.rb
123
127
  - spec/sinatra/recognize_spec.rb
@@ -1,15 +0,0 @@
1
- require 'http_router'
2
- HttpRouter.override_rack_mapper!
3
-
4
- map('/get/:id') { |env|
5
- [200, {'Content-type' => 'text/plain'}, ["My id is #{env['router.params'][:id]}\n"]]
6
- }
7
-
8
- post('/get/:id') { |env|
9
- [200, {'Content-type' => 'text/plain'}, ["My id is #{env['router.params'][:id]} and you posted!\n"]]
10
- }
11
-
12
- # crapbook-pro:~ joshua$ curl http://127.0.0.1:3000/get/123
13
- # My id is 123
14
- # crapbook-pro:~ joshua$ curl -X POST http://127.0.0.1:3000/get/123
15
- # My id is 123 and you posted!