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
@@ -1,3 +1,3 @@
1
1
  module Draper
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -2,9 +2,9 @@ module Rails
2
2
  module Generators
3
3
  class DecoratorGenerator < NamedBase
4
4
  source_root File.expand_path("../templates", __FILE__)
5
- check_class_collision :suffix => "Decorator"
5
+ check_class_collision suffix: "Decorator"
6
6
 
7
- class_option :parent, :type => :string, :desc => "The parent class for the generated decorator"
7
+ class_option :parent, type: :string, desc: "The parent class for the generated decorator"
8
8
 
9
9
  def create_decorator_file
10
10
  template 'decorator.rb', File.join('app/decorators', class_path, "#{file_name}_decorator.rb")
@@ -15,16 +15,16 @@ module Rails
15
15
  private
16
16
 
17
17
  def parent_class_name
18
- if options[:parent]
19
- options[:parent]
20
- elsif defined?(ApplicationDecorator)
21
- "ApplicationDecorator"
22
- else
23
- "Draper::Decorator"
18
+ options.fetch("parent") do
19
+ begin
20
+ require 'application_decorator'
21
+ ApplicationDecorator
22
+ rescue LoadError
23
+ "Draper::Decorator"
24
+ end
24
25
  end
25
26
  end
26
27
 
27
-
28
28
  # Rails 3.0.X compatibility, stolen from https://github.com/jnunemaker/mongomapper/pull/385/files#L1R32
29
29
  unless methods.include?(:module_namespacing)
30
30
  def module_namespacing(&block)
@@ -89,39 +89,19 @@ module Draper
89
89
  end
90
90
  end
91
91
 
92
- context "when the item decorator is inferrable from the collection decorator" do
93
- context "when the :with option was given" do
94
- it "uses the :with option" do
95
- decorator = ProductsDecorator.new([Product.new], with: OtherDecorator)
92
+ context "when the :with option was given" do
93
+ it "uses the :with option" do
94
+ decorator = CollectionDecorator.new([Product.new], with: OtherDecorator).first
96
95
 
97
- expect(*decorator).to be_decorated_with OtherDecorator
98
- end
99
- end
100
-
101
- context "when the :with option was not given" do
102
- it "infers the item decorator from the collection decorator" do
103
- decorator = ProductsDecorator.new([Product.new])
104
-
105
- expect(*decorator).to be_decorated_with ProductDecorator
106
- end
96
+ expect(decorator).to be_decorated_with OtherDecorator
107
97
  end
108
98
  end
109
99
 
110
- context "when the item decorator is not inferrable from the collection decorator" do
111
- context "when the :with option was given" do
112
- it "uses the :with option" do
113
- decorator = CollectionDecorator.new([Product.new], with: OtherDecorator)
114
-
115
- expect(*decorator).to be_decorated_with OtherDecorator
116
- end
117
- end
118
-
119
- context "when the :with option was not given" do
120
- it "infers the item decorator from each item" do
121
- decorator = CollectionDecorator.new([double(decorate: :inferred_decorator)])
100
+ context "when the :with option was not given" do
101
+ it "infers the item decorator from each item" do
102
+ decorator = CollectionDecorator.new([double(decorate: :inferred_decorator)]).first
122
103
 
123
- expect(*decorator).to be :inferred_decorator
124
- end
104
+ expect(decorator).to be :inferred_decorator
125
105
  end
126
106
  end
127
107
  end
@@ -255,5 +235,45 @@ module Draper
255
235
  end
256
236
  end
257
237
 
