joshbuddy-usher 0.5.4 → 0.5.6
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 +6 -6
- data/VERSION.yml +2 -2
- data/lib/usher.rb +45 -37
- data/lib/usher/interface.rb +3 -0
- data/lib/usher/interface/rack_interface.rb +61 -30
- data/lib/usher/interface/rails3_interface.rb +2 -1
- data/lib/usher/interface/text_interface.rb +44 -0
- data/lib/usher/node.rb +17 -13
- data/lib/usher/route.rb +16 -14
- data/lib/usher/util/generate.rb +120 -79
- data/lib/usher/util/rack-mixins.rb +24 -0
- data/spec/private/generate_spec.rb +78 -25
- data/spec/private/rack/dispatch_spec.rb +61 -29
- data/spec/private/rack/generate_spec.rb +41 -0
- data/spec/private/recognize_spec.rb +20 -5
- metadata +6 -2
data/README.rdoc
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
= Usher
|
1
|
+
= Usher
|
2
2
|
|
3
3
|
Tree-based router library. Useful for (specifically) for Rails and Rack, but probably generally useful for anyone interested in doing routing. Based on Ilya Grigorik suggestion, turns out looking up in a hash and following a tree is faster than Krauter's massive regex approach.
|
4
4
|
|
@@ -19,13 +19,13 @@ Tree-based router library. Useful for (specifically) for Rails and Rack, but pro
|
|
19
19
|
From the rdoc:
|
20
20
|
|
21
21
|
Creates a route from +path+ and +options+
|
22
|
-
|
22
|
+
|
23
23
|
=== +path+
|
24
24
|
A path consists a mix of dynamic and static parts delimited by <tt>/</tt>
|
25
25
|
|
26
26
|
==== Dynamic
|
27
27
|
Dynamic parts are prefixed with either :, *. :variable matches only one part of the path, whereas *variable can match one or
|
28
|
-
more parts.
|
28
|
+
more parts.
|
29
29
|
|
30
30
|
<b>Example:</b>
|
31
31
|
<tt>/path/:variable/path</tt> would match
|
@@ -35,7 +35,7 @@ more parts.
|
|
35
35
|
* <tt>/path/one_more/path</tt>
|
36
36
|
|
37
37
|
In the above examples, 'test', 'something_else' and 'one_more' respectively would be bound to the key <tt>:variable</tt>.
|
38
|
-
However, <tt>/path/test/one_more/path</tt> would not be matched.
|
38
|
+
However, <tt>/path/test/one_more/path</tt> would not be matched.
|
39
39
|
|
40
40
|
<b>Example:</b>
|
41
41
|
<tt>/path/*variable/path</tt> would match
|
@@ -59,7 +59,7 @@ But not
|
|
59
59
|
As well, the same logic applies for * variables as well, where only parts matchable by the supplied regex will
|
60
60
|
actually be bound to the variable
|
61
61
|
|
62
|
-
Variables can also have a greedy regex matcher. These matchers ignore all delimiters, and continue matching for as long as much as their
|
62
|
+
Variables can also have a greedy regex matcher. These matchers ignore all delimiters, and continue matching for as long as much as their
|
63
63
|
regex allows.
|
64
64
|
|
65
65
|
<b>Example:</b>
|
@@ -110,7 +110,7 @@ For instance, the path, <tt>/path/something(.xml|.html)</tt> would only match <t
|
|
110
110
|
[body] # Response body
|
111
111
|
]
|
112
112
|
end
|
113
|
-
|
113
|
+
|
114
114
|
routes = Usher::Interface.for(:rack) do
|
115
115
|
add('/hello/:name').to(app)
|
116
116
|
end
|
data/VERSION.yml
CHANGED
data/lib/usher.rb
CHANGED
@@ -7,12 +7,12 @@ require File.join(File.dirname(__FILE__), 'usher', 'exceptions')
|
|
7
7
|
require File.join(File.dirname(__FILE__), 'usher', 'util')
|
8
8
|
|
9
9
|
class Usher
|
10
|
-
attr_reader :root, :named_routes, :routes, :splitter,
|
11
|
-
:delimiters, :delimiter_chars, :delimiters_regex,
|
10
|
+
attr_reader :root, :named_routes, :routes, :splitter,
|
11
|
+
:delimiters, :delimiter_chars, :delimiters_regex,
|
12
12
|
:parent_route, :generator
|
13
|
-
|
13
|
+
|
14
14
|
# Returns whether the route set is empty
|
15
|
-
#
|
15
|
+
#
|
16
16
|
# set = Usher.new
|
17
17
|
# set.empty? => true
|
18
18
|
# set.add_route('/test')
|
@@ -20,13 +20,13 @@ class Usher
|
|
20
20
|
def empty?
|
21
21
|
@routes.empty?
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
def route_count
|
25
25
|
@routes.size
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
# Resets the route set back to its initial state
|
29
|
-
#
|
29
|
+
#
|
30
30
|
# set = Usher.new
|
31
31
|
# set.add_route('/test')
|
32
32
|
# set.empty? => false
|
@@ -39,14 +39,14 @@ class Usher
|
|
39
39
|
@grapher = Grapher.new
|
40
40
|
end
|
41
41
|
alias clear! reset!
|
42
|
-
|
42
|
+
|
43
43
|
# Creates a route set, with options
|
44
|
-
#
|
44
|
+
#
|
45
45
|
# <tt>:delimiters</tt>: Array of Strings. (default <tt>['/', '.']</tt>). Delimiters used in path separation. Array must be single character strings.
|
46
|
-
#
|
46
|
+
#
|
47
47
|
# <tt>:valid_regex</tt>: String. (default <tt>'[0-9A-Za-z\$\-_\+!\*\',]+'</tt>). String that can be interpolated into regex to match
|
48
48
|
# valid character sequences within path.
|
49
|
-
#
|
49
|
+
#
|
50
50
|
# <tt>:request_methods</tt>: Array of Symbols. (default <tt>[:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]</tt>)
|
51
51
|
# Array of methods called against the request object for the purposes of matching route requirements.
|
52
52
|
def initialize(options = nil)
|
@@ -64,13 +64,13 @@ class Usher
|
|
64
64
|
def can_generate?
|
65
65
|
!@generator.nil?
|
66
66
|
end
|
67
|
-
|
67
|
+
|
68
68
|
def generator
|
69
69
|
@generator
|
70
70
|
end
|
71
|
-
|
71
|
+
|
72
72
|
# Adds a route referencable by +name+. See add_route for format +path+ and +options+.
|
73
|
-
#
|
73
|
+
#
|
74
74
|
# set = Usher.new
|
75
75
|
# set.add_named_route(:test_route, '/test')
|
76
76
|
def add_named_route(name, path, options = nil)
|
@@ -78,7 +78,7 @@ class Usher
|
|
78
78
|
end
|
79
79
|
|
80
80
|
# Deletes a route referencable by +name+. At least the path and conditions have to match the route you intend to delete.
|
81
|
-
#
|
81
|
+
#
|
82
82
|
# set = Usher.new
|
83
83
|
# set.delete_named_route(:test_route, '/test')
|
84
84
|
def delete_named_route(name, path, options = nil)
|
@@ -87,7 +87,7 @@ class Usher
|
|
87
87
|
end
|
88
88
|
|
89
89
|
# Attaches a +route+ to a +name+
|
90
|
-
#
|
90
|
+
#
|
91
91
|
# set = Usher.new
|
92
92
|
# route = set.add_route('/test')
|
93
93
|
# set.name(:test, route)
|
@@ -97,13 +97,13 @@ class Usher
|
|
97
97
|
end
|
98
98
|
|
99
99
|
# Creates a route from +path+ and +options+
|
100
|
-
#
|
100
|
+
#
|
101
101
|
# === +path+
|
102
102
|
# A path consists a mix of dynamic and static parts delimited by <tt>/</tt>
|
103
103
|
#
|
104
104
|
# ==== Dynamic
|
105
105
|
# Dynamic parts are prefixed with either :, *. :variable matches only one part of the path, whereas *variable can match one or
|
106
|
-
# more parts.
|
106
|
+
# more parts.
|
107
107
|
#
|
108
108
|
# <b>Example:</b>
|
109
109
|
# <tt>/path/:variable/path</tt> would match
|
@@ -113,7 +113,7 @@ class Usher
|
|
113
113
|
# * <tt>/path/one_more/path</tt>
|
114
114
|
#
|
115
115
|
# In the above examples, 'test', 'something_else' and 'one_more' respectively would be bound to the key <tt>:variable</tt>.
|
116
|
-
# However, <tt>/path/test/one_more/path</tt> would not be matched.
|
116
|
+
# However, <tt>/path/test/one_more/path</tt> would not be matched.
|
117
117
|
#
|
118
118
|
# <b>Example:</b>
|
119
119
|
# <tt>/path/*variable/path</tt> would match
|
@@ -127,22 +127,22 @@ class Usher
|
|
127
127
|
#
|
128
128
|
# <b>Example:</b>
|
129
129
|
# <tt>/product/{:id,\d+}</tt> would match
|
130
|
-
#
|
130
|
+
#
|
131
131
|
# * <tt>/product/123</tt>
|
132
132
|
# * <tt>/product/4521</tt>
|
133
|
-
#
|
133
|
+
#
|
134
134
|
# But not
|
135
135
|
# * <tt>/product/AE-35</tt>
|
136
|
-
#
|
136
|
+
#
|
137
137
|
# As well, the same logic applies for * variables as well, where only parts matchable by the supplied regex will
|
138
138
|
# actually be bound to the variable
|
139
139
|
#
|
140
|
-
# Variables can also have a greedy regex matcher. These matchers ignore all delimiters, and continue matching for as long as much as their
|
140
|
+
# Variables can also have a greedy regex matcher. These matchers ignore all delimiters, and continue matching for as long as much as their
|
141
141
|
# regex allows.
|
142
142
|
#
|
143
143
|
# <b>Example:</b>
|
144
144
|
# <tt>/product/{!id,hello/world|hello}</tt> would match
|
145
|
-
#
|
145
|
+
#
|
146
146
|
# * <tt>/product/hello/world</tt>
|
147
147
|
# * <tt>/product/hello</tt>
|
148
148
|
#
|
@@ -176,7 +176,7 @@ class Usher
|
|
176
176
|
end
|
177
177
|
|
178
178
|
# Deletes a route. At least the path and conditions have to match the route you intend to delete.
|
179
|
-
#
|
179
|
+
#
|
180
180
|
# set = Usher.new
|
181
181
|
# set.delete_route('/test')
|
182
182
|
def delete_route(path, options = nil)
|
@@ -188,7 +188,7 @@ class Usher
|
|
188
188
|
end
|
189
189
|
|
190
190
|
# Recognizes a +request+ and returns +nil+ or an Usher::Node::Response, which is a struct containing a Usher::Route::Path and an array of arrays containing the extracted parameters.
|
191
|
-
#
|
191
|
+
#
|
192
192
|
# Request = Struct.new(:path)
|
193
193
|
# set = Usher.new
|
194
194
|
# route = set.add_route('/test')
|
@@ -198,7 +198,7 @@ class Usher
|
|
198
198
|
end
|
199
199
|
|
200
200
|
# Recognizes a +path+ and returns +nil+ or an Usher::Node::Response, which is a struct containing a Usher::Route::Path and an array of arrays containing the extracted parameters. Convenience method for when recognizing on the request object is unneeded.
|
201
|
-
#
|
201
|
+
#
|
202
202
|
# Request = Struct.new(:path)
|
203
203
|
# set = Usher.new
|
204
204
|
# route = set.add_route('/test')
|
@@ -208,35 +208,43 @@ class Usher
|
|
208
208
|
end
|
209
209
|
|
210
210
|
# Recognizes a set of +parameters+ and gets the closest matching Usher::Route::Path or +nil+ if no route exists.
|
211
|
-
#
|
211
|
+
#
|
212
212
|
# set = Usher.new
|
213
213
|
# route = set.add_route('/:controller/:action')
|
214
214
|
# set.path_for_options({:controller => 'test', :action => 'action'}) == path.route => true
|
215
215
|
def path_for_options(options)
|
216
216
|
@grapher.find_matching_path(options)
|
217
217
|
end
|
218
|
-
|
218
|
+
|
219
219
|
def parent_route=(route)
|
220
220
|
@parent_route = route
|
221
221
|
routes.each{|r| r.parent_route = route}
|
222
222
|
end
|
223
|
-
|
223
|
+
|
224
224
|
def dup
|
225
225
|
replacement = super
|
226
226
|
original = self
|
227
|
+
inverted_named_routes = original.named_routes.invert
|
227
228
|
replacement.instance_eval do
|
229
|
+
@parser = nil
|
228
230
|
reset!
|
229
231
|
original.routes.each do |route|
|
230
|
-
|
231
|
-
|
232
|
+
new_route = route.dup
|
233
|
+
new_route.router = self
|
234
|
+
@root.add(new_route)
|
235
|
+
@routes << new_route
|
236
|
+
if name = inverted_named_routes[route]
|
237
|
+
@named_routes[name] = new_route
|
238
|
+
end
|
232
239
|
end
|
240
|
+
send(:generator=, original.generator.class.new) if original.can_generate?
|
233
241
|
rebuild_grapher!
|
234
242
|
end
|
235
243
|
replacement
|
236
244
|
end
|
237
|
-
|
245
|
+
|
238
246
|
private
|
239
|
-
|
247
|
+
|
240
248
|
attr_accessor :request_methods
|
241
249
|
attr_reader :valid_regex
|
242
250
|
|
@@ -247,7 +255,7 @@ class Usher
|
|
247
255
|
end
|
248
256
|
@generator
|
249
257
|
end
|
250
|
-
|
258
|
+
|
251
259
|
def delimiters=(delimiters)
|
252
260
|
@delimiters = delimiters
|
253
261
|
@delimiter_chars = @delimiters.collect{|d| d[0]}
|
@@ -274,12 +282,12 @@ class Usher
|
|
274
282
|
end
|
275
283
|
end
|
276
284
|
end
|
277
|
-
|
285
|
+
|
278
286
|
route = parser.generate_route(path, conditions, requirements, default_values, generate_with)
|
279
287
|
route.to(options) if options && !options.empty?
|
280
288
|
route
|
281
289
|
end
|
282
|
-
|
290
|
+
|
283
291
|
def rebuild_grapher!
|
284
292
|
@grapher = Grapher.new
|
285
293
|
@routes.each{|r| @grapher.add_route(r)}
|
data/lib/usher/interface.rb
CHANGED
@@ -6,6 +6,7 @@ class Usher
|
|
6
6
|
autoload :RackInterface, File.join(File.dirname(__FILE__), 'interface', 'rack_interface')
|
7
7
|
autoload :EmailInterface, File.join(File.dirname(__FILE__), 'interface', 'email_interface')
|
8
8
|
autoload :Rails3Interface, File.join(File.dirname(__FILE__), 'interface', 'rails3_interface')
|
9
|
+
autoload :TextInterface, File.join(File.dirname(__FILE__), 'interface', 'text_interface')
|
9
10
|
|
10
11
|
def self.for(type, &blk)
|
11
12
|
class_for(type).new(&blk)
|
@@ -25,6 +26,8 @@ class Usher
|
|
25
26
|
EmailInterface
|
26
27
|
when :rails3
|
27
28
|
Rails3Interface
|
29
|
+
when :text
|
30
|
+
TextInterface
|
28
31
|
end
|
29
32
|
|
30
33
|
end
|
@@ -3,28 +3,34 @@ require 'rack'
|
|
3
3
|
class Usher
|
4
4
|
module Interface
|
5
5
|
class RackInterface
|
6
|
-
|
6
|
+
|
7
7
|
attr_reader :router
|
8
|
-
|
8
|
+
attr_accessor :app
|
9
|
+
|
10
|
+
DEFAULT_APPLICATION = lambda do |env|
|
11
|
+
Rack::Response.new("No route found", 404).finish
|
12
|
+
end
|
13
|
+
|
9
14
|
class Builder < Rack::Builder
|
10
|
-
|
15
|
+
|
11
16
|
def initialize(&block)
|
12
17
|
@usher = Usher::Interface::RackInterface.new
|
13
18
|
super
|
14
19
|
end
|
15
|
-
|
20
|
+
|
16
21
|
def map(path, options = nil, &block)
|
17
22
|
@usher.add(path, options).to(&block)
|
18
23
|
@ins << @usher unless @ins.last == @usher
|
19
24
|
end
|
20
|
-
|
25
|
+
|
21
26
|
end
|
22
|
-
|
23
|
-
def initialize(&blk)
|
27
|
+
|
28
|
+
def initialize(app = nil, &blk)
|
29
|
+
@app = app || DEFAULT_APPLICATION
|
24
30
|
@router = Usher.new(:request_methods => [:request_method, :host, :port, :scheme], :generator => Usher::Util::Generators::URL.new)
|
25
31
|
instance_eval(&blk) if blk
|
26
32
|
end
|
27
|
-
|
33
|
+
|
28
34
|
def dup
|
29
35
|
new_one = super
|
30
36
|
original = self
|
@@ -33,15 +39,15 @@ class Usher
|
|
33
39
|
end
|
34
40
|
new_one
|
35
41
|
end
|
36
|
-
|
42
|
+
|
37
43
|
def add(path, options = nil)
|
38
44
|
@router.add_route(path, options)
|
39
|
-
|
40
|
-
|
45
|
+
end
|
46
|
+
|
41
47
|
def parent_route=(route)
|
42
48
|
@router.parent_route = route
|
43
49
|
end
|
44
|
-
|
50
|
+
|
45
51
|
def parent_route
|
46
52
|
@router.parent_route
|
47
53
|
end
|
@@ -51,30 +57,55 @@ class Usher
|
|
51
57
|
end
|
52
58
|
|
53
59
|
def call(env)
|
54
|
-
env['usher.params'] ||= {}
|
55
60
|
response = @router.recognize(request = Rack::Request.new(env), request.path_info)
|
56
|
-
if response
|
57
|
-
|
58
|
-
|
59
|
-
|
61
|
+
after_match(env, response) if response
|
62
|
+
determine_respondant(response).call(env)
|
63
|
+
end
|
64
|
+
|
65
|
+
def generate(route, options = nil)
|
66
|
+
@router.generator.generate(route, options)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Allows a hook to be placed for sub classes to make use of between matching
|
70
|
+
# and calling the application
|
71
|
+
#
|
72
|
+
# @api plugin
|
73
|
+
def after_match(env, response)
|
74
|
+
params = response.path.route.default_values ?
|
75
|
+
response.path.route.default_values.merge(Hash[response.params]) :
|
76
|
+
Hash[response.params]
|
77
|
+
|
78
|
+
env['usher.params'] ?
|
79
|
+
env['usher.params'].merge!(params) :
|
80
|
+
env['usher.params'] = params
|
81
|
+
|
82
|
+
# consume the path_info to the script_name
|
83
|
+
# response.remaining_path
|
84
|
+
consume_path!(env, response) if response.partial_match?
|
85
|
+
end
|
86
|
+
|
87
|
+
# Determines which application to respond with.
|
88
|
+
#
|
89
|
+
# Within the request when determine respondant is called
|
90
|
+
# If there is a matching route to an application, that
|
91
|
+
# application is called, Otherwise the middleware application is called.
|
92
|
+
#
|
93
|
+
# @api private
|
94
|
+
def determine_respondant(response)
|
95
|
+
unless response
|
96
|
+
app
|
60
97
|
else
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
# consume the path_info to the script_name response.remaining_path
|
65
|
-
env["SCRIPT_NAME"] << response.matched_path || ""
|
66
|
-
env["PATH_INFO"] = response.remaining_path || ""
|
67
|
-
|
68
|
-
env['usher.params'].merge!(params)
|
69
|
-
|
70
|
-
response.path.route.destination.call(env)
|
98
|
+
respondant = response.path.route.destination
|
99
|
+
respondant = app unless respondant.respond_to?(:call)
|
100
|
+
respondant
|
71
101
|
end
|
72
102
|
end
|
73
103
|
|
74
|
-
|
75
|
-
|
104
|
+
# Consume the path from path_info to script_name
|
105
|
+
def consume_path!(env, response)
|
106
|
+
env["SCRIPT_NAME"] = (env["SCRIPT_NAME"] + response.matched_path) || ""
|
107
|
+
env["PATH_INFO"] = response.remaining_path || ""
|
76
108
|
end
|
77
|
-
|
78
109
|
end
|
79
110
|
end
|
80
111
|
end
|
@@ -26,12 +26,13 @@ class Usher
|
|
26
26
|
@configurations_files << file
|
27
27
|
end
|
28
28
|
|
29
|
-
def reload
|
29
|
+
def reload
|
30
30
|
@usher.reset!
|
31
31
|
@configurations_files.each do |c|
|
32
32
|
Kernel.load(c)
|
33
33
|
end
|
34
34
|
end
|
35
|
+
alias_method :reload!, :reload
|
35
36
|
|
36
37
|
def call(env)
|
37
38
|
request = ActionDispatch::Request.new(env)
|