Topic: Rails framework and shared code base for different clients

Our RoR application needs to be customised for each client. This does not mean that every bit of code is different for each client, but it does mean that there are some differences in data model, screens and behaviour.

For example, the dashboard for client x may need to show pending orders while client y wants to see recently dispatched orders, yet both clients want to see orders which are currently processed.

Clearly, the easiest way to do this is to create completely separate RoR applications and copy and paste code between them. But this approach has so many drawbacks that I'm not happy to go down this road. I'm after a far more elegant way to accomplish this.

Naturally I'd be inclined to create a base dashboard controller and client specific dashboard controllers for both clients (I already wrote about Subclassing controllers and views in the Rails forum and would apply the same technique here). With such hierarchies in place, I can maintain a shared code base and improvements made to shared code will benefit all clients.

This raises the question how the routs.rb file can determine which controller to instantiate. To make this task even more complex, on a development machine one would want to run both versions of this RoR application concurrently from a shared code base.

Ideally, I'd start the application by passing a command line argument to it that indicates the client. Using this data, the webserver (mongrel or webrick) could then be started using different yml files to initialise the RoR application and start the server(s) using non-conflicting ports.

If anybody has experience with this and would like to share their wisdom, I would hugely appreciate the feedback.

Pete

Re: Rails framework and shared code base for different clients

I can't believe I'm the only person who wants to use Rails to implement customized web applications.

Maybe I got no feedback because I didn't express what I'm trying to achieve clearly enough. So let me try a more pragmatic approach.

I want to run two slightly different flavors of my web server concurrently. I'd expect to specify the flavor and the port as command line arguments. For webrick, the startup commands might look like this:

ruby script/server --client A -p 3000
ruby script/server --client B -p 4000

So here are a few obstacles I have to overcome:

1. Where can I process the command line argument --client?

2. Can I use this to tell Rails which database.yml file to use?

3. Can I use this to tell Rails which routes.rb file to use?

Any input is greatly appreciated.

Pete

Last edited by petehug (2008-09-03 17:37:58)

Re: Rails framework and shared code base for different clients

You can:

RAILS_ENV=client_a ruby script/server

and then have in database.yml

client_a:
  #database info.

I'm not sure about the routes.rb file.

Re: Rails framework and shared code base for different clients

That was a good idea (though I'm using the MYAPP_TARGET environment variable to identify the flavor of the web server.

Finally, I got it working. Here is how:

1) At the top of environment.rb I have this code:

MYAPP_TARGET = ENV['MYAPP_TARGET']

case MYAPP_TARGET
  when 'FOO'
  when 'BAR'
  else
    raise "Environment variable MYAPP_TARGET missing or invalid!"
end

2) Also in environment.rb (somewhere in the global namespace) I make sure that the sessions don't get mixed up:

ActionController::Base.session_options[:session_key] = "#{MYAPP_TARGET}_#{RAILS_ENV}"

3) Still in environment.rb, I make sure that logging goes to the correct place and that the database.yml is picked up from the correct folder. The folders I want to use are:

RAILS_ROOT/log/FOO
RAILS_ROOT/log/BAR
RAILS_ROOT/config/FOO
RAILS_ROOT/config/BAR

I placed these statements inside the block that is invoked with Rails::Initializer.run do |config|:

config.log_path = "log/#{MYAPP_TARGET}/#{RAILS_ENV}.log"
config.database_configuration_file = "config/#{MYAPP_TARGET}/database.yml"

4) The routes.rb file is a bit tricky.

What I wanted to achieve was to define common routes in the standard RAILS_ROOT/config/routes.rb file, and routes specific to either FOO or BAR in separate files. Ultimately, I decided to do this by adding customroutes.rb files to the target specific config folders:

RAILS_ROOT/config/FOO/customroutes.rb
RAILS_ROOT/config/BAR/customroutes.rb

In environment.rb, I needed this line of code inside the block that is invoked with Rails::Initializer.run do |config| to ensure that the correct customroutes.rb can be found:

config.load_paths += %W( #{RAILS_ROOT}/config/#{MYAPP_TARGET} )

The customroutes.rb files are trivial. They look somewhat like this:

module CustomRoutes
  def self.addCustomRoutes(map)
    # Add your target specific routes here
    map.connect "dashboard", :controller => "FOO/dashboard", :action => 'show'
    # ...
  end
end

I modified the standard routes.rb file like this:

require 'customroutes'

ActionController::Routing::Routes.draw do | map |
  map.connect ':controller/service.wsdl', :action => 'wsdl'

  # Add custom routes
  CustomRoutes::addCustomRoutes(map)

  # default routes to follow
end

5) To allow implementing generic code as well as describing the subtle little differences, you should read this article.

