joshbuddy-usher 0.2.0 → 0.2.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
@@ -1,8 +1,16 @@
1
1
  = Usher
2
2
 
3
- Tree-based router for Ruby on Rails.
3
+ Tree-based router library. Useful for (specifically) for Rails and Rack, but probably generally useful for anyone interested in doing routing. Based on Ilya Grigorik suggestion, turns out looking up in a hash and following a tree is faster than Krauter's massive regex approach.
4
4
 
5
- This is a tree-based router (based on Ilya Grigorik suggestion). Turns out looking up in a hash and following a tree is faster than Krauter's massive regex approach, so why not? I said, Heck Yes, and here we are.
5
+ == Features
6
+
7
+ * Understands single and path-globbing variables
8
+ * Arbitrary HTTP header requirements
9
+ * No optimization phase, so routes are always alterable after the fact
10
+ * Understands Proc and Regex transformations, validations
11
+ * Really, really fast
12
+ * Relatively light and happy code-base, should be easy and fun to alter
13
+ * Interface and implementation are separate, encouraging cross-pollination
6
14
 
7
15
  == Route format
8
16
 
@@ -87,15 +95,13 @@ From the rdoc:
87
95
 
88
96
  * add support for () optional parts
89
97
  * Add support for arbitrary HTTP header checks
98
+ * Emit exceptions inline with relevant interfaces
99
+ * More RDoc! (optionally cowbell)
90
100
 
91
101
  == TODO
92
102
 
93
103
  * Make it integrate with merb
94
104
  * Make it integrate with rails3
95
105
  * Create decent DSL for use with rack
96
- * Emit exceptions inline with relevant interfaces
97
- * More RDoc! (optionally cowbell)
98
-
99
- Looks about 20-50% faster than the router Rails ships with for non-trivial cases.
100
106
 
101
107
  (Let me show you to your request)
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
+ :patch: 1
2
3
  :major: 0
3
4
  :minor: 2
4
- :patch: 0
@@ -1,5 +1,7 @@
1
1
  class Usher
2
2
  class UnrecognizedException < RuntimeError; end
3
- class ValidationException < RuntimeError; end
3
+ class ValidationException < RuntimeError
4
+
5
+ end
4
6
  class MissingParameterException < RuntimeError; end
5
7
  end
data/lib/usher/grapher.rb CHANGED
@@ -1,8 +1,5 @@
1
- require 'singleton'
2
-
3
1
  class Usher
4
2
  class Grapher
5
- include Singleton
6
3
 
7
4
  def initialize
8
5
  reset!
@@ -21,9 +21,9 @@ class Usher
21
21
  end
22
22
 
23
23
  def call(env)
24
- (path, params) = @routes.recognize(Request.new(env['REQUEST_URI'], env['REQUEST_METHOD']))
25
- env['usher.params'] = params.inject({}){|h,(k,v)| h[k]=v; h }
26
- path.route.params.call(env)
24
+ response = @routes.recognize(Request.new(env['REQUEST_URI'], env['REQUEST_METHOD']))
25
+ env['usher.params'] = response.params.inject({}){|h,(k,v)| h[k]=v; h }
26
+ response.path.route.params.call(env)
27
27
  end
28
28
 
29
29
  end
@@ -43,9 +43,9 @@ class Usher
43
43
  end
44
44
 
45
45
  def recognize(request)
46
- (path, params_list) = @usher.recognize(request)
47
- params = params_list.inject({}){|h,(k,v)| h[k]=v; h }
48
- request.path_parameters = (params_list.empty? ? path.route.params : path.route.params.merge(params)).with_indifferent_access
46
+ node = @usher.recognize(request)
47
+ params = node.params.inject({}){|h,(k,v)| h[k]=v; h }
48
+ request.path_parameters = (node.params.empty? ? node.path.route.params : node.path.route.params.merge(params)).with_indifferent_access
49
49
  "#{request.path_parameters[:controller].camelize}Controller".constantize
50
50
  rescue
51
51
  raise ActionController::RoutingError, "No route matches #{request.path.inspect} with #{request.inspect}"
