http_router 0.1.4 → 0.1.5

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/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!