draper 1.1.0 → 1.2.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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +7 -0
  3. data/CHANGELOG.md +140 -120
  4. data/Gemfile +5 -3
  5. data/Guardfile +22 -1
  6. data/README.md +4 -3
  7. data/draper.gemspec +3 -1
  8. data/lib/draper.rb +6 -0
  9. data/lib/draper/automatic_delegation.rb +8 -2
  10. data/lib/draper/collection_decorator.rb +17 -28
  11. data/lib/draper/decoratable.rb +6 -3
  12. data/lib/draper/decoratable/equality.rb +15 -3
  13. data/lib/draper/decorated_association.rb +11 -50
  14. data/lib/draper/decorates_assigned.rb +44 -0
  15. data/lib/draper/decorator.rb +19 -6
  16. data/lib/draper/factory.rb +87 -0
  17. data/lib/draper/helper_proxy.rb +2 -0
  18. data/lib/draper/railtie.rb +8 -2
  19. data/lib/draper/version.rb +1 -1
  20. data/lib/generators/decorator/decorator_generator.rb +9 -9
  21. data/spec/draper/collection_decorator_spec.rb +48 -28
  22. data/spec/draper/decoratable_spec.rb +13 -4
  23. data/spec/draper/decorated_association_spec.rb +53 -114
  24. data/spec/draper/decorates_assigned_spec.rb +71 -0
  25. data/spec/draper/decorator_spec.rb +58 -8
  26. data/spec/draper/factory_spec.rb +238 -0
  27. data/spec/draper/helper_proxy_spec.rb +11 -0
  28. data/spec/draper/lazy_helpers_spec.rb +21 -0
  29. data/spec/dummy/app/controllers/posts_controller.rb +3 -1
  30. data/spec/dummy/app/decorators/mongoid_post_decorator.rb +2 -0
  31. data/spec/dummy/app/views/posts/show.html.erb +1 -1
  32. data/spec/dummy/config/application.rb +1 -0
  33. data/spec/dummy/spec/decorators/active_model_serializers_spec.rb +11 -0
  34. data/spec/dummy/spec/decorators/post_decorator_spec.rb +12 -0
  35. data/spec/dummy/spec/models/mongoid_post_spec.rb +2 -4
  36. data/spec/dummy/spec/models/post_spec.rb +2 -10
  37. data/spec/dummy/spec/shared_examples/decoratable.rb +24 -0
  38. data/spec/generators/decorator/decorator_generator_spec.rb +83 -91
  39. data/spec/spec_helper.rb +1 -0
  40. metadata +50 -43
@@ -12,7 +12,7 @@ module Draper
12
12
 
13
13
  # Checks if the decorator responds to an instance method, or is able to
14
14
  # proxy it to the source object.
15
- def respond_to?(method, include_private = false)
15
+ def respond_to_missing?(method, include_private = false)
16
16
  super || delegatable?(method)
17
17
  end
18
18
 
@@ -31,7 +31,7 @@ module Draper
31
31
 
32
32
  # Checks if the decorator responds to a class method, or is able to proxy
33
33
  # it to the source class.
34
- def respond_to?(method, include_private = false)
34
+ def respond_to_missing?(method, include_private = false)
35
35
  super || delegatable?(method)
36
36
  end
37
37
 
@@ -39,6 +39,12 @@ module Draper
39
39
  def delegatable?(method)
40
40
  source_class? && source_class.respond_to?(method)
41
41
  end
42
+
43
+ # @private
44
+ # Avoids reloading the model class when ActiveSupport clears autoloaded
45
+ # dependencies in development mode.
46
+ def before_remove_const
47
+ end
42
48
  end
43
49
 
44
50
  included do
@@ -4,6 +4,10 @@ module Draper
4
4
  include Draper::ViewHelpers
5
5
  extend Draper::Delegation
6
6
 
