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.
- data/lib/http_router/node/free_regex.rb +1 -1
- data/lib/http_router/node.rb +15 -15
- data/lib/http_router/path.rb +3 -3
- data/lib/http_router/route.rb +17 -1
- data/lib/http_router/version.rb +1 -1
- data/lib/http_router.rb +107 -39
- data/test/test_variable.rb +7 -2
- metadata +14 -3
@@ -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
|
data/lib/http_router/node.rb
CHANGED
@@ -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
|
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
|
data/lib/http_router/path.rb
CHANGED
@@ -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, @
|
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
|
-
|
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
|
|
data/lib/http_router/route.rb
CHANGED
@@ -125,7 +125,7 @@ class HttpRouter
|
|
125
125
|
|
126
126
|
def url(*args)
|
127
127
|
result, extra_params = url_with_params(*args)
|
128
|
-
|
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
|
data/lib/http_router/version.rb
CHANGED
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
|
-
|
21
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
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
|
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
|
-
|
99
|
-
|
100
|
-
|
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
|
data/test/test_variable.rb
CHANGED
@@ -43,7 +43,7 @@ class TestVariable < MiniTest::Unit::TestCase
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def test_match_path
|
46
|
-
r = router { add
|
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
|
-
-
|
9
|
-
version: 0.6.
|
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-
|
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"
|