Hostname middleware for rails apps on multiple servers

August 15th, 2009 by Andrea Franz

If you run a rails application on multiple servers behind a load balancer, almost every time you make a request you have a response from a different host. If you want to know which host has sent back the response, you can use a middleware that adds the hostname in the page title of each request.

lib/hostname.rb:

class Hostname  
  TITLE_REGEXP = /(<title>)([^<]*)(<\/title>)/i
 
  def initialize(app, hostname="")
    @app          = app
    @title_suffix = " - on #{hostname}"
  end
 
  def call(env)
    status, headers, response = @app.call(env)
    add_hostname(response, headers) if headers["Content-Type"] =~ %r{text/html}
    [status, headers, response]
  end  
 
  def add_hostname(response, headers)
    response.each{|s| s.sub!(TITLE_REGEXP, "\\1\\2#{@title_suffix}\\3") if s =~ TITLE_REGEXP}
    headers["Content-Length"] = (headers["Content-Length"].to_i + @title_suffix.length).to_s
    nil
  end
end

To use it, add this line in your environment.rb:

config.middleware.use "Hostname", %x"hostname".chomp

When you don’t need it anymore you can simply remove the line above.


Testing rails generators with Cucumber

August 11th, 2009 by Andrea Franz

In the last days I switched a lot of old projects to use Cucumber, then I started writing test for Web App Theme. Here I haven’t models or controllers to test, because it’s just a generator, but I found enjoyable the use of plain text features to describe the ThemeGenerator behavior:

Feature: Layout generation
  In order to create a great application
  I should be able to generate a layout with Web App Theme
 
  # script/generate theme
  Scenario: Generate a layout    
    Given I have a new rails app
    And I have no layouts
    And I have no stylesheets
    When I generate a theme
    Then I should have a layout named "application.html.erb"
    And I should have a stylesheet named "web_app_theme.css"
    And I should have a stylesheet named "web_app_theme_override.css"
    And I should have a stylesheet named "themes/default/style.css"
 
  # script/generate theme admin
  Scenario: Generate a layout with a name
    Given I have a new rails app
    And I have no layouts
    And I generate a theme with name "admin"
    Then I should have a layout named "admin.html.erb"
 
  # script/generate theme --theme="drastic-dark"
  Scenario: Generate a layout choosing a theme
    Given I have a new rails app
    And I have no stylesheets
    And I generate a theme choosing the "drastic-dark" theme
    Then I should have a stylesheet named "themes/drastic-dark/style.css"
 
  # script/generate theme --theme=bec --no_layout
  Scenario: Generate only stylesheets without layout
    Given I have a new rails app
    And I have no layouts
    And I generate a theme without layout choosing the "bec" theme
    Then I should have a stylesheet named "themes/bec/style.css"
    But I should not have any layouts
 
  # script/generate theme --app_name="My New Application"
  Scenario: Generate layout with application name
    Given I have a new rails app
    And I have no layouts
    And I generate a theme with application name "My New Application"
    Then the layout "application.html.erb" should have "My New Application" as page title
 
  # script/generate theme --type=sign
  Scenario: Generate layout for signin and signup
    Given I have a new rails app
    And I have no layouts
    And I generate a theme for signin and signup
    Then I should have a layout named "sign.html.erb"
    And I should have a layout named "sign.html.erb" with just a box

Here my steps:

Given /^I have a new rails app$/ do
  generate_rails_app
end
 
Given /^I have no layouts$/ do
  remove_layouts  
end
 
Given /^I have no stylesheets$/ do
  remove_stylesheets
end
 
Given /^I generate a theme$/ do
  generate_layout(:theme)  
end
 
Given /^I generate a theme with name "([^\"]*)"$/ do |name|
  generate_layout(:theme, name)
end
 
Given /^I generate a theme choosing the "([^\"]*)" theme$/ do |theme_name|
  generate_layout(:theme, :theme => theme_name)
end
 
Then /^I should have a layout named "([^\"]*)"$/ do |filename|
  layout_exists?(filename).should be_true  
end
 
Then /^I should have a stylesheet named "([^\"]*)"$/ do |filename|
  stylesheet_exists?(filename).should be_true    
end
 
Given /^I generate a theme without layout choosing the "([^\"]*)" theme$/ do |theme_name|
  generate_layout(:theme, :theme => theme_name, :no_layout => true )
end
 
Then /^I should not have any layouts$/ do
  layouts_count.should == 0
end
 
Given /^I generate a theme with application name "([^\"]*)"$/ do |name|
  generate_layout(:theme, :app_name => name )
end
 