7
+ # @return [Class] the decorator class used to decorate each item, as set by
8
+ # {#initialize}.
9
+ attr_reader :decorator_class
10
+
7
11
  # @return [Hash] extra data to be used in user-defined methods, and passed
8
12
  # to each item's decorator.
9
13
  attr_accessor :context
@@ -49,13 +53,7 @@ module Draper
49
53
  end
50
54
 
51
55
  def to_s
52
- klass = begin
53
- decorator_class
54
- rescue Draper::UninferrableDecoratorError
55
- "inferred decorators"
56
- end
57
-
58
- "#<#{self.class.name} of #{klass} for #{source.inspect}>"
56
+ "#<#{self.class.name} of #{decorator_class || "inferred decorators"} for #{source.inspect}>"
59
57
  end
60
58
 
61
59
  def context=(value)
@@ -63,12 +61,17 @@ module Draper
63
61
  each {|item| item.context = value } if @decorated_collection
64
62
  end
65
63
 
66
- # @return [Class] the decorator class used to decorate each item, as set by
67
- # {#initialize} or as inferred from the collection decorator class (e.g.
68
- # `ProductsDecorator` maps to `ProductDecorator`).
69
- def decorator_class
70
- @decorator_class ||= self.class.inferred_decorator_class
64
+ # @return [true]
65
+ def decorated?
66
+ true
67
+ end
68
+
69
+ alias_method :decorated_with?, :instance_of?
70
+
71
+ def kind_of?(klass)
72
+ decorated_collection.kind_of?(klass) || super
71
73
  end
74
+ alias_method :is_a?, :kind_of?
72
75
 
73
76
  protected
74
77
 
@@ -82,24 +85,10 @@ module Draper
82
85
 
83
86
  private
84
87
 
85
- def self.inferred_decorator_class
86
- decorator_name = "#{name.chomp("Decorator").singularize}Decorator"
87
- decorator_uninferrable if decorator_name == name
88
-
89
- decorator_name.constantize
90
-
91
- rescue NameError
92
- decorator_uninferrable
93
- end
94
-
95
- def self.decorator_uninferrable
96
- raise Draper::UninferrableDecoratorError.new(self)
97
- end
98
-
99
88
  def item_decorator
100
- @item_decorator ||= begin
89
+ if decorator_class
101
90
  decorator_class.method(:decorate)
102
- rescue Draper::UninferrableDecoratorError
91
+ else
103
92
  ->(item, options) { item.decorate(options) }
104
93
  end
105
94
  end
@@ -52,7 +52,8 @@ module Draper
52
52
  # @param [Hash] options
53
53
  # see {Decorator.decorate_collection}.
54
54
  def decorate(options = {})
55
- decorator_class.decorate_collection(self.scoped, options)
55
+ collection = Rails::VERSION::MAJOR >= 4 ? all : scoped
56
+ decorator_class.decorate_collection(collection, options.reverse_merge(with: nil))
56
57
  end
57
58
 
58
59
  # Infers the decorator class to be used by {Decoratable#decorate} (e.g.
@@ -61,8 +62,10 @@ module Draper
61
62
  # @return [Class] the inferred decorator class.
62
63
  def decorator_class
63
64
  prefix = respond_to?(:model_name) ? model_name : name
64
- "#{prefix}Decorator".constantize
65
- rescue NameError
65
+ decorator_name = "#{prefix}Decorator"
66
+ decorator_name.constantize
67
+ rescue NameError => error
68
+ raise unless error.missing_name?(decorator_name)
66
69
  raise Draper::UninferrableDecoratorError.new(self)
67
70
  end
68
71
 
@@ -5,9 +5,21 @@ module Draper
5
5
  #
6
6
  # @return [Boolean]
7
7
  def ==(other)
