http_router 0.0.1 → 0.1.0
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 +11 -5
- data/VERSION +1 -1
- data/lib/http_router/node.rb +101 -55
- data/lib/http_router/path.rb +19 -2
- data/lib/http_router/response.rb +5 -0
- data/lib/http_router/root.rb +44 -35
- data/lib/http_router/route.rb +194 -26
- data/lib/http_router/sinatra.rb +5 -4
- data/lib/http_router.rb +32 -150
- data/spec/generate_spec.rb +17 -7
- data/spec/rack/dispatch_spec.rb +4 -4
- data/spec/rack/generate_spec.rb +3 -3
- data/spec/rack/route_spec.rb +2 -2
- data/spec/recognize_spec.rb +31 -30
- metadata +3 -3
data/README.rdoc
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
== Introduction
|
|
4
4
|
|
|
5
|
-
When I wrote Usher, I made a few
|
|
5
|
+
When I wrote Usher, I made a few compromises in design that I wasn't totally happy with. More and more features got added to it, and eventually, it became harder to maintain. I took a few moments to work in Node.js, and wrote a router there called Sherpa, which I was happier with. But I felt that by losing more abstraction, and tackling just the problem of HTTP routing, I could come up with something even better.
|
|
6
|
+
|
|
7
|
+
== Warning
|
|
8
|
+
|
|
9
|
+
This is very new code. Lots of stuff probably doesn't work right. I will likely never support all the features I had in Usher. Documentation is super-sparse.
|
|
6
10
|
|
|
7
11
|
== Features
|
|
8
12
|
|
|
@@ -38,14 +42,16 @@ e.g.
|
|
|
38
42
|
|
|
39
43
|
r = HttpRouter.new
|
|
40
44
|
r.add('/test/:variable(.:format)').name(:my_test_path).to {|env| [200, {}, "Hey dude #{env['router.params'][:variable]}"]}
|
|
41
|
-
r.add('/test').
|
|
42
|
-
r.add('/static').
|
|
45
|
+
r.add('/test').redirect("http://www.google.com/")
|
|
46
|
+
r.add('/static').static('/my_file_system')
|
|
43
47
|
|
|
44
|
-
As well, you can support regex matching and request conditions. To add a regex match, use <tt
|
|
45
|
-
To match on a request condition you can use <tt
|
|
48
|
+
As well, you can support regex matching and request conditions. To add a regex match, use <tt>matching(:id => /\d+/)</tt>.
|
|
49
|
+
To match on a request condition you can use <tt>condition(:request_method => %w(POST HEAD))</tt> or more succinctly <tt>request_method('POST', 'HEAD')</tt>.
|
|
46
50
|
|
|
47
51
|
There are convenience methods HttpRouter#get, HttpRouter#post, etc for each request method.
|
|
48
52
|
|
|
53
|
+
Routes will not be recognized unless <tt>#to</tt> has been called on it.
|
|
54
|
+
|
|
49
55
|
=== <tt>#url(name or route, *args)</tt>
|
|
50
56
|
|
|
51
57
|
Generates a route. The args can either be a hash, a list, or a mix of both.
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0
|
|
1
|
+
0.1.0
|
data/lib/http_router/node.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
class HttpRouter
|
|
2
2
|
class Node
|
|
3
|
-
attr_accessor :value, :variable, :catchall
|
|
4
|
-
attr_reader :linear, :lookup
|
|
3
|
+
attr_accessor :value, :variable, :catchall
|
|
4
|
+
attr_reader :linear, :lookup, :request_node, :extension_node
|
|
5
5
|
|
|
6
6
|
def initialize
|
|
7
7
|
reset!
|
|
@@ -32,71 +32,117 @@ class HttpRouter
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def add_extension(ext)
|
|
36
|
+
@extension_node ||= Node.new
|
|
37
|
+
@extension_node.add(ext)
|
|
38
|
+
end
|
|
39
|
+
|
|
35
40
|
def add_request_methods(options)
|
|
36
|
-
if options
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
41
|
+
if !options.empty?
|
|
42
|
+
generate_request_method_tree(options)
|
|
43
|
+
elsif @request_node
|
|
44
|
+
current_node = @request_node
|
|
45
|
+
while current_node.request_method
|
|
46
|
+
current_node = (current_node.catchall ||= RequestNode.new)
|
|
47
|
+
end
|
|
48
|
+
[current_node]
|
|
49
|
+
else
|
|
50
|
+
[self]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
protected
|
|
55
|
+
|
|
56
|
+
def transplant_value
|
|
57
|
+
if @value
|
|
58
|
+
target_node = @request_node
|
|
59
|
+
while target_node.request_method
|
|
60
|
+
target_node = (target_node.catchall ||= RequestNode.new)
|
|
61
|
+
end
|
|
62
|
+
target_node.value = @value
|
|
63
|
+
@value = nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def generate_request_method_tree(request_options)
|
|
68
|
+
raise if (request_options.keys & RequestNode::RequestMethods).size != request_options.size
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
current_nodes = [@request_node ||= RequestNode.new]
|
|
72
|
+
RequestNode::RequestMethods.each do |method|
|
|
73
|
+
current_nodes.each_with_index do |current_node, current_node_index|
|
|
74
|
+
if request_options[method] #we care about the method
|
|
75
|
+
if current_node # and we have to pay attention to what currently is there.
|
|
76
|
+
unless current_node.request_method
|
|
77
|
+
current_node.request_method = method
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
case RequestNode::RequestMethods.index(method) <=> RequestNode::RequestMethods.index(current_node.request_method)
|
|
81
|
+
when 0 #use this node
|
|
82
|
+
if request_options[method].is_a?(Regexp)
|
|
83
|
+
current_node = RequestNode.new
|
|
84
|
+
current_node.linear << [request_options[method], current_node]
|
|
85
|
+
elsif request_options[method].is_a?(Array)
|
|
86
|
+
current_nodes[current_node_index] = request_options[method].map{|val| current_node.lookup[val] ||= RequestNode.new}
|
|
87
|
+
else
|
|
88
|
+
current_nodes[current_node_index] = (current_node.lookup[request_options[method]] ||= RequestNode.new)
|
|
66
89
|
end
|
|
67
|
-
|
|
68
|
-
current_nodes[current_node_index] = RequestNode.new
|
|
90
|
+
when 1 #this node is farther ahead
|
|
91
|
+
current_nodes[current_node_index] = (current_node.catchall ||= RequestNode.new)
|
|
92
|
+
redo
|
|
93
|
+
when -1 #this method is more important than the current node
|
|
94
|
+
new_node = RequestNode.new
|
|
95
|
+
new_node.request_method = method
|
|
96
|
+
new_node.catchall = current_node
|
|
97
|
+
current_nodes[current_node_index] = new_node
|
|
69
98
|
redo
|
|
70
99
|
end
|
|
71
|
-
elsif !current_node
|
|
72
|
-
@request_node = RequestNode.new
|
|
73
|
-
current_nodes[current_node_index] = @request_node
|
|
74
|
-
redo
|
|
75
100
|
else
|
|
76
|
-
|
|
101
|
+
current_nodes[current_node_index] = RequestNode.new
|
|
102
|
+
redo
|
|
77
103
|
end
|
|
104
|
+
elsif !current_node
|
|
105
|
+
@request_node = RequestNode.new
|
|
106
|
+
current_nodes[current_node_index] = @request_node
|
|
107
|
+
redo
|
|
108
|
+
else
|
|
109
|
+
current_node.catchall ||= RequestNode.new
|
|
78
110
|
end
|
|
79
|
-
current_nodes.flatten!
|
|
80
111
|
end
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
112
|
+
current_nodes.flatten!
|
|
113
|
+
end
|
|
114
|
+
transplant_value
|
|
115
|
+
current_nodes
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def find_on_request_methods(request)
|
|
119
|
+
current_node = self
|
|
120
|
+
if current_node && current_node.request_node
|
|
121
|
+
current_node = current_node.request_node
|
|
122
|
+
while current_node
|
|
123
|
+
previous_node = current_node
|
|
124
|
+
break if current_node.nil? || current_node.is_a?(RoutingError) || current_node.value
|
|
125
|
+
request_value = request.send(current_node.request_method)
|
|
126
|
+
unless current_node.linear.empty?
|
|
127
|
+
next_node = current_node.linear.find do |(regexp, node)|
|
|
128
|
+
regexp === request_value
|
|
129
|
+
end
|
|
130
|
+
if next_node
|
|
131
|
+
current_node = next_node.last
|
|
132
|
+
next
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
current_node = current_node.lookup[request_value] || current_node.catchall
|
|
136
|
+
if current_node.nil?
|
|
137
|
+
current_node = previous_node.request_method == :request_method ? RoutingError.new(405, {"Allow" => previous_node.lookup.keys.join(", ")}) : nil
|
|
138
|
+
else
|
|
139
|
+
current_node
|
|
85
140
|
end
|
|
86
|
-
target_node.value = @value
|
|
87
|
-
@value = nil
|
|
88
|
-
end
|
|
89
|
-
current_nodes
|
|
90
|
-
elsif @request_node
|
|
91
|
-
current_node = @request_node
|
|
92
|
-
while current_node.request_method
|
|
93
|
-
current_node = (current_node.catchall ||= RequestNode.new)
|
|
94
141
|
end
|
|
95
|
-
[current_node]
|
|
96
|
-
else
|
|
97
|
-
[self]
|
|
98
142
|
end
|
|
143
|
+
current_node
|
|
99
144
|
end
|
|
145
|
+
|
|
100
146
|
end
|
|
101
147
|
|
|
102
148
|
class RequestNode < Node
|
data/lib/http_router/path.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
require 'cgi'
|
|
1
2
|
class HttpRouter
|
|
2
3
|
class Path
|
|
3
4
|
attr_reader :parts, :extension
|
|
4
5
|
attr_accessor :route
|
|
5
6
|
def initialize(path, parts, extension)
|
|
6
7
|
@path, @parts, @extension = path, parts, extension
|
|
7
|
-
@eval_path = path.gsub(/[:\*]([a-zA-Z0-9_]+)/) {"\#{args.shift || (options && options
|
|
8
|
+
@eval_path = path.gsub(/[:\*]([a-zA-Z0-9_]+)/) {"\#{args.shift || (options && options.delete(:#{$1})) || raise(MissingParameterException.new(\"missing parameter #{$1}\"))}" }
|
|
8
9
|
instance_eval "
|
|
9
10
|
def raw_url(args,options)
|
|
10
11
|
\"#{@eval_path}\"
|
|
@@ -16,9 +17,25 @@ class HttpRouter
|
|
|
16
17
|
path = raw_url(args, options)
|
|
17
18
|
raise TooManyParametersException.new unless args.empty?
|
|
18
19
|
Rack::Utils.uri_escape!(path)
|
|
20
|
+
generate_querystring(path, options)
|
|
19
21
|
path
|
|
20
22
|
end
|
|
21
23
|
|
|
24
|
+
def generate_querystring(uri, params)
|
|
25
|
+
if params && !params.empty?
|
|
26
|
+
uri_size = uri.size
|
|
27
|
+
params.each do |k,v|
|
|
28
|
+
case v
|
|
29
|
+
when Array
|
|
30
|
+
v.each { |v_part| uri << '&' << CGI.escape(k.to_s) << '%5B%5D=' << CGI.escape(v_part.to_s) }
|
|
31
|
+
else
|
|
32
|
+
uri << '&' << CGI.escape(k.to_s) << '=' << CGI.escape(v.to_s)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
uri[uri_size] = ??
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
22
39
|
def variables
|
|
23
40
|
unless @variables
|
|
24
41
|
@variables = @parts.select{|p| p.is_a?(Variable)}
|
|
@@ -28,7 +45,7 @@ class HttpRouter
|
|
|
28
45
|
end
|
|
29
46
|
|
|
30
47
|
def variable_names
|
|
31
|
-
variables.map{|v| v.name}
|
|
48
|
+
@variable_names ||= variables.map{|v| v.name}
|
|
32
49
|
end
|
|
33
50
|
|
|
34
51
|
def matches_extension?(extension)
|
data/lib/http_router/response.rb
CHANGED
data/lib/http_router/root.rb
CHANGED
|
@@ -5,17 +5,38 @@ class HttpRouter
|
|
|
5
5
|
reset!
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
+
def add_path(path)
|
|
9
|
+
node = path.parts.inject(self) { |node, part| node.add(part) }
|
|
10
|
+
if path.extension
|
|
11
|
+
node = node.add_extension(path.extension)
|
|
12
|
+
end
|
|
13
|
+
node
|
|
14
|
+
end
|
|
15
|
+
|
|
8
16
|
def find(request)
|
|
9
17
|
path = request.path_info.dup
|
|
10
18
|
path.slice!(-1) if @base.ignore_trailing_slash? && path[-1] == ?/
|
|
11
|
-
path
|
|
12
|
-
extension = $1
|
|
19
|
+
extension = extract_extension(path)
|
|
13
20
|
parts = @base.split(path)
|
|
14
21
|
parts << '' if path[path.size - 1] == ?/
|
|
15
22
|
|
|
16
|
-
current_node = self
|
|
17
23
|
params = []
|
|
18
|
-
|
|
24
|
+
if current_node = process_parts(parts, extension, params)
|
|
25
|
+
current_node = current_node.find_on_request_methods(request)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
process_response(current_node, parts, extension, params, request)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def process_parts(parts, extension, params)
|
|
34
|
+
current_node = self
|
|
35
|
+
loop do
|
|
36
|
+
if parts.empty? && current_node.extension_node && extension
|
|
37
|
+
parts << extension
|
|
38
|
+
current_node = current_node.extension_node
|
|
39
|
+
end
|
|
19
40
|
break if current_node.nil? || (current_node.value && current_node.value.route.partially_match?) || parts.empty?
|
|
20
41
|
unless current_node.linear.empty?
|
|
21
42
|
whole_path = parts.join('/')
|
|
@@ -49,41 +70,21 @@ class HttpRouter
|
|
|
49
70
|
break
|
|
50
71
|
else
|
|
51
72
|
current_node = nil
|
|
73
|
+
break
|
|
52
74
|
end
|
|
53
75
|
end
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
unless current_node.linear.empty?
|
|
62
|
-
next_node = current_node.linear.find do |(regexp, node)|
|
|
63
|
-
regexp === request_value
|
|
64
|
-
end
|
|
65
|
-
if next_node
|
|
66
|
-
current_node = next_node.last
|
|
67
|
-
next
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
current_node = current_node.lookup[request_value] || current_node.catchall
|
|
71
|
-
if current_node.nil?
|
|
72
|
-
current_node = previous_node.request_method == :request_method ? RoutingError.new(405, {"Allow" => previous_node.lookup.keys.join(", ")}) : nil
|
|
73
|
-
else
|
|
74
|
-
current_node
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
if current_node.is_a?(RoutingError)
|
|
79
|
-
current_node
|
|
80
|
-
elsif current_node && current_node.value
|
|
76
|
+
current_node
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def process_response(node, parts, extension, params, request)
|
|
80
|
+
if node.is_a?(RoutingError)
|
|
81
|
+
node
|
|
82
|
+
elsif node && node.value
|
|
81
83
|
if parts.empty?
|
|
82
|
-
post_match(
|
|
83
|
-
elsif
|
|
84
|
+
post_match(node.value, params, extension, request.path_info)
|
|
85
|
+
elsif node.value.route.partially_match?
|
|
84
86
|
rest = '/' << parts.join('/') << (extension ? ".#{extension}" : '')
|
|
85
|
-
|
|
86
|
-
post_match(current_node.value, params, nil, request.path_info[0, request.path_info.size - rest.size], rest)
|
|
87
|
+
post_match(node.value, params, nil, request.path_info[0, request.path_info.size - rest.size], rest)
|
|
87
88
|
else
|
|
88
89
|
nil
|
|
89
90
|
end
|
|
@@ -92,6 +93,14 @@ class HttpRouter
|
|
|
92
93
|
end
|
|
93
94
|
end
|
|
94
95
|
|
|
96
|
+
def extract_extension(path)
|
|
97
|
+
if path.gsub!(/\.([^\/\.]+)$/, '')
|
|
98
|
+
extension = $1
|
|
99
|
+
else
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
95
104
|
def post_match(path, params, extension, matched_path, remaining_path = nil)
|
|
96
105
|
if path.route.partially_match? || path.matches_extension?(extension)
|
|
97
106
|
Response.new(path, params, extension, matched_path, remaining_path)
|
data/lib/http_router/route.rb
CHANGED
|
@@ -3,14 +3,72 @@ class HttpRouter
|
|
|
3
3
|
attr_reader :dest, :paths
|
|
4
4
|
attr_accessor :trailing_slash_ignore, :partially_match, :default_values
|
|
5
5
|
|
|
6
|
-
def initialize(base,
|
|
7
|
-
@base
|
|
8
|
-
@
|
|
6
|
+
def initialize(base, path)
|
|
7
|
+
@base = base
|
|
8
|
+
@path = path
|
|
9
|
+
@original_path = path.dup
|
|
10
|
+
@partially_match = extract_partial_match(path)
|
|
11
|
+
@trailing_slash_ignore = extract_trailing_slash(path)
|
|
12
|
+
@variable_store = {}
|
|
13
|
+
@matches_with = {}
|
|
14
|
+
@conditions = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def method_missing(method, *args, &block)
|
|
18
|
+
if RequestNode::RequestMethods.include?(method)
|
|
19
|
+
condition(method => args)
|
|
20
|
+
else
|
|
21
|
+
super
|
|
22
|
+
end
|
|
9
23
|
end
|
|
10
24
|
|
|
11
25
|
def name(name)
|
|
12
26
|
@name = name
|
|
13
|
-
@base.
|
|
27
|
+
@base.named_routes[name] = self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get
|
|
31
|
+
request_method('GET', 'HEAD')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def post
|
|
35
|
+
request_method('POST')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def head
|
|
39
|
+
request_method('head')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def put
|
|
43
|
+
request_method('PUT')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def delete
|
|
47
|
+
request_method('DELETE')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def only_get
|
|
51
|
+
request_method('DELETE')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def condition(conditions)
|
|
55
|
+
guard_compiled
|
|
56
|
+
conditions.each do |k,v|
|
|
57
|
+
@conditions.key?(k) ?
|
|
58
|
+
@conditions[k] << v :
|
|
59
|
+
@conditions[k] = Array(v)
|
|
60
|
+
end
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
alias_method :conditions, :condition
|
|
64
|
+
|
|
65
|
+
def matching(*match)
|
|
66
|
+
guard_compiled
|
|
67
|
+
@matches_with.merge!(match.pop) if match.last.is_a?(Hash)
|
|
68
|
+
match.each_slice(2) do |(k,v)|
|
|
69
|
+
@matches_with[k] = v
|
|
70
|
+
end
|
|
71
|
+
self
|
|
14
72
|
end
|
|
15
73
|
|
|
16
74
|
def named
|
|
@@ -18,19 +76,38 @@ class HttpRouter
|
|
|
18
76
|
end
|
|
19
77
|
|
|
20
78
|
def to(dest = nil, &block)
|
|
79
|
+
compile
|
|
21
80
|
@dest = dest || block
|
|
22
81
|
self
|
|
23
82
|
end
|
|
24
83
|
|
|
25
|
-
def
|
|
84
|
+
def partial(match = true)
|
|
26
85
|
@partially_match = match
|
|
27
86
|
self
|
|
28
87
|
end
|
|
29
88
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
89
|
+
def compiled?
|
|
90
|
+
!@paths.nil?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def compile
|
|
94
|
+
unless @paths
|
|
95
|
+
@paths = compile_paths
|
|
96
|
+
@paths.each do |path|
|
|
97
|
+
path.route = self
|
|
98
|
+
current_node = @base.root.add_path(path)
|
|
99
|
+
working_set = current_node.add_request_methods(@conditions)
|
|
100
|
+
working_set.each do |current_node|
|
|
101
|
+
current_node.value = path
|
|
102
|
+
end
|
|
103
|
+
end
|
|
33
104
|
end
|
|
105
|
+
self
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def redirect(path, status = 302)
|
|
109
|
+
guard_compiled
|
|
110
|
+
raise(ArgumentError, "Status has to be an integer between 300 and 399") unless (300..399).include?(status)
|
|
34
111
|
to { |env|
|
|
35
112
|
params = env['router.params']
|
|
36
113
|
response = ::Rack::Response.new
|
|
@@ -40,10 +117,11 @@ class HttpRouter
|
|
|
40
117
|
self
|
|
41
118
|
end
|
|
42
119
|
|
|
43
|
-
def
|
|
120
|
+
def static(root)
|
|
121
|
+
guard_compiled
|
|
122
|
+
raise AlreadyCompiledException.new if compiled?
|
|
44
123
|
if File.directory?(root)
|
|
45
|
-
|
|
46
|
-
to ::Rack::File.new(root)
|
|
124
|
+
partial.to ::Rack::File.new(root)
|
|
47
125
|
else
|
|
48
126
|
to proc{|env| env['PATH_INFO'] = File.basename(root); ::Rack::File.new(File.dirname(root)).call(env)}
|
|
49
127
|
end
|
|
@@ -60,11 +138,14 @@ class HttpRouter
|
|
|
60
138
|
|
|
61
139
|
def url(*args)
|
|
62
140
|
options = args.last.is_a?(Hash) ? args.pop : nil
|
|
141
|
+
options = default_values.merge(options) if default_values && options
|
|
63
142
|
path = matching_path(args.empty? ? options : args)
|
|
64
143
|
raise UngeneratableRouteException.new unless path
|
|
65
144
|
path.url(args, options)
|
|
66
145
|
end
|
|
67
146
|
|
|
147
|
+
private
|
|
148
|
+
|
|
68
149
|
def matching_path(params)
|
|
69
150
|
if @paths.size == 1
|
|
70
151
|
@paths.first
|
|
@@ -77,26 +158,113 @@ class HttpRouter
|
|
|
77
158
|
end
|
|
78
159
|
nil
|
|
79
160
|
else
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
161
|
+
@paths.reverse_each do |path|
|
|
162
|
+
if params && !params.empty?
|
|
163
|
+
return path if (path.variable_names & params.keys).size == path.variable_names.size
|
|
164
|
+
elsif path.variable_names.empty?
|
|
165
|
+
return path
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def extract_partial_match(path)
|
|
174
|
+
path[-1] == ?* && path.slice!(-1)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def extract_trailing_slash(path)
|
|
178
|
+
path[-2, 2] == '/?' && path.slice!(-2, 2)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def extract_extension(path)
|
|
182
|
+
if match = path.match(/^(.*)(\.:([a-zA-Z_]+))$/)
|
|
183
|
+
path.replace(match[1])
|
|
184
|
+
Variable.new(@base, match[3].to_sym)
|
|
185
|
+
elsif match = path.match(/^(.*)(\.([a-zA-Z_]+))$/)
|
|
186
|
+
path.replace(match[1])
|
|
187
|
+
match[3]
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def compile_optionals(path)
|
|
193
|
+
start_index = 0
|
|
194
|
+
end_index = 1
|
|
195
|
+
|
|
196
|
+
paths = [""]
|
|
197
|
+
chars = path.split('')
|
|
198
|
+
|
|
199
|
+
chars.each do |c|
|
|
200
|
+
case c
|
|
201
|
+
when '('
|
|
202
|
+
# over current working set, double paths
|
|
203
|
+
(start_index...end_index).each do |path_index|
|
|
204
|
+
paths << paths[path_index].dup
|
|
205
|
+
end
|
|
206
|
+
start_index = end_index
|
|
207
|
+
end_index = paths.size
|
|
208
|
+
when ')'
|
|
209
|
+
start_index -= end_index - start_index
|
|
210
|
+
else
|
|
211
|
+
(start_index...end_index).each do |path_index|
|
|
212
|
+
paths[path_index] << c
|
|
91
213
|
end
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
paths
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def compile_paths
|
|
220
|
+
paths = compile_optionals(@path)
|
|
221
|
+
paths.map do |path|
|
|
222
|
+
original_path = path.dup
|
|
223
|
+
extension = extract_extension(path)
|
|
224
|
+
new_path = @base.split(path).map do |part|
|
|
225
|
+
case part[0]
|
|
226
|
+
when ?:
|
|
227
|
+
v_name = part[1, part.size].to_sym
|
|
228
|
+
@variable_store[v_name] ||= Variable.new(@base, v_name, @matches_with[v_name])
|
|
229
|
+
when ?*
|
|
230
|
+
v_name = part[1, part.size].to_sym
|
|
231
|
+
@variable_store[v_name] ||= Glob.new(@base, v_name, @matches_with[v_name])
|
|
232
|
+
else
|
|
233
|
+
generate_interstitial_parts(part)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
new_path.flatten!
|
|
237
|
+
Path.new(original_path, new_path, extension)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def generate_interstitial_parts(part)
|
|
242
|
+
part_segments = part.split(/(:[a-zA-Z_]+)/)
|
|
243
|
+
if part_segments.size > 1
|
|
244
|
+
index = 0
|
|
245
|
+
part_segments.map do |seg|
|
|
246
|
+
new_seg = if seg[0] == ?:
|
|
247
|
+
next_index = index + 1
|
|
248
|
+
scan_regex = if next_index == part_segments.size
|
|
249
|
+
/^[^\/]+/
|
|
250
|
+
else
|
|
251
|
+
/^.*?(?=#{Regexp.quote(part_segments[next_index])})/
|
|
95
252
|
end
|
|
253
|
+
v_name = seg[1, seg.size].to_sym
|
|
254
|
+
@variable_store[v_name] ||= Variable.new(@base, v_name, scan_regex)
|
|
255
|
+
else
|
|
256
|
+
/^#{Regexp.quote(seg)}/
|
|
96
257
|
end
|
|
97
|
-
|
|
258
|
+
index += 1
|
|
259
|
+
new_seg
|
|
98
260
|
end
|
|
261
|
+
else
|
|
262
|
+
part
|
|
99
263
|
end
|
|
100
264
|
end
|
|
265
|
+
|
|
266
|
+
def guard_compiled
|
|
267
|
+
raise AlreadyCompiledException.new if compiled?
|
|
268
|
+
end
|
|
101
269
|
end
|
|
102
270
|
end
|
data/lib/http_router/sinatra.rb
CHANGED
|
@@ -60,9 +60,6 @@ class HttpRouter
|
|
|
60
60
|
|
|
61
61
|
def route(verb, path, options={}, &block)
|
|
62
62
|
name = options.delete(:name)
|
|
63
|
-
options[:conditions] ||= {}
|
|
64
|
-
options[:conditions][:request_method] = verb
|
|
65
|
-
options[:conditions][:host] = options.delete(:host) if options.key?(:host)
|
|
66
63
|
|
|
67
64
|
define_method "#{verb} #{path}", &block
|
|
68
65
|
unbound_method = instance_method("#{verb} #{path}")
|
|
@@ -75,7 +72,11 @@ class HttpRouter
|
|
|
75
72
|
|
|
76
73
|
invoke_hook(:route_added, verb, path, block)
|
|
77
74
|
|
|
78
|
-
route = router.add(path
|
|
75
|
+
route = router.add(path)
|
|
76
|
+
route.request_method(verb)
|
|
77
|
+
route.host(options.delete(:host)) if options.key?(:host)
|
|
78
|
+
|
|
79
|
+
route.to(block)
|
|
79
80
|
route.name(name) if name
|
|
80
81
|
route
|
|
81
82
|
end
|
data/lib/http_router.rb
CHANGED
|
@@ -14,15 +14,18 @@ class HttpRouter
|
|
|
14
14
|
UngeneratableRouteException = Class.new(RuntimeError)
|
|
15
15
|
MissingParameterException = Class.new(RuntimeError)
|
|
16
16
|
TooManyParametersException = Class.new(RuntimeError)
|
|
17
|
+
AlreadyCompiledException = Class.new(RuntimeError)
|
|
17
18
|
RoutingError = Struct.new(:status, :headers)
|
|
18
19
|
|
|
19
|
-
attr_reader :routes
|
|
20
|
+
attr_reader :named_routes, :routes, :root
|
|
20
21
|
|
|
21
22
|
def initialize(options = nil)
|
|
22
|
-
reset!
|
|
23
23
|
@default_app = options && options[:default_app] || proc{|env| ::Rack::Response.new("Not Found", 404).finish }
|
|
24
24
|
@ignore_trailing_slash = options && options.key?(:ignore_trailing_slash) ? options[:ignore_trailing_slash] : true
|
|
25
25
|
@redirect_trailing_slash = options && options.key?(:redirect_trailing_slash) ? options[:redirect_trailing_slash] : false
|
|
26
|
+
@routes = []
|
|
27
|
+
@named_routes = {}
|
|
28
|
+
reset!
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
def ignore_trailing_slash?
|
|
@@ -35,7 +38,8 @@ class HttpRouter
|
|
|
35
38
|
|
|
36
39
|
def reset!
|
|
37
40
|
@root = Root.new(self)
|
|
38
|
-
@routes
|
|
41
|
+
@routes.clear
|
|
42
|
+
@named_routes.clear
|
|
39
43
|
end
|
|
40
44
|
|
|
41
45
|
def default(app)
|
|
@@ -47,150 +51,44 @@ class HttpRouter
|
|
|
47
51
|
with_delimiter ? path.split('(/)') : path.split('/')
|
|
48
52
|
end
|
|
49
53
|
|
|
50
|
-
def add(path
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
trailing_slash_ignore = extract_trailing_slash(path)
|
|
54
|
-
paths = compile(path, options)
|
|
55
|
-
|
|
56
|
-
route = Route.new(self, options && options[:default_values])
|
|
57
|
-
route.trailing_slash_ignore = trailing_slash_ignore
|
|
58
|
-
route.partially_match = partially_match
|
|
59
|
-
paths.each_with_index do |path, i|
|
|
60
|
-
current_node = @root
|
|
61
|
-
path.parts.each { |part| current_node = current_node.add(part) }
|
|
62
|
-
working_set = current_node.add_request_methods(options)
|
|
63
|
-
working_set.each do |current_node|
|
|
64
|
-
current_node.value = path
|
|
65
|
-
path.route = route
|
|
66
|
-
route.paths << current_node.value
|
|
67
|
-
end
|
|
68
|
-
end
|
|
54
|
+
def add(path)
|
|
55
|
+
route = Route.new(self, path.dup)
|
|
56
|
+
@routes << route
|
|
69
57
|
route
|
|
70
58
|
end
|
|
71
59
|
|
|
72
|
-
def get(path
|
|
73
|
-
|
|
74
|
-
options[:conditions][:request_method] = ['HEAD', 'GET'] #TODO, this should be able to take an array
|
|
75
|
-
add(path, options)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def post(path, options = {})
|
|
79
|
-
options[:conditions] ||= {}
|
|
80
|
-
options[:conditions][:request_method] = 'POST'
|
|
81
|
-
add(path, options)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def put(path, options = {})
|
|
85
|
-
options[:conditions] ||= {}
|
|
86
|
-
options[:conditions][:request_method] = 'PUT'
|
|
87
|
-
add(path, options)
|
|
60
|
+
def get(path)
|
|
61
|
+
add(path).get
|
|
88
62
|
end
|
|
89
63
|
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
options[:conditions][:request_method] = 'DELETE'
|
|
93
|
-
add(path, options)
|
|
64
|
+
def post(path)
|
|
65
|
+
add(path).post
|
|
94
66
|
end
|
|
95
67
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
options[:conditions][:request_method] = "GET"
|
|
99
|
-
add(path, options)
|
|
68
|
+
def put(path)
|
|
69
|
+
add(path).put
|
|
100
70
|
end
|
|
101
71
|
|
|
102
|
-
def
|
|
103
|
-
|
|
104
|
-
path.slice!(-1)
|
|
105
|
-
true
|
|
106
|
-
else
|
|
107
|
-
false
|
|
108
|
-
end
|
|
72
|
+
def delete(path)
|
|
73
|
+
add(path).delete
|
|
109
74
|
end
|
|
110
75
|
|
|
111
|
-
def
|
|
112
|
-
|
|
113
|
-
path.slice!(-2, 2)
|
|
114
|
-
true
|
|
115
|
-
else
|
|
116
|
-
false
|
|
117
|
-
end
|
|
76
|
+
def only_get(path)
|
|
77
|
+
add(path).only_get
|
|
118
78
|
end
|
|
119
79
|
|
|
120
|
-
def
|
|
121
|
-
|
|
122
|
-
path.replace(match[1])
|
|
123
|
-
Variable.new(self, match[3].to_sym)
|
|
124
|
-
elsif match = path.match(/^(.*)(\.([a-zA-Z_]+))$/)
|
|
125
|
-
path.replace(match[1])
|
|
126
|
-
match[3]
|
|
127
|
-
end
|
|
80
|
+
def recognize(env)
|
|
81
|
+
response = @root.find(env.is_a?(Hash) ? Rack::Request.new(env) : env)
|
|
128
82
|
end
|
|
129
83
|
|
|
130
|
-
def
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
case c
|
|
139
|
-
when '('
|
|
140
|
-
# over current working set, double paths
|
|
141
|
-
(start_index...end_index).each do |path_index|
|
|
142
|
-
paths << paths[path_index].dup
|
|
143
|
-
end
|
|
144
|
-
start_index = end_index
|
|
145
|
-
end_index = paths.size
|
|
146
|
-
when ')'
|
|
147
|
-
start_index -= end_index - start_index
|
|
148
|
-
else
|
|
149
|
-
(start_index...end_index).each do |path_index|
|
|
150
|
-
paths[path_index] << c
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
variables = {}
|
|
156
|
-
paths.map do |path|
|
|
157
|
-
original_path = path.dup
|
|
158
|
-
extension = extract_extension(path)
|
|
159
|
-
new_path = split(path).map do |part|
|
|
160
|
-
case part[0]
|
|
161
|
-
when ?:
|
|
162
|
-
v_name = part[1, part.size].to_sym
|
|
163
|
-
variables[v_name] ||= Variable.new(self, v_name, options && options[:matches_with] && options && options[:matches_with][v_name])
|
|
164
|
-
when ?*
|
|
165
|
-
v_name = part[1, part.size].to_sym
|
|
166
|
-
variables[v_name] ||= Glob.new(self, v_name, options && options[:matches_with] && options && options[:matches_with][v_name])
|
|
167
|
-
else
|
|
168
|
-
part_segments = part.split(/(:[a-zA-Z_]+)/)
|
|
169
|
-
if part_segments.size > 1
|
|
170
|
-
index = 0
|
|
171
|
-
part_segments.map do |seg|
|
|
172
|
-
new_seg = if seg[0] == ?:
|
|
173
|
-
next_index = index + 1
|
|
174
|
-
scan_regex = if next_index == part_segments.size
|
|
175
|
-
/^[^\/]+/
|
|
176
|
-
else
|
|
177
|
-
/^.*?(?=#{Regexp.quote(part_segments[next_index])})/
|
|
178
|
-
end
|
|
179
|
-
v_name = seg[1, seg.size].to_sym
|
|
180
|
-
variables[v_name] ||= Variable.new(self, v_name, scan_regex)
|
|
181
|
-
else
|
|
182
|
-
/^#{Regexp.quote(seg)}/
|
|
183
|
-
end
|
|
184
|
-
index += 1
|
|
185
|
-
new_seg
|
|
186
|
-
end
|
|
187
|
-
else
|
|
188
|
-
part
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
new_path.flatten!
|
|
193
|
-
Path.new(original_path, new_path, extension)
|
|
84
|
+
def url(route, *args)
|
|
85
|
+
case route
|
|
86
|
+
when Symbol
|
|
87
|
+
url(@named_routes[route], *args)
|
|
88
|
+
when nil
|
|
89
|
+
raise UngeneratableRouteException.new
|
|
90
|
+
else
|
|
91
|
+
route.url(*args)
|
|
194
92
|
end
|
|
195
93
|
end
|
|
196
94
|
|
|
@@ -208,10 +106,6 @@ class HttpRouter
|
|
|
208
106
|
elsif response && response.route.dest && response.route.dest.respond_to?(:call)
|
|
209
107
|
process_params(env, response)
|
|
210
108
|
consume_path!(request, response) if response.partial_match?
|
|
211
|
-
#if response.rest
|
|
212
|
-
# request.env["SCRIPT_NAME"] += request.env["PATH_INFO"][0, -response.rest.size]
|
|
213
|
-
# request.env["PATH_INFO"] = response.rest || ''
|
|
214
|
-
#end
|
|
215
109
|
response.route.dest.call(env)
|
|
216
110
|
else
|
|
217
111
|
@default_app.call(env)
|
|
@@ -219,6 +113,8 @@ class HttpRouter
|
|
|
219
113
|
end
|
|
220
114
|
end
|
|
221
115
|
|
|
116
|
+
private
|
|
117
|
+
|
|
222
118
|
def consume_path!(request, response)
|
|
223
119
|
request.env["SCRIPT_NAME"] = (request.env["SCRIPT_NAME"] + response.matched_path)
|
|
224
120
|
request.env["PATH_INFO"] = response.remaining_path || ""
|
|
@@ -233,18 +129,4 @@ class HttpRouter
|
|
|
233
129
|
end
|
|
234
130
|
end
|
|
235
131
|
|
|
236
|
-
def recognize(env)
|
|
237
|
-
response = @root.find(env.is_a?(Hash) ? Rack::Request.new(env) : env)
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def url(route, *args)
|
|
241
|
-
case route
|
|
242
|
-
when Symbol
|
|
243
|
-
url(@routes[route], *args)
|
|
244
|
-
when nil
|
|
245
|
-
raise UngeneratableRouteException.new
|
|
246
|
-
else
|
|
247
|
-
route.url(*args)
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
132
|
end
|
data/spec/generate_spec.rb
CHANGED
|
@@ -6,7 +6,7 @@ describe "HttpRouter#generate" do
|
|
|
6
6
|
context("static paths") do
|
|
7
7
|
['/', '/test', '/test/time', '/one/more/what', '/test.html'].each do |path|
|
|
8
8
|
it "should generate #{path.inspect}" do
|
|
9
|
-
route = @router.add(path)
|
|
9
|
+
route = @router.add(path).compile
|
|
10
10
|
@router.url(route).should == path
|
|
11
11
|
end
|
|
12
12
|
end
|
|
@@ -14,34 +14,44 @@ describe "HttpRouter#generate" do
|
|
|
14
14
|
|
|
15
15
|
context("dynamic paths") do
|
|
16
16
|
it "should generate from a hash" do
|
|
17
|
-
@router.add("/:var").name(:test)
|
|
17
|
+
@router.add("/:var").name(:test).compile
|
|
18
18
|
@router.url(:test, :var => 'test').should == '/test'
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
it "should generate from a hash with extra parts going to the query string" do
|
|
22
|
+
@router.add("/:var").name(:test).compile
|
|
23
|
+
@router.url(:test, :var => 'test', :query => 'string').should == '/test?query=string'
|
|
24
|
+
end
|
|
25
|
+
|
|
21
26
|
it "should generate from an array" do
|
|
22
|
-
@router.add("/:var").name(:test)
|
|
27
|
+
@router.add("/:var").name(:test).compile
|
|
23
28
|
@router.url(:test, 'test').should == '/test'
|
|
24
29
|
end
|
|
25
30
|
|
|
31
|
+
it "should generate from an array with extra parts going to the query string" do
|
|
32
|
+
@router.add("/:var").name(:test).compile
|
|
33
|
+
@router.url(:test, 'test', :query => 'string').should == '/test?query=string'
|
|
34
|
+
end
|
|
35
|
+
|
|
26
36
|
it "should generate with a format" do
|
|
27
|
-
@router.add("/test.:format").name(:test)
|
|
37
|
+
@router.add("/test.:format").name(:test).compile
|
|
28
38
|
@router.url(:test, 'html').should == '/test.html'
|
|
29
39
|
end
|
|
30
40
|
|
|
31
41
|
it "should generate with a format as a hash" do
|
|
32
|
-
@router.add("/test.:format").name(:test)
|
|
42
|
+
@router.add("/test.:format").name(:test).compile
|
|
33
43
|
@router.url(:test, :format => 'html').should == '/test.html'
|
|
34
44
|
end
|
|
35
45
|
|
|
36
46
|
it "should generate with an optional format" do
|
|
37
|
-
@router.add("/test(.:format)").name(:test)
|
|
47
|
+
@router.add("/test(.:format)").name(:test).compile
|
|
38
48
|
@router.url(:test, 'html').should == '/test.html'
|
|
39
49
|
@router.url(:test).should == '/test'
|
|
40
50
|
end
|
|
41
51
|
|
|
42
52
|
context "with optional parts" do
|
|
43
53
|
it "should generate both" do
|
|
44
|
-
@router.add("/:var1(/:var2)").name(:test)
|
|
54
|
+
@router.add("/:var1(/:var2)").name(:test).compile
|
|
45
55
|
@router.url(:test, 'var').should == '/var'
|
|
46
56
|
@router.url(:test, 'var', 'fooz').should == '/var/fooz'
|
|
47
57
|
@router.url(:test, :var1 => 'var').should == '/var'
|
data/spec/rack/dispatch_spec.rb
CHANGED
|
@@ -25,7 +25,7 @@ describe "Usher (for rack) route dispatching" do
|
|
|
25
25
|
describe "HTTP GET" do
|
|
26
26
|
before(:each) do
|
|
27
27
|
route_set.reset!
|
|
28
|
-
route_set.add('/sample'
|
|
28
|
+
route_set.add('/sample').request_method('GET').to(@app)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
it "should dispatch a request" do
|
|
@@ -42,7 +42,7 @@ describe "Usher (for rack) route dispatching" do
|
|
|
42
42
|
describe "HTTP POST" do
|
|
43
43
|
before(:each) do
|
|
44
44
|
route_set.reset!
|
|
45
|
-
route_set.add('/sample'
|
|
45
|
+
route_set.add('/sample').post.to(@app)
|
|
46
46
|
route_set.add('/sample').to(MockApp.new("You shouldn't get here if you are using POST"))
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -64,8 +64,8 @@ describe "Usher (for rack) route dispatching" do
|
|
|
64
64
|
|
|
65
65
|
it "should returns HTTP 405 if the method mis-matches" do
|
|
66
66
|
route_set.reset!
|
|
67
|
-
route_set.
|
|
68
|
-
route_set.
|
|
67
|
+
route_set.post('/sample').to(@app)
|
|
68
|
+
route_set.put('/sample').to(@app)
|
|
69
69
|
response = route_set.call_with_mock_request('/sample', 'GET')
|
|
70
70
|
response.status.should eql(405)
|
|
71
71
|
response['Allow'].should == 'POST, PUT'
|
data/spec/rack/generate_spec.rb
CHANGED
|
@@ -5,9 +5,9 @@ describe "Usher (for rack) route generation" do
|
|
|
5
5
|
before(:each) do
|
|
6
6
|
route_set.reset!
|
|
7
7
|
@app = MockApp.new("Hello World!")
|
|
8
|
-
route_set.add("/fixed").name(:fixed)
|
|
9
|
-
route_set.add("/named/simple/:named_simple_var").name(:simple)
|
|
10
|
-
route_set.add("/named/optional(/:named_optional_var)").name(:optional)
|
|
8
|
+
route_set.add("/fixed").name(:fixed).compile
|
|
9
|
+
route_set.add("/named/simple/:named_simple_var").name(:simple).compile
|
|
10
|
+
route_set.add("/named/optional(/:named_optional_var)").name(:optional).compile
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
describe "named routes" do
|
data/spec/rack/route_spec.rb
CHANGED
|
@@ -29,14 +29,14 @@ describe "Rack interface extensions for Usher::Route" do
|
|
|
29
29
|
|
|
30
30
|
describe "static file serving" do
|
|
31
31
|
it "should serve from a static directory" do
|
|
32
|
-
@route_set.get("/static").
|
|
32
|
+
@route_set.get("/static").static(File.dirname(__FILE__))
|
|
33
33
|
@env = Rack::MockRequest.env_for("/static/#{File.basename(__FILE__)}")
|
|
34
34
|
status, headers, body = @route_set.call(@env)
|
|
35
35
|
body.path.should == File.join(File.dirname(__FILE__), File.basename(__FILE__))
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
it "should serve a specific file" do
|
|
39
|
-
@route_set.get("/static-file").
|
|
39
|
+
@route_set.get("/static-file").static(__FILE__)
|
|
40
40
|
@env = Rack::MockRequest.env_for("/static-file")
|
|
41
41
|
status, headers, body = @route_set.call(@env)
|
|
42
42
|
body.path.should == __FILE__
|
data/spec/recognize_spec.rb
CHANGED
|
@@ -6,14 +6,14 @@ describe "HttpRouter#recognize" do
|
|
|
6
6
|
context("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
|
-
route = @router.add(path)
|
|
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
|
-
route = @router.add("/test(/optional)")
|
|
16
|
+
route = @router.add("/test(/optional)").to(:test)
|
|
17
17
|
@router.recognize(Rack::MockRequest.env_for('/test')).route.should == route
|
|
18
18
|
@router.recognize(Rack::MockRequest.env_for('/test/optional')).route.should == route
|
|
19
19
|
end
|
|
@@ -21,7 +21,7 @@ describe "HttpRouter#recognize" do
|
|
|
21
21
|
|
|
22
22
|
context("partial matching") do
|
|
23
23
|
it "should match partially or completely" do
|
|
24
|
-
route = @router.add("/test*")
|
|
24
|
+
route = @router.add("/test*").to(:test)
|
|
25
25
|
@router.recognize(Rack::MockRequest.env_for('/test')).route.should == route
|
|
26
26
|
response = @router.recognize(Rack::MockRequest.env_for('/test/optional'))
|
|
27
27
|
response.route.should == route
|
|
@@ -31,19 +31,19 @@ describe "HttpRouter#recognize" do
|
|
|
31
31
|
|
|
32
32
|
context("trailing slashes") do
|
|
33
33
|
it "should ignore a trailing slash" do
|
|
34
|
-
route = @router.add("/test")
|
|
34
|
+
route = @router.add("/test").to(:test)
|
|
35
35
|
@router.recognize(Rack::MockRequest.env_for('/test/')).route.should == route
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
it "should not recognize a trailing slash when used with the /? syntax and ignore_trailing_slash disabled" do
|
|
39
39
|
@router = HttpRouter.new(:ignore_trailing_slash => false)
|
|
40
|
-
route = @router.add("/test/?")
|
|
40
|
+
route = @router.add("/test/?").to(:test)
|
|
41
41
|
@router.recognize(Rack::MockRequest.env_for('/test/')).route.should == route
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
it "should recognize a trailing slash when used with the /? syntax and ignore_trailing_slash enabled" do
|
|
45
45
|
@router = HttpRouter.new(:ignore_trailing_slash => false)
|
|
46
|
-
route = @router.add("/test")
|
|
46
|
+
route = @router.add("/test").to(:test)
|
|
47
47
|
@router.recognize(Rack::MockRequest.env_for('/test/')).should be_nil
|
|
48
48
|
end
|
|
49
49
|
end
|
|
@@ -52,30 +52,30 @@ describe "HttpRouter#recognize" do
|
|
|
52
52
|
|
|
53
53
|
context "request methods" do
|
|
54
54
|
it "should pick a specific request_method" do
|
|
55
|
-
route = @router.
|
|
55
|
+
route = @router.post("/test").to(:test)
|
|
56
56
|
@router.recognize(Rack::MockRequest.env_for('/test', :method => 'POST')).route.should == route
|
|
57
57
|
@router.recognize(Rack::MockRequest.env_for('/test', :method => 'GET')).status.should == 405
|
|
58
58
|
@router.recognize(Rack::MockRequest.env_for('/test', :method => 'GET')).headers['Allow'].should == "POST"
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
it "should pick a specific request_method with other paths all through it" do
|
|
62
|
-
@router.
|
|
63
|
-
@router.
|
|
64
|
-
@router.
|
|
65
|
-
@router.
|
|
66
|
-
@router.add("/test/post").
|
|
67
|
-
@router.add("/test").
|
|
68
|
-
@router.recognize(Rack::MockRequest.env_for('/test', :method => 'POST')).
|
|
69
|
-
@router.recognize(Rack::MockRequest.env_for('/test', :method => 'GET')).
|
|
70
|
-
@router.recognize(Rack::MockRequest.env_for('/test', :method => 'PUT')).
|
|
71
|
-
@router.recognize(Rack::MockRequest.env_for('/test/post', :method => 'POST')).
|
|
72
|
-
@router.recognize(Rack::MockRequest.env_for('/test/post', :method => 'GET')).
|
|
73
|
-
@router.recognize(Rack::MockRequest.env_for('/test/post', :method => 'PUT')).
|
|
62
|
+
@router.post("/test").to(:test_post)
|
|
63
|
+
@router.post("/test/post").to(:test_post_post)
|
|
64
|
+
@router.get("/test").to(:test_get)
|
|
65
|
+
@router.get("/test/post").to(:test_post_get)
|
|
66
|
+
@router.add("/test/post").to(:test_post_catchall)
|
|
67
|
+
@router.add("/test").to(:test_catchall)
|
|
68
|
+
@router.recognize(Rack::MockRequest.env_for('/test', :method => 'POST')).dest.should == :test_post
|
|
69
|
+
@router.recognize(Rack::MockRequest.env_for('/test', :method => 'GET')).dest.should == :test_get
|
|
70
|
+
@router.recognize(Rack::MockRequest.env_for('/test', :method => 'PUT')).dest.should == :test_catchall
|
|
71
|
+
@router.recognize(Rack::MockRequest.env_for('/test/post', :method => 'POST')).dest.should == :test_post_post
|
|
72
|
+
@router.recognize(Rack::MockRequest.env_for('/test/post', :method => 'GET')).dest.should == :test_post_get
|
|
73
|
+
@router.recognize(Rack::MockRequest.env_for('/test/post', :method => 'PUT')).dest.should == :test_post_catchall
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
it "should move an endpoint to the non-specific request method when a more specific route gets added" do
|
|
77
|
-
@router.add("/test").name(:test_catchall)
|
|
78
|
-
@router.
|
|
77
|
+
@router.add("/test").name(:test_catchall).to(:test1)
|
|
78
|
+
@router.post("/test").request_method('POST').name(:test_post).to(:test2)
|
|
79
79
|
@router.recognize(Rack::MockRequest.env_for('/test', :method => 'POST')).route.named.should == :test_post
|
|
80
80
|
@router.recognize(Rack::MockRequest.env_for('/test', :method => 'PUT')).route.named.should == :test_catchall
|
|
81
81
|
end
|
|
@@ -84,7 +84,7 @@ describe "HttpRouter#recognize" do
|
|
|
84
84
|
|
|
85
85
|
context("dynamic paths") do
|
|
86
86
|
it "should recognize '/:variable'" do
|
|
87
|
-
route = @router.add('/:variable')
|
|
87
|
+
route = @router.add('/:variable').to(:test)
|
|
88
88
|
response = @router.recognize(Rack::MockRequest.env_for('/value'))
|
|
89
89
|
response.route.should == route
|
|
90
90
|
response.params.should == ['value']
|
|
@@ -92,15 +92,16 @@ describe "HttpRouter#recognize" do
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
it "should recognize '/test.:format'" do
|
|
95
|
-
route = @router.add('/test.:format')
|
|
95
|
+
route = @router.add('/test.:format').to(:test)
|
|
96
96
|
response = @router.recognize(Rack::MockRequest.env_for('/test.html'))
|
|
97
97
|
response.route.should == route
|
|
98
98
|
response.extension.should == 'html'
|
|
99
99
|
response.params_as_hash[:format].should == 'html'
|
|
100
|
+
@router.recognize(Rack::MockRequest.env_for('/test')).should be_nil
|
|
100
101
|
end
|
|
101
102
|
|
|
102
103
|
it "should recognize '/test(.:format)'" do
|
|
103
|
-
route = @router.add('/test(.:format)')
|
|
104
|
+
route = @router.add('/test(.:format)').to(:test)
|
|
104
105
|
response = @router.recognize(Rack::MockRequest.env_for('/test.html'))
|
|
105
106
|
response.route.should == route
|
|
106
107
|
response.extension.should == 'html'
|
|
@@ -112,7 +113,7 @@ describe "HttpRouter#recognize" do
|
|
|
112
113
|
end
|
|
113
114
|
|
|
114
115
|
it "should recognize '/:test.:format'" do
|
|
115
|
-
route = @router.add('/:test.:format')
|
|
116
|
+
route = @router.add('/:test.:format').to(:test)
|
|
116
117
|
response = @router.recognize(Rack::MockRequest.env_for('/hey.html'))
|
|
117
118
|
response.route.should == route
|
|
118
119
|
response.extension.should == 'html'
|
|
@@ -121,7 +122,7 @@ describe "HttpRouter#recognize" do
|
|
|
121
122
|
end
|
|
122
123
|
|
|
123
124
|
it "should recognize '/:test(.:format)'" do
|
|
124
|
-
route = @router.add('/:test(.:format)')
|
|
125
|
+
route = @router.add('/:test(.:format)').to(:test)
|
|
125
126
|
response = @router.recognize(Rack::MockRequest.env_for('/hey.html'))
|
|
126
127
|
response.route.should == route
|
|
127
128
|
response.extension.should == 'html'
|
|
@@ -136,13 +137,13 @@ describe "HttpRouter#recognize" do
|
|
|
136
137
|
|
|
137
138
|
context "globs" do
|
|
138
139
|
it "should recognize a glob" do
|
|
139
|
-
route = @router.add('/test/*variable')
|
|
140
|
+
route = @router.add('/test/*variable').to(:test)
|
|
140
141
|
response = @router.recognize(Rack::MockRequest.env_for('/test/one/two/three'))
|
|
141
142
|
response.route.should == route
|
|
142
143
|
response.params.should == [['one', 'two', 'three']]
|
|
143
144
|
end
|
|
144
145
|
it "should recognize a glob with a regexp" do
|
|
145
|
-
route = @router.add('/test/*variable/anymore'
|
|
146
|
+
route = @router.add('/test/*variable/anymore').matching(:variable => /^\d+$/).to(:test)
|
|
146
147
|
response = @router.recognize(Rack::MockRequest.env_for('/test/123/345/567/anymore'))
|
|
147
148
|
response.route.should == route
|
|
148
149
|
response.params.should == [['123', '345', '567']]
|
|
@@ -155,7 +156,7 @@ describe "HttpRouter#recognize" do
|
|
|
155
156
|
|
|
156
157
|
context("interstitial variables") do
|
|
157
158
|
it "should recognize interstitial variables" do
|
|
158
|
-
route = @router.add('/one-:variable-time')
|
|
159
|
+
route = @router.add('/one-:variable-time').to(:test)
|
|
159
160
|
response = @router.recognize(Rack::MockRequest.env_for('/one-value-time'))
|
|
160
161
|
response.route.should == route
|
|
161
162
|
response.params_as_hash[:variable].should == 'value'
|
|
@@ -164,7 +165,7 @@ describe "HttpRouter#recognize" do
|
|
|
164
165
|
|
|
165
166
|
context("dynamic greedy paths") do
|
|
166
167
|
it "should recognize greedy variables" do
|
|
167
|
-
route = @router.add('/:variable'
|
|
168
|
+
route = @router.add('/:variable').matching(:variable, /\d+/).to(:test)
|
|
168
169
|
response = @router.recognize(Rack::MockRequest.env_for('/123'))
|
|
169
170
|
response.route.should == route
|
|
170
171
|
response.params.should == ['123']
|
metadata
CHANGED
|
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
|
4
4
|
prerelease: false
|
|
5
5
|
segments:
|
|
6
6
|
- 0
|
|
7
|
-
- 0
|
|
8
7
|
- 1
|
|
9
|
-
|
|
8
|
+
- 0
|
|
9
|
+
version: 0.1.0
|
|
10
10
|
platform: ruby
|
|
11
11
|
authors:
|
|
12
12
|
- Joshua Hull
|
|
@@ -14,7 +14,7 @@ autorequire:
|
|
|
14
14
|
bindir: bin
|
|
15
15
|
cert_chain: []
|
|
16
16
|
|
|
17
|
-
date: 2010-05-
|
|
17
|
+
date: 2010-05-27 00:00:00 -04:00
|
|
18
18
|
default_executable:
|
|
19
19
|
dependencies: []
|
|
20
20
|
|