drape 1.0.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +27 -0
  3. data/.gitignore +18 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +1159 -0
  6. data/.travis.yml +14 -0
  7. data/.yardopts +1 -0
  8. data/CHANGELOG.md +230 -0
  9. data/CONTRIBUTING.md +20 -0
  10. data/Gemfile +16 -0
  11. data/Guardfile +26 -0
  12. data/LICENSE +7 -0
  13. data/README.md +592 -0
  14. data/Rakefile +76 -0
  15. data/drape.gemspec +31 -0
  16. data/gemfiles/5.0.gemfile +8 -0
  17. data/lib/drape.rb +63 -0
  18. data/lib/drape/automatic_delegation.rb +55 -0
  19. data/lib/drape/collection_decorator.rb +102 -0
  20. data/lib/drape/decoratable.rb +93 -0
  21. data/lib/drape/decoratable/equality.rb +26 -0
  22. data/lib/drape/decorated_association.rb +33 -0
  23. data/lib/drape/decorates_assigned.rb +44 -0
  24. data/lib/drape/decorator.rb +282 -0
  25. data/lib/drape/delegation.rb +13 -0
  26. data/lib/drape/factory.rb +91 -0
  27. data/lib/drape/finders.rb +36 -0
  28. data/lib/drape/helper_proxy.rb +43 -0
  29. data/lib/drape/helper_support.rb +5 -0
  30. data/lib/drape/lazy_helpers.rb +13 -0
  31. data/lib/drape/railtie.rb +69 -0
  32. data/lib/drape/tasks/test.rake +9 -0
  33. data/lib/drape/test/devise_helper.rb +30 -0
  34. data/lib/drape/test/minitest_integration.rb +6 -0
  35. data/lib/drape/test/rspec_integration.rb +20 -0
  36. data/lib/drape/test_case.rb +42 -0
  37. data/lib/drape/undecorate.rb +9 -0
  38. data/lib/drape/version.rb +3 -0
  39. data/lib/drape/view_context.rb +104 -0
  40. data/lib/drape/view_context/build_strategy.rb +46 -0
  41. data/lib/drape/view_helpers.rb +34 -0
  42. data/lib/generators/controller_override.rb +17 -0
  43. data/lib/generators/mini_test/decorator_generator.rb +20 -0
  44. data/lib/generators/mini_test/templates/decorator_spec.rb +4 -0
  45. data/lib/generators/mini_test/templates/decorator_test.rb +4 -0
  46. data/lib/generators/rails/decorator_generator.rb +36 -0
  47. data/lib/generators/rails/templates/decorator.rb +19 -0
  48. data/lib/generators/rspec/decorator_generator.rb +9 -0
  49. data/lib/generators/rspec/templates/decorator_spec.rb +4 -0
  50. data/lib/generators/test_unit/decorator_generator.rb +9 -0
  51. data/lib/generators/test_unit/templates/decorator_test.rb +4 -0
  52. data/spec/draper/collection_decorator_spec.rb +305 -0
  53. data/spec/draper/decoratable/equality_spec.rb +10 -0
  54. data/spec/draper/decoratable_spec.rb +203 -0
  55. data/spec/draper/decorated_association_spec.rb +85 -0
  56. data/spec/draper/decorates_assigned_spec.rb +70 -0
  57. data/spec/draper/decorator_spec.rb +828 -0
  58. data/spec/draper/factory_spec.rb +250 -0
  59. data/spec/draper/finders_spec.rb +165 -0
  60. data/spec/draper/helper_proxy_spec.rb +61 -0
  61. data/spec/draper/lazy_helpers_spec.rb +21 -0
  62. data/spec/draper/undecorate_spec.rb +19 -0
  63. data/spec/draper/view_context/build_strategy_spec.rb +116 -0
  64. data/spec/draper/view_context_spec.rb +160 -0
  65. data/spec/draper/view_helpers_spec.rb +8 -0
  66. data/spec/dummy/.gitignore +21 -0
  67. data/spec/dummy/Rakefile +6 -0
  68. data/spec/dummy/app/assets/config/manifest.js +3 -0
  69. data/spec/dummy/app/assets/images/.keep +0 -0
  70. data/spec/dummy/app/assets/javascripts/application.js +16 -0
  71. data/spec/dummy/app/assets/javascripts/cable.js +13 -0
  72. data/spec/dummy/app/assets/javascripts/channels/.keep +0 -0
  73. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  74. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  75. data/spec/dummy/app/channels/application_cable/connection.rb +5 -0
  76. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  77. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  78. data/spec/dummy/app/controllers/posts_controller.rb +21 -0
  79. data/spec/dummy/app/decorators/mongoid_post_decorator.rb +4 -0
  80. data/spec/dummy/app/decorators/post_decorator.rb +59 -0
  81. data/spec/dummy/app/helpers/application_helper.rb +5 -0
  82. data/spec/dummy/app/jobs/application_job.rb +2 -0
  83. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  84. data/spec/dummy/app/mailers/post_mailer.rb +18 -0
  85. data/spec/dummy/app/models/admin.rb +5 -0
  86. data/spec/dummy/app/models/application_record.rb +3 -0
  87. data/spec/dummy/app/models/concerns/.keep +0 -0
  88. data/spec/dummy/app/models/mongoid_post.rb +5 -0
  89. data/spec/dummy/app/models/post.rb +3 -0
  90. data/spec/dummy/app/models/user.rb +5 -0
  91. data/spec/dummy/app/views/layouts/application.html.erb +9 -0
  92. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  93. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  94. data/spec/dummy/app/views/post_mailer/decorated_email.html.erb +31 -0
  95. data/spec/dummy/app/views/posts/_post.html.erb +40 -0
  96. data/spec/dummy/app/views/posts/show.html.erb +1 -0
  97. data/spec/dummy/bin/bundle +3 -0
  98. data/spec/dummy/bin/rails +9 -0
  99. data/spec/dummy/bin/rake +9 -0
  100. data/spec/dummy/bin/setup +34 -0
  101. data/spec/dummy/bin/spring +15 -0
  102. data/spec/dummy/bin/update +29 -0
  103. data/spec/dummy/config.ru +5 -0
  104. data/spec/dummy/config/application.rb +21 -0
  105. data/spec/dummy/config/boot.rb +2 -0
  106. data/spec/dummy/config/cable.yml +10 -0
  107. data/spec/dummy/config/database.yml +25 -0
  108. data/spec/dummy/config/environment.rb +5 -0
  109. data/spec/dummy/config/environments/development.rb +51 -0
  110. data/spec/dummy/config/environments/production.rb +91 -0
  111. data/spec/dummy/config/environments/test.rb +42 -0
  112. data/spec/dummy/config/initializers/active_record_belongs_to_required_by_default.rb +6 -0
  113. data/spec/dummy/config/initializers/application_controller_renderer.rb +6 -0
  114. data/spec/dummy/config/initializers/assets.rb +11 -0
  115. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  116. data/spec/dummy/config/initializers/callback_terminator.rb +6 -0
  117. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  118. data/spec/dummy/config/initializers/devise.rb +3 -0
  119. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  120. data/spec/dummy/config/initializers/inflections.rb +16 -0
  121. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  122. data/spec/dummy/config/initializers/per_form_csrf_tokens.rb +4 -0
  123. data/spec/dummy/config/initializers/request_forgery_protection.rb +4 -0
  124. data/spec/dummy/config/initializers/session_store.rb +3 -0
  125. data/spec/dummy/config/initializers/ssl_options.rb +4 -0
  126. data/spec/dummy/config/initializers/to_time_preserves_timezone.rb +10 -0
  127. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  128. data/spec/dummy/config/locales/en.yml +23 -0
  129. data/spec/dummy/config/mongoid.yml +16 -0
  130. data/spec/dummy/config/puma.rb +47 -0
  131. data/spec/dummy/config/routes.rb +8 -0
  132. data/spec/dummy/config/secrets.yml +22 -0
  133. data/spec/dummy/config/spring.rb +6 -0
  134. data/spec/dummy/db/migrate/20121019115657_create_posts.rb +7 -0
  135. data/spec/dummy/db/schema.rb +19 -0
  136. data/spec/dummy/db/seeds.rb +2 -0
  137. data/spec/dummy/lib/assets/.keep +0 -0
  138. data/spec/dummy/lib/tasks/.keep +0 -0
  139. data/spec/dummy/lib/tasks/test.rake +16 -0
  140. data/spec/dummy/log/.keep +0 -0
  141. data/spec/dummy/public/404.html +67 -0
  142. data/spec/dummy/public/422.html +67 -0
  143. data/spec/dummy/public/500.html +66 -0
  144. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  145. data/spec/dummy/public/apple-touch-icon.png +0 -0
  146. data/spec/dummy/public/favicon.ico +0 -0
  147. data/spec/dummy/public/robots.txt +5 -0
  148. data/spec/dummy/spec/decorators/active_model_serializers_spec.rb +12 -0
  149. data/spec/dummy/spec/decorators/devise_spec.rb +64 -0
  150. data/spec/dummy/spec/decorators/helpers_spec.rb +21 -0
  151. data/spec/dummy/spec/decorators/post_decorator_spec.rb +66 -0
  152. data/spec/dummy/spec/decorators/spec_type_spec.rb +7 -0
  153. data/spec/dummy/spec/decorators/view_context_spec.rb +22 -0
  154. data/spec/dummy/spec/mailers/post_mailer_spec.rb +33 -0
  155. data/spec/dummy/spec/models/mongoid_post_spec.rb +8 -0
  156. data/spec/dummy/spec/models/post_spec.rb +6 -0
  157. data/spec/dummy/spec/shared_examples/decoratable.rb +26 -0
  158. data/spec/dummy/spec/spec_helper.rb +8 -0
  159. data/spec/dummy/test/controllers/.keep +0 -0
  160. data/spec/dummy/test/fixtures/.keep +0 -0
  161. data/spec/dummy/test/fixtures/files/.keep +0 -0
  162. data/spec/dummy/test/helpers/.keep +0 -0
  163. data/spec/dummy/test/integration/.keep +0 -0
  164. data/spec/dummy/test/mailers/.keep +0 -0
  165. data/spec/dummy/test/models/.keep +0 -0
  166. data/spec/dummy/test/test_helper.rb +10 -0
  167. data/spec/dummy/vendor/assets/javascripts/.keep +0 -0
  168. data/spec/dummy/vendor/assets/stylesheets/.keep +0 -0
  169. data/spec/generators/controller/controller_generator_spec.rb +24 -0
  170. data/spec/generators/decorator/decorator_generator_spec.rb +94 -0
  171. data/spec/integration/integration_spec.rb +68 -0
  172. data/spec/performance/active_record.rb +4 -0
  173. data/spec/performance/benchmark.rb +57 -0
  174. data/spec/performance/decorators.rb +41 -0
  175. data/spec/performance/models.rb +20 -0
  176. data/spec/spec_helper.rb +41 -0
  177. data/spec/support/dummy_app.rb +88 -0
  178. data/spec/support/matchers/have_text.rb +50 -0
  179. data/spec/support/shared_examples/decoratable_equality.rb +40 -0
  180. data/spec/support/shared_examples/view_helpers.rb +39 -0
  181. metadata +497 -0
