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