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.
- checksums.yaml +7 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +140 -120
- data/Gemfile +5 -3
- data/Guardfile +22 -1
- data/README.md +4 -3
- data/draper.gemspec +3 -1
- data/lib/draper.rb +6 -0
- data/lib/draper/automatic_delegation.rb +8 -2
- data/lib/draper/collection_decorator.rb +17 -28
- data/lib/draper/decoratable.rb +6 -3
- data/lib/draper/decoratable/equality.rb +15 -3
- data/lib/draper/decorated_association.rb +11 -50
- data/lib/draper/decorates_assigned.rb +44 -0
- data/lib/draper/decorator.rb +19 -6
- data/lib/draper/factory.rb +87 -0
- data/lib/draper/helper_proxy.rb +2 -0
- data/lib/draper/railtie.rb +8 -2
- data/lib/draper/version.rb +1 -1
- data/lib/generators/decorator/decorator_generator.rb +9 -9
- data/spec/draper/collection_decorator_spec.rb +48 -28
- data/spec/draper/decoratable_spec.rb +13 -4
- data/spec/draper/decorated_association_spec.rb +53 -114
- data/spec/draper/decorates_assigned_spec.rb +71 -0
- data/spec/draper/decorator_spec.rb +58 -8
- data/spec/draper/factory_spec.rb +238 -0
- data/spec/draper/helper_proxy_spec.rb +11 -0
- data/spec/draper/lazy_helpers_spec.rb +21 -0
- data/spec/dummy/app/controllers/posts_controller.rb +3 -1
- data/spec/dummy/app/decorators/mongoid_post_decorator.rb +2 -0
- data/spec/dummy/app/views/posts/show.html.erb +1 -1
- data/spec/dummy/config/application.rb +1 -0
- data/spec/dummy/spec/decorators/active_model_serializers_spec.rb +11 -0
- data/spec/dummy/spec/decorators/post_decorator_spec.rb +12 -0
- data/spec/dummy/spec/models/mongoid_post_spec.rb +2 -4
- data/spec/dummy/spec/models/post_spec.rb +2 -10
- data/spec/dummy/spec/shared_examples/decoratable.rb +24 -0
- data/spec/generators/decorator/decorator_generator_spec.rb +83 -91
- data/spec/spec_helper.rb +1 -0
- 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
|
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
|
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
|
-
|
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 [
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
89
|
+
if decorator_class
|
101
90
|
decorator_class.method(:decorate)
|
102
|
-
|
91
|
+
else
|
103
92
|
->(item, options) { item.decorate(options) }
|
104
93
|
end
|
105
94
|
end
|
data/lib/draper/decoratable.rb
CHANGED
@@ -52,7 +52,8 @@ module Draper
|
|
52
52
|
# @param [Hash] options
|
53
53
|
# see {Decorator.decorate_collection}.
|
54
54
|
def decorate(options = {})
|
55
|
-
|
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"
|
65
|
-
|
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
|
-
|
10
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
22
|
-
|
23
|
-
@
|
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
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
data/lib/draper/decorator.rb
CHANGED
@@ -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
|
-
|
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 :
|
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
|
199
|
-
|
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
|
212
|
-
|
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
|
data/lib/draper/helper_proxy.rb
CHANGED
data/lib/draper/railtie.rb
CHANGED
@@ -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
|
-
|
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.
|
56
|
+
Draper::ViewContext.build
|
51
57
|
end
|
52
58
|
|
53
59
|
rake_tasks do
|