data/lib/usher/node.rb CHANGED
@@ -7,6 +7,7 @@ class Usher
7
7
  class Node
8
8
 
9
9
  ConditionalTypes = [:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method]
10
+ Response = Struct.new(:path, :params)
10
11
 
11
12
  attr_reader :lookup
12
13
  attr_accessor :terminates, :exclusive_type, :parent, :value
@@ -70,14 +71,14 @@ class Usher
70
71
  route.paths.each do |path|
71
72
  parts = path.parts.dup
72
73
  ConditionalTypes.each do |type|
73
- parts.push(Route::Http.new(type, route.conditions[type])) if route.conditions[type]
74
+ parts.push(Route::RequestMethod.new(type, route.conditions[type])) if route.conditions[type]
74
75
  end
75
76
 
76
77
  current_node = self
77
78
  until parts.size.zero?
78
79
  key = parts.shift
79
80
  target_node = case key
80
- when Route::Http
81
+ when Route::RequestMethod
81
82
  if current_node.exclusive_type == key.type
82
83
  current_node.lookup[key.value] ||= Node.new(current_node, key)
83
84
  elsif current_node.lookup.empty?
@@ -85,12 +86,12 @@ class Usher
85
86
  current_node.lookup[key.value] ||= Node.new(current_node, key)
86
87
  else
87
88
  parts.unshift(key)
88
- current_node.lookup[nil] ||= Node.new(current_node, Route::Http.new(current_node.exclusive_type, nil))
89
+ current_node.lookup[nil] ||= Node.new(current_node, Route::RequestMethod.new(current_node.exclusive_type, nil))
89
90
  end
90
91
  else
91
92
  if current_node.exclusive_type
92
93
  parts.unshift(key)
93
- current_node.lookup[nil] ||= Node.new(current_node, Route::Http.new(current_node.exclusive_type, nil))
94
+ current_node.lookup[nil] ||= Node.new(current_node, Route::RequestMethod.new(current_node.exclusive_type, nil))
94
95
  else
95
96
  current_node.lookup[key.is_a?(Route::Variable) ? nil : key] ||= Node.new(current_node, key)
96
97
  end
@@ -113,7 +114,7 @@ class Usher
113
114
  end
114
115
  elsif path.size.zero? && !part
115
116
  if terminates?
116
- [terminates, params]
117
+ Response.new(terminates, params)
117
118
  else
118
119
  nil
119
120
  end
@@ -121,13 +122,8 @@ class Usher
121
122
  next_part.find(request, path, params)
122
123
  elsif next_part = @lookup[nil]
123
124
  if next_part.value.is_a?(Route::Variable)
124
- case t = next_part.value.transformer
125
- when Proc
126
- part = t.call(part)
127
- when Symbol
128
- part = part.send(t)
129
- end
130
- raise ValidationException.new("#{part} does not conform to #{next_part.value.validator}") if next_part.value.validator && (not next_part.value.validator === part)
125
+ part = next_part.value.transform!(part)
126
+ next_part.value.valid!(part)
131
127
  case next_part.value.type
132
128
  when :*
133
129
  params << [next_part.value.name, []]
@@ -1,9 +1,11 @@
1
+ require 'set'
2
+
1
3
  class Usher
2
4
  class Route
3
5
  class Path
4
6
 
5
7
  attr_reader :dynamic_parts, :dynamic_map, :dynamic_indicies, :route, :dynamic_set, :parts
6
-
8
+
7
9
  def initialize(route, parts)
8
10
  @route = route
9
11
  @parts = parts
@@ -1,6 +1,6 @@
1
1
  class Usher
2
2
  class Route
3
- class Http
3
+ class RequestMethod
4
4
 
5
5
  attr_reader :type, :value
6
6
 
@@ -1,9 +1,11 @@
1
+ require 'strscan'
2
+
1
3
  class Usher
2
4
  class Route
3
5
  class Splitter
4
6
 
