June 23rd, 2008
SubdomainFu: A New Way To Tame The Subdomain
An extremely common practice for Rails applications is to provide keyed access through subdomains (i.e. http://someaccount.awesomeapp.com/). However, there has never been a real unified convention for handling this functionality. DHH’s Account Location works for some circumstances but is more tailored for a Basecamp domain model (i.e. the app is on a separate domain from all other functionality, so you can always expect a subdomain) than the more common usage of one domain only.
SubdomainFu aims to provide a simple, generic toolset for dealing with subdomains in Rails applications. Rather than tie the functionality to something specific like an account, SubdomainFu simply provides a foundation upon which any subdomain-keyed system can easily be built.
Usage Fu
SubdomainFu works by riding on top of the URL Rewriting engine provided with
Rails. This way you can use it anywhere you normally generate URLs: through
url_for, in named routes, and in resources-based routes. There’s a small
amount of configuration that is needed to get you running (though the defaults
should work for most).
To set it up, you can modify any of these settings (the defaults are shown):
# in environment.rb
# These are the sizes of the domain (i.e. 0 for localhost, 1 for something.com)
# for each of your environments
SubdomainFu.tld_sizes = { :development => 0,
:test => 0,
:production => 1 }
# These are the subdomains that will be equivalent to no subdomain
SubdomainFu.mirrors = ["www"]
# This is the "preferred mirror" if you would rather show this subdomain
# in the URL than no subdomain at all.
SubdomainFu.preferred_mirror = "www"
Now when you’re in your application, you will have access to two useful
features: a current_subdomain method and the URL Rewriting helpers.
The current_subdomain method will give you the current subdomain or
return nil if there is no subdomain or the current subdomain is a mirror:
# http://some_subdomain.myapp.com/
current_subdomain # => "some_subdomain"
# http://www.myapp.com/ or http://myapp.com/
current_subdomain # => nil
# http://some.subdomain.myapp.com
current_subdomain # => "some.subdomain"
The URL rewriting features of SubdomainFu come through a :subdomain option
passed to any URL generating method. Here are some examples (in these examples,
the current page is considered to be ‘http://intridea.com/’):
url_for(:controller => "my_controller",
:action => "my_action",
:subdomain => "awesome") # => http://awesome.intridea.com/my_controller/my_action
users_url(:subdomain => false) # => http://intridea.com/users
# The full URL will be generated if the subdomain is not the same as the
# current subdomain, regardless of whether _path or _url is used.
users_path(:subdomain => "fun") # => http://fun.intridea.com/users
users_path(:subdomain => false) # => /users
While this is just a simple set of tools, it can allow the easy creation
of powerful subdomain-using tools. Note that the easiest way to locally
test multiple subdomains on your app is to edit /etc/hosts and add
subdomains like so:
127.0.0.1 localhost subdomain1.localhost subdomain2.localhost www.localhost
Adding an entry for each subdomain you want to use locally. Then you need to flush your local DNS cache to make sure your changes are picked up:
sudo dscacheutil -flushcache
Installation
SubdomainFu is available both as a traditional plugin and as a GemPlugin for Rails 2.1 and later. For a traditional plugin, install like so:
script/plugin install git://github.com/mbleigh/subdomain-fu.git
For a GemPlugin, add this dependency to your environment.rb:
config.gem 'mbleigh-subdomain-fu', :source => "http://gems.github.com/", :lib => "subdomain-fu"
Implementing A Simple Account Key System
Let’s take this functionality and implement a simple account-key system based off of the subdomain. We’ll start with some controller code (assuming that we have an Account model with a ‘subdomain’ field):
class ApplicationController < ActionController::Base
protected
# Will either fetch the current account or return nil if none is found
def current_account
@account ||= Account.find_by_subdomain(current_subdomain)
end
# Make this method visible to views as well
helper_method :current_account
# This is a before_filter we'll use in other controllers
def account_required
unless current_account
flash[:error] = "Could not find the account '#{current_subdomain}'"
redirect_to :controller => "site", :action => "home", :subdomain => false
end
end
end
That’s really all we need for a basic setup, now let’s say we have a
ProjectsController that you must specify an account to access:
class ProjectsController < ApplicationController
# Redirect users away if no subdomain is specified
before_filter :account_required
end
There’s lots more you can do with the plugin, but this is a simple use case that everyone can relate to.
Resources and Plans
A feature that I hoped would make it to the first release of SubdomainFu
but is now a planned feature is subdomain-aware routing so that you can
add conditional subdomain routes to your routes.rb file. Keep an eye
out for more on that in the future.
In the meantime, the project will live at its home on Acts As Community for intermittent updates, is available on GitHub as always, and bugs/feature requests may be passed on through the Lighthouse.
June 11th, 2008
GemPlugins: A Brief Introduction to the Future of Rails Plugins
The new Gem Dependencies of Rails 2.1 give developers an easier-than-ever ability to keep track of and maintain the various library dependencies inherent with any project. However, a much-overlooked additional feature of the Gem Dependencies is the ability to package traditional Rails plugins as a gem and have them hooked in properly. This article is designed as an introduction to how to write and use plugins as gems in Rails projects.
The Basics
The basic method by which this is achievable is that any plugin included through a config.gem command will automatically have the gem-packed file rails/init.rb run upon Rails’s initialization. All it takes is a little bit of effort, and any Rails plugin can be packaged as a gem and easily depended upon through gem dependencies.
You may be wondering why this is a “big deal.” Plugins are already dead simple to install in Rails (and you can even script/plugin install straight from Git now!), why do we need GemPlugins? It’s simple, really: RubyGems are a rock-solid established way of easily distributing versioned reusable bits of code. Using gems for plugins allows for a greater standardization of the way in which plugins are maintained and distributed, as well as a simple path for version-locking to ensure compatibility with legacy code etc.
Another reason that GemPlugins are important is that they provide a level of abstraction from Rails: by releasing a gem rails/init.rb you could also use the same exact code to release a Merb plugin or any other framework that supports gemified add-ons. I think you will begin to see a number of cross-framework plugins be developed as Rails gets some company and shares alike.
Using a GemPlugin
First, let’s go through the process required to use an existing gem plugin. I’m going to be using my Acts As Taggable On plugin as an example throughout because I just recently went through the process of making it available as a gem.
First, you will need to include the dependency in your environment.rb file. I’m assuming here that most plugins are going to be hosted on GitHub, but the same should be true for any gem source.
# in environment.rb
config.gem "mbleigh-acts-as-taggable-on", :source => "http://gems.github.com", :lib => "acts-as-taggable-on"
This is the standard usage of gem dependencies, and for more info on this you can see Ryan Daigle’s post or watch the RailsCast on the subject. Now assuming that you don’t already have the gem in question installed, it’s simple to grab it:
rake gems:install
This will automatically install any gem dependencies in your project, and will tell you what’s happening the same as if you had run gem install from the command line.
That’s it! Once you have successfully installed the necessary gem, you can simply start up your Rails server and the plugin will be loaded and initialized as though it were living in your vendor/plugins directory.
Now that you know how to use a GemPlugin, I’ll show you how you can take an existing plugin and gemify it quickly and painlessly.
Making a GemPlugin
Let’s say I have a plugin called awesome_fu that lives on GitHub at mbleigh/awesome-fu. I’ve already released this plugin, it works great, and now I want to make it compatible with GemPlugins.
First, let’s create a gemspec called awesome-fu.gemspec in line with the requirements for the GitHub Gem Repository. In order to make the file list, I usually find it’s easiest to “find **” in the plugin directory, then copy it into TextMate, make the modifications I need for manifest (using a regular expression to quote each of the files), and saving it in the spec. If you have only a few files in your plugin, it may be easier just to add them by hand.
Next we need to add rails/init.rb. This is a little bit troublesome, because we still want our plugin to work if installed through the traditional method, so we also need init.rb to run the same code (this is automatically fine in edge Rails). What I did for my plugin is copy all of my init.rb code into rails/init.rb and then change init.rb to the following:
require File.dirname(__FILE__) + "/rails/init"
Now they both run the same code without any kind of replication, great! This means that now I have set up my plugin to work equally as a GemPlugin or a traditional plugin with just a couple minutes of work.
All that’s left to do is switch on the RubyGem setting for my GitHub project, update the README, and push! Now anyone will be able to easily require the plugin as a gem dependency and you will get all of the accolades associated with releasing your plugin the “new and hip” way.
Caveat Coder
The one problem with GemPlugins that I have run into is that if you unpack your gems using “rake gems:unpack” the rails/init.rb file is not run on initialization. This is a known issue that is supposed (?) to be resolved but I have still experienced this problem in my experiments. Hopefully this issue will be fully resolved in edge Rails soon and the glorious future of GemPlugins can begin.
June 9th, 2008
Acts As Taggable On Grows Up
Acts As Taggable On (original post here), the tagging plugin with custom tag contexts, has gathered up some great new features over the past weeks thanks to the efforts of the community as well as fellow Intrideans Pradeep Elankumaran and Brendan Lim. I just wanted to take this opportunity to go over some of what’s new and interesting in the world of acts_as_taggable_on.
Community Fixes
First, Peter Cooper was kind enough to submit a patch that allows acts_as_taggable_on to work with Rails 2.1’s named_scope when using find_options_for_tag_counts.
Secondly, the much requested support for Single Table Inheritance is finally in! It was just a matter of using a class inheritable attribute instead of a class instance variable, and big thanks to slainer68 for hunting that down and taking the time to submit a patch.
If there’s anything you’ve hacked on to Acts As Taggable On, I urge you to submit a patch to the Lighthouse Project. I try to get new patches integrated into the codebase as quickly as possible, so please do submit anything!
During the Community Code Drive at RailsConf two great features were added: taggers and related objects.
Taggers
Tags can now have ownership, allowing for such things as User-tracked tags and more. This was a requested feature and something that I’d been looking forward to myself. Here’s the usage:
class User < ActiveRecord::Base
acts_as_tagger
end
class Photo < ActiveRecord::Base
acts_as_taggable_on :locations
end
@some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations)
@some_user.owned_taggings
@some_user.owned_tags
@some_photo.locations_from(@some_user)
Find Related
Another request (and another great idea) is the ability to find related objects by similar tags. This is now available through the @object.find_related_on_tags syntax:
@bobby = User.find_by_name("Bobby")
@bobby.skill_list # => ["jogging", "diving"]
@frankie = User.find_by_name("Frankie")
@frankie.skill_list # => ["hacking"]
@tom = User.find_by_name("Tom")
@tom.skill_list # => ["hacking", "jogging", "diving"]
@tom.find_related_on_skills # => [<User name="Bobby">,<User name="Frankie">]
@bobby.find_related_on_skills # => [<User name="Tom">]
@frankie.find_related_on_skills # => [<User name="Tom">]
Gemified!
Acts As Taggable On now works as a GemPlugin in Rails. This is a new way (as of Rails 2.1) of distributing plugins as gems and having them still automatically link up and do their magic. To use it as a gem, add it to your config/environment.rb like so:
config.gem "mbleigh-acts-as-taggable-on", :source => "http://gems.github.com", :lib => "acts-as-taggable-on"
Now you should be able to get the latest version of the plugin just by running rake gems:install. However, this hasn’t been working for me so the alternative is just to install the gem directly:
gem install mbleigh-acts-as-taggable-on --source http://gems.github.com/
Now when you run your Rails app, even though it’s not in vendor/plugins it should be running! To make sure, look for this line on startup:
** acts_as_taggable_on: initialized properly
There are still a couple of issues outstanding in Rails regarding GemPlugins (if you unpack it, it will not run the initialization properly for some reason), but I wanted to give everyone the latest and greatest way to install the plugin possible. It will still work fine using the conventional methods as well.
Community and Future
I’ve been really happy with the response and support of the community, and I would like to do everything possible to cultivate future participation. To that end, I have created an Acts As Community Project for acts_as_taggable_on that will hopefully provide some casual communication about the project. Feel free to post on the wall or in the forums, and look out for additions soon.
Finally, the area of the plugin that still needs some work is tag caching. This is not a particular area of my expertise, so I’m hoping that someone from the community will write up some specs that flesh out the caching functionality in new and interesting ways.
Thanks for all of the patches, and I hope you continue to enjoy using Acts As Taggable On!
UPDATE (6/10/08): The improvements keep on rolling! After writing the post, I went off on a tangent and decided to make the plugin work both traditionally and as a gem. See more details above in the “Gemified” section.