draper 1.0.0.beta6 → 1.0.0

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 (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