aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/routing/segments.rb
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_controller/routing/segments.rb')
-rw-r--r--actionpack/lib/action_controller/routing/segments.rb268
1 files changed, 268 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb
new file mode 100644
index 0000000000..efac2df1cb
--- /dev/null
+++ b/actionpack/lib/action_controller/routing/segments.rb
@@ -0,0 +1,268 @@
+module ActionController
+ module Routing
+ class Segment #:nodoc:
+ RESERVED_PCHAR = ':@&=+$,;'
+ UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze
+
+ attr_accessor :is_optional
+ alias_method :optional?, :is_optional
+
+ def initialize
+ self.is_optional = false
+ end
+
+ 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:
+ attr_accessor :value, :raw
+ alias_method :raw?, :raw
+
+ def initialize(value = nil)
+ super()
+ self.value = value
+ end
+
+ def interpolation_chunk
+ raw? ? value : super
+ end
+
+ def regexp_chunk
+ chunk = Regexp.escape(value)
+ optional? ? Regexp.optionalize(chunk) : chunk
+ end
+
+ 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:
+ def initialize(value = nil)
+ super(value)
+ self.raw = true
+ self.is_optional = true
+ end
+
+ def optionality_implied?
+ true
+ end
+ end
+
+ class DynamicSegment < Segment #:nodoc:
+ attr_accessor :key, :default, :regexp
+
+ def initialize(key = nil, options = {})
+ super()
+ self.key = key
+ self.default = options[:default] if options.key? :default
+ self.is_optional = true if options[:optional] || options.key?(:default)
+ 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
+ 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
+ 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
+
+ def interpolation_chunk(value_code = "#{local_name}")
+ "\#{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
+ Regexp.new "\\A#{regexp.source}\\Z" if regexp
+ end
+ def regexp_chunk
+ regexp ? "(#{regexp.source})" : "([^#{Routing::SEPARATORS.join}]+)"
+ end
+
+ def build_pattern(pattern)
+ chunk = regexp_chunk
+ chunk = "(#{chunk})" if Regexp.new(chunk).number_of_captures == 0
+ pattern = "#{chunk}#{pattern}"
+ optional? ? Regexp.optionalize(pattern) : pattern
+ end
+ 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
+
+ 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
+
+ # Don't URI.escape the controller name since it may contain slashes.
+ def interpolation_chunk(value_code = "#{local_name}")
+ "\#{#{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:
+ RESERVED_PCHAR = "#{Segment::RESERVED_PCHAR}/"
+ UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze
+
+ def interpolation_chunk(value_code = "#{local_name}")
+ "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::PathSegment::UNSAFE_PCHAR)}"
+ 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
+
+ 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
+ end
+end