| 18 | | (@items.include?(k) ? @defaults : @requirements)[k] = (v.nil? ? nil : v.to_s) |
|---|
| 19 | | end |
|---|
| 20 | | end |
|---|
| 21 | | |
|---|
| 22 | | @defaults.each do |k, v| |
|---|
| 23 | | raise ArgumentError, "A default has been specified for #{k}, but #{k} is not in the path!" unless @items.include? k |
|---|
| 24 | | @defaults[k] = v.to_s unless v.kind_of?(String) || v.nil? |
|---|
| 25 | | end |
|---|
| 26 | | @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)} |
|---|
| 27 | | |
|---|
| 28 | | # Add in defaults for :action and :id. |
|---|
| 29 | | [[:action, 'index'], [:id, nil]].each do |name, default| |
|---|
| 30 | | @defaults[name] = default if @items.include?(name) && ! (@requirements.key?(name) || @defaults.key?(name)) |
|---|
| 31 | | end |
|---|
| 32 | | end |
|---|
| 33 | | |
|---|
| 34 | | # Generate a URL given the provided options. |
|---|
| 35 | | # All values in options should be symbols. |
|---|
| 36 | | # Returns the path and the unused names in a 2 element array. |
|---|
| 37 | | # If generation fails, [nil, nil] is returned |
|---|
| 38 | | # Generation can fail because of a missing value, or because an equality check fails. |
|---|
| 39 | | # |
|---|
| 40 | | # Generate urls will be as short as possible. If the last component of a url is equal to the default value, |
|---|
| 41 | | # then that component is removed. This is applied as many times as possible. So, your index controller's |
|---|
| 42 | | # index action will generate [] |
|---|
| 43 | | def generate(options, defaults={}) |
|---|
| 44 | | non_matching = @requirements.keys.select {|name| ! passes_requirements?(name, options[name] || defaults[name])} |
|---|
| 45 | | non_matching.collect! {|name| requirements_for(name)} |
|---|
| 46 | | return nil, "Mismatching option#{'s' if non_matching.length > 1}:\n #{non_matching.join '\n '}" unless non_matching.empty? |
|---|
| 47 | | |
|---|
| 48 | | 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 |
|---|
| 49 | | components = @items.collect do |item| |
|---|
| 50 | | |
|---|
| 51 | | if item.kind_of? Symbol |
|---|
| 52 | | collection = false |
|---|
| 53 | | |
|---|
| 54 | | if /^\*/ =~ item.to_s |
|---|
| 55 | | collection = true |
|---|
| 56 | | item = item.to_s.sub(/^\*/,"").intern |
|---|
| | 45 | raise ArgumentError, "Valid criteria are strings, regular expressions, true, or nil" |
|---|
| | 46 | end |
|---|
| | 47 | end |
|---|
| | 48 | end |
|---|
| | 49 | |
|---|
| | 50 | class Component #:nodoc |
|---|
| | 51 | def dynamic?() false end |
|---|
| | 52 | def optional?() false end |
|---|
| | 53 | |
|---|
| | 54 | def key() nil end |
|---|
| | 55 | |
|---|
| | 56 | def self.new(string, *args) |
|---|
| | 57 | return super(string, *args) unless self == Component |
|---|
| | 58 | case string |
|---|
| | 59 | when ':controller' then ControllerComponent.new(:controller, *args) |
|---|
| | 60 | when /^:(\w+)$/ then DynamicComponent.new($1, *args) |
|---|
| | 61 | when /^\*(\w+)$/ then PathComponent.new($1, *args) |
|---|
| | 62 | else StaticComponent.new(string, *args) |
|---|
| | 63 | end |
|---|
| | 64 | end |
|---|
| | 65 | end |
|---|
| | 66 | |
|---|
| | 67 | class StaticComponent < Component #:nodoc |
|---|
| | 68 | attr_reader :value |
|---|
| | 69 | |
|---|
| | 70 | def initialize(value) |
|---|
| | 71 | @value = value |
|---|
| | 72 | end |
|---|
| | 73 | |
|---|
| | 74 | def write_recognition(g) |
|---|
| | 75 | g.if_next_matches(value) do |gp| |
|---|
| | 76 | gp.move_forward {|gpp| gpp.continue} |
|---|
| | 77 | end |
|---|
| | 78 | end |
|---|
| | 79 | |
|---|
| | 80 | def write_generation(g) |
|---|
| | 81 | g.add_segment(value) {|gp| gp.continue } |
|---|
| | 82 | end |
|---|
| | 83 | end |
|---|
| | 84 | |
|---|
| | 85 | class DynamicComponent < Component #:nodoc |
|---|
| | 86 | attr_reader :key, :default |
|---|
| | 87 | attr_accessor :condition |
|---|
| | 88 | |
|---|
| | 89 | def dynamic?() true end |
|---|
| | 90 | def optional?() @optional end |
|---|
| | 91 | |
|---|
| | 92 | def default=(default) |
|---|
| | 93 | @optional = true |
|---|
| | 94 | @default = default |
|---|
| | 95 | end |
|---|
| | 96 | |
|---|
| | 97 | def initialize(key, options = {}) |
|---|
| | 98 | @key = key.to_sym |
|---|
| | 99 | @default, @condition = options[:default], options[:condition] |
|---|
| | 100 | @optional = options.key?(:default) |
|---|
| | 101 | end |
|---|
| | 102 | |
|---|
| | 103 | def default_check(g) |
|---|
| | 104 | presence = "#{g.hash_value(key, !! default)}" |
|---|
| | 105 | if default |
|---|
| | 106 | "!(#{presence} && #{g.hash_value(key, false)} != #{default.inspect})" |
|---|
| | 107 | else |
|---|
| | 108 | "! #{presence}" |
|---|
| | 109 | end |
|---|
| | 110 | end |
|---|
| | 111 | |
|---|
| | 112 | def write_generation(g) |
|---|
| | 113 | wrote_dropout = write_dropout_generation(g) |
|---|
| | 114 | write_continue_generation(g, wrote_dropout) |
|---|
| | 115 | end |
|---|
| | 116 | |
|---|
| | 117 | def write_dropout_generation(g) |
|---|
| | 118 | return false unless optional? && g.after.all? {|c| c.optional?} |
|---|
| | 119 | |
|---|
| | 120 | check = [default_check(g)] |
|---|
| | 121 | gp = g.dup # Use another generator to write the conditions after the first && |
|---|
| | 122 | # We do this to ensure that the generator will not assume x_value is set. It will |
|---|
| | 123 | # not be set if it follows a false condition -- for example, false && (x = 2) |
|---|
| | 124 | |
|---|
| | 125 | gp.after.map {|c| c.default_check gp} |
|---|
| | 126 | gp.if(check.join(' && ')) { gp.finish } # If this condition is met, we stop here |
|---|
| | 127 | true |
|---|
| | 128 | end |
|---|
| | 129 | |
|---|
| | 130 | def write_continue_generation(g, use_else) |
|---|
| | 131 | test = Routing.test_condition(g.hash_value(key, true, default), condition || true) |
|---|
| | 132 | check = (use_else && condition.nil? && default) ? [:else] : [use_else ? :elsif : :if, test] |
|---|
| | 133 | |
|---|
| | 134 | g.send(*check) do |gp| |
|---|
| | 135 | gp.expire_for_keys(key) unless gp.after.empty? |
|---|
| | 136 | add_segments_to(gp) {|gpp| gpp.continue} |
|---|
| | 137 | end |
|---|
| | 138 | end |
|---|
| | 139 | |
|---|
| | 140 | def add_segments_to(g) |
|---|
| | 141 | g.add_segment(%(\#{CGI.escape(#{g.hash_value(key, true, default)})})) {|gp| yield gp} |
|---|
| | 142 | end |
|---|
| | 143 | |
|---|
| | 144 | def recognition_check(g) |
|---|
| | 145 | test_type = [true, nil].include?(condition) ? :presence : :constraint |
|---|
| | 146 | |
|---|
| | 147 | prefix = condition.is_a?(Regexp) ? "#{g.next_segment(true)} && " : '' |
|---|
| | 148 | check = prefix + Routing.test_condition(g.next_segment(true), condition || true) |
|---|
| | 149 | |
|---|
| | 150 | g.if(check) {|gp| yield gp, test_type} |
|---|
| | 151 | end |
|---|
| | 152 | |
|---|
| | 153 | def write_recognition(g) |
|---|
| | 154 | test_type = nil |
|---|
| | 155 | recognition_check(g) do |gp, test_type| |
|---|
| | 156 | assign_result(gp) {|gpp| gpp.continue} |
|---|
| | 157 | end |
|---|
| | 158 | |
|---|
| | 159 | if optional? && g.after.all? {|c| c.optional?} |
|---|
| | 160 | call = (test_type == :presence) ? [:else] : [:elsif, "! #{g.next_segment(true)}"] |
|---|
| | 161 | |
|---|
| | 162 | g.send(*call) do |gp| |
|---|
| | 163 | assign_default(gp) |
|---|
| | 164 | gp.after.each {|c| c.assign_default(gp)} |
|---|
| | 165 | gp.finish(false) |
|---|
| | 166 | end |
|---|
| | 167 | end |
|---|
| | 168 | end |
|---|
| | 169 | |
|---|
| | 170 | def assign_result(g, with_default = false) |
|---|
| | 171 | g.result key, "CGI.unescape(#{g.next_segment(true, with_default ? default : nil)})" |
|---|
| | 172 | g.move_forward {|gp| yield gp} |
|---|
| | 173 | end |
|---|
| | 174 | |
|---|
| | 175 | def assign_default(g) |
|---|
| | 176 | g.constant_result key, default unless default.nil? |
|---|
| | 177 | end |
|---|
| | 178 | end |
|---|
| | 179 | |
|---|
| | 180 | class ControllerComponent < DynamicComponent #:nodoc |
|---|
| | 181 | def key() :controller end |
|---|
| | 182 | |
|---|
| | 183 | def add_segments_to(g) |
|---|
| | 184 | g.add_segment(%(\#{#{g.hash_value(key, true, default)}})) {|gp| yield gp} |
|---|
| | 185 | end |
|---|
| | 186 | |
|---|
| | 187 | def recognition_check(g) |
|---|
| | 188 | g << "controller_result = ::ActionController::Routing::ControllerComponent.traverse_to_controller(#{g.path_name}, #{g.index_name})" |
|---|
| | 189 | g.if('controller_result') do |gp| |
|---|
| | 190 | gp << 'controller_value, segments_to_controller = controller_result' |
|---|
| | 191 | gp.move_forward('segments_to_controller') {|gpp| yield gpp, :constraint} |
|---|
| | 192 | end |
|---|
| | 193 | end |
|---|
| | 194 | |
|---|
| | 195 | def assign_result(g) |
|---|
| | 196 | g.result key, 'controller_value' |
|---|
| | 197 | yield g |
|---|
| | 198 | end |
|---|
| | 199 | |
|---|
| | 200 | def assign_default(g) |
|---|
| | 201 | ControllerComponent.assign_controller(g, default) |
|---|
| | 202 | end |
|---|
| | 203 | |
|---|
| | 204 | class << self |
|---|
| | 205 | def assign_controller(g, controller) |
|---|
| | 206 | expr = "::Controllers::#{controller.split('/').collect {|c| c.camelize}.join('::')}Controller" |
|---|
| | 207 | g.result :controller, expr, true |
|---|
| | 208 | end |
|---|
| | 209 | |
|---|
| | 210 | def traverse_to_controller(segments, start_at = 0) |
|---|
| | 211 | mod = ::Controllers |
|---|
| | 212 | length = segments.length |
|---|
| | 213 | index = start_at |
|---|
| | 214 | mod_name = controller_name = segment = nil |
|---|
| | 215 | |
|---|
| | 216 | while index < length |
|---|
| | 217 | return nil unless /^[a-z][a-z\d_]*$/ =~ (segment = segments[index]) |
|---|
| | 218 | index += 1 |
|---|
| | 219 | |
|---|
| | 220 | mod_name = segment.camelize |
|---|
| | 221 | controller_name = "#{mod_name}Controller" |
|---|
| | 222 | |
|---|
| | 223 | return eval("mod::#{controller_name}"), (index - start_at) if mod.const_available?(controller_name) |
|---|
| | 224 | return nil unless mod.const_available?(mod_name) |
|---|
| | 225 | mod = eval("mod::#{mod_name}") |
|---|
| | 226 | end |
|---|
| | 227 | end |
|---|
| | 228 | end |
|---|
| | 229 | end |
|---|
| | 230 | |
|---|
| | 231 | class PathComponent < DynamicComponent #:nodoc |
|---|
| | 232 | def optional?() true end |
|---|
| | 233 | def default() '' end |
|---|
| | 234 | def condition() nil end |
|---|
| | 235 | |
|---|
| | 236 | def write_generation(g) |
|---|
| | 237 | raise RoutingError, 'Path components must occur last' unless g.after.empty? |
|---|
| | 238 | g.if("#{g.hash_value(key, true)} && ! #{g.hash_value(key, true)}.empty?") do |
|---|
| | 239 | g << "#{g.hash_value(key, true)} = #{g.hash_value(key, true)}.join('/') unless #{g.hash_value(key, true)}.is_a?(String)" |
|---|
| | 240 | g.add_segment("\#{CGI.escape_skipping_slashes(#{g.hash_value(key, true)})}") {|gp| gp.finish } |
|---|
| | 241 | end |
|---|
| | 242 | g.else { g.finish } |
|---|
| | 243 | end |
|---|
| | 244 | |
|---|
| | 245 | def write_recognition(g) |
|---|
| | 246 | raise RoutingError, "Path components must occur last" unless g.after.empty? |
|---|
| | 247 | |
|---|
| | 248 | start = g.index_name |
|---|
| | 249 | start = "(#{start})" unless /^\w+$/ =~ start |
|---|
| | 250 | |
|---|
| | 251 | value_expr = "#{g.path_name}[#{start}..-1] || []" |
|---|
| | 252 | g.result key, "ActionController::Routing::PathComponent::Result.new(#{value_expr})" |
|---|
| | 253 | g.finish(false) |
|---|
| | 254 | end |
|---|
| | 255 | |
|---|
| | 256 | class Result < ::Array |
|---|
| | 257 | def to_s() join '/' end |
|---|
| | 258 | end |
|---|
| | 259 | end |
|---|
| | 260 | |
|---|
| | 261 | class Route |
|---|
| | 262 | attr_accessor :components, :known |
|---|
| | 263 | attr_reader :path, :options, :keys |
|---|
| | 264 | |
|---|
| | 265 | def initialize(path, options = {}) |
|---|
| | 266 | @path, @options = path, options |
|---|
| | 267 | |
|---|
| | 268 | initialize_components path |
|---|
| | 269 | defaults, conditions = initialize_hashes options.dup |
|---|
| | 270 | configure_components(defaults, conditions) |
|---|
| | 271 | initialize_keys |
|---|
| | 272 | end |
|---|
| | 273 | |
|---|
| | 274 | def inspect |
|---|
| | 275 | "<#{self.class} #{path.inspect}, #{options.inspect[1..-1]}>" |
|---|
| | 276 | end |
|---|
| | 277 | |
|---|
| | 278 | def write_generation(generator = CodeGeneration::GenerationGenerator.new) |
|---|
| | 279 | generator.before, generator.current, generator.after = [], components.first, (components[1..-1] || []) |
|---|
| | 280 | |
|---|
| | 281 | if known.empty? then generator.go |
|---|
| | 282 | else generator.if(generator.check_conditions(known)) {|gp| gp.go } |
|---|
| | 283 | end |
|---|
| | 284 | |
|---|
| | 285 | generator |
|---|
| | 286 | end |
|---|
| | 287 | |
|---|
| | 288 | def write_recognition(generator = CodeGeneration::RecognitionGenerator.new) |
|---|
| | 289 | g = generator.dup |
|---|
| | 290 | g.share_locals_with generator |
|---|
| | 291 | g.before, g.current, g.after = [], components.first, (components[1..-1] || []) |
|---|
| | 292 | |
|---|
| | 293 | known.each do |key, value| |
|---|
| | 294 | if key == :controller then ControllerComponent.assign_controller(g, value) |
|---|
| | 295 | else g.constant_result(key, value) |
|---|
| | 296 | end |
|---|
| | 297 | end |
|---|
| | 298 | |
|---|
| | 299 | g.go |
|---|
| | 300 | |
|---|
| | 301 | generator |
|---|
| | 302 | end |
|---|
| | 303 | |
|---|
| | 304 | def initialize_keys |
|---|
| | 305 | @keys = (components.collect {|c| c.key} + known.keys).compact |
|---|
| | 306 | @keys.freeze |
|---|
| | 307 | end |
|---|
| | 308 | |
|---|
| | 309 | def extra_keys(options) |
|---|
| | 310 | options.keys - @keys |
|---|
| | 311 | end |
|---|
| | 312 | |
|---|
| | 313 | def matches_controller?(controller) |
|---|
| | 314 | if known[:controller] then known[:controller] == controller |
|---|
| | 315 | else |
|---|
| | 316 | c = components.find {|c| c.key == :controller} |
|---|
| | 317 | return false unless c |
|---|
| | 318 | return c.condition.nil? || eval(Routing.test_condition('controller', c.condition)) |
|---|
| | 319 | end |
|---|
| | 320 | end |
|---|
| | 321 | |
|---|
| | 322 | protected |
|---|
| | 323 | |
|---|
| | 324 | def initialize_components(path) |
|---|
| | 325 | path = path.split('/') if path.is_a? String |
|---|
| | 326 | self.components = path.collect {|str| Component.new str} |
|---|
| | 327 | end |
|---|
| | 328 | |
|---|
| | 329 | def initialize_hashes(options) |
|---|
| | 330 | path_keys = components.collect {|c| c.key }.compact |
|---|
| | 331 | self.known = {} |
|---|
| | 332 | defaults = options.delete(:defaults) || {} |
|---|
| | 333 | conditions = options.delete(:require) || {} |
|---|
| | 334 | conditions.update(options.delete(:requirements) || {}) |
|---|
| | 335 | |
|---|
| | 336 | options.each do |k, v| |
|---|
| | 337 | if path_keys.include?(k) then (v.is_a?(Regexp) ? conditions : defaults)[k] = v |
|---|
| | 338 | else known[k] = v |
|---|
| 77 | | else |
|---|
| 78 | | item |
|---|
| 79 | | end |
|---|
| 80 | | end |
|---|
| 81 | | |
|---|
| 82 | | @items.reverse_each do |item| # Remove default components from the end of the generated url. |
|---|
| 83 | | break unless item.kind_of?(Symbol) && @defaults[item] == components.last |
|---|
| 84 | | components.pop |
|---|
| 85 | | end |
|---|
| 86 | | |
|---|
| 87 | | # If we have any nil components then we can't proceed. |
|---|
| 88 | | # This might need to be changed. In some cases we may be able to return all componets after nil as extras. |
|---|
| 89 | | missing = []; components.each_with_index {|c, i| missing << @items[i] if c.nil?} |
|---|
| 90 | | 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? |
|---|
| 91 | | |
|---|
| 92 | | unused = (options.keys - used_names.keys).inject({}) do |unused, key| |
|---|
| 93 | | unused[key] = options[key] if options[key] != @defaults[key] |
|---|
| 94 | | unused |
|---|
| 95 | | end |
|---|
| 96 | | |
|---|
| 97 | | components.collect! {|c| c.to_s} |
|---|
| 98 | | return components, unused |
|---|
| 99 | | end |
|---|
| 100 | | |
|---|
| 101 | | # Recognize the provided path, returning a hash of recognized values, or [nil, reason] if the path isn't recognized. |
|---|
| 102 | | # The path should be a list of component strings. |
|---|
| 103 | | # Options is a hash of the ?k=v pairs |
|---|
| 104 | | def recognize(components, options={}) |
|---|
| 105 | | options = options.clone |
|---|
| 106 | | components = components.clone |
|---|
| 107 | | controller_class = nil |
|---|
| 108 | | |
|---|
| 109 | | @items.each do |item| |
|---|
| 110 | | if item == :controller # Special case for controller |
|---|
| 111 | | if components.empty? && @defaults[:controller] |
|---|
| 112 | | controller_class, leftover = eat_path_to_controller(@defaults[:controller].split('/')) |
|---|
| 113 | | raise RoutingError, "Default controller does not exist: #{@defaults[:controller]}" if controller_class.nil? || leftover.empty? == false |
|---|
| 114 | | else |
|---|
| 115 | | controller_class, remaining_components = eat_path_to_controller(components) |
|---|
| 116 | | return nil, "No controller found at subpath #{components.join('/')}" if controller_class.nil? |
|---|
| 117 | | components = remaining_components |
|---|
| 118 | | end |
|---|
| 119 | | options[:controller] = controller_class.controller_path |
|---|
| 120 | | return nil, requirements_for(:controller) unless passes_requirements?(:controller, options[:controller]) |
|---|
| 121 | | elsif /^\*/ =~ item.to_s |
|---|
| 122 | | if components.empty? |
|---|
| 123 | | value = @defaults.has_key?(item) ? @defaults[item].clone : [] |
|---|
| 124 | | else |
|---|
| 125 | | value = components.clone |
|---|
| 126 | | end |
|---|
| 127 | | value.collect! {|c| CGI.unescape c} |
|---|
| 128 | | components = [] |
|---|
| 129 | | def value.to_s() self.join('/') end |
|---|
| 130 | | options[item.to_s.sub(/^\*/,"").intern] = value |
|---|
| 131 | | elsif item.kind_of? Symbol |
|---|
| 132 | | value = components.shift || @defaults[item] |
|---|
| 133 | | return nil, requirements_for(item) unless passes_requirements?(item, value) |
|---|
| 134 | | options[item] = value.nil? ? value : CGI.unescape(value) |
|---|
| 135 | | else |
|---|
| 136 | | return nil, "No value available for component #{item.inspect}" if components.empty? |
|---|
| 137 | | component = components.shift |
|---|
| 138 | | return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item |
|---|
| 139 | | end |
|---|
| 140 | | end |
|---|
| 141 | | |
|---|
| 142 | | if controller_class.nil? && @requirements[:controller] # Load a default controller |
|---|
| 143 | | controller_class, extras = eat_path_to_controller(@requirements[:controller].split('/')) |
|---|
| 144 | | raise RoutingError, "Illegal controller path for route default: #{@requirements[:controller]}" unless controller_class && extras.empty? |
|---|
| 145 | | options[:controller] = controller_class.controller_path |
|---|
| 146 | | end |
|---|
| 147 | | @requirements.each {|k,v| options[k] ||= v unless v.kind_of?(Regexp)} |
|---|
| 148 | | |
|---|
| 149 | | return nil, "Route recognition didn't find a controller class!" unless controller_class |
|---|
| 150 | | return nil, "Unused components were left: #{components.join '/'}" unless components.empty? |
|---|
| 151 | | options.delete_if {|k, v| v.nil?} # Remove nil values. |
|---|
| 152 | | return controller_class, options |
|---|
| 153 | | end |
|---|
| 154 | | |
|---|
| 155 | | def inspect |
|---|
| 156 | | when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}" |
|---|
| 157 | | default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}" |
|---|
| 158 | | "<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join('/').inspect}#{default_str}#{when_str}>" |
|---|
| 159 | | end |
|---|
| 160 | | |
|---|
| 161 | | protected |
|---|
| 162 | | # Find the controller given a list of path components. |
|---|
| 163 | | # Return the controller class and the unused path components. |
|---|
| 164 | | def eat_path_to_controller(path) |
|---|
| 165 | | path.inject([Controllers, 1]) do |(mod, length), name| |
|---|
| 166 | | name = name.camelize |
|---|
| 167 | | return nil, nil unless /^[A-Z][_a-zA-Z\d]*$/ =~ name |
|---|
| 168 | | controller_name = name + "Controller" |
|---|
| 169 | | return eval("mod::#{controller_name}"), path[length..-1] if mod.const_available? controller_name |
|---|
| 170 | | return nil, nil unless mod.const_available? name |
|---|
| 171 | | [mod.const_get(name), length + 1] |
|---|
| 172 | | end |
|---|
| 173 | | return nil, nil # Path ended, but no controller found. |
|---|
| 174 | | end |
|---|
| 175 | | |
|---|
| 176 | | def items=(path) |
|---|
| 177 | | items = path.split('/').collect {|c| (/^(:|\*)(\w+)$/ =~ c) ? (($1 == ':' ) ? $2.intern : "*#{$2}".intern) : c} if path.kind_of?(String) # split and convert ':xyz' to symbols |
|---|
| 178 | | items.shift if items.first == "" |
|---|
| 179 | | items.pop if items.last == "" |
|---|
| 180 | | @items = items |
|---|
| 181 | | |
|---|
| 182 | | # Verify uniqueness of each component. |
|---|
| 183 | | @items.inject({}) do |seen, item| |
|---|
| 184 | | if item.kind_of? Symbol |
|---|
| 185 | | raise ArgumentError, "Illegal route path -- duplicate item #{item}\n #{path.inspect}" if seen.key? item |
|---|
| 186 | | seen[item] = true |
|---|
| 187 | | end |
|---|
| 188 | | seen |
|---|
| 189 | | end |
|---|
| 190 | | end |
|---|
| 191 | | |
|---|
| 192 | | # Verify that the given value passes this route's requirements |
|---|
| 193 | | def passes_requirements?(name, value) |
|---|
| 194 | | return @defaults.key?(name) && @defaults[name].nil? if value.nil? # Make sure it's there if it should be |
|---|
| 195 | | |
|---|
| 196 | | case @requirements[name] |
|---|
| 197 | | when nil then true |
|---|
| 198 | | when Regexp then |
|---|
| 199 | | value = value.to_s |
|---|
| 200 | | match = @requirements[name].match(value) |
|---|
| 201 | | match && match[0].length == value.length |
|---|
| 202 | | else |
|---|
| 203 | | @requirements[name] == value.to_s |
|---|
| 204 | | end |
|---|
| 205 | | end |
|---|
| 206 | | def requirements_for(name) |
|---|
| 207 | | name = name.to_s.sub(/^\*/,"").intern if (/^\*/ =~ name.inspect) |
|---|
| 208 | | presence = (@defaults.key?(name) && @defaults[name].nil?) |
|---|
| 209 | | requirement = case @requirements[name] |
|---|
| 210 | | when nil then nil |
|---|
| 211 | | when Regexp then "match #{@requirements[name].inspect}" |
|---|
| 212 | | else "be equal to #{@requirements[name].inspect}" |
|---|
| 213 | | end |
|---|
| 214 | | if presence && requirement then "#{name} must be present and #{requirement}" |
|---|
| 215 | | elsif presence || requirement then "#{name} must #{requirement || 'be present'}" |
|---|
| 216 | | else "#{name} has no requirements" |
|---|
| 217 | | end |
|---|
| 218 | | end |
|---|
| 219 | | end |
|---|
| 220 | | |
|---|
| 221 | | class RouteSet#:nodoc: |
|---|
| | 350 | |
|---|
| | 351 | component.condition = conditions[component.key] if conditions.key?(component.key) |
|---|
| | 352 | end |
|---|
| | 353 | end |
|---|
| | 354 | end |
|---|
| | 355 | |
|---|
| | 356 | class RouteSet |
|---|
| | 357 | attr_reader :routes, :categories, :controller_to_selector |
|---|
| 224 | | end |
|---|
| 225 | | |
|---|
| 226 | | def add_route(route) |
|---|
| 227 | | raise TypeError, "#{route.inspect} is not a Route instance!" unless route.kind_of?(Route) |
|---|
| 228 | | @routes << route |
|---|
| 229 | | end |
|---|
| 230 | | def empty? |
|---|
| 231 | | @routes.empty? |
|---|
| 232 | | end |
|---|
| 233 | | def each |
|---|
| 234 | | @routes.each {|route| yield route} |
|---|
| 235 | | end |
|---|
| 236 | | |
|---|
| 237 | | # Generate a path for the provided options |
|---|
| 238 | | # Returns the path as an array of components and a hash of unused names |
|---|
| 239 | | # Raises RoutingError if not route can handle the provided components. |
|---|
| 240 | | # |
|---|
| 241 | | # Note that we don't return the first generated path. We do this so that when a route |
|---|
| 242 | | # generates a path from a subset of the available options we can keep looking for a |
|---|
| 243 | | # route which can generate a path that uses more options. |
|---|
| 244 | | # Note that we *do* return immediately if |
|---|
| 245 | | def generate(options, request) |
|---|
| 246 | | raise RoutingError, "There are no routes defined!" if @routes.empty? |
|---|
| 247 | | |
|---|
| 248 | | options = options.symbolize_keys |
|---|
| 249 | | defaults = request.path_parameters.symbolize_keys |
|---|
| 250 | | if options.empty? then options = defaults.clone # Get back the current url if no options was passed |
|---|
| 251 | | else expand_controller_path!(options, defaults) # Expand the supplied controller path. |
|---|
| 252 | | end |
|---|
| 253 | | defaults.delete_if {|k, v| options.key?(k) && options[k].nil?} # Remove defaults that have been manually cleared using :name => nil |
|---|
| 254 | | |
|---|
| 255 | | failures = [] |
|---|
| 256 | | selected = nil |
|---|
| 257 | | self.each do |route| |
|---|
| 258 | | path, unused = route.generate(options, defaults) |
|---|
| 259 | | if path.nil? |
|---|
| 260 | | failures << [route, unused] if ActionController::Base.debug_routes |
|---|
| 261 | | else |
|---|
| 262 | | return path, unused if unused.empty? # Found a perfect route -- we're finished. |
|---|
| 263 | | if selected.nil? || unused.length < selected.last.length |
|---|
| 264 | | failures << [selected.first, "A better url than #{selected[1]} was found."] if selected |
|---|
| 265 | | selected = [route, path, unused] |
|---|
| | 360 | @generation_methods = Hash.new(:generate_default_path) |
|---|
| | 361 | end |
|---|
| | 362 | |
|---|
| | 363 | def generate(options, request_or_recall_hash = {}) |
|---|
| | 364 | recall = request_or_recall_hash.is_a?(Hash) ? request_or_recall_hash : request_or_recall_hash.symbolized_path_parameters |
|---|
| | 365 | |
|---|
| | 366 | if ((rc_c = recall[:controller]) && rc_c.include?(?/)) || ((c = options[:controller]) && c.include?(?/)) |
|---|
| | 367 | options[:controller] = Routing.controller_relative_to(c, rc_c) |
|---|
| | 368 | end |
|---|
| | 369 | options = recall.dup if options.empty? # XXX move to url_rewriter? |
|---|
| | 370 | Routing.treat_hash(options) # XXX Move inwards (to generated code) or inline? |
|---|
| | 371 | merged = recall.merge(options) |
|---|
| | 372 | expire_on = Routing.expiry_hash(options, recall) |
|---|
| | 373 | |
|---|
| | 374 | path, keys = generate_path(merged, options, expire_on) |
|---|
| | 375 | |
|---|
| | 376 | # Factor out? |
|---|
| | 377 | extras = {} |
|---|
| | 378 | k = nil |
|---|
| | 379 | keys.each {|k| extras[k] = options[k]} |
|---|
| | 380 | [path, extras] |
|---|
| | 381 | end |
|---|
| | 382 | |
|---|
| | 383 | def generate_path(merged, options, expire_on) |
|---|
| | 384 | send @generation_methods[merged[:controller]], merged, options, expire_on |
|---|
| | 385 | end |
|---|
| | 386 | |
|---|
| | 387 | def write_generation |
|---|
| | 388 | @generation_methods = Hash.new(:generate_default_path) |
|---|
| | 389 | categorize_routes.each do |controller, routes| |
|---|
| | 390 | next unless routes.length < @routes.length |
|---|
| | 391 | |
|---|
| | 392 | ivar = controller.gsub('/', '__') |
|---|
| | 393 | method_name = "generate_path_for_#{ivar}".to_sym |
|---|
| | 394 | instance_variable_set "@#{ivar}", routes |
|---|
| | 395 | code = generation_code_for(ivar, method_name).to_s |
|---|
| | 396 | eval(code) |
|---|
| | 397 | |
|---|
| | 398 | @generation_methods[controller.to_s] = method_name |
|---|
| | 399 | @generation_methods[controller.to_sym] = method_name |
|---|
| | 400 | end |
|---|
| | 401 | |
|---|
| | 402 | eval(generation_code_for('routes', 'generate_default_path').to_s) |
|---|
| | 403 | end |
|---|
| | 404 | |
|---|
| | 405 | def recognize(request) |
|---|
| | 406 | string_path = request.path |
|---|
| | 407 | string_path.chomp! if string_path[0] == ?/ |
|---|
| | 408 | path = string_path.split '/' |
|---|
| | 409 | path.shift |
|---|
| | 410 | |
|---|
| | 411 | hash = recognize_path(path) |
|---|
| | 412 | raise RoutingError, "No route matches path #{path.inspect}" unless hash |
|---|
| | 413 | |
|---|
| | 414 | controller = hash['controller'] |
|---|
| | 415 | hash['controller'] = controller.controller_path |
|---|
| | 416 | request.path_parameters = hash |
|---|
| | 417 | controller.new |
|---|
| | 418 | end |
|---|
| | 419 | alias :recognize! :recognize |
|---|
| | 420 | |
|---|
| | 421 | def write_recognition |
|---|
| | 422 | g = generator = CodeGeneration::RecognitionGenerator.new |
|---|
| | 423 | g.finish_statement = Proc.new {|hash_expr| "return #{hash_expr}"} |
|---|
| | 424 | |
|---|
| | 425 | g.def "self.recognize_path(path)" do |
|---|
| | 426 | each do |route| |
|---|
| | 427 | g << 'index = 0' |
|---|
| | 428 | route.write_recognition(g) |
|---|
| | 429 | end |
|---|
| | 430 | end |
|---|
| | 431 | |
|---|
| | 432 | eval g.to_s |
|---|
| | 433 | end |
|---|
| | 434 | |
|---|
| | 435 | def generation_code_for(ivar = 'routes', method_name = nil) |
|---|
| | 436 | routes = instance_variable_get('@' + ivar) |
|---|
| | 437 | key_ivar = "@keys_for_#{ivar}" |
|---|
| | 438 | instance_variable_set(key_ivar, routes.collect {|route| route.keys}) |
|---|
| | 439 | |
|---|
| | 440 | g = generator = CodeGeneration::GenerationGenerator.new |
|---|
| | 441 | g.def "self.#{method_name}(merged, options, expire_on)" do |
|---|
| | 442 | g << 'unused_count = options.length + 1' |
|---|
| | 443 | g << "unused_keys = keys = options.keys" |
|---|
| | 444 | g << 'path = nil' |
|---|
| | 445 | |
|---|
| | 446 | routes.each_with_index do |route, index| |
|---|
| | 447 | g << "new_unused_keys = keys - #{key_ivar}[#{index}]" |
|---|
| | 448 | g << 'new_path = (' |
|---|
| | 449 | g.source.indent do |
|---|
| | 450 | if index.zero? |
|---|
| | 451 | g << "new_unused_count = new_unused_keys.length" |
|---|
| | 452 | g << "hash = merged; not_expired = true" |
|---|
| | 453 | route.write_generation(g.dup) |
|---|
| | 454 | else |
|---|
| | 455 | g.if "(new_unused_count = new_unused_keys.length) < unused_count" do |gp| |
|---|
| | 456 | gp << "hash = merged; not_expired = true" |
|---|
| | 457 | route.write_generation(gp) |
|---|
| | 458 | end |
|---|
| | 459 | end |
|---|
| 267 | | end |
|---|
| 268 | | end |
|---|
| 269 | | |
|---|
| 270 | | return selected[1..-1] unless selected.nil? |
|---|
| 271 | | raise RoutingError.new("Generation failure: No route for url_options #{options.inspect}, defaults: #{defaults.inspect}", failures) |
|---|
| 272 | | end |
|---|
| 273 | | |
|---|
| 274 | | # Recognize the provided path. |
|---|
| 275 | | # Raise RoutingError if the path can't be recognized. |
|---|
| 276 | | def recognize!(request) |
|---|
| 277 | | path = ((%r{^/?(.*)/?$} =~ request.path) ? $1 : request.path).split('/') |
|---|
| 278 | | raise RoutingError, "There are no routes defined!" if @routes.empty? |
|---|
| 279 | | |
|---|
| 280 | | failures = [] |
|---|
| 281 | | self.each do |route| |
|---|
| 282 | | controller, options = route.recognize(path) |
|---|
| 283 | | if controller.nil? |
|---|
| 284 | | failures << [route, options] if ActionController::Base.debug_routes |
|---|
| 285 | | else |
|---|
| 286 | | request.path_parameters = options |
|---|
| 287 | | return controller |
|---|
| 288 | | end |
|---|
| 289 | | end |
|---|
| 290 | | |
|---|
| 291 | | raise RoutingError.new("No route for path: #{path.join('/').inspect}", failures) |
|---|
| 292 | | end |
|---|
| 293 | | |
|---|
| 294 | | def expand_controller_path!(options, defaults) |
|---|
| 295 | | if options[:controller] |
|---|
| 296 | | if /^\// =~ options[:controller] |
|---|
| 297 | | options[:controller] = options[:controller][1..-1] |
|---|
| 298 | | defaults.clear # Sending to absolute controller implies fresh defaults |
|---|
| 299 | | else |
|---|
| 300 | | relative_to = defaults[:controller] ? defaults[:controller].split('/')[0..-2].join('/') : '' |
|---|
| 301 | | options[:controller] = relative_to.empty? ? options[:controller] : "#{relative_to}/#{options[:controller]}" |
|---|
| 302 | | defaults.delete(:action) if options.key?(:controller) |
|---|
| 303 | | end |
|---|
| 304 | | else |
|---|
| 305 | | options[:controller] = defaults[:controller] |
|---|
| 306 | | end |
|---|
| 307 | | end |
|---|
| 308 | | |
|---|
| 309 | | def route(*args) |
|---|
| 310 | | add_route(Route.new(*args)) |
|---|
| 311 | | end |
|---|
| 312 | | alias :connect :route |
|---|
| 313 | | |
|---|
| | 461 | g.source.lines.last << ' )' # Add the closing brace to the end line |
|---|
| | 462 | g.if 'new_path' do |
|---|
| | 463 | g << 'return new_path, [] if new_unused_count.zero?' |
|---|
| | 464 | g << 'path = new_path; unused_keys = new_unused_keys; unused_count = new_unused_count' |
|---|
| | 465 | end |
|---|
| | 466 | end |
|---|
| | 467 | |
|---|
| | 468 | g << "raise RoutingError, \"No url can be generated for the hash \#{options.inspect}\" unless path" |
|---|
| | 469 | g << "return path, unused_keys" |
|---|
| | 470 | end |
|---|
| | 471 | |
|---|
| | 472 | return g |
|---|
| | 473 | end |
|---|
| | 474 | |
|---|
| | 475 | def categorize_routes |
|---|
| | 476 | @categorized_routes = by_controller = Hash.new(self) |
|---|
| | 477 | |
|---|
| | 478 | known_controllers.each do |name| |
|---|
| | 479 | set = by_controller[name] = [] |
|---|
| | 480 | each do |route| |
|---|
| | 481 | set << route if route.matches_controller? name |
|---|
| | 482 | end |
|---|
| | 483 | end |
|---|
| | 484 | |
|---|
| | 485 | @categorized_routes |
|---|
| | 486 | end |
|---|
| | 487 | |
|---|
| | 488 | def known_controllers |
|---|
| | 489 | @routes.inject([]) do |known, route| |
|---|
| | 490 | if (controller = route.known[:controller]) |
|---|
| | 491 | if controller.is_a?(Regexp) |
|---|
| | 492 | known << controller.source.scan(%r{[\w\d/]+}).select {|word| controller =~ word} |
|---|
| | 493 | else known << controller |
|---|
| | 494 | end |
|---|
| | 495 | end |
|---|
| | 496 | known |
|---|
| | 497 | end.uniq |
|---|
| | 498 | end |
|---|
| | 499 | |
|---|