lib/eav_hashes/eav_entry.rb
module ActiveRecord
module EavHashes
# Used instead of nil when a nil value is assigned
# (otherwise, the value will try to deserialize itself and
# that would break everything horrifically)
class NilPlaceholder; end
# Represent an EAV row. This class should NOT be used directly, instead it should be inherited from
# by the class generated by eav_hash_for.
class EavEntry < ActiveRecord::Base
# prevent activerecord from thinking we're trying to do STI
self.abstract_class = true
# Tell ActiveRecord to convert the value to its DB storable format
before_save :serialize_value
# Let the key be assignable only once on creation
attr_readonly :entry_key
# Contains the values the value_type column should have based on the type of the value being stored
SUPPORTED_TYPES = {
:String => 0,
:Symbol => 1,
:Integer => 2,
:Fixnum => 2,
:Bignum => 2,
:Float => 3,
:Complex => 4,
:Rational => 5,
:Boolean => 6, # For code readability
:TrueClass => 6,
:FalseClass => 6,
:Object => 7 # anything else (including Hashes, Arrays) will be serialized to yaml and saved as Object
}
# Does some sanity checks.
def after_initialize
raise "key should be a string or symbol!" unless key.is_a? String or key.is_a? Symbol
raise "value should not be empty!" if @value.is_a? String and value.empty?
raise "value should not be nil!" if @value.nil?
end
# Gets the EAV row's value
def value
return nil if @value.is_a? NilPlaceholder
@value.nil? ? deserialize_value : @value
end
# Sets the EAV row's value
# @param [Object] val the value
def value= (val)
@value = (val.nil? ? NilPlaceholder.new : val)
end
def key
k = read_attribute :entry_key
(read_attribute :symbol_key) ? k.to_sym : k
end
# Raises an error if you try changing the key (unless no key is set)
def key= (val)
raise "Keys are immutable!" if read_attribute(:entry_key)
raise "Key must be a string!" unless val.is_a?(String) or val.is_a?(Symbol)
write_attribute :entry_key, val.to_s
write_attribute :symbol_key, (val.is_a? Symbol)
end
# Gets the value_type column's value for the type of value passed
# @param [Object] val the object whose value_type to determine
def self.get_value_type (val)
return nil if val.nil?
ret = SUPPORTED_TYPES[val.class.name.to_sym]
if ret.nil?
ret = SUPPORTED_TYPES[:Object]
end
ret
end
private
# Sets the value_type column to the appropriate value based on the value's type
def update_value_type
write_attribute :value_type, EavEntry.get_value_type(@value)
end
# Converts the value to its database-storable form and tells ActiveRecord that it's been changed (if it has)
def serialize_value
# Returning nil will prevent the row from being saved, to save some time since the EavHash that manages this
# entry will have marked it for deletion.
raise "Tried to save with a nil value!" if @value.nil? or @value.is_a? NilPlaceholder
update_value_type
if value_type == SUPPORTED_TYPES[:Object]
write_attribute :value, YAML::dump(@value)
else
write_attribute :value, @value.to_s
end
read_attribute :value
end
# Converts the value from it's database representation to the type specified in the value_type column.
def deserialize_value
if @value.nil?
@value = read_attribute :value
end
case value_type
when SUPPORTED_TYPES[:Object] # or Hash, Array, etc.
@value = YAML::load @value
when SUPPORTED_TYPES[:Symbol]
@value = @value.to_sym
when SUPPORTED_TYPES[:Integer] # or Fixnum, Bignum
@value = @value.to_i
when SUPPORTED_TYPES[:Float]
@value = @value.to_f
when SUPPORTED_TYPES[:Complex]
@value = Complex @value
when SUPPORTED_TYPES[:Rational]
@value = Rational @value
when SUPPORTED_TYPES[:Boolean]
@value = (@value == "true")
else
@value
end
end
end
end
end