http_router 0.6.4 → 0.6.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.
@@ -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"