8
- super ||
9
- other.respond_to?(:decorated?) && other.decorated? &&
10
- other.respond_to?(:source) && self == other.source
8
+ super || Equality.test_for_decorator(self, other)
9
+ end
10
+
11
+ # Compares an object to a possibly-decorated object.
12
+ #
13
+ # @return [Boolean]
14
+ def self.test(object, other)
15
+ return object == other if object.is_a?(Decoratable)
16
+ object == other || test_for_decorator(object, other)
17
+ end
18
+
19
+ # @private
20
+ def self.test_for_decorator(object, other)
21
+ other.respond_to?(:decorated?) && other.decorated? &&
22
+ other.respond_to?(:source) && test(object, other.source)
11
23
  end
12
24
  end
13
25
  end
@@ -8,66 +8,27 @@ module Draper
8
8
  @owner = owner
9
9
  @association = association
10
10
 
11
- @decorator_class = options[:with]
12
11
  @scope = options[:scope]
13
- @context = options.fetch(:context, owner.context)
14
- end
15
12
 
16
- def call
17
- return undecorated if undecorated.nil?
18
- decorated
13
+ decorator_class = options[:with]
14
+ context = options.fetch(:context, ->(context){ context })
15
+ @factory = Draper::Factory.new(with: decorator_class, context: context)
19
16
  end
20
17
 
21
- def context
22
- return @context.call(owner.context) if @context.respond_to?(:call)
23
- @context
18
+ def call
19
+ decorate unless defined?(@decorated)
20
+ @decorated
24
21
  end
25
22
 
26
23
  private
27
24
 
28
- attr_reader :owner, :association, :scope
29
-
30
- def source
31
- owner.source
32
- end
33
-
34
- def undecorated
35
- @undecorated ||= begin
36
- associated = source.send(association)
37
- associated = associated.send(scope) if scope
38
- associated
39
- end
40
- end
41
-
42
- def decorated
43
- @decorated ||= decorator.call(undecorated, context: context)
44
- end
25
+ attr_reader :factory, :owner, :association, :scope
45
26
 
46
- def collection?
47
- undecorated.respond_to?(:first)
48
- end
49
-
50
- def decorator
51
- return collection_decorator if collection?
52
- decorator_class.method(:decorate)
53
- end
54
-
55
- def collection_decorator
56
- klass = decorator_class || Draper::CollectionDecorator
57
-
58
- if klass.respond_to?(:decorate_collection)
59
- klass.method(:decorate_collection)
60
- else
61
- klass.method(:decorate)
62
- end
63
- end
64
-
65
- def decorator_class
66
- @decorator_class || inferred_decorator_class
67
- end
27
+ def decorate
28
+ associated = owner.source.send(association)
29
+ associated = associated.send(scope) if scope
68
30
 
69
- def inferred_decorator_class
70
- undecorated.decorator_class if undecorated.respond_to?(:decorator_class)
31
+ @decorated = factory.decorate(associated, context_args: owner.context)
71
32
  end
72
33
 
73
34
  end
@@ -0,0 +1,44 @@
1
+ module Draper
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 = Draper::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
@@ -2,7 +2,10 @@ module Draper
2
2
  class Decorator
3
3
  include Draper::ViewHelpers
4
4
  extend Draper::Delegation
5
+
5
6
  include ActiveModel::Serialization
7
+ include ActiveModel::Serializers::JSON
8
+ include ActiveModel::Serializers::Xml
6
9
 
7
10
  # @return the object being decorated.
8
11
  attr_reader :source
@@ -160,7 +163,7 @@ module Draper
160
163
  #
161
164
  # @return [Boolean]
162
165
  def ==(other)
163
- source.extend(Draper::Decoratable::Equality) == other
166
+ Draper::Decoratable::Equality.test(source, other)
164
167
  end
165
168
 
166
169
  # Checks if `self.kind_of?(klass)` or `source.kind_of?(klass)`
@@ -187,16 +190,24 @@ module Draper
187
190
  self
188
191
  end
189
192
 
193
+ # @return [Hash] the source's attributes, sliced to only include those
194
+ # implemented by the decorator.
195
+ def attributes
196
+ source.attributes.select {|attribute, _| respond_to?(attribute) }
197
+ end
198
+
190
199
  # ActiveModel compatibility