238
+ describe '#decorated?' do
239
+ it 'returns true' do
240
+ decorator = ProductsDecorator.new([Product.new])
241
+
242
+ expect(decorator).to be_decorated
243
+ end
244
+ end
245
+
246
+ describe '#decorated_with?' do
247
+ it "checks if a decorator has been applied to a collection" do
248
+ decorator = ProductsDecorator.new([Product.new])
249
+
250
+ expect(decorator).to be_decorated_with ProductsDecorator
251
+ expect(decorator).not_to be_decorated_with OtherDecorator
252
+ end
253
+ end
254
+
255
+ describe '#kind_of?' do
256
+ it 'asks the kind of its decorated collection' do
257
+ decorator = ProductsDecorator.new([])
258
+ decorator.decorated_collection.should_receive(:kind_of?).with(Array).and_return("true")
259
+ expect(decorator.kind_of?(Array)).to eq "true"
260
+ end
261
+
262
+ context 'when asking the underlying collection returns false' do
263
+ it 'asks the CollectionDecorator instance itself' do
264
+ decorator = ProductsDecorator.new([])
265
+ decorator.decorated_collection.stub(:kind_of?).with(::Draper::CollectionDecorator).and_return(false)
266
+ expect(decorator.kind_of?(::Draper::CollectionDecorator)).to be true
267
+ end
268
+ end
269
+ end
270
+
271
+ describe '#is_a?' do
272
+ it 'aliases to #kind_of?' do
273
+ decorator = ProductsDecorator.new([])
274
+ expect(decorator.method(:kind_of?)).to eq decorator.method(:is_a?)
275
+ end
276
+ end
277
+
258
278
  end
259
279
  end
@@ -108,17 +108,19 @@ module Draper
108
108
  end
109
109
 
110
110
  describe ".decorate" do
111
+ let(:scoping_method) { Rails::VERSION::MAJOR >= 4 ? :all : :scoped }
112
+
111
113
  it "calls #decorate_collection on .decorator_class" do
112
114
  scoped = [Product.new]
113
- Product.stub scoped: scoped
115
+ Product.stub scoping_method => scoped
114
116
 
115
- Product.decorator_class.should_receive(:decorate_collection).with(scoped, {}).and_return(:decorated_collection)
117
+ Product.decorator_class.should_receive(:decorate_collection).with(scoped, with: nil).and_return(:decorated_collection)
116
118
  expect(Product.decorate).to be :decorated_collection
117
119
  end
118
120
 
119
121
  it "accepts options" do
120
- options = {context: {some: "context"}}
121
- Product.stub scoped: []
122
+ options = {with: ProductDecorator, context: {some: "context"}}
123
+ Product.stub scoping_method => []
122
124
 
123
125
  Product.decorator_class.should_receive(:decorate_collection).with([], options)
124
126
  Product.decorate(options)
@@ -161,6 +163,13 @@ module Draper
161
163
  expect{Model.decorator_class}.to raise_error UninferrableDecoratorError
162
164
  end
163
165
  end
166
+
167
+ context "when an unrelated NameError is thrown" do
168
+ it "re-raises that error" do
169
+ String.any_instance.stub(:constantize).and_return{Draper::Base}
170
+ expect{Product.decorator_class}.to raise_error NameError, /Draper::Base/
171
+ end
172
+ end
164
173
  end
165
174
 
166
175
  end
@@ -4,139 +4,78 @@ module Draper
4
4
  describe DecoratedAssociation do
5
5
 
6
6
  describe "#initialize" do
7
- describe "options validation" do
8
- it "does not raise error on valid options" do
9
- valid_options = {with: Decorator, scope: :foo, context: {}}
10
- expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, valid_options)}.not_to raise_error
11
- end
12
-
13
- it "raises error on invalid options" do
14
- expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, foo: "bar")}.to raise_error ArgumentError, /Unknown key/
15
- end
7
+ it "accepts valid options" do
8
+ valid_options = {with: Decorator, scope: :foo, context: {}}
9
+ expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, valid_options)}.not_to raise_error
16
10
  end
17
- end
18
-
19
- describe "#call" do
20
- let(:context) { {some: "context"} }
21
- let(:options) { {} }
22
11
 
23
- let(:decorated_association) do
24
- owner = double(context: nil, source: double(association: associated))
25
-
26
- DecoratedAssociation.new(owner, :association, options).tap do |decorated_association|
27
- decorated_association.stub context: context
28
- end
12
+ it "rejects invalid options" do
13
+ expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, foo: "bar")}.to raise_error ArgumentError, /Unknown key/
29
14
  end
30
15
 
31
- context "for a singular association" do
32
- let(:associated) { Model.new }
33
-
34
- context "when :with option was given" do
35
- let(:options) { {with: Decorator} }
16
+ it "creates a factory" do
17
+ options = {with: Decorator, context: {foo: "bar"}}
36
18
 
