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
|
module ActionController
module Routing
ROUTE_FILE = defined?(RAILS_ROOT) ? File.expand_path(File.join(RAILS_ROOT, 'config', 'routes')) : nil
class Route
attr_reader :defaults # The defaults hash
def initialize(path, hash={})
raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash)
@defaults = {}
@requirements = {}
self.items = path
hash.each do |k, v|
raise TypeError, "Hash may only contain symbols!" unless k.kind_of? Symbol
(@items.include?(k) ? @defaults : @requirements)[k] = v
end
# 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.inject([]) {|a, (k, v)| ((options[k] || defaults[k]) == v) ? a : a << k}
return nil, "Options mismatch requirements: #{non_matching.join ', '}" unless non_matching.empty?
used_names = @requirements.inject({}) {|hash, (k, v)| hash[k] = true; hash}
components = @items.collect do |item|
if item.kind_of? Symbol
used_names[item] = true
value = options[item] || defaults[item] || @defaults[item]
return nil, "#{item.inspect} was not given and has no default." if value.nil? && ! (@defaults.key?(item) && @defaults[item].nil?) # Don't leave if nil value.
defaults = {} unless defaults == {} || value == defaults[item] # Stop using defaults if this component isn't the same as the default.
value
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}
components.unshift(components.shift + '/') if components.length == 1 && @items.first == :controller # Add '/' to controllers
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
elsif item.kind_of? Symbol
value = components.shift || @defaults[item]
return nil, "No value or default for parameter #{item.inspect}" if value.nil? && ! (@defaults.key?(item) && @defaults[item].nil?)
options[item] = 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
options = @requirements.merge(options)
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
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.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
end
class RouteSet
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
expand_controller_path!(options, defaults)
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
options.each {|k, v| request.path_parameters[k] = CGI.unescape(v)}
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 require_dependency(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.draw(*args, &block)
Routes.draw(*args) {|*args| block.call(*args)}
end
Routes = RouteSet.new
Routes.reload # Server will die on load if SyntaxError
end
end
|