mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Prepare Route#generate and Route#recognize early. Also refactor segments a bit to try to make immutable.
This commit is contained in:
parent
e0513e33c4
commit
a5db148825
8 changed files with 259 additions and 298 deletions
|
@ -48,14 +48,10 @@ module ActionController
|
||||||
end
|
end
|
||||||
when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true)
|
when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true)
|
||||||
when /\A\?(.*?)\?/
|
when /\A\?(.*?)\?/
|
||||||
returning segment = StaticSegment.new($1) do
|
StaticSegment.new($1, :optional => true)
|
||||||
segment.is_optional = true
|
|
||||||
end
|
|
||||||
when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1)
|
when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1)
|
||||||
when Regexp.new(separator_pattern) then
|
when Regexp.new(separator_pattern) then
|
||||||
returning segment = DividerSegment.new($&) do
|
DividerSegment.new($&, :optional => (optional_separators.include? $&))
|
||||||
segment.is_optional = (optional_separators.include? $&)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
[segment, $~.post_match]
|
[segment, $~.post_match]
|
||||||
end
|
end
|
||||||
|
@ -176,29 +172,16 @@ module ActionController
|
||||||
defaults, requirements, conditions = divide_route_options(segments, options)
|
defaults, requirements, conditions = divide_route_options(segments, options)
|
||||||
requirements = assign_route_options(segments, defaults, requirements)
|
requirements = assign_route_options(segments, defaults, requirements)
|
||||||
|
|
||||||
route = Route.new
|
# TODO: Segments should be frozen on initialize
|
||||||
|
segments.each { |segment| segment.freeze }
|
||||||
|
|
||||||
route.segments = segments
|
route = Route.new(segments, requirements, conditions)
|
||||||
route.requirements = requirements
|
|
||||||
route.conditions = conditions
|
|
||||||
|
|
||||||
if !route.significant_keys.include?(:action) && !route.requirements[:action]
|
|
||||||
route.requirements[:action] = "index"
|
|
||||||
route.significant_keys << :action
|
|
||||||
end
|
|
||||||
|
|
||||||
# Routes cannot use the current string interpolation method
|
|
||||||
# if there are user-supplied <tt>:requirements</tt> as the interpolation
|
|
||||||
# code won't raise RoutingErrors when generating
|
|
||||||
if options.key?(:requirements) || route.requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION
|
|
||||||
route.optimise = false
|
|
||||||
end
|
|
||||||
|
|
||||||
if !route.significant_keys.include?(:controller)
|
if !route.significant_keys.include?(:controller)
|
||||||
raise ArgumentError, "Illegal route: the :controller must be specified!"
|
raise ArgumentError, "Illegal route: the :controller must be specified!"
|
||||||
end
|
end
|
||||||
|
|
||||||
route
|
route.freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -20,6 +20,7 @@ module ActionController
|
||||||
|
|
||||||
class Optimiser
|
class Optimiser
|
||||||
attr_reader :route, :kind
|
attr_reader :route, :kind
|
||||||
|
|
||||||
def initialize(route, kind)
|
def initialize(route, kind)
|
||||||
@route = route
|
@route = route
|
||||||
@kind = kind
|
@kind = kind
|
||||||
|
|
|
@ -67,28 +67,6 @@ module ActionController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def recognize_optimized(path, env)
|
|
||||||
write_recognize_optimized
|
|
||||||
recognize_optimized(path, env)
|
|
||||||
end
|
|
||||||
|
|
||||||
def write_recognize_optimized
|
|
||||||
tree = segment_tree(routes)
|
|
||||||
body = generate_code(tree)
|
|
||||||
instance_eval %{
|
|
||||||
def recognize_optimized(path, env)
|
|
||||||
segments = to_plain_segments(path)
|
|
||||||
index = #{body}
|
|
||||||
return nil unless index
|
|
||||||
while index < routes.size
|
|
||||||
result = routes[index].recognize(path, env) and return result
|
|
||||||
index += 1
|
|
||||||
end
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
}, __FILE__, __LINE__
|
|
||||||
end
|
|
||||||
|
|
||||||
def segment_tree(routes)
|
def segment_tree(routes)
|
||||||
tree = [0]
|
tree = [0]
|
||||||
|
|
||||||
|
@ -151,6 +129,24 @@ module ActionController
|
||||||
segments << nil
|
segments << nil
|
||||||
segments
|
segments
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def write_recognize_optimized!
|
||||||
|
tree = segment_tree(routes)
|
||||||
|
body = generate_code(tree)
|
||||||
|
instance_eval %{
|
||||||
|
def recognize_optimized(path, env)
|
||||||
|
segments = to_plain_segments(path)
|
||||||
|
index = #{body}
|
||||||
|
return nil unless index
|
||||||
|
while index < routes.size
|
||||||
|
result = routes[index].recognize(path, env) and return result
|
||||||
|
index += 1
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
}, __FILE__, __LINE__
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,11 +3,25 @@ module ActionController
|
||||||
class Route #:nodoc:
|
class Route #:nodoc:
|
||||||
attr_accessor :segments, :requirements, :conditions, :optimise
|
attr_accessor :segments, :requirements, :conditions, :optimise
|
||||||
|
|
||||||
def initialize
|
def initialize(segments = [], requirements = {}, conditions = {})
|
||||||
@segments = []
|
@segments = segments
|
||||||
@requirements = {}
|
@requirements = requirements
|
||||||
@conditions = {}
|
@conditions = conditions
|
||||||
@optimise = true
|
|
||||||
|
if !significant_keys.include?(:action) && !requirements[:action]
|
||||||
|
@requirements[:action] = "index"
|
||||||
|
@significant_keys << :action
|
||||||
|
end
|
||||||
|
|
||||||
|
# Routes cannot use the current string interpolation method
|
||||||
|
# if there are user-supplied <tt>:requirements</tt> as the interpolation
|
||||||
|
# code won't raise RoutingErrors when generating
|
||||||
|
has_requirements = @segments.detect { |segment| segment.respond_to?(:regexp) && segment.regexp }
|
||||||
|
if has_requirements || @requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION
|
||||||
|
@optimise = false
|
||||||
|
else
|
||||||
|
@optimise = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Indicates whether the routes should be optimised with the string interpolation
|
# Indicates whether the routes should be optimised with the string interpolation
|
||||||
|
@ -22,129 +36,6 @@ module ActionController
|
||||||
end.compact
|
end.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
# Write and compile a +generate+ method for this Route.
|
|
||||||
def write_generation
|
|
||||||
# Build the main body of the generation
|
|
||||||
body = "expired = false\n#{generation_extraction}\n#{generation_structure}"
|
|
||||||
|
|
||||||
# If we have conditions that must be tested first, nest the body inside an if
|
|
||||||
body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
|
|
||||||
args = "options, hash, expire_on = {}"
|
|
||||||
|
|
||||||
# Nest the body inside of a def block, and then compile it.
|
|
||||||
raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
|
|
||||||
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
|
|
||||||
|
|
||||||
# expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
|
|
||||||
# are the same as the keys that were recalled from the previous request. Thus,
|
|
||||||
# we can use the expire_on.keys to determine which keys ought to be used to build
|
|
||||||
# the query string. (Never use keys from the recalled request when building the
|
|
||||||
# query string.)
|
|
||||||
|
|
||||||
method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"
|
|
||||||
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
|
|
||||||
|
|
||||||
method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"
|
|
||||||
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
|
|
||||||
raw_method
|
|
||||||
end
|
|
||||||
|
|
||||||
# Build several lines of code that extract values from the options hash. If any
|
|
||||||
# of the values are missing or rejected then a return will be executed.
|
|
||||||
def generation_extraction
|
|
||||||
segments.collect do |segment|
|
|
||||||
segment.extraction_code
|
|
||||||
end.compact * "\n"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Produce a condition expression that will check the requirements of this route
|
|
||||||
# upon generation.
|
|
||||||
def generation_requirements
|
|
||||||
requirement_conditions = requirements.collect do |key, req|
|
|
||||||
if req.is_a? Regexp
|
|
||||||
value_regexp = Regexp.new "\\A#{req.to_s}\\Z"
|
|
||||||
"hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
|
|
||||||
else
|
|
||||||
"hash[:#{key}] == #{req.inspect}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
requirement_conditions * ' && ' unless requirement_conditions.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
def generation_structure
|
|
||||||
segments.last.string_structure segments[0..-2]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Write and compile a +recognize+ method for this Route.
|
|
||||||
def write_recognition
|
|
||||||
# Create an if structure to extract the params from a match if it occurs.
|
|
||||||
body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
|
|
||||||
body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"
|
|
||||||
|
|
||||||
# Build the method declaration and compile it
|
|
||||||
method_decl = "def recognize(path, env={})\n#{body}\nend"
|
|
||||||
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
|
|
||||||
method_decl
|
|
||||||
end
|
|
||||||
|
|
||||||
# Plugins may override this method to add other conditions, like checks on
|
|
||||||
# host, subdomain, and so forth. Note that changes here only affect route
|
|
||||||
# recognition, not generation.
|
|
||||||
def recognition_conditions
|
|
||||||
result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
|
|
||||||
result << "conditions[:method] === env[:method]" if conditions[:method]
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
# Build the regular expression pattern that will match this route.
|
|
||||||
def recognition_pattern(wrap = true)
|
|
||||||
pattern = ''
|
|
||||||
segments.reverse_each do |segment|
|
|
||||||
pattern = segment.build_pattern pattern
|
|
||||||
end
|
|
||||||
wrap ? ("\\A" + pattern + "\\Z") : pattern
|
|
||||||
end
|
|
||||||
|
|
||||||
# Write the code to extract the parameters from a matched route.
|
|
||||||
def recognition_extraction
|
|
||||||
next_capture = 1
|
|
||||||
extraction = segments.collect do |segment|
|
|
||||||
x = segment.match_extraction(next_capture)
|
|
||||||
next_capture += Regexp.new(segment.regexp_chunk).number_of_captures
|
|
||||||
x
|
|
||||||
end
|
|
||||||
extraction.compact
|
|
||||||
end
|
|
||||||
|
|
||||||
# Write the real generation implementation and then resend the message.
|
|
||||||
def generate(options, hash, expire_on = {})
|
|
||||||
write_generation
|
|
||||||
generate options, hash, expire_on
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate_extras(options, hash, expire_on = {})
|
|
||||||
write_generation
|
|
||||||
generate_extras options, hash, expire_on
|
|
||||||
end
|
|
||||||
|
|
||||||
# Generate the query string with any extra keys in the hash and append
|
|
||||||
# it to the given path, returning the new path.
|
|
||||||
def append_query_string(path, hash, query_keys=nil)
|
|
||||||
return nil unless path
|
|
||||||
query_keys ||= extra_keys(hash)
|
|
||||||
"#{path}#{build_query_string(hash, query_keys)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Determine which keys in the given hash are "extra". Extra keys are
|
|
||||||
# those that were not used to generate a particular route. The extra
|
|
||||||
# keys also do not include those recalled from the prior request, nor
|
|
||||||
# do they include any keys that were implied in the route (like a
|
|
||||||
# <tt>:controller</tt> that is required, but not explicitly used in the
|
|
||||||
# text of the route.)
|
|
||||||
def extra_keys(hash, recall={})
|
|
||||||
(hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
|
|
||||||
end
|
|
||||||
|
|
||||||
# Build a query string from the keys of the given hash. If +only_keys+
|
# Build a query string from the keys of the given hash. If +only_keys+
|
||||||
# is given (as an array), only the keys indicated will be used to build
|
# is given (as an array), only the keys indicated will be used to build
|
||||||
# the query string. The query string will correctly build array parameter
|
# the query string. The query string will correctly build array parameter
|
||||||
|
@ -161,12 +52,6 @@ module ActionController
|
||||||
elements.empty? ? '' : "?#{elements.sort * '&'}"
|
elements.empty? ? '' : "?#{elements.sort * '&'}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Write the real recognition implementation and then resend the message.
|
|
||||||
def recognize(path, environment={})
|
|
||||||
write_recognition
|
|
||||||
recognize path, environment
|
|
||||||
end
|
|
||||||
|
|
||||||
# A route's parameter shell contains parameter values that are not in the
|
# A route's parameter shell contains parameter values that are not in the
|
||||||
# route's path, but should be placed in the recognized hash.
|
# route's path, but should be placed in the recognized hash.
|
||||||
#
|
#
|
||||||
|
@ -186,7 +71,7 @@ module ActionController
|
||||||
# includes keys that appear inside the path, and keys that have requirements
|
# includes keys that appear inside the path, and keys that have requirements
|
||||||
# placed upon them.
|
# placed upon them.
|
||||||
def significant_keys
|
def significant_keys
|
||||||
@significant_keys ||= returning [] do |sk|
|
@significant_keys ||= returning([]) do |sk|
|
||||||
segments.each { |segment| sk << segment.key if segment.respond_to? :key }
|
segments.each { |segment| sk << segment.key if segment.respond_to? :key }
|
||||||
sk.concat requirements.keys
|
sk.concat requirements.keys
|
||||||
sk.uniq!
|
sk.uniq!
|
||||||
|
@ -209,12 +94,7 @@ module ActionController
|
||||||
end
|
end
|
||||||
|
|
||||||
def matches_controller_and_action?(controller, action)
|
def matches_controller_and_action?(controller, action)
|
||||||
unless defined? @matching_prepared
|
prepare_matching!
|
||||||
@controller_requirement = requirement_for(:controller)
|
|
||||||
@action_requirement = requirement_for(:action)
|
|
||||||
@matching_prepared = true
|
|
||||||
end
|
|
||||||
|
|
||||||
(@controller_requirement.nil? || @controller_requirement === controller) &&
|
(@controller_requirement.nil? || @controller_requirement === controller) &&
|
||||||
(@action_requirement.nil? || @action_requirement === action)
|
(@action_requirement.nil? || @action_requirement === action)
|
||||||
end
|
end
|
||||||
|
@ -226,7 +106,23 @@ module ActionController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
# TODO: Route should be prepared and frozen on initialize
|
||||||
|
def freeze
|
||||||
|
unless frozen?
|
||||||
|
write_generation!
|
||||||
|
write_recognition!
|
||||||
|
prepare_matching!
|
||||||
|
|
||||||
|
parameter_shell
|
||||||
|
significant_keys
|
||||||
|
defaults
|
||||||
|
to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
def requirement_for(key)
|
def requirement_for(key)
|
||||||
return requirements[key] if requirements.key? key
|
return requirements[key] if requirements.key? key
|
||||||
segments.each do |segment|
|
segments.each do |segment|
|
||||||
|
@ -234,6 +130,126 @@ module ActionController
|
||||||
end
|
end
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Write and compile a +generate+ method for this Route.
|
||||||
|
def write_generation!
|
||||||
|
# Build the main body of the generation
|
||||||
|
body = "expired = false\n#{generation_extraction}\n#{generation_structure}"
|
||||||
|
|
||||||
|
# If we have conditions that must be tested first, nest the body inside an if
|
||||||
|
body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
|
||||||
|
args = "options, hash, expire_on = {}"
|
||||||
|
|
||||||
|
# Nest the body inside of a def block, and then compile it.
|
||||||
|
raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
|
||||||
|
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
|
||||||
|
|
||||||
|
# expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
|
||||||
|
# are the same as the keys that were recalled from the previous request. Thus,
|
||||||
|
# we can use the expire_on.keys to determine which keys ought to be used to build
|
||||||
|
# the query string. (Never use keys from the recalled request when building the
|
||||||
|
# query string.)
|
||||||
|
|
||||||
|
method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"
|
||||||
|
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
|
||||||
|
|
||||||
|
method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"
|
||||||
|
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
|
||||||
|
raw_method
|
||||||
|
end
|
||||||
|
|
||||||
|
# Build several lines of code that extract values from the options hash. If any
|
||||||
|
# of the values are missing or rejected then a return will be executed.
|
||||||
|
def generation_extraction
|
||||||
|
segments.collect do |segment|
|
||||||
|
segment.extraction_code
|
||||||
|
end.compact * "\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Produce a condition expression that will check the requirements of this route
|
||||||
|
# upon generation.
|
||||||
|
def generation_requirements
|
||||||
|
requirement_conditions = requirements.collect do |key, req|
|
||||||
|
if req.is_a? Regexp
|
||||||
|
value_regexp = Regexp.new "\\A#{req.to_s}\\Z"
|
||||||
|
"hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
|
||||||
|
else
|
||||||
|
"hash[:#{key}] == #{req.inspect}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
requirement_conditions * ' && ' unless requirement_conditions.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def generation_structure
|
||||||
|
segments.last.string_structure segments[0..-2]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Write and compile a +recognize+ method for this Route.
|
||||||
|
def write_recognition!
|
||||||
|
# Create an if structure to extract the params from a match if it occurs.
|
||||||
|
body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
|
||||||
|
body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"
|
||||||
|
|
||||||
|
# Build the method declaration and compile it
|
||||||
|
method_decl = "def recognize(path, env = {})\n#{body}\nend"
|
||||||
|
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
|
||||||
|
method_decl
|
||||||
|
end
|
||||||
|
|
||||||
|
# Plugins may override this method to add other conditions, like checks on
|
||||||
|
# host, subdomain, and so forth. Note that changes here only affect route
|
||||||
|
# recognition, not generation.
|
||||||
|
def recognition_conditions
|
||||||
|
result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
|
||||||
|
result << "conditions[:method] === env[:method]" if conditions[:method]
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
# Build the regular expression pattern that will match this route.
|
||||||
|
def recognition_pattern(wrap = true)
|
||||||
|
pattern = ''
|
||||||
|
segments.reverse_each do |segment|
|
||||||
|
pattern = segment.build_pattern pattern
|
||||||
|
end
|
||||||
|
wrap ? ("\\A" + pattern + "\\Z") : pattern
|
||||||
|
end
|
||||||
|
|
||||||
|
# Write the code to extract the parameters from a matched route.
|
||||||
|
def recognition_extraction
|
||||||
|
next_capture = 1
|
||||||
|
extraction = segments.collect do |segment|
|
||||||
|
x = segment.match_extraction(next_capture)
|
||||||
|
next_capture += Regexp.new(segment.regexp_chunk).number_of_captures
|
||||||
|
x
|
||||||
|
end
|
||||||
|
extraction.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate the query string with any extra keys in the hash and append
|
||||||
|
# it to the given path, returning the new path.
|
||||||
|
def append_query_string(path, hash, query_keys = nil)
|
||||||
|
return nil unless path
|
||||||
|
query_keys ||= extra_keys(hash)
|
||||||
|
"#{path}#{build_query_string(hash, query_keys)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Determine which keys in the given hash are "extra". Extra keys are
|
||||||
|
# those that were not used to generate a particular route. The extra
|
||||||
|
# keys also do not include those recalled from the prior request, nor
|
||||||
|
# do they include any keys that were implied in the route (like a
|
||||||
|
# <tt>:controller</tt> that is required, but not explicitly used in the
|
||||||
|
# text of the route.)
|
||||||
|
def extra_keys(hash, recall = {})
|
||||||
|
(hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_matching!
|
||||||
|
unless defined? @matching_prepared
|
||||||
|
@controller_requirement = requirement_for(:controller)
|
||||||
|
@action_requirement = requirement_for(:action)
|
||||||
|
@matching_prepared = true
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -194,6 +194,8 @@ module ActionController
|
||||||
def initialize
|
def initialize
|
||||||
self.routes = []
|
self.routes = []
|
||||||
self.named_routes = NamedRouteCollection.new
|
self.named_routes = NamedRouteCollection.new
|
||||||
|
|
||||||
|
write_recognize_optimized!
|
||||||
end
|
end
|
||||||
|
|
||||||
# Subclasses and plugins may override this method to specify a different
|
# Subclasses and plugins may override this method to specify a different
|
||||||
|
@ -231,7 +233,6 @@ module ActionController
|
||||||
Routing.use_controllers! nil # Clear the controller cache so we may discover new ones
|
Routing.use_controllers! nil # Clear the controller cache so we may discover new ones
|
||||||
clear!
|
clear!
|
||||||
load_routes!
|
load_routes!
|
||||||
install_helpers
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# reload! will always force a reload whereas load checks the timestamp first
|
# reload! will always force a reload whereas load checks the timestamp first
|
||||||
|
|
|
@ -4,11 +4,12 @@ module ActionController
|
||||||
RESERVED_PCHAR = ':@&=+$,;'
|
RESERVED_PCHAR = ':@&=+$,;'
|
||||||
UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze
|
UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze
|
||||||
|
|
||||||
|
# TODO: Convert :is_optional accessor to read only
|
||||||
attr_accessor :is_optional
|
attr_accessor :is_optional
|
||||||
alias_method :optional?, :is_optional
|
alias_method :optional?, :is_optional
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
self.is_optional = false
|
@is_optional = false
|
||||||
end
|
end
|
||||||
|
|
||||||
def extraction_code
|
def extraction_code
|
||||||
|
@ -63,12 +64,14 @@ module ActionController
|
||||||
end
|
end
|
||||||
|
|
||||||
class StaticSegment < Segment #:nodoc:
|
class StaticSegment < Segment #:nodoc:
|
||||||
attr_accessor :value, :raw
|
attr_reader :value, :raw
|
||||||
alias_method :raw?, :raw
|
alias_method :raw?, :raw
|
||||||
|
|
||||||
def initialize(value = nil)
|
def initialize(value = nil, options = {})
|
||||||
super()
|
super()
|
||||||
self.value = value
|
@value = value
|
||||||
|
@raw = options[:raw] if options.key?(:raw)
|
||||||
|
@is_optional = options[:optional] if options.key?(:optional)
|
||||||
end
|
end
|
||||||
|
|
||||||
def interpolation_chunk
|
def interpolation_chunk
|
||||||
|
@ -97,10 +100,8 @@ module ActionController
|
||||||
end
|
end
|
||||||
|
|
||||||
class DividerSegment < StaticSegment #:nodoc:
|
class DividerSegment < StaticSegment #:nodoc:
|
||||||
def initialize(value = nil)
|
def initialize(value = nil, options = {})
|
||||||
super(value)
|
super(value, {:raw => true, :optional => true}.merge(options))
|
||||||
self.raw = true
|
|
||||||
self.is_optional = true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def optionality_implied?
|
def optionality_implied?
|
||||||
|
@ -109,13 +110,17 @@ module ActionController
|
||||||
end
|
end
|
||||||
|
|
||||||
class DynamicSegment < Segment #:nodoc:
|
class DynamicSegment < Segment #:nodoc:
|
||||||
attr_accessor :key, :default, :regexp
|
attr_reader :key
|
||||||
|
|
||||||
|
# TODO: Convert these accessors to read only
|
||||||
|
attr_accessor :default, :regexp
|
||||||
|
|
||||||
def initialize(key = nil, options = {})
|
def initialize(key = nil, options = {})
|
||||||
super()
|
super()
|
||||||
self.key = key
|
@key = key
|
||||||
self.default = options[:default] if options.key? :default
|
@default = options[:default] if options.key?(:default)
|
||||||
self.is_optional = true if options[:optional] || options.key?(:default)
|
@regexp = options[:regexp] if options.key?(:regexp)
|
||||||
|
@is_optional = true if options[:optional] || options.key?(:default)
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
|
|
|
@ -67,66 +67,56 @@ class SegmentTest < Test::Unit::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_interpolation_statement
|
def test_interpolation_statement
|
||||||
s = ROUTING::StaticSegment.new
|
s = ROUTING::StaticSegment.new("Hello")
|
||||||
s.value = "Hello"
|
|
||||||
assert_equal "Hello", eval(s.interpolation_statement([]))
|
assert_equal "Hello", eval(s.interpolation_statement([]))
|
||||||
assert_equal "HelloHello", eval(s.interpolation_statement([s]))
|
assert_equal "HelloHello", eval(s.interpolation_statement([s]))
|
||||||
|
|
||||||
s2 = ROUTING::StaticSegment.new
|
s2 = ROUTING::StaticSegment.new("-")
|
||||||
s2.value = "-"
|
|
||||||
assert_equal "Hello-Hello", eval(s.interpolation_statement([s, s2]))
|
assert_equal "Hello-Hello", eval(s.interpolation_statement([s, s2]))
|
||||||
|
|
||||||
s3 = ROUTING::StaticSegment.new
|
s3 = ROUTING::StaticSegment.new("World")
|
||||||
s3.value = "World"
|
|
||||||
assert_equal "Hello-World", eval(s3.interpolation_statement([s, s2]))
|
assert_equal "Hello-World", eval(s3.interpolation_statement([s, s2]))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class StaticSegmentTest < Test::Unit::TestCase
|
class StaticSegmentTest < Test::Unit::TestCase
|
||||||
def test_interpolation_chunk_should_respect_raw
|
def test_interpolation_chunk_should_respect_raw
|
||||||
s = ROUTING::StaticSegment.new
|
s = ROUTING::StaticSegment.new('Hello World')
|
||||||
s.value = 'Hello World'
|
assert !s.raw?
|
||||||
assert ! s.raw?
|
|
||||||
assert_equal 'Hello%20World', s.interpolation_chunk
|
assert_equal 'Hello%20World', s.interpolation_chunk
|
||||||
|
|
||||||
s.raw = true
|
s = ROUTING::StaticSegment.new('Hello World', :raw => true)
|
||||||
assert s.raw?
|
assert s.raw?
|
||||||
assert_equal 'Hello World', s.interpolation_chunk
|
assert_equal 'Hello World', s.interpolation_chunk
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_regexp_chunk_should_escape_specials
|
def test_regexp_chunk_should_escape_specials
|
||||||
s = ROUTING::StaticSegment.new
|
s = ROUTING::StaticSegment.new('Hello*World')
|
||||||
|
|
||||||
s.value = 'Hello*World'
|
|
||||||
assert_equal 'Hello\*World', s.regexp_chunk
|
assert_equal 'Hello\*World', s.regexp_chunk
|
||||||
|
|
||||||
s.value = 'HelloWorld'
|
s = ROUTING::StaticSegment.new('HelloWorld')
|
||||||
assert_equal 'HelloWorld', s.regexp_chunk
|
assert_equal 'HelloWorld', s.regexp_chunk
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_regexp_chunk_should_add_question_mark_for_optionals
|
def test_regexp_chunk_should_add_question_mark_for_optionals
|
||||||
s = ROUTING::StaticSegment.new
|
s = ROUTING::StaticSegment.new("/", :optional => true)
|
||||||
s.value = "/"
|
|
||||||
s.is_optional = true
|
|
||||||
assert_equal "/?", s.regexp_chunk
|
assert_equal "/?", s.regexp_chunk
|
||||||
|
|
||||||
s.value = "hello"
|
s = ROUTING::StaticSegment.new("hello", :optional => true)
|
||||||
assert_equal "(?:hello)?", s.regexp_chunk
|
assert_equal "(?:hello)?", s.regexp_chunk
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class DynamicSegmentTest < Test::Unit::TestCase
|
class DynamicSegmentTest < Test::Unit::TestCase
|
||||||
def segment
|
def segment(options = {})
|
||||||
unless @segment
|
unless @segment
|
||||||
@segment = ROUTING::DynamicSegment.new
|
@segment = ROUTING::DynamicSegment.new(:a, options)
|
||||||
@segment.key = :a
|
|
||||||
end
|
end
|
||||||
@segment
|
@segment
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_extract_value
|
def test_extract_value
|
||||||
s = ROUTING::DynamicSegment.new
|
s = ROUTING::DynamicSegment.new(:a)
|
||||||
s.key = :a
|
|
||||||
|
|
||||||
hash = {:a => '10', :b => '20'}
|
hash = {:a => '10', :b => '20'}
|
||||||
assert_equal '10', eval(s.extract_value)
|
assert_equal '10', eval(s.extract_value)
|
||||||
|
@ -149,31 +139,31 @@ class DynamicSegmentTest < Test::Unit::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_regexp_value_check_rejects_nil
|
def test_regexp_value_check_rejects_nil
|
||||||
segment.regexp = /\d+/
|
segment = segment(:regexp => /\d+/)
|
||||||
|
|
||||||
a_value = nil
|
a_value = nil
|
||||||
assert ! eval(segment.value_check)
|
assert !eval(segment.value_check)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_optional_regexp_value_check_should_accept_nil
|
def test_optional_regexp_value_check_should_accept_nil
|
||||||
segment.regexp = /\d+/
|
segment = segment(:regexp => /\d+/, :optional => true)
|
||||||
segment.is_optional = true
|
|
||||||
a_value = nil
|
a_value = nil
|
||||||
assert eval(segment.value_check)
|
assert eval(segment.value_check)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_regexp_value_check_rejects_no_match
|
def test_regexp_value_check_rejects_no_match
|
||||||
segment.regexp = /\d+/
|
segment = segment(:regexp => /\d+/)
|
||||||
|
|
||||||
a_value = "Hello20World"
|
a_value = "Hello20World"
|
||||||
assert ! eval(segment.value_check)
|
assert !eval(segment.value_check)
|
||||||
|
|
||||||
a_value = "20Hi"
|
a_value = "20Hi"
|
||||||
assert ! eval(segment.value_check)
|
assert !eval(segment.value_check)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_regexp_value_check_accepts_match
|
def test_regexp_value_check_accepts_match
|
||||||
segment.regexp = /\d+/
|
segment = segment(:regexp => /\d+/)
|
||||||
|
|
||||||
a_value = "30"
|
a_value = "30"
|
||||||
assert eval(segment.value_check)
|
assert eval(segment.value_check)
|
||||||
end
|
end
|
||||||
|
@ -184,14 +174,14 @@ class DynamicSegmentTest < Test::Unit::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_optional_value_needs_no_check
|
def test_optional_value_needs_no_check
|
||||||
segment.is_optional = true
|
segment = segment(:optional => true)
|
||||||
|
|
||||||
a_value = nil
|
a_value = nil
|
||||||
assert_equal nil, segment.value_check
|
assert_equal nil, segment.value_check
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_regexp_value_check_should_accept_match_with_default
|
def test_regexp_value_check_should_accept_match_with_default
|
||||||
segment.regexp = /\d+/
|
segment = segment(:regexp => /\d+/, :default => '200')
|
||||||
segment.default = '200'
|
|
||||||
|
|
||||||
a_value = '100'
|
a_value = '100'
|
||||||
assert eval(segment.value_check)
|
assert eval(segment.value_check)
|
||||||
|
@ -234,7 +224,7 @@ class DynamicSegmentTest < Test::Unit::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_extraction_code_should_return_on_mismatch
|
def test_extraction_code_should_return_on_mismatch
|
||||||
segment.regexp = /\d+/
|
segment = segment(:regexp => /\d+/)
|
||||||
hash = merged = {:a => 'Hi', :b => '3'}
|
hash = merged = {:a => 'Hi', :b => '3'}
|
||||||
options = {:b => '3'}
|
options = {:b => '3'}
|
||||||
a_value = nil
|
a_value = nil
|
||||||
|
@ -292,7 +282,7 @@ class DynamicSegmentTest < Test::Unit::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_value_regexp_should_match_exacly
|
def test_value_regexp_should_match_exacly
|
||||||
segment.regexp = /\d+/
|
segment = segment(:regexp => /\d+/)
|
||||||
assert_no_match segment.value_regexp, "Hello 10 World"
|
assert_no_match segment.value_regexp, "Hello 10 World"
|
||||||
assert_no_match segment.value_regexp, "Hello 10"
|
assert_no_match segment.value_regexp, "Hello 10"
|
||||||
assert_no_match segment.value_regexp, "10 World"
|
assert_no_match segment.value_regexp, "10 World"
|
||||||
|
@ -300,40 +290,36 @@ class DynamicSegmentTest < Test::Unit::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_regexp_chunk_should_return_string
|
def test_regexp_chunk_should_return_string
|
||||||
segment.regexp = /\d+/
|
segment = segment(:regexp => /\d+/)
|
||||||
assert_kind_of String, segment.regexp_chunk
|
assert_kind_of String, segment.regexp_chunk
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_build_pattern_non_optional_with_no_captures
|
def test_build_pattern_non_optional_with_no_captures
|
||||||
# Non optional
|
# Non optional
|
||||||
a_segment = ROUTING::DynamicSegment.new
|
a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /\d+/)
|
||||||
a_segment.regexp = /\d+/ #number_of_captures is 0
|
|
||||||
assert_equal "(\\d+)stuff", a_segment.build_pattern('stuff')
|
assert_equal "(\\d+)stuff", a_segment.build_pattern('stuff')
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_build_pattern_non_optional_with_captures
|
def test_build_pattern_non_optional_with_captures
|
||||||
# Non optional
|
# Non optional
|
||||||
a_segment = ROUTING::DynamicSegment.new
|
a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /(\d+)(.*?)/)
|
||||||
a_segment.regexp = /(\d+)(.*?)/ #number_of_captures is 2
|
|
||||||
assert_equal "((\\d+)(.*?))stuff", a_segment.build_pattern('stuff')
|
assert_equal "((\\d+)(.*?))stuff", a_segment.build_pattern('stuff')
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_optionality_implied
|
def test_optionality_implied
|
||||||
a_segment = ROUTING::DynamicSegment.new
|
a_segment = ROUTING::DynamicSegment.new(:id)
|
||||||
a_segment.key = :id
|
|
||||||
assert a_segment.optionality_implied?
|
assert a_segment.optionality_implied?
|
||||||
|
|
||||||
a_segment.key = :action
|
a_segment = ROUTING::DynamicSegment.new(:action)
|
||||||
assert a_segment.optionality_implied?
|
assert a_segment.optionality_implied?
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_modifiers_must_be_handled_sensibly
|
def test_modifiers_must_be_handled_sensibly
|
||||||
a_segment = ROUTING::DynamicSegment.new
|
a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /david|jamis/i)
|
||||||
a_segment.regexp = /david|jamis/i
|
|
||||||
assert_equal "((?i-mx:david|jamis))stuff", a_segment.build_pattern('stuff')
|
assert_equal "((?i-mx:david|jamis))stuff", a_segment.build_pattern('stuff')
|
||||||
a_segment.regexp = /david|jamis/x
|
a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /david|jamis/x)
|
||||||
assert_equal "((?x-mi:david|jamis))stuff", a_segment.build_pattern('stuff')
|
assert_equal "((?x-mi:david|jamis))stuff", a_segment.build_pattern('stuff')
|
||||||
a_segment.regexp = /david|jamis/
|
a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /david|jamis/)
|
||||||
assert_equal "(david|jamis)stuff", a_segment.build_pattern('stuff')
|
assert_equal "(david|jamis)stuff", a_segment.build_pattern('stuff')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -560,7 +546,7 @@ class RouteBuilderTest < Test::Unit::TestCase
|
||||||
action = segments[-4]
|
action = segments[-4]
|
||||||
|
|
||||||
assert_equal :action, action.key
|
assert_equal :action, action.key
|
||||||
action.regexp = /show|in/ # Use 'in' to check partial matches
|
segments[-4] = ROUTING::DynamicSegment.new(:action, :regexp => /show|in/)
|
||||||
|
|
||||||
builder.assign_default_route_options(segments)
|
builder.assign_default_route_options(segments)
|
||||||
|
|
||||||
|
@ -661,10 +647,10 @@ class RoutingTest < Test::Unit::TestCase
|
||||||
ActionController::Routing.controller_paths = []
|
ActionController::Routing.controller_paths = []
|
||||||
assert_equal [], ActionController::Routing.possible_controllers
|
assert_equal [], ActionController::Routing.possible_controllers
|
||||||
|
|
||||||
ActionController::Routing::Routes.load!
|
|
||||||
ActionController::Routing.controller_paths = [
|
ActionController::Routing.controller_paths = [
|
||||||
root, root + '/app/controllers', root + '/vendor/plugins/bad_plugin/lib'
|
root, root + '/app/controllers', root + '/vendor/plugins/bad_plugin/lib'
|
||||||
]
|
]
|
||||||
|
ActionController::Routing::Routes.load!
|
||||||
|
|
||||||
assert_equal ["admin/user", "plugin", "user"], ActionController::Routing.possible_controllers.sort
|
assert_equal ["admin/user", "plugin", "user"], ActionController::Routing.possible_controllers.sort
|
||||||
ensure
|
ensure
|
||||||
|
@ -1374,34 +1360,20 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def slash_segment(is_optional = false)
|
def slash_segment(is_optional = false)
|
||||||
returning ROUTING::DividerSegment.new('/') do |s|
|
ROUTING::DividerSegment.new('/', :optional => is_optional)
|
||||||
s.is_optional = is_optional
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_route
|
def default_route
|
||||||
unless defined?(@default_route)
|
unless defined?(@default_route)
|
||||||
@default_route = ROUTING::Route.new
|
segments = []
|
||||||
|
segments << ROUTING::StaticSegment.new('/', :raw => true)
|
||||||
@default_route.segments << (s = ROUTING::StaticSegment.new)
|
segments << ROUTING::DynamicSegment.new(:controller)
|
||||||
s.value = '/'
|
segments << slash_segment(:optional)
|
||||||
s.raw = true
|
segments << ROUTING::DynamicSegment.new(:action, :default => 'index', :optional => true)
|
||||||
|
segments << slash_segment(:optional)
|
||||||
@default_route.segments << (s = ROUTING::DynamicSegment.new)
|
segments << ROUTING::DynamicSegment.new(:id, :optional => true)
|
||||||
s.key = :controller
|
segments << slash_segment(:optional)
|
||||||
|
@default_route = ROUTING::Route.new(segments).freeze
|
||||||
@default_route.segments << slash_segment(:optional)
|
|
||||||
@default_route.segments << (s = ROUTING::DynamicSegment.new)
|
|
||||||
s.key = :action
|
|
||||||
s.default = 'index'
|
|
||||||
s.is_optional = true
|
|
||||||
|
|
||||||
@default_route.segments << slash_segment(:optional)
|
|
||||||
@default_route.segments << (s = ROUTING::DynamicSegment.new)
|
|
||||||
s.key = :id
|
|
||||||
s.is_optional = true
|
|
||||||
|
|
||||||
@default_route.segments << slash_segment(:optional)
|
|
||||||
end
|
end
|
||||||
@default_route
|
@default_route
|
||||||
end
|
end
|
||||||
|
@ -1489,29 +1461,16 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_significant_keys
|
def test_significant_keys
|
||||||
user_url = ROUTING::Route.new
|
segments = []
|
||||||
user_url.segments << (s = ROUTING::StaticSegment.new)
|
segments << ROUTING::StaticSegment.new('/', :raw => true)
|
||||||
s.value = '/'
|
segments << ROUTING::StaticSegment.new('user')
|
||||||
s.raw = true
|
segments << ROUTING::StaticSegment.new('/', :raw => true, :optional => true)
|
||||||
|
segments << ROUTING::DynamicSegment.new(:user)
|
||||||
|
segments << ROUTING::StaticSegment.new('/', :raw => true, :optional => true)
|
||||||
|
|
||||||
user_url.segments << (s = ROUTING::StaticSegment.new)
|
requirements = {:controller => 'users', :action => 'show'}
|
||||||
s.value = 'user'
|
|
||||||
|
|
||||||
user_url.segments << (s = ROUTING::StaticSegment.new)
|
|
||||||
s.value = '/'
|
|
||||||
s.raw = true
|
|
||||||
s.is_optional = true
|
|
||||||
|
|
||||||
user_url.segments << (s = ROUTING::DynamicSegment.new)
|
|
||||||
s.key = :user
|
|
||||||
|
|
||||||
user_url.segments << (s = ROUTING::StaticSegment.new)
|
|
||||||
s.value = '/'
|
|
||||||
s.raw = true
|
|
||||||
s.is_optional = true
|
|
||||||
|
|
||||||
user_url.requirements = {:controller => 'users', :action => 'show'}
|
|
||||||
|
|
||||||
|
user_url = ROUTING::Route.new(segments, requirements)
|
||||||
keys = user_url.significant_keys.sort_by { |k| k.to_s }
|
keys = user_url.significant_keys.sort_by { |k| k.to_s }
|
||||||
assert_equal [:action, :controller, :user], keys
|
assert_equal [:action, :controller, :user], keys
|
||||||
end
|
end
|
||||||
|
|
|
@ -117,8 +117,8 @@ XML
|
||||||
@controller = TestController.new
|
@controller = TestController.new
|
||||||
@request = ActionController::TestRequest.new
|
@request = ActionController::TestRequest.new
|
||||||
@response = ActionController::TestResponse.new
|
@response = ActionController::TestResponse.new
|
||||||
ActionController::Routing::Routes.reload
|
|
||||||
ActionController::Routing.use_controllers! %w(content admin/user test_test/test)
|
ActionController::Routing.use_controllers! %w(content admin/user test_test/test)
|
||||||
|
ActionController::Routing::Routes.load_routes!
|
||||||
end
|
end
|
||||||
|
|
||||||
def teardown
|
def teardown
|
||||||
|
@ -412,7 +412,7 @@ XML
|
||||||
|
|
||||||
def test_assert_routing_with_method
|
def test_assert_routing_with_method
|
||||||
with_routing do |set|
|
with_routing do |set|
|
||||||
set.draw { |map| map.resources(:content) }
|
set.draw { |map| map.resources(:content) }
|
||||||
assert_routing({ :method => 'post', :path => 'content' }, { :controller => 'content', :action => 'create' })
|
assert_routing({ :method => 'post', :path => 'content' }, { :controller => 'content', :action => 'create' })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue