Specing Layouts

Just a quick little snippet for those trying to write specs for their layout.

Create your spec in spec/views/layout/application.html.erb_spec.html, and add this class to it:

class Layout < Application
  layout nil
  def application; render; end
end

Then just test it as you would any other ordinary view.

Porting Rails Plugins to DataMapper

As a follow-up to my previous post, here's some gotchas to be aware of if you're looking to support both ActiveRecord and DataMapper in a Merb (and/or Rails) plugin.

The Strategy

The best way I've found to handle multiple ORM support in your plugin is not to start monkeypatching around to make one ORM handle like another. I've done it, and can tell you that wrapping one ORM's backend into another is ugly.

The better way is to localize the points where your plugin interacts with the data model, with an eye to swapping them out. For the above example, I would be better off taking the method that used the reflection method and putting it inside an ActiveRecord-specific module. Then, create a DataMapper-specific module that defines the same method, but instead relies on the DM backend to get at the association information. Finally, when the plugin was loaded I could just include one of the modules based on which ORM was loaded into the runtime.

Basic Translation

Now that we have a plan, we can start translating our extracted functions from AR bits to DM bits. There's a bunch of fairly straightforward transformations we can make.

It's unfortunate that there isn't more unity between the two, as from a library-developer's perspective it would make this sort of thing much easier, but the DM team is pretty vocal about wanting the best API they can get, and not worrying about being hobbled by how AR does things. I don't particularly disagree.

ActiveRecordDataMapper
.find(:all, ...) .all(...)
.find(:first, ...) .first(...)
.find(id) .get(...)
.find_all_by_id(id).all(:id => ids)
.table_name .storage_name
.primary_key .key.first.name
.connection repository.adapter

Raw SQL

If you're running raw SQL queries, firstly, I'm sorry. Secondly, you want to run .query instead of .execute. Thirdly, if you care about getting the results of the query back, AR returns an array of arrays, DM returns an array of hashlike objects, so you want to map them for their values array. The hashlike object in question is order-preserving, so you'll get things out in the right order. If you're concerned, grab one of the result objects and verify that the keys array is in the correct order.

Hooks

ActiveRecord defines a few hook points, along the lines of before_create and after_save. DataMapper uses (a modified version of) the Extlib gem, allowing it to hook pretty much any method. The syntax is like before(:create) and after(:save). AR's hooks pass in the object to work with, DM's have the object available as self.

In before hooks, the AR hook chain stops if your method returns false, in DM you must throw :halt.

Things You Shouldn't Be Doing Anyways

If you're manually setting @attribute values in your AR code, you'll need to use instance_variable_set for DataMapper. I recommend writing manual accessor methods to wrap it for abstraction.

If you're wanting some arbitrary data structures back, I recommend using OpenStruct (require 'ostruct') to pass structured data back and forth. This was especially handy when I wanted some results from DM to look like AR, because I was just doing an adapter (bad me!) and the client code wanted to interact with the AR object. Just be aware that OpenStruct doesn't quite clear out all its methods, so you might want to define some custom readers anyway. I had problems with type in particular, which is a deprecated alias of class - redefining the method to return @table[:type] fixed that up nicely.

Porting Rails Plugins to Merb

We're about to start up a new project at work, and we've decided to go with Merb (yay!) rather than Rails. Before we get started, though, we wanted to make sure that we'd be able to integrate well with the various plugins available for Rails.

The first two libraries we wanted to use were ultrasphinx, an interface to the Sphinx fulltext search engine, and async-observer, an abstraction library using the beanstalkd work queue library to delay actions to be processed at a later time, rather than while processing the page.

The good news is that both of the projects are available on GitHub, which means easy forks, and easy contributions back to the source if my changes are as good as I think they are.

So, today I'd like to talk about the basic changes you'll need to make to a Rails plugin so that it will play nicely with Merb, and a few of the extra hooks that come into play. In the near future, I hope to provide a bit of a primer on adapting a plugin that uses ActiveRecord so that it will also work with Datamapper. Preview tip from that post, if you're calling any methods provided by ActiveRecord, please make an adapter class/module to pass those methods through, as it makes ports like these much easier.

