1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
|
module ActionController
# See http://manuals.rubyonrails.com/read/chapter/65
module Routing
class Route #:nodoc:
attr_reader :defaults # The defaults hash
def initialize(path, hash={})
raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash)
@defaults = hash[:defaults].kind_of?(Hash) ? hash.delete(:defaults) : {}
@requirements = hash[:requirements].kind_of?(Hash) ? hash.delete(:requirements) : {}
self.items = path
hash.each do |k, v|
raise TypeError, "Hash keys must be symbols!" unless k.kind_of? Symbol
if v.kind_of? Regexp
raise ArgumentError, "Regexp requirement on #{k}, but #{k} is not in this route's path!" unless @items.include? k
@requirements[k] = v
else
(@items.include?(k) ? @defaults : @requirements)[k] = (v.nil? ? nil : v.to_s)
end
end
@defaults.each do |k, v|
raise ArgumentError, "A default has been specified for #{k}, but #{k} is not in the path!" unless @items.include? k
@defaults[k] = v.to_s unless v.kind_of?(String) || v.nil?
end
@requirements.each {|k, v| raise ArgumentError, "A Regexp requirement has been specified for #{k}, but #{k} is not in the path!" if v.kind_of?(Regexp) && ! @items.include?(k)}
# Add in defaults for :action and :id.
[[:action, 'index'], [:id, nil]].each do |name, default|
@defaults[name] = default if @items.include?(name) && ! (@requirements.key?(name) || @defaults.key?(name))
end
end
# Generate a URL given the provided options.
# All values in options should be symbols.
# Returns the path and the unused names in a 2 element array.
# If generation fails, [nil, nil] is returned
# Generation can fail because of a missing value, or because an equality check fails.
#
# Generate urls will be as short as possible. If the last component of a url is equal to the default value,
# then that component is removed. This is applied as many times as possible. So, your index controller's
# index action will generate []
def generate(options, defaults={})
non_matching = @requirements.keys.select {|name| ! passes_requirements?(name, options[name] || defaults[name])}
non_matching.collect! {|name| requirements_for(name)}
return nil, "Mismatching option#{'s' if non_matching.length > 1}:\n #{non_matching.join '\n '}" unless non_matching.empty?
used_names = @requirements.inject({}) {|hash, (k, v)| hash[k] = true; hash} # Mark requirements as used so they don't get put in the query params
components = @items.collect do |item|
if item.kind_of? Symbol
collection = false
if /^\*/ =~ item.to_s
collection = true
item = item.to_s.sub(/^\*/,"").intern
end
used_names[item] = true
value = options[item] || defaults[item] || @defaults[item]
return nil, requirements_for(item) unless passes_requirements?(item, value)
defaults = {} unless defaults == {} || value == defaults[item] # Stop using defaults if this component isn't the same as the default.
if value.nil? || item == :controller
value
elsif collection
Routing.extract_parameter_value(value).gsub(/%2F/, "/")
else
Routing.extract_parameter_value(value)
end
else
item
end
end
@items.reverse_each do |item| # Remove default components from the end of the generated url.
break unless item.kind_of?(Symbol) && @defaults[item] == components.last
components.pop
end
# If we have any nil components then we can't proceed.
# This might need to be changed. In some cases we may be able to return all componets after nil as extras.
missing = []; components.each_with_index {|c, i| missing << @items[i] if c.nil?}
return nil, "No values provided for component#{'s' if missing.length > 1} #{missing.join ', '} but values are required due to use of later components" unless missing.empty? # how wide is your screen?
unused = (options.keys - used_names.keys).inject({}) do |unused, key|
unused[key] = options[key] if options[key] != @defaults[key]
unused
end
components.collect! {|c| c.to_s}
return components, unused
end
# Recognize the provided path, returning a hash of recognized values, or [nil, reason] if the path isn't recognized.
# The path should be a list of component strings.
# Options is a hash of the ?k=v pairs
def recognize(components, options={})
options = options.clone
components = components.clone
controller_class = nil
@items.each do |item|
if item == :controller # Special case for controller
if components.empty? && @defaults[:controller]
controller_class, leftover = eat_path_to_controller(@defaults[:controller].split('/'))
raise RoutingError, "Default controller does not exist: #{@defaults[:controller]}" if controller_class.nil? || leftover.empty? == false
else
controller_class, remaining_components = eat_path_to_controller(components)
return nil, "No controller found at subpath #{components.join('/')}" if controller_class.nil?
components = remaining_components
end
options[:controller] = controller_class.controller_path
return nil, requirements_for(:controller) unless passes_requirements?(:controller, options[:controller])
elsif /^\*/ =~ item.to_s
value = components.join("/") || @defaults[item]
components = []
options[item.to_s.sub(/^\*/,"").intern] = value.nil? ? value : CGI.unescape(value)
elsif item.kind_of? Symbol
value = components.shift || @defaults[item]
return nil, requirements_for(item) unless passes_requirements?(item, value)
options[item] = value.nil? ? value : CGI.unescape(value)
else
return nil, "No value available for component #{item.inspect}" if components.empty?
component = components.shift
return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item
end
end
if controller_class.nil? && @requirements[:controller] # Load a default controller
controller_class, extras = eat_path_to_controller(@requirements[:controller].split('/'))
raise RoutingError, "Illegal controller path for route default: #{@requirements[:controller]}" unless controller_class && extras.empty?
options[:controller] = controller_class.controller_path
end
@requirements.each {|k,v| options[k] ||= v unless v.kind_of?(Regexp)}
return nil, "Route recognition didn't find a controller class!" unless controller_class
return nil, "Unused components were left: #{components.join '/'}" unless components.empty?
options.delete_if {|k, v| v.nil?} # Remove nil values.
return controller_class, options
end
def inspect
when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}"
default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}"
"<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join('/').inspect}#{default_str}#{when_str}>"
end
protected
# Find the controller given a list of path components.
# Return the controller class and the unused path components.
def eat_path_to_controller(path)
path.inject([Controllers, 1]) do |(mod, length), name|
name = name.camelize
return nil, nil unless /^[A-Z][_a-zA-Z\d]*$/ =~ name
controller_name = name + "Controller"
return mod.const_get(controller_name), path[length..-1] if mod.const_available? controller_name
return nil, nil unless mod.const_available? name
[mod.const_get(name), length + 1]
end
return nil, nil # Path ended, but no controller found.
end
def items=(path)
items = path.split('/').collect {|c| (/^(:|\*)(\w+)$/ =~ c) ? (($1 == ':' ) ? $2.intern : "*#{$2}".intern) : c} if path.kind_of?(String) # split and convert ':xyz' to symbols
items.shift if items.first == ""
items.pop if items.last == ""
@items = items
# Verify uniqueness of each component.
@items.inject({}) do |seen, item|
if item.kind_of? Symbol
raise ArgumentError, "Illegal route path -- duplicate item #{item}\n #{path.inspect}" if seen.key? item
seen[item] = true
end
seen
end
end
# Verify that the given value passes this route's requirements
def passes_requirements?(name, value)
return @defaults.key?(name) && @defaults[name].nil? if value.nil? # Make sure it's there if it should be
case @requirements[name]
when nil then true
when Regexp then
value = value.to_s
match = @requirements[name].match(value)
match && match[0].length == value.length
else
@requirements[name] == value.to_s
end
end
def requirements_for(name)
name = name.to_s.sub(/^\*/,"").intern if (/^\*/ =~ name.inspect)
presence = (@defaults.key?(name) && @defaults[name].nil?)
requirement = case @requirements[name]
when nil then nil
when Regexp then "match #{@requirements[name].inspect}"
else "be equal to #{@requirements[name].inspect}"
end
if presence && requirement then "#{name} must be present and #{requirement}"
elsif presence || requirement then "#{name} must #{requirement || 'be present'}"
else "#{name} has no requirements"
end
end
end
class RouteSet#:nodoc:
def initialize
@routes = []
end
def add_route(route)
raise TypeError, "#{route.inspect} is not a Route instance!" unless route.kind_of?(Route)
@routes << route
end
def empty?
@routes.empty?
end
def each
@routes.each {|route| yield route}
end
# Generate a path for the provided options
# Returns the path as an array of components and a hash of unused names
# Raises RoutingError if not route can handle the provided components.
#
# Note that we don't return the first generated path. We do this so that when a route
# generates a path from a subset of the available options we can keep looking for a
# route which can generate a path that uses more options.
# Note that we *do* return immediately if
def generate(options, request)
raise RoutingError, "There are no routes defined!" if @routes.empty?
options = options.symbolize_keys
defaults = request.path_parameters.symbolize_keys
if options.empty? then options = defaults.clone # Get back the current url if no options was passed
else expand_controller_path!(options, defaults) # Expand the supplied controller path.
end
defaults.delete_if {|k, v| options.key?(k) && options[k].nil?} # Remove defaults that have been manually cleared using :name => nil
failures = []
selected = nil
self.each do |route|
path, unused = route.generate(options, defaults)
if path.nil?
failures << [route, unused] if ActionController::Base.debug_routes
else
return path, unused if unused.empty? # Found a perfect route -- we're finished.
if selected.nil? || unused.length < selected.last.length
failures << [selected.first, "A better url than #{selected[1]} was found."] if selected
selected = [route, path, unused]
end
end
end
return selected[1..-1] unless selected.nil?
raise RoutingError.new("Generation failure: No route for url_options #{options.inspect}, defaults: #{defaults.inspect}", failures)
end
# Recognize the provided path.
# Raise RoutingError if the path can't be recognized.
def recognize!(request)
path = ((%r{^/?(.*)/?$} =~ request.path) ? $1 : request.path).split('/')
raise RoutingError, "There are no routes defined!" if @routes.empty?
failures = []
self.each do |route|
controller, options = route.recognize(path)
if controller.nil?
failures << [route, options] if ActionController::Base.debug_routes
else
request.path_parameters = options
return controller
end
end
raise RoutingError.new("No route for path: #{path.join('/').inspect}", failures)
end
def expand_controller_path!(options, defaults)
if options[:controller]
if /^\// =~ options[:controller]
options[:controller] = options[:controller][1..-1]
defaults.clear # Sending to absolute controller implies fresh defaults
else
relative_to = defaults[:controller] ? defaults[:controller].split('/')[0..-2].join('/') : ''
options[:controller] = relative_to.empty? ? options[:controller] : "#{relative_to}/#{options[:controller]}"
defaults.delete(:action) if options.key?(:controller)
end
else
options[:controller] = defaults[:controller]
end
end
def route(*args)
add_route(Route.new(*args))
end
alias :connect :route
def reload
begin
route_file = defined?(RAILS_ROOT) ? File.join(RAILS_ROOT, 'config', 'routes') : nil
require_dependency(route_file) if route_file
rescue LoadError, ScriptError => e
raise RoutingError, "Cannot load config/routes.rb:\n #{e.message}"
ensure # Ensure that there is at least one route:
connect(':controller/:action/:id', :action => 'index', :id => nil) if @routes.empty?
end
end
def draw
@routes.clear
yield self
end
end
def self.extract_parameter_value(parameter)
value = (parameter.respond_to?(:to_param) ? parameter.to_param : parameter).to_s
CGI.escape(value)
end
def self.draw(*args, &block) #:nodoc:
Routes.draw(*args) {|*args| block.call(*args)}
end
Routes = RouteSet.new
end
end
|