usher 0.4.8 → 0.5.1
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 +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
|