polymorphic association helpers, now with twice the helpfulness

I extensively updated my polymorphic has_many :through association helper plugin, and it’s worth a second look. You now get snazzy _push() and _delete() methods on the polymorphic collection:

example
petfood = Petfood.find(1)
petfood.eaters.map(&:class) # => [Dog, Cat, Cat, Bird]

petfood.eaters_push(Cat.create)
petfood.reload
petfood.cats.size # => 3
petfood.eaters.size # => 5

petfood.eaters_delete(petfood.dogs[0])
petfood.reload
petfood.dogs.size # => 0
petfood.eaters.size # => 4

# works both ways
petfood.eaters[0].petfoods.include?(petfood) # => true

And configuration is easier than even regular ActiveRecord:

usage
class Petfood < ActiveRecord::Base
  has_many_polymorphs :eaters, :from => [:dogs, :cats, :birds]
end

class EatersPetfood < ActiveRecord::Base
  belongs_to :petfood
  belongs_to :eater, :polymorphic => true
end

class Dog < ActiveRecord::Base
end

For more information and the plugin itself, go here.

a better way of building ruby, rails, lighttpd, mysql, and postgres on OS X tiger

motivation

basics

First, install the Apple Xcode tools from your OS X installation disc.

Second, install DarwinPorts from here. It’s just a regular Mac package.

Now go to the terminal (Applications/Utilities/Terminal) and type the following commands:

sudo port -d selfupdate
sudo port install lighttpd +ssl
sudo port install rb-rubygems
sudo port install rb-fcgi
sudo port install mysql4 +server

If rubygems doesn’t install properly, try Jamie’s tip.

If the mysql install fails, try setting your GCC version back to 3.3 for the duration of the command:

sudo gcc_select 3.3
sudo port install mysql4 +server
sudo gcc_select 4.0

The above trick may help you if other packages fail, too. You can try mysql5; I’m just not sure if the ruby mysql bindings are current enough.

rmagick or postgres or others

Also maybe you want rmagick (more information): sudo port install rb-rmagick

You could also use the gem (sudo gem install rmagick) if you need a more up-to-date version. The gem version may have more complicated dependencies, though.

For postgres, do: sudo port install postgresql81 +server

If you need other things, you can use port list whatever to browse the available DarwinPorts packages. whatever can contain wildcards (example: port list rb* ).

fix the system path

Next, add /opt/local/bin to the system PATH in the file /etc/profile, so that it looks as below. You might as well add /opt/local/sbin and the /usr/local/ folders while you’re at it. So in the end, the line should look like this:

PATH="/opt/local/bin:/opt/local/sbin:/usr/local/bin:\
/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin"

The PATH environment variable tells the system where to look (and in what order) for executable files that are not run with an explicit path. It is better to put it in /etc/profile, which is system-wide, as opposed to ~/.bashrc, which is specific to the current user.

gems

Please note; if installing gems fails with some kind of require error, try reinstalling the rb-rubygems port. It seems to not complete correctly the first time around for many people.

Rails, rake, mysql:

sudo gem install rails
sudo gem install rake

# do this all on one line
sudo gem install mysql -- --with-mysql-config=/opt/local/bin/mysql_config

You may have to use mysql_config4 or mysql_config5, depending.

Make sure to choose a ruby version of the mysql gem, and not win32. If you are having compile errors with the 2.7 version, and are using the mysql.com distribution of MySQL instead of the MacPorts distribution, this article might help you.

Capistrano:

sudo gem install capistrano

If you use postgres because of its awesomeness:

# do this all on one line
sudo gem install postgres -- --with-pgsql-include-dir=/opt/local/include/postgresql81
--with-pgsql-lib-dir=/opt/local/lib/postgresql81

make lighttpd start automatically

Next, you can configure a webserver daemons. This step is optional, and not very useful if you don’t need to develop under a more deployment-like environment. Normally script/server from the Rails directory will be your development web server.

The lighttpd configuration file lives at /opt/local/etc/lighttpd.conf. If you put this file in /Library/LaunchDaemons/, and load it into the launchd list via sudo launchctl load -w /Library/LaunchDaemons/net.lighttpd.plist, it will boot automatically at start-up. You can then stop and start the lighttpd launchd daemon with sudo launchctl start net.lighttpd, etc.

