segments.rb 9.9 KB
Newer Older
1 2 3 4
module ActionController
  module Routing
    class Segment #:nodoc:
      RESERVED_PCHAR = ':@&=+$,;'
5 6
      SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
      UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
7

8
      # TODO: Convert :is_optional accessor to read only
9 10 11 12
      attr_accessor :is_optional
      alias_method :optional?, :is_optional

      def initialize
13
        @is_optional = false
14 15
      end

16 17 18 19
      def number_of_captures
        Regexp.new(regexp_chunk).number_of_captures
      end

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
      def extraction_code
        nil
      end

      # Continue generating string for the prior segments.
      def continue_string_structure(prior_segments)
        if prior_segments.empty?
          interpolation_statement(prior_segments)
        else
          new_priors = prior_segments[0..-2]
          prior_segments.last.string_structure(new_priors)
        end
      end

      def interpolation_chunk
        URI.escape(value, UNSAFE_PCHAR)
      end

      # Return a string interpolation statement for this segment and those before it.
      def interpolation_statement(prior_segments)
        chunks = prior_segments.collect { |s| s.interpolation_chunk }
        chunks << interpolation_chunk
        "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}"
      end

      def string_structure(prior_segments)
        optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments)
      end

      # Return an if condition that is true if all the prior segments can be generated.
      # If there are no optional segments before this one, then nil is returned.
      def all_optionals_available_condition(prior_segments)
        optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact
        optional_locals.empty? ? nil : " if #{optional_locals * ' && '}"
      end

      # Recognition

      def match_extraction(next_capture)
        nil
      end

      # Warning

      # Returns true if this segment is optional? because of a default. If so, then
      # no warning will be emitted regarding this segment.
      def optionality_implied?
        false
      end
    end

    class StaticSegment < Segment #:nodoc:
72
      attr_reader :value, :raw
73 74
      alias_method :raw?, :raw

75
      def initialize(value = nil, options = {})
76
        super()
77 78 79
        @value = value
        @raw = options[:raw] if options.key?(:raw)
        @is_optional = options[:optional] if options.key?(:optional)
80 81 82 83 84 85 86 87 88 89 90
      end

      def interpolation_chunk
        raw? ? value : super
      end

      def regexp_chunk
        chunk = Regexp.escape(value)
        optional? ? Regexp.optionalize(chunk) : chunk
      end

91 92 93 94
      def number_of_captures
        0
      end

95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
      def build_pattern(pattern)
        escaped = Regexp.escape(value)
        if optional? && ! pattern.empty?
          "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})"
        elsif optional?
          Regexp.optionalize escaped
        else
          escaped + pattern
        end
      end

      def to_s
        value
      end
    end

    class DividerSegment < StaticSegment #:nodoc:
112 113
      def initialize(value = nil, options = {})
        super(value, {:raw => true, :optional => true}.merge(options))
114 115 116 117 118 119 120 121
      end

      def optionality_implied?
        true
      end
    end

    class DynamicSegment < Segment #:nodoc:
122 123 124 125
      attr_reader :key

      # TODO: Convert these accessors to read only
      attr_accessor :default, :regexp
126 127 128

      def initialize(key = nil, options = {})
        super()
129 130 131 132
        @key = key
        @default = options[:default] if options.key?(:default)
        @regexp = options[:regexp] if options.key?(:regexp)
        @is_optional = true if options[:optional] || options.key?(:default)
133 134 135 136 137 138 139 140 141 142 143 144 145 146
      end

      def to_s
        ":#{key}"
      end

      # The local variable name that the value of this segment will be extracted to.
      def local_name
        "#{key}_value"
      end

      def extract_value
        "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}"
      end
J
Joshua Peek 已提交
147

148 149 150 151 152 153 154 155 156 157 158
      def value_check
        if default # Then we know it won't be nil
          "#{value_regexp.inspect} =~ #{local_name}" if regexp
        elsif optional?
          # If we have a regexp check that the value is not given, or that it matches.
          # If we have no regexp, return nil since we do not require a condition.
          "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp
        else # Then it must be present, and if we have a regexp, it must match too.
          "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}"
        end
      end
J
Joshua Peek 已提交
159

160 161 162 163 164 165 166 167 168 169 170
      def expiry_statement
        "expired, hash = true, options if !expired && expire_on[:#{key}]"
      end

      def extraction_code
        s = extract_value
        vc = value_check
        s << "\nreturn [nil,nil] unless #{vc}" if vc
        s << "\n#{expiry_statement}"
      end

