Josh McArthur

Publishing with ActiveRecord: a guide to encapsulated code

17 Dec 2012

It’s not at all uncommon to be asked by a client “So, I’d really like to be able to edit that bit of the page just there - can we do that?”. The answer, of course, is yes, but managing content has no end of complexities. Just one of these many is publishing - that is, controlling content, where it is visible, and to whom. In this blog post, I’m going to describe how I worked to move code out of several applications into a shared library, and the trials and tribulations I had ensuring this eventual Rubygem worked with as many combinations of other Rubygems as possible.

Moving Code

The first step to properly encapsulating code is to identify specific code functions that can be moved out of individual codebases. In the case of the has_publishing library, I identified the processes of publishing a record, creating a draft record, marking a record for future publishing and the querying of all the above states to be a common functionality that was required in a number of other sites. Once this functionality was identified, it was no big deal to copy-and-paste relevant bits of code into a new gem file structure, generated by the lovely bundle gem command. Of course, because this code had been pulled in from a number of different locations, it was still very disorganized and messy. Fortunately, I had the next step:

Refactoring Code

Once the code is gathered together in a central library, it’s all set to be tidied up, tested, and pushed out for the world to enjoy. For a gem that adds functionality to another application, the best way to rapidly prototype how things may fit togther is to set up a quick Rails app, and add the gem as a local dependency:

# Gemfile
gem 'has_publishing', :path => '../has_publishing'

This way, any changes you make to the gem are instantly reflected in your testing harness. The other part of refactoring code is to ensure that the code gets covered by comprehensive unit tests.

Refactoring code can take different forms, depending on the state of the code! Largely, you’ll be looking for duplicated or overly-complex code that can be merged into common behaviour, or any other code that you don’t feel is ready for the public to see.

Testing Code

Testing code in a gem is always more tricky that testing within an application, because you don’t have an exact context to test against - just behaviour. Typically, you need to do a bit more set up to have an environment that provides just enough context to run your tests against.

The easiest way to do this for this sort of project is to replicate the same sort of support that Rails provides for testing models against a database, in as little code as possible. Here’s how I set up a database-backed model for testing against in has_publishing:

# Establish a database connection 
# to a local SQLite3 database
ActiveRecord::Base.establish_connection(
  :adapter  => 'sqlite3',
  :database => File.join(File.dirname(__FILE__), 'has_publishing_test.db')
)


# Create the data table for the model
# I'll be testing against, and then
# manually migrate the database to this
# point 
class CreateTestModels < ActiveRecord::Migration
  def self.up
    if ActiveRecord::Base.connection.table_exists? "test_models"
      drop_table :test_models
    end

    create_table :test_models do |t|
      t.datetime :published_at
      t.datetime :embargoed_until
      t.string :kind
      t.boolean :dirty
      t.references :published

      t.timestamps
    end
  end

  def self.down
    drop_table :test_models
  end
end

CreateTestModels.migrate(:up)

# Add the testing model, complete
# with has_publishing
class TestModel < ActiveRecord::Base
  has_publishing
end

With this testing harness in place, it’s pretty simple to make assertions about how that model should behave with has_publishing - it’s simply ActiveRecord. The reason I prefer not to take the approach of many Rails-exclusive gems and generate an entire dummy application to test with is that I feel like this approach is much more transparent, and just enough set up to get the tests passing, without getting in the way.

Bootstrapping with Rails engines

Although this gem is framework-agnostic (only requiring ActiveRecord), I do want to make it easier for people using Rails to add publishing to their models. Because of this, I have added a Rails generator to my gem. This generator can add the migration to add publishing attributes for any model with a single command, making it much easier to get the correct database schema set up quickly.

Rails engines are related, but not the same as generators. The easiest way to think of them is pluggable sub-applications - they may include extra functionality, change existing functionality or perform some special function, but they are typically just bundles of controllers, models and views, often bundled with generators to set all of these objects up within a Rails application.

Adding generators to a gem is actually very easy to do, but it did have a couple of gotchas to ensure that Rails actually picks up the generator and makes it available for use.

Gem Compatiblity