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 --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Ruby on Rails: Core" group. To post to this group, send email to rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
John D. Hume
2008-Sep-08 12:39 UTC
Re: 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/b509a2fe2b62ac5I 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. --- http://elhumidor.blogspot.com/ --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Ruby on Rails: Core" group. To post to this group, send email to rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Jonathan Viney
2008-Sep-08 12:48 UTC
Re: a generic solution to defaults in AR model initialization
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.tron@gmail.com> wrote:> > 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 > > > >--~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Ruby on Rails: Core" group. To post to this group, send email to rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
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--~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Ruby on Rails: Core" group. To post to this group, send email to rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---