Rake Routes Review Repeat

If you have a large and active project with many, many nested routes you’ll find that listing routes using rake:routes usually ends up a mass of info spewed into terminal that is painful to wade through even with grep.

It became a pain for me and I decided to take a stab at extending rake:routes to add a task to simplify the output and add some new information. Most of the time I grep rake:routes to confirm a path name or to see if I’m missing a route. Some of the time I wonder about whether there are orphaned routes or whether there are missing controller methods. It would be nice to get some help on these too.

So I put together a rake task for routes that you can drop into lib/tasks/routes.rake to get more succinct output.

>>rake routes:by_controller

With this I’m able to get a brief listing by controller of the action, method and path along with some information to identify any routes that have been orphaned. Perhaps some of the resources can be removed or some paths should be filtered (ie. :only =>[:index, :show]).

For example: running “rake routes:by_controller pattern=com.e” will result in output like the following (except better in terminal; color coded):

Attempting to match routes controller methods to app controller methods

Match controllers on pattern ~= /com.e/

Controller methods implemented
*Controller methods not implemented


CONTROLLER: user/tickets/comments
  *index    GET     user_ticket_comments                                                   
  create    POST                                                                           
  *new      GET     new_user_ticket_comment                                                
  *edit     GET     edit_user_ticket_comment                                               
  *show     GET     user_ticket_comment                                                    
  *update   PUT                                                                            
  *destroy  DELETE                                                                         


CONTROLLER: comments
  *index    GET     comments                                                               
  create    POST                                                                           
  *new      GET     new_comment                                                            
  *edit     GET     edit_comment                                                           
  *show     GET     comment                                                                
  *update   PUT                                                                            
  *destroy  DELETE                                                                         


Number of methods: 14
Controller methods implemented: 2
*Controller methods not implemented: 12
 

How does it help?

  1. It becomes easy to copy and paste a path.
  2. You’ll see ‘create’ is defined in the controller but that the other actions aren’t. Maybe they should be implemented (or maybe not) and just filtered from routes (ie. :only => [:create]).
  3. A succinct, uncluttered view by controller.

Any way, grab the code below and have fun.

namespace :routes do

  desc 'Brief listing of routes info plus matches to app controller methods'
  task :by_controller => :environment do
    task_setup "Attempting to match routes controller methods to app controller methods"
    methods = find_routes_controller_methods_matched_to_app_controller_methods(@pattern)
    index_width, controller_width, action_width, path_width, method_width, segment_width, app_controller_action = get_array_elements_max_width(methods)
    last_controller = ''
    methods.each do |index, controller, action, path, method, segment, app_controller_action|
      @methods_count += 1
      @missing_methods_count += 1 unless app_controller_action 
      puts "\n\nCONTROLLER: #{controller.ljust(controller_width).strip}\n" unless last_controller == controller
      puts "  #{(app_controller_action ? green(action) : yellow("*#{action}")).ljust(action_width+colorize_width)}  #{method.ljust(method_width)}  #{path.ljust(path_width)}"
      last_controller = controller
    end
    display_counts
  end

  def get_array_elements_max_width(array)
    array.first.enum_with_index.map {|element,idx| array.map {|element| element[idx]}.map {|n| n ? n.length : 0}.max}
  end

  def list_directories(directory, pattern)
    result = []
    Dir.glob("#{directory}/*") do |file|
      next if file[0] == ?.
      if File.directory? file
        result.push(*list_directories(file, pattern))
      elsif file =~ pattern
        result << file
      end
    end
    result
  end

  def find_routes_controller_actions(pattern='')
    routes = []
    ActionController::Routing::Routes.routes.each do |route|
      path = ActionController::Routing::Routes.named_routes.routes.index(route).to_s
      method = route.conditions[:method].blank? ? 'GET' : route.conditions[:method].to_s.upcase
      segment = route.segments.inject("") { |str,s| str << s.to_s }
      segment.chop! if segment.length > 1
      controller = route.requirements.empty? ? "" : route.requirements[:controller]
      action = route.requirements.empty? ? "" : route.requirements[:action]
      route.requirements.delete :controller
      route.requirements.delete :action

      if controller =~ /#{@pattern}/
        unless routes.assoc "#{controller}:#{action}"
          routes << ["#{controller}:#{action}", controller, action, path, method, segment] unless controller.blank?
        end
      end
    end
    routes
  end

  def find_app_controller_actions(pattern='')
    controller_actions = []
    controllers = list_directories "#{RAILS_ROOT}/app/controllers", /_controller.rb$/
    controllers.each {|a| a.gsub!("#{RAILS_ROOT}/app/controllers/",''); a.gsub!('.rb','')}
    controllers.each do |controller_path|
      if controller_path =~ /#{@pattern}/
        controller = controller_path.camelize.gsub(".rb","")
        (eval("#{controller}.public_instance_methods") -
            ApplicationController.public_instance_methods -
            Object.methods).sort.each {|method| controller_actions << ["#{controller.underscore.gsub!("_controller",'')}:#{method}"]}
      end
    end
    controller_actions
  end

  def find_routes_controller_methods_matched_to_app_controller_methods(pattern='')
    controller_actions = find_app_controller_actions(@pattern)
    find_routes_controller_actions(@pattern).map! {|p| p << controller_actions.find {|i| i[0] == p[0]}}
  end

  def task_setup(title)
    @pattern = ENV['pattern'] ? ENV['pattern'].downcase : ''
    @methods_count = 0
    @missing_methods_count = 0
    puts "\n#{title}\n\n"
    puts "Match controllers on pattern =~ /#{@pattern}/\n\n"
    puts green "Controller methods implemented"
    puts yellow "*Controller methods not implemented"
  end

  def display_counts
    puts "\n\nNumber of methods: #{@methods_count}"
    puts green "Controller methods implemented #{@methods_count - @missing_methods_count}"
    puts yellow "*Controller methods not implemented #{@missing_methods_count}"
  end

  def colorize(text, color_code)
    "#{color_code}#{text}\e[0m"
  end
  def colorize_width; 10;end
  def red(text); colorize(text, "\e[31m"); end
  def green(text); colorize(text, "\e[32m"); end
  def yellow(text); colorize(text, "\e[33m"); end

