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 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