Re: a generic solution to defaults in AR model initialization

tron
2008-09-08T15:48:33Z

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




On Sep 8, 1:48 pm, "Jonathan Viney" <jonathan.vi...@gmail.com>
wrote:
> 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.
>
> On Mon, Sep 8, 2008 at 10:07 PM, tron <viktor.t...@gmail.com> wrote:
>
> > This afterthought to this ancient but still relevant post
>
>
>http://groups.google.com/group/rubyonrails-core/browse_thread/thread/...
>
> > 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