http_router 0.6.4 → 0.6.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,7 +8,7 @@ class HttpRouter
8
8
 
9
9
  def [](request)
10
10
  whole_path = "/#{join_whole_path(request)}"
11
- if match = @matcher.match(whole_path)
11
+ if match = @matcher.match(whole_path) and match[0].size == whole_path.size
12
12
  request = request.clone
13
13
  request.extra_env['router.regex_match'] = match
14
14
  match.names.size.times{|i| request.params << match[i + 1]} if match.respond_to?(:names) && match.names
@@ -141,20 +141,6 @@ class HttpRouter
141
141
  add_prioritized_match(SpanningRegex.new(@router, regexp, matching_indicies, priority, splitting_indicies))
142
142
  end
143
143
 
144
- def add_prioritized_match(match)
145
- @linear ||= []
146
- if match.priority != 0
147
- @linear.each_with_index { |n, i|
148
- if match.priority > (n.priority || 0)
149
- @linear[i, 0] = match
150
- return @linear[i]
151
- end
152
- }
153
- end
154
- @linear << match
155
- @linear.last
156
- end
157
-
158
144
  def add_free_match(regexp)
159
145
  @linear ||= []
160
146
  @linear << FreeRegex.new(@router, regexp)
@@ -172,8 +158,22 @@ class HttpRouter
172
158
  end
173
159
 
174
160
  def join_whole_path(request)
175
- request.path.join('/')
161
+ request.path * '/'
176
162
  end
177
163
 
164
+ private
165
+ def add_prioritized_match(match)
166
+ @linear ||= []
167
+ if match.priority != 0
168
+ @linear.each_with_index { |n, i|
169
+ if match.priority > (n.priority || 0)
170
+ @linear[i, 0] = match
171
+ return @linear[i]
172
+ end
173
+ }
174
+ end
175
+ @linear << match
176
+ @linear.last
177
+ end
178
178
  end
179
179
  end
@@ -2,7 +2,7 @@ class HttpRouter
2
2
  class Path
3
3
  attr_reader :route, :param_names
4
4
  def initialize(route, path, param_names = [])
5
- @route, @path, @param_names, @static = route, path, param_names, param_names.empty?
5
+ @route, @path, @param_names, @dynamic = route, path, param_names, !param_names.empty?
6
6
  duplicate_param_names = param_names.dup.uniq!
7
7
  raise AmbiguousVariableException, "You have duplicate variable name present: #{duplicate_param_names.join(', ')}" if duplicate_param_names
8
8
  if path.respond_to?(:split)
@@ -34,7 +34,7 @@ class HttpRouter
34
34
  end
35
35
 
36
36
  def hashify_params(params)
37
- !@static && params ? param_names.zip(params).inject({}) { |h, (k,v)| h[k] = v; h } : {}
37
+ @dynamic && params ? param_names.zip(params).inject({}) { |h, (k,v)| h[k] = v; h } : {}
38
38
  end
39
39
 
40
40
  def url(args, options)
@@ -50,7 +50,7 @@ class HttpRouter
50
50
  end
51
51
 
52
52
  private
53
- def raw_url(args,options)
53
+ def raw_url(args, options)
54
54
  raise UngeneratableRouteException
55
55
  end
56
56
 
@@ -125,7 +125,7 @@ class HttpRouter
125
125
 
126
126
  def url(*args)
127
127
  result, extra_params = url_with_params(*args)
128
- @router.append_querystring(result, extra_params)
128
+ append_querystring(result, extra_params)
129
129
  end
130
130
 
131
131
  def clone(new_router)
@@ -262,5 +262,21 @@ class HttpRouter
262
262
  end
263
263
  path_obj
264
264
  end
265
+
266
+ def append_querystring(uri, params)
267
+ if params && !params.empty?
268
+ uri_size = uri.size
269
+ params.each do |k,v|
270
+ case v
271
+ when Array
272
+ v.each { |v_part| uri << '&' << ::Rack::Utils.escape(k.to_s) << '%5B%5D=' << ::Rack::Utils.escape(v_part.to_s) }
273
+ else
274
+ uri << '&' << ::Rack::Utils.escape(k.to_s) << '=' << ::Rack::Utils.escape(v.to_s)
275
+ end
276
+ end
277
+ uri[uri_size] = ??
278
+ end
279
+ uri
280
+ end
265
281
  end
266
282
  end
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  class HttpRouter #:nodoc
3
- VERSION = '0.6.4'
3
+ VERSION = '0.6.5'
4
4
  end
data/lib/http_router.rb CHANGED
@@ -13,18 +13,45 @@ class HttpRouter
13
13
  attr_reader :root, :routes, :known_methods, :named_routes
14
14
  attr_accessor :default_app, :url_mount