end

The Zen of Laptoplessness

Sh1t happens and occasionally it’s something that disrupts the ‘flow‘; at least, my ‘flow’. A few weeks ago it happened to me. My day started opening the lid of my MacbookPro, hitting the return key and finding myself staring at a grey screen with an Apple logo and a barely perceptable spinner. No matter, just force shut down and restart, right? And restart, hmm and restart in safe mode, hmmm. Fish out the MacOS dvd and boot off of the cd, hmmmm.

The Apple Store

Two hours later, way behind in my day, I abandon the effort and venture off to the Apple Store to get in the queue to find out what is going on; all the while hopeful that it’s a recoverable gitch. However, Once there I’m greeted at closed doors by a greeter who asks “Do you have a reservation?” Of course not, having my MBP go lame wasn’t something that I planned. “Sorry, come back when we open at 10” was the greeter’s reply. So I’m back at 10 and once again the Apple store greeter asks if I have a reservation but this time I get a “come on in”. The store is already overrun with customers who had reservations and I head for the sales counter where I’m directed to the store ‘concierge’ (the person in the ‘orange’ shirt). I find him, describe my situation and he asks if I have a reservation with the store technician. I say tell him no but that I have a business account and expect some preference and then he shuffles off to the back room for 10 minutes to confirm. But a history of spending big bucks at Apple wasn’t a ticket to the front of the line. The best the concierge could offer was for me to spend $100 for Apple ProCare and then he could move to the front of the queue. Or I could leave my machine but essentially I would be authorizing Apple to do whatever with no guarantees they wouldn’t do further damage or wipe out the disk drive. Needless to say the dialogue which was heading south quickly hit bottom. I wasn’t feeling helped at all. Fortunately, there was some help at the end as the Apple store concierge came up with a list of other authorized Apple dealers that might help me sooner. So with these phone numbers in hand I left. And as I left, I was eyeing the 17″ inch MBP, sweet, thinking I might have an excuse for a new MBP. Still amp’d but at least I’m breathing normally now.

The Verdict

