aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
blob: b18d99d54ecaef9461dc2454b008f80a5def4059 (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
module ActiveRecord::Associations::Builder
  class HasAndBelongsToMany # :nodoc:
    class JoinTableResolver
      KnownTable = Struct.new :join_table

      class KnownClass
        def initialize(lhs_class, rhs_class_name)
          @lhs_class      = lhs_class
          @rhs_class_name = rhs_class_name
          @join_table     = nil
        end

        def join_table
          @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
        end

        private

        def klass
          @lhs_class.send(:compute_type, @rhs_class_name)
        end
      end

      def self.build(lhs_class, name, options)
        if options[:join_table]
          KnownTable.new options[:join_table].to_s
        else
          class_name = options.fetch(:class_name) {
            name.to_s.camelize.singularize
          }
          KnownClass.new lhs_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 :left_model
          attr_accessor :name
          attr_accessor :table_name_resolver
          attr_accessor :left_reflection
          attr_accessor :right_reflection
        end

        def self.table_name
          table_name_resolver.join_table
        end

        def self.compute_type(class_name)
          left_model.compute_type class_name
        end

        def self.add_left_association(name, options)
          belongs_to name, options
          self.left_reflection = _reflect_on_association(name)
        end

        def self.add_right_association(name, options)
          rhs_name = name.to_s.singularize.to_sym
          belongs_to rhs_name, options
          self.right_reflection = _reflect_on_association(rhs_name)
        end

        def self.retrieve_connection
          left_model.retrieve_connection
        end

      }

      join_model.name                = "HABTM_#{association_name.to_s.camelize}"
      join_model.table_name_resolver = habtm
      join_model.left_model          = lhs_model

      join_model.add_left_association :left_side, anonymous_class: lhs_model
      join_model.add_right_association association_name, belongs_to_options(options)
      join_model
    end

    def middle_reflection(join_model)
      middle_name = [lhs_model.name.downcase.pluralize,
                     association_name].join('_'.freeze).gsub('::'.freeze, '_'.freeze).to_sym
      middle_options = middle_options join_model

      HasMany.create_reflection(lhs_model,
                                middle_name,
                                nil,
                                middle_options)
    end

    private

    def middle_options(join_model)
      middle_options = {}
      middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}"
      middle_options[:source] = join_model.left_reflection.name
      if options.key? :foreign_key
        middle_options[:foreign_key] = options[:foreign_key]
      end
      middle_options
    end

    def belongs_to_options(options)
      rhs_options = {}

      if options.key? :class_name
        rhs_options[:foreign_key] = options[:class_name].to_s.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
end