Sunday, February 3, 2013

Specing an initializer block in a Rails Engine

Im currently in the process of writing a ruby gem that acts as a plugin CMS for an existing rails application. To do this I'm learning a lot about Rails Engines.

I may write more about my experiences with rails engines but for those who haven't used this (awesome) feature yet a rails engine is basically a gem (or plugin... for now) that not only interacts with a rails application, but can inject its own controllers, models, views and other standard rails components into your application.

As part of this CMS I want to dynamically map routes to the controller path in the gem that handles displaying a page. So if the user creates a page called Foo with a "slug" of foo/bar I want to dynamically bind a route like:

get "/foo/bar", :to => "cmsgem/pages#display"

This should happen when a page is published (if the route doesn't already exist.) It should also happen when the application spins up. It should look at all published pages and bind them.

To do this you can define an initializer in your main rails engine class. It looks something like:

module Cmsgem
  class Engine < ::Rails::Engine

    initializer 'cmsgem.bind_dynamic_routes', :after => :disable_dependency_loading do |app|
      app.routes.draw do
        Cmsgem::Page.where(:published => true).each do |p|
          get "/#{p.slug}", :to => "cmsgem/pages#display"


(note: if you want to know why i have the :after => :disable_dependency_loading check this article)

The problem I ran into was, how do I test this? This code executes when you initialize rails. Rspec spins up it's access to rails prior to running your tests, which means I can't create a Page and test for the existence of that route, because the page would be created after initialization and not get referenced when the code block above runs.

So I dug into the routing API a bit, and came up with this:

describe Cmsgem::Engine do
  it "dynamically adds routes from pages at initialization" do
    page = Cmsgem::Page.create(:slug => "rspec/test", :title => "Rspec Test", :content => "This is an rspec test")

    initializer = { |i| == "cmsgem.bind_dynamic_routes" }.first

    route = Rails.application.routes.recognize_path("/rspec/test", :method => :get)
    route.should_not be_nil

Basically what's happening is that I'm finding my custom initializer in the collection of initializers that my gem runs, then explicitly rerunning it. You have to pass in the rails application as it is used to bind the routes in the gem. 

I'd be curious if there's a better solution to this problem. Maybe some way to setup a record in rspec pre-initializer or a better way to reinitialize rails. If anyone has any suggestions let me know. Hopefully I'll post more about this gem as I go along.