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 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.3'
13
- s.rubyforge_project = 'joshbuddy-usher' # This line would be new
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
@@ -1,4 +1,4 @@
1
1
  ---
2
- :minor: 4
3
- :patch: 8
4
2
  :major: 0
3
+ :minor: 5
4
+ :patch: 1
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
- autoload :Generators, File.join(File.dirname(__FILE__), 'usher', 'generate')
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
- @route_count.zero?
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
- @tree = Node.root(self, @request_methods, @globs_capture_separators)
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
- @globs_capture_separators = options && options.key?(:globs_capture_separators) ? options.delete(:globs_capture_separators) : false
56
- @delimiters = options && options.delete(:delimiters) || ['/', '.']
57
- @valid_regex = options && options.delete(:valid_regex) || '[0-9A-Za-z\$\-_\+!\*\',]+'
58
- @request_methods = options && options.delete(:request_methods) || [:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]
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
- # Adds a route referencable by +name+. Sett add_route for format +path+ and +options+.
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
- conditions = options && options.delete(:conditions) || nil
144
- requirements = options && options.delete(:requirements) || nil
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
- @route_count += 1
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
- @tree.find(self, request, @splitter.url_split(path))
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
- unless path.dynamic_keys.size.zero?
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 { |r|
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
@@ -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.new(&blk)
17
+ Rails2_2Interface
13
18
  when :rails2_3
14
- Rails2_3Interface.new(&blk)
19
+ Rails2_3Interface
15
20
  when :merb
16
- MerbInterface.new(&blk)
21
+ MerbInterface
17
22
  when :rack
18
- RackInterface.new(&blk)
23
+ RackInterface
19
24
  when :email
20
- EmailInterface.new(&blk)
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]+', :globs_capture_separators => true)
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
- @routes = Usher.new(:request_methods => [:method, :host, :port, :scheme])
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
- @routes.add_route(path, options)
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
- @routes.reset!
25
+ @router.reset!
21
26
  end
22
27
 
23
28
  def call(env)
24
- response = @routes.recognize(Rack::Request.new(env))
25
- params = {}
26
- response.params.each{ |hk| params[hk.first] = hk.last}
27
- env['usher.params'] = params
28
- response.path.route.destination.call(env)
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