~michaelforrest/use-case-mapper/trunk

« back to all changes in this revision

Viewing changes to vendor/rails/actionpack/lib/action_controller/routing/builder.rb

  • Committer: Michael Forrest
  • Date: 2010-10-15 16:28:50 UTC
  • Revision ID: michael.forrest@canonical.com-20101015162850-tj2vchanv0kr0dun
refrozeĀ gems

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
module ActionController
 
2
  module Routing
 
3
    class RouteBuilder #:nodoc:
 
4
      attr_reader :separators, :optional_separators
 
5
      attr_reader :separator_regexp, :nonseparator_regexp, :interval_regexp
 
6
 
 
7
      def initialize
 
8
        @separators = Routing::SEPARATORS
 
9
        @optional_separators = %w( / )
 
10
 
 
11
        @separator_regexp = /[#{Regexp.escape(separators.join)}]/
 
12
        @nonseparator_regexp = /\A([^#{Regexp.escape(separators.join)}]+)/
 
13
        @interval_regexp = /(.*?)(#{separator_regexp}|$)/
 
14
      end
 
15
 
 
16
      # Accepts a "route path" (a string defining a route), and returns the array
 
17
      # of segments that corresponds to it. Note that the segment array is only
 
18
      # partially initialized--the defaults and requirements, for instance, need
 
19
      # to be set separately, via the +assign_route_options+ method, and the
 
20
      # <tt>optional?</tt> method for each segment will not be reliable until after
 
21
      # +assign_route_options+ is called, as well.
 
22
      def segments_for_route_path(path)
 
23
        rest, segments = path, []
 
24
 
 
25
        until rest.empty?
 
26
          segment, rest = segment_for(rest)
 
27
          segments << segment
 
28
        end
 
29
        segments
 
30
      end
 
31
 
 
32
      # A factory method that returns a new segment instance appropriate for the
 
33
      # format of the given string.
 
34
      def segment_for(string)
 
35
        segment =
 
36
          case string
 
37
            when  /\A\.(:format)?\// 
 
38
              OptionalFormatSegment.new
 
39
            when /\A:(\w+)/
 
40
              key = $1.to_sym
 
41
              key == :controller ? ControllerSegment.new(key) : DynamicSegment.new(key)
 
42
            when /\A\*(\w+)/
 
43
              PathSegment.new($1.to_sym, :optional => true)
 
44
            when /\A\?(.*?)\?/
 
45
              StaticSegment.new($1, :optional => true)
 
46
            when nonseparator_regexp
 
47
              StaticSegment.new($1)
 
48
            when separator_regexp
 
49
              DividerSegment.new($&, :optional => optional_separators.include?($&))
 
50
          end
 
51
        [segment, $~.post_match]
 
52
      end
 
53
 
 
54
      # Split the given hash of options into requirement and default hashes. The
 
55
      # segments are passed alongside in order to distinguish between default values
 
56
      # and requirements.
 
57
      def divide_route_options(segments, options)
 
58
        options = options.except(:path_prefix, :name_prefix)
 
59
 
 
60
        if options[:namespace]
 
61
          options[:controller] = "#{options.delete(:namespace).sub(/\/$/, '')}/#{options[:controller]}"
 
62
        end
 
63
 
 
64
        requirements = (options.delete(:requirements) || {}).dup
 
65
        defaults     = (options.delete(:defaults)     || {}).dup
 
66
        conditions   = (options.delete(:conditions)   || {}).dup
 
67
 
 
68
        validate_route_conditions(conditions)
 
69
 
 
70
        path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact
 
71
        options.each do |key, value|
 
72
          hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements
 
73
          hash[key] = value
 
74
        end
 
75
 
 
76
        [defaults, requirements, conditions]
 
77
      end
 
78
 
 
79
      # Takes a hash of defaults and a hash of requirements, and assigns them to
 
80
      # the segments. Any unused requirements (which do not correspond to a segment)
 
81
      # are returned as a hash.
 
82
      def assign_route_options(segments, defaults, requirements)
 
83
        route_requirements = {} # Requirements that do not belong to a segment
 
84
 
 
85
        segment_named = Proc.new do |key|
 
86
          segments.detect { |segment| segment.key == key if segment.respond_to?(:key) }
 
87
        end
 
88
 
 
89
        requirements.each do |key, requirement|
 
90
          segment = segment_named[key]
 
91
          if segment
 
92
            raise TypeError, "#{key}: requirements on a path segment must be regular expressions" unless requirement.is_a?(Regexp)
 
93
            if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
 
94
              raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
 
95
            end
 
96
            if requirement.multiline?
 
97
              raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
 
98
            end
 
99
            segment.regexp = requirement
 
100
          else
 
101
            route_requirements[key] = requirement
 
102
          end
 
103
        end
 
104
 
 
105
        defaults.each do |key, default|
 
106
          segment = segment_named[key]
 
107
          raise ArgumentError, "#{key}: No matching segment exists; cannot assign default" unless segment
 
108
          segment.is_optional = true
 
109
          segment.default = default.to_param if default
 
110
        end
 
111
 
 
112
        assign_default_route_options(segments)
 
113
        ensure_required_segments(segments)
 
114
        route_requirements
 
115
      end
 
116
 
 
117
      # Assign default options, such as 'index' as a default for <tt>:action</tt>. This
 
118
      # method must be run *after* user supplied requirements and defaults have
 
119
      # been applied to the segments.
 
120
      def assign_default_route_options(segments)
 
121
        segments.each do |segment|
 
122
          next unless segment.is_a? DynamicSegment
 
123
          case segment.key
 
124
            when :action
 
125
              if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index'
 
126
                segment.default ||= 'index'
 
127
                segment.is_optional = true
 
128
              end
 
129
            when :id
 
130
              if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ ''
 
131
                segment.is_optional = true
 
132
              end
 
133
          end
 
134
        end
 
135
      end
 
136
 
 
137
      # Makes sure that there are no optional segments that precede a required
 
138
      # segment. If any are found that precede a required segment, they are
 
139
      # made required.
 
140
      def ensure_required_segments(segments)
 
141
        allow_optional = true
 
142
        segments.reverse_each do |segment|
 
143
          allow_optional &&= segment.optional?
 
144
          if !allow_optional && segment.optional?
 
145
            unless segment.optionality_implied?
 
146
              warn "Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required."
 
147
            end
 
148
            segment.is_optional = false
 
149
          elsif allow_optional && segment.respond_to?(:default) && segment.default
 
150
            # if a segment has a default, then it is optional
 
151
            segment.is_optional = true
 
152
          end
 
153
        end
 
154
      end
 
155
 
 
156
      # Construct and return a route with the given path and options.
 
157
      def build(path, options)
 
158
        # Wrap the path with slashes
 
159
        path = "/#{path}" unless path[0] == ?/
 
160
        path = "#{path}/" unless path[-1] == ?/
 
161
 
 
162
        prefix = options[:path_prefix].to_s.gsub(/^\//,'')
 
163
        path = "/#{prefix}#{path}" unless prefix.blank?
 
164
 
 
165
        segments = segments_for_route_path(path)
 
166
        defaults, requirements, conditions = divide_route_options(segments, options)
 
167
        requirements = assign_route_options(segments, defaults, requirements)
 
168
 
 
169
        # TODO: Segments should be frozen on initialize
 
170
        segments.each { |segment| segment.freeze }
 
171
 
 
172
        route = Route.new(segments, requirements, conditions)
 
173
 
 
174
        if !route.significant_keys.include?(:controller)
 
175
          raise ArgumentError, "Illegal route: the :controller must be specified!"
 
176
        end
 
177
 
 
178
        route.freeze
 
179
      end
 
180
 
 
181
      private
 
182
        def validate_route_conditions(conditions)
 
183
          if method = conditions[:method]
 
184
            [method].flatten.each do |m|
 
185
              if m == :head
 
186
                raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers"
 
187
              end
 
188
 
 
189
              unless HTTP_METHODS.include?(m.to_sym)
 
190
                raise ArgumentError, "Invalid HTTP method specified in route conditions: #{conditions.inspect}"
 
191
              end
 
192
            end
 
193
          end
 
194
        end
 
195
    end
 
196
  end
 
197
end