module Chusaku # Handles extracting information about the Rails project's routes. class Routes class << self # Primary method to call. # # Example output: # # { # "users" => { # "edit" => [ # {verb: "GET", path: "/users/:id", name: "edit_user"} # ], # "update" => [ # {verb: "PATCH", path: "/users", name: "edit_user"}, # {verb: "PUT", path: "/users", name: "edit_user"} # ] # }, # "empanadas" => { # "create" => [ # {verb: "POST", path: "/empanadas", name: nil} # ] # } # } # # @return [Hash] Routes hash def call routes = {} populate_routes(Rails.application, routes) backfill_routes(routes) end private def populate_routes(app, routes) app.routes.routes.each do |route| if route.app.engine? populate_routes(route.app.app, routes) next end controller, action, defaults = extract_data_from(route) routes[controller] ||= {} routes[controller][action] ||= [] add_info_for \ route: route, routes: routes, controller: controller, action: action, defaults: defaults end end # Adds formatted route info for the given param combination. # # @param route [Hash] Route info # @param routes [Hash] Collection of all route info # @param controller [String] Controller key # @param action [String] Action key # @param defaults [Hash] Default parameters for route # @return [void] def add_info_for(route:, routes:, controller:, action:, defaults:) verbs_for(route).each do |verb| routes[controller][action] .push(format(route: route, verb: verb, defaults: defaults)) routes[controller][action].uniq! end end # Extract the HTTP verbs for a Rails route. Required for older versions of # Rails that return regular expressions for a route verb which sometimes # contains multiple verbs. # # @param route [ActionDispatch::Journey::Route] Route given by Rails # @return [Array] List of HTTP verbs for the given route def verbs_for(route) route_verb = route.verb.to_s %w[GET POST PUT PATCH DELETE].select do |verb| route_verb.include?(verb) end end # Formats information for a given route. # # @param route [ActionDispatch::Journey::Route] Route given by Rails # @param verb [String] HTTP verb # @param defaults [Hash] Default parameters for route # @return [Hash] { verb => String, path => String, name => String } def format(route:, verb:, defaults:) { verb: verb, path: route.path.spec.to_s.gsub("(.:format)", ""), name: route.name, defaults: defaults } end # Given a routes hash, backfill entries that aren't already filled by # `Rails.application.routes`. # # @param routes [Hash] Routes hash generated by this class # @return [Hash] Backfilled routes hash def backfill_routes(routes) paths = {} # Map paths to their verbs and names. # # Resulting hash looks like: # # ```ruby # { # "/users/:id" => { # "GET" => "edit_user", # "PATCH" => "edit_user" # } # } # ```` routes.each do |_controller, actions| actions.each do |_action, data| data.each do |datum| paths[datum[:path]] ||= {} paths[datum[:path]][datum[:verb]] ||= datum[:name] end end end # Backfill names for routes that don't have them. # # First try to match based on the path and verb. If that doesn't work, # try to match based on the path alone. routes.each do |_controller, actions| actions.each do |_action, data| data.each do |datum| datum[:name] ||= paths.dig(datum[:path], datum[:verb]) datum[:name] ||= paths[datum[:path]]&.values&.compact&.first end end end routes end # Given a route, extract the controller and action strings. # # @param route [ActionDispatch::Journey::Route] Route instance # @return [Array] (String, String, Hash) def extract_data_from(route) defaults = route.defaults.dup controller = defaults.delete(:controller) action = defaults.delete(:action) [controller, action, defaults] end end end end