171
      def interpolation_chunk(value_code = local_name)
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
        "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}"
      end

      def string_structure(prior_segments)
        if optional? # We have a conditional to do...
          # If we should not appear in the url, just write the code for the prior
          # segments. This occurs if our value is the default value, or, if we are
          # optional, if we have nil as our value.
          "if #{local_name} == #{default.inspect}\n" +
            continue_string_structure(prior_segments) +
          "\nelse\n" + # Otherwise, write the code up to here
            "#{interpolation_statement(prior_segments)}\nend"
        else
          interpolation_statement(prior_segments)
        end
      end

      def value_regexp
190
        Regexp.new "\\A#{regexp.to_s}\\Z" if regexp
191
      end
192

193
      def regexp_chunk
J
Joshua Peek 已提交
194
        if regexp
195 196 197 198 199 200 201 202
          if regexp_has_modifiers?
            "(#{regexp.to_s})"
          else
            "(#{regexp.source})"
          end
        else
          "([^#{Routing::SEPARATORS.join}]+)"
        end
203 204
      end

205 206 207 208 209 210 211 212
      def number_of_captures
        if regexp
          regexp.number_of_captures + 1
        else
          1
        end
      end

213
      def build_pattern(pattern)
214
        pattern = "#{regexp_chunk}#{pattern}"
215 216
        optional? ? Regexp.optionalize(pattern) : pattern
      end
217

218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
      def match_extraction(next_capture)
        # All non code-related keys (such as :id, :slug) are URI-unescaped as
        # path parameters.
        default_value = default ? default.inspect : nil
        %[
          value = if (m = match[#{next_capture}])
            URI.unescape(m)
          else
            #{default_value}
          end
          params[:#{key}] = value if value
        ]
      end

      def optionality_implied?
        [:action, :id].include? key
      end

236 237 238
      def regexp_has_modifiers?
        regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0
      end
239 240 241 242 243 244 245 246
    end

    class ControllerSegment < DynamicSegment #:nodoc:
      def regexp_chunk
        possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name }
        "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))"
      end

247 248 249 250
      def number_of_captures
        1
      end

251
      # Don't URI.escape the controller name since it may contain slashes.
252
      def interpolation_chunk(value_code = local_name)
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
        "\#{#{value_code}.to_s}"
      end

      # Make sure controller names like Admin/Content are correctly normalized to
      # admin/content
      def extract_value
        "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase"
      end

      def match_extraction(next_capture)
        if default
          "params[:#{key}] = match[#{next_capture}] ? match[#{next_capture}].downcase : '#{default}'"
        else
          "params[:#{key}] = match[#{next_capture}].downcase if match[#{next_capture}]"
        end
      end
    end

    class PathSegment < DynamicSegment #:nodoc:
272
      def interpolation_chunk(value_code = local_name)
273 274 275 276
        "\#{#{value_code}}"
      end

      def extract_value
277
        "#{local_name} = hash[:#{key}] && Array(hash[:#{key}]).collect { |path_component| URI.escape(path_component.to_param, ActionController::Routing::Segment::UNSAFE_PCHAR) }.to_param #{"|| #{default.inspect}" if default}"
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
      end

      def default
        ''
      end

      def default=(path)
        raise RoutingError, "paths cannot have non-empty default values" unless path.blank?
      end

      def match_extraction(next_capture)
        "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}"
      end

      def regexp_chunk
        regexp || "(.*)"
      end

296 297 298 299
      def number_of_captures
        regexp ? regexp.number_of_captures : 1
      end

300 301 302 303 304 305 306 307 308 309 310
      def optionality_implied?
        true
      end

      class Result < ::Array #:nodoc:
        def to_s() join '/' end
        def self.new_escaped(strings)
          new strings.collect {|str| URI.unescape str}
        end
      end
    end
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
    
    # The OptionalFormatSegment allows for any resource route to have an optional
    # :format, which decreases the amount of routes created by 50%.
    class OptionalFormatSegment < DynamicSegment
    
      def initialize(key = nil, options = {})
        super(:format, {:optional => true}.merge(options))            
      end
    
      def interpolation_chunk
        "." + super
      end
    
      def regexp_chunk
        '(\.[^/?\.]+)?'
      end
    
      def to_s
        '(.:format)?'
      end
    
      #the value should not include the period (.)
      def match_extraction(next_capture)
        %[
          if (m = match[#{next_capture}])
            params[:#{key}] = URI.unescape(m.from(1))
          end
        ]
      end
    end
    
342 343
  end
end