usher 0.4.8
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +3 -0
- data/Manifest.txt +35 -0
- data/README.rdoc +126 -0
- data/Rakefile +72 -0
- data/VERSION.yml +4 -0
- data/lib/usher/exceptions.rb +5 -0
- data/lib/usher/generate.rb +131 -0
- data/lib/usher/grapher.rb +65 -0
- data/lib/usher/interface/email_interface.rb +27 -0
- data/lib/usher/interface/merb_interface.rb +61 -0
- data/lib/usher/interface/rack_interface/mapper.rb +0 -0
- data/lib/usher/interface/rack_interface/route.rb +9 -0
- data/lib/usher/interface/rack_interface.rb +37 -0
- data/lib/usher/interface/rails2_2_interface/mapper.rb +44 -0
- data/lib/usher/interface/rails2_2_interface.rb +135 -0
- data/lib/usher/interface/rails2_3_interface.rb +135 -0
- data/lib/usher/interface.rb +27 -0
- data/lib/usher/node.rb +138 -0
- data/lib/usher/route/path.rb +24 -0
- data/lib/usher/route/request_method.rb +22 -0
- data/lib/usher/route/variable.rb +37 -0
- data/lib/usher/route.rb +58 -0
- data/lib/usher/splitter.rb +159 -0
- data/lib/usher.rb +184 -0
- data/rails/init.rb +8 -0
- data/spec/private/email/recognize_spec.rb +38 -0
- data/spec/private/generate_spec.rb +141 -0
- data/spec/private/grapher_spec.rb +41 -0
- data/spec/private/path_spec.rb +68 -0
- data/spec/private/rack/dispatch_spec.rb +29 -0
- data/spec/private/rails2_2/compat.rb +1 -0
- data/spec/private/rails2_2/generate_spec.rb +28 -0
- data/spec/private/rails2_2/path_spec.rb +16 -0
- data/spec/private/rails2_2/recognize_spec.rb +79 -0
- data/spec/private/rails2_3/compat.rb +1 -0
- data/spec/private/rails2_3/generate_spec.rb +28 -0
- data/spec/private/rails2_3/path_spec.rb +16 -0
- data/spec/private/rails2_3/recognize_spec.rb +79 -0
- data/spec/private/recognize_spec.rb +178 -0
- data/spec/private/request_method_spec.rb +15 -0
- data/spec/private/split_spec.rb +76 -0
- data/spec/spec.opts +7 -0
- metadata +120 -0
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
class Usher
|
4
|
+
class Splitter
|
5
|
+
|
6
|
+
def self.for_delimiters(delimiters, valid_regex)
|
7
|
+
delimiters_regex = delimiters.collect{|d| Regexp.quote(d)} * '|'
|
8
|
+
SplitterInstance.new(
|
9
|
+
delimiters,
|
10
|
+
Regexp.new('((:|\*)?' + valid_regex + '|' + delimiters_regex + '|\(|\)|\||\{)'),
|
11
|
+
Regexp.new("[#{delimiters.collect{|d| Regexp.quote(d)}}]|[^#{delimiters.collect{|d| Regexp.quote(d)}}]+")
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :paths
|
16
|
+
|
17
|
+
class SplitterInstance
|
18
|
+
|
19
|
+
attr_reader :delimiter_chars
|
20
|
+
|
21
|
+
def initialize(delimiters, split_regex, url_split_regex)
|
22
|
+
@delimiters = delimiters
|
23
|
+
@delimiter_chars = delimiters.collect{|d| d[0]}
|
24
|
+
@split_regex = split_regex
|
25
|
+
@url_split_regex = url_split_regex
|
26
|
+
end
|
27
|
+
|
28
|
+
def url_split(path)
|
29
|
+
path.scan(@url_split_regex)
|
30
|
+
end
|
31
|
+
|
32
|
+
def split(path, requirements = nil, default_values = nil)
|
33
|
+
parts = Group.new(:all, nil)
|
34
|
+
ss = StringScanner.new(path)
|
35
|
+
current_group = parts
|
36
|
+
while !ss.eos?
|
37
|
+
part = ss.scan(@split_regex)
|
38
|
+
case part[0]
|
39
|
+
when ?*, ?:
|
40
|
+
type = part.slice!(0).chr.to_sym
|
41
|
+
current_group << Usher::Route::Variable.new(type, part, requirements && requirements[part.to_sym])
|
42
|
+
when ?{
|
43
|
+
pattern = ''
|
44
|
+
count = 1
|
45
|
+
variable = ss.scan(/[:\*]([^,]+),/)
|
46
|
+
until count.zero?
|
47
|
+
regex_part = ss.scan(/\{|\}|[^\{\}]+/)
|
48
|
+
case regex_part[0]
|
49
|
+
when ?{
|
50
|
+
count += 1
|
51
|
+
when ?}
|
52
|
+
count -= 1
|
53
|
+
end
|
54
|
+
pattern << regex_part
|
55
|
+
end
|
56
|
+
pattern.slice!(pattern.length - 1)
|
57
|
+
regex = Regexp.new(pattern)
|
58
|
+
if variable
|
59
|
+
variable_type = variable.slice!(0).chr.to_sym
|
60
|
+
variable_name = variable[0, variable.size - 1].to_sym
|
61
|
+
current_group << Usher::Route::Variable.new(variable_type, variable_name, requirements && requirements[variable_name], regex)
|
62
|
+
else
|
63
|
+
current_group << regex
|
64
|
+
end
|
65
|
+
when ?(
|
66
|
+
new_group = Group.new(:any, current_group)
|
67
|
+
current_group << new_group
|
68
|
+
current_group = new_group
|
69
|
+
when ?)
|
70
|
+
current_group = current_group.parent.type == :one ? current_group.parent.parent : current_group.parent
|
71
|
+
when ?|
|
72
|
+
unless current_group.parent.type == :one
|
73
|
+
detached_group = current_group.parent.pop
|
74
|
+
new_group = Group.new(:one, detached_group.parent)
|
75
|
+
detached_group.parent = new_group
|
76
|
+
detached_group.type = :all
|
77
|
+
new_group << detached_group
|
78
|
+
new_group.parent << new_group
|
79
|
+
end
|
80
|
+
current_group.parent << Group.new(:all, current_group.parent)
|
81
|
+
current_group = current_group.parent.last
|
82
|
+
else
|
83
|
+
current_group << part
|
84
|
+
end
|
85
|
+
end unless !path || path.empty?
|
86
|
+
paths = calc_paths(parts)
|
87
|
+
paths.each do |path|
|
88
|
+
path.each_with_index do |part, index|
|
89
|
+
if part.is_a?(Usher::Route::Variable)
|
90
|
+
part.default_value = default_values[part.name] if default_values
|
91
|
+
|
92
|
+
case part.type
|
93
|
+
when :*
|
94
|
+
part.look_ahead = path[index + 1, path.size].find{|p| !p.is_a?(Usher::Route::Variable) && !@delimiter_chars.include?(p[0])} || nil
|
95
|
+
when :':'
|
96
|
+
part.look_ahead = path[index + 1, path.size].find{|p| @delimiter_chars.include?(p[0])} || @delimiters.first
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
paths
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def cartesian_product!(lval, rval)
|
107
|
+
product = []
|
108
|
+
(lval.size * rval.size).times do |index|
|
109
|
+
val = []
|
110
|
+
val.push(*lval[index % lval.size])
|
111
|
+
val.push(*rval[index % rval.size])
|
112
|
+
product << val
|
113
|
+
end
|
114
|
+
lval.replace(product)
|
115
|
+
end
|
116
|
+
|
117
|
+
def calc_paths(parts)
|
118
|
+
if parts.is_a?(Group)
|
119
|
+
paths = [[]]
|
120
|
+
case parts.type
|
121
|
+
when :all
|
122
|
+
parts.each do |p|
|
123
|
+
cartesian_product!(paths, calc_paths(p))
|
124
|
+
end
|
125
|
+
when :any
|
126
|
+
parts.each do |p|
|
127
|
+
cartesian_product!(paths, calc_paths(p))
|
128
|
+
end
|
129
|
+
paths.unshift([])
|
130
|
+
when :one
|
131
|
+
cartesian_product!(paths, parts.collect do |p|
|
132
|
+
calc_paths(p)
|
133
|
+
end)
|
134
|
+
end
|
135
|
+
paths.each{|p| p.compact!; p.flatten! }
|
136
|
+
paths
|
137
|
+
else
|
138
|
+
[[parts]]
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class Group < Array
|
145
|
+
attr_accessor :type
|
146
|
+
attr_accessor :parent
|
147
|
+
|
148
|
+
def inspect
|
149
|
+
"#{type}->#{super}"
|
150
|
+
end
|
151
|
+
|
152
|
+
def initialize(type, parent)
|
153
|
+
@type = type
|
154
|
+
@parent = parent
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
data/lib/usher.rb
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'usher', 'node')
|
2
|
+
require File.join(File.dirname(__FILE__), 'usher', 'route')
|
3
|
+
require File.join(File.dirname(__FILE__), 'usher', 'grapher')
|
4
|
+
require File.join(File.dirname(__FILE__), 'usher', 'interface')
|
5
|
+
require File.join(File.dirname(__FILE__), 'usher', 'splitter')
|
6
|
+
require File.join(File.dirname(__FILE__), 'usher', 'exceptions')
|
7
|
+
|
8
|
+
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:
|
15
|
+
|
16
|
+
# Returns whether the route set is empty
|
17
|
+
#
|
18
|
+
# set = Usher.new
|
19
|
+
# set.empty? => true
|
20
|
+
# set.add_route('/test')
|
21
|
+
# set.empty? => false
|
22
|
+
def empty?
|
23
|
+
@route_count.zero?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Resets the route set back to its initial state
|
27
|
+
#
|
28
|
+
# set = Usher.new
|
29
|
+
# set.add_route('/test')
|
30
|
+
# set.empty? => false
|
31
|
+
# set.reset!
|
32
|
+
# set.empty? => true
|
33
|
+
def reset!
|
34
|
+
@tree = Node.root(self, @request_methods, @globs_capture_separators)
|
35
|
+
@named_routes = {}
|
36
|
+
@routes = []
|
37
|
+
@route_count = 0
|
38
|
+
@grapher = Grapher.new
|
39
|
+
end
|
40
|
+
alias clear! reset!
|
41
|
+
|
42
|
+
# Creates a route set, with options
|
43
|
+
#
|
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
|
+
# <tt>:delimiters</tt>: Array of Strings. (default <tt>['/', '.']</tt>). Delimiters used in path separation. Array must be single character strings.
|
48
|
+
#
|
49
|
+
# <tt>:valid_regex</tt>: String. (default <tt>'[0-9A-Za-z\$\-_\+!\*\',]+'</tt>). String that can be interpolated into regex to match
|
50
|
+
# valid character sequences within path.
|
51
|
+
#
|
52
|
+
# <tt>:request_methods</tt>: Array of Symbols. (default <tt>[:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]</tt>)
|
53
|
+
# Array of methods called against the request object for the purposes of matching route requirements.
|
54
|
+
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)
|
60
|
+
reset!
|
61
|
+
end
|
62
|
+
|
63
|
+
# Adds a route referencable by +name+. Sett add_route for format +path+ and +options+.
|
64
|
+
#
|
65
|
+
# set = Usher.new
|
66
|
+
# set.add_named_route(:test_route, '/test')
|
67
|
+
def add_named_route(name, path, options = nil)
|
68
|
+
add_route(path, options).name(name)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Attaches a +route+ to a +name+
|
72
|
+
#
|
73
|
+
# set = Usher.new
|
74
|
+
# route = set.add_route('/test')
|
75
|
+
# set.name(:test, route)
|
76
|
+
def name(name, route)
|
77
|
+
@named_routes[name] = route
|
78
|
+
route
|
79
|
+
end
|
80
|
+
|
81
|
+
# Creates a route from +path+ and +options+
|
82
|
+
#
|
83
|
+
# === +path+
|
84
|
+
# A path consists a mix of dynamic and static parts delimited by <tt>/</tt>
|
85
|
+
#
|
86
|
+
# ==== Dynamic
|
87
|
+
# Dynamic parts are prefixed with either :, *. :variable matches only one part of the path, whereas *variable can match one or
|
88
|
+
# more parts.
|
89
|
+
#
|
90
|
+
# <b>Example:</b>
|
91
|
+
# <tt>/path/:variable/path</tt> would match
|
92
|
+
#
|
93
|
+
# * <tt>/path/test/path</tt>
|
94
|
+
# * <tt>/path/something_else/path</tt>
|
95
|
+
# * <tt>/path/one_more/path</tt>
|
96
|
+
#
|
97
|
+
# In the above examples, 'test', 'something_else' and 'one_more' respectively would be bound to the key <tt>:variable</tt>.
|
98
|
+
# However, <tt>/path/test/one_more/path</tt> would not be matched.
|
99
|
+
#
|
100
|
+
# <b>Example:</b>
|
101
|
+
# <tt>/path/*variable/path</tt> would match
|
102
|
+
#
|
103
|
+
# * <tt>/path/one/two/three/path</tt>
|
104
|
+
# * <tt>/path/four/five/path</tt>
|
105
|
+
#
|
106
|
+
# In the above examples, ['one', 'two', 'three'] and ['four', 'five'] respectively would be bound to the key :variable.
|
107
|
+
#
|
108
|
+
# As well, variables can have a regex matcher.
|
109
|
+
#
|
110
|
+
# <b>Example:</b>
|
111
|
+
# <tt>/product/{:id,\d+}</tt> would match
|
112
|
+
#
|
113
|
+
# * <tt>/product/123</tt>
|
114
|
+
# * <tt>/product/4521</tt>
|
115
|
+
#
|
116
|
+
# But not
|
117
|
+
# * <tt>/product/AE-35</tt>
|
118
|
+
#
|
119
|
+
# As well, the same logic applies for * variables as well, where only parts matchable by the supplied regex will
|
120
|
+
# actually be bound to the variable
|
121
|
+
#
|
122
|
+
# ==== Static
|
123
|
+
#
|
124
|
+
# Static parts of literal character sequences. For instance, <tt>/path/something.html</tt> would match only the same path.
|
125
|
+
# As well, static parts can have a regex pattern in them as well, such as <tt>/path/something.{html|xml}</tt> which would match only
|
126
|
+
# <tt>/path/something.html</tt> and <tt>/path/something.xml</tt>
|
127
|
+
#
|
128
|
+
# ==== Optional sections
|
129
|
+
#
|
130
|
+
# Sections of a route can be marked as optional by surrounding it with brackets. For instance, in the above static example, <tt>/path/something(.html)</tt> would match both <tt>/path/something</tt> and <tt>/path/something.html</tt>.
|
131
|
+
#
|
132
|
+
# ==== One and only one sections
|
133
|
+
#
|
134
|
+
# Sections of a route can be marked as "one and only one" by surrounding it with brackets and separating parts of the route with pipes.
|
135
|
+
# For instance, the path, <tt>/path/something(.xml|.html)</tt> would only match <tt>/path/something.xml</tt> and
|
136
|
+
# <tt>/path/something.html</tt>. Generally its more efficent to use one and only sections over using regex.
|
137
|
+
#
|
138
|
+
# === +options+
|
139
|
+
# * +requirements+ - After transformation, tests the condition using ===. If it returns false, it raises an <tt>Usher::ValidationException</tt>
|
140
|
+
# * +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
|
+
# * Any other key is interpreted as a requirement for the variable of its name.
|
142
|
+
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)
|
159
|
+
@routes << route
|
160
|
+
@grapher.add_route(route)
|
161
|
+
@route_count += 1
|
162
|
+
route
|
163
|
+
end
|
164
|
+
|
165
|
+
# 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.
|
166
|
+
#
|
167
|
+
# Request = Struct.new(:path)
|
168
|
+
# set = Usher.new
|
169
|
+
# route = set.add_route('/test')
|
170
|
+
# set.recognize(Request.new('/test')).path.route == route => true
|
171
|
+
def recognize(request, path = request.path)
|
172
|
+
@tree.find(self, request, @splitter.url_split(path))
|
173
|
+
end
|
174
|
+
|
175
|
+
# Recognizes a set of +parameters+ and gets the closest matching Usher::Route::Path or +nil+ if no route exists.
|
176
|
+
#
|
177
|
+
# set = Usher.new
|
178
|
+
# route = set.add_route('/:controller/:action')
|
179
|
+
# set.path_for_options({:controller => 'test', :action => 'action'}) == path.route => true
|
180
|
+
def path_for_options(options)
|
181
|
+
@grapher.find_matching_path(options)
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
if Rails::VERSION::MAJOR == 2 && Rails::VERSION::MINOR == 3
|
2
|
+
ActionController::Routing.module_eval "remove_const(:Routes); Routes = Usher::Interface.for(:rails2_3)"
|
3
|
+
elsif Rails::VERSION::MAJOR == 2 && Rails::VERSION::MINOR >= 2
|
4
|
+
class Usher::Interface::Rails2_2Interface::Mapper
|
5
|
+
include ActionController::Resources
|
6
|
+
end
|
7
|
+
ActionController::Routing.module_eval "remove_const(:Routes); Routes = Usher::Interface.for(:rails2_2)"
|
8
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'lib/usher'
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
def build_email_mock(email)
|
6
|
+
request = mock "Request"
|
7
|
+
request.should_receive(:email).any_number_of_times.and_return(email)
|
8
|
+
request
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "Usher (for email) route recognition" do
|
12
|
+
|
13
|
+
before(:each) do
|
14
|
+
@route_set = Usher::Interface.for(:email)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should recognize a simple request" do
|
18
|
+
receiver = mock('receiver')
|
19
|
+
receiver.should_receive(:action).with({}).exactly(1)
|
20
|
+
@route_set.for('joshbuddy@gmail.com') { |params| receiver.action(params) }
|
21
|
+
@route_set.act('joshbuddy@gmail.com')
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should recognize a wildcard domain" do
|
25
|
+
receiver = mock('receiver')
|
26
|
+
receiver.should_receive(:action).with({:domain => 'gmail.com'}).exactly(1)
|
27
|
+
@route_set.for('joshbuddy@*domain') { |params| receiver.action(params) }
|
28
|
+
@route_set.act('joshbuddy@gmail.com')
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should recognize a complex email" do
|
32
|
+
receiver = mock('receiver')
|
33
|
+
receiver.should_receive(:action).with({:subject => 'sub+ect', :id => '123', :sid => '456', :tok => 'sdqwe123ae', :domain => 'mydomain.org'}).exactly(1)
|
34
|
+
@route_set.for(':subject.{:id,^\d+$}-{:sid,^\d+$}-{:tok,^\w+$}@*domain') { |params| receiver.action(params) }
|
35
|
+
@route_set.act('sub+ect.123-456-sdqwe123ae@mydomain.org')
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'lib/usher'
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
describe "Usher URL generation" do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
@route_set = Usher.new
|
8
|
+
@route_set.reset!
|
9
|
+
@url_generator = Usher::Generators::URL.new(@route_set)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should generate a simple URL" do
|
13
|
+
@route_set.add_named_route(:sample, '/sample', :controller => 'sample', :action => 'action')
|
14
|
+
@url_generator.generate(:sample, {}).should == '/sample'
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should generate a simple URL with a single variable" do
|
18
|
+
@route_set.add_named_route(:sample, '/sample/:action', :controller => 'sample')
|
19
|
+
@url_generator.generate(:sample, {:action => 'action'}).should == '/sample/action'
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should generate a simple URL with a single variable (and escape)" do
|
23
|
+
@route_set.add_named_route(:sample, '/sample/:action', :controller => 'sample')
|
24
|
+
@url_generator.generate(:sample, {:action => 'action time'}).should == '/sample/action%20time'
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should generate a simple URL with a single variable (thats not a string)" do
|
28
|
+
@route_set.add_named_route(:sample, '/sample/:action/:id', :controller => 'sample')
|
29
|
+
@url_generator.generate(:sample, {:action => 'action', :id => 123}).should == '/sample/action/123'
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should generate a simple URL with a glob variable" do
|
33
|
+
@route_set.add_named_route(:sample, '/sample/*action', :controller => 'sample')
|
34
|
+
@url_generator.generate(:sample, {:action => ['foo', 'baz']}).should == '/sample/foo/baz'
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should generate a mutliple vairable URL from a hash" do
|
38
|
+
@route_set.add_named_route(:sample, '/sample/:first/:second', :controller => 'sample')
|
39
|
+
@url_generator.generate(:sample, {:first => 'zoo', :second => 'maz'}).should == '/sample/zoo/maz'
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should generate a mutliple vairable URL from an array" do
|
43
|
+
@route_set.add_named_route(:sample, '/sample/:first/:second', :controller => 'sample')
|
44
|
+
@url_generator.generate(:sample, ['maz', 'zoo']).should == '/sample/maz/zoo'
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should generate append extra hash variables to the end" do
|
48
|
+
@route_set.add_named_route(:sample, '/sample/:first/:second', :controller => 'sample')
|
49
|
+
@url_generator.generate(:sample, {:first => 'maz', :second => 'zoo', :third => 'zanz'}).should == '/sample/maz/zoo?third=zanz'
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should generate append extra hash variables to the end (when the first parts are an array)" do
|
53
|
+
@route_set.add_named_route(:sample, '/sample/:first/:second', :controller => 'sample')
|
54
|
+
['/sample/maz/zoo?four=jane&third=zanz', '/sample/maz/zoo?third=zanz&four=jane'].include?(@url_generator.generate(:sample, ['maz', 'zoo', {:third => 'zanz', :four => 'jane'}])).should == true
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should generate append extra hash variables to the end using [] syntax if its an array" do
|
58
|
+
@route_set.add_named_route(:sample, '/sample/:first/:second', :controller => 'sample')
|
59
|
+
@url_generator.generate(:sample, {:first => 'maz', :second => 'zoo', :third => ['zanz', 'susie']}).should == '/sample/maz/zoo?third%5B%5D=zanz&third%5B%5D=susie'
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should generate a mutliple vairable URL from an array" do
|
63
|
+
@route_set.add_named_route(:sample, '/sample/:first/:second', :controller => 'sample')
|
64
|
+
@url_generator.generate(:sample, ['maz', 'zoo']).should == '/sample/maz/zoo'
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should generate a simple URL with a format" do
|
68
|
+
@route_set.add_named_route(:sample, '/sample/:action.:format', :controller => 'sample')
|
69
|
+
@url_generator.generate(:sample, {:action => 'action', :format => 'html'}).should == '/sample/action.html'
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should generate from parameters" do
|
73
|
+
caf = @route_set.add_route('/:controller/:action.:format')
|
74
|
+
ca = @route_set.add_route('/:controller/:action')
|
75
|
+
@url_generator.generate(nil, {:controller => 'controller', :action => 'action'}).should == '/controller/action'
|
76
|
+
@url_generator.generate(nil, {:controller => 'controller', :action => 'action', :format => 'html'}).should == '/controller/action.html'
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should use the first route when generating a URL from two ambiguous routes" do
|
80
|
+
@route_set.add_route('/:controller/:action')
|
81
|
+
@route_set.add_route('/:action/:controller')
|
82
|
+
@url_generator.generate(nil, {:controller => 'controller', :action => 'action'}).should == '/controller/action'
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should accept an array of parameters" do
|
86
|
+
caf = @route_set.add_named_route(:name, '/:controller/:action.:format')
|
87
|
+
@url_generator.generate(:name, ['controller', 'action', 'html']).should == '/controller/action.html'
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should generate a route with a specific host" do
|
91
|
+
caf = @route_set.add_named_route(:name, '/:controller/:action.:format', :generate_with => {:host => 'www.slashdot.org', :port => 80})
|
92
|
+
@url_generator.generate_full(:name, Rack::Request.new(Rack::MockRequest.env_for("http://localhost:8080")), ['controller', 'action', 'html']).should == 'http://www.slashdot.org/controller/action.html'
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should require all the parameters (hash) to generate a route" do
|
96
|
+
proc {@url_generator.generate(@route_set.add_route('/:controller/:action'), {:controller => 'controller'})}.should raise_error Usher::MissingParameterException
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should generate from a route" do
|
100
|
+
@url_generator.generate(@route_set.add_route('/:controller/:action'), {:controller => 'controller', :action => 'action'}).should == '/controller/action'
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should require all the parameters (array) to generate a route" do
|
104
|
+
@route_set.add_named_route(:name, '/:controller/:action.:format')
|
105
|
+
proc {@url_generator.generate(:name, ['controller', 'action'])}.should raise_error Usher::MissingParameterException
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should generate a route when only one parameter is given" do
|
109
|
+
@route_set.add_named_route(:name, '/:controller')
|
110
|
+
@url_generator.generate(:name, 'controller').should == '/controller'
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should generate the correct route from a route containing optional parts" do
|
114
|
+
@route_set.add_named_route(:name, '/:controller(/:action(/:id))')
|
115
|
+
@url_generator.generate(:name, {:controller => 'controller'}).should == '/controller'
|
116
|
+
@url_generator.generate(:name, {:controller => 'controller', :action => 'action'}).should == '/controller/action'
|
117
|
+
@url_generator.generate(:name, {:controller => 'controller', :action => 'action', :id => 'id'}).should == '/controller/action/id'
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should generate a route using defaults for everything but the first parameter" do
|
121
|
+
@route_set.add_named_route(:name, '/:one/:two/:three', {:default_values => {:one => 'one', :two => 'two', :three => 'three'}})
|
122
|
+
@url_generator.generate(:name, {:one => "1"}).should == '/1/two/three'
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should generate a route using defaults for everything" do
|
126
|
+
@route_set.add_named_route(:name, '/:one/:two/:three', {:default_values => {:one => 'one', :two => 'two', :three => 'three'}})
|
127
|
+
@url_generator.generate(:name).should == '/one/two/three'
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should generate a route using defaults and optionals using the last parameter" do
|
131
|
+
@route_set.add_named_route(:opts_with_defaults, '/:one(/:two(/:three))', {:default_values => {:one => '1', :two => '2', :three => '3'}})
|
132
|
+
@url_generator.generate(:opts_with_defaults, {:three => 'three'}).should == '/1/2/three'
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should generate a route with optional segments given two nested optional parameters" do
|
136
|
+
@route_set.add_named_route(:optionals, '/:controller(/:action(/:id))(.:format)')
|
137
|
+
@url_generator.generate(:optionals, {:controller => "foo", :action => "bar"}).should == '/foo/bar'
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'lib/usher'
|
2
|
+
|
3
|
+
|
4
|
+
describe "Usher grapher" do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
@route_set = Usher.new
|
8
|
+
@route_set.reset!
|
9
|
+
@url_generator = Usher::Generators::URL.new(@route_set)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should find a simple path" do
|
13
|
+
@route_set.add_route('/:a/:b/:c')
|
14
|
+
@url_generator.generate(nil, {:a => 'A', :b => 'B', :c => 'C'}).should == '/A/B/C'
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should pick a more specific route" do
|
18
|
+
@route_set.add_route('/:a/:b')
|
19
|
+
@route_set.add_route('/:a/:b/:c')
|
20
|
+
@url_generator.generate(nil, {:a => 'A', :b => 'B', :c => 'C'}).should == '/A/B/C'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should fail to generate a route when none matches" do
|
24
|
+
@route_set.add_route('/:a/:b')
|
25
|
+
proc {@url_generator.generate(nil, {:c => 'C', :d => 'D'}) }.should raise_error Usher::UnrecognizedException
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should find the most specific route and append extra parts on as a query string" do
|
29
|
+
@route_set.add_route('/:a/:b/:c')
|
30
|
+
@route_set.add_route('/:a/:b')
|
31
|
+
@url_generator.generate(nil, {:a => 'A', :b => 'B', :d => 'C'}).should == '/A/B?d=C'
|
32
|
+
end
|
33
|
+
|
34
|
+
# FIXME
|
35
|
+
#it "should do a validity check against the incoming variables when asked to" do
|
36
|
+
# route_set.add_route('/:a/:b', :b => /\d+/)
|
37
|
+
# route_set.generate_url(nil, {:a => 'A', :b => 'B'}).should == '/A/B'
|
38
|
+
# proc{ route_set.generate_url(nil, {:a => 'A', :b => 'B'})}.should raise_error Usher::ValidationException
|
39
|
+
#end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'lib/usher'
|
2
|
+
|
3
|
+
route_set = Usher.new
|
4
|
+
|
5
|
+
describe "Usher route adding" do
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
route_set.reset!
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should be empty after a reset" do
|
12
|
+
route_set.add_route('/sample', :controller => 'sample')
|
13
|
+
route_set.empty?.should == false
|
14
|
+
route_set.reset!
|
15
|
+
route_set.empty?.should == true
|
16
|
+
end
|
17
|
+
|
18
|
+
it "shouldn't care about routes without a controller" do
|
19
|
+
proc { route_set.add_route('/bad/route') }.should_not raise_error
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should add every kind of optional route possible" do
|
23
|
+
route_set.add_route('/a/b(/c)(/d(/e))')
|
24
|
+
route_set.routes.first.paths.collect{|a| a.parts }.should == [
|
25
|
+
['/', "a", '/', "b"],
|
26
|
+
['/', "a", '/', "b", '/', "c", '/', "d"],
|
27
|
+
['/', "a", '/', "b", '/', "d", '/', "e"],
|
28
|
+
['/', "a", '/', "b", '/', "c"],
|
29
|
+
['/', "a", '/', "b", '/', "d"],
|
30
|
+
['/', "a", '/', "b", '/', "c", '/', "d", '/', "e"]
|
31
|
+
]
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should allow named routes to be added" do
|
36
|
+
route_set.add_named_route(:route, '/bad/route', :controller => 'sample').should == route_set.named_routes[:route]
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should calculate depths for nodes" do
|
40
|
+
route_set.add_named_route(:route, '/bad/route/three/four')
|
41
|
+
route_set.tree.depth.should == 0
|
42
|
+
route_set.tree.lookup['/'].depth.should == 1
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should pp for nodes" do
|
46
|
+
route_set.add_named_route(:route, '/bad/route/three/four')
|
47
|
+
route_set.tree.depth.should == 0
|
48
|
+
old_out = $stdout
|
49
|
+
$stdout = (output = StringIO.new)
|
50
|
+
route_set.tree.lookup['/'].lookup['bad'].lookup['/'].pp
|
51
|
+
$stdout = old_out
|
52
|
+
output.rewind
|
53
|
+
output.read.should == <<-HEREDOC
|
54
|
+
3: "/" false
|
55
|
+
route ==>
|
56
|
+
4: "route" false
|
57
|
+
/ ==>
|
58
|
+
5: "/" false
|
59
|
+
three ==>
|
60
|
+
6: "three" false
|
61
|
+
/ ==>
|
62
|
+
7: "/" false
|
63
|
+
four ==>
|
64
|
+
8: "four" true
|
65
|
+
HEREDOC
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'lib/usher'
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
|
5
|
+
route_set = Usher::Interface.for(:rack)
|
6
|
+
|
7
|
+
describe "Usher (for rack) route dispatching" do
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
route_set.reset!
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should dispatch a simple request" do
|
14
|
+
app = mock 'app'
|
15
|
+
app.should_receive(:call).once.with {|v| v['usher.params'].should == {} }
|
16
|
+
route_set.add('/sample').to(app)
|
17
|
+
route_set.call(Rack::MockRequest.env_for("/sample", :method => 'GET'))
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should dispatch a POST request" do
|
21
|
+
bad_app = mock 'bad_app'
|
22
|
+
app = mock 'app'
|
23
|
+
app.should_receive(:call).once.with {|v| v['usher.params'].should == {} }
|
24
|
+
route_set.add('/sample').to(bad_app)
|
25
|
+
route_set.add('/sample', :requirements => {:request_method => 'POST'}).to(app)
|
26
|
+
route_set.call(Rack::MockRequest.env_for("/sample", :request_method => 'POST'))
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'activesupport'
|