joshbuddy-usher 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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