draper 1.0.0.beta6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.travis.yml +6 -0
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +20 -0
  4. data/Gemfile +11 -0
  5. data/README.md +14 -17
  6. data/Rakefile +5 -3
  7. data/draper.gemspec +2 -2
  8. data/lib/draper.rb +2 -1
  9. data/lib/draper/automatic_delegation.rb +50 -0
  10. data/lib/draper/collection_decorator.rb +26 -7
  11. data/lib/draper/decoratable.rb +71 -32
  12. data/lib/draper/decorated_association.rb +11 -7
  13. data/lib/draper/decorator.rb +114 -148
  14. data/lib/draper/delegation.rb +13 -0
  15. data/lib/draper/finders.rb +9 -6
  16. data/lib/draper/helper_proxy.rb +4 -3
  17. data/lib/draper/lazy_helpers.rb +10 -6
  18. data/lib/draper/railtie.rb +5 -4
  19. data/lib/draper/tasks/test.rake +22 -0
  20. data/lib/draper/test/devise_helper.rb +34 -0
  21. data/lib/draper/test/minitest_integration.rb +2 -3
  22. data/lib/draper/test/rspec_integration.rb +4 -59
  23. data/lib/draper/test_case.rb +33 -0
  24. data/lib/draper/version.rb +1 -1
  25. data/lib/draper/view_helpers.rb +4 -3
  26. data/lib/generators/decorator/templates/decorator.rb +7 -25
  27. data/lib/generators/mini_test/decorator_generator.rb +20 -0
  28. data/lib/generators/mini_test/templates/decorator_spec.rb +4 -0
  29. data/lib/generators/mini_test/templates/decorator_test.rb +4 -0
  30. data/lib/generators/test_unit/templates/decorator_test.rb +1 -1
  31. data/spec/draper/collection_decorator_spec.rb +25 -3
  32. data/spec/draper/decorated_association_spec.rb +18 -7
  33. data/spec/draper/decorator_spec.rb +125 -165
  34. data/spec/draper/finders_spec.rb +0 -13
  35. data/spec/dummy/app/controllers/localized_urls.rb +1 -1
  36. data/spec/dummy/app/controllers/posts_controller.rb +3 -9
  37. data/spec/dummy/app/decorators/post_decorator.rb +4 -1
  38. data/spec/dummy/config/application.rb +3 -3
  39. data/spec/dummy/config/environments/development.rb +4 -4
  40. data/spec/dummy/config/environments/test.rb +2 -2
  41. data/spec/dummy/lib/tasks/test.rake +10 -0
  42. data/spec/dummy/mini_test/mini_test_integration_test.rb +46 -0
  43. data/spec/dummy/spec/decorators/post_decorator_spec.rb +2 -2
  44. data/spec/dummy/spec/decorators/rspec_integration_spec.rb +19 -0
  45. data/spec/dummy/spec/mailers/post_mailer_spec.rb +2 -2
  46. data/spec/dummy/spec/spec_helper.rb +0 -1
  47. data/spec/generators/decorator/decorator_generator_spec.rb +43 -2
  48. data/spec/integration/integration_spec.rb +2 -2
  49. data/spec/spec_helper.rb +17 -21
  50. data/spec/support/active_record.rb +0 -13
  51. data/spec/support/dummy_app.rb +4 -3
  52. metadata +26 -23
  53. data/lib/draper/security.rb +0 -48
  54. data/lib/draper/tasks/tu.rake +0 -5
  55. data/lib/draper/test/test_unit_integration.rb +0 -18
  56. data/spec/draper/security_spec.rb +0 -158
  57. data/spec/dummy/config/initializers/wrap_parameters.rb +0 -14
  58. data/spec/dummy/lib/tasks/spec.rake +0 -5
  59. data/spec/minitest-rails/spec_type_spec.rb +0 -63
@@ -1,4 +1,5 @@
1
1
  module Draper
2
+ # @private
2
3
  class DecoratedAssociation
3
4
 
4
5
  def initialize(owner, association, options)
@@ -24,7 +25,7 @@ module Draper
24
25
 
25
26
  private
26
27
 
27
- attr_reader :owner, :association, :decorator_class, :scope
28
+ attr_reader :owner, :association, :scope
28
29
 