Then /^the layout "([^\"]*)" should have "([^\"]*)" as page title$/ do |layout, title|
  layout_title(layout).should == title
end
 
Given /^I generate a theme for signin and signup$/ do
  generate_layout(:theme, :layout_type => :sign)
end
 
Then /^I should have a layout named "([^\"]*)" with just a box$/ do |layout|
  layout_with_box?(layout).should be_true
end

Basically I create a temp folder that I use as fake rails root, and there I launch the generator. After each feature I remove that folder.

And here my env.rb

$:.unshift(File.dirname(__FILE__) + "/../../rails_generators")
require "rubygems"
require "rails_generator"
require 'rails_generator/scripts/generate'
require "fileutils"
require "theme/theme_generator"
 
web_app_theme_root  = File.join(File.dirname(__FILE__), "/../../")
tmp_rails_app_name  = "tmp_rails_app"
tmp_rails_app_root  = File.join(web_app_theme_root, tmp_rails_app_name)
 
Rails::Generator::Base.append_sources(Rails::Generator::PathSource.new(:plugin, "#{web_app_theme_root}/rails_generators/"))
 
module GeneratorHelpers
  def generate_rails_app
    FileUtils.mkdir(File.join(@app_root))
  end    
 
  def remove_layouts
    FileUtils.rm_rf(File.join(@app_root, "app", "views", "layouts"))
  end
 
  def remove_stylesheets
    FileUtils.rm_rf(File.join(@app_root, "public", "stylesheets"))
  end
 
  def generate_layout(*args)
    options = !args.empty? && args.last.is_a?(Hash) ? args.pop : {}
    options.merge!({:destination => @app_root, :quiet => true})    
    Rails::Generator::Scripts::Generate.new.run(args, options)
  end
 
  def layouts_count
    Dir[File.join(@app_root, "app", "views", "layouts", "**", "*.erb")].size
  end
 
  def layout_exists?(filename)
    File.exists?(File.join(@app_root, "app", "views", "layouts", filename))
  end
 
  def stylesheet_exists?(relative_path)
    File.exists?(File.join(@app_root, "public", "stylesheets", relative_path)).should be_true
  end
 
  def layout_title(layout)
    File.open(File.join(@app_root, "app", "views", "layouts", layout), "r").read.match(/<title>([^<]*)<\/title>/)[1]
  end
 
  def layout_with_box?(layout)
    File.open(File.join(@app_root, "app", "views", "layouts", layout), "r").read =~ %r|<div id="box">|
  end
end
 
Before do
  @app_root = tmp_rails_app_root  
end
 
After do
  FileUtils.rm_rf(tmp_rails_app_root)
end
 
World(GeneratorHelpers)

2 minutes admin layout with rails and the web-app-theme generator

July 30th, 2009 by Andrea Franz

Many people found out a rails generator inside my web-app-theme project and asked me how to use it.
Here an example, starting from scratch with a new rails app that manages music Albums.

rails cool_albums
cd cool_albums
script/generate scaffold Album name:string artist:string date:date
rake db:migrate

After creating the first controller with a scaffold or with your hands, start creating a theme:

script/plugin install git://github.com/pilu/web-app-theme.git
script/generate theme application --app_name="My Cool Albums" --theme="drastic-dark"

The first argument (“application”) is the name of the layout that the generator will create (application.html.erb).
The –app_name option specifies the name used as page title, and with the –theme specifies which theme to use among all the available themes inside the plugin.

Now remove the default index.html created by rails and the layout created by the scaffold:

rm app/views/layouts/albums.html.erb 
rm public/index.html

Add the following line in your routes.rb to set the default page of the application:

map.root :controller => :albums

Start the server

script/server

Ok, the layout has been successfully created, but we need to apply a theme for each one of the views generated by the scaffold.

script/generate themed albums album --layout=application --with_will_paginate

With the first 2 arguments I specified the controller path (albums) and the model used (album).
The –layout options is used by the themed generator to know where to add the Albums menu link.

Since we want to use will paginate (we set the –with_will_paginate option), we need to change one line in our albums controller from:

@albums = Album.all

to:

@albums = Album.paginate(:per_page => 10, :page => params[:page])

Here a trick to show form error messages inside the auto generated forms, you can add the following lines in your environment.rb:

ActionView::Base.field_error_proc = Proc.new do |html_tag, instance| 
  if html_tag =~ /<label/
    %|<div class="fieldWithErrors">#{html_tag} <span class="error">#{[instance.error_message].join(', ')}</span></div>|
  else
    html_tag
  end
end

Ok, restart your server and you are done.

Feel free to fork the project from github to improve the generator or to add a new theme.