aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/builder/association.rb
blob: 4b72846ef6673d99464f91b1b076bfb7c3586b8f (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
# This is the parent Association class which defines certain class variables (valid_options) and
# instance variables (model, name, scope, options, reflection) which would be common across all the associations that we known today in Rails..
# Every association need to have the values of these variables set and they are used at multiple places
# The heirarchy is defined as follows:
#  Association 
#    - SingularAssociation
#      - BelongsTo
#      - HasOne
#    - CollectionAssociation
#      - HasMany
#      - HasAndBelongsToMany
#
# The HasMany :Through association is a special case of HasMany association with the :through option set for it
#

module ActiveRecord::Associations::Builder
  class Association #:nodoc:
    class << self
      attr_accessor :valid_options
    end

    self.valid_options = [:class_name, :foreign_key, :validate]

    attr_reader :model, :name, :scope, :options, :reflection

    def self.build(*args, &block)
      new(*args, &block).build
    end

    def initialize(model, name, scope, options)
      raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)

      @model   = model
      @name    = name

      if scope.is_a?(Hash)
        @scope   = nil
        @options = scope
      else
        @scope   = scope
        @options = options
      end

      if @scope && @scope.arity == 0
        prev_scope = @scope
        @scope = proc { instance_exec(&prev_scope) }
      end
    end

    def mixin
      @model.generated_feature_methods
    end

    include Module.new { def build; end }

    def build
      validate_options
      define_accessors
      configure_dependency if options[:dependent]
      @reflection = model.create_reflection(macro, name, scope, options, model)
      super # provides an extension point
      @reflection
    end

    def macro
      raise NotImplementedError
    end

    def valid_options
      Association.valid_options
    end

    def validate_options
      options.assert_valid_keys(valid_options)
    end
    
    # Defines the setter and getter methods for the association
    # class Post < ActiveRecord::Base
    #   has_many :comments
    # end
    # 
    # Post.first.comments and Post.first.comments= methods are defined by this method...

    def define_accessors
      define_readers
      define_writers
    end

    def define_readers
      mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}(*args)
          association(:#{name}).reader(*args)
        end
      CODE
    end

    def define_writers
      mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}=(value)
          association(:#{name}).writer(value)
        end
      CODE
    end

    def configure_dependency
      unless valid_dependent_options.include? options[:dependent]
        raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{options[:dependent]}"
      end

      if options[:dependent] == :restrict
        ActiveSupport::Deprecation.warn(
          "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \
          "provides the same functionality."
        )
      end

      mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{macro}_dependent_for_#{name}
          association(:#{name}).handle_dependency
        end
      CODE

      model.before_destroy "#{macro}_dependent_for_#{name}"
    end

    def valid_dependent_options
      raise NotImplementedError
    end
  end
end