With a tiny bit of help from Apple it’s off MacForce in Portland and within minutes I find Jason, their technician, who quickly made an assessment, none too pleasing, that offered some hope that all was not lost and chances were the disk drive was ok. Apparently, the back edge of the lid holding the lcd panel had begun to split, the hard drive wouldn’t stop spinning and the cpu fan switched on immediately on booting. In hindsight, I should of been suspicious of increasingly long boot and shutdown times. I ‘m betting I’m now paying the price for being careless. For more than 2 years I’ve taken the durability of my MBP for granted and its finally taken it’s toll. It was going to cost a 1000 bucks to fix the lid/lcd panel, $80 to unload the hard drive and $129 bucks for a portable drive to dump it’s contains to. Ouch.

I got to be optimistic for a day thinking the drive was ok. It was not to be. The news from Jason came the next day that the disk drive was toast and unreadable and throwing IO errors. The only way to perhaps recover the drive was to send it off to a data recovery service which would cost over $300 dollars. However, it would require me to tell them the exact file names of the files I wanted recovered and, of course, no guarantee I’d get any data. Ouch, Zing, Sh1t….

I took the weekend to think about whether to spend the money on recovering a handful of files. I figured I had enough backed up to forego the data recovery. So, no way it’s not going to happen, no money, no laptop, no data. I had fits and starts of backups and cds to get back most of what I needed except for a few photos, years of email archives, some music, some half baked projects and a few stray files I probably would need but don’t know it.

The Recovery

Fortunately, I had a another mac sitting around, a Xeon quad pro with 11gb of memory (I should be ecstatic, right?). It was pretty much a headless server that was barely used since getting the laptop. And fortunately (or not), I was using Time Machine. But it had been a while since a full backup and I was always willy nilly shutting Time Machine off and on as it was a nuisance; just took too long to do a backup over wifi (and getting longer & longer with each missed backup). And I had Evernote and iDisk on Mobileme. It was a bit of effort over two days to get everything back in place to be productive again; hunting down cds in storage boxes, trying to find license numbers, getting all my ruby gems back and playing nice with each other. What a pain, it could have been worse I guess.

In the meantime, I’m paying attention to backups and redundancy. Perhaps you should too. Here’s a few steps I’ve taken.

  1. Mobile Me iDisk
    • adjust storage allocation so that iDisk gets most of the storage (ie. 18GB idisk, 2 GB email)
    • run Keychain First Aid to make sure Keychain Access was working properly. If Keychain is broken so are your file syncs.
    • clean-up idisk, i found myself asking over and over why did I put this file on iDisk
    • setup iDisk to keep files local and periodically update with Mobileme (that took 12 hours to finish!)
    • get the new iDisk app for your iPhone, I keep my reference books and other documents on iDisk. It’s sweet to be able to access these from my iPhone
    • try out Apple’s Backup 3 to see if you can use it to sync selected files on your machine to iDisk. I’ve got a couple of files I’ve selected to backup to iDisk using Backup’s ‘Quickpicks’ ( ~/.bash_login, hosts, irbrc and others). You can even create your own ‘Quickpicks’ if you want.
  2. Evernote
    • if you don’t use Evernote get it now. All my notes are kept in Evernote which is accessible via the web, the iPhone and any other machine you’ve installed Evernote on. Fortunately, many, many hard earned tips, tidbits and snippets weren’t lost because I had been using Evernote.
  3. Portable disk drive
    • I’ve purchased the Mac edition of Iomega’s eGo 500 GB 2.5″ Firewire800/400/USB2, Ruby Red, of course, It has drop protection up to 51″, a 3 yr warranty and has a small footprint (.625″ x 3.5″ x 5.625″). I plan to attach it to whatever machine I’m using wherever I am to handle backups continuously!

The Zen

For now, I’m content to keep my only computer at the office. I’ve trimmed the backpack contents down to a camera and sketchbook and have more unplugged time at home. The only cpu within reach now is an iPhone (pitiful but thank goodness anyway).

Not sure how long I can go laptopless, I’ll soon see. I’ve got this phantom pain, an ache of not having a laptop within reach, whenever and wherever, to get that quick rush from a discovery at the end of a google search or to put the wraps on a coding problem when an ‘aha!’ moment hits. I’m not sure that’ll go away. For now, I’m addressing the ache with some reading and I’m currently halfway through a novel, “Water for Elephants”, it’s about an old man in a nursing home who reflects on his life traveling with a circus in the 1930s. Hmmm, kind of makes me think what I’ll be reflecting on one day…..