37
- it "uses the specified decorator" do
38
- Decorator.should_receive(:decorate).with(associated, context: context).and_return(:decorated)
39
- expect(decorated_association.call).to be :decorated
40
- end
41
- end
42
-
43
- context "when :with option was not given" do
44
- it "infers the decorator" do
45
- associated.stub decorator_class: OtherDecorator
46
-
47
- OtherDecorator.should_receive(:decorate).with(associated, context: context).and_return(:decorated)
48
- expect(decorated_association.call).to be :decorated
49
- end
50
- end
19
+ Factory.should_receive(:new).with(options)
20
+ DecoratedAssociation.new(double, :association, options)
51
21
  end
52
22
 
53
- context "for a collection association" do
54
- let(:associated) { [] }
55
-
56
- context "when :with option is a collection decorator" do
57
- let(:options) { {with: ProductsDecorator} }
58
-
59
- it "uses the specified decorator" do
60
- ProductsDecorator.should_receive(:decorate).with(associated, context: context).and_return(:decorated_collection)
61
- expect(decorated_association.call).to be :decorated_collection
62
- end
63
- end
64
-
65
- context "when :with option is a singular decorator" do
66
- let(:options) { {with: ProductDecorator} }
67
-
68
- it "uses a CollectionDecorator of the specified decorator" do
69
- ProductDecorator.should_receive(:decorate_collection).with(associated, context: context).and_return(:decorated_collection)
70
- expect(decorated_association.call).to be :decorated_collection
71
- end
72
- end
73
-
74
- context "when :with option was not given" do
75
- context "when the collection itself is decoratable" do
76
- before { associated.stub decorator_class: ProductsDecorator }
77
-
78
- it "infers the decorator" do
79
- ProductsDecorator.should_receive(:decorate).with(associated, context: context).and_return(:decorated_collection)
80
- expect(decorated_association.call).to be :decorated_collection
81
- end
82
- end
83
-
84
- context "when the collection is not decoratable" do
85
- it "uses a CollectionDecorator of inferred decorators" do
86
- CollectionDecorator.should_receive(:decorate).with(associated, context: context).and_return(:decorated_collection)
87
- expect(decorated_association.call).to be :decorated_collection
88
- end
89
- end
23
+ describe ":with option" do
24
+ it "defaults to nil" do
25
+ Factory.should_receive(:new).with(with: nil, context: anything())
26
+ DecoratedAssociation.new(double, :association, {})
90
27
  end
91
28
  end
92
29
 
93
- context "with a scope" do
94
- let(:options) { {scope: :foo} }
95
- let(:associated) { double(foo: scoped) }
96
- let(:scoped) { Product.new }
97
-
98
- it "applies the scope before decoration" do
99
- expect(decorated_association.call.source).to be scoped
30
+ describe ":context option" do
31
+ it "defaults to the identity function" do
32
+ Factory.should_receive(:new).with do |options|
33
+ options[:context].call(:anything) == :anything
34
+ end
35
+ DecoratedAssociation.new(double, :association, {})
100
36
  end
101
37
  end
102
38
  end
103
39
 
104
- describe "#context" do
105
- let(:owner_context) { {some: "context"} }
106
- let(:options) { {} }
107
- let(:decorated_association) do
108
- owner = double(context: owner_context)
109
- DecoratedAssociation.new(owner, :association, options)
40
+ describe "#call" do
41
+ it "calls the factory" do
42
+ factory = double
43
+ Factory.stub new: factory
44
+ associated = double
45
+ owner_context = {foo: "bar"}
46
+ source = double(association: associated)
47
+ owner = double(source: source, context: owner_context)
48
+ decorated_association = DecoratedAssociation.new(owner, :association, {})
49
+ decorated = double
50
+
51
+ factory.should_receive(:decorate).with(associated, context_args: owner_context).and_return(decorated)
52
+ expect(decorated_association.call).to be decorated
110
53
  end
111
54
 
112
- context "when :context option was given" do
113
- let(:options) { {context: context} }
55
+ it "memoizes" do
56
+ factory = double
57
+ Factory.stub new: factory
58
+ owner = double(source: double(association: double), context: {})
59
+ decorated_association = DecoratedAssociation.new(owner, :association, {})
60
+ decorated = double
114
61
 
