lib/modis/attribute.rb in modis-1.3.0 vs lib/modis/attribute.rb in modis-1.4.0

- old
+ new

@@ -1,57 +1,81 @@ module Modis module Attribute - TYPES = [:string, :integer, :float, :timestamp, :boolean, :array, :hash] + TYPES = { string: [String], + integer: [Fixnum], + float: [Float], + timestamp: [Time], + hash: [Hash], + array: [Array], + boolean: [TrueClass, FalseClass] }.freeze def self.included(base) base.extend ClassMethods base.instance_eval do bootstrap_attributes end end module ClassMethods - def bootstrap_attributes + def bootstrap_attributes(parent = nil) + attr_reader :attributes + class << self - attr_accessor :attributes + attr_accessor :attributes, :attributes_with_defaults end - self.attributes = {} + self.attributes = parent ? parent.attributes.dup : {} + self.attributes_with_defaults = parent ? parent.attributes_with_defaults.dup : {} - attribute :id, :integer + attribute :id, :integer unless parent end - def attribute(name, types, options = {}) + def attribute(name, type, options = {}) name = name.to_s - types = Array[*types] raise AttributeError, "Attribute with name '#{name}' has already been specified." if attributes.key?(name) - types.each { |type| raise UnsupportedAttributeType, type unless TYPES.include?(type) } - attributes[name] = options.update(types: types) - define_attribute_methods [name] - class_eval <<-EOS, __FILE__, __LINE__ + type_classes = Array(type).map do |t| + raise UnsupportedAttributeType, t unless TYPES.key?(t) + TYPES[t] + end.flatten + + attributes[name] = options.update(type: type) + attributes_with_defaults[name] = options[:default] if options[:default] + define_attribute_methods([name]) + + value_coercion = type == :timestamp ? 'value = Time.new(*value) if value && value.is_a?(Array) && value.count == 7' : nil + predicate = type_classes.map { |cls| "value.is_a?(#{cls.name})" }.join(' || ') + + type_check = <<-RUBY + if value && !(#{predicate}) + raise Modis::AttributeCoercionError, "Received value of type '\#{value.class}', expected '#{type_classes.join("', '")}' for attribute '#{name}'." + end + RUBY + + class_eval <<-RUBY, __FILE__, __LINE__ def #{name} attributes['#{name}'] end def #{name}=(value) - ensure_type('#{name}', value) - #{name}_will_change! unless value == attributes['#{name}'] - attributes['#{name}'] = value + #{value_coercion} + + # ActiveSupport's Time#<=> does not perform well when comparing with NilClass. + if (value.nil? ^ attributes['#{name}'].nil?) || (value != attributes['#{name}']) + #{type_check} + #{name}_will_change! + attributes['#{name}'] = value + end end - EOS + RUBY end end - def attributes - @attributes ||= Hash[self.class.attributes.keys.zip] - end - def assign_attributes(hash) hash.each do |k, v| setter = "#{k}=" - send(setter, v) if respond_to?(setter) + send(setter, v) if self.class.attributes.key?(k.to_s) end end def write_attribute(key, value) attributes[key.to_s] = value @@ -63,21 +87,17 @@ protected def set_sti_type return unless self.class.sti_child? - assign_attributes(type: self.class.name) + write_attribute(:type, self.class.name) end def reset_changes - @changed_attributes.clear if @changed_attributes + @changed_attributes = nil end def apply_defaults - defaults = {} - self.class.attributes.each do |attribute, options| - defaults[attribute] = options[:default] if options[:default] - end - assign_attributes(defaults) + @attributes = Hash[self.class.attributes_with_defaults] end end end