Applying this technique, I can implement controllers like these:

RAILS_ROOT/app/controllers/dashboard_controller.rb: Generic DashboardController
RAILS_ROOT/app/controllers/FOO/dashboard_controller.rb: FOO spezialised DashboardController
RAILS_ROOT/app/controllers/BAR/dashboard_controller.rb: BAR spezialised DashboardController

I can also implement view templates in the correct location:

RAILS_ROOT/app/views/: Generic DashboardController templates
RAILS_ROOT/app/views/FOO/: FOO spezialised DashboardController templates
RAILS_ROOT/app/views/BAR/: BAR spezialised DashboardController templates

All this gives me a very powerful mechanism to overload precisely what needs overloading and no more. Of course, a bit of care needs to be taken to setup the routes correctly, but you'll no doubt find a clever way to do this yourself.

6) I had another small issue with my own vendor plugin that had slightly different flavours for the two targets FOO and BAR. I simply created target specific directories for target specific files:

RAILS_ROOT/vendor/plugins/mylib/lib/FOO
RAILS_ROOT/vendor/plugins/mylib/lib/BAR

In the RAILS_ROOT/vendor/plugins/mylib/lib/mylib.rb (which includes my library startup code) I added the following code:

MYLIB_BASE = File.expand_path(File.dirname(__FILE__))
MYLIB_CUSTOM = "#{MYLIB_BASE}/#{MYAPP_TARGET}"

$:.unshift MYLIB_CUSTOM

I can now have identically named files in RAILS_ROOT/vendor/plugins/mylib/lib/FOO and RAILS_ROOT/vendor/plugins/mylib/lib/BAR. When a file is referenced elsewhere with the require statement, it will load the file from the correct location.

7) In my case I wrote two command files (I'm a windows user) to start my server.

foo.cmd:

set MYAPP_TARGET=FOO
ruby script/server webrick -p 3000

bar.cmd:

set MYAPP_TARGET=BAR
ruby script/server webrick -p 3100

8) I did not address issues such as different graphic files, default templates, scripts, style sheets, etc. for different targets. I'm pretty certain though that similar techniques as applied and described above can be applied for that purpose.

Summary
Using the above technique, you can implement two or more slightly different front ends using the same code. The advantages are:

1) Enables you to describe the differences instead of copying and pasting nearly identical code resulting in reduced code and easier maintenance.
2) Allows you to test all versions concurrently so that you can verify a generic change has no adverse effect on one of the target implementations.

I hope this helps somebody.

Last edited by petehug (2010-03-24 18:18:59)

Re: Rails framework and shared code base for different clients

I just learned something that might come in handy for people wanting to do something similar as described in this thread.

A problem arose where I needed to run more than one flavor of my rails application concurrently under Windows.

While I could easily start the application from a shell by first setting an environment variable and then launching mongrel, in production we need to start mongrel via mongrel_service. Environment variables don't work here so we need to somehow pass to our rails application which flavor we want.

The trick is remarkably simple.

Since OptParse is used to parse command line arguments we can rely on a feature it has. Let's assume you start mongrel using this command:

mongrel_rails start TARGET=FOO -p3030

This will leave will leave "TARGET=FOO" as the only array element in ARGV while all else is removed. If you specified "-TARGET FOO" instead of "TARGET=FOO", OptParse would bark back at you complaining that -TARGET is not a recognized option. No such issues seem to arise for command line arguments that are not preceeded by a dash ('-').

To parse any "residue" arguments before rails is loaded, you can now implement a file config/preinitialize.rb. This module will be loaded even before environment.rb is loaded so it is the ideal place for us to parse such "residue" arguments.

We normally expect the value of TARGET to be set in an environment variable, but with this new knowledge we can now implement code in config/preinitialize.rb to set this environment variable if it is passed in as a "residue" argument:

ARGV.each do | arg |
  if arg.index('TARGET=')
    ENV['TARGET']=arg[7..-1]
  end
end

The same holds if you start rails using script/server via:

ruby script/server mongrel -p3030 TRAGET=FOO

or

ruby script/server webrick -p3030 TRAGET=FOO

This is important for me as currently I must use one of these options for debugging under Netbeans (6.8).

The only issue you might have is installing the service. If you're using:

mongrel_rails service::install -N MyApp_FOO TARGET=FOO ...

the service MyApp_FOO is registered, but if you look at the "Path to executable" with the SMC, you may see that TARGET=FOO is missing. You make the change manually using regedit by adding the missing bit to the string stored in this registry value:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\MyApp_FOO\ImagePath

I have submitted a patch to the mongrel_service developers to fix this issue so future mongrel_service gems (version 0.4 or later) shouldn't exhibit this problem.

Last edited by petehug (2010-04-14 17:10:50)