29
30
  def source
30
31
  owner.source
@@ -48,12 +49,7 @@ module Draper
48
49
 
49
50
  def decorator
50
51
  return collection_decorator if collection?
51
-
52
- if decorator_class
53
- decorator_class.method(:decorate)
54
- else
55
- ->(item, options) { item.decorate(options) }
56
- end
52
+ decorator_class.method(:decorate)
57
53
  end
58
54
 
59
55
  def collection_decorator
@@ -66,5 +62,13 @@ module Draper
66
62
  end
67
63
  end
68
64
 
65
+ def decorator_class
66
+ @decorator_class || inferred_decorator_class
67
+ end
68
+
69
+ def inferred_decorator_class
70
+ undecorated.decorator_class if undecorated.respond_to?(:decorator_class)
71
+ end
72
+
69
73
  end
70
74
  end
@@ -3,83 +3,102 @@ require 'active_support/core_ext/array/extract_options'
3
3
  module Draper
4
4
  class Decorator
5
5
  include Draper::ViewHelpers
6
+ extend Draper::Delegation
6
7
  include ActiveModel::Serialization if defined?(ActiveModel::Serialization)
7
8
 
9
+ # @return the object being decorated.
8
10
  attr_reader :source
9
11
  alias_method :model, :source
10
12
  alias_method :to_source, :source
11
13
 
14
+ # @return [Hash] extra data to be used in user-defined methods.
12
15
  attr_accessor :context
13
16
 
14
- # Initialize a new decorator instance by passing in
15
- # an instance of the source class. Pass in an optional
16
- # :context inside the options hash which is available
17
- # for later use.
17
+ # Wraps an object in a new instance of the decorator.
18
18
  #
19
- # A decorator cannot be applied to other instances of the
20
- # same decorator and will instead result in a decorator
21
- # with the same target as the original.
22
- # You can, however, apply several decorators in a chain but
23
- # you will get a warning if the same decorator appears at
24
- # multiple places in the chain.
19
+ # Decorators may be applied to other decorators. However, applying a
20
+ # decorator to an instance of itself will create a decorator with the same
21
+ # source as the original, rather than redecorating the other instance.
25
22
  #
26
- # @param [Object] source object to decorate
27
- # @option options [Hash] :context context available to the decorator
23
+ # @param [Object] source
24
+ # object to decorate.
25
+ # @option options [Hash] :context ({})
26
+ # extra data to be stored in the decorator and used in user-defined
27
+ # methods.
28
28
  def initialize(source, options = {})
29
29
  options.assert_valid_keys(:context)
30
30
  source.to_a if source.respond_to?(:to_a) # forces evaluation of a lazy query from AR
31
31
  @source = source
32
32
  @context = options.fetch(:context, {})
33
- handle_multiple_decoration(options) if source.is_a?(Draper::Decorator)
33
+ handle_multiple_decoration(options) if source.instance_of?(self.class)
34
34
  end
35
35
 
36
36
  class << self
37
37
  alias_method :decorate, :new
38
38
  end
39
39
 
40
- # Specify the class that this class decorates.
40
+ # Automatically delegates instance methods to the source object. Class
41
+ # methods will be delegated to the {source_class}, if it is set.
41
42
  #
42
- # @param [String, Symbol, Class] Class or name of class to decorate.
43
- def self.decorates(klass)
44
- @source_class = klass.to_s.camelize.constantize
43
+ # @return [void]
44
+ def self.delegate_all
45
+ include Draper::AutomaticDelegation
45
46
  end
46
47
 
47
- # @return [Class] The source class corresponding to this
48
- # decorator class
48
+ # Sets the source class corresponding to the decorator class.
49
+ #
50
+ # @note This is only necessary if you wish to proxy class methods to the
51
+ # source (including when using {decorates_finders}), and the source class
52
+ # cannot be inferred from the decorator class (e.g. `ProductDecorator`
53
+ # maps to `Product`).
54
+ # @param [String, Symbol, Class] source_class
55
+ # source class (or class name) that corresponds to this decorator.
56
+ # @return [void]
57
+ def self.decorates(source_class)
58
+ @source_class = source_class.to_s.camelize.constantize
59
+ end
60
+
61
+ # Returns the source class corresponding to the decorator class, as set by
62
+ # {decorates}, or as inferred from the decorator class name (e.g.
63
+ # `ProductDecorator` maps to `Product`).
64
+ #
65
+ # @return [Class] the source class that corresponds to this decorator.
49
66
  def self.source_class
