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