File: formatter.rb

package info (click to toggle)
rails 2%3A7.2.2.1%2Bdfsg-7
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 43,352 kB
  • sloc: ruby: 349,799; javascript: 30,703; yacc: 46; sql: 43; sh: 29; makefile: 27
file content (226 lines) | stat: -rw-r--r-- 6,830 bytes parent folder | download | duplicates (3)
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# frozen_string_literal: true

# :markup: markdown

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

      class RouteWithParams
        attr_reader :params

        def initialize(route, parameterized_parts, params)
          @route = route
          @parameterized_parts = parameterized_parts
          @params = params
        end

        def path(_)
          @route.format(@parameterized_parts)
        end
      end

      class MissingRoute
        attr_reader :routes, :name, :constraints, :missing_keys, :unmatched_keys

        def initialize(constraints, missing_keys, unmatched_keys, routes, name)
          @constraints = constraints
          @missing_keys = missing_keys
          @unmatched_keys = unmatched_keys
          @routes = routes
          @name = name
        end

        def path(method_name)
          raise ActionController::UrlGenerationError.new(message, routes, name, method_name)
        end

        def params
          path("unknown")
        end

        def message
          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?
          message
        end
      end

      def generate(name, options, path_parameters)
        original_options = options.dup
        path_params = options.delete(:path_params) || {}
        options = path_params.merge(options)
        constraints = path_parameters.merge(options)
        missing_keys = nil

        match_route(name, constraints) do |route|
          parameterized_parts = extract_parameterized_parts(route, options, path_parameters)

          # 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.delete_if do |key, _|
            # top-level params' normal behavior of generating query_params should be
            # preserved even if the same key is also a bind_param
            parameterized_parts.key?(key) || route.defaults.key?(key) ||
              (path_params.key?(key) && !original_options.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 RouteWithParams.new(route, parameterized_parts, params)
        end

        unmatched_keys = (missing_keys || []) & constraints.keys
        missing_keys = (missing_keys || []) - unmatched_keys

        MissingRoute.new(constraints, missing_keys, unmatched_keys, routes, name)
      end

      def clear
        @cache = nil
      end

      def eager_load!
        cache
        nil
      end

      private
        def extract_parameterized_parts(route, options, recall)
          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].nil? && recall[part].nil?)
          } | route.required_parts

          parameterized_parts.delete_if do |bad_key, _|
            !keys_to_keep.include?(bad_key)
          end

          parameterized_parts.each do |k, v|
            if k == :controller
              parameterized_parts[k] = v
            else
              parameterized_parts[k] = v.to_param
            end
          end

          parameterized_parts.compact!
          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

        # Returns an array populated with missing keys if any are present.
        def missing_keys(route, parts)
          missing_keys = nil
          tests = route.path.requirements_for_missing_keys_check
          route.required_parts.each { |key|
            case tests[key]
            when nil
              unless parts[key]
                missing_keys ||= []
                missing_keys << key
              end
            else
              unless tests[key].match?(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