Friday, November 16, 2012

Writing Rails Engines #1: Getting Started


In Rails 3, Engines are miniature Rails applications that you embed into your main application. I think they’re a great idea, more on that below, but they sure generate a lot of questions once you start trying to build them. A lot. I have notes for four posts at least. Here’s the first, how to get started.
Why Engines?
As an example, let me show you two screen shots. The first is the content management system from CrowdVine. I like a lot of the ideas inside of the CrowdVine version, but hate the implementation (I can say that because I wrote most of it). The second screen shot is from Yak, a simple programmer friendly CMS service that we’re working on. It has a near identical feature set but a cleaner implementation and much better UI. I want this new CMS to exist within CrowdVine. And whenever I make an upgrade to it, in Yak or CrowdVine, I want it that update available in my other app. This is what Engines are for.
Engines let you keep your feature together intact: models, controller, and views. It opens up a world of code libraries where the thing you’re trying to DRY up is UI heavy. Think about authentication, isn’t there a best practice for the design of a login screen with Twitter/Facebook options that you can just copy? YES! And in fact there are engines for that already, see omnisocial.
Creating an Engine
1. How do you create the boilerplate? Engines are just Gems with an /app directory, so you could use whatever method you already use for creating Gems. There’s also an application called enginex that will give you the basics, but I found I needed much more. So I started with a more robust engine example, omnisocial, and edited it. This isn’t optimal, but it only takes about 15 minutes and is a good way to learn how other people do it. After you’re done you should end up with a structure like this:
# Your typical Rails application structure app app/models app/views app/controllers app/helpers app/mailers  # Many people embed a rails application here in order # to make mocking up a test environment easier. test  # Engines are just Ruby gems and a lot of the setup lives # here, as well as your migration and asset files. lib lib/my_engine.rb lib/my_engine lib/my_engine/engine.rb lib/generators lib/generators/my_engine lib/generators/my_engine/templates lib/generators/my_engine/templates/assets lib/generators/my_engine/my_engine_generator.rb  # Having these two directories allows access to # rails generate script script/rails config config/routes.rb # Yes, your engine has it's own routes.  # If you generate your migrations they'll be created here # and you'll need to move them into your assets dir. db db/migrate/  VERSION # Example: 0.1.13 Gemfile # Just like any other Gemfile LICENSE # Apache is a good choice. README.rdoc my_engine.gemspec 
2. How can I have access to the Rails generator? Sometimes it’s helpful to be able to generate migrations or scaffolding. If you approach this like a Gem you’re not going to have any of the Rails app structure that allows you to call rails generate. But I also learned, through trial and error, that having too much of the structure causes conflicts with the main application, in particular you can’t have config/initializers. I’m not sure yet what the minimum is, but you’ll need script/rails and most of config/.
3. How do you develop your engine while developing your main app?Generally during development you want your main application to include a local development copy of your engine and in production to include a public version, for example one sitting on Github. Easy right? You should be able to add gem groups for each Rails environment to your Gemfile, like this:
group :development do gem "my_engine", :path => "../my_engine" end group :production do gem "my_engine", :git => "git://github.com/yourname/my_engine.git" end 
In Bundler 1.0 and earlier, that generates this error message: "You cannot specify the same gem twice coming from different sources." The work around, which I found from Cowboy Coded, is to use an environment variable. Seriously. This is supposed to be fixed in Bundler 1.1, but I haven’t been able to confirm this. Here’s the work around, which involves adding an ENV variable to your .bashrc and using that to control which library gets loaded from your Gemfile.
# Put this in your .bashrc # export MY_BUNDLE_ENV="dev" if ENV['MY_BUNDLE_ENV'] == "dev" gem "big_library", :path => "../big_library" else gem "big_library", :git => "git://github.com/tonystubblebine/big_library.git" end 
4. How do I run migrations or use other assets like CSS files and images?You can’t do anything useful unless you can run migrations from your engine and include some basic CSS. You need two things, a generator file that tells your main app what to do when you run rails generate my_module and template files that your generator copies into place.
Your asset files will live in lib/generators/my_engine/templates. Your generator, lib/my_engine/big_cms/my_engine_generator.rb, should look like this:
require 'rails/generators' require 'rails/generators/migration' class MyEngineGenerator < Rails::Generators::Base include Rails::Generators::Migration  def self.source_root File.join(File.dirname(__FILE__), 'templates') end  # Implement the required interface for Rails::Generators::Migration. # taken from http://github.com/rails/rails/blob/master/activerecord/lib/generators/active_record.rb def self.next_migration_number(dirname) #:nodoc: if ActiveRecord::Base.timestamped_migrations Time.now.utc.strftime("%Y%m%d%H%M%S") else "%.3d" % (current_migration_number(dirname) + 1) end end   def create_migration_files %w{create_dummy_table add_foo_fields_to_dummy_table}.each do |migration| migration_template "#{migration}_migration.rb", "db/migrate/#{migration}.rb" sleep(1) # cheap hack to make sure migration numbers end up being different end end  def copy_assets copy_file 'assets/stylesheets/my_module.css', 'public/stylesheets/my_module.css' end  def copy_config_files copy_file 'my_module_initializer.rb', 'config/initializers/my_module.rb' end end 
There are a couple of oddities above, most notably the handling of migrations. It seems like you have to implement next_migration_number yourself in order to get migration files with timestamps. Then, because Rails wants migration timestamps to be unique, I needed a delay after each migration to force different timestamps for each migration file. Talk about hacky. I assume that this stuff will just get cleaned up over time.
5. Where can I get more help? For now, these are the best two tutorials I could find, both from The Modest Rubyist, Rails 3 Plugins: Writing an engine and Rails 3 Plugins: Rake tasks, generators, and initializers
This looks like a black art, right? Either that or I'm doing things the wrong way (please let me know if there are better ways). This is supposed to be the easy part and we're already resorting to cheap hacks. We haven't even started talking about the magic parts--how to customize the engine from within your main application.

0 comments:

Post a Comment