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
@@ -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