aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/builder/association.rb
blob: fd939ab9ba79925718a9494af14653f520eae5e3 (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
135
136
137
138
module ActiveRecord::Associations::Builder
  class Association #:nodoc:
    class_attribute :valid_options
    self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate, :references]

    # Set by subclasses
    class_attribute :macro

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

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

    def initialize(model, name, scope, options)
      @model   = model
      @name    = name

      if options
        @scope   = scope
        @options = options
      else
        @scope   = nil
        @options = scope

        convert_deprecated_options_to_scope!
      end
    end

    # FIXME: references should not be in this list
    DEPRECATED_OPTIONS = [:readonly, :references, :order, :limit, :joins, :group, :having,
                          :offset, :select, :uniq, :include, :conditions]

    class DeprecatedOptionsProc
      attr_reader :options

      def initialize(options)
        @options = options
      end

      def to_proc
        options = self.options
        proc do |owner|
          if options[:where].respond_to?(:to_proc)
            context = owner || self
            where(context.instance_eval(&options[:where]))
              .merge!(options.except(:where))
          else
            scoped(options)
          end
        end
      end

      def arity
        1
      end
    end

    def convert_deprecated_options_to_scope!
      deprecated_options = options.slice(*DEPRECATED_OPTIONS)

      unless deprecated_options.empty?
        deprecated_options[:includes] = deprecated_options.delete(:include)    if deprecated_options[:include]
        deprecated_options[:where]    = deprecated_options.delete(:conditions) if deprecated_options[:conditions]

        @scope   = DeprecatedOptionsProc.new(deprecated_options)
        @options = options.except(*DEPRECATED_OPTIONS)
      end
    end

    def mixin
      @model.generated_feature_methods
    end

    def build
      validate_options
      reflection = model.create_reflection(self.class.macro, name, scope, options, model)
      define_accessors
      reflection
    end

    private

      def validate_options
        options.assert_valid_keys(self.class.valid_options)
      end

      def define_accessors
        define_readers
        define_writers
      end

      def define_readers
        name = self.name
        mixin.redefine_method(name) do |*params|
          association(name).reader(*params)
        end
      end

      def define_writers
        name = self.name
        mixin.redefine_method("#{name}=") do |value|
          association(name).writer(value)
        end
      end

      def dependent_restrict_raises?
        ActiveRecord::Base.dependent_restrict_raises == true
      end

      def dependent_restrict_deprecation_warning
        if dependent_restrict_raises?
          msg = "In the next release, `:dependent => :restrict` will not raise a `DeleteRestrictionError`. "\
                "Instead, it will add an error on the model. To fix this warning, make sure your code " \
                "isn't relying on a `DeleteRestrictionError` and then add " \
                "`config.active_record.dependent_restrict_raises = false` to your application config."
          ActiveSupport::Deprecation.warn msg
        end
      end

      def define_restrict_dependency_method
        name = self.name
        mixin.redefine_method(dependency_method_name) do
          has_one_macro = association(name).reflection.macro == :has_one
          if has_one_macro ? !send(name).nil? : send(name).exists?
            if dependent_restrict_raises?
              raise ActiveRecord::DeleteRestrictionError.new(name)
            else
              key  = has_one_macro ? "one" : "many"
              errors.add(:base, :"restrict_dependent_destroy.#{key}",
                         :record => self.class.human_attribute_name(name).downcase)
              return false
            end
          end
        end
      end
  end
end