5
- ScanRegex = /((:|\*||\.:|\.)[0-9a-z_]+|\/|\(|\)|\|)/
6
- UrlScanRegex = /\/|\.?\w+/
7
+ ScanRegex = /((:|\*||\.:|\.)[0-9A-Za-z\$\-_\+!\*',]+|\/|\(|\)|\|)/
8
+ UrlScanRegex = /\/|\.?[0-9A-Za-z\$\-_\+!\*',]+/
7
9
 
8
10
  attr_reader :paths
9
11
 
@@ -13,6 +13,30 @@ class Usher
13
13
  def to_s
14
14
  "#{type}#{name}"
15
15
  end
16
+
17
+ def transform!(val)
18
+ return val unless @transformer
19
+
20
+ case @transformer
21
+ when Proc
22
+ @transformer.call(val)
23
+ when Symbol
24
+ val.send(@transformer)
25
+ end
26
+ rescue Exception => e
27
+ raise ValidationException.new("#{val} could not be successfully transformed by #{@transformer}, root cause #{e.inspect}")
28
+ end
29
+
30
+ def valid!(val)
31
+ case @validator
32
+ when Proc
33
+ @validator.call(val)
34
+ else
35
+ @validator === val or raise
36
+ end if @validator
37
+ rescue Exception => e
38
+ raise ValidationException.new(e, "#{val} does not conform to #{@validator}")
39
+ end
16
40
 
17
41
  def ==(o)
18
42
  o && (o.type == @type && o.name == @name && o.validator == @validator)
data/lib/usher/route.rb CHANGED
@@ -3,7 +3,7 @@ $:.unshift File.dirname(__FILE__)
3
3
  require 'route/path'
4
4
  require 'route/splitter'
5
5
  require 'route/variable'
6
- require 'route/http'
6
+ require 'route/request_method'
7
7
 
8
8
  class Usher
9
9
  class Route
@@ -18,7 +18,15 @@ class Usher
18
18
  @paths = Splitter.new(@original_path, @requirements, @transformers).paths.collect {|path| Path.new(self, path)}
19
19
  @primary_path = @paths.first
20
20
  end
21
-
21
+
22
+
23
+ # Sets +options+ on a route
24
+ #
25
+ # Request = Struct.new(:path)
26
+ # set = Usher.new
27
+ # route = set.add_route('/test')
28
+ # route.to(:controller => 'testing', :action => 'index')
29
+ # set.recognize(Request.new('/test')).first.params => {:controller => 'testing', :action => 'index'}
22
30
  def to(options)
23
31
  @params = options
24
32
  self
data/lib/usher.rb CHANGED
@@ -33,7 +33,7 @@ class Usher
33
33
  @named_routes = {}
34
34
  @routes = []
35
35
  @route_count = 0
36
- Grapher.instance.reset!
36
+ @grapher = Grapher.new
37
37
  end
38
38
  alias clear! reset!
39
39
 
@@ -120,16 +120,17 @@ class Usher
120
120
 
121
121
  @tree.add(route)
122
122
  @routes << route
123
- Grapher.instance.add_route(route)
123
+ @grapher.add_route(route)
124
124
  @route_count += 1
125
125
  route
126
126
  end
127
127
 
128
- # Recognizes a +request+ and returns +nil+ or an Usher::Route::Path
128
+ # Recognizes a +request+ 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.
129
129
  #
130
+ # Request = Struct.new(:path)
130
131
  # set = Usher.new
131
132
  # route = set.add_route('/test')
132
- # set.name(:test, route)
133
+ # set.recognize(Request.new('/test')).path.route == route => true
133
134
  def recognize(request)
134
135
  @tree.find(request)
135
136
  end
@@ -140,7 +141,7 @@ class Usher
140
141
  # route = set.add_route('/:controller/:action')
141
142
  # set.route_for_options({:controller => 'test', :action => 'action'}) == path.route => true
142
143
  def route_for_options(options)
143
- Grapher.instance.find_matching_path(options)
144
+ @grapher.find_matching_path(options)
144
145
  end
145
146
 
146
147
  # Generates a completed URL based on a +route+ or set of +params+
@@ -156,6 +157,8 @@ class Usher
156
157
  @named_routes[route]
157
158
  when nil
158
159
  route_for_options(params)
160
+ when Route
161
+ route.paths.first
159
162
  else
160
163
  route
161
164
  end
@@ -188,7 +191,7 @@ class Usher
188
191
  generated_path << '/' << p.to_s
189
192
  end
190
193
  end
191
- unless params_hash.blank?
194
+ unless params_hash.empty?
192
195
  has_query = generated_path[??]
193
196
  params_hash.each do |k,v|
194
197
  case v
@@ -32,17 +32,17 @@ describe "Usher route recognition" do
32
32
 
33
33
  it "should recognize a format-style variable" do
34
34
  target_route = route_set.add_route('/sample.:format', :controller => 'sample', :action => 'action')
35
- route_set.recognize(build_request({:method => 'get', :path => '/sample.html', :domain => 'admin.host.com'})).should == [target_route.paths.first, [[:format , 'html']]]
35
+ route_set.recognize(build_request({:method => 'get', :path => '/sample.html', :domain => 'admin.host.com'})).should == Usher::Node::Response.new(target_route.paths.first, [[:format , 'html']])
36
36
  end
37
37
 
38
38
  it "should recognize a format-style literal" do
39
39
  target_route = route_set.add_route(':action.html', :controller => 'sample', :action => 'action')
40
- route_set.recognize(build_request({:method => 'get', :path => '/sample.html', :domain => 'admin.host.com'})).should == [target_route.paths.first, [[:action , 'sample']]]
40
+ route_set.recognize(build_request({:method => 'get', :path => '/sample.html', :domain => 'admin.host.com'})).should == Usher::Node::Response.new(target_route.paths.first, [[:action , 'sample']])
41
41
  end
42
42
 
43
43
  it "should recognize a format-style variable along side another variable" do
44
44
  target_route = route_set.add_route(':action.:format', :controller => 'sample', :action => 'action')
45
- route_set.recognize(build_request({:method => 'get', :path => '/sample.html', :domain => 'admin.host.com'})).should == [target_route.paths.first, [[:action , 'sample'], [:format, 'html']]]
45
+ route_set.recognize(build_request({:method => 'get', :path => '/sample.html', :domain => 'admin.host.com'})).should == Usher::Node::Response.new(target_route.paths.first, [[:action , 'sample'], [:format, 'html']])
46
46
  end
47
47
 
48
48
  it "should recognize a specific route when several http-style restrictions are used" do
@@ -65,12 +65,17 @@ describe "Usher route recognition" do
65
65
 
66
66
  it "should use a transformer (proc) on incoming variables" do
67
67
  route_set.add_route('/:controller/:action/:id', :transformers => {:id => proc{|v| v.to_i}})
68
- route_set.recognize(build_request({:method => 'get', :path => '/products/show/123asd', :domain => 'admin.host.com'})).last.rassoc(123).first.should == :id
68
+ route_set.recognize(build_request({:method => 'get', :path => '/products/show/123asd', :domain => 'admin.host.com'})).params.rassoc(123).first.should == :id
69
+ end
70
+
71
+ it "shouldn't care about mildly weird characters in the URL" do
72
+ route = route_set.add_route('/!asd,qwe/hjk$qwe/:id')
73
+ route_set.recognize(build_request({:method => 'get', :path => '/!asd,qwe/hjk$qwe/09AZaz$-_+!*\'', :domain => 'admin.host.com'})).params.rassoc('09AZaz$-_+!*\'').first.should == :id
69
74
  end
70
75
 
71
76
  it "should use a transformer (symbol) on incoming variables" do
72
77
  route_set.add_route('/:controller/:action/:id', :transformers => {:id => :to_i})
73
- route_set.recognize(build_request({:method => 'get', :path => '/products/show/123asd', :domain => 'admin.host.com'})).last.rassoc(123).first.should == :id
78
+ route_set.recognize(build_request({:method => 'get', :path => '/products/show/123asd', :domain => 'admin.host.com'})).params.rassoc(123).first.should == :id
74
79
  end
75
80
 
76
81
  it "should should raise if malformed variables are used" do
@@ -78,4 +83,9 @@ describe "Usher route recognition" do
78
83
  proc {route_set.recognize(build_request({:method => 'get', :path => '/products/show/qweasd', :domain => 'admin.host.com'}))}.should raise_error
79
84
  end
80
85
 
86
+ it "should should raise if transformer proc raises (anything)" do
87
+ route_set.add_route('/products/show/:id', :transformers => {:id => proc{|v| Integer(v)}})
88
+ proc {route_set.recognize(build_request({:method => 'get', :path => '/products/show/qweasd', :domain => 'admin.host.com'}))}.should raise_error(Usher::ValidationException)
89
+ end
90
+
81
91
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: joshbuddy-usher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Hull
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-03-31 00:00:00 -07:00
12
+ date: 2009-04-02 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -49,12 +49,10 @@ files:
49
49
  - lib/usher/interface/rails2_interface/mapper.rb
50
50
  - lib/usher/interface/rails2_interface.rb
51
51
  - lib/usher/interface.rb
52
- - lib/usher/node
53
- - lib/usher/node/lookup.rb
54
52
  - lib/usher/node.rb
55
53
  - lib/usher/route
56
- - lib/usher/route/http.rb
57
54
  - lib/usher/route/path.rb
55
+ - lib/usher/route/request_method.rb
58
56
  - lib/usher/route/splitter.rb
59
57
  - lib/usher/route/variable.rb
60
58
  - lib/usher/route.rb
@@ -1,78 +0,0 @@
1
- class Usher
2
- class Node
3
- class Lookup
4
-
5
- def initialize
6
- @hash = {}
7
- @regexes = []
8
- @hash_reverse = {}
9
- @regexes_reverse = {}
10
- end
11
-
12
- def empty?
13
- @hash.empty? && @regexes.empty?
14
- end
15
-
16
- def keys
17
- @hash.keys + @regexes.collect{|r| r.first}
18
- end
19
-
20
- def values
21
- @hash.values + @regexes.collect{|r| r.last}
22
- end
23
-
24
- def each
25
- @hash.each{|k,v| yield k,v }
26
- @regexes.each{|v| yield v.first, v.last }
27
- end
28
-
29
- def delete_value(value)
30
- @hash.delete(@hash_reverse[value]) || ((rr = @regexes_reverse[value]) && @regexes.delete_at(rr[0]))
31
- end
32
-
33
- def []=(key, value)
34
- case key
35
- when Regexp
36
- @regexes << [key, value]
37
- @regex_test = nil
38
- @regexes_reverse[value] = [@regexes.size - 1, key, value]
39
- else
40
- @hash[key] = value
41
- @hash_reverse[value] = key
42
- end
43
- end
44
-
45
- def replace(src, dest)
46
- if @hash_reverse.key?(src)
47
- key = @hash_reverse[src]
48
- @hash[key] = dest
49
- @hash_reverse.delete(src)
50
- @hash_reverse[dest] = key
51
- elsif @regexes_reverse.key?(src)
52
- key = @regexes_reverse[src]
53
- @regexes[rkey[0]] = [rkey[1], dest]
54
- @regexes_reverse.delete(src)
55
- @regexes_reverse[dest] = [rkey[0], rkey[1], dest]
56
- end
57
- end
58
-
59
- def [](key)
60
- @hash[key] || regex_lookup(key)
61
- end
62
-
63
- private
64
- def regex_test
65
- @regex_test ||= Regexp.union(*@regexes.collect{|r| r[0]})
66
- end
67
-
68
- def regex_lookup(key)
69
- if !@regexes.empty? && key.is_a?(String) && data = regex_test.match(key)
70
- (data_array = data.to_a).each_index do |i|
71
- break @regexes[i].last if data_array.at(i)
72
- end
73
- end
74
- end
75
-
76
- end
77
- end
78
- end