lib/eav_hashes/eav_hash.rb
module ActiveRecord
module EavHashes
# Wraps a bunch of EavEntries and lets you use them like you would a hash
# This class should not be used directly and you should instead let eav_hash_for create one for you
class EavHash
# Creates a new EavHash. You should really let eav_hash_for do this for you...
# @param [ActiveRecord::Base] owner the Model which will own this hash
# @param [Hash] options the options hash which eav_hash generated
def initialize(owner, options)
Util::sanity_check options
@owner = owner
@options = options
end
# Saves any modified entries and deletes any which have been nil'd to save DB space
def save_entries
# The entries are lazy-loaded, so don't do anything if they haven't been accessed or modified
return unless (@entries and @changes_made)
@entries.values.each do |entry|
if entry.value.nil?
entry.delete
else
set_entry_owner(entry)
entry.save
end
end
end
# Gets the value of an EAV attribute
# @param [String, Symbol] key
def [](key)
raise "Key must be a string or a symbol!" unless key.is_a?(String) or key.is_a?(Symbol)
load_entries_if_needed
return @entries[key].value if @entries[key]
nil
end
# Sets the value of the EAV attribute `key` to `value`
# @param [String, Symbol] key the attribute
# @param [Object] value the value
def []=(key, value)
update_or_create_entry key, value
end
# I don't know why Ruby hashes don't have a shovel operator, but I will make damn sure that I
# fight the power and stick it to the man by implementing it.
# @param [Hash, EavHash] dirt the dirt to shovel (ba dum, tss)
def <<(dirt)
if dirt.is_a? Hash
dirt.each do |key, value|
update_or_create_entry key, value
end
elsif dirt.is_a? EavHash
dirt.entries.each do |key, entry|
update_or_create_entry key, entry.value
end
else
raise "You can't shovel something that's not a Hash or EavHash here!"
end
self
end
# Gets the raw hash containing EavEntries by their keys
def entries
load_entries_if_needed
end
# Gets the actual values this EavHash contains
def values
load_entries_if_needed
ret = []
@entries.values.each do |value|
ret << value
end
ret
end
# Gets the keys this EavHash manages
def keys
load_entries_if_needed
@entries.keys
end
# Emulates Hash.each
def each (&block)
as_hash.each &block
end
# Emulates Hash.each_pair (same as each)
def each_pair (&block)
each &block
end
# Empties the hash by setting all the values to nil
# (without committing them, of course)
def clear
load_entries_if_needed
@entries.each do |_, entry|
entry.value = nil
end
end
# Returns a hash with each entry key mapped to its actual value,
# not the internal EavEntry
def as_hash
load_entries_if_needed
hsh = {}
@entries.each do |k, entry|
hsh[k] = entry.value
end
hsh
end
# Take the crap out of #inspect calls
def inspect
as_hash
end
private
def update_or_create_entry(key, value)
raise "Key must be a string or a symbol!" unless key.is_a?(String) or key.is_a?(Symbol)
load_entries_if_needed
@changes_made = true
@owner.updated_at = Time.now
if @entries[key]
@entries[key].value = value
else
new_entry = @options[:entry_class].new
set_entry_owner(new_entry)
new_entry.key = key
new_entry.value = value
@entries[key] = new_entry
value
end
end
# Since entries are lazy-loaded, this is called just before an operation on an entry happens and
# loads the rows only once per EavHash lifetime.
def load_entries_if_needed
if @entries.nil?
@entries = {}
rows_from_model = @owner.send("#{@options[:entry_assoc_name]}")
rows_from_model.each do |row|
@entries[row.key] = row
end
end
@entries
end
# Sets an entry's owner ID. This is called when we save attributes for a model which has just been
# created and not committed to the DB prior to having its EAV hash(es) modified
# @param [EavEntry] entry the entry whose owner to change
def set_entry_owner(entry)
entry.send "#{@options[:parent_assoc_name]}_id=", @owner.id
end
end
end
end