module ActiveRecord::Associations::Builder class HABTM class JoinTableResolver KnownTable = Struct.new :join_table class KnownClass def initialize(rhs_class, lhs_class_name) @rhs_class = rhs_class @lhs_class_name = lhs_class_name @join_table = nil end def join_table @join_table ||= [@rhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") end private def klass; @lhs_class_name.constantize; end end def self.build(rhs_class, name, options) if options[:join_table] KnownTable.new options[:join_table] else class_name = options.fetch(:class_name) { name.to_s.camelize.singularize } KnownClass.new rhs_class, class_name end end end attr_reader :lhs_model, :association_name, :options def initialize(association_name, lhs_model, options) @association_name = association_name @lhs_model = lhs_model @options = options end def through_model habtm = JoinTableResolver.build lhs_model, association_name, options join_model = Class.new(ActiveRecord::Base) { class << self; attr_accessor :class_resolver attr_accessor :name attr_accessor :table_name_resolver attr_accessor :left_association_name attr_accessor :right_association_name end def self.table_name table_name_resolver.join_table end def self.compute_type(class_name) class_resolver.compute_type class_name end def self.add_left_association(name, options) self.left_association_name = name belongs_to name, options end def self.add_right_association(name, options) rhs_name = name.to_s.singularize.to_sym self.right_association_name = rhs_name belongs_to rhs_name, options end } join_model.name = "HABTM_#{association_name.to_s.camelize}" join_model.table_name_resolver = habtm join_model.class_resolver = lhs_model join_model.add_left_association :left_side, class: lhs_model join_model.add_right_association association_name, belongs_to_options(options) join_model end def middle_options(join_model) middle_options = {} middle_options[:class] = join_model middle_options[:source] = join_model.left_association_name if options.key? :foreign_key middle_options[:foreign_key] = options[:foreign_key] end middle_options end private def belongs_to_options(options) rhs_options = {} if options.key? :class_name rhs_options[:foreign_key] = options[:class_name].foreign_key rhs_options[:class_name] = options[:class_name] end if options.key? :association_foreign_key rhs_options[:foreign_key] = options[:association_foreign_key] end rhs_options end end class HasAndBelongsToMany < CollectionAssociation #:nodoc: def macro :has_and_belongs_to_many end def valid_options super + [:join_table, :association_foreign_key] end def define_callbacks(model, reflection) super name = self.name model.send(:include, Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 def destroy_associations association(:#{name}).delete_all super end RUBY }) end end end