/
decorator.rb
273 lines (237 loc) · 8.54 KB
/
decorator.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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
module Draper
class Decorator
include Draper::ViewHelpers
extend Draper::Delegation
include ActiveModel::Serialization
include ActiveModel::Serializers::JSON
include ActiveModel::Serializers::Xml
# @return the object being decorated.
attr_reader :object
alias_method :model, :object
# @return [Hash] extra data to be used in user-defined methods.
attr_accessor :context
# Wraps an object in a new instance of the decorator.
#
# Decorators may be applied to other decorators. However, applying a
# decorator to an instance of itself will create a decorator with the same
# source as the original, rather than redecorating the other instance.
#
# @param [Object] object
# object to decorate.
# @option options [Hash] :context ({})
# extra data to be stored in the decorator and used in user-defined
# methods.
def initialize(object, options = {})
options.assert_valid_keys(:context)
@object = object
@context = options.fetch(:context, {})
handle_multiple_decoration(options) if object.instance_of?(self.class)
end
class << self
alias_method :decorate, :new
end
# Automatically delegates instance methods to the source object. Class
# methods will be delegated to the {object_class}, if it is set.
#
# @return [void]
def self.delegate_all
include Draper::AutomaticDelegation
end
# Sets the source class corresponding to the decorator class.
#
# @note This is only necessary if you wish to proxy class methods to the
# source (including when using {decorates_finders}), and the source class
# cannot be inferred from the decorator class (e.g. `ProductDecorator`
# maps to `Product`).
# @param [String, Symbol, Class] object_class
# source class (or class name) that corresponds to this decorator.
# @return [void]
def self.decorates(object_class)
@object_class = object_class.to_s.camelize.constantize
alias_object_to_object_class_name
end
# Returns the source class corresponding to the decorator class, as set by
# {decorates}, or as inferred from the decorator class name (e.g.
# `ProductDecorator` maps to `Product`).
#
# @return [Class] the source class that corresponds to this decorator.
def self.object_class
@object_class ||= inferred_object_class
end
# Checks whether this decorator class has a corresponding {object_class}.
def self.object_class?
object_class
rescue Draper::UninferrableObjectError
false
end
# Automatically decorates ActiveRecord finder methods, so that you can use
# `ProductDecorator.find(id)` instead of
# `ProductDecorator.decorate(Product.find(id))`.
#
# Finder methods are applied to the {object_class}.
#
# @return [void]
def self.decorates_finders
extend Draper::Finders
end
# Automatically decorate an association.
#
# @param [Symbol] association
# name of the association to decorate (e.g. `:products`).
# @option options [Class] :with
# the decorator to apply to the association.
# @option options [Symbol] :scope
# a scope to apply when fetching the association.
# @option options [Hash, #call] :context
# extra data to be stored in the associated decorator. If omitted, the
# associated decorator's context will be the same as the parent
# decorator's. If a Proc is given, it will be called with the parent's
# context and should return a new context hash for the association.
# @return [void]
def self.decorates_association(association, options = {})
options.assert_valid_keys(:with, :scope, :context)
define_method(association) do
decorated_associations[association] ||= Draper::DecoratedAssociation.new(self, association, options)
decorated_associations[association].call
end
end
# @overload decorates_associations(*associations, options = {})
# Automatically decorate multiple associations.
# @param [Symbols*] associations
# names of the associations to decorate.
# @param [Hash] options
# see {decorates_association}.
# @return [void]
def self.decorates_associations(*associations)
options = associations.extract_options!
associations.each do |association|
decorates_association(association, options)
end
end
# Decorates a collection of objects. The class of the collection decorator
# is inferred from the decorator class if possible (e.g. `ProductDecorator`
# maps to `ProductsDecorator`), but otherwise defaults to
# {Draper::CollectionDecorator}.
#
# @param [Object] object
# collection to decorate.
# @option options [Class, nil] :with (self)
# the decorator class used to decorate each item. When `nil`, it is
# inferred from each item.
# @option options [Hash] :context
# extra data to be stored in the collection decorator.
def self.decorate_collection(object, options = {})
options.assert_valid_keys(:with, :context)
collection_decorator_class.new(object, options.reverse_merge(with: self))
end
# @return [Array<Class>] the list of decorators that have been applied to
# the object.
def applied_decorators
chain = object.respond_to?(:applied_decorators) ? object.applied_decorators : []
chain << self.class
end
# Checks if a given decorator has been applied to the object.
#
# @param [Class] decorator_class
def decorated_with?(decorator_class)
applied_decorators.include?(decorator_class)
end
# Checks if this object is decorated.
#
# @return [true]
def decorated?
true
end
# Compares the source object with a possibly-decorated object.
#
# @return [Boolean]
def ==(other)
Draper::Decoratable::Equality.test(object, other)
end
# Delegates equality to :== as expected
#
# @return [Boolean]
def eql?(other)
self == other
end
# Returns a unique hash for a decorated object based on
# the decorator class and the object being decorated.
#
# @return [Fixnum]
def hash
self.class.hash ^ object.hash
end
# Checks if `self.kind_of?(klass)` or `object.kind_of?(klass)`
#
# @param [Class] klass
def kind_of?(klass)
super || object.kind_of?(klass)
end
alias_method :is_a?, :kind_of?
# Checks if `self.instance_of?(klass)` or `object.instance_of?(klass)`
#
# @param [Class] klass
def instance_of?(klass)
super || object.instance_of?(klass)
end
delegate :to_s
# In case object is nil
delegate :present?, :blank?
# ActiveModel compatibility
# @private
def to_model
self
end
# @return [Hash] the object's attributes, sliced to only include those
# implemented by the decorator.
def attributes
object.attributes.select {|attribute, _| respond_to?(attribute) }
end
# ActiveModel compatibility
delegate :to_param, :to_partial_path
# ActiveModel compatibility
singleton_class.delegate :model_name, to: :object_class
# @return [Class] the class created by {decorate_collection}.
def self.collection_decorator_class
name = collection_decorator_name
name.constantize
rescue NameError
Draper::CollectionDecorator
end
private
def self.inherited(subclass)
subclass.alias_object_to_object_class_name
super
end
def self.alias_object_to_object_class_name
alias_method object_class.name.underscore, :object if object_class?
end
def self.object_class_name
raise NameError if name.nil? || name.demodulize !~ /.+Decorator$/
name.chomp("Decorator")
end
def self.inferred_object_class
name = object_class_name
name.constantize
rescue NameError => error
raise if name && !error.missing_name?(name)
raise Draper::UninferrableObjectError.new(self)
end
def self.collection_decorator_name
plural = object_class_name.pluralize
raise NameError if plural == object_class_name
"#{plural}Decorator"
end
def handle_multiple_decoration(options)
if object.applied_decorators.last == self.class
@context = object.context unless options.has_key?(:context)
@object = object.object
else
warn "Reapplying #{self.class} decorator to target that is already decorated with it. Call stack:\n#{caller(1).join("\n")}"
end
end
def decorated_associations
@decorated_associations ||= {}
end
end
end