joshbuddy-usher 0.5.1 → 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +2 -0
- data/Rakefile +5 -27
- data/VERSION.yml +2 -2
- data/lib/usher.rb +104 -67
- data/lib/usher/grapher.rb +2 -1
- data/lib/usher/interface.rb +12 -5
- data/lib/usher/interface/rack_interface.rb +31 -13
- data/lib/usher/interface/rails2_2_interface.rb +2 -3
- data/lib/usher/interface/rails2_3_interface.rb +2 -3
- data/lib/usher/interface/rails3_interface.rb +57 -0
- data/lib/usher/node.rb +76 -50
- data/lib/usher/route.rb +44 -10
- data/lib/usher/route/path.rb +22 -9
- data/lib/usher/util/generate.rb +12 -14
- data/lib/usher/util/parser.rb +50 -0
- data/spec/private/generate_spec.rb +86 -32
- data/spec/private/grapher_spec.rb +5 -6
- data/spec/private/path_spec.rb +35 -4
- data/spec/private/rack/dispatch_spec.rb +100 -15
- data/spec/private/recognize_spec.rb +68 -50
- data/spec/spec_helper.rb +22 -0
- metadata +6 -3
data/README.rdoc
CHANGED
@@ -12,6 +12,7 @@ Tree-based router library. Useful for (specifically) for Rails and Rack, but pro
|
|
12
12
|
* Really, really fast
|
13
13
|
* Relatively light and happy code-base, should be easy and fun to alter (it hovers around 1,000 LOC, 800 for the core)
|
14
14
|
* Interface and implementation are separate, encouraging cross-pollination
|
15
|
+
* Works in 1.9!
|
15
16
|
|
16
17
|
== Route format
|
17
18
|
|
@@ -67,6 +68,7 @@ regex allows.
|
|
67
68
|
* <tt>/product/hello/world</tt>
|
68
69
|
* <tt>/product/hello</tt>
|
69
70
|
|
71
|
+
|
70
72
|
==== Static
|
71
73
|
|
72
74
|
Static parts of literal character sequences. For instance, <tt>/path/something.html</tt> would match only the same path.
|
data/Rakefile
CHANGED
@@ -10,6 +10,11 @@ begin
|
|
10
10
|
s.authors = ["Joshua Hull"]
|
11
11
|
s.files = FileList["[A-Z]*", "{lib,spec,rails}/**/*"]
|
12
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"
|
17
|
+
rubyforge.remote_doc_path = ''
|
13
18
|
end
|
14
19
|
rescue LoadError
|
15
20
|
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
@@ -42,30 +47,3 @@ Rake::RDocTask.new do |rd|
|
|
42
47
|
rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
|
43
48
|
rd.rdoc_dir = 'rdoc'
|
44
49
|
end
|
45
|
-
|
46
|
-
# These are new tasks
|
47
|
-
begin
|
48
|
-
require 'rake/contrib/sshpublisher'
|
49
|
-
namespace :rubyforge do
|
50
|
-
|
51
|
-
desc "Release gem and RDoc documentation to RubyForge"
|
52
|
-
task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
|
53
|
-
|
54
|
-
namespace :release do
|
55
|
-
desc "Publish RDoc to RubyForge."
|
56
|
-
task :docs => [:rdoc] do
|
57
|
-
config = YAML.load(
|
58
|
-
File.read(File.expand_path('~/.rubyforge/user-config.yml'))
|
59
|
-
)
|
60
|
-
|
61
|
-
host = "#{config['username']}@rubyforge.org"
|
62
|
-
remote_dir = "/var/www/gforge-projects/joshbuddy-usher/"
|
63
|
-
local_dir = 'rdoc'
|
64
|
-
|
65
|
-
Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
rescue LoadError
|
70
|
-
puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
|
71
|
-
end
|
data/VERSION.yml
CHANGED
data/lib/usher.rb
CHANGED
@@ -7,11 +7,9 @@ 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
|
-
|
11
|
-
|
12
|
-
|
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,10 +33,9 @@ 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!
|
@@ -49,20 +50,26 @@ class Usher
|
|
49
50
|
# <tt>:request_methods</tt>: Array of Symbols. (default <tt>[:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]</tt>)
|
50
51
|
# Array of methods called against the request object for the purposes of matching route requirements.
|
51
52
|
def initialize(options = nil)
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
@request_methods = options && options.delete(:request_methods) || [:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]
|
57
|
-
@splitter = Splitter.for_delimiters(self, @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]
|
58
57
|
reset!
|
59
58
|
end
|
60
59
|
|
61
60
|
def parser
|
62
|
-
@parser ||= Util::Parser.for_delimiters(self,
|
61
|
+
@parser ||= Util::Parser.for_delimiters(self, valid_regex)
|
63
62
|
end
|
64
63
|
|
65
|
-
|
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+.
|
66
73
|
#
|
67
74
|
# set = Usher.new
|
68
75
|
# set.add_named_route(:test_route, '/test')
|
@@ -70,6 +77,15 @@ class Usher
|
|
70
77
|
add_route(path, options).name(name)
|
71
78
|
end
|
72
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
|
+
|
73
89
|
# Attaches a +route+ to a +name+
|
74
90
|
#
|
75
91
|
# set = Usher.new
|
@@ -150,57 +166,24 @@ class Usher
|
|
150
166
|
# * +requirements+ - After transformation, tests the condition using ===. If it returns false, it raises an <tt>Usher::ValidationException</tt>
|
151
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.
|
152
168
|
# * Any other key is interpreted as a requirement for the variable of its name.
|
153
|
-
def add_route(
|
154
|
-
|
155
|
-
|
156
|
-
default_values = options && options.delete(:default_values) || nil
|
157
|
-
generate_with = options && options.delete(:generate_with) || nil
|
158
|
-
if options
|
159
|
-
options.delete_if do |k, v|
|
160
|
-
if v.is_a?(Regexp) || v.is_a?(Proc)
|
161
|
-
(requirements ||= {})[k] = v
|
162
|
-
true
|
163
|
-
end
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
unprocessed_path = parser.parse(unprocessed_path, requirements, default_values) if unprocessed_path.is_a?(String)
|
168
|
-
|
169
|
-
unless unprocessed_path.first.is_a?(Route::Util::Group)
|
170
|
-
group = Usher::Route::Util::Group.new(:all, nil)
|
171
|
-
unprocessed_path.each{|p| group << p}
|
172
|
-
unprocessed_path = group
|
173
|
-
end
|
174
|
-
|
175
|
-
paths = Route::Util.expand_path(unprocessed_path)
|
176
|
-
|
177
|
-
paths.each do |path|
|
178
|
-
path.each_with_index do |part, index|
|
179
|
-
part.default_value = default_values[part.name] if part.is_a?(Usher::Route::Variable) && default_values && default_values[part.name]
|
180
|
-
case part
|
181
|
-
when Usher::Route::Variable::Glob
|
182
|
-
part.look_ahead = path[index + 1, path.size].find{|p| !p.is_a?(Usher::Route::Variable) && !delimiter_chars.include?(p[0])} || nil
|
183
|
-
when Usher::Route::Variable
|
184
|
-
part.look_ahead = path[index + 1, path.size].find{|p| delimiter_chars.include?(p[0])} || delimiters.first
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
route = Route.new(
|
190
|
-
paths,
|
191
|
-
self,
|
192
|
-
conditions,
|
193
|
-
requirements,
|
194
|
-
default_values,
|
195
|
-
generate_with
|
196
|
-
)
|
197
|
-
|
198
|
-
route.to(options) if options && !options.empty?
|
199
|
-
|
200
|
-
@tree.add(route)
|
169
|
+
def add_route(path, options = nil)
|
170
|
+
route = get_route(path, options)
|
171
|
+
@root.add(route)
|
201
172
|
@routes << route
|
202
173
|
@grapher.add_route(route)
|
203
|
-
|
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!
|
204
187
|
route
|
205
188
|
end
|
206
189
|
|
@@ -211,7 +194,7 @@ class Usher
|
|
211
194
|
# route = set.add_route('/test')
|
212
195
|
# set.recognize(Request.new('/test')).path.route == route => true
|
213
196
|
def recognize(request, path = request.path)
|
214
|
-
@
|
197
|
+
@root.find(self, request, path, @splitter.url_split(path))
|
215
198
|
end
|
216
199
|
|
217
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.
|
@@ -233,4 +216,58 @@ class Usher
|
|
233
216
|
@grapher.find_matching_path(options)
|
234
217
|
end
|
235
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
|
236
273
|
end
|
data/lib/usher/grapher.rb
CHANGED
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
|
@@ -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::Util::Generators::URL.new(@routes)
|
8
|
+
@router = Usher.new(:request_methods => [:request_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
|
@@ -12,8 +12,7 @@ class Usher
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def reset!
|
15
|
-
@usher ||= Usher.new
|
16
|
-
@url_generator ||= Usher::Util::Generators::URL.new(@usher)
|
15
|
+
@usher ||= Usher.new(:generator => Usher::Util::Generators::URL.new)
|
17
16
|
@module ||= Module.new
|
18
17
|
@module.instance_methods.each do |selector|
|
19
18
|
@module.class_eval { remove_method selector }
|
@@ -86,7 +85,7 @@ class Usher
|
|
86
85
|
end
|
87
86
|
|
88
87
|
def generate_url(route, params)
|
89
|
-
@
|
88
|
+
@usher.generator.generate(route, params)
|
90
89
|
end
|
91
90
|
|
92
91
|
def path_for_options(options)
|
@@ -72,8 +72,7 @@ class Usher
|
|
72
72
|
end
|
73
73
|
|
74
74
|
def reset!
|
75
|
-
@router = Usher.new
|
76
|
-
@url_generator = Usher::Util::Generators::URL.new(@router)
|
75
|
+
@router = Usher.new(:generator => Usher::Util::Generators::URL.new)
|
77
76
|
@configuration_files = []
|
78
77
|
@module ||= Module.new
|
79
78
|
@controller_route_added = false
|
@@ -123,7 +122,7 @@ class Usher
|
|
123
122
|
end
|
124
123
|
|
125
124
|
def generate_url(route, params)
|
126
|
-
@
|
125
|
+
@router.generator.generate(route, params)
|
127
126
|
end
|
128
127
|
|
129
128
|
def path_for_options(options)
|