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

      class KnownClass # :nodoc:
        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.to_s
        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, required: false, **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, required: false, **options
          self.right_reflection = _reflect_on_association(rhs_name)
        end

        def self.retrieve_connection
          left_model.retrieve_connection
        end

        private

          def self.suppress_composite_primary_key(pk)
            pk unless pk.is_a?(Array)
          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