# TODO: Fix formatting on member/collection methods module Rails module Upgrading module FakeRouter module ActionController module Routing class Routes def self.setup @redrawer = Rails::Upgrading::RouteRedrawer.new end def self.redrawer @redrawer end def self.draw yield @redrawer end end end end end class RoutesUpgrader def generate_new_routes if has_routes_file? upgrade_routes else raise FileNotFoundError, "Can't find your routes file [config/routes.rb]!" end end def has_routes_file? File.exists?("config/routes.rb") end def routes_code File.read("config/routes.rb") end def upgrade_routes FakeRouter::ActionController::Routing::Routes.setup # Read and eval the file; our fake route mapper will capture # the calls to draw routes and generate new route code FakeRouter.module_eval(routes_code) # Give the route set to the code generator and get its output generator = RouteGenerator.new(FakeRouter::ActionController::Routing::Routes.redrawer.routes) generator.generate end end class RouteRedrawer attr_accessor :routes def self.stack @stack end def self.stack=(val) @stack = val end def initialize @routes = [] # The old default route was actually two routes; we generate the new style # one only if we haven't generated it for the first old default route. @default_route_generated = false # Setup the stack for parents; used use proper indentation self.class.stack = [@routes] end def root(options) debug "mapping root" @routes << FakeRoute.new("/", options) end def connect(path, options={}) debug "connecting #{path}" if (path == ":controller/:action/:id.:format" || path == ":controller/:action/:id") if !@default_route_generated current_parent << FakeRoute.new("/:controller(/:action(/:id))", {:default_route => true}) @default_route_generated = true end else current_parent << FakeRoute.new(path, options) end end def resources(*args) if block_given? parent = FakeResourceRoute.new(args.shift) debug "mapping resources #{parent.name} with block" parent = stack(parent) do yield(self) end current_parent << parent else if args.last.is_a?(Hash) current_parent << FakeResourceRoute.new(args.shift, args.pop) debug "mapping resources #{current_parent.last.name} w/o block with args" else args.each do |a| current_parent << FakeResourceRoute.new(a) debug "mapping resources #{current_parent.last.name}" end end end end def resource(*args) if block_given? parent = FakeSingletonResourceRoute.new(args.shift) debug "mapping resource #{parent.name} with block" parent = stack(parent) do yield(self) end current_parent << parent else if args.last.is_a?(Hash) current_parent << FakeSingletonResourceRoute.new(args.shift, args.pop) debug "mapping resources #{current_parent.last.name} w/o block with args" else args.each do |a| current_parent << FakeSingletonResourceRoute.new(a) debug "mapping resources #{current_parent.last.name}" end end end end def namespace(name, options = {}) debug "mapping namespace #{name}" namespace = FakeNamespace.new(name, options) namespace = stack(namespace) do yield(self) end current_parent << namespace end def method_missing(m, *args) debug "named route: #{m}" current_parent << FakeRoute.new(args.shift, args.pop, m.to_s) end def self.indent ' ' * ((stack.length) * 2) end private def debug(txt) puts txt if ENV['DEBUG'] end def stack(obj) self.class.stack << obj yield self.class.stack.pop end def current_parent self.class.stack.last end end class RouteObject def indent_lines(code_lines) if code_lines.length > 1 code_lines.flatten.map {|l| "#{@indent}#{l.chomp}"}.join("\n") + "\n" else "#{@indent}#{code_lines.shift}" end end def opts_to_string(opts) opts.is_a?(Hash) ? opts.map {|k, v| ":#{k} => " + (v.is_a?(Hash) ? ('{ ' + opts_to_string(v) + ' }') : "#{value_to_string(v)}") }.join(", ") : opts.to_s end def value_to_string(value) case value when Regexp then value.inspect when String then "'" + value.to_s + "'" else value.to_s end end end class FakeNamespace < RouteObject attr_accessor :routes, :name, :options def initialize(name, options = {}) @routes = [] @name, @options = name, options @indent = RouteRedrawer.indent end def to_route_code if !@options.empty? options = ', ' + opts_to_string(@options) else options = '' end lines = ["namespace :#{@name}#{options} do", @routes.map {|r| r.to_route_code}, "end"] indent_lines(lines) end def <<(val) @routes << val end def last @routes.last end end class FakeRoute < RouteObject attr_accessor :name, :path, :options def initialize(path, options, name = "") @path = path @options = options || {} @name = name @indent = RouteRedrawer.indent end def to_route_code if @options[:default_route] indent_lines ["match '#{@path}'"] else base = "match '%s' => '%s#%s'" extra_options = [] if not name.empty? extra_options << ":as => :#{name}" end if @options[:requirements] @options[:constraints] = @options.delete(:requirements) end if @options[:conditions] @options[:via] = @options.delete(:conditions).delete(:method) end if @options[:method] @options[:via] = @options.delete(:method).to_s end @options ||= {} base = (base % [@path, @options.delete(:controller), (@options.delete(:action) || "index")]) opts = opts_to_string(@options) route_pieces = ([base] + extra_options + [opts]) route_pieces.delete("") indent_lines [route_pieces.join(", ")] end end end class FakeResourceRoute < RouteObject attr_accessor :name, :children def initialize(name, options = {}) @name = name @children = [] @options = options @indent = RouteRedrawer.indent end def to_route_code # preserve :only & :except options copied_options = @options.reject { |k,v| ![:only, :except].member?(k) } unless copied_options.empty? copied_options_str = ", " + copied_options.map { |k, v| "#{k.inspect} => #{v.inspect}" }.join(",") end if !@children.empty? || @options.has_key?(:collection) || @options.has_key?(:member) prefix = ["#{route_method} :#{@name}#{copied_options_str} do"] lines = prefix + custom_methods + [@children.map {|r| r.to_route_code}.join("\n"), "end"] indent_lines(lines) else base = "#{route_method} :%s#{copied_options_str}" indent_lines [base % [@name]] end end def custom_methods collection_code = generate_custom_methods_for(:collection) member_code = generate_custom_methods_for(:member) [collection_code, member_code] end def generate_custom_methods_for(group) return "" unless @options[group] method_code = [] RouteRedrawer.stack << self @options[group].each do |name, methods| [*methods].each do |method| method_code << "#{method} :#{name}" end end RouteRedrawer.stack.pop indent_lines ["#{group} do", method_code, "end"].flatten end def route_method "resources" end def <<(val) @children << val end def last @children.last end end class FakeSingletonResourceRoute < FakeResourceRoute def route_method "resource" end end class RouteGenerator def initialize(routes) @routes = routes @new_code = "" end def generate @new_code = @routes.map do |r| r.to_route_code end.join("\n") "#{app_name.underscore.classify}::Application.routes.draw do\n#{@new_code}\nend\n" end private def app_name File.basename(Dir.pwd) end end end end