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 (GitHub - thoughtbot/factory_bot at 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