-
Notifications
You must be signed in to change notification settings - Fork 21.6k
/
collection_proxy.rb
127 lines (112 loc) · 4.15 KB
/
collection_proxy.rb
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
module ActiveRecord
module Associations
# Association proxies in Active Record are middlemen between the object that
# holds the association, known as the <tt>@owner</tt>, and the actual associated
# object, known as the <tt>@target</tt>. The kind of association any proxy is
# about is available in <tt>@reflection</tt>. That's an instance of the class
# ActiveRecord::Reflection::AssociationReflection.
#
# For example, given
#
# class Blog < ActiveRecord::Base
# has_many :posts
# end
#
# blog = Blog.find(:first)
#
# the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
# <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
# the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
#
# This class has most of the basic instance methods removed, and delegates
# unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
# corner case, it even removes the +class+ method and that's why you get
#
# blog.posts.class # => Array
#
# though the object behind <tt>blog.posts</tt> is not an Array, but an
# ActiveRecord::Associations::HasManyAssociation.
#
# The <tt>@target</tt> object is not \loaded until needed. For example,
#
# blog.posts.count
#
# is computed directly through SQL and does not trigger by itself the
# instantiation of the actual post records.
class CollectionProxy # :nodoc:
alias :proxy_extend :extend
instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }
delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from,
:lock, :readonly, :having, :to => :scoped
delegate :target, :load_target, :loaded?, :scoped,
:to => :@association
delegate :select, :find, :first, :last,
:build, :create, :create!,
:concat, :delete_all, :destroy_all, :delete, :destroy, :uniq,
:sum, :count, :size, :length, :empty?,
:any?, :many?, :include?,
:to => :@association
def initialize(association)
@association = association
Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) }
end
def respond_to?(*args)
super ||
(load_target && target.respond_to?(*args)) ||
@association.klass.respond_to?(*args)
end
def method_missing(method, *args, &block)
match = DynamicFinderMatch.match(method)
if match && match.creator?
attributes = match.attribute_names
return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)])
end
if target.respond_to?(method) || (!@association.klass.respond_to?(method) && Class.respond_to?(method))
if load_target
if target.respond_to?(method)
target.send(method, *args, &block)
else
begin
super
rescue NoMethodError => e
raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}")
end
end
end
elsif @association.klass.scopes[method]
@association.cached_scope(method, args)
else
scoped.readonly(nil).send(method, *args, &block)
end
end
# Forwards <tt>===</tt> explicitly to the \target because the instance method
# removal above doesn't catch it. Loads the \target if needed.
def ===(other)
other === load_target
end
def to_ary
load_target.dup
end
alias_method :to_a, :to_ary
def <<(*records)
@association.concat(records) && self
end
alias_method :push, :<<
def clear
delete_all
self
end
def reload
@association.reload
self
end
def new(*args, &block)
if @association.is_a?(HasManyThroughAssociation)
@association.build(*args, &block)
else
method_missing(:new, *args, &block)
end
end
end
end
end