| 354 | | |
|---|
| 355 | | class Route #:nodoc: |
|---|
| 356 | | attr_accessor :segments, :requirements, :conditions, :optimise |
|---|
| 357 | | |
|---|
| 358 | | def initialize |
|---|
| 359 | | @segments = [] |
|---|
| 360 | | @requirements = {} |
|---|
| 361 | | @conditions = {} |
|---|
| 362 | | @optimise = true |
|---|
| 363 | | end |
|---|
| 364 | | |
|---|
| 365 | | # Indicates whether the routes should be optimised with the string interpolation |
|---|
| 366 | | # version of the named routes methods. |
|---|
| 367 | | def optimise? |
|---|
| 368 | | @optimise && ActionController::Base::optimise_named_routes |
|---|
| 369 | | end |
|---|
| 370 | | |
|---|
| 371 | | def segment_keys |
|---|
| 372 | | segments.collect do |segment| |
|---|
| 373 | | segment.key if segment.respond_to? :key |
|---|
| 374 | | end.compact |
|---|
| 375 | | end |
|---|
| 376 | | |
|---|
| 377 | | # Write and compile a +generate+ method for this Route. |
|---|
| 378 | | def write_generation |
|---|
| 379 | | # Build the main body of the generation |
|---|
| 380 | | body = "expired = false\n#{generation_extraction}\n#{generation_structure}" |
|---|
| 381 | | |
|---|
| 382 | | # If we have conditions that must be tested first, nest the body inside an if |
|---|
| 383 | | body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements |
|---|
| 384 | | args = "options, hash, expire_on = {}" |
|---|
| 385 | | |
|---|
| 386 | | # Nest the body inside of a def block, and then compile it. |
|---|
| 387 | | raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend" |
|---|
| 388 | | instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" |
|---|
| 389 | | |
|---|
| 390 | | # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash |
|---|
| 391 | | # are the same as the keys that were recalled from the previous request. Thus, |
|---|
| 392 | | # we can use the expire_on.keys to determine which keys ought to be used to build |
|---|
| 393 | | # the query string. (Never use keys from the recalled request when building the |
|---|
| 394 | | # query string.) |
|---|
| 395 | | |
|---|
| 396 | | method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend" |
|---|
| 397 | | instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" |
|---|
| 398 | | |
|---|
| 399 | | method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend" |
|---|
| 400 | | instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" |
|---|
| 401 | | raw_method |
|---|
| 402 | | end |
|---|
| 403 | | |
|---|
| 404 | | # Build several lines of code that extract values from the options hash. If any |
|---|
| 405 | | # of the values are missing or rejected then a return will be executed. |
|---|
| 406 | | def generation_extraction |
|---|
| 407 | | segments.collect do |segment| |
|---|
| 408 | | segment.extraction_code |
|---|
| 409 | | end.compact * "\n" |
|---|
| 410 | | end |
|---|
| 411 | | |
|---|
| 412 | | # Produce a condition expression that will check the requirements of this route |
|---|
| 413 | | # upon generation. |
|---|
| 414 | | def generation_requirements |
|---|
| 415 | | requirement_conditions = requirements.collect do |key, req| |
|---|
| 416 | | if req.is_a? Regexp |
|---|
| 417 | | value_regexp = Regexp.new "\\A#{req.source}\\Z" |
|---|
| 418 | | "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]" |
|---|
| 419 | | else |
|---|
| 420 | | "hash[:#{key}] == #{req.inspect}" |
|---|
| 421 | | end |
|---|
| 422 | | end |
|---|
| 423 | | requirement_conditions * ' && ' unless requirement_conditions.empty? |
|---|
| 424 | | end |
|---|
| 425 | | |
|---|
| 426 | | def generation_structure |
|---|
| 427 | | segments.last.string_structure segments[0..-2] |
|---|
| 428 | | end |
|---|
| 429 | | |
|---|
| 430 | | # Write and compile a +recognize+ method for this Route. |
|---|
| 431 | | def write_recognition |
|---|
| 432 | | # Create an if structure to extract the params from a match if it occurs. |
|---|
| 433 | | body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams" |
|---|
| 434 | | body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend" |
|---|
| 435 | | |
|---|
| 436 | | # Build the method declaration and compile it |
|---|
| 437 | | method_decl = "def recognize(path, env={})\n#{body}\nend" |
|---|
| 438 | | instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" |
|---|
| 439 | | method_decl |
|---|
| 440 | | end |
|---|
| 441 | | |
|---|
| 442 | | # Plugins may override this method to add other conditions, like checks on |
|---|
| 443 | | # host, subdomain, and so forth. Note that changes here only affect route |
|---|
| 444 | | # recognition, not generation. |
|---|
| 445 | | def recognition_conditions |
|---|
| 446 | | result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"] |
|---|
| 447 | | result << "conditions[:method] === env[:method]" if conditions[:method] |
|---|
| 448 | | result |
|---|
| 449 | | end |
|---|
| 450 | | |
|---|
| 451 | | # Build the regular expression pattern that will match this route. |
|---|
| 452 | | def recognition_pattern(wrap = true) |
|---|
| 453 | | pattern = '' |
|---|
| 454 | | segments.reverse_each do |segment| |
|---|
| 455 | | pattern = segment.build_pattern pattern |
|---|
| 456 | | end |
|---|
| 457 | | wrap ? ("\\A" + pattern + "\\Z") : pattern |
|---|
| 458 | | end |
|---|
| 459 | | |
|---|
| 460 | | # Write the code to extract the parameters from a matched route. |
|---|
| 461 | | def recognition_extraction |
|---|
| 462 | | next_capture = 1 |
|---|
| 463 | | extraction = segments.collect do |segment| |
|---|
| 464 | | x = segment.match_extraction(next_capture) |
|---|
| 465 | | next_capture += Regexp.new(segment.regexp_chunk).number_of_captures |
|---|
| 466 | | x |
|---|
| 467 | | end |
|---|
| 468 | | extraction.compact |
|---|
| 469 | | end |
|---|
| 470 | | |
|---|
| 471 | | # Write the real generation implementation and then resend the message. |
|---|
| 472 | | def generate(options, hash, expire_on = {}) |
|---|
| 473 | | write_generation |
|---|
| 474 | | generate options, hash, expire_on |
|---|
| 475 | | end |
|---|
| 476 | | |
|---|
| 477 | | def generate_extras(options, hash, expire_on = {}) |
|---|
| 478 | | write_generation |
|---|
| 479 | | generate_extras options, hash, expire_on |
|---|
| 480 | | end |
|---|
| 481 | | |
|---|
| 482 | | # Generate the query string with any extra keys in the hash and append |
|---|
| 483 | | # it to the given path, returning the new path. |
|---|
| 484 | | def append_query_string(path, hash, query_keys=nil) |
|---|
| 485 | | return nil unless path |
|---|
| 486 | | query_keys ||= extra_keys(hash) |
|---|
| 487 | | "#{path}#{build_query_string(hash, query_keys)}" |
|---|
| 488 | | end |
|---|
| 489 | | |
|---|
| 490 | | # Determine which keys in the given hash are "extra". Extra keys are |
|---|
| 491 | | # those that were not used to generate a particular route. The extra |
|---|
| 492 | | # keys also do not include those recalled from the prior request, nor |
|---|
| 493 | | # do they include any keys that were implied in the route (like a |
|---|
| 494 | | # :controller that is required, but not explicitly used in the text of |
|---|
| 495 | | # the route.) |
|---|
| 496 | | def extra_keys(hash, recall={}) |
|---|
| 497 | | (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys |
|---|
| 498 | | end |
|---|
| 499 | | |
|---|
| 500 | | # Build a query string from the keys of the given hash. If +only_keys+ |
|---|
| 501 | | # is given (as an array), only the keys indicated will be used to build |
|---|
| 502 | | # the query string. The query string will correctly build array parameter |
|---|
| 503 | | # values. |
|---|
| 504 | | def build_query_string(hash, only_keys = nil) |
|---|
| 505 | | elements = [] |
|---|
| 506 | | |
|---|
| 507 | | (only_keys || hash.keys).each do |key| |
|---|
| 508 | | if value = hash[key] |
|---|
| 509 | | elements << value.to_query(key) |
|---|
| 510 | | end |
|---|
| 511 | | end |
|---|
| 512 | | |
|---|
| 513 | | elements.empty? ? '' : "?#{elements.sort * '&'}" |
|---|
| 514 | | end |
|---|
| 515 | | |
|---|
| 516 | | # Write the real recognition implementation and then resend the message. |
|---|
| 517 | | def recognize(path, environment={}) |
|---|
| 518 | | write_recognition |
|---|
| 519 | | recognize path, environment |
|---|
| 520 | | end |
|---|
| 521 | | |
|---|
| 522 | | # A route's parameter shell contains parameter values that are not in the |
|---|
| 523 | | # route's path, but should be placed in the recognized hash. |
|---|
| 524 | | # |
|---|
| 525 | | # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route: |
|---|
| 526 | | # |
|---|
| 527 | | # map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/ |
|---|
| 528 | | # |
|---|
| 529 | | def parameter_shell |
|---|
| 530 | | @parameter_shell ||= returning({}) do |shell| |
|---|
| 531 | | requirements.each do |key, requirement| |
|---|
| 532 | | shell[key] = requirement unless requirement.is_a? Regexp |
|---|
| 533 | | end |
|---|
| 534 | | end |
|---|
| 535 | | end |
|---|
| 536 | | |
|---|
| 537 | | # Return an array containing all the keys that are used in this route. This |
|---|
| 538 | | # includes keys that appear inside the path, and keys that have requirements |
|---|
| 539 | | # placed upon them. |
|---|
| 540 | | def significant_keys |
|---|
| 541 | | @significant_keys ||= returning [] do |sk| |
|---|
| 542 | | segments.each { |segment| sk << segment.key if segment.respond_to? :key } |
|---|
| 543 | | sk.concat requirements.keys |
|---|
| 544 | | sk.uniq! |
|---|
| 545 | | end |
|---|
| 546 | | end |
|---|
| 547 | | |
|---|
| 548 | | # Return a hash of key/value pairs representing the keys in the route that |
|---|
| 549 | | # have defaults, or which are specified by non-regexp requirements. |
|---|
| 550 | | def defaults |
|---|
| 551 | | @defaults ||= returning({}) do |hash| |
|---|
| 552 | | segments.each do |segment| |
|---|
| 553 | | next unless segment.respond_to? :default |
|---|
| 554 | | hash[segment.key] = segment.default unless segment.default.nil? |
|---|
| 555 | | end |
|---|
| 556 | | requirements.each do |key,req| |
|---|
| 557 | | next if Regexp === req || req.nil? |
|---|
| 558 | | hash[key] = req |
|---|
| 559 | | end |
|---|
| 560 | | end |
|---|
| 561 | | end |
|---|
| 562 | | |
|---|
| 563 | | def matches_controller_and_action?(controller, action) |
|---|
| 564 | | unless defined? @matching_prepared |
|---|
| 565 | | @controller_requirement = requirement_for(:controller) |
|---|
| 566 | | @action_requirement = requirement_for(:action) |
|---|
| 567 | | @matching_prepared = true |
|---|
| 568 | | end |
|---|
| 569 | | |
|---|
| 570 | | (@controller_requirement.nil? || @controller_requirement === controller) && |
|---|
| 571 | | (@action_requirement.nil? || @action_requirement === action) |
|---|
| 572 | | end |
|---|
| 573 | | |
|---|
| 574 | | def to_s |
|---|
| 575 | | @to_s ||= begin |
|---|
| 576 | | segs = segments.inject("") { |str,s| str << s.to_s } |
|---|
| 577 | | "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect] |
|---|
| 578 | | end |
|---|
| 579 | | end |
|---|
| 580 | | |
|---|
| 581 | | protected |
|---|
| 582 | | def requirement_for(key) |
|---|
| 583 | | return requirements[key] if requirements.key? key |
|---|
| 584 | | segments.each do |segment| |
|---|
| 585 | | return segment.regexp if segment.respond_to?(:key) && segment.key == key |
|---|
| 586 | | end |
|---|
| 587 | | nil |
|---|
| 588 | | end |
|---|
| 589 | | |
|---|
| 590 | | end |
|---|
| 591 | | |
|---|
| 592 | | class Segment #:nodoc: |
|---|
| 593 | | RESERVED_PCHAR = ':@&=+$,;' |
|---|
| 594 | | UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze |
|---|
| 595 | | |
|---|
| 596 | | attr_accessor :is_optional |
|---|
| 597 | | alias_method :optional?, :is_optional |
|---|
| 598 | | |
|---|
| 599 | | def initialize |
|---|
| 600 | | self.is_optional = false |
|---|
| 601 | | end |
|---|
| 602 | | |
|---|
| 603 | | def extraction_code |
|---|
| 604 | | nil |
|---|
| 605 | | end |
|---|
| 606 | | |
|---|
| 607 | | # Continue generating string for the prior segments. |
|---|
| 608 | | def continue_string_structure(prior_segments) |
|---|
| 609 | | if prior_segments.empty? |
|---|
| 610 | | interpolation_statement(prior_segments) |
|---|
| 611 | | else |
|---|
| 612 | | new_priors = prior_segments[0..-2] |
|---|
| 613 | | prior_segments.last.string_structure(new_priors) |
|---|
| 614 | | end |
|---|
| 615 | | end |
|---|
| 616 | | |
|---|
| 617 | | def interpolation_chunk |
|---|
| 618 | | URI.escape(value, UNSAFE_PCHAR) |
|---|
| 619 | | end |
|---|
| 620 | | |
|---|
| 621 | | # Return a string interpolation statement for this segment and those before it. |
|---|
| 622 | | def interpolation_statement(prior_segments) |
|---|
| 623 | | chunks = prior_segments.collect { |s| s.interpolation_chunk } |
|---|
| 624 | | chunks << interpolation_chunk |
|---|
| 625 | | "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}" |
|---|
| 626 | | end |
|---|
| 627 | | |
|---|
| 628 | | def string_structure(prior_segments) |
|---|
| 629 | | optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments) |
|---|
| 630 | | end |
|---|
| 631 | | |
|---|
| 632 | | # Return an if condition that is true if all the prior segments can be generated. |
|---|
| 633 | | # If there are no optional segments before this one, then nil is returned. |
|---|
| 634 | | def all_optionals_available_condition(prior_segments) |
|---|
| 635 | | optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact |
|---|
| 636 | | optional_locals.empty? ? nil : " if #{optional_locals * ' && '}" |
|---|
| 637 | | end |
|---|
| 638 | | |
|---|
| 639 | | # Recognition |
|---|
| 640 | | |
|---|
| 641 | | def match_extraction(next_capture) |
|---|
| 642 | | nil |
|---|
| 643 | | end |
|---|
| 644 | | |
|---|
| 645 | | # Warning |
|---|
| 646 | | |
|---|
| 647 | | # Returns true if this segment is optional? because of a default. If so, then |
|---|
| 648 | | # no warning will be emitted regarding this segment. |
|---|
| 649 | | def optionality_implied? |
|---|
| 650 | | false |
|---|
| 651 | | end |
|---|
| 652 | | end |
|---|
| 653 | | |
|---|
| 654 | | class StaticSegment < Segment #:nodoc: |
|---|
| 655 | | attr_accessor :value, :raw |
|---|
| 656 | | alias_method :raw?, :raw |
|---|
| 657 | | |
|---|
| 658 | | def initialize(value = nil) |
|---|
| 659 | | super() |
|---|
| 660 | | self.value = value |
|---|
| 661 | | end |
|---|
| 662 | | |
|---|
| 663 | | def interpolation_chunk |
|---|
| 664 | | raw? ? value : super |
|---|
| 665 | | end |
|---|
| 666 | | |
|---|
| 667 | | def regexp_chunk |
|---|
| 668 | | chunk = Regexp.escape(value) |
|---|
| 669 | | optional? ? Regexp.optionalize(chunk) : chunk |
|---|
| 670 | | end |
|---|
| 671 | | |
|---|
| 672 | | def build_pattern(pattern) |
|---|
| 673 | | escaped = Regexp.escape(value) |
|---|
| 674 | | if optional? && ! pattern.empty? |
|---|
| 675 | | "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})" |
|---|
| 676 | | elsif optional? |
|---|
| 677 | | Regexp.optionalize escaped |
|---|
| 678 | | else |
|---|
| 679 | | escaped + pattern |
|---|
| 680 | | end |
|---|
| 681 | | end |
|---|
| 682 | | |
|---|
| 683 | | def to_s |
|---|
| 684 | | value |
|---|
| 685 | | end |
|---|
| 686 | | end |
|---|
| 687 | | |
|---|
| 688 | | class DividerSegment < StaticSegment #:nodoc: |
|---|
| 689 | | def initialize(value = nil) |
|---|
| 690 | | super(value) |
|---|
| 691 | | self.raw = true |
|---|
| 692 | | self.is_optional = true |
|---|
| 693 | | end |
|---|
| 694 | | |
|---|
| 695 | | def optionality_implied? |
|---|
| 696 | | true |
|---|
| 697 | | end |
|---|
| 698 | | end |
|---|
| 699 | | |
|---|
| 700 | | class DynamicSegment < Segment #:nodoc: |
|---|
| 701 | | attr_accessor :key, :default, :regexp |
|---|
| 702 | | |
|---|
| 703 | | def initialize(key = nil, options = {}) |
|---|
| 704 | | super() |
|---|
| 705 | | self.key = key |
|---|
| 706 | | self.default = options[:default] if options.key? :default |
|---|
| 707 | | self.is_optional = true if options[:optional] || options.key?(:default) |
|---|
| 708 | | end |
|---|
| 709 | | |
|---|
| 710 | | def to_s |
|---|
| 711 | | ":#{key}" |
|---|
| 712 | | end |
|---|
| 713 | | |
|---|
| 714 | | # The local variable name that the value of this segment will be extracted to. |
|---|
| 715 | | def local_name |
|---|
| 716 | | "#{key}_value" |
|---|
| 717 | | end |
|---|
| 718 | | |
|---|
| 719 | | def extract_value |
|---|
| 720 | | "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}" |
|---|
| 721 | | end |
|---|
| 722 | | def value_check |
|---|
| 723 | | if default # Then we know it won't be nil |
|---|
| 724 | | "#{value_regexp.inspect} =~ #{local_name}" if regexp |
|---|
| 725 | | elsif optional? |
|---|
| 726 | | # If we have a regexp check that the value is not given, or that it matches. |
|---|
| 727 | | # If we have no regexp, return nil since we do not require a condition. |
|---|
| 728 | | "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp |
|---|
| 729 | | else # Then it must be present, and if we have a regexp, it must match too. |
|---|
| 730 | | "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}" |
|---|
| 731 | | end |
|---|
| 732 | | end |
|---|
| 733 | | def expiry_statement |
|---|
| 734 | | "expired, hash = true, options if !expired && expire_on[:#{key}]" |
|---|
| 735 | | end |
|---|
| 736 | | |
|---|
| 737 | | def extraction_code |
|---|
| 738 | | s = extract_value |
|---|
| 739 | | vc = value_check |
|---|
| 740 | | s << "\nreturn [nil,nil] unless #{vc}" if vc |
|---|
| 741 | | s << "\n#{expiry_statement}" |
|---|
| 742 | | end |
|---|
| 743 | | |
|---|
| 744 | | def interpolation_chunk(value_code = "#{local_name}") |
|---|
| 745 | | "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}" |
|---|
| 746 | | end |
|---|
| 747 | | |
|---|
| 748 | | def string_structure(prior_segments) |
|---|
| 749 | | if optional? # We have a conditional to do... |
|---|
| 750 | | # If we should not appear in the url, just write the code for the prior |
|---|
| 751 | | # segments. This occurs if our value is the default value, or, if we are |
|---|
| 752 | | # optional, if we have nil as our value. |
|---|
| 753 | | "if #{local_name} == #{default.inspect}\n" + |
|---|
| 754 | | continue_string_structure(prior_segments) + |
|---|
| 755 | | "\nelse\n" + # Otherwise, write the code up to here |
|---|
| 756 | | "#{interpolation_statement(prior_segments)}\nend" |
|---|
| 757 | | else |
|---|
| 758 | | interpolation_statement(prior_segments) |
|---|
| 759 | | end |
|---|
| 760 | | end |
|---|
| 761 | | |
|---|
| 762 | | def value_regexp |
|---|
| 763 | | Regexp.new "\\A#{regexp.source}\\Z" if regexp |
|---|
| 764 | | end |
|---|
| 765 | | def regexp_chunk |
|---|
| 766 | | regexp ? "(#{regexp.source})" : "([^#{Routing::SEPARATORS.join}]+)" |
|---|
| 767 | | end |
|---|
| 768 | | |
|---|
| 769 | | def build_pattern(pattern) |
|---|
| 770 | | chunk = regexp_chunk |
|---|
| 771 | | chunk = "(#{chunk})" if Regexp.new(chunk).number_of_captures == 0 |
|---|
| 772 | | pattern = "#{chunk}#{pattern}" |
|---|
| 773 | | optional? ? Regexp.optionalize(pattern) : pattern |
|---|
| 774 | | end |
|---|
| 775 | | def match_extraction(next_capture) |
|---|
| 776 | | # All non code-related keys (such as :id, :slug) are URI-unescaped as |
|---|
| 777 | | # path parameters. |
|---|
| 778 | | default_value = default ? default.inspect : nil |
|---|
| 779 | | %[ |
|---|
| 780 | | value = if (m = match[#{next_capture}]) |
|---|
| 781 | | URI.unescape(m) |
|---|
| 782 | | else |
|---|
| 783 | | #{default_value} |
|---|
| 784 | | end |
|---|
| 785 | | params[:#{key}] = value if value |
|---|
| 786 | | ] |
|---|
| 787 | | end |
|---|
| 788 | | |
|---|
| 789 | | def optionality_implied? |
|---|
| 790 | | [:action, :id].include? key |
|---|
| 791 | | end |
|---|
| 792 | | |
|---|
| 793 | | end |
|---|
| 794 | | |
|---|
| 795 | | class ControllerSegment < DynamicSegment #:nodoc: |
|---|
| 796 | | def regexp_chunk |
|---|
| 797 | | possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name } |
|---|
| 798 | | "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))" |
|---|
| 799 | | end |
|---|
| 800 | | |
|---|
| 801 | | # Don't URI.escape the controller name since it may contain slashes. |
|---|
| 802 | | def interpolation_chunk(value_code = "#{local_name}") |
|---|
| 803 | | "\#{#{value_code}.to_s}" |
|---|
| 804 | | end |
|---|
| 805 | | |
|---|
| 806 | | # Make sure controller names like Admin/Content are correctly normalized to |
|---|
| 807 | | # admin/content |
|---|
| 808 | | def extract_value |
|---|
| 809 | | "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase" |
|---|
| 810 | | end |
|---|
| 811 | | |
|---|
| 812 | | def match_extraction(next_capture) |
|---|
| 813 | | if default |
|---|
| 814 | | "params[:#{key}] = match[#{next_capture}] ? match[#{next_capture}].downcase : '#{default}'" |
|---|
| 815 | | else |
|---|
| 816 | | "params[:#{key}] = match[#{next_capture}].downcase if match[#{next_capture}]" |
|---|
| 817 | | end |
|---|
| 818 | | end |
|---|
| 819 | | end |
|---|
| 820 | | |
|---|
| 821 | | class PathSegment < DynamicSegment #:nodoc: |
|---|
| 822 | | RESERVED_PCHAR = "#{Segment::RESERVED_PCHAR}/" |
|---|
| 823 | | UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze |
|---|
| 824 | | |
|---|
| 825 | | def interpolation_chunk(value_code = "#{local_name}") |
|---|
| 826 | | "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::PathSegment::UNSAFE_PCHAR)}" |
|---|
| 827 | | end |
|---|
| 828 | | |
|---|
| 829 | | def default |
|---|
| 830 | | '' |
|---|
| 831 | | end |
|---|
| 832 | | |
|---|
| 833 | | def default=(path) |
|---|
| 834 | | raise RoutingError, "paths cannot have non-empty default values" unless path.blank? |
|---|
| 835 | | end |
|---|
| 836 | | |
|---|
| 837 | | def match_extraction(next_capture) |
|---|
| 838 | | "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}" |
|---|
| 839 | | end |
|---|
| 840 | | |
|---|
| 841 | | def regexp_chunk |
|---|
| 842 | | regexp || "(.*)" |
|---|
| 843 | | end |
|---|
| 844 | | |
|---|
| 845 | | def optionality_implied? |
|---|
| 846 | | true |
|---|
| 847 | | end |
|---|
| 848 | | |
|---|
| 849 | | class Result < ::Array #:nodoc: |
|---|
| 850 | | def to_s() join '/' end |
|---|
| 851 | | def self.new_escaped(strings) |
|---|
| 852 | | new strings.collect {|str| URI.unescape str} |
|---|
| 853 | | end |
|---|
| 854 | | end |
|---|
| 855 | | end |
|---|
| 856 | | |
|---|
| 857 | | class RouteBuilder #:nodoc: |
|---|
| 858 | | attr_accessor :separators, :optional_separators |
|---|
| 859 | | |
|---|
| 860 | | def initialize |
|---|
| 861 | | self.separators = Routing::SEPARATORS |
|---|
| 862 | | self.optional_separators = %w( / ) |
|---|
| 863 | | end |
|---|
| 864 | | |
|---|
| 865 | | def separator_pattern(inverted = false) |
|---|
| 866 | | "[#{'^' if inverted}#{Regexp.escape(separators.join)}]" |
|---|
| 867 | | end |
|---|
| 868 | | |
|---|
| 869 | | def interval_regexp |
|---|
| 870 | | Regexp.new "(.*?)(#{separators.source}|$)" |
|---|
| 871 | | end |
|---|
| 872 | | |
|---|
| 873 | | # Accepts a "route path" (a string defining a route), and returns the array |
|---|
| 874 | | # of segments that corresponds to it. Note that the segment array is only |
|---|
| 875 | | # partially initialized--the defaults and requirements, for instance, need |
|---|
| 876 | | # to be set separately, via the #assign_route_options method, and the |
|---|
| 877 | | # #optional? method for each segment will not be reliable until after |
|---|
| 878 | | # #assign_route_options is called, as well. |
|---|
| 879 | | def segments_for_route_path(path) |
|---|
| 880 | | rest, segments = path, [] |
|---|
| 881 | | |
|---|
| 882 | | until rest.empty? |
|---|
| 883 | | segment, rest = segment_for rest |
|---|
| 884 | | segments << segment |
|---|
| 885 | | end |
|---|
| 886 | | segments |
|---|
| 887 | | end |
|---|
| 888 | | |
|---|
| 889 | | # A factory method that returns a new segment instance appropriate for the |
|---|
| 890 | | # format of the given string. |
|---|
| 891 | | def segment_for(string) |
|---|
| 892 | | segment = case string |
|---|
| 893 | | when /\A:(\w+)/ |
|---|
| 894 | | key = $1.to_sym |
|---|
| 895 | | case key |
|---|
| 896 | | when :controller then ControllerSegment.new(key) |
|---|
| 897 | | else DynamicSegment.new key |
|---|
| 898 | | end |
|---|
| 899 | | when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true) |
|---|
| 900 | | when /\A\?(.*?)\?/ |
|---|
| 901 | | returning segment = StaticSegment.new($1) do |
|---|
| 902 | | segment.is_optional = true |
|---|
| 903 | | end |
|---|
| 904 | | when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1) |
|---|
| 905 | | when Regexp.new(separator_pattern) then |
|---|
| 906 | | returning segment = DividerSegment.new($&) do |
|---|
| 907 | | segment.is_optional = (optional_separators.include? $&) |
|---|
| 908 | | end |
|---|
| 909 | | end |
|---|
| 910 | | [segment, $~.post_match] |
|---|
| 911 | | end |
|---|
| 912 | | |
|---|
| 913 | | # Split the given hash of options into requirement and default hashes. The |
|---|
| 914 | | # segments are passed alongside in order to distinguish between default values |
|---|
| 915 | | # and requirements. |
|---|
| 916 | | def divide_route_options(segments, options) |
|---|
| 917 | | options = options.dup |
|---|
| 918 | | |
|---|
| 919 | | if options[:namespace] |
|---|
| 920 | | options[:controller] = "#{options[:path_prefix]}/#{options[:controller]}" |
|---|
| 921 | | options.delete(:path_prefix) |
|---|
| 922 | | options.delete(:name_prefix) |
|---|
| 923 | | options.delete(:namespace) |
|---|
| 924 | | end |
|---|
| 925 | | |
|---|
| 926 | | requirements = (options.delete(:requirements) || {}).dup |
|---|
| 927 | | defaults = (options.delete(:defaults) || {}).dup |
|---|
| 928 | | conditions = (options.delete(:conditions) || {}).dup |
|---|
| 929 | | |
|---|
| 930 | | path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact |
|---|
| 931 | | options.each do |key, value| |
|---|
| 932 | | hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements |
|---|
| 933 | | hash[key] = value |
|---|
| 934 | | end |
|---|
| 935 | | |
|---|
| 936 | | [defaults, requirements, conditions] |
|---|
| 937 | | end |
|---|
| 938 | | |
|---|
| 939 | | # Takes a hash of defaults and a hash of requirements, and assigns them to |
|---|
| 940 | | # the segments. Any unused requirements (which do not correspond to a segment) |
|---|
| 941 | | # are returned as a hash. |
|---|
| 942 | | def assign_route_options(segments, defaults, requirements) |
|---|
| 943 | | route_requirements = {} # Requirements that do not belong to a segment |
|---|
| 944 | | |
|---|
| 945 | | segment_named = Proc.new do |key| |
|---|
| 946 | | segments.detect { |segment| segment.key == key if segment.respond_to?(:key) } |
|---|
| 947 | | end |
|---|
| 948 | | |
|---|
| 949 | | requirements.each do |key, requirement| |
|---|
| 950 | | segment = segment_named[key] |
|---|
| 951 | | if segment |
|---|
| 952 | | raise TypeError, "#{key}: requirements on a path segment must be regular expressions" unless requirement.is_a?(Regexp) |
|---|
| 953 | | if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} |
|---|
| 954 | | raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" |
|---|
| 955 | | end |
|---|
| 956 | | segment.regexp = requirement |
|---|
| 957 | | else |
|---|
| 958 | | route_requirements[key] = requirement |
|---|
| 959 | | end |
|---|
| 960 | | end |
|---|
| 961 | | |
|---|
| 962 | | defaults.each do |key, default| |
|---|
| 963 | | segment = segment_named[key] |
|---|
| 964 | | raise ArgumentError, "#{key}: No matching segment exists; cannot assign default" unless segment |
|---|
| 965 | | segment.is_optional = true |
|---|
| 966 | | segment.default = default.to_param if default |
|---|
| 967 | | end |
|---|
| 968 | | |
|---|
| 969 | | assign_default_route_options(segments) |
|---|
| 970 | | ensure_required_segments(segments) |
|---|
| 971 | | route_requirements |
|---|
| 972 | | end |
|---|
| 973 | | |
|---|
| 974 | | # Assign default options, such as 'index' as a default for :action. This |
|---|
| 975 | | # method must be run *after* user supplied requirements and defaults have |
|---|
| 976 | | # been applied to the segments. |
|---|
| 977 | | def assign_default_route_options(segments) |
|---|
| 978 | | segments.each do |segment| |
|---|
| 979 | | next unless segment.is_a? DynamicSegment |
|---|
| 980 | | case segment.key |
|---|
| 981 | | when :action |
|---|
| 982 | | if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index' |
|---|
| 983 | | segment.default ||= 'index' |
|---|
| 984 | | segment.is_optional = true |
|---|
| 985 | | end |
|---|
| 986 | | when :id |
|---|
| 987 | | if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ '' |
|---|
| 988 | | segment.is_optional = true |
|---|
| 989 | | end |
|---|
| 990 | | end |
|---|
| 991 | | end |
|---|
| 992 | | end |
|---|
| 993 | | |
|---|
| 994 | | # Makes sure that there are no optional segments that precede a required |
|---|
| 995 | | # segment. If any are found that precede a required segment, they are |
|---|
| 996 | | # made required. |
|---|
| 997 | | def ensure_required_segments(segments) |
|---|
| 998 | | allow_optional = true |
|---|
| 999 | | segments.reverse_each do |segment| |
|---|
| 1000 | | allow_optional &&= segment.optional? |
|---|
| 1001 | | if !allow_optional && segment.optional? |
|---|
| 1002 | | unless segment.optionality_implied? |
|---|
| 1003 | | warn "Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required." |
|---|
| 1004 | | end |
|---|
| 1005 | | segment.is_optional = false |
|---|
| 1006 | | elsif allow_optional && segment.respond_to?(:default) && segment.default |
|---|
| 1007 | | # if a segment has a default, then it is optional |
|---|
| 1008 | | segment.is_optional = true |
|---|
| 1009 | | end |
|---|
| 1010 | | end |
|---|
| 1011 | | end |
|---|
| 1012 | | |
|---|
| 1013 | | # Construct and return a route with the given path and options. |
|---|
| 1014 | | def build(path, options) |
|---|
| 1015 | | # Wrap the path with slashes |
|---|
| 1016 | | path = "/#{path}" unless path[0] == ?/ |
|---|
| 1017 | | path = "#{path}/" unless path[-1] == ?/ |
|---|
| 1018 | | |
|---|
| 1019 | | path = "/#{options[:path_prefix].to_s.gsub(/^\//,'')}#{path}" if options[:path_prefix] |
|---|
| 1020 | | |
|---|
| 1021 | | segments = segments_for_route_path(path) |
|---|
| 1022 | | defaults, requirements, conditions = divide_route_options(segments, options) |
|---|
| 1023 | | requirements = assign_route_options(segments, defaults, requirements) |
|---|
| 1024 | | |
|---|
| 1025 | | route = Route.new |
|---|
| 1026 | | |
|---|
| 1027 | | route.segments = segments |
|---|
| 1028 | | route.requirements = requirements |
|---|
| 1029 | | route.conditions = conditions |
|---|
| 1030 | | |
|---|
| 1031 | | if !route.significant_keys.include?(:action) && !route.requirements[:action] |
|---|
| 1032 | | route.requirements[:action] = "index" |
|---|
| 1033 | | route.significant_keys << :action |
|---|
| 1034 | | end |
|---|
| 1035 | | |
|---|
| 1036 | | # Routes cannot use the current string interpolation method |
|---|
| 1037 | | # if there are user-supplied :requirements as the interpolation |
|---|
| 1038 | | # code won't raise RoutingErrors when generating |
|---|
| 1039 | | if options.key?(:requirements) || route.requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION |
|---|
| 1040 | | route.optimise = false |
|---|
| 1041 | | end |
|---|
| 1042 | | |
|---|
| 1043 | | if !route.significant_keys.include?(:controller) |
|---|
| 1044 | | raise ArgumentError, "Illegal route: the :controller must be specified!" |
|---|
| 1045 | | end |
|---|
| 1046 | | |
|---|
| 1047 | | route |
|---|
| 1048 | | end |
|---|
| 1049 | | end |
|---|
| 1050 | | |
|---|
| 1051 | | class RouteSet #:nodoc: |
|---|
| 1052 | | # Mapper instances are used to build routes. The object passed to the draw |
|---|
| 1053 | | # block in config/routes.rb is a Mapper instance. |
|---|
| 1054 | | # |
|---|
| 1055 | | # Mapper instances have relatively few instance methods, in order to avoid |
|---|
| 1056 | | # clashes with named routes. |
|---|
| 1057 | | class Mapper #:doc: |
|---|
| 1058 | | def initialize(set) #:nodoc: |
|---|
| 1059 | | @set = set |
|---|
| 1060 | | end |
|---|
| 1061 | | |
|---|
| 1062 | | # Create an unnamed route with the provided +path+ and +options+. See |
|---|
| 1063 | | # ActionController::Routing for an introduction to routes. |
|---|
| 1064 | | def connect(path, options = {}) |
|---|
| 1065 | | @set.add_route(path, options) |
|---|
| 1066 | | end |
|---|
| 1067 | | |
|---|
| 1068 | | # Creates a named route called "root" for matching the root level request. |
|---|
| 1069 | | def root(options = {}) |
|---|
| 1070 | | named_route("root", '', options) |
|---|
| 1071 | | end |
|---|
| 1072 | | |
|---|
| 1073 | | def named_route(name, path, options = {}) #:nodoc: |
|---|
| 1074 | | @set.add_named_route(name, path, options) |
|---|
| 1075 | | end |
|---|
| 1076 | | |
|---|
| 1077 | | # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model. |
|---|
| 1078 | | # Example: |
|---|
| 1079 | | # |
|---|
| 1080 | | # map.namespace(:admin) do |admin| |
|---|
| 1081 | | # admin.resources :products, |
|---|
| 1082 | | # :has_many => [ :tags, :images, :variants ] |
|---|
| 1083 | | # end |
|---|
| 1084 | | # |
|---|
| 1085 | | # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController. |
|---|
| 1086 | | # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for |
|---|
| 1087 | | # Admin::TagsController. |
|---|
| 1088 | | def namespace(name, options = {}, &block) |
|---|
| 1089 | | if options[:namespace] |
|---|
| 1090 | | with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block) |
|---|
| 1091 | | else |
|---|
| 1092 | | with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block) |
|---|
| 1093 | | end |
|---|
| 1094 | | end |
|---|
| 1095 | | |
|---|
| 1096 | | def method_missing(route_name, *args, &proc) #:nodoc: |
|---|
| 1097 | | super unless args.length >= 1 && proc.nil? |
|---|
| 1098 | | @set.add_named_route(route_name, *args) |
|---|
| 1099 | | end |
|---|
| 1100 | | end |
|---|
| 1101 | | |
|---|
| 1102 | | # A NamedRouteCollection instance is a collection of named routes, and also |
|---|
| 1103 | | # maintains an anonymous module that can be used to install helpers for the |
|---|
| 1104 | | # named routes. |
|---|
| 1105 | | class NamedRouteCollection #:nodoc: |
|---|
| 1106 | | include Enumerable |
|---|
| 1107 | | include ActionController::Routing::Optimisation |
|---|
| 1108 | | attr_reader :routes, :helpers |
|---|
| 1109 | | |
|---|
| 1110 | | def initialize |
|---|
| 1111 | | clear! |
|---|
| 1112 | | end |
|---|
| 1113 | | |
|---|
| 1114 | | def clear! |
|---|
| 1115 | | @routes = {} |
|---|
| 1116 | | @helpers = [] |
|---|
| 1117 | | |
|---|
| 1118 | | @module ||= Module.new |
|---|
| 1119 | | @module.instance_methods.each do |selector| |
|---|
| 1120 | | @module.class_eval { remove_method selector } |
|---|
| 1121 | | end |
|---|
| 1122 | | end |
|---|
| 1123 | | |
|---|
| 1124 | | def add(name, route) |
|---|
| 1125 | | routes[name.to_sym] = route |
|---|
| 1126 | | define_named_route_methods(name, route) |
|---|
| 1127 | | end |
|---|
| 1128 | | |
|---|
| 1129 | | def get(name) |
|---|
| 1130 | | routes[name.to_sym] |
|---|
| 1131 | | end |
|---|
| 1132 | | |
|---|
| 1133 | | alias []= add |
|---|
| 1134 | | alias [] get |
|---|
| 1135 | | alias clear clear! |
|---|
| 1136 | | |
|---|
| 1137 | | def each |
|---|
| 1138 | | routes.each { |name, route| yield name, route } |
|---|
| 1139 | | self |
|---|
| 1140 | | end |
|---|
| 1141 | | |
|---|
| 1142 | | def names |
|---|
| 1143 | | routes.keys |
|---|
| 1144 | | end |
|---|
| 1145 | | |
|---|
| 1146 | | def length |
|---|
| 1147 | | routes.length |
|---|
| 1148 | | end |
|---|
| 1149 | | |
|---|
| 1150 | | def reset! |
|---|
| 1151 | | old_routes = routes.dup |
|---|
| 1152 | | clear! |
|---|
| 1153 | | old_routes.each do |name, route| |
|---|
| 1154 | | add(name, route) |
|---|
| 1155 | | end |
|---|
| 1156 | | end |
|---|
| 1157 | | |
|---|
| 1158 | | def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false) |
|---|
| 1159 | | reset! if regenerate |
|---|
| 1160 | | Array(destinations).each do |dest| |
|---|
| 1161 | | dest.send! :include, @module |
|---|
| 1162 | | end |
|---|
| 1163 | | end |
|---|
| 1164 | | |
|---|
| 1165 | | private |
|---|
| 1166 | | def url_helper_name(name, kind = :url) |
|---|
| 1167 | | :"#{name}_#{kind}" |
|---|
| 1168 | | end |
|---|
| 1169 | | |
|---|
| 1170 | | def hash_access_name(name, kind = :url) |
|---|
| 1171 | | :"hash_for_#{name}_#{kind}" |
|---|
| 1172 | | end |
|---|
| 1173 | | |
|---|
| 1174 | | def define_named_route_methods(name, route) |
|---|
| 1175 | | {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts| |
|---|
| 1176 | | hash = route.defaults.merge(:use_route => name).merge(opts) |
|---|
| 1177 | | define_hash_access route, name, kind, hash |
|---|
| 1178 | | define_url_helper route, name, kind, hash |
|---|
| 1179 | | end |
|---|
| 1180 | | end |
|---|
| 1181 | | |
|---|
| 1182 | | def define_hash_access(route, name, kind, options) |
|---|
| 1183 | | selector = hash_access_name(name, kind) |
|---|
| 1184 | | @module.module_eval <<-end_eval # We use module_eval to avoid leaks |
|---|
| 1185 | | def #{selector}(options = nil) |
|---|
| 1186 | | options ? #{options.inspect}.merge(options) : #{options.inspect} |
|---|
| 1187 | | end |
|---|
| 1188 | | protected :#{selector} |
|---|
| 1189 | | end_eval |
|---|
| 1190 | | helpers << selector |
|---|
| 1191 | | end |
|---|
| 1192 | | |
|---|
| 1193 | | def define_url_helper(route, name, kind, options) |
|---|
| 1194 | | selector = url_helper_name(name, kind) |
|---|
| 1195 | | # The segment keys used for positional paramters |
|---|
| 1196 | | |
|---|
| 1197 | | hash_access_method = hash_access_name(name, kind) |
|---|
| 1198 | | |
|---|
| 1199 | | # allow ordered parameters to be associated with corresponding |
|---|
| 1200 | | # dynamic segments, so you can do |
|---|
| 1201 | | # |
|---|
| 1202 | | # foo_url(bar, baz, bang) |
|---|
| 1203 | | # |
|---|
| 1204 | | # instead of |
|---|
| 1205 | | # |
|---|
| 1206 | | # foo_url(:bar => bar, :baz => baz, :bang => bang) |
|---|
| 1207 | | # |
|---|
| 1208 | | # Also allow options hash, so you can do |
|---|
| 1209 | | # |
|---|
| 1210 | | # foo_url(bar, baz, bang, :sort_by => 'baz') |
|---|
| 1211 | | # |
|---|
| 1212 | | @module.module_eval <<-end_eval # We use module_eval to avoid leaks |
|---|
| 1213 | | def #{selector}(*args) |
|---|
| 1214 | | #{generate_optimisation_block(route, kind)} |
|---|
| 1215 | | |
|---|
| 1216 | | opts = if args.empty? || Hash === args.first |
|---|
| 1217 | | args.first || {} |
|---|
| 1218 | | else |
|---|
| 1219 | | options = args.extract_options! |
|---|
| 1220 | | args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)| |
|---|
| 1221 | | h[k] = v |
|---|
| 1222 | | h |
|---|
| 1223 | | end |
|---|
| 1224 | | options.merge(args) |
|---|
| 1225 | | end |
|---|
| 1226 | | |
|---|
| 1227 | | url_for(#{hash_access_method}(opts)) |
|---|
| 1228 | | end |
|---|
| 1229 | | protected :#{selector} |
|---|
| 1230 | | end_eval |
|---|
| 1231 | | helpers << selector |
|---|
| 1232 | | end |
|---|
| 1233 | | end |
|---|
| 1234 | | |
|---|
| 1235 | | attr_accessor :routes, :named_routes |
|---|
| 1236 | | |
|---|
| 1237 | | def initialize |
|---|
| 1238 | | self.routes = [] |
|---|
| 1239 | | self.named_routes = NamedRouteCollection.new |
|---|
| 1240 | | end |
|---|
| 1241 | | |
|---|
| 1242 | | # Subclasses and plugins may override this method to specify a different |
|---|
| 1243 | | # RouteBuilder instance, so that other route DSL's can be created. |
|---|
| 1244 | | def builder |
|---|
| 1245 | | @builder ||= RouteBuilder.new |
|---|
| 1246 | | end |
|---|
| 1247 | | |
|---|
| 1248 | | def draw |
|---|
| 1249 | | clear! |
|---|
| 1250 | | yield Mapper.new(self) |
|---|
| 1251 | | install_helpers |
|---|
| 1252 | | end |
|---|
| 1253 | | |
|---|
| 1254 | | def clear! |
|---|
| 1255 | | routes.clear |
|---|
| 1256 | | named_routes.clear |
|---|
| 1257 | | @combined_regexp = nil |
|---|
| 1258 | | @routes_by_controller = nil |
|---|
| 1259 | | end |
|---|
| 1260 | | |
|---|
| 1261 | | def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) |
|---|
| 1262 | | Array(destinations).each { |d| d.module_eval { include Helpers } } |
|---|
| 1263 | | named_routes.install(destinations, regenerate_code) |
|---|
| 1264 | | end |
|---|
| 1265 | | |
|---|
| 1266 | | def empty? |
|---|
| 1267 | | routes.empty? |
|---|
| 1268 | | end |
|---|
| 1269 | | |
|---|
| 1270 | | def load! |
|---|
| 1271 | | Routing.use_controllers! nil # Clear the controller cache so we may discover new ones |
|---|
| 1272 | | clear! |
|---|
| 1273 | | load_routes! |
|---|
| 1274 | | install_helpers |
|---|
| 1275 | | end |
|---|
| 1276 | | |
|---|
| 1277 | | # reload! will always force a reload whereas load checks the timestamp first |
|---|
| 1278 | | alias reload! load! |
|---|
| 1279 | | |
|---|
| 1280 | | def reload |
|---|
| 1281 | | if @routes_last_modified && defined?(RAILS_ROOT) |
|---|
| 1282 | | mtime = File.stat("#{RAILS_ROOT}/config/routes.rb").mtime |
|---|
| 1283 | | # if it hasn't been changed, then just return |
|---|
| 1284 | | return if mtime == @routes_last_modified |
|---|
| 1285 | | # if it has changed then record the new time and fall to the load! below |
|---|
| 1286 | | @routes_last_modified = mtime |
|---|
| 1287 | | end |
|---|
| 1288 | | load! |
|---|
| 1289 | | end |
|---|
| 1290 | | |
|---|
| 1291 | | def load_routes! |
|---|
| 1292 | | if defined?(RAILS_ROOT) && defined?(::ActionController::Routing::Routes) && self == ::ActionController::Routing::Routes |
|---|
| 1293 | | load File.join("#{RAILS_ROOT}/config/routes.rb") |
|---|
| 1294 | | @routes_last_modified = File.stat("#{RAILS_ROOT}/config/routes.rb").mtime |
|---|
| 1295 | | else |
|---|
| 1296 | | add_route ":controller/:action/:id" |
|---|
| 1297 | | end |
|---|
| 1298 | | end |
|---|
| 1299 | | |
|---|
| 1300 | | def add_route(path, options = {}) |
|---|
| 1301 | | route = builder.build(path, options) |
|---|
| 1302 | | routes << route |
|---|
| 1303 | | route |
|---|
| 1304 | | end |
|---|
| 1305 | | |
|---|
| 1306 | | def add_named_route(name, path, options = {}) |
|---|
| 1307 | | # TODO - is options EVER used? |
|---|
| 1308 | | name = options[:name_prefix] + name.to_s if options[:name_prefix] |
|---|
| 1309 | | named_routes[name.to_sym] = add_route(path, options) |
|---|
| 1310 | | end |
|---|
| 1311 | | |
|---|
| 1312 | | def options_as_params(options) |
|---|
| 1313 | | # If an explicit :controller was given, always make :action explicit |
|---|
| 1314 | | # too, so that action expiry works as expected for things like |
|---|
| 1315 | | # |
|---|
| 1316 | | # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) |
|---|
| 1317 | | # |
|---|
| 1318 | | # (the above is from the unit tests). In the above case, because the |
|---|
| 1319 | | # controller was explicitly given, but no action, the action is implied to |
|---|
| 1320 | | # be "index", not the recalled action of "show". |
|---|
| 1321 | | # |
|---|
| 1322 | | # great fun, eh? |
|---|
| 1323 | | |
|---|
| 1324 | | options_as_params = options.clone |
|---|
| 1325 | | options_as_params[:action] ||= 'index' if options[:controller] |
|---|
| 1326 | | options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action] |
|---|
| 1327 | | options_as_params |
|---|
| 1328 | | end |
|---|
| 1329 | | |
|---|
| 1330 | | def build_expiry(options, recall) |
|---|
| 1331 | | recall.inject({}) do |expiry, (key, recalled_value)| |
|---|
| 1332 | | expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param) |
|---|
| 1333 | | expiry |
|---|
| 1334 | | end |
|---|
| 1335 | | end |
|---|
| 1336 | | |
|---|
| 1337 | | # Generate the path indicated by the arguments, and return an array of |
|---|
| 1338 | | # the keys that were not used to generate it. |
|---|
| 1339 | | def extra_keys(options, recall={}) |
|---|
| 1340 | | generate_extras(options, recall).last |
|---|
| 1341 | | end |
|---|
| 1342 | | |
|---|
| 1343 | | def generate_extras(options, recall={}) |
|---|
| 1344 | | generate(options, recall, :generate_extras) |
|---|
| 1345 | | end |
|---|
| 1346 | | |
|---|
| 1347 | | def generate(options, recall = {}, method=:generate) |
|---|
| 1348 | | named_route_name = options.delete(:use_route) |
|---|
| 1349 | | generate_all = options.delete(:generate_all) |
|---|
| 1350 | | if named_route_name |
|---|
| 1351 | | named_route = named_routes[named_route_name] |
|---|
| 1352 | | options = named_route.parameter_shell.merge(options) |
|---|
| 1353 | | end |
|---|
| 1354 | | |
|---|
| 1355 | | options = options_as_params(options) |
|---|
| 1356 | | expire_on = build_expiry(options, recall) |
|---|
| 1357 | | |
|---|
| 1358 | | if options[:controller] |
|---|
| 1359 | | options[:controller] = options[:controller].to_s |
|---|
| 1360 | | end |
|---|
| 1361 | | # if the controller has changed, make sure it changes relative to the |
|---|
| 1362 | | # current controller module, if any. In other words, if we're currently |
|---|
| 1363 | | # on admin/get, and the new controller is 'set', the new controller |
|---|
| 1364 | | # should really be admin/set. |
|---|
| 1365 | | if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/ |
|---|
| 1366 | | old_parts = recall[:controller].split('/') |
|---|
| 1367 | | new_parts = options[:controller].split('/') |
|---|
| 1368 | | parts = old_parts[0..-(new_parts.length + 1)] + new_parts |
|---|
| 1369 | | options[:controller] = parts.join('/') |
|---|
| 1370 | | end |
|---|
| 1371 | | |
|---|
| 1372 | | # drop the leading '/' on the controller name |
|---|
| 1373 | | options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/ |
|---|
| 1374 | | merged = recall.merge(options) |
|---|
| 1375 | | |
|---|
| 1376 | | if named_route |
|---|
| 1377 | | path = named_route.generate(options, merged, expire_on) |
|---|
| 1378 | | if path.nil? |
|---|
| 1379 | | raise_named_route_error(options, named_route, named_route_name) |
|---|
| 1380 | | else |
|---|
| 1381 | | return path |
|---|
| 1382 | | end |
|---|
| 1383 | | else |
|---|
| 1384 | | merged[:action] ||= 'index' |
|---|
| 1385 | | options[:action] ||= 'index' |
|---|
| 1386 | | |
|---|
| 1387 | | controller = merged[:controller] |
|---|
| 1388 | | action = merged[:action] |
|---|
| 1389 | | |
|---|
| 1390 | | raise RoutingError, "Need controller and action!" unless controller && action |
|---|
| 1391 | | |
|---|
| 1392 | | if generate_all |
|---|
| 1393 | | # Used by caching to expire all paths for a resource |
|---|
| 1394 | | return routes.collect do |route| |
|---|
| 1395 | | route.send!(method, options, merged, expire_on) |
|---|
| 1396 | | end.compact |
|---|
| 1397 | | end |
|---|
| 1398 | | |
|---|
| 1399 | | # don't use the recalled keys when determining which routes to check |
|---|
| 1400 | | routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }] |
|---|
| 1401 | | |
|---|
| 1402 | | routes.each do |route| |
|---|
| 1403 | | results = route.send!(method, options, merged, expire_on) |
|---|
| 1404 | | return results if results && (!results.is_a?(Array) || results.first) |
|---|
| 1405 | | end |
|---|
| 1406 | | end |
|---|
| 1407 | | |
|---|
| 1408 | | raise RoutingError, "No route matches #{options.inspect}" |
|---|
| 1409 | | end |
|---|
| 1410 | | |
|---|
| 1411 | | # try to give a helpful error message when named route generation fails |
|---|
| 1412 | | def raise_named_route_error(options, named_route, named_route_name) |
|---|
| 1413 | | diff = named_route.requirements.diff(options) |
|---|
| 1414 | | unless diff.empty? |
|---|
| 1415 | | raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}" |
|---|
| 1416 | | else |
|---|
| 1417 | | required_segments = named_route.segments.select {|seg| (!seg.optional?) && (!seg.is_a?(DividerSegment)) } |
|---|
| 1418 | | required_keys_or_values = required_segments.map { |seg| seg.key rescue seg.value } # we want either the key or the value from the segment |
|---|
| 1419 | | raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect} - you may have ambiguous routes, or you may need to supply additional parameters for this route. content_url has the following required parameters: #{required_keys_or_values.inspect} - are they all satisfied?" |
|---|
| 1420 | | end |
|---|
| 1421 | | end |
|---|
| 1422 | | |
|---|
| 1423 | | def recognize(request) |
|---|
| 1424 | | params = recognize_path(request.path, extract_request_environment(request)) |
|---|
| 1425 | | request.path_parameters = params.with_indifferent_access |
|---|
| 1426 | | "#{params[:controller].camelize}Controller".constantize |
|---|
| 1427 | | end |
|---|
| 1428 | | |
|---|
| 1429 | | def recognize_path(path, environment={}) |
|---|
| 1430 | | routes.each do |route| |
|---|
| 1431 | | result = route.recognize(path, environment) and return result |
|---|
| 1432 | | end |
|---|
| 1433 | | |
|---|
| 1434 | | allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } } |
|---|
| 1435 | | |
|---|
| 1436 | | if environment[:method] && !HTTP_METHODS.include?(environment[:method]) |
|---|
| 1437 | | raise NotImplemented.new(*allows) |
|---|
| 1438 | | elsif !allows.empty? |
|---|
| 1439 | | raise MethodNotAllowed.new(*allows) |
|---|
| 1440 | | else |
|---|
| 1441 | | raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}" |
|---|
| 1442 | | end |
|---|
| 1443 | | end |
|---|
| 1444 | | |
|---|
| 1445 | | def routes_by_controller |
|---|
| 1446 | | @routes_by_controller ||= Hash.new do |controller_hash, controller| |
|---|
| 1447 | | controller_hash[controller] = Hash.new do |action_hash, action| |
|---|
| 1448 | | action_hash[action] = Hash.new do |key_hash, keys| |
|---|
| 1449 | | key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys) |
|---|
| 1450 | | end |
|---|
| 1451 | | end |
|---|
| 1452 | | end |
|---|
| 1453 | | end |
|---|
| 1454 | | |
|---|
| 1455 | | def routes_for(options, merged, expire_on) |
|---|
| 1456 | | raise "Need controller and action!" unless controller && action |
|---|
| 1457 | | controller = merged[:controller] |
|---|
| 1458 | | merged = options if expire_on[:controller] |
|---|
| 1459 | | action = merged[:action] || 'index' |
|---|
| 1460 | | |
|---|
| 1461 | | routes_by_controller[controller][action][merged.keys] |
|---|
| 1462 | | end |
|---|
| 1463 | | |
|---|
| 1464 | | def routes_for_controller_and_action(controller, action) |
|---|
| 1465 | | selected = routes.select do |route| |
|---|
| 1466 | | route.matches_controller_and_action? controller, action |
|---|
| 1467 | | end |
|---|
| 1468 | | (selected.length == routes.length) ? routes : selected |
|---|
| 1469 | | end |
|---|
| 1470 | | |
|---|
| 1471 | | def routes_for_controller_and_action_and_keys(controller, action, keys) |
|---|
| 1472 | | selected = routes.select do |route| |
|---|
| 1473 | | route.matches_controller_and_action? controller, action |
|---|
| 1474 | | end |
|---|
| 1475 | | selected.sort_by do |route| |
|---|
| 1476 | | (keys - route.significant_keys).length |
|---|
| 1477 | | end |
|---|
| 1478 | | end |
|---|
| 1479 | | |
|---|
| 1480 | | # Subclasses and plugins may override this method to extract further attributes |
|---|
| 1481 | | # from the request, for use by route conditions and such. |
|---|
| 1482 | | def extract_request_environment(request) |
|---|
| 1483 | | { :method => request.method } |
|---|
| 1484 | | end |
|---|
| 1485 | | end |
|---|
| | 313 | |
|---|