Converting a more complex Rails 2.3 style plugin to non-deprecated under Rails 3.2

If you have a Rails plugin in your ./vendor/plugins/, Rails 3.2 warns you this is deprecated and future versions of Rails will not support this. The deprecation notice links you to an announcement that suggests you:

Extract your vendor/plugins to their own gems and bundle them in your Gemfile. If they’re tiny, not worthy of the own gem, fold it into your app as lib/myplugin/* and config/initializers/myplugin.rb.

Tiny or not, if your plugin did not include models, views or helpers, it was just some straight ruby logic in ‘lib’, simply moving the files to your ./lib instead of ./vendor/plugins is pretty straightforward, and Matt Coneybeare gives you a nice walkthrough. 

But what if your plugin does have Rails models, helpers, views, and you don’t want to completely rearchitect/refactor it, you just want to get it working non-deprecated in Rails 3.2 (and future releases) as simply as possible? And you don’t really want it to be an external gem, you want to keep it’s source in your main app source repo, same as it was with the plugin?

Turns out, this is pretty straightforward too — either you can make it a sort of ‘fake’ gem that’s actually just part of your app source repo, or you can even take it a step further and dispense with the ‘fake gem’ part. In either case, the key is Rails 3 Engines, which are pretty much the abstracted and more general replacement for the deprecated plugins.

My Situation

I’ve got a local homegrown plugin in, let’s call it,  ./vendor/plugins/my_widget.  This was written back in Rails 2.3 days.  Originally I thought eventually it would be abstracted into it’s own gem, that’s why it started as a plugin, to make it easy to turn into a gem later.

But, well, as these things go, it kind of turned into a mess. At this point, I’d never release it to the public as a gem as it is, it would need a significant rewrite or refactoring. And it’s not even used in any other local internal apps, just the one, and it’s probably going to stay that way.  Yeah, the code is a mess, and technical debt, and it ought to be fixed one way or another eventually — but it’s working, and I want to schedule refactor or rewrite on my own timeline, not Rails.

So how can I keep it working in future versions of Rails (without deprecation warnings in 2.3), with as little refactoring/rearchitecture as possible?  Turns out it’s not too bad.

Make it into a sort of fake gem

It turns out turning a Rails 2.3-style plugin (with models, views, and helpers) into a gem that works identically is pretty easy, thanks to Rails3 Engine functionality.

But I don’t really want to extract the code into an independent gem, in it’s own source repo. The plugin’s a mess, and ended up being kind of tightly coupled to the app. I don’t want the added dependency management of tracking which versions of the extracted gem go with which versions of the app, and this plugin is only going to be used with this app anyway!

Okay, no problem, let’s take advantage of bunder to treat it like a gem, but keep it locally.

First move it out of vendor/plugins (it was never third-party vendor code anyway!) , let’s just put it at the root directory for the Rails app for now:

git mv ./vendor/plugins/widget ./widget

Now you’ve got to add a `widget.gemspec` in ./widget, so we can treat it like a gem (even though the code just lives in the local app).  One easy way to do this is, in some other directory, use rails’ own `rails plugin new widget` command to create a gem skeleton, and then just copy the widget.gemspec from there into `$original_rails_app/widget/widget.gemspec`, fill out the details as appropriate.  Include a comment explaining what we’re doing, this is something that used to be a ./vendor/plugin, we’re changing in into something with the structure of a gem to transition it to modern Rails without deprecations.

Now in your Gemfile, just tell bundler to use this so-called ‘gem’, but at the local app source location:

gem "widget", :path => './widget'

Bingo, we made it into a (kinda sorta) ‘gem’, it’s code is available, we don’t get a Rails 3.2 deprecation warning anymore.

What about models/views/helpers?

But our models/views/helpers aren’t available to the app like they were in the plugin. How do we fix this? Turns out it’s easy, we just need to tell Rails to treat it like an Engine, and that’s as easy as dropping a placeholder class in.

You probably already have a ./widget/lib/widget.rb file that defines a module Widget from your old plugin. Just open up that file and add this to it:

module Widget
  class Engine < Rails::Engine
  end
  # all sorts of stuff you had already maybe goes here
end

That’s it, the existence of Widget::Engine which subclasses Rails::Engine tells Rails to treat this as an Engine, and all your ./app/models, ./app/views, and ./app/helpers will be available to your app just the same as they were when it was in ./vendor/plugins/widget.

There is additional configuration/customization of the Engine you can do, the Engine rdocs are pretty decent.

What about init.rb?

If you had an `init.rb` file before, you’re going to have examine it, some of it may no longer be relevant in Rails3, much of it is going to have to be rewritten to be appropriate for Rails3 rather than used verbatim — all  of it is going to have to be moved out of init.rb. Moved where?  Into your new Engine!  For instance, if you have some code that you simply want to run at startup time, here’s one way to do it:

module Widget
  class Engine << Rails::Engine
    initializer "widget" do
       do_at_startup
    end
  end
end

If you start getting more than a handful of lines of code inside your Widget::Engine, it probably makes sense to extract it into a seperate file. Just put it in ./widget/lib/widget/engine.rb, and add a `require “widget/engine”` to the top of `./widget/lib/widget.rb`.

Oops, lib isn’t auto-loaded anymore?

One additional difference is all your code in `./widget/lib` is no longer “auto-loaded”, you can’t just use it in your rails app, you need to explicitly `require` it for you.  That was a default change in Rails3 in general for ./lib, but it didn’t apply to old style ./vendor/plugins. But now it does. No problem, you can either change your code adding all sorts of `requires` everywhere, or you can just tell the engine to restore autoload for ./widget/lib!

module Widget
  class Engine < Rails::Engine
    # Mimic old vendored plugin behavior, marc_display/lib is autoloaded.
    config.autoload_paths << File.expand_path("..", __FILE__)

    #...
  end
  #...
end

Depending on whether you’ve left the engine in ./widget/lib/widget.rb or moved it to ./widget/lib/widget/engine.rb, you may have to muck about with the exact path.

Great, this works! We’ve switched our `vendor/plugins/widget` to a sort of fake local ‘gem’ , changing as little of the architecture as possible, keeping the app working and all tests passing, but without deprecation warnings.

Make it not a gem at all

Okay, that was sort of cheesy, having to give it a gemspec and add it to the Gemfile even though it’s not really a gem.

Can we get around that?  Turns out we can, easily.  What made our transition work was purely Rails::Engine, the fact that it’s a gem is almost irrelevant.

By making it a gem and listing it in the Gemfile, all that’s happening is that bundler is adding the gem to the ruby load path, and “require”ing the base ./widget/lib/widget.rb file.   (See the section on “bundler standalone” here for some info/example.) No other magic going on from bundler/rubygems, that’s it, Rails::Engine takes care of the rest.

Okay, we can do this ourselves, no problem.

Remove the gemspec. Remove the line from the Gemfile. Now we need to duplicate what bundler had been doing, and at about the same stage in the boot process so everything will work as expected. Here’s one way, in your ./config/application.rb:

module AppName
  class Application < Rails::Application
   #....

    config.before_configuration do
      $:.unshift File.expand_path("#{__FILE__}/../../widget/lib")
      require 'widget'      
    end

Bingo, this now should work just the same as when we made it a fake gem, but without needing to actually make it a fake gem.

While this seems less hacky, I’m not sure it’s actually less confusing especially for possible future maintainers.  Who’s gonna know to look for a `before_configuration` modifying load path in config/application.rb to understand what’s going on? Although hacky, at least people are used to checking Gemfiles and more likely to have some basic understanding of bundler and local paths for “gems”.

And let’s face it, this whole solution is hacky no matter what, we’re knowingly being kind of hacky to migrate our Rails 2.3-style plugin to something that Rails3 won’t complain about with as little rearchitecture as possible.

So personally, I actually prefer the ‘fake gem’ approach.

But it’s interesting and awfully nice to know that Rails will let us do whatever the heck we want here, there’s no magic in “gems”, the magic, if any, is all in Rails::Engine, and you can load your Rails::Engine however you want, so long as it gets loaded.

4 thoughts on “Converting a more complex Rails 2.3 style plugin to non-deprecated under Rails 3.2

  1. hii, myself khamar.
    I’m looking for a documentation on how to create a plugin in rails2.3.5
    if i ‘ve to create a plugin for an existing application with models and controllers and views.
    can you help me in it.

  2. Khamar, mainly, why would you want to do this? Plugins were a way to create ‘shareable’ code; you really need to create code which can be shared between multiple different Rails 2.3 apps? I would not bother creating a plugin for Rails 2.3, I’d just create ordinary application code — and I’d try to migrate off Rails 2.3 for an app that was still being developed, Rails 2.3 is no longer supported.

    But, if you really do want docs on plugins in Rails 2.3, I’d look at the Rails 2.3 version of the Rails Guides, which is kept online. Here’s the guide on plugins for Rails 2.3, although it’s kind of skeletal, the docs on plugins were improved in later versions of Rails. (In Rails 3, it became easier to create an ‘engine’ gem that served the purpose of a plugin; in Rails 4, a gem of this sort is the _only_ way to do it, straight non-gem plugins are no longer supported)

    http://guides.rubyonrails.org/v2.3.11/plugins.html

Leave a comment