15
15
 
16
+ # Raised when a Route is not able to be generated.
16
17
  UngeneratableRouteException = Class.new(RuntimeError)
18
+ # Raised when a Route is generated that isn't valid.
17
19
  InvalidRouteException = Class.new(RuntimeError)
20
+ # Raised when a Route is not able to be generated due to a missing parameter.
18
21
  MissingParameterException = Class.new(RuntimeError)
19
22
 
20
- def initialize(options = nil, &blk)
21
- reset!
23
+ # Creates a new HttpRouter.
24
+ # Can be called with either <tt>HttpRouter.new(proc{|env| ... }, { .. options .. })</tt> or with the first argument omitted.
25
+ # If there is a proc first, then it's used as the default app in the case of a non-match.
26
+ # Supported options are
27
+ # * :default_app -- Default application used if there is a non-match on #call. Defaults to 404 generator.
28
+ # * :ignore_trailing_slash -- Ignore a trailing / when attempting to match. Defaults to +true+.
29
+ # * :redirect_trailing_slash -- On trailing /, redirect to the same path without the /. Defaults to +false+.
30
+ def initialize(*args, &blk)
31
+ default_app, options = args.first.is_a?(Hash) ? [nil, args.first] : [args.first, args[1]]
22
32
  @options = options
23
33
  @default_app = default_app || options && options[:default_app] || proc{|env| ::Rack::Response.new("Not Found", 404).finish }
24
34
  @ignore_trailing_slash = options && options.key?(:ignore_trailing_slash) ? options[:ignore_trailing_slash] : true
35
+ @redirect_trailing_slash = options && options.key?(:redirect_trailing_slash) ? options[:redirect_trailing_slash] : false
36
+ reset!
25
37
  instance_eval(&blk) if blk
26
38
  end
27
39
 
40
+ # Adds a path to be recognized.
41
+ #
42
+ # To assign a part of the path to a specific variable, use :variable_name within the route.
43
+ # 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>.
44
+ #
45
+ # You can receive mulitple parts into a single variable by using the glob syntax.
46
+ # 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>.
47
+ #
48
+ # 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.
49
+ #
50
+ # 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.
51
+ #
52
+ # 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.
53
+ #
54
+ # Returns the route object.
28
55
  def add(path, opts = {}, &app)
29
56
  route = case path
30
57
  when Regexp
@@ -41,40 +68,65 @@ class HttpRouter
41
68
  @routes << route
42
69
  end
43
70
 
44
- def add_with_request_method(path, method, opts = {}, &app)
45
- route = add(path, opts).send(method.to_sym)
46
- route.to(app) if app
47
- route
48
- end
71
+ # Adds a path that only responds to the request method +GET+.
72
+ #
73
+ # Returns the route object.
74
+ def get(path, opts = {}, &app); add_with_request_method(path, :get, opts, &app); end
49
75
 
50
- [:post, :get, :delete, :put, :head].each do |rm|
51
- class_eval "def #{rm}(path, opts = {}, &app); add_with_request_method(path, #{rm.inspect}, opts, &app); end", __FILE__, __LINE__
52
- end
76
+ # Adds a path that only responds to the request method +POST+.
77
+ #
78
+ # Returns the route object.
79
+ def post(path, opts = {}, &app); add_with_request_method(path, :post, opts, &app); end
80
+
81
+ # Adds a path that only responds to the request method +HEAD+.
82
+ #
83
+ # Returns the route object.
84
+ def head(path, opts = {}, &app); add_with_request_method(path, :head, opts, &app); end
85
+
86
+ # Adds a path that only responds to the request method +DELETE+.
87
+ #
88
+ # Returns the route object.
89
+ def delete(path, opts = {}, &app); add_with_request_method(path, :delete, opts, &app); end
90
+
91
+ # Adds a path that only responds to the request method +PUT+.
92
+ #
93
+ # Returns the route object.
94
+ def put(path, opts = {}, &app); add_with_request_method(path, :put, opts, &app); end
53
95
 
54
96
  def recognize(env)
55
97
  call(env, false)
56
98
  end
57
99
 
100
+ # Rack compatible #call. If matching route is found, and +dest+ value responds to #call, processing will pass to the matched route. Otherwise,
101
+ # the default application will be called. The router will be available in the env under the key <tt>router</tt>. And parameters matched will
102
+ # be available under the key <tt>router.params</tt>.
58
103
  def call(env, perform_call = true)
59
104
  rack_request = Rack::Request.new(env)
