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