50
67
  @source_class ||= inferred_source_class
51
68
  end
52
69
 
53
- # Checks whether this decorator class has a corresponding
54
- # source class
70
+ # Checks whether this decorator class has a corresponding {source_class}.
55
71
  def self.source_class?
56
72
  source_class
57
73
  rescue Draper::UninferrableSourceError
58
74
  false
59
75
  end
60
76
 
61
- # Automatically decorates ActiveRecord finder methods, so that
62
- # you can use `ProductDecorator.find(id)` instead of
77
+ # Automatically decorates ActiveRecord finder methods, so that you can use
78
+ # `ProductDecorator.find(id)` instead of
63
79
  # `ProductDecorator.decorate(Product.find(id))`.
64
80
  #
65
- # The model class to be found is defined by `decorates` or
66
- # inferred from the decorator class name.
81
+ # Finder methods are applied to the {source_class}.
67
82
  #
83
+ # @return [void]
68
84
  def self.decorates_finders
69
85
  extend Draper::Finders
70
86
  end
71
87
 
72
- # Typically called within a decorator definition, this method causes
73
- # the assocation to be decorated when it is retrieved.
74
- #
75
- # @param [Symbol] association name of association to decorate, like `:products`
76
- # @option options [Class] :with the decorator to apply to the association
77
- # @option options [Symbol] :scope a scope to apply when fetching the association
78
- # @option options [Hash, #call] :context context available to decorated
79
- # objects in collection. Passing a `lambda` or similar will result in that
80
- # block being called when the association is evaluated. The block will be
81
- # passed the base decorator's `context` Hash and should return the desired
82
- # context Hash for the decorated items.
88
+ # Automatically decorate an association.
89
+ #
90
+ # @param [Symbol] association
91
+ # name of the association to decorate (e.g. `:products`).
92
+ # @option options [Class] :with
93
+ # the decorator to apply to the association.
94
+ # @option options [Symbol] :scope
95
+ # a scope to apply when fetching the association.
96
+ # @option options [Hash, #call] :context
97
+ # extra data to be stored in the associated decorator. If omitted, the
98
+ # associated decorator's context will be the same as the parent
99
+ # decorator's. If a Proc is given, it will be called with the parent's
100
+ # context and should return a new context hash for the association.
101
+ # @return [void]
83
102
  def self.decorates_association(association, options = {})
84
103
  options.assert_valid_keys(:with, :scope, :context)
85
104
  define_method(association) do
@@ -88,11 +107,13 @@ module Draper
88
107
  end
89
108
  end
90
109
 
91
- # A convenience method for decorating multiple associations. Calls
92
- # decorates_association on each of the given symbols.
93
- #
94
- # @param [Symbols*] associations names of associations to decorate
95
- # @param [Hash] options passed to `decorate_association`
110
+ # @overload decorates_associations(*associations, options = {})
111
+ # Automatically decorate multiple associations.
112
+ # @param [Symbols*] associations
113
+ # names of the associations to decorate.
114
+ # @param [Hash] options
115
+ # see {decorates_association}.
116
+ # @return [void]
96
117
  def self.decorates_associations(*associations)
97
118
  options = associations.extract_options!
98
119
  associations.each do |association|
@@ -100,167 +121,112 @@ module Draper
100
121
  end
101
122
  end
102
123
 