60
- request = Request.new(rack_request.path_info, rack_request, perform_call)
61
- response = catch(:success) { @root[request] }
62
- if !response
63
- supported_methods = (@known_methods - [env['REQUEST_METHOD']]).select do |m|
64
- test_env = Rack::Request.new(rack_request.env.clone)
65
- test_env.env['REQUEST_METHOD'] = m
66
- test_env.env['HTTP_ROUTER_405_TESTING_ACCEPTANCE'] = true
67
- test_request = Request.new(test_env.path_info, test_env, 405)
68
- catch(:success) { @root[test_request] }
69
- end
70
- supported_methods.empty? ? @default_app.call(env) : [405, {'Allow' => supported_methods.sort.join(", ")}, []]
71
- elsif response
72
- response
105
+ if redirect_trailing_slash? && (rack_request.head? || rack_request.get?) && rack_request.path_info[-1] == ?/
106
+ response = ::Rack::Response.new
107
+ response.redirect(request.path_info[0, request.path_info.size - 1], 302)
108
+ response.finish
73
109
  else
74
- @default_app.call(env)
110
+ request = Request.new(rack_request.path_info, rack_request, perform_call)
111
+ response = catch(:success) { @root[request] }
112
+ if !response
113
+ supported_methods = (@known_methods - [env['REQUEST_METHOD']]).select do |m|
114
+ test_env = Rack::Request.new(rack_request.env.clone)
115
+ test_env.env['REQUEST_METHOD'] = m
116
+ test_env.env['HTTP_ROUTER_405_TESTING_ACCEPTANCE'] = true
117
+ test_request = Request.new(test_env.path_info, test_env, 405)
118
+ catch(:success) { @root[test_request] }
119
+ end
120
+ supported_methods.empty? ? @default_app.call(env) : [405, {'Allow' => supported_methods.sort.join(", ")}, []]
121
+ elsif response
122
+ response
123
+ else
124
+ @default_app.call(env)
125
+ end
75
126
  end
76
127
  end
77
128
 
129
+ # Resets the router to a clean state.
78
130
  def reset!
79
131
  @root = Node.new(self)
80
132
  @default_app = Proc.new{ |env| Rack::Response.new("Your request couldn't be found", 404).finish }
@@ -83,32 +135,41 @@ class HttpRouter
83
135
  @known_methods = ['GET', "POST", "PUT", "DELETE"]
84
136
  end
85
137
 
138
+ # Assigns the default application.
139
+ def default(app)
140
+ @default_app = app
141
+ end
142
+
143
+ # Generate a URL for a specified route. This will accept a list of variable values plus any other variable names named as a hash.
144
+ # This first value must be either the Route object or the name of the route.
145
+ #
146
+ # Example:
147
+ # router = HttpRouter.new
148
+ # router.add('/:foo.:format).name(:test).compile
149
+ # router.url(:test, 123, 'html')
150
+ # # ==> "/123.html"
151
+ # router.url(:test, 123, :format => 'html')
152
+ # # ==> "/123.html"
153
+ # router.url(:test, :foo => 123, :format => 'html')
154
+ # # ==> "/123.html"
155
+ # router.url(:test, :foo => 123, :format => 'html', :fun => 'inthesun')
156
+ # # ==> "/123.html?fun=inthesun"
86
157
  def url(route, *args)
87
158
  case route
88
- when Symbol then url(@named_routes[route], *args)
159
+ when Symbol then @named_routes.key?(route) ? @named_routes[route].url(*args) : raise(UngeneratableRouteException)
89
160
  when Route then route.url(*args)
90
161
  else raise UngeneratableRouteException
91
162
  end
92
163
  end
93
164
 
165
+ # Ignore trailing slash feature enabled? See #initialize for details.
94
166
  def ignore_trailing_slash?
95
167
  @ignore_trailing_slash
96
168
  end
97
169
 
98
- def append_querystring(uri, params)
99
- if params && !params.empty?
100
- uri_size = uri.size
101
- params.each do |k,v|
102
- case v
103
- when Array
104
- v.each { |v_part| uri << '&' << ::Rack::Utils.escape(k.to_s) << '%5B%5D=' << ::Rack::Utils.escape(v_part.to_s) }
105
- else
106
- uri << '&' << ::Rack::Utils.escape(k.to_s) << '=' << ::Rack::Utils.escape(v.to_s)
107
- end
108
- end
109
- uri[uri_size] = ??
110
- end
111
- uri
170
+ # Redirect trailing slash feature enabled? See #initialize for details.
171
+ def redirect_trailing_slash?
172
+ @redirect_trailing_slash
112
173
  end
113
174
 
114
175
  # Creates a deep-copy of the router.
@@ -126,4 +187,11 @@ class HttpRouter
126
187
  end
127
188
  cloned_router
128
189
  end
190
+
191
+ private
192
+ def add_with_request_method(path, method, opts = {}, &app)
193
+ route = add(path, opts).send(method.to_sym)
194
+ route.to(app) if app
195
+ route
196
+ end
129
197
  end
