usher 0.4.8 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +12 -1
- data/Rakefile +5 -29
- data/VERSION.yml +2 -2
- data/lib/usher.rb +126 -37
- data/lib/usher/grapher.rb +5 -4
- data/lib/usher/interface.rb +12 -5
- data/lib/usher/interface/email_interface.rb +1 -1
- data/lib/usher/interface/rack_interface.rb +31 -13
- data/lib/usher/interface/rails2_2_interface.rb +3 -4
- data/lib/usher/interface/rails2_3_interface.rb +3 -4
- data/lib/usher/interface/rails3_interface.rb +57 -0
- data/lib/usher/node.rb +121 -73
- data/lib/usher/route.rb +45 -11
- data/lib/usher/route/path.rb +50 -11
- data/lib/usher/route/util.rb +65 -0
- data/lib/usher/route/variable.rb +22 -11
- data/lib/usher/splitter.rb +4 -141
- data/lib/usher/util.rb +6 -0
- data/lib/usher/util/generate.rb +129 -0
- data/lib/usher/util/parser.rb +145 -0
- data/spec/private/email/recognize_spec.rb +2 -4
- data/spec/private/generate_spec.rb +86 -32
- data/spec/private/grapher_spec.rb +5 -6
- data/spec/private/parser_spec.rb +75 -0
- data/spec/private/path_spec.rb +35 -4
- data/spec/private/rack/dispatch_spec.rb +100 -15
- data/spec/private/recognize_spec.rb +88 -50
- data/spec/spec_helper.rb +22 -0
- metadata +13 -7
- data/lib/usher/generate.rb +0 -131
- data/spec/private/split_spec.rb +0 -76
data/README.rdoc
CHANGED
@@ -5,12 +5,14 @@ Tree-based router library. Useful for (specifically) for Rails and Rack, but pro
|
|
5
5
|
== Features
|
6
6
|
|
7
7
|
* Understands single and path-globbing variables
|
8
|
+
* Understands arbitrary regex variables
|
8
9
|
* Arbitrary HTTP header requirements
|
9
10
|
* No optimization phase, so routes are always alterable after the fact
|
10
11
|
* Understands Proc and Regex transformations, validations
|
11
12
|
* Really, really fast
|
12
|
-
* Relatively light and happy code-base, should be easy and fun to alter
|
13
|
+
* Relatively light and happy code-base, should be easy and fun to alter (it hovers around 1,000 LOC, 800 for the core)
|
13
14
|
* Interface and implementation are separate, encouraging cross-pollination
|
15
|
+
* Works in 1.9!
|
14
16
|
|
15
17
|
== Route format
|
16
18
|
|
@@ -57,6 +59,15 @@ But not
|
|
57
59
|
As well, the same logic applies for * variables as well, where only parts matchable by the supplied regex will
|
58
60
|
actually be bound to the variable
|
59
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
|
63
|
+
regex allows.
|
64
|
+
|
65
|
+
<b>Example:</b>
|
66
|
+
<tt>/product/{!id,hello/world|hello}</tt> would match
|
67
|
+
|
68
|
+
* <tt>/product/hello/world</tt>
|
69
|
+
* <tt>/product/hello</tt>
|
70
|
+
|
60
71
|
==== Static
|
61
72
|
|
62
73
|
Static parts of literal character sequences. For instance, <tt>/path/something.html</tt> would match only the same path.
|
data/Rakefile
CHANGED
@@ -9,8 +9,11 @@ begin
|
|
9
9
|
s.homepage = "http://github.com/joshbuddy/usher"
|
10
10
|
s.authors = ["Joshua Hull"]
|
11
11
|
s.files = FileList["[A-Z]*", "{lib,spec,rails}/**/*"]
|
12
|
-
s.add_dependency 'fuzzyhash', '>=0.0.
|
13
|
-
s.rubyforge_project = 'joshbuddy-usher'
|
12
|
+
s.add_dependency 'fuzzyhash', '>=0.0.6'
|
13
|
+
s.rubyforge_project = 'joshbuddy-usher'
|
14
|
+
end
|
15
|
+
Jeweler::RubyforgeTasks.new do |rubyforge|
|
16
|
+
rubyforge.doc_task = "rdoc"
|
14
17
|
end
|
15
18
|
rescue LoadError
|
16
19
|
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
@@ -43,30 +46,3 @@ Rake::RDocTask.new do |rd|
|
|
43
46
|
rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
|
44
47
|
rd.rdoc_dir = 'rdoc'
|
45
48
|
end
|
46
|
-
|
47
|
-
# These are new tasks
|
48
|
-
begin
|
49
|
-
require 'rake/contrib/sshpublisher'
|
50
|
-
namespace :rubyforge do
|
51
|
-
|
52
|
-
desc "Release gem and RDoc documentation to RubyForge"
|
53
|
-
task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
|
54
|
-
|
55
|
-
namespace :release do
|
56
|
-
desc "Publish RDoc to RubyForge."
|
57
|
-
task :docs => [:rdoc] do
|
58
|
-
config = YAML.load(
|
59
|
-
File.read(File.expand_path('~/.rubyforge/user-config.yml'))
|
60
|
-
)
|
61
|
-
|
62
|
-
host = "#{config['username']}@rubyforge.org"
|
63
|
-
remote_dir = "/var/www/gforge-projects/joshbuddy-usher/"
|
64
|
-
local_dir = 'rdoc'
|
65
|
-
|
66
|
-
Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
rescue LoadError
|
71
|
-
puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
|
72
|
-
end
|
data/VERSION.yml
CHANGED
data/lib/usher.rb
CHANGED
@@ -4,14 +4,12 @@ require File.join(File.dirname(__FILE__), 'usher', 'grapher')
|
|
4
4
|
require File.join(File.dirname(__FILE__), 'usher', 'interface')
|
5
5
|
require File.join(File.dirname(__FILE__), 'usher', 'splitter')
|
6
6
|
require File.join(File.dirname(__FILE__), 'usher', 'exceptions')
|
7
|
+
require File.join(File.dirname(__FILE__), 'usher', 'util')
|
7
8
|
|
8
9
|
class Usher
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
attr_reader :tree, :named_routes, :route_count, :routes, :splitter, :delimiters
|
13
|
-
|
14
|
-
SymbolArraySorter = proc {|a,b| a.hash <=> b.hash} #:nodoc:
|
10
|
+
attr_reader :root, :named_routes, :routes, :splitter,
|
11
|
+
:delimiters, :delimiter_chars, :delimiters_regex,
|
12
|
+
:parent_route, :generator
|
15
13
|
|
16
14
|
# Returns whether the route set is empty
|
17
15
|
#
|
@@ -20,9 +18,13 @@ class Usher
|
|
20
18
|
# set.add_route('/test')
|
21
19
|
# set.empty? => false
|
22
20
|
def empty?
|
23
|
-
@
|
21
|
+
@routes.empty?
|
24
22
|
end
|
25
|
-
|
23
|
+
|
24
|
+
def route_count
|
25
|
+
@routes.size
|
26
|
+
end
|
27
|
+
|
26
28
|
# Resets the route set back to its initial state
|
27
29
|
#
|
28
30
|
# set = Usher.new
|
@@ -31,19 +33,15 @@ class Usher
|
|
31
33
|
# set.reset!
|
32
34
|
# set.empty? => true
|
33
35
|
def reset!
|
34
|
-
@
|
36
|
+
@root = Node.root(self, request_methods)
|
35
37
|
@named_routes = {}
|
36
38
|
@routes = []
|
37
|
-
@route_count = 0
|
38
39
|
@grapher = Grapher.new
|
39
40
|
end
|
40
41
|
alias clear! reset!
|
41
42
|
|
42
43
|
# Creates a route set, with options
|
43
44
|
#
|
44
|
-
# <tt>:globs_capture_separators</tt>: +true+ or +false+. (default +false+) Specifies whether glob matching will also include separators
|
45
|
-
# that are matched.
|
46
|
-
#
|
47
45
|
# <tt>:delimiters</tt>: Array of Strings. (default <tt>['/', '.']</tt>). Delimiters used in path separation. Array must be single character strings.
|
48
46
|
#
|
49
47
|
# <tt>:valid_regex</tt>: String. (default <tt>'[0-9A-Za-z\$\-_\+!\*\',]+'</tt>). String that can be interpolated into regex to match
|
@@ -52,15 +50,26 @@ class Usher
|
|
52
50
|
# <tt>:request_methods</tt>: Array of Symbols. (default <tt>[:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]</tt>)
|
53
51
|
# Array of methods called against the request object for the purposes of matching route requirements.
|
54
52
|
def initialize(options = nil)
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
@splitter = Splitter.for_delimiters(@delimiters, @valid_regex)
|
53
|
+
self.generator = options && options.delete(:generator)
|
54
|
+
self.delimiters = options && options.delete(:delimiters) || ['/', '.']
|
55
|
+
self.valid_regex = options && options.delete(:valid_regex) || '[0-9A-Za-z\$\-_\+!\*\',]+'
|
56
|
+
self.request_methods = options && options.delete(:request_methods) || [:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]
|
60
57
|
reset!
|
61
58
|
end
|
62
59
|
|
63
|
-
|
60
|
+
def parser
|
61
|
+
@parser ||= Util::Parser.for_delimiters(self, valid_regex)
|
62
|
+
end
|
63
|
+
|
64
|
+
def can_generate?
|
65
|
+
!@generator.nil?
|
66
|
+
end
|
67
|
+
|
68
|
+
def generator
|
69
|
+
@generator
|
70
|
+
end
|
71
|
+
|
72
|
+
# Adds a route referencable by +name+. See add_route for format +path+ and +options+.
|
64
73
|
#
|
65
74
|
# set = Usher.new
|
66
75
|
# set.add_named_route(:test_route, '/test')
|
@@ -68,6 +77,15 @@ class Usher
|
|
68
77
|
add_route(path, options).name(name)
|
69
78
|
end
|
70
79
|
|
80
|
+
# Deletes a route referencable by +name+. At least the path and conditions have to match the route you intend to delete.
|
81
|
+
#
|
82
|
+
# set = Usher.new
|
83
|
+
# set.delete_named_route(:test_route, '/test')
|
84
|
+
def delete_named_route(name, path, options = nil)
|
85
|
+
delete_route(path, options)
|
86
|
+
@named_routes.delete(name)
|
87
|
+
end
|
88
|
+
|
71
89
|
# Attaches a +route+ to a +name+
|
72
90
|
#
|
73
91
|
# set = Usher.new
|
@@ -119,6 +137,15 @@ class Usher
|
|
119
137
|
# As well, the same logic applies for * variables as well, where only parts matchable by the supplied regex will
|
120
138
|
# actually be bound to the variable
|
121
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
|
141
|
+
# regex allows.
|
142
|
+
#
|
143
|
+
# <b>Example:</b>
|
144
|
+
# <tt>/product/{!id,hello/world|hello}</tt> would match
|
145
|
+
#
|
146
|
+
# * <tt>/product/hello/world</tt>
|
147
|
+
# * <tt>/product/hello</tt>
|
148
|
+
#
|
122
149
|
# ==== Static
|
123
150
|
#
|
124
151
|
# Static parts of literal character sequences. For instance, <tt>/path/something.html</tt> would match only the same path.
|
@@ -140,25 +167,23 @@ class Usher
|
|
140
167
|
# * +conditions+ - Accepts any of the +request_methods+ specificied in the construction of Usher. This can be either a <tt>string</tt> or a regular expression.
|
141
168
|
# * Any other key is interpreted as a requirement for the variable of its name.
|
142
169
|
def add_route(path, options = nil)
|
143
|
-
|
144
|
-
|
145
|
-
default_values = options && options.delete(:default_values) || nil
|
146
|
-
generate_with = options && options.delete(:generate_with) || nil
|
147
|
-
if options
|
148
|
-
options.delete_if do |k, v|
|
149
|
-
if v.is_a?(Regexp) || v.is_a?(Proc)
|
150
|
-
(requirements ||= {})[k] = v
|
151
|
-
true
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
155
|
-
route = Route.new(path, self, conditions, requirements, default_values, generate_with)
|
156
|
-
route.to(options) if options && !options.empty?
|
157
|
-
|
158
|
-
@tree.add(route)
|
170
|
+
route = get_route(path, options)
|
171
|
+
@root.add(route)
|
159
172
|
@routes << route
|
160
173
|
@grapher.add_route(route)
|
161
|
-
|
174
|
+
route.parent_route = parent_route if parent_route
|
175
|
+
route
|
176
|
+
end
|
177
|
+
|
178
|
+
# Deletes a route. At least the path and conditions have to match the route you intend to delete.
|
179
|
+
#
|
180
|
+
# set = Usher.new
|
181
|
+
# set.delete_route('/test')
|
182
|
+
def delete_route(path, options = nil)
|
183
|
+
route = get_route(path, options)
|
184
|
+
@root.delete(route)
|
185
|
+
@routes = @root.unique_routes
|
186
|
+
rebuild_grapher!
|
162
187
|
route
|
163
188
|
end
|
164
189
|
|
@@ -169,7 +194,17 @@ class Usher
|
|
169
194
|
# route = set.add_route('/test')
|
170
195
|
# set.recognize(Request.new('/test')).path.route == route => true
|
171
196
|
def recognize(request, path = request.path)
|
172
|
-
@
|
197
|
+
@root.find(self, request, path, @splitter.url_split(path))
|
198
|
+
end
|
199
|
+
|
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
|
+
#
|
202
|
+
# Request = Struct.new(:path)
|
203
|
+
# set = Usher.new
|
204
|
+
# route = set.add_route('/test')
|
205
|
+
# set.recognize_path('/test').path.route == route => true
|
206
|
+
def recognize_path(path)
|
207
|
+
recognize(nil, path)
|
173
208
|
end
|
174
209
|
|
175
210
|
# Recognizes a set of +parameters+ and gets the closest matching Usher::Route::Path or +nil+ if no route exists.
|
@@ -181,4 +216,58 @@ class Usher
|
|
181
216
|
@grapher.find_matching_path(options)
|
182
217
|
end
|
183
218
|
|
219
|
+
def parent_route=(route)
|
220
|
+
@parent_route = route
|
221
|
+
routes.each{|r| r.parent_route = route}
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
|
226
|
+
attr_accessor :request_methods
|
227
|
+
attr_reader :valid_regex
|
228
|
+
|
229
|
+
def generator=(generator)
|
230
|
+
if generator
|
231
|
+
@generator = generator
|
232
|
+
@generator.usher = self
|
233
|
+
end
|
234
|
+
@generator
|
235
|
+
end
|
236
|
+
|
237
|
+
def delimiters=(delimiters)
|
238
|
+
@delimiters = delimiters
|
239
|
+
@delimiter_chars = @delimiters.collect{|d| d[0]}
|
240
|
+
@delimiters_regex = @delimiters.collect{|d| Regexp.quote(d)} * '|'
|
241
|
+
@delimiters
|
242
|
+
end
|
243
|
+
|
244
|
+
def valid_regex=(valid_regex)
|
245
|
+
@valid_regex = valid_regex
|
246
|
+
@splitter = Splitter.for_delimiters(self, @valid_regex)
|
247
|
+
@valid_regex
|
248
|
+
end
|
249
|
+
|
250
|
+
def get_route(path, options = nil)
|
251
|
+
conditions = options && options.delete(:conditions) || nil
|
252
|
+
requirements = options && options.delete(:requirements) || nil
|
253
|
+
default_values = options && options.delete(:default_values) || nil
|
254
|
+
generate_with = options && options.delete(:generate_with) || nil
|
255
|
+
if options
|
256
|
+
options.delete_if do |k, v|
|
257
|
+
if v.is_a?(Regexp) || v.is_a?(Proc)
|
258
|
+
(requirements ||= {})[k] = v
|
259
|
+
true
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
route = parser.generate_route(path, conditions, requirements, default_values, generate_with)
|
265
|
+
route.to(options) if options && !options.empty?
|
266
|
+
route
|
267
|
+
end
|
268
|
+
|
269
|
+
def rebuild_grapher!
|
270
|
+
@grapher = Grapher.new
|
271
|
+
@routes.each{|r| @grapher.add_route(r)}
|
272
|
+
end
|
184
273
|
end
|
data/lib/usher/grapher.rb
CHANGED
@@ -14,7 +14,7 @@ class Usher
|
|
14
14
|
|
15
15
|
def add_route(route)
|
16
16
|
route.paths.each do |path|
|
17
|
-
|
17
|
+
if path.dynamic?
|
18
18
|
path.dynamic_keys.each do |k|
|
19
19
|
@orders[path.dynamic_keys.size][k] << path
|
20
20
|
@key_count[k] += 1
|
@@ -50,16 +50,17 @@ class Usher
|
|
50
50
|
end
|
51
51
|
set.size.downto(1) do |o|
|
52
52
|
set.each do |k|
|
53
|
-
@orders[o][k].each
|
53
|
+
@orders[o][k].each do |r|
|
54
54
|
if r.can_generate_from?(set)
|
55
55
|
@cache[set] = r
|
56
56
|
return r
|
57
57
|
end
|
58
|
-
|
58
|
+
end
|
59
59
|
end
|
60
60
|
end
|
61
|
-
nil
|
62
61
|
end
|
62
|
+
nil
|
63
63
|
end
|
64
|
+
|
64
65
|
end
|
65
66
|
end
|
data/lib/usher/interface.rb
CHANGED
@@ -5,19 +5,26 @@ class Usher
|
|
5
5
|
autoload :MerbInterface, File.join(File.dirname(__FILE__), 'interface', 'merb_interface')
|
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
|
+
autoload :Rails3Interface, File.join(File.dirname(__FILE__), 'interface', 'rails3_interface')
|
8
9
|
|
9
10
|
def self.for(type, &blk)
|
11
|
+
class_for(type).new(&blk)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.class_for(type)
|
10
15
|
case type
|
11
16
|
when :rails2_2
|
12
|
-
Rails2_2Interface
|
17
|
+
Rails2_2Interface
|
13
18
|
when :rails2_3
|
14
|
-
Rails2_3Interface
|
19
|
+
Rails2_3Interface
|
15
20
|
when :merb
|
16
|
-
MerbInterface
|
21
|
+
MerbInterface
|
17
22
|
when :rack
|
18
|
-
RackInterface
|
23
|
+
RackInterface
|
19
24
|
when :email
|
20
|
-
EmailInterface
|
25
|
+
EmailInterface
|
26
|
+
when :rails3
|
27
|
+
Rails3Interface
|
21
28
|
end
|
22
29
|
|
23
30
|
end
|
@@ -3,7 +3,7 @@ class Usher
|
|
3
3
|
class EmailInterface
|
4
4
|
|
5
5
|
def initialize(&blk)
|
6
|
-
@routes = Usher.new(:delimiters => ['@', '-', '.'], :valid_regex => '[\+a-zA-Z0-9]+'
|
6
|
+
@routes = Usher.new(:delimiters => ['@', '-', '.'], :valid_regex => '[\+a-zA-Z0-9]+')
|
7
7
|
instance_eval(&blk) if blk
|
8
8
|
end
|
9
9
|
|
@@ -4,34 +4,52 @@ class Usher
|
|
4
4
|
module Interface
|
5
5
|
class RackInterface
|
6
6
|
|
7
|
-
attr_accessor :routes
|
8
|
-
|
9
7
|
def initialize(&blk)
|
10
|
-
@
|
11
|
-
@generator = Usher::Generators::URL.new(@routes)
|
8
|
+
@router = Usher.new(:request_methods => [:method, :host, :port, :scheme], :generator => Usher::Util::Generators::URL.new)
|
12
9
|
instance_eval(&blk) if blk
|
13
10
|
end
|
14
11
|
|
15
12
|
def add(path, options = nil)
|
16
|
-
@
|
13
|
+
@router.add_route(path, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def parent_route=(route)
|
17
|
+
@router.parent_route = route
|
18
|
+
end
|
19
|
+
|
20
|
+
def parent_route
|
21
|
+
@router.parent_route
|
17
22
|
end
|
18
23
|
|
19
24
|
def reset!
|
20
|
-
@
|
25
|
+
@router.reset!
|
21
26
|
end
|
22
27
|
|
23
28
|
def call(env)
|
24
|
-
|
25
|
-
|
26
|
-
response.
|
27
|
-
|
28
|
-
|
29
|
+
env['usher.params'] ||= {}
|
30
|
+
response = @router.recognize(request = Rack::Request.new(env), request.path_info)
|
31
|
+
if response.nil?
|
32
|
+
body = "No route found"
|
33
|
+
headers = {"Content-Type" => "text/plain", "Content-Length" => body.length.to_s}
|
34
|
+
[404, headers, [body]]
|
35
|
+
else
|
36
|
+
params = response.path.route.default_values || {}
|
37
|
+
response.params.each{ |hk| params[hk.first] = hk.last}
|
38
|
+
|
39
|
+
# consume the path_info to the script_name response.remaining_path
|
40
|
+
env["SCRIPT_NAME"] << response.matched_path || ""
|
41
|
+
env["PATH_INFO"] = response.remaining_path || ""
|
42
|
+
|
43
|
+
env['usher.params'].merge!(params)
|
44
|
+
|
45
|
+
response.path.route.destination.call(env)
|
46
|
+
end
|
29
47
|
end
|
30
48
|
|
31
49
|
def generate(route, params = nil, options = nil)
|
32
|
-
@generator.generate(route, params, options)
|
50
|
+
@usher.generator.generate(route, params, options)
|
33
51
|
end
|
34
52
|
|
35
53
|
end
|
36
54
|
end
|
37
|
-
end
|
55
|
+
end
|