The Problem

Using subdomains with Rails is kind of a pain. The only official support for subdomains is as asset hosts, as described in the AssetTagHelper documentation.

Matthew Hollingworth’s subdomain_routes covers most of the gaps in dealing with subdomains in Rails, including; generating subdomain routes, scoping resources to subdomains, and exposing subdomain information to controllers.

What I want to talk about is how I deal with subdomains in a development-environment context.

Hands off my /etc/hosts

Most of the resources I’ve seen regarding working with domain names in Rails involve manually modifying the system’s /etc/hosts file, though Eloy Duran’s PassengerPane makes it simple for those developing with Phusion Passenger.

Robby Russell calls out the gem ghost, which makes managing custom domains and subdomains drop-dead simple. The gem provides the ability to map domain names to arbitrary IPs, but in development you probably just want to map everything to the loopback address 127.0.0.1.

Putting it into practice

I like to have each application manage its own domain-to-ip mappings, so I need to make sure all of the domains the application will use are accessible. How you go about this is up to you, but here’s simple example showing how to use ghost to manage four cdn hosts, the www subdomain, and the canonical domain name from config/environments/development.rb.

if hostname = ENV['RAILS_HOSTNAME']
  require 'ghost'
  cdn_domain_templates = (1..4).map { |i| 'cdn%d.%%s' % i }
  (cdn_domain_templates + [ 'www.%s', '%s' ]).each do |template|
    Host.add(template % hostname)
  end
end

After starting your application in development mode, ghost will attempt to map these domain names if necessary. It escalates its permissions via sudo so you may be prompted to enter your password.

$ ghost list
Listing 6 host(s):
      cdn1.devdomain.com -> 127.0.0.1
      cdn2.devdomain.com -> 127.0.0.1
      cdn3.devdomain.com -> 127.0.0.1
      cdn4.devdomain.com -> 127.0.0.1
           devdomain.com -> 127.0.0.1
       www.devdomain.com -> 127.0.0.1

I typically leave these hostnames sitting around, but cleaning them up is as simple as issuing the command sudo ghost empty. The advantage to leaving the host entries in place is that the initial security escalation needed to establish the entries is bypassed on subsequent launches of the application.

Working in Parallels

If you use Parallels to run your application through its paces on Windows, it can be convenient to use the same domain names within the virtual machine and in OS X.

By default, virtual hosts within Parallels are set up to use Bridged Networking, causing the host to appear as a peer on the same LAN as the Mac on which it’s running. This means that in order to reach the Mac, the virtual host needs to know the Mac’s IP, which may be subject to change. To insulate yourself from this hassle, you can instead isolate the virtual host on a new subnet.

To do this, first go the Network Preferences in Parallels and enable DHCP for Shared Networking:
Parallels Global Preferences

Next, configure the virtual host to use Shared Networking:
Parallels Virtual Machine Preferences

Finally, update the virtual host’s hosts file, located at %SystemRoot%\system32\drivers\etc\ to map your application’s domains and subdomains to the IP belonging to the virtual network adapter managed by Parallels.

Configure Hosts

Now you should be able to access your application by hostname from the virtual host and can therefore copy and paste URLs between environments.

Another interesting technique I spotted in the gRaphaƫl code base is the use of a prefixed + used to cast a variable to a numeric type.

var data = [ undefined, null, 1, 10.2, new Date() ];
for (var i = 0, len = data.length; i < len; i++)
  console.log('cast %o: %o', data[i], +data[i]);

If you run this code, notice how casting undefined returns NaN which will ripple through your calculations. However, my favorite thing about this technique is how it handles Date objects. The following examples are equivalent.

var now = new Date();
console.log('now.valueOf: %o', now.valueOf());
console.log('+now: %o', +now);

Nice.

In hacking on the gRaphaĆ«l code base, I came across a recurring pattern syntax pattern I hadn’t encountered in JavaScript before.

Essentially, instead of using this very common conditional construct:

if (a) b;

The author uses:

!a && b;

Similarly,

if (a) b; else c;

Is written:

!a && b; a && c;

I was curious about the performance difference between the styles, and wrote a benchmark to compare them.

I only tested in WebKit nightly 53415, but I found that in every case, the if / if-else construct outperformed the pure-logic construct. The differences in performance are admittedly negligible, so I’ll be sticking to if-else for readability, and I’ll accept the performance benefit as a happy side-effect.

Cookies stored with a path option are treated somewhat differently in Internet Explorer than they are in many other browsers. The issues are especially prevalent in modern web applications with “clean” urls.

For example, a cookie stored with a path of /movies/the-wizard-of-speed-and-time is not visible to Internet Explorer users visiting /movies/the-wizard-of-speed-and-time, but is visible to Safari and to Firefox users.

The Wizard of Speed and Time

To deal with this issue it is necessary to issue the cookie with a path one directory higher than the specific page you’re looking at. Additionally, the cookie name needs to be disambiguated from other movie-specific cookies since the path scope /movies is no longer exclusive to a single movie.

Also remember, cookie paths are case-sensitive, so your application should either issue a cookie whose path matches the request the user issues, or the user should be redirected to a canonical, normalized url.

When business logic dictates that something only happens on Saturday, this snippet can help keep your code and tests legible.

module DatePredicates
  Date::DAYNAMES.each_with_index do |day_name, i|
    define_method "#{day_name}?" do i == wday end
  end
end
 
class Date
  include DatePredicates
end

Date.today.Tuesday?
=> true