103
- # Specifies a black list of methods which may *not* be proxied to
104
- # the wrapped object.
105
- #
106
- # Do not use both `.allows` and `.denies` together, either write
107
- # a whitelist with `.allows` or a blacklist with `.denies`
108
- #
109
- # @param [Symbols*] methods methods to deny like `:find, :find_by_name`
110
- def self.denies(*methods)
111
- security.denies(*methods)
112
- end
113
-
114
- # Specifies that all methods may *not* be proxied to the wrapped object.
115
- #
116
- # Do not use `.allows` and `.denies` in combination with '.denies_all'
117
- def self.denies_all
118
- security.denies_all
119
- end
120
-
121
- # Specifies a white list of methods which *may* be proxied to
122
- # the wrapped object. When `allows` is used, only the listed
123
- # methods and methods defined in the decorator itself will be
124
- # available.
125
- #
126
- # Do not use both `.allows` and `.denies` together, either write
127
- # a whitelist with `.allows` or a blacklist with `.denies`
128
- #
129
- # @param [Symbols*] methods methods to allow like `:find, :find_by_name`
130
- def self.allows(*methods)
131
- security.allows(*methods)
132
- end
133
-
134
- # Creates a new CollectionDecorator for the given collection.
124
+ # Decorates a collection of objects. The class of the collection decorator
125
+ # is inferred from the decorator class if possible (e.g. `ProductDecorator`
126
+ # maps to `ProductsDecorator`), but otherwise defaults to
127
+ # {Draper::CollectionDecorator}.
135
128
  #
136
- # @param [Object] source collection to decorate
137
- # @param [Hash] options passed to each item's decorator (except
138
- # for the keys listed below)
139
- # @option options [Class] :with (self) the class used to decorate
140
- # items
141
- # @option options [Hash] :context context available to decorated items
129
+ # @param [Object] source
130
+ # collection to decorate.
131
+ # @option options [Class, nil] :with (self)
132
+ # the decorator class used to decorate each item. When `nil`, it is
133
+ # inferred from each item.
134
+ # @option options [Hash] :context
135
+ # extra data to be stored in the collection decorator.
142
136
  def self.decorate_collection(source, options = {})
143
137
  options.assert_valid_keys(:with, :context)
144
- Draper::CollectionDecorator.new(source, options.reverse_merge(with: self))
138
+ collection_decorator_class.new(source, options.reverse_merge(with: self))
145
139
  end
146
140
 
147
- # Get the chain of decorators applied to the object.
148
- #
149
- # @return [Array] list of decorator classes
141
+ # @return [Array<Class>] the list of decorators that have been applied to
142
+ # the object.
150
143
  def applied_decorators
151
144
  chain = source.respond_to?(:applied_decorators) ? source.applied_decorators : []
152
145
  chain << self.class
153
146
  end
154
147
 
155
- # Checks if a given decorator has been applied.
148
+ # Checks if a given decorator has been applied to the object.
156
149
  #
157
150
  # @param [Class] decorator_class
158
151
  def decorated_with?(decorator_class)
159
152
  applied_decorators.include?(decorator_class)
160
153
  end
161
154
 
155
+ # Checks if this object is decorated.
156
+ #
157
+ # @return [true]
162
158
  def decorated?
163
159
  true
164
160
  end
165
161
 
166
- # Delegates == to the decorated models
162
+ # Delegated to the source object.
167
163
  #
168
- # @return [Boolean] true if other's model == self's model
164
+ # @return [Boolean]
169
165
  def ==(other)
170
166
  source == (other.respond_to?(:source) ? other.source : other)
171
167
  end
172
168
 
169
+ # Checks if `self.kind_of?(klass)` or `source.kind_of?(klass)`
170
+ #
171
+ # @param [Class] klass
173
172
  def kind_of?(klass)
174
173
  super || source.kind_of?(klass)
175
174
  end
176
175
  alias_method :is_a?, :kind_of?
177
176
 
178
- # We always want to delegate present, in case we decorate a nil object.
177
+ # Checks if `self.instance_of?(klass)` or `source.instance_of?(klass)`
179
178
  #
180
- # I don't like the idea of decorating a nil object, but we'll deal with
181
- # that later.
182
- def present?
183
- source.present?
179
+ # @param [Class] klass
180
+ def instance_of?(klass)
181
+ super || source.instance_of?(klass)
184
182
  end
185
183
 
186
- # For ActiveModel compatibilty
184
+ # In case source is nil
185
+ delegate :present?
186
+
187
+ # ActiveModel compatibility
188
+ # @private
187
189
  def to_model
188
190
  self
189
191
  end
190
192
 
191
- # For ActiveModel compatibility
192
- def to_param
193
- source.to_param
194
- end
193
+ # ActiveModel compatibility
194
+ delegate :to_param, :to_partial_path
195
195
 
