lib/simple_model/attributes.rb in simple_model-1.1.1 vs lib/simple_model/attributes.rb in simple_model-1.2.0

- old
+ new

@@ -1,175 +1,184 @@ +require 'simple_model/exceptions' module SimpleModel - # require all that active support we know and love - require 'active_support/core_ext/array/extract_options' - require 'active_support/core_ext/object/blank' - module Attributes include ExtendCore extend ActiveSupport::Concern - include ActiveModel::AttributeMethods - #Set attribute values to those supplied at initialization - def initialize(*attrs) - set_attributes(attrs.extract_options!) + include ActiveModel::AttributeMethods + + def initialize(*attrs) + attrs = attrs.extract_options! + set(attributes_with_for_init(attrs)) end - - # Place to store set attributes and their values + + # Returns true if attribute has been initialized + def initialized?(attr) + attributes.key?(attr.to_sym) + end + def attributes - @attributes ||= {} - @attributes + @attributes ||= HashWithIndifferentAccess.new end + + def attributes=attrs + @attributes = attrs + end + + def get(attr) + self.send(attr) + end + alias :read :get - def set_attributes(attrs) - attrs.each do |attr| - self.send("#{attr[0].to_sym}=",attr[1]) + # 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 - # Hook to run method after attribute is converted but before it is set - def before_attribute_set(method,val) - end + private - alias :update_attributes :set_attributes - - def self.included(base) - base.extend(ClassMethods) + def fetch_default_value(arg) + return self.send(arg) if (arg.is_a?(Symbol) && self.respond_to?(arg)) + arg end - def fetch_default + # Returns attribute that have defaults in a hash: {:attrbute => "default value"} + def attributes_with_for_init(attrs) + d = attrs.with_indifferent_access + self.class.defined_attributes.each do |k,v| + d[k] = fetch_default_value(v[:default]) if (d[k].blank? && v[:default] && v[:initialize]) + end + d end - - module ClassMethods + 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 persistance. 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 - # Hook to call class method after attribute method definitions - def after_attribute_definition(attr) + def defined_attributes + @defined_attributes ||= {} end - # Defines a reader method that returns a default value if current value - # is nil, if :default is present in the options hash - def define_reader_with_options(attr,options) - if options.has_key?(:default) - define_method(attr.to_s) do - default = (options[:default].is_a?(Symbol) ? self.send(options[:default]) : options[:default]) - val = instance_variable_get("@#{attr.to_s}") - val = default unless instance_variable_defined?("@#{attr.to_s}") - val - end - else - attr_reader attr - end + def defined_attributes=defined_attributes + @defined_attributes = defined_attributes end - def define_setter(attr,cast_methods) - define_method("#{attr.to_s}=") do |val| - val = val.cast_to(cast_methods) - before_attribute_set(attr,val) - instance_variable_set("@#{attr}", val) - attributes[attr] = val - val - 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 :intialize 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 - # Builder for attribute methods - def build_attribute_methods(attr,options={},cast_methods=[]) - define_reader_with_options(attr,options) - define_setter(attr,cast_methods) - after_attribute_definition attr + def default_attribute_settings=default_attribute_settings + @default_attribute_settings = default_attribute_settings end - - # Left this use a module eval for reference, saw no noticable improvement - # in speed, so I would rather use code than strings for now -# def define_setter_with_eval(attr,cast_methods) -# module_eval <<-STR, __FILE__, __LINE__ -# def #{attr.to_s}=#{attr.to_s} -# val = #{attr.to_s}.cast_to(#{cast_methods}) -# before_attribute_set(:#{attr.to_s},val) -# @#{attr.to_s} = val -# attributes[:#{attr.to_s}] = val -# val -# end -# STR -# end + + def add_defined_attribute(attr,options) + self.defined_attributes[attr] = options + define_attribute_methods self.defined_attributes.keys + end - #creates setter and getter datatype special attribute - def has_attributes(*attrs) - options = attrs.extract_options! - attrs.each do |attr| - build_attribute_methods(attr,options) + # 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 - alias :has_attribute :has_attributes - - # Creates setter and getter methods for boolean attributes - def has_booleans(*attrs) - options = attrs.extract_options! - attrs.each do |attr| - build_attribute_methods(attr,options,[:to_s,:to_b]) - define_method ("#{attr.to_s}?") do - send("#{attr.to_s}".to_sym).to_s.to_b + + 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 + if (options.key?(:default) && (!self.initialized?(attr) || (!options[:allow_blank] && self.attributes[attr].blank?))) + self.attributes[attr] = fetch_default_value(options[:default]) end + options[:on_get].call(self,self.attributes[attr]) end - end - alias :has_boolean :has_booleans - - # Creates setter and getter methods for integer attributes - def has_ints(*attrs) - options = attrs.extract_options! - attrs.each do |attr| - build_attribute_methods(attr,options,[:to_i]) + 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 - alias :has_int :has_ints - - # Creates setter and getter methods for currency attributes - # attributes are cast to BigDecimal and rounded to nearest cent - # #Warning, rounding occurs on all sets, so if you need to keep higher prescsion - # use has_decimals - def has_currency(*attrs) - options = attrs.extract_options! - attrs.each do |attr| - build_attribute_methods(attr,options,[:to_s,:to_currency]) - + + 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| + val = fetch_default_value(options[:default]) if (!options[:allow_blank] && options.key?(:default) && val.blank?) + begin + val = options[:on_set].call(self,val) + rescue NoMethodError => e + raise ArgumentError, "#{val} could not be set for #{attr}: #{e.message}" + end + will_change = "#{attr}_will_change!".to_sym + self.send(will_change) if (self.respond_to?(will_change) && val != self.attributes[attr]) + self.attributes[attr] = val + options[:after_set].call(self,val) if options[:after_set] end end - - def has_decimals(*attrs) - options = attrs.extract_options! - attrs.each do |attr| - build_attribute_methods(attr,options,[:to_f,:to_d]) - - end - end - alias :has_decimal :has_decimals - - # Creates setter and getter methods for float attributes - def has_floats(*attrs) - options = attrs.extract_options! - attrs.each do |attr| - build_attribute_methods(attr,options,[:to_f]) - - end - end - alias :has_float :has_floats + + 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} + } - # Creates setter and getter methods for date attributes - def has_dates(*attrs) - options = attrs.extract_options! - attrs.each do |attr| - build_attribute_methods(attr,options,[:to_s,:to_date]) - + 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 - alias :has_date :has_dates - - # Creates setter and getter methods for time attributes - def has_times(*attrs) - options = attrs.extract_options! - attrs.each do |attr| - build_attribute_methods(attr,options,[:to_s,:to_time]) - - end - end - alias :has_time :has_times end + + def self.included(base) + base.extend(Attributes::ClassMethods) + base.send(:include, ActiveModel::Dirty) if base.is_a?(Class) # Add Dirty to the class + end end -end +end \ No newline at end of file