As you might have heard by now, Michael Granger and I are working on a high-level development framework and web-based IDE for Ruby on Rails dubbed ' John Henry'. To ensure compatibility across Rails versions, John Henry's codebase will need to be layered cleanly on top of Rails and then the application can be written and maintained using John Henry's own web-based functionality.
I have a good mental picture of what I'm looking to accomplish, but pulling it off is going to take some serious hacking! I've already figured out that I need to dig deep into the inner workings of Rails (David and crew have written tons of clean code, but it's often very cryptic!) I think others might get serious benefit from these experiments as well, both as advanced documentation and to start writing packaged, reusable components for Rails. So based on that hope, I'm dedicating the time to writing up my experiments and results while I work.
The focus of this particular article is adding a library-based application to your normal Rails application. By library-based, I mean that I want to embed a whole Rails application (in my case John Henry's Hammer app) into the vendor directory so that its controllers, helpers, models and views are all available. However, since they are not meant to be customized by the developer, I don't want all those extra .rb files cluttering up the app/ directory.
I think the first thing I need to do today is to create a Rails app that will serve as my example application and proof-of-concept. I'm tempted to make it simple, perhaps even simply copy the Depot application in the Rails book. On the other hand, I'll quickly run out of features to mimic if I go that route, which means I'll have to start making them up off the top of my head. That is both a time-waster and leads to unrealistic examples.
Another option is to mimic an existing web application, preferably in a domain that hasn't been tackled in Rails yet. I really want something non-trivial and one that comes to mind immediately is ACIDPlanet, where I host some of my original compositions. I can see from the URL that it was written using ASP and I know from experience that it has tens of thousands of active users. Alright, I'll make a Rails version and call it folksong.
In the folksong/vendor directory, I drop in a HEAD version of Rails (aka EdgeRails) and I create the following new directory structure where I think my JohnHenry (JH, for short) code will live. I'm calling JH web-based code Hammer, get it?. ;-)
vendor/johnhenry
vendor/johnhenry/hammer
vendor/johnhenry/hammer/app
vendor/johnhenry/hammer/app/controllers
vendor/johnhenry/hammer/app/helpers
vendor/johnhenry/hammer/app/models
vendor/johnhenry/hammer/app/views
vendor/johnhenry/hammer/app/views/layouts
What to do now? Well, the first thing I need to get working is for the hammer application pieces to be available in the folksong application. "Wait a gosh-darned second!", I can hear some of you saying, "Why on earth aren't you using the Rails component model, you know, where you should be sticking those directories under the component/ directory of the app?"
Good question. Mostly it's because John Henry is not a pluggable component. You don't drop John Henry files into an existing app and use it where appropriate. You do use a John Henry script to bootstrap your application and from that point on, you can write the entire thing via your browser if you want. JH provides pluggable components at a higher abstraction-level that Rails, such as user management and task workflow. So without getting too much further into the implementation details of JH, I'll defend my choice with a desire to keep the JH code out of the way of the app code.
Okay, if I can get a simple HelloWorld example up and running with this configuration I think I'll be happy with my progress for today. I need to tell Rails to include the hammer code when running in development mode, so I head over to config/environment.rb and make some edits.
After some trial and error, I figure out that I need to add my johnhenry directories to ADDITIONAL_LOAD_PATHS and to add a line to the following block of code, which makes my controllers available to the dispatching system.
Controllers = Dependencies::LoadingModule.root(
File.join(RAILS_ROOT, 'app', 'controllers'),
File.join(RAILS_ROOT, 'components'),
File.join(RAILS_ROOT, 'vendor','johnhenry','hammer','app','controllers')
)
Heh, I just realized that I'm assuming that you know both Ruby and Rails pretty well, otherwise you wouldn't be interested in reading this stuff. I'm picking up the pace now, so don't say I didn't warn you.
Where were we? Oh yeah, let's get Hello world! to display. I go into my hammer/controllers directory and create a HelloworldController class that extends ActionController::Base. In that new controller I define a method named index with the single body line render :text => 'Hello world!'. Bingo! It works, just as expected.
I burned some time trying to get my John Henry controllers to properly find their templates before hitting a good solution. Before I get into the solution, I'll explain why I had a problem. Digging into ActionController::Base showed me that Rails uses a static variable to hold the path to the templates. In fact, it's pretty plain to see how it is used in environment.rb
[ActionController, ActionMailer].each { |mod| mod::Base.template_root ||= "#{RAILS_ROOT}/app/views/" }
I tried setting the template_root variable of my John Henry controller to be the following:
"#{RAILS_ROOT}/vendor/johnhenry/hammer/app/views"
However, that resulted in some strange behavior. The first time I hit my JH controller, it read the template properly, but the second time I hit it, my custom template path was gone. Like I said, strange. I tried figuring out what part of the Rails code overwrites that variable, but then the solution hit me. Just override self.template_root in my JH controller.
class HammerController < ActionController::Base
def self.template_root
"#{RAILS_ROOT}/vendor/johnhenry/hammer/app/views"
end
end
Wonderful! Now templates are working as intended. I do a quick check to make sure partials code works too. Yep, works as intended. Cool! That's enough for today.
In the next article I'll probably get into some of the first John Henry user-interface decisions and start figuring out how to store and manipulate a Rails-specific abstract syntax tree in Ruby. For the last few months I've been thinking that it would make sense to use some sort of RDF representation, but perhaps a pure Ruby solution is better.
Obie Fernandez
Originally published on obiefernandez.com in September 2005.