191
- delegate :attributes, :to_param, :to_partial_path
200
+ delegate :to_param, :to_partial_path
192
201
 
193
202
  # ActiveModel compatibility
194
203
  singleton_class.delegate :model_name, to: :source_class
195
204
 
196
205
  # @return [Class] the class created by {decorate_collection}.
197
206
  def self.collection_decorator_class
198
- collection_decorator_name.constantize
199
- rescue NameError
207
+ name = collection_decorator_name
208
+ name.constantize
209
+ rescue NameError => error
210
+ raise if name && !error.missing_name?(name)
200
211
  Draper::CollectionDecorator
201
212
  end
202
213
 
@@ -208,8 +219,10 @@ module Draper
208
219
  end
209
220
 
210
221
  def self.inferred_source_class
211
- source_name.constantize
212
- rescue NameError
222
+ name = source_name
223
+ name.constantize
224
+ rescue NameError => error
225
+ raise if name && !error.missing_name?(name)
213
226
  raise Draper::UninferrableSourceError.new(self)
214
227
  end
215
228
 
@@ -0,0 +1,87 @@
1
+ module Draper
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] source
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(source, options = {})
30
+ return nil if source.nil?
31
+ Worker.new(decorator_class, source).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, source)
41
+ @decorator_class = decorator_class
42
+ @source = source
43
+ end
44
+
45
+ def call(options)
46
+ update_context options
47
+ decorator.call(source, options)
48
+ end
49
+
50
+ def decorator
51
+ return decorator_method(decorator_class) if decorator_class
52
+ return source_decorator if decoratable?
53
+ return decorator_method(Draper::CollectionDecorator) if collection?
54
+ raise Draper::UninferrableDecoratorError.new(source.class)
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :decorator_class, :source
60
+
61
+ def source_decorator
62
+ ->(source, options) { source.decorate(options) }
63
+ end
64
+
65
+ def decorator_method(klass)
66
+ if collection? && klass.respond_to?(:decorate_collection)
67
+ klass.method(:decorate_collection)
68
+ else
69
+ klass.method(:decorate)
70
+ end
71
+ end
72
+
73
+ def collection?
74
+ source.respond_to?(:first)
75
+ end
76
+
77
+ def decoratable?
78
+ source.respond_to?(:decorate)
79
+ end
80
+
81
+ def update_context(options)
82
+ args = options.delete(:context_args)
83
+ options[:context] = options[:context].call(*Array.wrap(args)) if options[:context].respond_to?(:call)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -16,6 +16,8 @@ module Draper
16
16
  send(method, *args, &block)
17
17
  end
18
18
 
19
+ delegate :capture, to: :view_context
20
+
19
21
  protected
20
22
 
21
23
  attr_reader :view_context
@@ -17,7 +17,7 @@ module Draper
17
17
  config.after_initialize do |app|
18
18
  app.config.paths.add 'app/decorators', eager_load: true
19
19
 
20
- unless Rails.env.production?
20
+ if Rails.env.test?
21
21
  require 'draper/test_case'
22
22
  require 'draper/test/rspec_integration' if defined?(RSpec) and RSpec.respond_to?(:configure)
23
23
  require 'draper/test/minitest_integration' if defined?(MiniTest::Rails)
@@ -44,10 +44,16 @@ module Draper
44
44
  end
45
45
  end
46
46
 
47
+ initializer "draper.setup_active_model_serializers" do |app|
48
+ ActiveSupport.on_load :active_model_serializers do
49
+ Draper::CollectionDecorator.send :include, ActiveModel::ArraySerializerSupport
50
+ end
51
+ end
52
+
47
53
  console do
48
54
  require 'action_controller/test_case'
49
55
  ApplicationController.new.view_context
50
- Draper::ViewContext.build_view_context
56
+ Draper::ViewContext.build
51
57
  end
52
58
 
53
59
  rake_tasks do