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 +3 -2
- data/Rakefile +8 -0
- data/VERSION +1 -1
- data/examples/middleware.ru +46 -0
- data/examples/rack_mapper.ru +22 -0
- data/lib/http_router/glob.rb +7 -6
- data/lib/http_router/node.rb +17 -20
- data/lib/http_router/optional_compiler.rb +35 -0
- data/lib/http_router/path.rb +14 -4
- data/lib/http_router/route.rb +89 -63
- data/lib/http_router/variable.rb +12 -8
- data/lib/http_router.rb +110 -29
- data/spec/generate_spec.rb +8 -0
- data/spec/misc_spec.rb +22 -15
- data/spec/rack/middleware_spec.rb +20 -0
- data/spec/recognize_spec.rb +59 -31
- metadata +9 -5
- data/examples/simple_with_mapper.ru +0 -15
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 (~
|
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.
|
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
|
data/lib/http_router/glob.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
class HttpRouter
|
2
2
|
class Glob < Variable
|
3
|
-
def matches(
|
4
|
-
|
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
|
-
|
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
|
data/lib/http_router/node.rb
CHANGED
@@ -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(
|
7
|
-
@router =
|
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.
|
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
|
-
|
141
|
+
unless parts.empty?
|
142
|
+
whole_path = parts.join('/')
|
144
143
|
if @linear && !@linear.empty?
|
145
|
-
|
144
|
+
response = nil
|
145
|
+
dupped_parts = nil
|
146
146
|
next_node = @linear.find do |(tester, node)|
|
147
|
-
if tester.
|
148
|
-
|
149
|
-
|
150
|
-
node
|
151
|
-
elsif tester.respond_to?(:
|
152
|
-
|
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
|
-
|
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.
|
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
|
data/lib/http_router/path.rb
CHANGED
@@ -1,14 +1,23 @@
|
|
1
1
|
require 'cgi'
|
2
2
|
class HttpRouter
|
3
3
|
class Path
|
4
|
-
attr_reader :parts
|
5
|
-
|
6
|
-
|
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)
|
data/lib/http_router/route.rb
CHANGED
@@ -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
|
3
|
+
attr_reader :dest, :paths, :path, :matches_with
|
4
|
+
attr_accessor :trailing_slash_ignore, :partially_match, :default_values
|
5
5
|
|
6
|
-
def initialize(
|
7
|
-
@router =
|
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[:
|
37
|
-
|
38
|
-
|
39
|
-
if options && options[:
|
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('
|
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
|
-
|
132
|
-
|
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 =
|
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
|
data/lib/http_router/variable.rb
CHANGED
@@ -2,19 +2,23 @@ class HttpRouter
|
|
2
2
|
class Variable
|
3
3
|
attr_reader :name, :matches_with
|
4
4
|
|
5
|
-
def initialize(
|
6
|
-
@router =
|
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(
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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,
|
7
|
-
autoload :Root,
|
8
|
-
autoload :Variable,
|
9
|
-
autoload :Glob,
|
10
|
-
autoload :Route,
|
11
|
-
autoload :Response,
|
12
|
-
autoload :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
|
-
|
29
|
-
|
30
|
-
|
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
|
-
@
|
34
|
-
@
|
35
|
-
@
|
55
|
+
@middleware = options && options.key?(:middleware) ? options[:middleware] : false
|
56
|
+
@routes = []
|
57
|
+
@named_routes = {}
|
58
|
+
@init_block = block
|
36
59
|
reset!
|
37
|
-
|
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
|
-
|
59
|
-
|
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
|
-
|
63
|
-
|
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
|
-
#
|
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
|
-
|
151
|
-
|
152
|
-
@
|
153
|
-
|
154
|
-
new_route
|
155
|
-
|
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
|
-
|
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
|
data/spec/generate_spec.rb
CHANGED
@@ -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 =>
|
9
|
-
@router.recognize(Rack::MockRequest.env_for('http://host2/variable', :method => 'POST')).
|
10
|
-
@router.recognize(Rack::MockRequest.env_for('http://host1/variable', :method => 'POST')).
|
11
|
-
@router.recognize(Rack::MockRequest.env_for('http://host2/123',
|
12
|
-
@router.recognize(Rack::MockRequest.env_for('http://host1/123',
|
13
|
-
@router.recognize(Rack::MockRequest.env_for('http://host1/123',
|
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 "
|
41
|
-
it "
|
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.
|
47
|
-
|
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
|
+
|
data/spec/recognize_spec.rb
CHANGED
@@ -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("
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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 "
|
102
|
-
it "should
|
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
|
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
|
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
|
194
|
-
route = @router.add('/test/*variable/anymore').matching(:variable =>
|
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
|
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
|
214
|
-
route = @router.add('/one-:variable-time').matching(:variable =>
|
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
|
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
|
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
|
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
|
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:
|
4
|
+
hash: 17
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
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
|
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!
|