115
- context "and is callable" do
116
- let(:context) { ->(*){ :dynamic_context } }
117
-
118
- it "calls it with the owner's context" do
119
- context.should_receive(:call).with(owner_context)
120
- decorated_association.context
121
- end
122
-
123
- it "returns the lambda's return value" do
124
- expect(decorated_association.context).to be :dynamic_context
125
- end
126
- end
127
-
128
- context "and is not callable" do
129
- let(:context) { {other: "context"} }
130
-
131
- it "returns the specified value" do
132
- expect(decorated_association.context).to be context
133
- end
134
- end
62
+ factory.should_receive(:decorate).once.and_return(decorated)
63
+ expect(decorated_association.call).to be decorated
64
+ expect(decorated_association.call).to be decorated
135
65
  end
136
66
 
137
- context "when :context option was not given" do
138
- it "returns the owner's context" do
139
- expect(decorated_association.context).to be owner_context
67
+ context "when the :scope option was given" do
68
+ it "applies the scope before decoration" do
69
+ factory = double
70
+ Factory.stub new: factory
71
+ scoped = double
72
+ source = double(association: double(applied_scope: scoped))
73
+ owner = double(source: source, context: {})
74
+ decorated_association = DecoratedAssociation.new(owner, :association, scope: :applied_scope)
75
+ decorated = double
76
+
77
+ factory.should_receive(:decorate).with(scoped, anything()).and_return(decorated)
78
+ expect(decorated_association.call).to be decorated
140
79
  end
141
80
  end
142
81
  end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+
3
+ module Draper
4
+ describe DecoratesAssigned do
5
+ let(:controller_class) do
6
+ Class.new do
7
+ extend DecoratesAssigned
8
+
9
+ def self.helper_method(method)
10
+ helper_methods << method
11
+ end
12
+
13
+ def self.helper_methods
14
+ @helper_methods ||= []
15
+ end
16
+ end
17
+ end
18
+
19
+ describe ".decorates_assigned" do
20
+ it "adds helper methods" do
21
+ controller_class.decorates_assigned :article, :author
22
+
23
+ expect(controller_class.instance_methods).to include :article
24
+ expect(controller_class.instance_methods).to include :author
25
+
26
+ expect(controller_class.helper_methods).to include :article
27
+ expect(controller_class.helper_methods).to include :author
28
+ end
29
+
30
+ it "creates a factory" do
31
+ Factory.should_receive(:new).once
32
+ controller_class.decorates_assigned :article, :author
33
+ end
34
+
35
+ it "passes options to the factory" do
36
+ options = {foo: "bar"}
37
+
38
+ Factory.should_receive(:new).with(options)
39
+ controller_class.decorates_assigned :article, :author, options
40
+ end
41
+
42
+ describe "the generated method" do
43
+ it "decorates the instance variable" do
44
+ source = double
45
+ factory = double
46
+ Factory.stub new: factory
47
+
48
+ controller_class.decorates_assigned :article
49
+ controller = controller_class.new
50
+ controller.instance_variable_set "@article", source
51
+
52
+ factory.should_receive(:decorate).with(source, context_args: controller).and_return(:decorated)
53
+ expect(controller.article).to be :decorated
54
+ end
55
+
56
+ it "memoizes" do
57
+ factory = double
58
+ Factory.stub new: factory
59
+
60
+ controller_class.decorates_assigned :article
61
+ controller = controller_class.new
62
+
63
+ factory.should_receive(:decorate).once
64
+ controller.article
65
+ controller.article
66
+ end
67
+ end
68
+ end
69
+
70
+ end
71
+ end
@@ -145,6 +145,13 @@ module Draper
145
145
  ProductDecorator.decorate_collection([], options)
146
146
  end
147
147
  end
148
+
149
+ context "when a NameError is thrown" do
150
+ it "re-raises that error" do
151
+ String.any_instance.stub(:constantize).and_return{Draper::DecoratedEnumerableProxy}
152
+ expect{ProductDecorator.decorate_collection([])}.to raise_error NameError, /Draper::DecoratedEnumerableProxy/
153
+ end
154
+ end
148
155
  end
149
156
 
150
157
  describe ".decorates" do
@@ -170,20 +177,23 @@ module Draper
170
177
  end
171
178
 
172
179
  describe ".source_class" do