Basics

To get your plugin to get picked up properly, Rails requires an init.rb file in the plugin root. The equivalent for Merb is a file named after your plugin, in the /lib directory. Copying /init.rb to /lib/async_observer.rb worked for that one, Ultrasphinx is somewhat better behaved in that its init.rb just required ultrasphinx, so both Rails and Merb worked for me out of the box.

If you depend on anything in Merb, you'll need to add to the docs that applications should add the plugin dependency inside a Merb::BootLoader.before_app_loads block - otherwise nothing in Merb is defined yet. This is of particular importance if you want to switch behaviour based on whether the Rails or Merb constants are defined. As Rails handles loading plugins itself, there's no concern for keeping things special for Rails.

If you plan on dealing with the app directory structure or environment, an easy way to do it is:

if defined?(Rails)
  ROOT = RAILS_ROOT
  ENV = RAILS_ENV
elsif defined?(Merb)
  ROOT = Merb.root
  ENV = (Merb.env == 'rake' ? 'development' : Merb.env)
end

The additional rake environment transparently proxies to the development db connection, so if you just want to compare your plugin's interpretation of ENV the above will make that cleaner.

Rake Tasks

Rails automatically loads any files matching tasks/*.rake in the plugin dir.

Merb needs to be told explicitly, relative to the lib directory. The canonical example from the docs is:

if defined?(Merb::Plugins)
  Merb::Plugins.add_rakefiles "merb_sequel" / "merbtasks"
end

Unfortunately, it seems that it wants specified a single file with a .rb extension, which is incompatible with the Rails Way. The easiest fix I've found is to add a file called tasks.rb under /lib, inside which you just manually require the individual rake files:

load File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'tasks', 'merb_sequel.rake'))

Do note that the load is necessary, require doesn't pick the file up properly.

Finally, if you have any tasks that depend on environment, the easiest way to get compatability with both frameworks is to add task :environment => :merb_env to your merbtasks.rb file.

Generators

I haven't looked into generators in too much depth, but the API between Rails::Generator::Base and Merb::GeneratorBase seem different enough to warrant not reusing the generation script. If you conditionally define a generator based on the defined?ness of those two base classes, you should be able to reuse all your generation templates, and both Rails and Merb look in the same place for generators, so that should be the only adaptation necessary.

ORM Integration

Check back next time, as I write up my experience writing an ActiveRecord shim for the latest DataMapper. It's vaguely ugly. I highly recommend if you're working on a plugin now that interacts with models to abstract any access to the database into a module, and include it appropriately. This goes double if you're using any AR magic ;)

Two Questions

Question #1: Why is Symbol#to_proc so popular?

class Array
  alias old_map map

  def map(method=nil, *args, &blk)
    return old_each(&blk) if block_given?
    old_map do |e|
      e.send(method, *args)
    end
  end
end

ary = %w(hello world)
puts ary.map(:reverse).join(" ")
puts ary.map(:*, 2).join(" ")
puts ary.map(:slice, 1, 3).join(" ")

It should be trivial to do this for the other common enumerable methods, the only places I see Symbol#to_proc used, anyway.

I posit that the answer is question 2.

Question #2: Why do I need to do this method hackery in Array instead of Enumerable? I can understand Array having its own definitions of the standard collection methods for performance reasons, but in a properly OO system, that should be transparent to me.

Embedding Gems in Merb

Daniel Manges did up instructions on storing explicit versions of gems in your rails app. If instead you're using merb, you probably want to do that too, as it makes for much easier deploys.

Thankfully, it's much less pain in merb:

gem install async-observer -i gems

The -i option tells rubygems to install the gems to that directory, and when you require the gem from inside merb, it will look in the local gems directory first, and find yours.

Optionally, you can skip generating docs by adding --no-rdoc --no-ri to the line, and depending on what you're installing, you may want to --ignore-dependencies as well.

If the gem in question builds a binary extension, you may be out of luck if you try to deploy it to a different architecture.

The only problem I've found so far is that gem cleanup won't accept a directory to clean, so you'll need to manually remove individual gems when you upgrade:

gem uninstall -i async-observer