usher 0.4.8
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/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'
|