Note, loading the launchd item will fail if there isn’t a working configuration file at /opt/local/etc/lighttpd.conf.

make the db start automatically

Lastly, DarwinPorts will also put a file similar to org.darwinports.mysql5.plist or org.darwinports.postgresql81-server.plist in your /Library/LaunchDaemons/ folder, depending on which RDBMS you have decided to install. You can use this to start the database server automatically, just like starting lighttpd as mentioned above. You need to do all the same steps:

sudo launchctl load -w /LibraryLaunchDaemons/org.darwinports.postgresql81-server.plist
sudo launchctl start org.darwinports.postgresql81-server

Chances are, starting the server will silently fail. If you look at the .plist file with a text editor, you will notice that it is calling some wrapper scripts that are probably in /opt/local/etc/LaunchDaemons/ somewhere. If you execute these by hand, from the command line, you can see what the problem is. Usually it is expecting your actual database to be in some other location than where you put it, assuming you initialized the database at all. Resolve this either by modifying the wrapper script directly, or by adding the appropriate env variable to /etc/profile.

done

I am assuming that you will be able to load your schema and configure your database users/roles yourself.

Note, don’t try to run two instances of port in parallel, even if you have dual cores. They will fight.

Also, you will want an IDE/editor such as RadRails (yay!) or TextMate (hmm…). And you will need Firefox and probably want the following extensions: Web Developer, Tamper Data, Html Validator, FireBug, Tab Mix Plus, User Agent Switcher, ColorZilla, and MeasureIt.

Also, I recommend using a visual database modeler, even if you have to run it under Parallels. Your choices are mainly DBDesigner (mysql only, free), Toad Data Modeler (formerly CASE Studio, now free), Sybase PowerDesigner (expensive), and Rational Rose (expensive). You may expect your data model to stay simple, but chances are it won’t. The penalty for not understanding the complete implications of your data model is great; this is one place where the enterprise got it right. That said, do what you think is best.

really done

And now you are done, and all set up to use Prototype to make the best lightbox ever, even better than the five hundred other ones which are also the best ever.

This is all based on me piecing together the things that worked for me on OS X 10.4.6 (Intel). Let me know if you have trouble.

rmagick on OS X

Some people have complicated ways of installing RMagick on OS X. If I was using Solaris or something I would have to do that, but I’m not.

First, you need DarwinPorts. Just run the package. Then:

rmagick the easy way
sudo port install rb-rmagick

That’s all.

The big benefit to this is that you can uninstall the packages via port uninstall packagename if you need to. Building directly from the tarballs makes it almost impossible to uninstall.

Note, this will, however, install DarwinPorts versions of Ruby, Ruby Gems, etc. See the comments for more details. Rails works great with the DarwinPorts Ruby, though, and in the end it will save you a lot of trouble to switch over.

regular expressions in lighttpd host redirects

So, you have a rails app installed at, say, blog.site.com, and you want http://www.site.com and site.com to redirect to it. Here’s how to do it in lighttpd.

rewrite incoming urls

$HTTP["host"] =~ "^(www\.|)site\.com$" {
  url.redirect = (
    "^/(.*)" => "http://blog.site.com/$1",
    ""       => "http://blog.site.com/"
  )
}

This will forward visitors; their browsers will show blog.site.com. If you want Rails to listen on a group of virtual hosts (so that the hostname the user sees for the site is not changed), you can do a similar thing in the fastcgi.server section for that Rails instance, but leave out the rewrite rules.

listen on multiple hosts

$HTTP["host"] =~ "^(blog\.|www\.|)site\.com$" {
  # the rest of your site configuration here
}

Usually you would want the first, so that you can refer to a canonical hostname without confusing the user (is there one site or are there really two?). But there are some situations where you would have a number of virtually identical sites all run from the same Rails instance, and let Rails check ENV[‘HOST’] to make some minor customizations. A webmail provider who offers multiple hostnames would probably want this.

How does this work? Here’s a character-by-character explanation.

breakdown
$HTTP["host"]             # we will compare the regex to the
                          # requested hostname HTTP environment variable
