aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
blob: 4b47824148fe69d600e34011ea41ed5495d2800e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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 self.define_callbacks(model, reflection)
      super
      name = reflection.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