180
+ protect_class ProductDecorator
181
+ protect_class Namespaced::ProductDecorator
182
+
173
183
  context "when not set by .decorates" do
174
- it "raises an error for a so-named 'Decorator'" do
184
+ it "raises an UninferrableSourceError for a so-named 'Decorator'" do
175
185
  expect{Decorator.source_class}.to raise_error UninferrableSourceError
176
186
  end
177
187
 
178
- it "raises an error for anonymous decorators" do
188
+ it "raises an UninferrableSourceError for anonymous decorators" do
179
189
  expect{Class.new(Decorator).source_class}.to raise_error UninferrableSourceError
180
190
  end
181
191
 
182
- it "raises an error for a decorator without a model" do
192
+ it "raises an UninferrableSourceError for a decorator without a model" do
183
193
  expect{OtherDecorator.source_class}.to raise_error UninferrableSourceError
184
194
  end
185
195
 
186
- it "raises an error for other naming conventions" do
196
+ it "raises an UninferrableSourceError for other naming conventions" do
187
197
  expect{ProductPresenter.source_class}.to raise_error UninferrableSourceError
188
198
  end
189
199
 
@@ -194,6 +204,13 @@ module Draper
194
204
  it "infers namespaced sources" do
195
205
  expect(Namespaced::ProductDecorator.source_class).to be Namespaced::Product
196
206
  end
207
+
208
+ context "when an unrelated NameError is thrown" do
209
+ it "re-raises that error" do
210
+ String.any_instance.stub(:constantize).and_return{SomethingThatDoesntExist}
211
+ expect{ProductDecorator.source_class}.to raise_error NameError, /SomethingThatDoesntExist/
212
+ end
213
+ end
197
214
  end
198
215
  end
199
216
 
@@ -345,6 +362,15 @@ module Draper
345
362
  end
346
363
  end
347
364
 
365
+ describe "#attributes" do
366
+ it "returns only the source's attributes that are implemented by the decorator" do
367
+ decorator = Decorator.new(double(attributes: {foo: "bar", baz: "qux"}))
368
+ decorator.stub(:foo)
369
+
370
+ expect(decorator.attributes).to eq({foo: "bar"})
371
+ end
372
+ end
373
+
348
374
  describe ".model_name" do
349
375
  it "delegates to the source class" do
350
376
  Decorator.stub source_class: double(model_name: :delegated)
@@ -354,13 +380,18 @@ module Draper
354
380
  end
355
381
 
356
382
  describe "#==" do
357
- it "ensures the source has a decoration-aware #==" do
383
+ it "works for a source that does not include Decoratable" do
358
384
  source = Object.new
359
385
  decorator = Decorator.new(source)
360
386
 
361
- expect(source).not_to be_a_kind_of Draper::Decoratable::Equality
362
- decorator == :something
363
- expect(source).to be_a_kind_of Draper::Decoratable::Equality
387
+ expect(decorator).to eq Decorator.new(source)
388
+ end
389
+
390
+ it "works for a multiply-decorated source that does not include Decoratable" do
391
+ source = Object.new
392
+ decorator = Decorator.new(source)
393
+
394
+ expect(decorator).to eq ProductDecorator.new(Decorator.new(source))
364
395
  end
365
396
 
366
397
  it "is true when source #== is true" do
@@ -554,6 +585,25 @@ module Draper
554
585
  end
555
586
  end
556
587
  end
588
+
589
+ describe "#respond_to_missing?" do
590
+ it "allows #method to be called on delegated methods" do
591
+ source = Class.new{def hello_world; end}.new
592
+ decorator = Decorator.new(source)
593
+
594
+ expect { decorator.method(:hello_world) }.not_to raise_error NameError
595
+ expect(decorator.method(:hello_world)).not_to be_nil
596
+ end
597
+ end
598
+
599
+ describe ".respond_to_missing?" do
600
+ it "allows .method to be called on delegated class methods" do
601
+ Decorator.stub source_class: double(hello_world: :delegated)
602
+
603
+ expect { Decorator.method(:hello_world) }.not_to raise_error NameError
604
+ expect(Decorator.method(:hello_world)).not_to be_nil
605
+ end
606
+ end
557
607
  end
558
608
 
559
609
  describe "class spoofing" do