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