module ActsAsJoinable module Dirty def self.included(base) base.extend ClassMethods base.send :include, InstanceMethods end module ClassMethods end module InstanceMethods def write_join_attribute(name, value, context = nil) name = name.to_s assert_valid_join_attribute(name) join_attributes_changed = join_attributes_changed_context(context) if join_attributes_changed.include?(name) old = join_attributes_changed[name] join_attributes_changed.delete(name) unless join_attribute_value_changed?(name, old, value) else old = clone_join_attribute_value(name, context) join_attributes_changed[name] = old if join_attribute_value_changed?(name, old, value) end join_attributes_context(context)[name] = value value end def join_attributes_changed?(context = nil) !join_attributes_changed_context(context).empty? end def join_attributes_changed(context = nil) join_attributes_changed_context(context).keys end def join_attribute_changes(context = nil) join_attributes_changed(context).inject({}) do |changes, join_attribute| changes[join_attribute] = join_attribute_change(join_attribute, context) changes end end def reload(*args) #:nodoc: result = super @join_attributes.clear if @join_attributes @join_attributes_changed.clear if @join_attributes_changed result end private def assert_valid_join_attribute(name) #raise(ArgumentError, "Unknown join_attribute: #{name}") unless join_attribute_definitions.include?(name) end def join_attributes_context(context) @join_attributes ||= {} @join_attributes[context.is_a?(Symbol) ? context.to_s : context] ||= {} end def join_attributes_context_loaded?(context) true #join_attribute_definitions.length == join_attributes_context(context).length end # Generates a clone of the current value stored for the join_attribute with # the given name / context def clone_join_attribute_value(name, context) value = join_attribute(name, context) value.duplicable? ? value.clone : value rescue TypeError, NoMethodError value end def join_attribute(name, context = nil) join_attributes_context(context)[name] end # Keeps track of all join_attributes that have been changed so that they can # be properly updated in the database. Maps context -> join_attribute -> value. def join_attributes_changed_context(context) @join_attributes_changed ||= {} @join_attributes_changed[context.is_a?(Symbol) ? context.to_s : context] ||= {} end def join_attribute_changed?(name, context) join_attributes_changed_context(context).include?(name) end def join_attribute_change(name, context) [join_attributes_changed_context(context)[name], join_attribute(name, context)] if join_attribute_changed?(name, context) end def join_attribute_was(name, context) join_attribute_changed?(name, context) ? join_attributes_changed_context(context)[name] : join_attribute(name, context) end def join_attribute_will_change!(name, context) join_attributes_changed_context(context)[name] = clone_join_attribute_value(name, context) end def reset_join_attribute!(name, context) write_join_attribute(name, join_attributes_changed_context(context)[name], context) if join_attribute_changed?(name, context) end def join_attribute_value_changed?(name, old, value) old != value end end end end