draper 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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