@@ -43,7 +43,7 @@ class TestVariable < MiniTest::Unit::TestCase
43
43
  end
44
44
 
45
45
  def test_match_path
46
- r = router { add(%r{^/(test123|\d+)$}) }
46
+ r = router { add %r{/(test123|\d+)} }
47
47
  assert_route r, '/test123'
48
48
  assert_route r, '/123'
49
49
  assert_route nil, '/test123andmore'
@@ -92,6 +92,10 @@ class TestVariable < MiniTest::Unit::TestCase
92
92
  assert_route '/test/*variable.:format', 'test/one/two/three.html', {:variable => ['one', 'two', 'three'], :format => 'html'}
93
93
  end
94
94
 
95
+ def test_glob_with_optional_format
96
+ assert_route '/test/*variable(.:format)', 'test/one/two/three.html', {:variable => ['one', 'two', 'three'], :format => 'html'}
97
+ end
98
+
95
99
  def test_glob_with_literal
96
100
  assert_route '/test/*variable.html', 'test/one/two/three.html', {:variable => ['one', 'two', 'three']}
97
101
  end
@@ -116,6 +120,7 @@ class TestVariable < MiniTest::Unit::TestCase
116
120
  def test_match_path_with_groups
117
121
  r = router { add(%r{/(?<year>\\d{4})/(?<month>\\d{2})/(?<day>\\d{2})/?}) }
118
122
  assert_route r, '/1234/23/56', {:year => '1234', :month => '23', :day => '56'}
119
- end"
123
+ end
124
+ "
120
125
  end
121
126
  end
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http_router
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 13
4
5
  prerelease: false
5
6
  segments:
6
7
  - 0
7
8
  - 6
8
- - 4
9
- version: 0.6.4
9
+ - 5
10
+ version: 0.6.5
10
11
  platform: ruby
11
12
  authors:
12
13
  - Joshua Hull
@@ -14,7 +15,7 @@ autorequire:
14
15
  bindir: bin
15
16
  cert_chain: []
16
17
 
17
- date: 2011-03-22 00:00:00 -07:00
18
+ date: 2011-03-23 00:00:00 -07:00
18
19
  default_executable:
19
20
  dependencies:
20
21
  - !ruby/object:Gem::Dependency
@@ -25,6 +26,7 @@ dependencies:
25
26
  requirements:
26
27
  - - ">="
27
28
  - !ruby/object:Gem::Version
29
+ hash: 23
28
30
  segments:
29
31
  - 1
30
32
  - 0
@@ -40,6 +42,7 @@ dependencies:
40
42
  requirements:
41
43
  - - ~>
42
44
  - !ruby/object:Gem::Version
45
+ hash: 21
43
46
  segments:
44
47
  - 0
45
48
  - 2
@@ -55,6 +58,7 @@ dependencies:
55
58
  requirements:
56
59
  - - ~>
57
60
  - !ruby/object:Gem::Version
61
+ hash: 15
58
62
  segments:
59
63
  - 2
60
64
  - 0
@@ -70,6 +74,7 @@ dependencies:
70
74
  requirements:
71
75
  - - ">="
72
76
  - !ruby/object:Gem::Version
77
+ hash: 3
73
78
  segments:
74
79
  - 0
75
80
  version: "0"
@@ -83,6 +88,7 @@ dependencies:
83
88
  requirements:
84
89
  - - ">="
85
90
  - !ruby/object:Gem::Version
91
+ hash: 3
86
92
  segments:
87
93
  - 0
88
94
  version: "0"
@@ -96,6 +102,7 @@ dependencies:
96
102
  requirements:
97
103
  - - ">="
98
104
  - !ruby/object:Gem::Version
105
+ hash: 3
99
106
  segments:
100
107
  - 0
101
108
  version: "0"
@@ -109,6 +116,7 @@ dependencies:
109
116
  requirements:
110
117
  - - ">="
111
118
  - !ruby/object:Gem::Version
119
+ hash: 3
112
120
  segments:
113
121
  - 0
114
122
  version: "0"
@@ -122,6 +130,7 @@ dependencies:
122
130
  requirements:
123
131
  - - ~>
124
132
  - !ruby/object:Gem::Version
133
+ hash: 23
125
134
  segments:
126
135
  - 1
127
136
  - 0
@@ -208,6 +217,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
208
217
  requirements:
209
218
  - - ">="
210
219
  - !ruby/object:Gem::Version
220
+ hash: 3
211
221
  segments:
212
222
  - 0
213
223
  version: "0"
@@ -216,6 +226,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
216
226
  requirements:
217
227
  - - ">="
218
228
  - !ruby/object:Gem::Version
229
+ hash: 3
219
230
  segments:
220
231
  - 0
221
232
  version: "0"