association.rb 4.1 KB
Newer Older
1 2
require 'active_support/core_ext/module/attribute_accessors'

V
Vijay Dev 已提交
3 4 5 6
# This is the parent Association class which defines the variables
# used by all associations.
#
# The hierarchy is defined as follows:
7
#  Association
8
#    - SingularAssociation
V
Vijay Dev 已提交
9 10
#      - BelongsToAssociation
#      - HasOneAssociation
11
#    - CollectionAssociation
V
Vijay Dev 已提交
12
#      - HasManyAssociation
13

14 15
module ActiveRecord::Associations::Builder
  class Association #:nodoc:
16
    class << self
17
      attr_accessor :extensions
18 19 20
      # TODO: This class accessor is needed to make activerecord-deprecated_finders work.
      # We can move it to a constant in 5.0.
      attr_accessor :valid_options
21
    end
22
    self.extensions = []
23

24
    self.valid_options = [:class_name, :class, :foreign_key, :validate]
25

26 27
    attr_reader :name, :scope, :options

28
    def self.build(model, name, scope, options, &block)
29 30 31 32 33 34
      if model.dangerous_attribute_method?(name)
        raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
                             "this will conflict with a method #{name} already defined by Active Record. " \
                             "Please choose a different association name."
      end

35 36
      builder = create_builder model, name, scope, options, &block
      reflection = builder.build(model)
37
      define_accessors model, reflection
38
      define_callbacks model, reflection
39
      builder.define_extensions model
40
      reflection
41
    end
42

43
    def self.create_builder(model, name, scope, options, &block)
44 45
      raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)

46 47 48 49 50
      new(model, name, scope, options, &block)
    end

    def initialize(model, name, scope, options)
      # TODO: Move this to create_builder as soon we drop support to activerecord-deprecated_finders.
51 52 53 54 55
      if scope.is_a?(Hash)
        options = scope
        scope   = nil
      end

56 57 58 59
      # TODO: Remove this model argument as soon we drop support to activerecord-deprecated_finders.
      @name    = name
      @scope   = scope
      @options = options
60

61
      validate_options
62

63 64
      if scope && scope.arity == 0
        @scope = proc { instance_exec(&scope) }
J
Jon Leighton 已提交
65
      end
66 67
    end

68 69
    def build(model)
      ActiveRecord::Reflection.create(macro, name, scope, options, model)
70 71
    end

72
    def macro
73 74 75
      raise NotImplementedError
    end

76 77
    def valid_options
      Association.valid_options + Association.extensions.flat_map(&:valid_options)
78 79
    end

80 81
    def validate_options
      options.assert_valid_keys(valid_options)
J
Jon Leighton 已提交
82
    end
83

84
    def define_extensions(model)
85 86
    end

87 88
    def self.define_callbacks(model, reflection)
      add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent]
89
      Association.extensions.each do |extension|
90
        extension.build model, reflection
91 92 93
      end
    end

94 95 96 97
    # Defines the setter and getter methods for the association
    # class Post < ActiveRecord::Base
    #   has_many :comments
    # end
98
    #
99
    # Post.first.comments and Post.first.comments= methods are defined by this method...
100
    def self.define_accessors(model, reflection)
101
      mixin = model.generated_association_methods
102
      name = reflection.name
103 104
      define_readers(mixin, name)
      define_writers(mixin, name)
J
Jon Leighton 已提交
105
    end
106

107
    def self.define_readers(mixin, name)
108 109 110 111 112
      mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}(*args)
          association(:#{name}).reader(*args)
        end
      CODE
J
Jon Leighton 已提交
113
    end
114

115
    def self.define_writers(mixin, name)
116 117 118 119 120
      mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}=(value)
          association(:#{name}).writer(value)
        end
      CODE
J
Jon Leighton 已提交
121
    end
122

123
    def self.valid_dependent_options
124 125 126
      raise NotImplementedError
    end

127 128
    private

129 130 131
    def self.add_before_destroy_callbacks(model, reflection)
      unless valid_dependent_options.include? reflection.options[:dependent]
        raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}"
132
      end
133

134
      name = reflection.name
135
      model.before_destroy lambda { |o| o.association(name).handle_dependency }
J
Jon Leighton 已提交
136
    end
137
  end
138
end