Making #composed_of more useful

plugin | rails November 15 2006

Update: my patch finally got added to edge rails.

Active Record allows you to abstract fields into an aggregate object by using the composed_of declaration. This is handy, but the current implementation can be a real pain.

The first and somewhat trivial issue is the composed_of declaration builds a string and evals it to define the attribute accessors. It’s not a big deal, but it’s dirty. The second issue is that aggregate objects are not easy to manipulate in Rails, especially in forms.

Fixing it

So, I’ve written a plugin that overrides the Active Record implementation of composed_of, which allows you to specify a block to convert incoming parameters to the correct type.

Personally, this has been most helpful when using the Money gem:

class Account < ActiveRecord::Base
  composed_of :balance, :class_name => "Money", :mapping => %w(cents cents) do |amount|
      amount.to_money
  end
end

If #balance= receives anything besides a Money object, it will call the block to try to convert the parameter to a Money object.

>> account = Account.new :balance => 100
>> account.balance
=> #&lt;Money:0x2612770 @cents=10000, @currency="USD"&gt;

And now it can transparently be used in forms:

  <%= text_field :account, :balance %>

This can even be used for more advanced aggregations:

class User < ActiveRecord::Base
  composed_of :address, :class_name => "Address"
        :mapping => [%w(street street), %w(city city), %w(state state), %w(zip zip)] do |addr|
    Address.new(addr[:street], addr[:city], addr[:state], addr[:zip])
  end
end

A user can now be created from a hash:

  User.new(:address => {:street => "123 A Street", :city => "Somewhere", :state => "NO", :zip => 12345})

I didn’t quite realize all the implications of this extension until I was writing up the docs for this plugin. Active Record magically does type casting for a limited set of types, namely dates and numbers. But this essentially allows you to have a form of type casting for any attribute. Interesting…

Installation

This has also been submitted as a patch to the Rails trac but hasn’t been accepted yet.

  script/plugin install http://source.collectiveidea.com/public/rails/plugins/composed_of_conversion
posted by brandon | updated October 24th 12:02 AM
comments feed

9 comments

  1. Thanks for this article. I am currently facing a puzzle: is it possible to create ActiveRecord aggregations for objects whose attributes include other objects? If I don’t need to be able to access the attributes of the sub-object, can I just declare the object column as type :binary and be done with it? The Rails docs are mum on this so any hints would be greatly appreciated!

    jake jake
    December 04, 2006 at 02:39 PM
  2. jake:

    I’m not sure that I get what you mean, could you give an example?

    Brandon Brandon
    December 08, 2006 at 02:35 PM
  3. i had been banging my head on my desk for an hour or so, but you came to my rescue. Hail the liberator! Hail Brandon! I hope your patch gets into rails.

    jerry rirchardson jerry rirchardson
    January 12, 2007 at 10:54 AM
  4. Thanks for this—it was a big help.

    What I’d like to be able to do is to pass the value from an instance method that I create into the mapping of “composed_of”.

    In both the Rails source and your plugin, the value that gets passed from the model containing the composed_of call into the other model has to be in @attributes.

    I made it work by simply changing the read_method method slightly. Instead of reading the attribute, I just call send and pass it the first element of the the mapping.

    read_attribute(pair.first)

    to

    send(pair.first)

    in

    
            def reader_method(name, class_name, mapping, allow_nil)
              module_eval do
                define_method(name) do |*args|
                  force_reload = args.first || false
                  if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
                    # instance_variable_set("@#{name}", class_name.constantize.new(*mapping.collect {|pair| read_attribute(pair.first)}))
                    instance_variable_set("@#{name}", class_name.constantize.new( *mapping.collect do |pair| send(pair.first) end ))
                  end
                  return instance_variable_get("@#{name}")
                end
              end
            end        
    

    Can you think of any negative effects that this might have? Is there a better way to do it?

    Thanks!

    Dave Bryand Dave Bryand
    June 21, 2007 at 12:31 PM
  5. Dave,

    The only issue I can think of is if you want the composed_of object to use the same name as an attribute. For example, if you have a field called balance that you want to be replaced by the composed_of.

    Other than that, I think it’s a good idea.

    Brandon Brandon
    June 22, 2007 at 02:42 AM
  6. Sweet—I don’t think that will be an issue in my case.

    Thanks!

    Dave Bryand Dave Bryand
    June 26, 2007 at 02:21 PM
  7. Brandon, I’ve been using your patch for some time and nursing it along with an update lately (update available on dev.rubyonrails.org). I’m tired of this being a patch since it fixes an egregious problem with nil values as well.

    Any chance you can shake the trees by posting to the Rails core Google Group? I’ll pile on and back you up, and I know Ian White will as well (he and I worked on an earlier patch that fixed that nil problem).

    -Chris

    Chris Cruft Chris Cruft
    October 09, 2007 at 09:00 AM
  8. Brandon Brandon
    October 24, 2007 at 12:05 AM
  9. Congratulations on the accepted patch, Brandon. I’ve just begun using it in my project together with the Money gem, too.

    class Project < ActiveRecord::Base
      composed_of :budget, :class_name => "Money", :mapping => %w(budget cents), :allow_nil => true do |amount|
        amount.to_money
      end
    end
    

    The backend part works great but I seem to have one problem with displaying the form field in an edit form:

    <%= f.text_field "budget" %>
    

    This displays the cents value in the input field (i.e. “24099”) while I obviously would like it to display it in EUR instead (“240.99”). When I then submit the form without changing anything, the “24099” gets converted to a Money object representing 24099 EUR.

    Looking at Rails’s form_helper.rb, the reason seems to be that Rails calls @project.budget_before_type_cast to construct the display value for the input field and sure enough, this yields the cents value.

    Have you experienced this problem? Or does it work differently for you? Overwriting budget_before_type_cast should do the trick, but I’m curious why nobody else seems to have this problem. I’m on Rails 2.0.1.

    Ole Begemann Ole Begemann
    December 14, 2007 at 09:56 AM

Speak your mind:

(Required)

(Required)


(You may use textile in your comments.)

About

I'm Brandon Keepers, a web application developer that likes beautiful code, valid markup and adherence to standards. As a part of Collective Idea in Holland, Michigan, I practice Agile software development primarily using Ruby on Rails.

-86.103171 42.785037

Contact:

more ยป

Syndicate