1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
|
# frozen_string_literal: true
require "action_controller/metal/exceptions"
module ActionDispatch
# :stopdoc:
module Journey
# The Formatter class is used for formatting URLs. For example, parameters
# passed to +url_for+ in Rails will eventually call Formatter#generate.
class Formatter
attr_reader :routes
def initialize(routes)
@routes = routes
@cache = nil
end
def generate(name, options, path_parameters, parameterize = nil)
constraints = path_parameters.merge(options)
missing_keys = nil
match_route(name, constraints) do |route|
parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
# 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?
missing_keys = missing_keys(route, parameterized_parts)
next if missing_keys && !missing_keys.empty?
params = options.dup.delete_if do |key, _|
parameterized_parts.key?(key) || route.defaults.key?(key)
end
defaults = route.defaults
required_parts = route.required_parts
route.parts.reverse_each do |key|
break if defaults[key].nil? && parameterized_parts[key].present?
next if parameterized_parts[key].to_s != defaults[key].to_s
break if required_parts.include?(key)
parameterized_parts.delete(key)
end
return [route.format(parameterized_parts), params]
end
unmatched_keys = (missing_keys || []) & constraints.keys
missing_keys = (missing_keys || []) - unmatched_keys
message = +"No route matches #{Hash[constraints.sort_by { |k, v| k.to_s }].inspect}"
message << ", missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty?
message << ", possible unmatched constraints: #{unmatched_keys.sort.inspect}" if unmatched_keys && !unmatched_keys.empty?
raise ActionController::UrlGenerationError, message
end
def clear
@cache = nil
end
private
def extract_parameterized_parts(route, options, recall, parameterize = nil)
parameterized_parts = recall.merge(options)
keys_to_keep = route.parts.reverse_each.drop_while { |part|
!(options.key?(part) || route.scope_options.key?(part)) || (options[part] || recall[part]).nil?
} | route.required_parts
parameterized_parts.delete_if do |bad_key, _|
!keys_to_keep.include?(bad_key)
end
if parameterize
parameterized_parts.each do |k, v|
parameterized_parts[k] = parameterize.call(k, v)
end
end
parameterized_parts.keep_if { |_, v| v }
parameterized_parts
end
def named_routes
routes.named_routes
end
def match_route(name, options)
if named_routes.key?(name)
yield named_routes[name]
else
routes = non_recursive(cache, options)
supplied_keys = options.each_with_object({}) do |(k, v), h|
h[k.to_s] = true if v
end
hash = routes.group_by { |_, r| r.score(supplied_keys) }
hash.keys.sort.reverse_each do |score|
break if score < 0
hash[score].sort_by { |i, _| i }.each do |_, route|
yield route
end
end
end
end
def non_recursive(cache, options)
routes = []
queue = [cache]
while queue.any?
c = queue.shift
routes.concat(c[:___routes]) if c.key?(:___routes)
options.each do |pair|
queue << c[pair] if c.key?(pair)
end
end
routes
end
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
# Returns an array populated with missing keys if any are present.
def missing_keys(route, parts)
missing_keys = nil
tests = route.path.requirements
route.required_parts.each { |key|
case tests[key]
when nil
unless parts[key]
missing_keys ||= []
missing_keys << key
end
when RegexCaseComparator
unless RegexCaseComparator::DEFAULT_REGEX === parts[key]
missing_keys ||= []
missing_keys << key
end
else
unless /\A#{tests[key]}\Z/ === parts[key]
missing_keys ||= []
missing_keys << key
end
end
}
missing_keys
end
def possibles(cache, options, depth = 0)
cache.fetch(:___routes) { [] } + options.find_all { |pair|
cache.key?(pair)
}.flat_map { |pair|
possibles(cache[pair], options, depth + 1)
}
end
def build_cache
root = { ___routes: [] }
routes.routes.each_with_index do |route, i|
leaf = route.required_defaults.inject(root) do |h, tuple|
h[tuple] ||= {}
end
(leaf[:___routes] ||= []) << [i, route]
end
root
end
def cache
@cache ||= build_cache
end
end
end
# :startdoc:
end
|