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 +12 -6
- data/VERSION.yml +1 -1
- data/lib/usher/exceptions.rb +3 -1
- data/lib/usher/grapher.rb +0 -3
- data/lib/usher/interface/rack_interface.rb +3 -3
- data/lib/usher/interface/rails2_interface.rb +3 -3
- data/lib/usher/node.rb +8 -12
- data/lib/usher/route/path.rb +3 -1
- data/lib/usher/route/{http.rb → request_method.rb} +1 -1
- data/lib/usher/route/splitter.rb +4 -2
- data/lib/usher/route/variable.rb +24 -0
- data/lib/usher/route.rb +10 -2
- data/lib/usher.rb +9 -6
- data/spec/recognize_spec.rb +15 -5
- metadata +3 -5
- data/lib/usher/node/lookup.rb +0 -78
data/README.rdoc
CHANGED
@@ -1,8 +1,16 @@
|
|
1
1
|
= Usher
|
2
2
|
|
3
|
-
Tree-based router for
|
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
|
-
|
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
data/lib/usher/exceptions.rb
CHANGED
data/lib/usher/grapher.rb
CHANGED
@@ -21,9 +21,9 @@ class Usher
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def call(env)
|
24
|
-
|
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
|
-
|
47
|
-
params =
|
48
|
-
request.path_parameters = (
|
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::
|
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::
|
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::
|
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::
|
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
|
-
|
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
|
-
|
125
|
-
|
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, []]
|
data/lib/usher/route/path.rb
CHANGED
data/lib/usher/route/splitter.rb
CHANGED
@@ -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-
|
6
|
-
UrlScanRegex =
|
7
|
+
ScanRegex = /((:|\*||\.:|\.)[0-9A-Za-z\$\-_\+!\*',]+|\/|\(|\)|\|)/
|
8
|
+
UrlScanRegex = /\/|\.?[0-9A-Za-z\$\-_\+!\*',]+/
|
7
9
|
|
8
10
|
attr_reader :paths
|
9
11
|
|
data/lib/usher/route/variable.rb
CHANGED
@@ -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/
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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.
|
194
|
+
unless params_hash.empty?
|
192
195
|
has_query = generated_path[??]
|
193
196
|
params_hash.each do |k,v|
|
194
197
|
case v
|
data/spec/recognize_spec.rb
CHANGED
@@ -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 ==
|
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 ==
|
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 ==
|
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'})).
|
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'})).
|
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.
|
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-
|
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
|
data/lib/usher/node/lookup.rb
DELETED
@@ -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
|