@@ -0,0 +1,33 @@
1
+ module Drape
2
+ # @private
3
+ class DecoratedAssociation
4
+ def initialize(owner, association, options)
5
+ options.assert_valid_keys(:with, :scope, :context)
6
+
7
+ @owner = owner
8
+ @association = association
9
+
10
+ @scope = options[:scope]
11
+
12
+ decorator_class = options[:with]
13
+ context = options.fetch(:context, -> (inner_context) { inner_context })
14
+ @factory = Drape::Factory.new(with: decorator_class, context: context)
15
+ end
16
+
17
+ def call
18
+ decorate unless defined?(@decorated)
19
+ @decorated
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :factory, :owner, :association, :scope
25
+
26
+ def decorate
27
+ associated = owner.object.send(association)
28
+ associated = associated.send(scope) if scope
29
+
30
+ @decorated = factory.decorate(associated, context_args: owner.context)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,44 @@
1
+ module Drape
2
+ module DecoratesAssigned
3
+ # @overload decorates_assigned(*variables, options = {})
4
+ # Defines a helper method to access decorated instance variables.
5
+ #
6
+ # @example
7
+ # # app/controllers/articles_controller.rb
8
+ # class ArticlesController < ApplicationController
9
+ # decorates_assigned :article
10
+ #
11
+ # def show
12
+ # @article = Article.find(params[:id])
13
+ # end
14
+ # end
15
+ #
16
+ # # app/views/articles/show.html.erb
17
+ # <%= article.decorated_title %>
18
+ #
19
+ # @param [Symbols*] variables
20
+ # names of the instance variables to decorate (without the `@`).
21
+ # @param [Hash] options
22
+ # @option options [Decorator, CollectionDecorator] :with (nil)
23
+ # decorator class to use. If nil, it is inferred from the instance
24
+ # variable.
25
+ # @option options [Hash, #call] :context
26
+ # extra data to be stored in the decorator. If a Proc is given, it will
27
+ # be passed the controller and should return a new context hash.
28
+ def decorates_assigned(*variables)
29
+ factory = Drape::Factory.new(variables.extract_options!)
30
+
31
+ variables.each do |variable|
32
+ undecorated = "@#{variable}"
33
+ decorated = "@decorated_#{variable}"
34
+
35
+ define_method variable do
36
+ return instance_variable_get(decorated) if instance_variable_defined?(decorated)
37
+ instance_variable_set decorated, factory.decorate(instance_variable_get(undecorated), context_args: self)
38
+ end
39
+
40
+ helper_method variable
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,282 @@
1
+ module Drape
2
+ class Decorator
3
+ include Drape::ViewHelpers
4
+ extend Drape::Delegation
5
+
6
+ include ActiveModel::Serialization
7
+ include ActiveModel::Serializers::JSON
8
+ include ActiveModel::Serializers::Xml
9
+
10
+ # @return the object being decorated.
11
+ attr_reader :object
12
+ alias model object
13
+ alias source object # TODO: deprecate this
14
+ alias to_source object # TODO: deprecate this
15
+
16
+ # @return [Hash] extra data to be used in user-defined methods.
17
+ attr_accessor :context
18
+
19
+ # Wraps an object in a new instance of the decorator.
20
+ #
21
+ # Decorators may be applied to other decorators. However, applying a
22
+ # decorator to an instance of itself will create a decorator with the same
23
+ # source as the original, rather than redecorating the other instance.
24
+ #
25
+ # @param [Object] object
26
+ # object to decorate.
27
+ # @option options [Hash] :context ({})
28
+ # extra data to be stored in the decorator and used in user-defined
29
+ # methods.
30
+ def initialize(object, options = {})
31
+ options.assert_valid_keys(:context)
32
+ @object = object
33
+ @context = options.fetch(:context, {})
34
+ handle_multiple_decoration(options) if object.instance_of?(self.class)
35
+ end
36
+
37
+ class << self
38
+ alias decorate new
39
+ end
40
+
41
+ # Automatically delegates instance methods to the source object. Class
42
+ # methods will be delegated to the {object_class}, if it is set.
43
+ #
44
+ # @return [void]
45
+ def self.delegate_all
46
+ include Drape::AutomaticDelegation
47
+ end
48
+
49
+ # Sets the source class corresponding to the decorator class.
50
+ #
51
+ # @note This is only necessary if you wish to proxy class methods to the
52
+ # source (including when using {decorates_finders}), and the source class
53
+ # cannot be inferred from the decorator class (e.g. `ProductDecorator`
54
+ # maps to `Product`).
55
+ # @param [String, Symbol, Class] object_class
56
+ # source class (or class name) that corresponds to this decorator.
57
+ # @return [void]
58
+ def self.decorates(object_class)
59
+ @object_class = object_class.to_s.camelize.constantize
60
+ alias_object_to_object_class_name
61
+ end
62
+
63
+ # Returns the source class corresponding to the decorator class, as set by
64
+ # {decorates}, or as inferred from the decorator class name (e.g.
65
+ # `ProductDecorator` maps to `Product`).
66
+ #
67
+ # @return [Class] the source class that corresponds to this decorator.
68
+ def self.object_class
69
+ @object_class ||= inferred_object_class
70
+ end
71
+
72
+ # Checks whether this decorator class has a corresponding {object_class}.
73
+ def self.object_class?
74
+ object_class
75
+ rescue Drape::UninferrableSourceError
76
+ false
77
+ end
78
+
79
+ class << self # TODO deprecate this
80
+ alias source_class object_class
81
+ alias source_class? object_class?
82
+ end
83
+
84
+ # Automatically decorates ActiveRecord finder methods, so that you can use
85
+ # `ProductDecorator.find(id)` instead of
86
+ # `ProductDecorator.decorate(Product.find(id))`.
87
+ #
88
+ # Finder methods are applied to the {object_class}.
89
+ #
90
+ # @return [void]
91
+ def self.decorates_finders
92
+ extend Drape::Finders
93
+ end
94
+
95
+ # Automatically decorate an association.
96
+ #
97
+ # @param [Symbol] association
98
+ # name of the association to decorate (e.g. `:products`).
99
+ # @option options [Class] :with
100
+ # the decorator to apply to the association.
101
+ # @option options [Symbol] :scope
102
+ # a scope to apply when fetching the association.
103
+ # @option options [Hash, #call] :context
104
+ # extra data to be stored in the associated decorator. If omitted, the
105
+ # associated decorator's context will be the same as the parent
106
+ # decorator's. If a Proc is given, it will be called with the parent's
107
+ # context and should return a new context hash for the association.
108
+ # @return [void]
109
+ def self.decorates_association(association, options = {})
110
+ options.assert_valid_keys(:with, :scope, :context)
111
+ define_method(association) do
112
+ decorated_associations[association] ||= Drape::DecoratedAssociation.new(self, association, options)
113
+ decorated_associations[association].call
114
+ end
115
+ end
116
+
117
+ # @overload decorates_associations(*associations, options = {})
118
+ # Automatically decorate multiple associations.
119
+ # @param [Symbols*] associations
120
+ # names of the associations to decorate.
121
+ # @param [Hash] options
122
+ # see {decorates_association}.
123
+ # @return [void]
124
+ def self.decorates_associations(*associations)
125
+ options = associations.extract_options!
126
+ associations.each do |association|
127
+ decorates_association(association, options)
128
+ end
129
+ end
130
+
131
+ # Decorates a collection of objects. The class of the collection decorator
132
+ # is inferred from the decorator class if possible (e.g. `ProductDecorator`
133
+ # maps to `ProductsDecorator`), but otherwise defaults to
134
+ # {Drape::CollectionDecorator}.
135
+ #
136
+ # @param [Object] object
137
+ # collection to decorate.
138
+ # @option options [Class, nil] :with (self)
139
+ # the decorator class used to decorate each item. When `nil`, it is
140
+ # inferred from each item.
141
+ # @option options [Hash] :context
142
+ # extra data to be stored in the collection decorator.
143
+ def self.decorate_collection(object, options = {})
144
+ options.assert_valid_keys(:with, :context)
145
+ collection_decorator_class.new(object, options.reverse_merge(with: self))
146
+ end
147
+
148
+ # @return [Array<Class>] the list of decorators that have been applied to
149
+ # the object.
150
+ def applied_decorators
151
+ chain = object.respond_to?(:applied_decorators) ? object.applied_decorators : []
152
+ chain << self.class
153
+ end
154
+
155
+ # Checks if a given decorator has been applied to the object.
156
+ #
157
+ # @param [Class] decorator_class
158
+ def decorated_with?(decorator_class)
159
+ applied_decorators.include?(decorator_class)
160
+ end
161
+
162
+ # Checks if this object is decorated.
163
+ #
164
+ # @return [true]
165
+ def decorated?
166
+ true
167
+ end
168
+
169
+ # Compares the source object with a possibly-decorated object.
170
+ #
171
+ # @return [Boolean]
172
+ def ==(other)
173
+ Drape::Decoratable::Equality.test(object, other)
174
+ end
175
+
176
+ # Delegates equality to :== as expected
177
+ #
178
+ # @return [Boolean]
179
+ def eql?(other)
180
+ self == other
181
+ end
182
+
183
+ # Returns a unique hash for a decorated object based on
184
+ # the decorator class and the object being decorated.
185
+ #
186
+ # @return [Fixnum]
187
+ def hash
188
+ self.class.hash ^ object.hash
189
+ end
190
+
191
+ # Checks if `self.kind_of?(klass)` or `object.kind_of?(klass)`
192
+ #
193
+ # @param [Class] klass
194
+ def kind_of?(klass)
195
+ super || object.is_a?(klass)
196
+ end
197
+ alias is_a? kind_of?
198
+
199
+ # Checks if `self.instance_of?(klass)` or `object.instance_of?(klass)`
200
+ #
201
+ # @param [Class] klass
202
+ def instance_of?(klass)
203
+ super || object.instance_of?(klass)
204
+ end
205
+
206
+ delegate :to_s
207
+
208
+ # In case object is nil
209
+ delegate :present?, :blank?
210
+
211
+ # ActiveModel compatibility
212
+ # @private
213
+ def to_model
214
+ self
215
+ end
216
+
217
+ # @return [Hash] the object's attributes, sliced to only include those
218
+ # implemented by the decorator.
219
+ def attributes
220
+ object.attributes.select { |attribute, _| respond_to?(attribute) }
221
+ end
222
+
223
+ # ActiveModel compatibility
224
+ delegate :to_param, :to_partial_path
225
+
226
+ # ActiveModel compatibility
227
+ singleton_class.delegate :model_name, to: :object_class
228
+
229
+ # @return [Class] the class created by {decorate_collection}.
230
+ def self.collection_decorator_class
231
+ name = collection_decorator_name
232
+ name.constantize
233
+ rescue NameError => error
234
+ raise if name && !error.missing_name?(name)
235
+ Drape::CollectionDecorator
236
+ end
237
+
238
+ def self.inherited(subclass)
239
+ subclass.alias_object_to_object_class_name
240
+ super
241
+ end
242
+
243
+ def self.alias_object_to_object_class_name
244
+ alias_method object_class.name.underscore, :object if object_class?
245
+ end
246
+
247
+ def self.object_class_name
248
+ raise NameError if name.nil? || name.demodulize !~ /.+Decorator$/
249
+ name.chomp('Decorator')
250
+ end
251
+
252
+ def self.inferred_object_class
253
+ name = object_class_name
254
+ name.constantize
255
+ rescue NameError => error
256
+ raise if name && !error.missing_name?(name)
257
+ raise Drape::UninferrableSourceError, self
258
+ end
259
+
260
+ def self.collection_decorator_name
261
+ plural = object_class_name.pluralize
262
+ raise NameError if plural == object_class_name
263
+ "#{plural}Decorator"
264
+ end
265
+
266
+ private
267
+
268
+ def handle_multiple_decoration(options)
269
+ if object.applied_decorators.last == self.class
270
+ @context = object.context unless options.key?(:context)
271
+ @object = object.object
272
+ else
273
+ warn "Reapplying #{self.class} decorator to target that is already decorated with it. " +
274
+ "Call stack:\n#{caller(1).join("\n")}"
275
+ end
276
+ end
277
+
278
+ def decorated_associations
279
+ @decorated_associations ||= {}
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,13 @@
1
+ module Drape
2
+ module Delegation
3
+ # @overload delegate(*methods, options = {})
4
+ # Overrides {http://api.rubyonrails.org/classes/Module.html#method-i-delegate Module.delegate}
5
+ # to make `:object` the default delegation target.
6
+ #
7
+ # @return [void]
8
+ def delegate(*methods)
9
+ options = methods.extract_options!
10
+ super(*methods, options.reverse_merge(to: :object))
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,91 @@
1
+ module Drape
2
+ class Factory
3
+ # Creates a decorator factory.
4
+ #
5
+ # @option options [Decorator, CollectionDecorator] :with (nil)
6
+ # decorator class to use. If nil, it is inferred from the object
7
+ # passed to {#decorate}.
8
+ # @option options [Hash, #call] context
9
+ # extra data to be stored in created decorators. If a proc is given, it
10
+ # will be called each time {#decorate} is called and its return value
11
+ # will be used as the context.
12
+ def initialize(options = {})
13
+ options.assert_valid_keys(:with, :context)
14
+ @decorator_class = options.delete(:with)
15
+ @default_options = options
16
+ end
17
+
18
+ # Decorates an object, inferring whether to create a singular or collection
19
+ # decorator from the type of object passed.
20
+ #
21
+ # @param [Object] object
22
+ # object to decorate.
23
+ # @option options [Hash] context
24
+ # extra data to be stored in the decorator. Overrides any context passed
25
+ # to the constructor.
26
+ # @option options [Object, Array] context_args (nil)
27
+ # argument(s) to be passed to the context proc.
28
+ # @return [Decorator, CollectionDecorator] the decorated object.
29
+ def decorate(object, options = {})
30
+ return nil if object.nil?
31
+ Worker.new(decorator_class, object).call(options.reverse_merge(default_options))
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :decorator_class, :default_options
37
+
38
+ # @private
39
+ class Worker
40
+ def initialize(decorator_class, object)
41
+ @decorator_class = decorator_class
42
+ @object = object
43
+ end
44
+
45
+ def call(options)
46
+ update_context options
47
+ decorator.call(object, options)
48
+ end
49
+
50
+ def decorator
51
+ return decorator_method(decorator_class) if decorator_class
52
+ return object_decorator if decoratable?
53
+ return decorator_method(Drape::CollectionDecorator) if collection?
54
+ raise Drape::UninferrableDecoratorError, object.class
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :decorator_class, :object
60
+
61
+ def object_decorator
62
+ if collection?
63
+ ->(object, options) { object.decorator_class.decorate_collection(object, options.reverse_merge(with: nil)) }
64
+ else
65
+ ->(object, options) { object.decorate(options) }
66
+ end
67
+ end
68
+
69
+ def decorator_method(klass)
70
+ if collection? && klass.respond_to?(:decorate_collection)
71
+ klass.method(:decorate_collection)
72
+ else
73
+ klass.method(:decorate)
74
+ end
75
+ end
76
+
77
+ def collection?
78
+ object.respond_to?(:first) && !object.is_a?(Struct)
79
+ end
80
+
81
+ def decoratable?
82
+ object.respond_to?(:decorate)
83
+ end
84
+
85
+ def update_context(options)
86
+ args = options.delete(:context_args)
87
+ options[:context] = options[:context].call(*Array.wrap(args)) if options[:context].respond_to?(:call)
88
+ end
89
+ end
90
+ end
91
+ end