2013-01-14 08:18:23 -05:00
|
|
|
require 'action_controller/metal/exceptions'
|
|
|
|
|
2012-12-19 15:54:47 -05:00
|
|
|
module ActionDispatch
|
|
|
|
module Journey
|
2012-12-19 19:24:25 -05:00
|
|
|
# The Formatter class is used for formatting URLs. For example, parameters
|
2013-05-09 07:57:08 -04:00
|
|
|
# passed to +url_for+ in Rails will eventually call Formatter#generate.
|
2012-12-19 19:24:25 -05:00
|
|
|
class Formatter # :nodoc:
|
2012-12-19 15:54:47 -05:00
|
|
|
attr_reader :routes
|
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
def initialize(routes)
|
2012-12-19 15:54:47 -05:00
|
|
|
@routes = routes
|
|
|
|
@cache = nil
|
|
|
|
end
|
|
|
|
|
2014-07-17 14:26:59 -04:00
|
|
|
def generate(name, options, path_parameters, parameterize = nil)
|
|
|
|
constraints = path_parameters.merge(options)
|
2015-07-27 02:45:29 -04:00
|
|
|
missing_keys = nil # need for variable scope
|
2012-12-19 15:54:47 -05:00
|
|
|
|
|
|
|
match_route(name, constraints) do |route|
|
2014-07-17 14:26:59 -04:00
|
|
|
parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
|
2013-07-16 08:27:22 -04:00
|
|
|
|
|
|
|
# Skip this route unless a name has been provided or it is a
|
|
|
|
# standard Rails route since we can't determine whether an options
|
|
|
|
# hash passed to url_for matches a Rack application or a redirect.
|
|
|
|
next unless name || route.dispatcher?
|
2012-12-19 15:54:47 -05:00
|
|
|
|
|
|
|
missing_keys = missing_keys(route, parameterized_parts)
|
2015-07-30 15:12:37 -04:00
|
|
|
next if missing_keys && !missing_keys.empty?
|
2012-12-19 15:54:47 -05:00
|
|
|
params = options.dup.delete_if do |key, _|
|
|
|
|
parameterized_parts.key?(key) || route.defaults.key?(key)
|
|
|
|
end
|
|
|
|
|
2014-05-21 14:55:15 -04:00
|
|
|
defaults = route.defaults
|
|
|
|
required_parts = route.required_parts
|
2015-07-25 01:25:47 -04:00
|
|
|
parameterized_parts.keep_if do |key, value|
|
2015-09-02 06:35:44 -04:00
|
|
|
(defaults[key].nil? && value.present?) || value.to_s != defaults[key].to_s || required_parts.include?(key)
|
2014-05-21 14:55:15 -04:00
|
|
|
end
|
|
|
|
|
2012-12-19 15:54:47 -05:00
|
|
|
return [route.format(parameterized_parts), params]
|
|
|
|
end
|
|
|
|
|
2015-04-08 10:18:56 -04:00
|
|
|
message = "No route matches #{Hash[constraints.sort_by{|k,v| k.to_s}].inspect}"
|
2015-07-30 15:12:37 -04:00
|
|
|
message << " missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty?
|
2013-01-14 08:18:23 -05:00
|
|
|
|
|
|
|
raise ActionController::UrlGenerationError, message
|
2012-12-19 15:54:47 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def clear
|
|
|
|
@cache = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
def extract_parameterized_parts(route, options, recall, parameterize = nil)
|
2013-01-02 11:01:01 -05:00
|
|
|
parameterized_parts = recall.merge(options)
|
2012-12-19 15:54:47 -05:00
|
|
|
|
2015-07-24 23:38:34 -04:00
|
|
|
keys_to_keep = route.parts.reverse_each.drop_while { |part|
|
2012-12-20 15:42:39 -05:00
|
|
|
!options.key?(part) || (options[part] || recall[part]).nil?
|
|
|
|
} | route.required_parts
|
|
|
|
|
2015-07-30 13:34:48 -04:00
|
|
|
parameterized_parts.delete_if do |bad_key, _|
|
|
|
|
!keys_to_keep.include?(bad_key)
|
2012-12-20 15:42:39 -05:00
|
|
|
end
|
2012-12-19 15:54:47 -05:00
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
if parameterize
|
|
|
|
parameterized_parts.each do |k, v|
|
|
|
|
parameterized_parts[k] = parameterize.call(k, v)
|
|
|
|
end
|
2012-12-19 15:54:47 -05:00
|
|
|
end
|
|
|
|
|
2013-04-08 15:13:13 -04:00
|
|
|
parameterized_parts.keep_if { |_, v| v }
|
2012-12-20 15:42:39 -05:00
|
|
|
parameterized_parts
|
|
|
|
end
|
2012-12-19 15:54:47 -05:00
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
def named_routes
|
|
|
|
routes.named_routes
|
|
|
|
end
|
2012-12-19 15:54:47 -05:00
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
def match_route(name, options)
|
|
|
|
if named_routes.key?(name)
|
|
|
|
yield named_routes[name]
|
|
|
|
else
|
2014-05-22 14:16:56 -04:00
|
|
|
routes = non_recursive(cache, options)
|
2012-12-19 15:54:47 -05:00
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
hash = routes.group_by { |_, r| r.score(options) }
|
2012-12-19 15:54:47 -05:00
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
hash.keys.sort.reverse_each do |score|
|
2014-05-22 10:44:25 -04:00
|
|
|
break if score < 0
|
2012-12-19 15:54:47 -05:00
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
hash[score].sort_by { |i, _| i }.each do |_, route|
|
|
|
|
yield route
|
|
|
|
end
|
2012-12-19 15:54:47 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
def non_recursive(cache, options)
|
|
|
|
routes = []
|
2014-05-22 14:33:26 -04:00
|
|
|
queue = [cache]
|
2012-12-19 15:54:47 -05:00
|
|
|
|
2014-05-22 14:33:26 -04:00
|
|
|
while queue.any?
|
|
|
|
c = queue.shift
|
2012-12-20 15:42:39 -05:00
|
|
|
routes.concat(c[:___routes]) if c.key?(:___routes)
|
2012-12-19 15:54:47 -05:00
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
options.each do |pair|
|
2014-05-22 14:33:26 -04:00
|
|
|
queue << c[pair] if c.key?(pair)
|
2012-12-20 15:42:39 -05:00
|
|
|
end
|
2012-12-19 15:54:47 -05:00
|
|
|
end
|
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
routes
|
|
|
|
end
|
2012-12-19 15:54:47 -05:00
|
|
|
|
2015-07-24 23:47:34 -04:00
|
|
|
module RegexCaseComparator
|
|
|
|
DEFAULT_INPUT = /[-_.a-zA-Z0-9]+\/[-_.a-zA-Z0-9]+/
|
|
|
|
DEFAULT_REGEX = /\A#{DEFAULT_INPUT}\Z/
|
|
|
|
|
|
|
|
def self.===(regex)
|
|
|
|
DEFAULT_INPUT == regex
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
# Returns an array populated with missing keys if any are present.
|
|
|
|
def missing_keys(route, parts)
|
2015-07-27 02:52:45 -04:00
|
|
|
missing_keys = nil
|
2012-12-20 15:42:39 -05:00
|
|
|
tests = route.path.requirements
|
|
|
|
route.required_parts.each { |key|
|
2015-07-24 23:47:34 -04:00
|
|
|
case tests[key]
|
|
|
|
when nil
|
2015-07-27 02:52:45 -04:00
|
|
|
unless parts[key]
|
|
|
|
missing_keys ||= []
|
|
|
|
missing_keys << key
|
|
|
|
end
|
2015-07-24 23:47:34 -04:00
|
|
|
when RegexCaseComparator
|
2015-07-27 02:52:45 -04:00
|
|
|
unless RegexCaseComparator::DEFAULT_REGEX === parts[key]
|
|
|
|
missing_keys ||= []
|
|
|
|
missing_keys << key
|
|
|
|
end
|
2015-07-24 23:47:34 -04:00
|
|
|
else
|
2015-07-27 02:52:45 -04:00
|
|
|
unless /\A#{tests[key]}\Z/ === parts[key]
|
|
|
|
missing_keys ||= []
|
|
|
|
missing_keys << key
|
|
|
|
end
|
2012-12-20 15:42:39 -05:00
|
|
|
end
|
|
|
|
}
|
|
|
|
missing_keys
|
|
|
|
end
|
2012-12-19 15:54:47 -05:00
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
def possibles(cache, options, depth = 0)
|
|
|
|
cache.fetch(:___routes) { [] } + options.find_all { |pair|
|
|
|
|
cache.key?(pair)
|
2014-03-01 01:41:49 -05:00
|
|
|
}.flat_map { |pair|
|
2012-12-20 15:42:39 -05:00
|
|
|
possibles(cache[pair], options, depth + 1)
|
2014-03-01 01:41:49 -05:00
|
|
|
}
|
2012-12-20 15:42:39 -05:00
|
|
|
end
|
2012-12-19 15:54:47 -05:00
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
def build_cache
|
|
|
|
root = { ___routes: [] }
|
2015-08-14 21:04:49 -04:00
|
|
|
routes.routes.each_with_index do |route, i|
|
2012-12-20 15:42:39 -05:00
|
|
|
leaf = route.required_defaults.inject(root) do |h, tuple|
|
|
|
|
h[tuple] ||= {}
|
|
|
|
end
|
|
|
|
(leaf[:___routes] ||= []) << [i, route]
|
2012-12-19 15:54:47 -05:00
|
|
|
end
|
2012-12-20 15:42:39 -05:00
|
|
|
root
|
2012-12-19 15:54:47 -05:00
|
|
|
end
|
|
|
|
|
2012-12-20 15:42:39 -05:00
|
|
|
def cache
|
|
|
|
@cache ||= build_cache
|
|
|
|
end
|
2012-12-19 15:54:47 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|