196
- def method_missing(method, *args, &block)
197
- if delegatable_method?(method)
198
- self.class.define_proxy(method)
199
- send(method, *args, &block)
200
- else
201
- super
202
- end
203
- end
196
+ # ActiveModel compatibility
197
+ singleton_class.delegate :model_name, to: :source_class
204
198
 
205
- def respond_to?(method, include_private = false)
206
- super || delegatable_method?(method)
207
- end
208
-
209
- def self.method_missing(method, *args, &block)
210
- if delegatable_method?(method)
211
- source_class.send(method, *args, &block)
212
- else
213
- super
214
- end
215
- end
216
-
217
- def self.respond_to?(method, include_private = false)
218
- super || delegatable_method?(method)
199
+ # @return [Class] the class created by {decorate_collection}.
200
+ def self.collection_decorator_class
201
+ collection_decorator_name.constantize
202
+ rescue NameError
203
+ Draper::CollectionDecorator
219
204
  end
220
205
 
221
206
  private
222
207
 
223
- def delegatable_method?(method)
224
- allow?(method) && source.respond_to?(method)
225
- end
226
-
227
- def self.delegatable_method?(method)
228
- source_class? && source_class.respond_to?(method)
208
+ def self.source_name
209
+ raise NameError if name.nil? || name.demodulize !~ /.+Decorator$/
210
+ name.chomp("Decorator")
229
211
  end
230
212
 
231
213
  def self.inferred_source_class
232
- uninferrable_source if name.nil? || name.demodulize !~ /.+Decorator$/
233
-
234
- begin
235
- name.chomp("Decorator").constantize
236
- rescue NameError
237
- uninferrable_source
238
- end
239
- end
240
-
241
- def self.uninferrable_source
214
+ source_name.constantize
215
+ rescue NameError
242
216
  raise Draper::UninferrableSourceError.new(self)
243
217
  end
244
218
 
245
- def self.define_proxy(method)
246
- define_method(method) do |*args, &block|
247
- source.send(method, *args, &block)
248
- end
249
- end
250
-
251
- def self.security
252
- @security ||= Security.new
253
- end
254
-
255
- def allow?(method)
256
- self.class.security.allow?(method)
219
+ def self.collection_decorator_name
220
+ plural = source_name.pluralize
221
+ raise NameError if plural == source_name
222
+ "#{plural}Decorator"
257
223
  end
258
224
 
259
225
  def handle_multiple_decoration(options)
260
- if source.instance_of?(self.class)
226
+ if source.applied_decorators.last == self.class
261
227
  @context = source.context unless options.has_key?(:context)
262
228
  @source = source.source
263
- elsif source.decorated_with?(self.class)
229
+ else
264
230
  warn "Reapplying #{self.class} decorator to target that is already decorated with it. Call stack:\n#{caller(1).join("\n")}"
265
231
  end
266
232
  end
@@ -0,0 +1,13 @@
1
+ module Draper
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 `:source` the default delegation target.
6
+ #
7
+ # @return [void]
8
+ def delegate(*methods)
9
+ options = methods.extract_options!
10
+ super *methods, options.reverse_merge(to: :source)
11
+ end
12
+ end
13
+ end
@@ -1,4 +1,7 @@
1
1
  module Draper
2
+ # Provides automatically-decorated finder methods for your decorators. You
3
+ # do not have to extend this module directly; it is extended by
4
+ # {Decorator.decorates_finders}.
2
5
  module Finders
3
6
 
4
7
  def find(id, options = {})
@@ -17,17 +20,17 @@ module Draper
17
20
  decorate(source_class.last, options)
18
21
  end
19
22
 
23
+ # Decorates dynamic finder methods (`find_all_by_` and friends).
20
24
  def method_missing(method, *args, &block)
21
- result = super
25
+ return super unless method =~ /^find_(all_|last_|or_(initialize_|create_))?by_/
26
+
27
+ result = source_class.send(method, *args, &block)
22
28
  options = args.extract_options!
23
29
 
24
- case method.to_s
25
- when /^find_((last_)?by_|or_(initialize|create)_by_)/
26
- decorate(result, options)
27
- when /^find_all_by_/
30
+ if method =~ /^find_all/
28
31
  decorate_collection(result, options)
29
32
  else
30
- result
33
+ decorate(result, options)
31
34
  end
32
35
  end
33
36
  end