=~                        # the regex match operator
"                         # the regex string must be quoted for lightty
^                         # match the beginning of the string
(                         # start a submatch
www\.                     # match "www."
|                         # logical OR
                          # match nothing at all (remember, the
                          # character to our left must be the beginning
                          # of the string, thus matching site.com but
                          # not mysite.com)
)                         # close the submatch
site\.com                 # match "site.com"
$                         # match the end of the string (for instance,
                          # "site.com.au" won't match, but would if you
                          # left out the $)
"                         # end the regex

If the above expression matches and returns true, then the block is executed:

cont.
 {                        # start block
  url.redirect = (        

The last line there asks to perform the comparison on the incoming url string, then assigns the result, below, to the redirect. This acts more or less as a function call, redirecting the client via a 301-moved (I think it’s 301) HTTP header. Anyway the important part is that it does it through official HTTP headers and not some wacky way or via an HTML META tag.

Now we begin what is effectively a SWITCH/CASE statement. If the string matches this on the left, return that on the right.

cont.
"^                        # start of string (the hostname is excluded;
                          # lightty treats it seperately from the URL)
/                         # match a regular forward slash
(.*)                      # match everything that comes after
" =>                      # return the following value
"http://blog.site.com/    # nothing unusual here
$1                        # what is this? 

Regarding the $1: notice that the “match everything” line above has parentheses around it. Another function of parens, borrowed from Perl, is to save the enclosed match to a variable. The first set goes into $1, the second into $2, etc. So what we end up doing is appending the /somewhere/on/your/site part of the requested URL to blog.site.com.

cont.
",
""                        # or match nothing (this only will be true
                          # when the site is accessed without a slash:
                          # "http://blog.site.com"
=> "http://blog.site.com/" # redirect to the home
  )
}

That’s all. Regular expressions are powerful.

polymorphic association helpers

notice

introduction

Josh Susser writes about how has_many :through does not support polymorphic associations even in edge Rails. He offers a number of workarounds.

Needing a lot of associations like this in a project, I wrote a module that wraps his second solution inside a helper function, just like the Rails built-in association helpers. It makes keeping track of which field acts as which name in which table a hundred times easier.

You also get snazzy .push() and .delete() methods on the polymorphic collection, too—no CRUD involved.

update

Chris Wanstrath turned my module into a plugin and also made the code a little more maintainable. Now I just need somebody to write in with comprehensive unit tests…

Download from here.

Because of the plugin, I removed the cut-and-paste module code from below. But I still recommend you look at the plugin source in order to get a better grasp on what exactly is getting defined behind the scenes.

usage

# this is the (typed) class that accepts the (untyped) polymorphic objects as children
class Petfood < ActiveRecord::Base
  has_many_polymorphs :eaters, :from => [:dogs, :cats, :birds]
end

# this is the join table
class EatersPetfood < ActiveRecord::Base
  belongs_to :petfood
  belongs_to :eater, :polymorphic => true
end

# this is an example of one of the child classes
class Dog < ActiveRecord::Base
  # nothing
end

You can use the :through key if your join table is not named in the standard (eaters_petfoods) way. Other ActiveRecord options should work, but may require minor modifications.

I eliminated the explicit association in the child classes entirely. It’s cooler and drier, like Antarctica. Also, it should warn you now if you try to create loops in your graph of polymorphic association relationships. Loops will not work without explicitly renaming the parent class in the association. These associations are all single-directional.

But now you can do:

example
petfood = Petfood.find(1)
petfood.eaters.map(&:class) # => [Dog, Cat, Cat, Bird]

petfood.eaters.push(Cat.create)
petfood.reload
petfood.cats.size # => 3
petfood.eaters.size # => 5

petfood.eaters.delete(petfood.dogs[0])
petfood.reload
petfood.dogs.size # => 0
petfood.eaters.size # => 4

# works both ways
petfood.eaters[0].petfoods.include?(petfood) # => true

If you want to avoid name conflicts on the secondary collection methods, you can pass in :rename_individual_collections => true and the polymorphic name will be prepended to the methods (e.g., petfood.eaters_cats instead of just petfood.cats).

Extra bonus: self-referential associations work. If you need to access the join table association for some reason from a class as a child of itself, though, you need to use join_table_name_as_child instead of just join_table_name; otherwise you get them going the opposite way.

I am proud. Let me know if you find bugs. It will print out a lot of debugging information on initialization if in development mode.