draper 1.0.0.beta6 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +6 -0
- data/.yardopts +1 -1
- data/CHANGELOG.md +20 -0
- data/Gemfile +11 -0
- data/README.md +14 -17
- data/Rakefile +5 -3
- data/draper.gemspec +2 -2
- data/lib/draper.rb +2 -1
- data/lib/draper/automatic_delegation.rb +50 -0
- data/lib/draper/collection_decorator.rb +26 -7
- data/lib/draper/decoratable.rb +71 -32
- data/lib/draper/decorated_association.rb +11 -7
- data/lib/draper/decorator.rb +114 -148
- data/lib/draper/delegation.rb +13 -0
- data/lib/draper/finders.rb +9 -6
- data/lib/draper/helper_proxy.rb +4 -3
- data/lib/draper/lazy_helpers.rb +10 -6
- data/lib/draper/railtie.rb +5 -4
- data/lib/draper/tasks/test.rake +22 -0
- data/lib/draper/test/devise_helper.rb +34 -0
- data/lib/draper/test/minitest_integration.rb +2 -3
- data/lib/draper/test/rspec_integration.rb +4 -59
- data/lib/draper/test_case.rb +33 -0
- data/lib/draper/version.rb +1 -1
- data/lib/draper/view_helpers.rb +4 -3
- data/lib/generators/decorator/templates/decorator.rb +7 -25
- data/lib/generators/mini_test/decorator_generator.rb +20 -0
- data/lib/generators/mini_test/templates/decorator_spec.rb +4 -0
- data/lib/generators/mini_test/templates/decorator_test.rb +4 -0
- data/lib/generators/test_unit/templates/decorator_test.rb +1 -1
- data/spec/draper/collection_decorator_spec.rb +25 -3
- data/spec/draper/decorated_association_spec.rb +18 -7
- data/spec/draper/decorator_spec.rb +125 -165
- data/spec/draper/finders_spec.rb +0 -13
- data/spec/dummy/app/controllers/localized_urls.rb +1 -1
- data/spec/dummy/app/controllers/posts_controller.rb +3 -9
- data/spec/dummy/app/decorators/post_decorator.rb +4 -1
- data/spec/dummy/config/application.rb +3 -3
- data/spec/dummy/config/environments/development.rb +4 -4
- data/spec/dummy/config/environments/test.rb +2 -2
- data/spec/dummy/lib/tasks/test.rake +10 -0
- data/spec/dummy/mini_test/mini_test_integration_test.rb +46 -0
- data/spec/dummy/spec/decorators/post_decorator_spec.rb +2 -2
- data/spec/dummy/spec/decorators/rspec_integration_spec.rb +19 -0
- data/spec/dummy/spec/mailers/post_mailer_spec.rb +2 -2
- data/spec/dummy/spec/spec_helper.rb +0 -1
- data/spec/generators/decorator/decorator_generator_spec.rb +43 -2
- data/spec/integration/integration_spec.rb +2 -2
- data/spec/spec_helper.rb +17 -21
- data/spec/support/active_record.rb +0 -13
- data/spec/support/dummy_app.rb +4 -3
- metadata +26 -23
- data/lib/draper/security.rb +0 -48
- data/lib/draper/tasks/tu.rake +0 -5
- data/lib/draper/test/test_unit_integration.rb +0 -18
- data/spec/draper/security_spec.rb +0 -158
- data/spec/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/spec/dummy/lib/tasks/spec.rake +0 -5
- 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, :
|
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
|
data/lib/draper/decorator.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
#
|
20
|
-
#
|
21
|
-
#
|
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
|
27
|
-
#
|
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.
|
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
|
-
#
|
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
|
-
# @
|
43
|
-
def self.
|
44
|
-
|
43
|
+
# @return [void]
|
44
|
+
def self.delegate_all
|
45
|
+
include Draper::AutomaticDelegation
|
45
46
|
end
|
46
47
|
|
47
|
-
#
|
48
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
# @option options [Class] :with
|
77
|
-
#
|
78
|
-
# @option options [
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
# context
|
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
|
-
#
|
92
|
-
#
|
93
|
-
#
|
94
|
-
#
|
95
|
-
#
|
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
|
-
#
|
104
|
-
# the
|
105
|
-
#
|
106
|
-
#
|
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
|
137
|
-
#
|
138
|
-
#
|
139
|
-
#
|
140
|
-
#
|
141
|
-
# @option options [Hash] :context
|
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
|
-
|
138
|
+
collection_decorator_class.new(source, options.reverse_merge(with: self))
|
145
139
|
end
|
146
140
|
|
147
|
-
#
|
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
|
-
#
|
162
|
+
# Delegated to the source object.
|
167
163
|
#
|
168
|
-
# @return [Boolean]
|
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
|
-
#
|
177
|
+
# Checks if `self.instance_of?(klass)` or `source.instance_of?(klass)`
|
179
178
|
#
|
180
|
-
#
|
181
|
-
|
182
|
-
|
183
|
-
source.present?
|
179
|
+
# @param [Class] klass
|
180
|
+
def instance_of?(klass)
|
181
|
+
super || source.instance_of?(klass)
|
184
182
|
end
|
185
183
|
|
186
|
-
#
|
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
|
-
#
|
192
|
-
|
193
|
-
source.to_param
|
194
|
-
end
|
193
|
+
# ActiveModel compatibility
|
194
|
+
delegate :to_param, :to_partial_path
|
195
195
|
|
196
|
-
|
197
|
-
|
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
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
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
|
224
|
-
|
225
|
-
|
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
|
-
|
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.
|
246
|
-
|
247
|
-
|
248
|
-
|
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.
|
226
|
+
if source.applied_decorators.last == self.class
|
261
227
|
@context = source.context unless options.has_key?(:context)
|
262
228
|
@source = source.source
|
263
|
-
|
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
|
data/lib/draper/finders.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|