module SimpleModel module Attributes include ExtendCore extend ActiveSupport::Concern include ActiveModel::AttributeMethods def initialize(*attrs) attrs = attrs.extract_options! attrs = attributes_with_for_init(attrs) attrs = self.class.before_initialize.call(self,attrs) if self.class.before_initialize set(attrs) self.class.after_initialize.call(self) if self.class.after_initialize end # Returns true if attribute has been initialized def initialized?(attr) attributes.key?(attr.to_sym) end def attributes @attributes ||= HashWithIndifferentAccess.new end def attributes=attrs @attributes = attrs end def get(attr) self.send(attr) end alias :read :get # Accepts a hash where the keys are methods and the values are values to be set. # set(:foo => "bar", :dime => 0.1) def set(*attrs) attrs.extract_options!.each do |attr,val| self.send("#{attr.to_s}=",val) end end alias :set_attributes :set private def fetch_default_value(arg) return self.send(arg) if (arg.is_a?(Symbol) && self.respond_to?(arg)) arg end # Returns attribute that have defaults in a hash: {:attribute => "default value"} # Checks for alias attributes to ensure they are not overwritten def attributes_with_for_init(attrs) d = attrs.with_indifferent_access self.class.defined_attributes.each do |k,v| if allow_set_default?(d,k,v) d[k] = fetch_default_value(v[:default]) end end d end def allow_set_default?(d,k,v) (v[:default] && v[:initialize] && (d[k].blank? && (self.class.alias_attributes[k].blank? || d.key?(self.class.alias_attributes[k]) && d[self.class.alias_attributes[k]].blank?))) end private def allow_attribute_action?(obj,val,options) return true if (options[:if].blank? && options[:unless].blank?) b = true if options[:if].is_a?(Symbol) if options[:if] == :blank b = (b && val.blank?) else b = (b && send(options[:if])) end end b = (b && options[:if].call(obj,val)) if options[:if].is_a?(Proc) if options[:unless].is_a?(Symbol) if options[:unless] == :blank b = (b && !val.blank?) else b = (b && !send(options[:unless])) end end b = (b && !options[:unless].call(obj,val)) if options[:unless].is_a?(Proc) b end # Rails 3.2 + required when searching for attributes in from inherited classes/models def attribute(name) attributes[name.to_sym] end module ClassMethods # Creates a new instance where the attributes store is set to object # provided, which allows one to pass a session store hash or any other # hash-like object to be used for persistence. Typically used for modeling # session stores for authorization or shopping carts # EX: # class ApplicationController < ActionController::Base # def session_user # session[:user] ||= {} # @session_user ||= SessionUser.new_with_store(session[:user]) # end # helper_method :session_user # end # def new_with_store(session_hash) new = self.new() new.attributes = session_hash new.set(new.send(:attributes_with_for_init,session_hash)) new end def alias_attributes @alias_attributes ||= HashWithIndifferentAccess.new end def alias_attributes=alias_attributes @alias_attributes = alias_attributes end def defined_attributes @defined_attributes ||= HashWithIndifferentAccess.new end def defined_attributes=defined_attributes @defined_attributes = defined_attributes end def attribute_defined?(attr) (self.defined_attributes[attr] || self.superclass.respond_to?(:attribute_defined?) && self.superclass.attribute_defined?(attr)) end # The default settings for a SimpeModel class # Options: # * :on_set - accepts a lambda that is run when an attribute is set # * :on_get - accepts a lambda that is run when you get/read an attribute # * :default - the default value for the attribute, can be a symbol that is sent for a method # * :initialize - informations the object whether or not it should initialize the attribute with :default value, defaults to true # ** If :initialize is set to false you must set :allow_blank to false or it will never set the default value # * :allow_blank - when set to false, if an attributes value is blank attempts to set the default value, defaults to true def default_attribute_settings @default_attribute_settings ||= {:attributes_method => :attributes, :on_set => lambda {|obj,attr| attr}, :on_get => lambda {|obj,attr| attr}, :allow_blank => true, :initialize => true } end def default_attribute_settings=default_attribute_settings @default_attribute_settings = default_attribute_settings end # We want to re-run define_attribute_methods since attributes are not all defined # at once, so we must set @attribute_methods_generated to nil to allow the # re-run to occur ONLY IN RAILS 3.0. def add_defined_attribute(attr,options) self.defined_attributes[attr] = options @attribute_methods_generated = nil #if (ActiveModel::VERSION::MAJOR == 3 && ActiveModel::VERSION::MINOR == 0) define_attribute_methods(self.defined_attributes.keys) end # builds the setter and getter methods def create_attribute_methods(attributes,options) unless attributes.blank? attributes.each do |attr| define_reader_with_options(attr,options) define_setter_with_options(attr,options) end end end def define_reader_with_options(attr,options) add_defined_attribute(attr,options) options = default_attribute_settings.merge(options) if options[:on_get].blank? define_method(attr) do val = self.attributes[attr] if (options.key?(:default) && (!self.initialized?(attr) || (!options[:allow_blank] && val.blank?))) val = self.attributes[attr] = fetch_default_value(options[:default]) end options[:on_get].call(self,val) end define_method("#{attr.to_s}?") do val = self.send(attr) if val.respond_to?(:to_b) val = val.to_b else val = !val.blank? if val.respond_to?(:blank?) end val end end # Creates setter methods for the provided attributes # On set, it will mark the attribute as changed if the attributes has been # initialized. def define_setter_with_options(attr,options) add_defined_attribute(attr,options) options = default_attribute_settings.merge(options) if (options[:on_set].blank? || options[:after_set].blank?) define_method("#{attr.to_s}=") do |val| if allow_attribute_action?(self,val,options) val = fetch_default_value(options[:default]) if (!options[:allow_blank] && options.key?(:default) && val.blank?) val = options[:on_set].call(self,val) unless (val.blank? && !options[:allow_blank] ) will_change = "#{attr}_will_change!".to_sym self.send(will_change) if (initialized?(attr) && val != self.attributes[attr]) self.attributes[attr] = val options[:after_set].call(self,val) if options[:after_set] end end end AVAILABLE_ATTRIBUTE_METHODS = { :has_attribute => {:alias => :has_attributes}, :has_boolean => {:cast_to => :to_b, :alias => :has_booleans}, :has_currency => {:cast_to => :to_d, :alias => :has_currencies}, :has_date => {:cast_to => :to_date, :alias => :has_dates} , :has_decimal => {:cast_to => :to_d, :alias => :has_decimals}, :has_float => {:cast_to => :to_f, :alias => :has_floats}, :has_int => {:cast_to => :to_i, :alias => :has_ints}, :has_time => {:cast_to => :to_time, :alias => :has_times} } AVAILABLE_ATTRIBUTE_METHODS.each do |method,method_options| define_method(method) do |*attributes| options = default_attribute_settings.merge(attributes.extract_options!) options[:on_set] = lambda {|obj,val| val.send(method_options[:cast_to]) } if method_options[:cast_to] create_attribute_methods(attributes,options) end module_eval("alias #{method_options[:alias]} #{method}") end # Creates alias setter and getter for the supplied attribute using the supplied alias # See spec for example. def alias_attribute(new_alias,attribute) alias_attributes[attribute] = new_alias define_method(new_alias) do self.send(attribute) end define_method("#{new_alias.to_s}=") do |*args, &block| self.send("#{attribute.to_s}=",*args, &block) end end # A hook to perform actions on the pending attributes or the object before # the pending attributes have been initialized. # Expects an lambda that accept the object, the pending attributes hash and # should return a hash to be set # EX: lambda {|obj,attrs| attrs.select{|k,v| !v.blank?}} def before_initialize @before_initialize end # Expects an lambda that accept the object, the pending attributes hash and # should return a hash to be set # EX: lambda {|obj,attrs| attrs.select{|k,v| !v.blank?}} def before_initialize=before_initialize raise TypeError "before_initialize must be a lambda that accepts the attirbutes to be initialize" unless before_initialize.is_a?(Proc) @before_initialize = before_initialize end # A hook to perform actions after all attributes have been initialized # Expects an lambda that accept the object and the pending attributes hash # EX: lambda {|obj| puts "initialized"} def after_initialize @after_initialize end # Expects an lambda that accept the object and the pending attributes hash # EX: lambda {|obj| puts "initialized"} def after_initialize=after_initialize raise TypeError "after_initalize must be a Proc" unless after_initialize.is_a?(Proc) @after_initialize = after_initialize end # Must inherit super's defined_attributes and alias_attributes # Rails 3.0 does some weird stuff with ActiveModel::Dirty so we need a # hack to keep things working when a class inherits from a super that # has ActiveModel::Dirty included def inherited(base) base.alias_attributes = self.alias_attributes.merge(base.alias_attributes) super # Rails 3.0 Hack if (ActiveModel::VERSION::MAJOR == 3 && ActiveModel::VERSION::MINOR == 0) base.attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' base.attribute_method_affix :prefix => 'reset_', :suffix => '!' end end end # Rails 3.0 does some weird stuff with ActiveModel::Dirty so we need a # hack to keep things working when a class includes a module that has # ActiveModel::Dirty included def self.included(base) base.extend(Attributes::ClassMethods) base.send(:include, ActiveModel::Dirty) base.send(:include, ActiveModel::Validations) base.send(:include, ActiveModel::Conversion) base.extend ActiveModel::Naming base.extend ActiveModel::Callbacks base.send(:include, ActiveModel::Validations::Callbacks) # Rails 3.0 Hack if (ActiveModel::VERSION::MAJOR == 3 && ActiveModel::VERSION::MINOR == 0) base.attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' base.attribute_method_affix :prefix => 'reset_', :suffix => '!' end end end end