Coding with headphones

Jul 4, 2008 10:12am

Extending Your Rails Application

I played around yesterday for a few hours with a concept that I’ve recently been thinking about: application extension. I’ve been thinking about an application for a vertical market. This application would be more or less the same across various clients, but there might be small differences on a per-client basis.

So the question is, how can I write a Rails application where the main application is essentially like a generic framework, but be able to override certain features or add new features. This way, the bulk of the code would only need to be created once. The client-specific features could simply be plugged in.

Well, it turns out that it may be possible. As I see it, there are really only a couple of things that would need to be overridden or extended. Firstly, we have controllers. Controllers provide the functionality for using the application from a procedural, use-case perspective. Then we have models. Models represent the logic, data and behaviour of the application. Finally, we have views. Views provide the user interface and user experience within the application.

It seems to me that that it should be possible to alter dynamically the collection of controllers, models and views making up the basis of the core application.

The mechanism? Well, the ability to re-open Ruby classes of course. In my mind (and I’m sure that this is not a new concept), this could work using the following approach.

1) The Rails application loads normally.

2) We assume that any ‘extensions’ to the application will appear in a folder called ‘app_extension’ (or whatever) and that the directory structure of this folder mirrors that of the rails app. Visually, it could look something like this:

application/

app/

controllers/

helpers/

models/

views/

lib/

app_extension/

app/

controllers/

helpers/

models/

views/

lib/

3) In an initializer (which is run close to the end of the environment loading process, we check to see if this folder exists. If it does, we reopen ActiveSupport::Dependencies and reimplement a method called load_missing_constant. It could look something like the code show below (Note: this may not be the best approach, and I haven’t tested it beyond verifying that it works. Comments/suggestions are encouraged).

  def self.enhance_dependencies
ActiveSupport::Dependencies.class_eval do
#load up the stuff in the extension dir
ext_path = File.join("#{Rails.root}", "app_extension")
class_variable_set(:@@extension_dir, Dir["#{ext_path}/**/*.rb"].flatten.freeze)
alias_method :"old_load_missing_constant", :"load_missing_constant"
def load_missing_constant(from_mod, const_name)
extended = false
obj = old_load_missing_constant(from_mod, const_name)
unless obj.nil?
#get the name of the file
name = qualified_name_for(from_mod, const_name).underscore
#determine if there's a match in the extension dir
matches = class_variable_get(:@@extension_dir).select {|file| File.basename(file, ".rb") == name}
if matches && !matches.empty?
if !loaded.include?(File.expand_path(matches.first))
require_or_load(matches.first)
extend_view_path(obj) if controller?(obj)
extended = true
end
end
end
unless obj.respond_to?(:extended?)
mark_extended(obj, extended)
end
obj
end

private

def mark_extended(klazz, extended=false)
klazz.class_eval(
%{
def self.extended?
#{extended}
end
}
)
end

def controller?(klazz)
klazz.ancestors.include?(ActionController::Base)
end

def extend_view_path(klazz)
return unless klazz.respond_to?(:view_paths)
#Puts the app extension view path first in the controller's array
#of view paths so that it will be searched first.
klazz.view_paths.unshift(File.join("#{Rails.root}/app_extension", "app", "views"))
end
end
end

This code essentially aliases the original load_missing_constant and defines a new method that loads files within the app_extension directory. When an unloaded constant is encountered, we call the original method so that it is loaded properly, but we also search the files in the app_extension directory to see if there is a version there as well. If there is, we load it too. This effectively re-opens the class and adds new functionality or overrides existing functionality. Additionally, we add a method to the newly loaded class called extended? so that we can know in the regular application if the class has been modified by the extension. Also, if the newly loaded constant is derived from ActionController::Base, we inject the path to the views directory in the app_extension folder. The path must be at the beginning of the view_paths array so that it is searched first, effectively allowing us to override views. If the view exists in the app_extension view path, it will be used. If not, the default view path will be used.

So this gives us a rudimentary means to extend the base application. Is it perfect? I doubt it. For example, the only way to remove functionality would be to undefine a method, etc. Does it address everything? I doubt it. For example, it’s likely that there may be client-specific migrations that need to be run, etc.

Perhaps in my next post I’ll address the possibility of hosting client-specific migrations within app_extensions. I want to be able to run rake db:migrate and have them picked up. I’ll have to spend some time researching the possible approaches to this.

Page 1 of 1