joshbuddy-usher 0.5.1 → 0.5.2
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 +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)
|