a generic solution to defaults in AR model initialization

This afterthought to this ancient but still relevant post
http://groups.google.com/group/rubyonrails-core/browse_thread/thread/b509a2fe2b62ac5

After realizing that after_initialize callback is a serious misnomer,
I had to figure out a way
to implement a generic solution for defaults.

Below is what I came up with.
You can simply give a defaults class method in your models. Features:

* It is also able to assign to associations
* warns against undefined assigners (typos)
* defaults can be overriden in the hash passed to the 'new'
constructor
* block given to new are correctly executed

Comments welcome

V

E.g.
class Person < ActiveRecord::Base
  has_one :profile
  def self.defaults
   { :profile => Profile.new, :name => 'tron' }
end
end

config/initializers/active_record_defaults.rb

class ActiveRecord::Base
  # this does not work with associations...

  def initialize_with_defaults(attributes=nil,&block)
    defaults = self.class.respond_to?(:defaults) ?
self.class.defaults : false

    initialize_without_defaults(attributes) do

      if defaults then
        defaults.each_pair do |a,v|
          assigner_f = "#{a}="
          # we force check of default attributes even if
          # they are assigned already but do not override
          if respond_to?(assigner_f) then
            # assign value if it does not already have one
            # nil is important cause value can be false!!
            send(assigner_f,v) if send(a).nil?
          else
            raise(ArgumentError.new("unable to assign to '%s'" % a))
          end
        end
      end
      yield self if block_given?
    end
  end
  alias_method_chain :initialize, :defaults
end

This afterthought to this ancient but still relevant post
http://groups.google.com/group/rubyonrails-core/browse_thread/thread/b509a2fe2b62ac5

I wish you'd also warned how annoying that thread was. It just ruined
my breakfast, and I only got half-way through.

Anyway, I like the functionality. I'd suggest that the instance be
made available to the code providing the defaults. The way you show it
implemented, that would mean making it an argument to #defaults.

  def initialize_with_defaults(attributes=nil,&block)
    defaults = self.class.respond_to?(:defaults) ?
self.class.defaults(self) : false
    ...

But I'm not crazy about that API (or rather, lack thereof), as it
doesn't communicate intent. I think I'd rather do something like this.

class Thing < ActiveRecord::Base
  default_attributes do |thing|
    {:name => (thing.type ? "Some #{thing.type}" : "Unnamed")}
  end
end

or

class Thing < ActiveRecord::Base
  default_attributes :defaults
  def defaults
    {:name => (type ? "Some #{type}" : "Unnamed")}
  end
end

Or we could have something that looked like factory_girl
(http://github.com/thoughtbot/factory_girl/tree/master), though the
feature may not justify the complexity. (Not to mention that if you
wanted the complexity, you could just use factory_girl, though until
now I'd only thought of it as an alternative to fixtures in testing.)

-hume.

Does the plugin I wrote a while back do what’s needed?

http://svn.viney.net.nz/things/rails/plugins/active_record_defaults/

Cheers,
-Jonathan.

Jonathan, cool, it does exactly what I need and more, but
once we are at it let me raise some issues.

The way you define defaults, symbol values are called on the instance

This intends to capture the situation when an attribute is by default
assumes the value of another attribute (e.g.,
default :login, :first_name)
But where would the value of first name come from, if it is
initialized in new, its fine, but if it is itself a default, what
happens?
The problem is that the attribute setters are called in a random order
(that is determined by the access order of hash map), therefore
the behaviour in such cases is not even consistent.

Tag.write_inheritable_attribute(:attribute_defaults,nil)

=> nil

Tag.defaults :name => 2, :id => :name

=> [#<ActiveRecord::Defaults::Default:0xb6cb7878 @attribute="name",
@value=2>, #<ActiveRecord::Defaults::Default:0xb6cb7828
@attribute="id", @value=:name>]

Tag.new

=> #<Tag id: 2, name: 2, created_at: nil, updated_at: nil>

id is set after name

Tag.write_inheritable_attribute(:attribute_defaults,nil)

=> nil

Tag.defaults :id => :name, :name => 2

=> [#<ActiveRecord::Defaults::Default:0xb6cb0fa0 @attribute="name",
@value=2>, #<ActiveRecord::Defaults::Default:0xb6cb0f50
@attribute="id", @value=:name>]

Tag.new

=> #<Tag id: 2, name: 2, created_at: nil, updated_at: nil>

same when the order is changed. this is because setting :name
incidentally happens before setting :id

But when we want to set name to anything that id is:

Tag.write_inheritable_attribute(:attribute_defaults,nil)

=> nil

Tag.defaults :name => :id, :id => 2

=> [#<ActiveRecord::Defaults::Default:0xb6ca82c4 @attribute="name",
@value=:id>, #<ActiveRecord::Defaults::Default:0xb6ca8274
@attribute="id", @value=2>]

Tag.new

=> #<Tag id: 2, name: nil, created_at: nil, updated_at: nil>

OOPS then name is nil, since :id has no value at the time it is
called.
[Don't call me names for setting ids, this is just a demonstration
ok? ;-))

This also demonstrates another problem, which is that default
assignments are not consistent with what
can be given in the hash of the new constructor:

Tag.new(:name => :id)
=> #<Tag id: 2, name: :id, created_at: nil, updated_at: nil>

In this case the symbol translates to the string ':id' (bye the way,
how on earth does that then magically become "--- :id\n" after a
database save???)

For me it would also be feasible to interpret ':id' magically as a
string just like in "#{:id}" which gives "id"
which means that the default interpretation for symbols as methods is
at least not obvious.

Yet another thing: apart from belongs_to associations, the way you
determine whether defaults should be applied is you look
at whether the attribute was given to 'new'.
The other option was my solution to check whether an attributes reader
results in nil.

Your solution has the clear advantage that defaults CAN now be
overriden with nil
but it also assumes that no other attributes are ever set in the
original initialize unless explicitly mentioned in a keys given to
new?
Is this assumption correct (again apart from associations)?

Looking forward to your ideas

Viktor