| 173 | | "hash[:#{key}] == #{req.inspect}" |
|---|
| 174 | | end |
|---|
| 175 | | end |
|---|
| 176 | | requirement_conditions * ' && ' unless requirement_conditions.empty? |
|---|
| 177 | | end |
|---|
| 178 | | def generation_structure |
|---|
| 179 | | segments.last.string_structure segments[0..-2] |
|---|
| 180 | | end |
|---|
| 181 | | |
|---|
| 182 | | # Write and compile a +recognize+ method for this Route. |
|---|
| 183 | | def write_recognition |
|---|
| 184 | | # Create an if structure to extract the params from a match if it occurs. |
|---|
| 185 | | body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams" |
|---|
| 186 | | body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend" |
|---|
| 187 | | |
|---|
| 188 | | # Build the method declaration and compile it |
|---|
| 189 | | method_decl = "def recognize(path, env={})\n#{body}\nend" |
|---|
| 190 | | instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" |
|---|
| 191 | | method_decl |
|---|
| 192 | | end |
|---|
| 193 | | |
|---|
| 194 | | # Plugins may override this method to add other conditions, like checks on |
|---|
| 195 | | # host, subdomain, and so forth. Note that changes here only affect route |
|---|
| 196 | | # recognition, not generation. |
|---|
| 197 | | def recognition_conditions |
|---|
| 198 | | result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"] |
|---|
| 199 | | result << "conditions[:method] === env[:method]" if conditions[:method] |
|---|
| 200 | | result |
|---|
| 201 | | end |
|---|
| 202 | | |
|---|
| 203 | | # Build the regular expression pattern that will match this route. |
|---|
| 204 | | def recognition_pattern(wrap = true) |
|---|
| 205 | | pattern = '' |
|---|
| 206 | | segments.reverse_each do |segment| |
|---|
| 207 | | pattern = segment.build_pattern pattern |
|---|
| 208 | | end |
|---|
| 209 | | wrap ? ("\\A" + pattern + "\\Z") : pattern |
|---|
| 210 | | end |
|---|
| 211 | | |
|---|
| 212 | | # Write the code to extract the parameters from a matched route. |
|---|
| 213 | | def recognition_extraction |
|---|
| 214 | | next_capture = 1 |
|---|
| 215 | | extraction = segments.collect do |segment| |
|---|
| 216 | | x = segment.match_extraction next_capture |
|---|
| 217 | | next_capture += Regexp.new(segment.regexp_chunk).number_of_captures |
|---|
| 218 | | x |
|---|
| 219 | | end |
|---|
| 220 | | extraction.compact |
|---|
| 221 | | end |
|---|
| 222 | | |
|---|
| 223 | | # Write the real generation implementation and then resend the message. |
|---|
| 224 | | def generate(options, hash, expire_on = {}) |
|---|
| 225 | | write_generation |
|---|
| 226 | | generate options, hash, expire_on |
|---|
| 227 | | end |
|---|
| 228 | | |
|---|
| 229 | | def generate_extras(options, hash, expire_on = {}) |
|---|
| 230 | | write_generation |
|---|
| 231 | | generate_extras options, hash, expire_on |
|---|
| 232 | | end |
|---|
| 233 | | |
|---|
| 234 | | # Generate the query string with any extra keys in the hash and append |
|---|
| 235 | | # it to the given path, returning the new path. |
|---|
| 236 | | def append_query_string(path, hash, query_keys=nil) |
|---|
| 237 | | return nil unless path |
|---|
| 238 | | query_keys ||= extra_keys(hash) |
|---|
| 239 | | "#{path}#{build_query_string(hash, query_keys)}" |
|---|
| 240 | | end |
|---|
| 241 | | |
|---|
| 242 | | # Determine which keys in the given hash are "extra". Extra keys are |
|---|
| 243 | | # those that were not used to generate a particular route. The extra |
|---|
| 244 | | # keys also do not include those recalled from the prior request, nor |
|---|
| 245 | | # do they include any keys that were implied in the route (like a |
|---|
| 246 | | # :controller that is required, but not explicitly used in the text of |
|---|
| 247 | | # the route.) |
|---|
| 248 | | def extra_keys(hash, recall={}) |
|---|
| 249 | | (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys |
|---|
| 250 | | end |
|---|
| 251 | | |
|---|
| 252 | | # Build a query string from the keys of the given hash. If +only_keys+ |
|---|
| 253 | | # is given (as an array), only the keys indicated will be used to build |
|---|
| 254 | | # the query string. The query string will correctly build array parameter |
|---|
| 255 | | # values. |
|---|
| 256 | | def build_query_string(hash, only_keys=nil) |
|---|
| 257 | | elements = [] |
|---|
| 258 | | |
|---|
| 259 | | only_keys ||= hash.keys |
|---|
| 260 | | |
|---|
| 261 | | only_keys.each do |key| |
|---|
| 262 | | value = hash[key] or next |
|---|
| 263 | | key = CGI.escape key.to_s |
|---|
| 264 | | if value.class == Array |
|---|
| 265 | | key << '[]' |
|---|
| 266 | | else |
|---|
| 267 | | value = [ value ] |
|---|
| 268 | | end |
|---|
| 269 | | value.each { |val| elements << "#{key}=#{CGI.escape(val.to_param.to_s)}" } |
|---|
| 270 | | end |
|---|
| 271 | | |
|---|
| 272 | | query_string = "?#{elements.join("&")}" unless elements.empty? |
|---|
| 273 | | query_string || "" |
|---|
| 274 | | end |
|---|
| 275 | | |
|---|
| 276 | | # Write the real recognition implementation and then resend the message. |
|---|
| 277 | | def recognize(path, environment={}) |
|---|
| 278 | | write_recognition |
|---|
| 279 | | recognize path, environment |
|---|
| 280 | | end |
|---|
| 281 | | |
|---|
| 282 | | # A route's parameter shell contains parameter values that are not in the |
|---|
| 283 | | # route's path, but should be placed in the recognized hash. |
|---|
| 284 | | # |
|---|
| 285 | | # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route: |
|---|
| 286 | | # |
|---|
| 287 | | # map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/ |
|---|
| 288 | | # |
|---|
| 289 | | def parameter_shell |
|---|
| 290 | | @parameter_shell ||= returning({}) do |shell| |
|---|
| 291 | | requirements.each do |key, requirement| |
|---|
| 292 | | shell[key] = requirement unless requirement.is_a? Regexp |
|---|
| 293 | | end |
|---|
| 294 | | end |
|---|
| 295 | | end |
|---|
| 296 | | |
|---|
| 297 | | # Return an array containing all the keys that are used in this route. This |
|---|
| 298 | | # includes keys that appear inside the path, and keys that have requirements |
|---|
| 299 | | # placed upon them. |
|---|
| 300 | | def significant_keys |
|---|
| 301 | | @significant_keys ||= returning [] do |sk| |
|---|
| 302 | | segments.each { |segment| sk << segment.key if segment.respond_to? :key } |
|---|
| 303 | | sk.concat requirements.keys |
|---|
| 304 | | sk.uniq! |
|---|
| 305 | | end |
|---|
| 306 | | end |
|---|
| 307 | | |
|---|
| 308 | | # Return a hash of key/value pairs representing the keys in the route that |
|---|
| 309 | | # have defaults, or which are specified by non-regexp requirements. |
|---|
| 310 | | def defaults |
|---|
| 311 | | @defaults ||= returning({}) do |hash| |
|---|
| 312 | | segments.each do |segment| |
|---|
| 313 | | next unless segment.respond_to? :default |
|---|
| 314 | | hash[segment.key] = segment.default unless segment.default.nil? |
|---|
| 315 | | end |
|---|
| 316 | | requirements.each do |key,req| |
|---|
| 317 | | next if Regexp === req || req.nil? |
|---|
| 318 | | hash[key] = req |
|---|
| 319 | | end |
|---|
| 320 | | end |
|---|
| 321 | | end |
|---|
| 322 | | |
|---|
| 323 | | def matches_controller_and_action?(controller, action) |
|---|
| 324 | | unless @matching_prepared |
|---|
| 325 | | @controller_requirement = requirement_for(:controller) |
|---|
| 326 | | @action_requirement = requirement_for(:action) |
|---|
| 327 | | @matching_prepared = true |
|---|
| 328 | | end |
|---|
| 329 | | |
|---|
| 330 | | (@controller_requirement.nil? || @controller_requirement === controller) && |
|---|
| 331 | | (@action_requirement.nil? || @action_requirement === action) |
|---|
| 332 | | end |
|---|
| 333 | | |
|---|
| 334 | | def to_s |
|---|
| 335 | | @to_s ||= begin |
|---|
| 336 | | segs = segments.inject("") { |str,s| str << s.to_s } |
|---|
| 337 | | "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect] |
|---|
| 338 | | end |
|---|
| 339 | | end |
|---|
| 340 | | |
|---|
| 341 | | protected |
|---|
| 342 | | def requirement_for(key) |
|---|
| 343 | | return requirements[key] if requirements.key? key |
|---|
| 344 | | segments.each do |segment| |
|---|
| 345 | | return segment.regexp if segment.respond_to?(:key) && segment.key == key |
|---|
| 346 | | end |
|---|
| 347 | | nil |
|---|
| 348 | | end |
|---|
| 349 | | |
|---|
| 350 | | end |
|---|
| 351 | | |
|---|
| 352 | | class Segment |
|---|
| 353 | | attr_accessor :is_optional |
|---|
| 354 | | alias_method :optional?, :is_optional |
|---|
| 355 | | |
|---|
| 356 | | def initialize |
|---|
| 357 | | self.is_optional = false |
|---|
| 358 | | end |
|---|
| 359 | | |
|---|
| 360 | | def extraction_code |
|---|
| 361 | | nil |
|---|
| 362 | | end |
|---|
| 363 | | |
|---|
| 364 | | # Continue generating string for the prior segments. |
|---|
| 365 | | def continue_string_structure(prior_segments) |
|---|
| 366 | | if prior_segments.empty? |
|---|
| 367 | | interpolation_statement(prior_segments) |
|---|
| | 27 | hash.delete k |
|---|
| | 28 | keys_to_delete << k |
|---|
| | 29 | end |
|---|
| | 30 | end |
|---|
| | 31 | hash |
|---|
| | 32 | end |
|---|
| | 33 | |
|---|
| | 34 | def test_condition(expression, condition) |
|---|
| | 35 | case condition |
|---|
| | 36 | when String then "(#{expression} == #{condition.inspect})" |
|---|
| | 37 | when Regexp then |
|---|
| | 38 | condition = Regexp.new("^#{condition.source}$") unless /^\^.*\$$/ =~ condition.source |
|---|
| | 39 | "(#{condition.inspect} =~ #{expression})" |
|---|
| | 40 | when Array then |
|---|
| | 41 | conds = condition.collect do |condition| |
|---|
| | 42 | cond = test_condition(expression, condition) |
|---|
| | 43 | (cond[0, 1] == '(' && cond[-1, 1] == ')') ? cond : "(#{cond})" |
|---|
| | 44 | end |
|---|
| | 45 | "(#{conds.join(' || ')})" |
|---|
| | 46 | when true then expression |
|---|
| | 47 | when nil then "! #{expression}" |
|---|
| | 48 | else |
|---|
| | 49 | raise ArgumentError, "Valid criteria are strings, regular expressions, true, or nil" |
|---|
| | 50 | end |
|---|
| | 51 | end |
|---|
| | 52 | end |
|---|
| | 53 | |
|---|
| | 54 | class Component #:nodoc: |
|---|
| | 55 | def dynamic?() false end |
|---|
| | 56 | def optional?() false end |
|---|
| | 57 | |
|---|
| | 58 | def key() nil end |
|---|
| | 59 | |
|---|
| | 60 | def self.new(string, *args) |
|---|
| | 61 | return super(string, *args) unless self == Component |
|---|
| | 62 | case string |
|---|
| | 63 | when ':controller' then ControllerComponent.new(:controller, *args) |
|---|
| | 64 | when /^:(\w+)$/ then DynamicComponent.new($1, *args) |
|---|
| | 65 | when /^\*(\w+)$/ then PathComponent.new($1, *args) |
|---|
| | 66 | else StaticComponent.new(string, *args) |
|---|
| | 67 | end |
|---|
| | 68 | end |
|---|
| | 69 | end |
|---|
| | 70 | |
|---|
| | 71 | class StaticComponent < Component #:nodoc: |
|---|
| | 72 | attr_reader :value |
|---|
| | 73 | |
|---|
| | 74 | def initialize(value) |
|---|
| | 75 | @value = value |
|---|
| | 76 | end |
|---|
| | 77 | |
|---|
| | 78 | def write_recognition(g) |
|---|
| | 79 | g.if_next_matches(value) do |gp| |
|---|
| | 80 | gp.move_forward {|gpp| gpp.continue} |
|---|
| | 81 | end |
|---|
| | 82 | end |
|---|
| | 83 | |
|---|
| | 84 | def write_generation(g) |
|---|
| | 85 | g.add_segment(value) {|gp| gp.continue } |
|---|
| | 86 | end |
|---|
| | 87 | end |
|---|
| | 88 | |
|---|
| | 89 | class DynamicComponent < Component #:nodoc: |
|---|
| | 90 | attr_reader :key, :default |
|---|
| | 91 | attr_accessor :condition |
|---|
| | 92 | |
|---|
| | 93 | def dynamic?() true end |
|---|
| | 94 | def optional?() @optional end |
|---|
| | 95 | |
|---|
| | 96 | def default=(default) |
|---|
| | 97 | @optional = true |
|---|
| | 98 | @default = default |
|---|
| | 99 | end |
|---|
| | 100 | |
|---|
| | 101 | def initialize(key, options = {}) |
|---|
| | 102 | @key = key.to_sym |
|---|
| | 103 | @optional = false |
|---|
| | 104 | default, @condition = options[:default], options[:condition] |
|---|
| | 105 | self.default = default if options.key?(:default) |
|---|
| | 106 | end |
|---|
| | 107 | |
|---|
| | 108 | def default_check(g) |
|---|
| | 109 | presence = "#{g.hash_value(key, !! default)}" |
|---|
| | 110 | if default |
|---|
| | 111 | "!(#{presence} && #{g.hash_value(key, false)} != #{default.to_s.inspect})" |
|---|
| 369 | | new_priors = prior_segments[0..-2] |
|---|
| 370 | | prior_segments.last.string_structure(new_priors) |
|---|
| 371 | | end |
|---|
| 372 | | end |
|---|
| 373 | | |
|---|
| 374 | | # Return a string interpolation statement for this segment and those before it. |
|---|
| 375 | | def interpolation_statement(prior_segments) |
|---|
| 376 | | chunks = prior_segments.collect { |s| s.interpolation_chunk } |
|---|
| 377 | | chunks << interpolation_chunk |
|---|
| 378 | | "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}" |
|---|
| 379 | | end |
|---|
| 380 | | |
|---|
| 381 | | def string_structure(prior_segments) |
|---|
| 382 | | optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments) |
|---|
| 383 | | end |
|---|
| 384 | | |
|---|
| 385 | | # Return an if condition that is true if all the prior segments can be generated. |
|---|
| 386 | | # If there are no optional segments before this one, then nil is returned. |
|---|
| 387 | | def all_optionals_available_condition(prior_segments) |
|---|
| 388 | | optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact |
|---|
| 389 | | optional_locals.empty? ? nil : " if #{optional_locals * ' && '}" |
|---|
| 390 | | end |
|---|
| 391 | | |
|---|
| 392 | | # Recognition |
|---|
| 393 | | |
|---|
| 394 | | def match_extraction(next_capture) |
|---|
| 395 | | nil |
|---|
| 396 | | end |
|---|
| 397 | | |
|---|
| 398 | | # Warning |
|---|
| 399 | | |
|---|
| 400 | | # Returns true if this segment is optional? because of a default. If so, then |
|---|
| 401 | | # no warning will be emitted regarding this segment. |
|---|
| 402 | | def optionality_implied? |
|---|
| 403 | | false |
|---|
| 404 | | end |
|---|
| 405 | | end |
|---|
| 406 | | |
|---|
| 407 | | class StaticSegment < Segment |
|---|
| 408 | | attr_accessor :value, :raw |
|---|
| 409 | | alias_method :raw?, :raw |
|---|
| 410 | | |
|---|
| 411 | | def initialize(value = nil) |
|---|
| 412 | | super() |
|---|
| 413 | | self.value = value |
|---|
| 414 | | end |
|---|
| 415 | | |
|---|
| 416 | | def interpolation_chunk |
|---|
| 417 | | raw? ? value : CGI.escape(value) |
|---|
| 418 | | end |
|---|
| 419 | | |
|---|
| 420 | | def regexp_chunk |
|---|
| 421 | | chunk = Regexp.escape value |
|---|
| 422 | | optional? ? Regexp.optionalize(chunk) : chunk |
|---|
| 423 | | end |
|---|
| 424 | | |
|---|
| 425 | | def build_pattern(pattern) |
|---|
| 426 | | escaped = Regexp.escape(value) |
|---|
| 427 | | if optional? && ! pattern.empty? |
|---|
| 428 | | "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})" |
|---|
| 429 | | elsif optional? |
|---|
| 430 | | Regexp.optionalize escaped |
|---|
| 431 | | else |
|---|
| 432 | | escaped + pattern |
|---|
| 433 | | end |
|---|
| 434 | | end |
|---|
| 435 | | |
|---|
| 436 | | def to_s |
|---|
| 437 | | value |
|---|
| 438 | | end |
|---|
| 439 | | end |
|---|
| 440 | | |
|---|
| 441 | | class DividerSegment < StaticSegment |
|---|
| 442 | | def initialize(value = nil) |
|---|
| 443 | | super(value) |
|---|
| 444 | | self.raw = true |
|---|
| 445 | | self.is_optional = true |
|---|
| 446 | | end |
|---|
| 447 | | |
|---|
| 448 | | def optionality_implied? |
|---|
| 449 | | true |
|---|
| 450 | | end |
|---|
| 451 | | end |
|---|
| 452 | | |
|---|
| 453 | | class DynamicSegment < Segment |
|---|
| 454 | | attr_accessor :key, :default, :regexp |
|---|
| 455 | | |
|---|
| 456 | | def initialize(key = nil, options = {}) |
|---|
| 457 | | super() |
|---|
| 458 | | self.key = key |
|---|
| 459 | | self.default = options[:default] if options.key? :default |
|---|
| 460 | | self.is_optional = true if options[:optional] || options.key?(:default) |
|---|
| 461 | | end |
|---|
| 462 | | |
|---|
| 463 | | def to_s |
|---|
| 464 | | ":#{key}" |
|---|
| 465 | | end |
|---|
| 466 | | |
|---|
| 467 | | # The local variable name that the value of this segment will be extracted to. |
|---|
| 468 | | def local_name |
|---|
| 469 | | "#{key}_value" |
|---|
| 470 | | end |
|---|
| 471 | | |
|---|
| 472 | | def extract_value |
|---|
| 473 | | "#{local_name} = hash[:#{key}] #{"|| #{default.inspect}" if default}" |
|---|
| 474 | | end |
|---|
| 475 | | def value_check |
|---|
| 476 | | if default # Then we know it won't be nil |
|---|
| 477 | | "#{value_regexp.inspect} =~ #{local_name}" if regexp |
|---|
| 478 | | elsif optional? |
|---|
| 479 | | # If we have a regexp check that the value is not given, or that it matches. |
|---|
| 480 | | # If we have no regexp, return nil since we do not require a condition. |
|---|
| 481 | | "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp |
|---|
| 482 | | else # Then it must be present, and if we have a regexp, it must match too. |
|---|
| 483 | | "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}" |
|---|
| 484 | | end |
|---|
| 485 | | end |
|---|
| 486 | | def expiry_statement |
|---|
| 487 | | "not_expired, hash = false, options if not_expired && expire_on[:#{key}]" |
|---|
| 488 | | end |
|---|
| 489 | | |
|---|
| 490 | | def extraction_code |
|---|
| 491 | | s = extract_value |
|---|
| 492 | | vc = value_check |
|---|
| 493 | | s << "\nreturn [nil,nil] unless #{vc}" if vc |
|---|
| 494 | | s << "\n#{expiry_statement}" |
|---|
| 495 | | end |
|---|
| 496 | | |
|---|
| 497 | | def interpolation_chunk |
|---|
| 498 | | "\#{CGI.escape(#{local_name}.to_s)}" |
|---|
| 499 | | end |
|---|
| 500 | | |
|---|
| 501 | | def string_structure(prior_segments) |
|---|
| 502 | | if optional? # We have a conditional to do... |
|---|
| 503 | | # If we should not appear in the url, just write the code for the prior |
|---|
| 504 | | # segments. This occurs if our value is the default value, or, if we are |
|---|
| 505 | | # optional, if we have nil as our value. |
|---|
| 506 | | "if #{local_name} == #{default.inspect}\n" + |
|---|
| 507 | | continue_string_structure(prior_segments) + |
|---|
| 508 | | "\nelse\n" + # Otherwise, write the code up to here |
|---|
| 509 | | "#{interpolation_statement(prior_segments)}\nend" |
|---|
| 510 | | else |
|---|
| 511 | | interpolation_statement(prior_segments) |
|---|
| 512 | | end |
|---|
| 513 | | end |
|---|
| 514 | | |
|---|
| 515 | | def value_regexp |
|---|
| 516 | | Regexp.new "\\A#{regexp.source}\\Z" if regexp |
|---|
| 517 | | end |
|---|
| 518 | | def regexp_chunk |
|---|
| 519 | | regexp ? "(#{regexp.source})" : "([^#{Routing::SEPARATORS.join}]+)" |
|---|
| 520 | | end |
|---|
| 521 | | |
|---|
| 522 | | def build_pattern(pattern) |
|---|
| 523 | | chunk = regexp_chunk |
|---|
| 524 | | chunk = "(#{chunk})" if Regexp.new(chunk).number_of_captures == 0 |
|---|
| 525 | | pattern = "#{chunk}#{pattern}" |
|---|
| 526 | | optional? ? Regexp.optionalize(pattern) : pattern |
|---|
| 527 | | end |
|---|
| 528 | | def match_extraction(next_capture) |
|---|
| 529 | | hangon = (default ? "|| #{default.inspect}" : "if match[#{next_capture}]") |
|---|
| 530 | | |
|---|
| 531 | | # All non code-related keys (such as :id, :slug) have to be unescaped as other CGI params |
|---|
| 532 | | "params[:#{key}] = match[#{next_capture}] #{hangon}" |
|---|
| 533 | | end |
|---|
| 534 | | |
|---|
| 535 | | def optionality_implied? |
|---|
| 536 | | [:action, :id].include? key |
|---|
| 537 | | end |
|---|
| 538 | | |
|---|
| 539 | | end |
|---|
| 540 | | |
|---|
| 541 | | class ControllerSegment < DynamicSegment |
|---|
| 542 | | def regexp_chunk |
|---|
| 543 | | possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name } |
|---|
| 544 | | "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))" |
|---|
| 545 | | end |
|---|
| 546 | | |
|---|
| 547 | | # Don't CGI.escape the controller name, since it may have slashes in it, |
|---|
| 548 | | # like admin/foo. |
|---|
| 549 | | def interpolation_chunk |
|---|
| 550 | | "\#{#{local_name}.to_s}" |
|---|
| 551 | | end |
|---|
| 552 | | |
|---|
| 553 | | # Make sure controller names like Admin/Content are correctly normalized to |
|---|
| 554 | | # admin/content |
|---|
| 555 | | def extract_value |
|---|
| 556 | | "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase" |
|---|
| 557 | | end |
|---|
| 558 | | |
|---|
| 559 | | def match_extraction(next_capture) |
|---|
| 560 | | hangon = (default ? "|| #{default.inspect}" : "if match[#{next_capture}]") |
|---|
| 561 | | "params[:#{key}] = match[#{next_capture}].downcase #{hangon}" |
|---|
| 562 | | end |
|---|
| 563 | | end |
|---|
| 564 | | |
|---|
| 565 | | class PathSegment < DynamicSegment |
|---|
| 566 | | EscapedSlash = CGI.escape("/") |
|---|
| 567 | | def interpolation_chunk |
|---|
| 568 | | "\#{CGI.escape(#{local_name}.to_s).gsub(#{EscapedSlash.inspect}, '/')}" |
|---|
| 569 | | end |
|---|
| 570 | | |
|---|
| 571 | | def default |
|---|
| 572 | | '' |
|---|
| 573 | | end |
|---|
| 574 | | |
|---|
| 575 | | def default=(path) |
|---|
| 576 | | raise RoutingError, "paths cannot have non-empty default values" unless path.blank? |
|---|
| 577 | | end |
|---|
| 578 | | |
|---|
| 579 | | def match_extraction(next_capture) |
|---|
| 580 | | "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}" |
|---|
| 581 | | end |
|---|
| 582 | | |
|---|
| 583 | | def regexp_chunk |
|---|
| 584 | | regexp || "(.*)" |
|---|
| 585 | | end |
|---|
| 586 | | |
|---|
| | 113 | "! #{presence}" |
|---|
| | 114 | end |
|---|
| | 115 | end |
|---|
| | 116 | |
|---|
| | 117 | def write_generation(g) |
|---|
| | 118 | wrote_dropout = write_dropout_generation(g) |
|---|
| | 119 | write_continue_generation(g, wrote_dropout) |
|---|
| | 120 | end |
|---|
| | 121 | |
|---|
| | 122 | def write_dropout_generation(g) |
|---|
| | 123 | return false unless optional? && g.after.all? {|c| c.optional?} |
|---|
| | 124 | |
|---|
| | 125 | check = [default_check(g)] |
|---|
| | 126 | gp = g.dup # Use another generator to write the conditions after the first && |
|---|
| | 127 | # We do this to ensure that the generator will not assume x_value is set. It will |
|---|
| | 128 | # not be set if it follows a false condition -- for example, false && (x = 2) |
|---|
| | 129 | |
|---|
| | 130 | check += gp.after.map {|c| c.default_check gp} |
|---|
| | 131 | gp.if(check.join(' && ')) { gp.finish } # If this condition is met, we stop here |
|---|
| | 132 | true |
|---|
| | 133 | end |
|---|
| | 134 | |
|---|
| | 135 | def write_continue_generation(g, use_else) |
|---|
| | 136 | test = Routing.test_condition(g.hash_value(key, true, default), condition || true) |
|---|
| | 137 | check = (use_else && condition.nil? && default) ? [:else] : [use_else ? :elsif : :if, test] |
|---|
| | 138 | |
|---|
| | 139 | g.send(*check) do |gp| |
|---|
| | 140 | gp.expire_for_keys(key) unless gp.after.empty? |
|---|
| | 141 | add_segments_to(gp) {|gpp| gpp.continue} |
|---|
| | 142 | end |
|---|
| | 143 | end |
|---|
| | 144 | |
|---|
| | 145 | def add_segments_to(g) |
|---|
| | 146 | g.add_segment(%(\#{CGI.escape(#{g.hash_value(key, true, default)})})) {|gp| yield gp} |
|---|
| | 147 | end |
|---|
| | 148 | |
|---|
| | 149 | def recognition_check(g) |
|---|
| | 150 | test_type = [true, nil].include?(condition) ? :presence : :constraint |
|---|
| | 151 | |
|---|
| | 152 | prefix = condition.is_a?(Regexp) ? "#{g.next_segment(true)} && " : '' |
|---|
| | 153 | check = prefix + Routing.test_condition(g.next_segment(true), condition || true) |
|---|
| | 154 | |
|---|
| | 155 | g.if(check) {|gp| yield gp, test_type} |
|---|
| | 156 | end |
|---|
| | 157 | |
|---|
| | 158 | def write_recognition(g) |
|---|
| | 159 | test_type = nil |
|---|
| | 160 | recognition_check(g) do |gp, test_type| |
|---|
| | 161 | assign_result(gp) {|gpp| gpp.continue} |
|---|
| | 162 | end |
|---|
| | 163 | |
|---|
| | 164 | if optional? && g.after.all? {|c| c.optional?} |
|---|
| | 165 | call = (test_type == :presence) ? [:else] : [:elsif, "! #{g.next_segment(true)}"] |
|---|
| | 166 | |
|---|
| | 167 | g.send(*call) do |gp| |
|---|
| | 168 | assign_default(gp) |
|---|
| | 169 | gp.after.each {|c| c.assign_default(gp)} |
|---|
| | 170 | gp.finish(false) |
|---|
| | 171 | end |
|---|
| | 172 | end |
|---|
| | 173 | end |
|---|
| | 174 | |
|---|
| | 175 | def assign_result(g, with_default = false) |
|---|
| | 176 | g.result key, "CGI.unescape(#{g.next_segment(true, with_default ? default : nil)})" |
|---|
| | 177 | g.move_forward {|gp| yield gp} |
|---|
| | 178 | end |
|---|
| | 179 | |
|---|
| | 180 | def assign_default(g) |
|---|
| | 181 | g.constant_result key, default unless default.nil? |
|---|
| | 182 | end |
|---|
| | 183 | end |
|---|
| | 184 | |
|---|
| | 185 | class ControllerComponent < DynamicComponent #:nodoc: |
|---|
| | 186 | def key() :controller end |
|---|
| | 187 | |
|---|
| | 188 | def add_segments_to(g) |
|---|
| | 189 | g.add_segment(%(\#{#{g.hash_value(key, true, default)}})) {|gp| yield gp} |
|---|
| | 190 | end |
|---|
| | 191 | |
|---|
| | 192 | def recognition_check(g) |
|---|
| | 193 | g << "controller_result = ::ActionController::Routing::ControllerComponent.traverse_to_controller(#{g.path_name}, #{g.index_name})" |
|---|
| | 194 | g.if('controller_result') do |gp| |
|---|
| | 195 | gp << 'controller_value, segments_to_controller = controller_result' |
|---|
| | 196 | if condition |
|---|
| | 197 | gp << "controller_path = #{gp.path_name}[#{gp.index_name},segments_to_controller].join('/')" |
|---|
| | 198 | gp.if(Routing.test_condition("controller_path", condition)) do |gpp| |
|---|
| | 199 | gpp.move_forward('segments_to_controller') {|gppp| yield gppp, :constraint} |
|---|
| | 200 | end |
|---|
| | 201 | else |
|---|
| | 202 | gp.move_forward('segments_to_controller') {|gpp| yield gpp, :constraint} |
|---|
| | 203 | end |
|---|
| | 204 | end |
|---|
| | 205 | end |
|---|
| | 206 | |
|---|
| | 207 | def assign_result(g) |
|---|
| | 208 | g.result key, 'controller_value' |
|---|
| | 209 | yield g |
|---|
| | 210 | end |
|---|
| | 211 | |
|---|
| | 212 | def assign_default(g) |
|---|
| | 213 | ControllerComponent.assign_controller(g, default) |
|---|
| | 214 | end |
|---|
| | 215 | |
|---|
| | 216 | class << self |
|---|
| | 217 | def assign_controller(g, controller) |
|---|
| | 218 | expr = "::#{controller.split('/').collect {|c| c.camelize}.join('::')}Controller" |
|---|
| | 219 | g.result :controller, expr, true |
|---|
| | 220 | end |
|---|
| | 221 | |
|---|
| | 222 | def file_kinds(kind) |
|---|
| | 223 | ((@file_kinds ||= []) << kind).uniq! || @file_kinds |
|---|
| | 224 | end |
|---|
| | 225 | |
|---|
| | 226 | def traverse_to_controller(segments, start_at = 0) |
|---|
| | 227 | mod = ::Object |
|---|
| | 228 | length = segments.length |
|---|
| | 229 | index = start_at |
|---|
| | 230 | mod_name = controller_name = segment = nil |
|---|
| | 231 | while index < length |
|---|
| | 232 | return nil unless /\A[A-Za-z][A-Za-z\d_]*\Z/ =~ (segment = segments[index]) |
|---|
| | 233 | index += 1 |
|---|
| | 234 | |
|---|
| | 235 | file_kinds :app |
|---|
| | 236 | mod_name = segment.camelize |
|---|
| | 237 | controller_name = "#{mod_name}Controller" |
|---|
| | 238 | path_suffix = File.join(segments[start_at..(index - 1)]) |
|---|
| | 239 | next_mod = nil |
|---|
| | 240 | |
|---|
| | 241 | # If the controller is already present, or if we load it, return it. |
|---|
| | 242 | if mod.const_defined?(controller_name) || attempt_load(mod, controller_name, path_suffix + "_controller") == :defined |
|---|
| | 243 | controller = mod.const_get(controller_name) |
|---|
| | 244 | return nil unless controller.is_a?(Class) && controller.ancestors.include?(ActionController::Base) # it's not really a controller? |
|---|
| | 245 | return [controller, (index - start_at)] |
|---|
| | 246 | end |
|---|
| | 247 | |
|---|
| | 248 | # No controller? Look for the module |
|---|
| | 249 | if mod.const_defined? mod_name |
|---|
| | 250 | next_mod = mod.send(:const_get, mod_name) |
|---|
| | 251 | next_mod = nil unless next_mod.is_a?(Module) |
|---|
| | 252 | else |
|---|
| | 253 | # Try to load a file that defines the module we want. |
|---|
| | 254 | case attempt_load(mod, mod_name, path_suffix) |
|---|
| | 255 | when :defined then next_mod = mod.const_get mod_name |
|---|
| | 256 | when :dir then # We didn't find a file, but there's a dir. |
|---|
| | 257 | next_mod = Module.new # So create a module for the directory |
|---|
| | 258 | mod.send :const_set, mod_name, next_mod |
|---|
| | 259 | else |
|---|
| | 260 | return nil |
|---|
| | 261 | end |
|---|
| | 262 | end |
|---|
| | 263 | mod = next_mod |
|---|
| | 264 | |
|---|
| | 265 | return nil unless mod && mod.is_a?(Module) |
|---|
| | 266 | end |
|---|
| | 267 | nil |
|---|
| | 268 | end |
|---|
| | 269 | |
|---|
| | 270 | protected |
|---|
| | 271 | def safe_load_paths #:nodoc: |
|---|
| | 272 | if defined?(RAILS_ROOT) |
|---|
| | 273 | $LOAD_PATH.select do |base| |
|---|
| | 274 | base = File.expand_path(base) |
|---|
| | 275 | extended_root = File.expand_path(RAILS_ROOT) |
|---|
| | 276 | base.match(/\A#{Regexp.escape(extended_root)}\/*#{file_kinds(:lib) * '|'}/) || base =~ %r{rails-[\d.]+/builtin} |
|---|
| | 277 | end |
|---|
| | 278 | else |
|---|
| | 279 | $LOAD_PATH |
|---|
| | 280 | end |
|---|
| | 281 | end |
|---|
| | 282 | |
|---|
| | 283 | def attempt_load(mod, const_name, path) |
|---|
| | 284 | has_dir = false |
|---|
| | 285 | safe_load_paths.each do |load_path| |
|---|
| | 286 | full_path = File.join(load_path, path) |
|---|
| | 287 | file_path = full_path + '.rb' |
|---|
| | 288 | if File.file?(file_path) # Found a .rb file? Load it up |
|---|
| | 289 | require_dependency(file_path) |
|---|
| | 290 | return :defined if mod.const_defined? const_name |
|---|
| | 291 | else |
|---|
| | 292 | has_dir ||= File.directory?(full_path) |
|---|
| | 293 | end |
|---|
| | 294 | end |
|---|
| | 295 | return (has_dir ? :dir : nil) |
|---|
| | 296 | end |
|---|
| | 297 | end |
|---|
| | 298 | end |
|---|
| | 299 | |
|---|
| | 300 | class PathComponent < DynamicComponent #:nodoc: |
|---|
| | 301 | def optional?() true end |
|---|
| | 302 | def default() [] end |
|---|
| | 303 | def condition() nil end |
|---|
| | 304 | |
|---|
| | 305 | def default=(value) |
|---|
| | 306 | raise RoutingError, "All path components have an implicit default of []" unless value == [] |
|---|
| | 307 | end |
|---|
| | 308 | |
|---|
| | 309 | def write_generation(g) |
|---|
| | 310 | raise RoutingError, 'Path components must occur last' unless g.after.empty? |
|---|
| | 311 | g.if("#{g.hash_value(key, true)} && ! #{g.hash_value(key, true)}.empty?") do |
|---|
| | 312 | g << "#{g.hash_value(key, true)} = #{g.hash_value(key, true)}.join('/') unless #{g.hash_value(key, true)}.is_a?(String)" |
|---|
| | 313 | g.add_segment("\#{CGI.escape_skipping_slashes(#{g.hash_value(key, true)})}") {|gp| gp.finish } |
|---|
| | 314 | end |
|---|
| | 315 | g.else { g.finish } |
|---|
| | 316 | end |
|---|
| | 317 | |
|---|
| | 318 | def write_recognition(g) |
|---|
| | 319 | raise RoutingError, "Path components must occur last" unless g.after.empty? |
|---|
| | 320 | |
|---|
| | 321 | start = g.index_name |
|---|
| | 322 | start = "(#{start})" unless /^\w+$/ =~ start |
|---|
| | 323 | |
|---|
| | 324 | value_expr = "#{g.path_name}[#{start}..-1] || []" |
|---|
| | 325 | g.result key, "ActionController::Routing::PathComponent::Result.new_escaped(#{value_expr})" |
|---|
| | 326 | g.finish(false) |
|---|
| | 327 | end |
|---|
| | 328 | |
|---|
| 591 | | end |
|---|
| 592 | | end |
|---|
| 593 | | end |
|---|
| 594 | | |
|---|
| 595 | | class RouteBuilder |
|---|
| 596 | | attr_accessor :separators, :optional_separators |
|---|
| 597 | | |
|---|
| | 333 | end |
|---|
| | 334 | end |
|---|
| | 335 | end |
|---|
| | 336 | |
|---|
| | 337 | class Route #:nodoc: |
|---|
| | 338 | attr_accessor :components, :known |
|---|
| | 339 | attr_reader :path, :options, :keys, :defaults |
|---|
| | 340 | |
|---|
| | 341 | def initialize(path, options = {}) |
|---|
| | 342 | @path, @options = path, options |
|---|
| | 343 | |
|---|
| | 344 | initialize_components path |
|---|
| | 345 | defaults, conditions = initialize_hashes options.dup |
|---|
| | 346 | @defaults = defaults.dup |
|---|
| | 347 | configure_components(defaults, conditions) |
|---|
| | 348 | add_default_requirements |
|---|
| | 349 | initialize_keys |
|---|
| | 350 | end |
|---|
| | 351 | |
|---|
| | 352 | def inspect |
|---|
| | 353 | "<#{self.class} #{path.inspect}, #{options.inspect[1..-1]}>" |
|---|
| | 354 | end |
|---|
| | 355 | |
|---|
| | 356 | def write_generation(generator = CodeGeneration::GenerationGenerator.new) |
|---|
| | 357 | generator.before, generator.current, generator.after = [], components.first, (components[1..-1] || []) |
|---|
| | 358 | |
|---|
| | 359 | if known.empty? then generator.go |
|---|
| | 360 | else |
|---|
| | 361 | # Alter the conditions to allow :action => 'index' to also catch :action => nil |
|---|
| | 362 | altered_known = known.collect do |k, v| |
|---|
| | 363 | if k == :action && v== 'index' then [k, [nil, 'index']] |
|---|
| | 364 | else [k, v] |
|---|
| | 365 | end |
|---|
| | 366 | end |
|---|
| | 367 | generator.if(generator.check_conditions(altered_known)) {|gp| gp.go } |
|---|
| | 368 | end |
|---|
| | 369 | |
|---|
| | 370 | generator |
|---|
| | 371 | end |
|---|
| | 372 | |
|---|
| | 373 | def write_recognition(generator = CodeGeneration::RecognitionGenerator.new) |
|---|
| | 374 | g = generator.dup |
|---|
| | 375 | g.share_locals_with generator |
|---|
| | 376 | g.before, g.current, g.after = [], components.first, (components[1..-1] || []) |
|---|
| | 377 | |
|---|
| | 378 | known.each do |key, value| |
|---|
| | 379 | if key == :controller then ControllerComponent.assign_controller(g, value) |
|---|
| | 380 | else g.constant_result(key, value) |
|---|
| | 381 | end |
|---|
| | 382 | end |
|---|
| | 383 | |
|---|
| | 384 | g.go |
|---|
| | 385 | |
|---|
| | 386 | generator |
|---|
| | 387 | end |
|---|
| | 388 | |
|---|
| | 389 | def initialize_keys |
|---|
| | 390 | @keys = (components.collect {|c| c.key} + known.keys).compact |
|---|
| | 391 | @keys.freeze |
|---|
| | 392 | end |
|---|
| | 393 | |
|---|
| | 394 | def extra_keys(options) |
|---|
| | 395 | options.keys - @keys |
|---|
| | 396 | end |
|---|
| | 397 | |
|---|
| | 398 | def matches_controller?(controller) |
|---|
| | 399 | if known[:controller] then known[:controller] == controller |
|---|
| | 400 | else |
|---|
| | 401 | c = components.find {|c| c.key == :controller} |
|---|
| | 402 | return false unless c |
|---|
| | 403 | return c.condition.nil? || eval(Routing.test_condition('controller', c.condition)) |
|---|
| | 404 | end |
|---|
| | 405 | end |
|---|
| | 406 | |
|---|
| | 407 | protected |
|---|
| | 408 | def initialize_components(path) |
|---|
| | 409 | path = path.split('/') if path.is_a? String |
|---|
| | 410 | path.shift if path.first.blank? |
|---|
| | 411 | self.components = path.collect {|str| Component.new str} |
|---|
| | 412 | end |
|---|
| | 413 | |
|---|
| | 414 | def initialize_hashes(options) |
|---|
| | 415 | path_keys = components.collect {|c| c.key }.compact |
|---|
| | 416 | self.known = {} |
|---|
| | 417 | defaults = options.delete(:defaults) || {} |
|---|
| | 418 | conditions = options.delete(:require) || {} |
|---|
| | 419 | conditions.update(options.delete(:requirements) || {}) |
|---|
| | 420 | |
|---|
| | 421 | options.each do |k, v| |
|---|
| | 422 | if path_keys.include?(k) then (v.is_a?(Regexp) ? conditions : defaults)[k] = v |
|---|
| | 423 | else known[k] = v |
|---|
| | 424 | end |
|---|
| | 425 | end |
|---|
| | 426 | [defaults, conditions] |
|---|
| | 427 | end |
|---|
| | 428 | |
|---|
| | 429 | def configure_components(defaults, conditions) |
|---|
| | 430 | components.each do |component| |
|---|
| | 431 | if defaults.key?(component.key) then component.default = defaults[component.key] |
|---|
| | 432 | elsif component.key == :action then component.default = 'index' |
|---|
| | 433 | elsif component.key == :id then component.default = nil |
|---|
| | 434 | end |
|---|
| | 435 | |
|---|
| | 436 | component.condition = conditions[component.key] if conditions.key?(component.key) |
|---|
| | 437 | end |
|---|
| | 438 | end |
|---|
| | 439 | |
|---|
| | 440 | def add_default_requirements |
|---|
| | 441 | component_keys = components.collect {|c| c.key} |
|---|
| | 442 | known[:action] ||= 'index' unless component_keys.include? :action |
|---|
| | 443 | end |
|---|
| | 444 | end |
|---|
| | 445 | |
|---|
| | 446 | class RouteSet #:nodoc: |
|---|
| | 447 | attr_reader :routes, :categories, :controller_to_selector |
|---|