draper_new 3.0.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 (144) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +15 -0
  5. data/.yardopts +1 -0
  6. data/CHANGELOG.md +230 -0
  7. data/CONTRIBUTING.md +20 -0
  8. data/Gemfile +16 -0
  9. data/Guardfile +26 -0
  10. data/LICENSE +7 -0
  11. data/README.md +587 -0
  12. data/Rakefile +69 -0
  13. data/draper_new.gemspec +31 -0
  14. data/gemfiles/4.0.gemfile +3 -0
  15. data/gemfiles/4.1.gemfile +3 -0
  16. data/gemfiles/4.2.6.gemfile +3 -0
  17. data/gemfiles/4.2.gemfile +3 -0
  18. data/lib/draper.rb +63 -0
  19. data/lib/draper/automatic_delegation.rb +56 -0
  20. data/lib/draper/collection_decorator.rb +100 -0
  21. data/lib/draper/decoratable.rb +96 -0
  22. data/lib/draper/decoratable/equality.rb +26 -0
  23. data/lib/draper/decorated_association.rb +35 -0
  24. data/lib/draper/decorates_assigned.rb +44 -0
  25. data/lib/draper/decorator.rb +293 -0
  26. data/lib/draper/delegation.rb +13 -0
  27. data/lib/draper/factory.rb +91 -0
  28. data/lib/draper/finders.rb +37 -0
  29. data/lib/draper/helper_proxy.rb +44 -0
  30. data/lib/draper/helper_support.rb +5 -0
  31. data/lib/draper/lazy_helpers.rb +15 -0
  32. data/lib/draper/railtie.rb +70 -0
  33. data/lib/draper/tasks/test.rake +22 -0
  34. data/lib/draper/test/devise_helper.rb +30 -0
  35. data/lib/draper/test/minitest_integration.rb +6 -0
  36. data/lib/draper/test/rspec_integration.rb +20 -0
  37. data/lib/draper/test_case.rb +42 -0
  38. data/lib/draper/undecorate.rb +9 -0
  39. data/lib/draper/version.rb +3 -0
  40. data/lib/draper/view_context.rb +104 -0
  41. data/lib/draper/view_context/build_strategy.rb +48 -0
  42. data/lib/draper/view_helpers.rb +37 -0
  43. data/lib/generators/controller_override.rb +17 -0
  44. data/lib/generators/mini_test/decorator_generator.rb +20 -0
  45. data/lib/generators/mini_test/templates/decorator_spec.rb +4 -0
  46. data/lib/generators/mini_test/templates/decorator_test.rb +4 -0
  47. data/lib/generators/rails/decorator_generator.rb +36 -0
  48. data/lib/generators/rails/templates/decorator.rb +19 -0
  49. data/lib/generators/rspec/decorator_generator.rb +9 -0
  50. data/lib/generators/rspec/templates/decorator_spec.rb +4 -0
  51. data/lib/generators/test_unit/decorator_generator.rb +9 -0
  52. data/lib/generators/test_unit/templates/decorator_test.rb +4 -0
  53. data/spec/draper/collection_decorator_spec.rb +307 -0
  54. data/spec/draper/decoratable/equality_spec.rb +10 -0
  55. data/spec/draper/decoratable_spec.rb +202 -0
  56. data/spec/draper/decorated_association_spec.rb +84 -0
  57. data/spec/draper/decorates_assigned_spec.rb +71 -0
  58. data/spec/draper/decorator_spec.rb +816 -0
  59. data/spec/draper/factory_spec.rb +251 -0
  60. data/spec/draper/finders_spec.rb +166 -0
  61. data/spec/draper/helper_proxy_spec.rb +61 -0
  62. data/spec/draper/lazy_helpers_spec.rb +21 -0
  63. data/spec/draper/undecorate_spec.rb +19 -0
  64. data/spec/draper/view_context/build_strategy_spec.rb +116 -0
  65. data/spec/draper/view_context_spec.rb +154 -0
  66. data/spec/draper/view_helpers_spec.rb +8 -0
  67. data/spec/dummy/.rspec +2 -0
  68. data/spec/dummy/Rakefile +7 -0
  69. data/spec/dummy/app/controllers/application_controller.rb +4 -0
  70. data/spec/dummy/app/controllers/localized_urls.rb +5 -0
  71. data/spec/dummy/app/controllers/posts_controller.rb +20 -0
  72. data/spec/dummy/app/decorators/mongoid_post_decorator.rb +4 -0
  73. data/spec/dummy/app/decorators/post_decorator.rb +60 -0
  74. data/spec/dummy/app/helpers/application_helper.rb +5 -0
  75. data/spec/dummy/app/mailers/application_mailer.rb +3 -0
  76. data/spec/dummy/app/mailers/post_mailer.rb +19 -0
  77. data/spec/dummy/app/models/admin.rb +5 -0
  78. data/spec/dummy/app/models/mongoid_post.rb +5 -0
  79. data/spec/dummy/app/models/post.rb +3 -0
  80. data/spec/dummy/app/models/user.rb +5 -0
  81. data/spec/dummy/app/views/layouts/application.html.erb +11 -0
  82. data/spec/dummy/app/views/post_mailer/decorated_email.html.erb +1 -0
  83. data/spec/dummy/app/views/posts/_post.html.erb +40 -0
  84. data/spec/dummy/app/views/posts/show.html.erb +1 -0
  85. data/spec/dummy/bin/rails +4 -0
  86. data/spec/dummy/config.ru +4 -0
  87. data/spec/dummy/config/application.rb +71 -0
  88. data/spec/dummy/config/boot.rb +5 -0
  89. data/spec/dummy/config/database.yml +25 -0
  90. data/spec/dummy/config/environment.rb +5 -0
  91. data/spec/dummy/config/environments/development.rb +33 -0
  92. data/spec/dummy/config/environments/production.rb +57 -0
  93. data/spec/dummy/config/environments/test.rb +31 -0
  94. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  95. data/spec/dummy/config/initializers/inflections.rb +15 -0
  96. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  97. data/spec/dummy/config/initializers/secret_token.rb +8 -0
  98. data/spec/dummy/config/initializers/session_store.rb +8 -0
  99. data/spec/dummy/config/locales/en.yml +5 -0
  100. data/spec/dummy/config/mongoid.yml +79 -0
  101. data/spec/dummy/config/routes.rb +9 -0
  102. data/spec/dummy/db/migrate/20121019115657_create_posts.rb +8 -0
  103. data/spec/dummy/db/schema.rb +21 -0
  104. data/spec/dummy/db/seeds.rb +2 -0
  105. data/spec/dummy/fast_spec/post_decorator_spec.rb +37 -0
  106. data/spec/dummy/lib/tasks/test.rake +16 -0
  107. data/spec/dummy/public/404.html +26 -0
  108. data/spec/dummy/public/422.html +26 -0
  109. data/spec/dummy/public/500.html +25 -0
  110. data/spec/dummy/public/favicon.ico +0 -0
  111. data/spec/dummy/script/rails +6 -0
  112. data/spec/dummy/spec/decorators/active_model_serializers_spec.rb +16 -0
  113. data/spec/dummy/spec/decorators/devise_spec.rb +64 -0
  114. data/spec/dummy/spec/decorators/helpers_spec.rb +21 -0
  115. data/spec/dummy/spec/decorators/post_decorator_spec.rb +66 -0
  116. data/spec/dummy/spec/decorators/spec_type_spec.rb +7 -0
  117. data/spec/dummy/spec/decorators/view_context_spec.rb +22 -0
  118. data/spec/dummy/spec/mailers/post_mailer_spec.rb +33 -0
  119. data/spec/dummy/spec/models/mongoid_post_spec.rb +8 -0
  120. data/spec/dummy/spec/models/post_spec.rb +6 -0
  121. data/spec/dummy/spec/shared_examples/decoratable.rb +24 -0
  122. data/spec/dummy/spec/spec_helper.rb +8 -0
  123. data/spec/dummy/test/decorators/minitest/devise_test.rb +64 -0
  124. data/spec/dummy/test/decorators/minitest/helpers_test.rb +21 -0
  125. data/spec/dummy/test/decorators/minitest/spec_type_test.rb +52 -0
  126. data/spec/dummy/test/decorators/minitest/view_context_test.rb +24 -0
  127. data/spec/dummy/test/decorators/test_unit/devise_test.rb +64 -0
  128. data/spec/dummy/test/decorators/test_unit/helpers_test.rb +21 -0
  129. data/spec/dummy/test/decorators/test_unit/view_context_test.rb +24 -0
  130. data/spec/dummy/test/minitest_helper.rb +2 -0
  131. data/spec/dummy/test/test_helper.rb +3 -0
  132. data/spec/generators/controller/controller_generator_spec.rb +22 -0
  133. data/spec/generators/decorator/decorator_generator_spec.rb +92 -0
  134. data/spec/integration/integration_spec.rb +66 -0
  135. data/spec/performance/active_record.rb +4 -0
  136. data/spec/performance/benchmark.rb +55 -0
  137. data/spec/performance/decorators.rb +45 -0
  138. data/spec/performance/models.rb +20 -0
  139. data/spec/spec_helper.rb +41 -0
  140. data/spec/support/dummy_app.rb +85 -0
  141. data/spec/support/matchers/have_text.rb +50 -0
  142. data/spec/support/shared_examples/decoratable_equality.rb +40 -0
  143. data/spec/support/shared_examples/view_helpers.rb +39 -0
  144. metadata +420 -0
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+ require 'support/shared_examples/decoratable_equality'
3
+
4
+ module Draper
5
+ describe Decoratable::Equality do
6
+ describe "#==" do
7
+ it_behaves_like "decoration-aware #==", Object.new.extend(Decoratable::Equality)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,202 @@
1
+ require 'spec_helper'
2
+ require 'support/shared_examples/decoratable_equality'
3
+
4
+ module Draper
5
+ describe Decoratable do
6
+
7
+ describe "#decorate" do
8
+ it "returns a decorator for self" do
9
+ product = Product.new
10
+ decorator = product.decorate
11
+
12
+ expect(decorator).to be_a ProductDecorator
13
+ expect(decorator.object).to be product
14
+ end
15
+
16
+ it "accepts context" do
17
+ context = {some: "context"}
18
+ decorator = Product.new.decorate(context: context)
19
+
20
+ expect(decorator.context).to be context
21
+ end
22
+
23
+ it "uses the #decorator_class" do
24
+ product = Product.new
25
+ product.stub decorator_class: OtherDecorator
26
+
27
+ expect(product.decorate).to be_an_instance_of OtherDecorator
28
+ end
29
+ end
30
+
31
+ describe "#applied_decorators" do
32
+ it "returns an empty list" do
33
+ expect(Product.new.applied_decorators).to eq []
34
+ end
35
+ end
36
+
37
+ describe "#decorated_with?" do
38
+ it "returns false" do
39
+ expect(Product.new).not_to be_decorated_with Decorator
40
+ end
41
+ end
42
+
43
+ describe "#decorated?" do
44
+ it "returns false" do
45
+ expect(Product.new).not_to be_decorated
46
+ end
47
+ end
48
+
49
+ describe "#decorator_class?" do
50
+ it "returns true for decoratable model" do
51
+ expect(Product.new.decorator_class?).to be_truthy
52
+ end
53
+
54
+ it "returns false for non-decoratable model" do
55
+ expect(Model.new.decorator_class?).to be_falsey
56
+ end
57
+ end
58
+
59
+ describe ".decorator_class?" do
60
+ it "returns true for decoratable model" do
61
+ expect(Product.decorator_class?).to be_truthy
62
+ end
63
+
64
+ it "returns false for non-decoratable model" do
65
+ expect(Model.decorator_class?).to be_falsey
66
+ end
67
+ end
68
+
69
+ describe "#decorator_class" do
70
+ it "delegates to .decorator_class" do
71
+ product = Product.new
72
+
73
+ Product.should_receive(:decorator_class).and_return(:some_decorator)
74
+ expect(product.decorator_class).to be :some_decorator
75
+ end
76
+ end
77
+
78
+ describe "#==" do
79
+ it_behaves_like "decoration-aware #==", Product.new
80
+ end
81
+
82
+ describe "#===" do
83
+ it "is true when #== is true" do
84
+ product = Product.new
85
+
86
+ product.should_receive(:==).and_return(true)
87
+ expect(product === :anything).to be_truthy
88
+ end
89
+
90
+ it "is false when #== is false" do
91
+ product = Product.new
92
+
93
+ product.should_receive(:==).and_return(false)
94
+ expect(product === :anything).to be_falsey
95
+ end
96
+ end
97
+
98
+ describe ".====" do
99
+ it "is true for an instance" do
100
+ expect(Product === Product.new).to be_truthy
101
+ end
102
+
103
+ it "is true for a derived instance" do
104
+ expect(Product === Class.new(Product).new).to be_truthy
105
+ end
106
+
107
+ it "is false for an unrelated instance" do
108
+ expect(Product === Model.new).to be_falsey
109
+ end
110
+
111
+ it "is true for a decorated instance" do
112
+ decorator = double(object: Product.new)
113
+
114
+ expect(Product === decorator).to be_truthy
115
+ end
116
+
117
+ it "is true for a decorated derived instance" do
118
+ decorator = double(object: Class.new(Product).new)
119
+
120
+ expect(Product === decorator).to be_truthy
121
+ end
122
+
123
+ it "is false for a decorated unrelated instance" do
124
+ decorator = double(object: Model.new)
125
+
126
+ expect(Product === decorator).to be_falsey
127
+ end
128
+ end
129
+
130
+ describe ".decorate" do
131
+ let(:scoping_method) { Rails::VERSION::MAJOR >= 4 ? :all : :scoped }
132
+
133
+ it "calls #decorate_collection on .decorator_class" do
134
+ scoped = [Product.new]
135
+ Product.stub scoping_method => scoped
136
+
137
+ Product.decorator_class.should_receive(:decorate_collection).with(scoped, with: nil).and_return(:decorated_collection)
138
+ expect(Product.decorate).to be :decorated_collection
139
+ end
140
+
141
+ it "accepts options" do
142
+ options = {with: ProductDecorator, context: {some: "context"}}
143
+ Product.stub scoping_method => []
144
+
145
+ Product.decorator_class.should_receive(:decorate_collection).with([], options)
146
+ Product.decorate(options)
147
+ end
148
+ end
149
+
150
+ describe ".decorator_class" do
151
+ context "for classes" do
152
+ it "infers the decorator from the class" do
153
+ expect(Product.decorator_class).to be ProductDecorator
154
+ end
155
+
156
+ context "without a decorator on its own" do
157
+ it "infers the decorator from a superclass" do
158
+ expect(SpecialProduct.decorator_class).to be ProductDecorator
159
+ end
160
+ end
161
+ end
162
+
163
+ context "for ActiveModel classes" do
164
+ it "infers the decorator from the model name" do
165
+ allow(Product).to receive(:model_name){"Other"}
166
+
167
+ expect(Product.decorator_class).to be OtherDecorator
168
+ end
169
+ end
170
+
171
+ context "in a namespace" do
172
+ context "for classes" do
173
+ it "infers the decorator from the class" do
174
+ expect(Namespaced::Product.decorator_class).to be Namespaced::ProductDecorator
175
+ end
176
+ end
177
+
178
+ context "for ActiveModel classes" do
179
+ it "infers the decorator from the model name" do
180
+ Namespaced::Product.stub(:model_name).and_return("Namespaced::Other")
181
+
182
+ expect(Namespaced::Product.decorator_class).to be Namespaced::OtherDecorator
183
+ end
184
+ end
185
+ end
186
+
187
+ context "when the decorator can't be inferred" do
188
+ it "throws an UninferrableDecoratorError" do
189
+ expect{Model.decorator_class}.to raise_error UninferrableDecoratorError
190
+ end
191
+ end
192
+
193
+ context "when an unrelated NameError is thrown" do
194
+ it "re-raises that error" do
195
+ String.any_instance.stub(:constantize) { Draper::Base }
196
+ expect{Product.decorator_class}.to raise_error NameError, /Draper::Base/
197
+ end
198
+ end
199
+ end
200
+
201
+ end
202
+ end
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+
3
+ module Draper
4
+ describe DecoratedAssociation do
5
+
6
+ describe "#initialize" do
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
10
+ end
11
+
12
+ it "rejects invalid options" do
13
+ expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, foo: "bar")}.to raise_error ArgumentError, /Unknown key/
14
+ end
15
+
16
+ it "creates a factory" do
17
+ options = {with: Decorator, context: {foo: "bar"}}
18
+
19
+ Factory.should_receive(:new).with(options)
20
+ DecoratedAssociation.new(double, :association, options)
21
+ end
22
+
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, {})
27
+ end
28
+ end
29
+
30
+ describe ":context option" do
31
+ it "defaults to the identity function" do
32
+ Factory.should_receive(:new) do |options|
33
+ options[:context].call(:anything) == :anything
34
+ end
35
+ DecoratedAssociation.new(double, :association, {})
36
+ end
37
+ end
38
+ end
39
+
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
+ object = double(association: associated)
47
+ owner = double(object: object, 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
53
+ end
54
+
55
+ it "memoizes" do
56
+ factory = double
57
+ Factory.stub new: factory
58
+ owner = double(object: double(association: double), context: {})
59
+ decorated_association = DecoratedAssociation.new(owner, :association, {})
60
+ decorated = double
61
+
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
65
+ end
66
+
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
+ object = double(association: double(applied_scope: scoped))
73
+ owner = double(object: object, 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
79
+ end
80
+ end
81
+ end
82
+
83
+ end
84
+ 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
+ object = 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", object
51
+
52
+ factory.should_receive(:decorate).with(object, 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
@@ -0,0 +1,816 @@
1
+ require 'spec_helper'
2
+ require 'support/shared_examples/view_helpers'
3
+
4
+ module Draper
5
+ describe Decorator do
6
+ it_behaves_like "view helpers", Decorator.new(Model.new)
7
+
8
+ describe "#initialize" do
9
+ describe "options validation" do
10
+ it "does not raise error on valid options" do
11
+ valid_options = {context: {}}
12
+ expect{Decorator.new(Model.new, valid_options)}.not_to raise_error
13
+ end
14
+
15
+ it "raises error on invalid options" do
16
+ expect{Decorator.new(Model.new, foo: "bar")}.to raise_error ArgumentError, /Unknown key/
17
+ end
18
+ end
19
+
20
+ it "sets the object" do
21
+ object = Model.new
22
+ decorator = Decorator.new(object)
23
+
24
+ expect(decorator.object).to be object
25
+ end
26
+
27
+ it "stores context" do
28
+ context = {some: "context"}
29
+ decorator = Decorator.new(Model.new, context: context)
30
+
31
+ expect(decorator.context).to be context
32
+ end
33
+
34
+ context "when decorating an instance of itself" do
35
+ it "applies to the object instead" do
36
+ object = Model.new
37
+ decorated = Decorator.new(object)
38
+ redecorated = Decorator.new(decorated)
39
+
40
+ expect(redecorated.object).to be object
41
+ end
42
+
43
+ context "with context" do
44
+ it "overwrites existing context" do
45
+ decorated = Decorator.new(Model.new, context: {some: "context"})
46
+ new_context = {other: "context"}
47
+ redecorated = Decorator.new(decorated, context: new_context)
48
+
49
+ expect(redecorated.context).to be new_context
50
+ end
51
+ end
52
+
53
+ context "without context" do
54
+ it "preserves existing context" do
55
+ old_context = {some: "context"}
56
+ decorated = Decorator.new(Model.new, context: old_context)
57
+ redecorated = Decorator.new(decorated)
58
+
59
+ expect(redecorated.context).to be old_context
60
+ end
61
+ end
62
+ end
63
+
64
+ it "decorates other decorators" do
65
+ decorated = OtherDecorator.new(Model.new)
66
+ redecorated = Decorator.new(decorated)
67
+
68
+ expect(redecorated.object).to be decorated
69
+ end
70
+
71
+ context "when it has been applied previously" do
72
+ it "warns" do
73
+ decorated = OtherDecorator.new(Decorator.new(Model.new))
74
+
75
+ warning_message = nil
76
+ Object.any_instance.stub(:warn) { |instance, message| warning_message = message }
77
+
78
+ expect{Decorator.new(decorated)}.to change{warning_message}
79
+ expect(warning_message).to start_with "Reapplying Draper::Decorator"
80
+ expect(warning_message).to include caller(1).first
81
+ end
82
+
83
+ it "decorates anyway" do
84
+ decorated = OtherDecorator.new(Decorator.new(Model.new))
85
+ Object.any_instance.stub(:warn)
86
+ redecorated = Decorator.decorate(decorated)
87
+
88
+ expect(redecorated.object).to be decorated
89
+ end
90
+ end
91
+ end
92
+
93
+ describe "#context=" do
94
+ it "modifies the context" do
95
+ decorator = Decorator.new(Model.new, context: {some: "context"})
96
+ new_context = {other: "context"}
97
+
98
+ decorator.context = new_context
99
+ expect(decorator.context).to be new_context
100
+ end
101
+ end
102
+
103
+ describe ".decorate_collection" do
104
+ describe "options validation" do
105
+ before { CollectionDecorator.stub(:new) }
106
+
107
+ it "does not raise error on valid options" do
108
+ valid_options = {with: OtherDecorator, context: {}}
109
+ expect{Decorator.decorate_collection([], valid_options)}.not_to raise_error
110
+ end
111
+
112
+ it "raises error on invalid options" do
113
+ expect{Decorator.decorate_collection([], foo: "bar")}.to raise_error ArgumentError, /Unknown key/
114
+ end
115
+ end
116
+
117
+ context "without a custom collection decorator" do
118
+ it "creates a CollectionDecorator using itself for each item" do
119
+ object = [Model.new]
120
+
121
+ CollectionDecorator.should_receive(:new).with(object, with: Decorator)
122
+ Decorator.decorate_collection(object)
123
+ end
124
+
125
+ it "passes options to the collection decorator" do
126
+ options = {with: OtherDecorator, context: {some: "context"}}
127
+
128
+ CollectionDecorator.should_receive(:new).with([], options)
129
+ Decorator.decorate_collection([], options)
130
+ end
131
+ end
132
+
133
+ context "with a custom collection decorator" do
134
+ it "creates a custom collection decorator using itself for each item" do
135
+ object = [Model.new]
136
+
137
+ ProductsDecorator.should_receive(:new).with(object, with: ProductDecorator)
138
+ ProductDecorator.decorate_collection(object)
139
+ end
140
+
141
+ it "passes options to the collection decorator" do
142
+ options = {with: OtherDecorator, context: {some: "context"}}
143
+
144
+ ProductsDecorator.should_receive(:new).with([], options)
145
+ ProductDecorator.decorate_collection([], options)
146
+ end
147
+ end
148
+
149
+ context "when a NameError is thrown" do
150
+ it "re-raises that error" do
151
+ String.any_instance.stub(:constantize) { Draper::DecoratedEnumerableProxy }
152
+ expect{ProductDecorator.decorate_collection([])}.to raise_error NameError, /Draper::DecoratedEnumerableProxy/
153
+ end
154
+ end
155
+ end
156
+
157
+ describe ".decorates" do
158
+ protect_class Decorator
159
+
160
+ it "sets .object_class with a symbol" do
161
+ Decorator.decorates :product
162
+
163
+ expect(Decorator.object_class).to be Product
164
+ end
165
+
166
+ it "sets .object_class with a string" do
167
+ Decorator.decorates "product"
168
+
169
+ expect(Decorator.object_class).to be Product
170
+ end
171
+
172
+ it "sets .object_class with a class" do
173
+ Decorator.decorates Product
174
+
175
+ expect(Decorator.object_class).to be Product
176
+ end
177
+ end
178
+
179
+ describe ".object_class" do
180
+ protect_class ProductDecorator
181
+ protect_class Namespaced::ProductDecorator
182
+
183
+ context "when not set by .decorates" do
184
+ it "raises an UninferrableSourceError for a so-named 'Decorator'" do
185
+ expect{Decorator.object_class}.to raise_error UninferrableSourceError
186
+ end
187
+
188
+ it "raises an UninferrableSourceError for anonymous decorators" do
189
+ expect{Class.new(Decorator).object_class}.to raise_error UninferrableSourceError
190
+ end
191
+
192
+ it "raises an UninferrableSourceError for a decorator without a model" do
193
+ skip
194
+ expect{OtherDecorator.object_class}.to raise_error UninferrableSourceError
195
+ end
196
+
197
+ it "raises an UninferrableSourceError for other naming conventions" do
198
+ expect{ProductPresenter.object_class}.to raise_error UninferrableSourceError
199
+ end
200
+
201
+ it "infers the source for '<Model>Decorator'" do
202
+ expect(ProductDecorator.object_class).to be Product
203
+ end
204
+
205
+ it "infers namespaced sources" do
206
+ expect(Namespaced::ProductDecorator.object_class).to be Namespaced::Product
207
+ end
208
+
209
+ context "when an unrelated NameError is thrown" do
210
+ it "re-raises that error" do
211
+ String.any_instance.stub(:constantize) { SomethingThatDoesntExist }
212
+ expect{ProductDecorator.object_class}.to raise_error NameError, /SomethingThatDoesntExist/
213
+ end
214
+ end
215
+ end
216
+
217
+ it "is aliased to .source_class" do
218
+ expect(ProductDecorator.source_class).to be Product
219
+ end
220
+ end
221
+
222
+ describe ".object_class?" do
223
+ it "returns truthy when .object_class is set" do
224
+ Decorator.stub(:object_class).and_return(Model)
225
+
226
+ expect(Decorator.object_class?).to be_truthy
227
+ end
228
+
229
+ it "returns false when .object_class is not inferrable" do
230
+ Decorator.stub(:object_class).and_raise(UninferrableSourceError.new(Decorator))
231
+
232
+ expect(Decorator.object_class?).to be_falsey
233
+ end
234
+
235
+ it "is aliased to .source_class?" do
236
+ Decorator.stub(:object_class).and_return(Model)
237
+
238
+ expect(Decorator.source_class?).to be_truthy
239
+ end
240
+ end
241
+
242
+ describe ".decorates_association" do
243
+ protect_class Decorator
244
+
245
+ describe "options validation" do
246
+ before { DecoratedAssociation.stub(:new).and_return(->{}) }
247
+
248
+ it "does not raise error on valid options" do
249
+ valid_options = {with: Class, scope: :sorted, context: {}}
250
+ expect{Decorator.decorates_association(:children, valid_options)}.not_to raise_error
251
+ end
252
+
253
+ it "raises error on invalid options" do
254
+ expect{Decorator.decorates_association(:children, foo: "bar")}.to raise_error ArgumentError, /Unknown key/
255
+ end
256
+ end
257
+
258
+ describe "defines an association method" do
259
+ it "creates a DecoratedAssociation" do
260
+ options = {with: Class.new, scope: :foo, context: {}}
261
+ Decorator.decorates_association :children, options
262
+ decorator = Decorator.new(Model.new)
263
+
264
+ DecoratedAssociation.should_receive(:new).with(decorator, :children, options).and_return(->{})
265
+ decorator.children
266
+ end
267
+
268
+ it "memoizes the DecoratedAssociation" do
269
+ Decorator.decorates_association :children
270
+ decorator = Decorator.new(Model.new)
271
+
272
+ DecoratedAssociation.should_receive(:new).once.and_return(->{})
273
+ decorator.children
274
+ decorator.children
275
+ end
276
+
277
+ it "calls the DecoratedAssociation" do
278
+ Decorator.decorates_association :children
279
+ decorator = Decorator.new(Model.new)
280
+ decorated_association = ->{}
281
+ DecoratedAssociation.stub(:new).and_return(decorated_association)
282
+
283
+ decorated_association.should_receive(:call).and_return(:decorated)
284
+ expect(decorator.children).to be :decorated
285
+ end
286
+ end
287
+ end
288
+
289
+ describe ".decorates_associations" do
290
+ protect_class Decorator
291
+
292
+ it "decorates each of the associations" do
293
+ Decorator.should_receive(:decorates_association).with(:friends, {})
294
+ Decorator.should_receive(:decorates_association).with(:enemies, {})
295
+ Decorator.decorates_associations :friends, :enemies
296
+ end
297
+
298
+ it "dispatches options" do
299
+ options = {with: Class.new, scope: :foo, context: {}}
300
+
301
+ Decorator.should_receive(:decorates_association).with(:friends, options)
302
+ Decorator.should_receive(:decorates_association).with(:enemies, options)
303
+ Decorator.decorates_associations :friends, :enemies, options
304
+ end
305
+ end
306
+
307
+ describe "#applied_decorators" do
308
+ it "returns a list of decorators applied to a model" do
309
+ decorator = ProductDecorator.new(OtherDecorator.new(Decorator.new(Model.new)))
310
+
311
+ expect(decorator.applied_decorators).to eq [Decorator, OtherDecorator, ProductDecorator]
312
+ end
313
+ end
314
+
315
+ describe "#decorated_with?" do
316
+ it "checks if a decorator has been applied to a model" do
317
+ decorator = ProductDecorator.new(Decorator.new(Model.new))
318
+
319
+ expect(decorator).to be_decorated_with Decorator
320
+ expect(decorator).to be_decorated_with ProductDecorator
321
+ expect(decorator).not_to be_decorated_with OtherDecorator
322
+ end
323
+ end
324
+
325
+ describe "#decorated?" do
326
+ it "returns true" do
327
+ decorator = Decorator.new(Model.new)
328
+
329
+ expect(decorator).to be_decorated
330
+ end
331
+ end
332
+
333
+ describe "#object" do
334
+ it "returns the wrapped object" do
335
+ object = Model.new
336
+ decorator = Decorator.new(object)
337
+
338
+ expect(decorator.object).to be object
339
+ expect(decorator.model).to be object
340
+ expect(decorator.to_source).to be object
341
+ end
342
+
343
+ it "is aliased to #model" do
344
+ object = Model.new
345
+ decorator = Decorator.new(object)
346
+
347
+ expect(decorator.model).to be object
348
+ end
349
+
350
+ it "is aliased to #source" do
351
+ object = Model.new
352
+ decorator = Decorator.new(object)
353
+
354
+ expect(decorator.source).to be object
355
+ end
356
+
357
+ it "is aliased to #to_source" do
358
+ object = Model.new
359
+ decorator = Decorator.new(object)
360
+
361
+ expect(decorator.to_source).to be object
362
+ end
363
+ end
364
+
365
+ describe "aliasing object to object class name" do
366
+ context "when object_class is inferrable from the decorator name" do
367
+ it "aliases object to the object class name" do
368
+ object = double
369
+ decorator = ProductDecorator.new(object)
370
+
371
+ expect(decorator.product).to be object
372
+ end
373
+ end
374
+
375
+ context "when object_class is set by decorates" do
376
+ it "aliases object to the object class name" do
377
+ decorator_class = Class.new(Decorator) { decorates Product }
378
+ object = double
379
+ decorator = decorator_class.new(object)
380
+
381
+ expect(decorator.product).to be object
382
+ end
383
+ end
384
+
385
+ context "when object_class's name is several words long" do
386
+ it "underscores the method name" do
387
+ stub_const "LongWindedModel", Class.new
388
+ decorator_class = Class.new(Decorator) { decorates LongWindedModel }
389
+ object = double
390
+ decorator = decorator_class.new(object)
391
+
392
+ expect(decorator.long_winded_model).to be object
393
+ end
394
+ end
395
+
396
+ context "when object_class is not set" do
397
+ it "does not alias object" do
398
+ decorator_class = Class.new(Decorator)
399
+
400
+ expect(decorator_class.instance_methods).to eq Decorator.instance_methods
401
+ end
402
+ end
403
+ end
404
+
405
+ describe "#to_model" do
406
+ it "returns the decorator" do
407
+ decorator = Decorator.new(Model.new)
408
+
409
+ expect(decorator.to_model).to be decorator
410
+ end
411
+ end
412
+
413
+ describe "#to_param" do
414
+ it "delegates to the object" do
415
+ decorator = Decorator.new(double(to_param: :delegated))
416
+
417
+ expect(decorator.to_param).to be :delegated
418
+ end
419
+ end
420
+
421
+ describe "#present?" do
422
+ it "delegates to the object" do
423
+ decorator = Decorator.new(double(present?: :delegated))
424
+
425
+ expect(decorator.present?).to be :delegated
426
+ end
427
+ end
428
+
429
+ describe "#blank?" do
430
+ it "delegates to the object" do
431
+ decorator = Decorator.new(double(blank?: :delegated))
432
+
433
+ expect(decorator.blank?).to be :delegated
434
+ end
435
+ end
436
+
437
+ describe "#to_partial_path" do
438
+ it "delegates to the object" do
439
+ decorator = Decorator.new(double(to_partial_path: :delegated))
440
+
441
+ expect(decorator.to_partial_path).to be :delegated
442
+ end
443
+ end
444
+
445
+ describe "#to_s" do
446
+ it "delegates to the object" do
447
+ decorator = Decorator.new(double(to_s: :delegated))
448
+
449
+ expect(decorator.to_s).to be :delegated
450
+ end
451
+ end
452
+
453
+ describe "#inspect" do
454
+ it "returns a detailed description of the decorator" do
455
+ decorator = ProductDecorator.new(double)
456
+
457
+ expect(decorator.inspect).to match /#<ProductDecorator:0x\h+ .+>/
458
+ end
459
+
460
+ it "includes the object" do
461
+ decorator = Decorator.new(double(inspect: "#<the object>"))
462
+
463
+ expect(decorator.inspect).to include "@object=#<the object>"
464
+ end
465
+
466
+ it "includes the context" do
467
+ decorator = Decorator.new(double, context: {foo: "bar"})
468
+
469
+ expect(decorator.inspect).to include '@context={:foo=>"bar"}'
470
+ end
471
+
472
+ it "includes other instance variables" do
473
+ decorator = Decorator.new(double)
474
+ decorator.instance_variable_set :@foo, "bar"
475
+
476
+ expect(decorator.inspect).to include '@foo="bar"'
477
+ end
478
+ end
479
+
480
+ describe "#attributes" do
481
+ it "returns only the object's attributes that are implemented by the decorator" do
482
+ decorator = Decorator.new(double(attributes: {foo: "bar", baz: "qux"}))
483
+ decorator.stub(:foo)
484
+
485
+ expect(decorator.attributes).to eq({foo: "bar"})
486
+ end
487
+ end
488
+
489
+ describe ".model_name" do
490
+ it "delegates to the source class" do
491
+ Decorator.stub object_class: double(model_name: :delegated)
492
+
493
+ expect(Decorator.model_name).to be :delegated
494
+ end
495
+ end
496
+
497
+ describe "#==" do
498
+ it "works for a object that does not include Decoratable" do
499
+ object = Object.new
500
+ decorator = Decorator.new(object)
501
+
502
+ expect(decorator).to eq Decorator.new(object)
503
+ end
504
+
505
+ it "works for a multiply-decorated object that does not include Decoratable" do
506
+ object = Object.new
507
+ decorator = Decorator.new(object)
508
+
509
+ expect(decorator).to eq ProductDecorator.new(Decorator.new(object))
510
+ end
511
+
512
+ it "is true when object #== is true" do
513
+ object = Model.new
514
+ decorator = Decorator.new(object)
515
+ other = double(object: Model.new)
516
+
517
+ object.should_receive(:==).with(other).and_return(true)
518
+ expect(decorator == other).to be_truthy
519
+ end
520
+
521
+ it "is false when object #== is false" do
522
+ object = Model.new
523
+ decorator = Decorator.new(object)
524
+ other = double(object: Model.new)
525
+
526
+ object.should_receive(:==).with(other).and_return(false)
527
+ expect(decorator == other).to be_falsey
528
+ end
529
+ end
530
+
531
+ describe "#===" do
532
+ it "is true when #== is true" do
533
+ decorator = Decorator.new(Model.new)
534
+ allow(decorator).to receive(:==) { true }
535
+
536
+ expect(decorator === :anything).to be_truthy
537
+ end
538
+
539
+ it "is false when #== is false" do
540
+ decorator = Decorator.new(Model.new)
541
+ decorator.stub(:==).with(:anything).and_return(false)
542
+
543
+ expect(decorator === :anything).to be_falsey
544
+ end
545
+ end
546
+
547
+ describe "#eql?" do
548
+ it "is true when #eql? is true" do
549
+ first = Decorator.new('foo')
550
+ second = Decorator.new('foo')
551
+
552
+ expect(first.eql? second).to be
553
+ end
554
+
555
+ it "is false when #eql? is false" do
556
+ first = Decorator.new('foo')
557
+ second = Decorator.new('bar')
558
+
559
+ expect(first.eql? second).to_not be
560
+ end
561
+ end
562
+
563
+ describe "#hash" do
564
+ it "is consistent for equal objects" do
565
+ object = Model.new
566
+ first = Decorator.new(object)
567
+ second = Decorator.new(object)
568
+
569
+ expect(first.hash == second.hash).to be
570
+ end
571
+ end
572
+
573
+ describe ".delegate" do
574
+ protect_class Decorator
575
+
576
+ it "defaults the :to option to :object" do
577
+ Object.should_receive(:delegate).with(:foo, :bar, to: :object)
578
+ Decorator.delegate :foo, :bar
579
+ end
580
+
581
+ it "does not overwrite the :to option if supplied" do
582
+ Object.should_receive(:delegate).with(:foo, :bar, to: :baz)
583
+ Decorator.delegate :foo, :bar, to: :baz
584
+ end
585
+ end
586
+
587
+ context "with .delegate_all" do
588
+ protect_class Decorator
589
+
590
+ before { Decorator.delegate_all }
591
+
592
+ describe "#method_missing" do
593
+ it "delegates missing methods that exist on the object" do
594
+ decorator = Decorator.new(double(hello_world: :delegated))
595
+
596
+ expect(decorator.hello_world).to be :delegated
597
+ end
598
+
599
+ it "adds delegated methods to the decorator when they are used" do
600
+ decorator = Decorator.new(double(hello_world: :delegated))
601
+
602
+ expect(decorator.methods).not_to include :hello_world
603
+ decorator.hello_world
604
+ expect(decorator.methods).to include :hello_world
605
+ end
606
+
607
+ it "passes blocks to delegated methods" do
608
+ object = Model.new
609
+ object.stub(:hello_world) { |*args, &block| block.call }
610
+ decorator = Decorator.new(object)
611
+
612
+ expect(decorator.hello_world{:yielded}).to be :yielded
613
+ end
614
+
615
+ it "does not confuse Kernel#Array" do
616
+ decorator = Decorator.new(Model.new)
617
+
618
+ expect(Array(decorator)).to be_an Array
619
+ end
620
+
621
+ it "delegates already-delegated methods" do
622
+ object = Class.new{ delegate :bar, to: :foo }.new
623
+ object.stub foo: double(bar: :delegated)
624
+ decorator = Decorator.new(object)
625
+
626
+ expect(decorator.bar).to be :delegated
627
+ end
628
+
629
+ it "does not delegate private methods" do
630
+ object = Class.new{ private; def hello_world; end }.new
631
+ decorator = Decorator.new(object)
632
+
633
+ expect{decorator.hello_world}.to raise_error NoMethodError
634
+ end
635
+
636
+ it "does not delegate methods that do not exist on the object" do
637
+ decorator = Decorator.new(Model.new)
638
+
639
+ expect(decorator.methods).not_to include :hello_world
640
+ expect{decorator.hello_world}.to raise_error NoMethodError
641
+ expect(decorator.methods).not_to include :hello_world
642
+ end
643
+ end
644
+
645
+ context ".method_missing" do
646
+ context "without a source class" do
647
+ it "raises a NoMethodError on missing methods" do
648
+ expect{Decorator.hello_world}.to raise_error NoMethodError
649
+ end
650
+ end
651
+
652
+ context "with a source class" do
653
+ it "delegates methods that exist on the source class" do
654
+ object_class = Class.new
655
+ object_class.stub hello_world: :delegated
656
+ Decorator.stub object_class: object_class
657
+
658
+ expect(Decorator.hello_world).to be :delegated
659
+ end
660
+
661
+ it "does not delegate methods that do not exist on the source class" do
662
+ Decorator.stub object_class: Class.new
663
+
664
+ expect{Decorator.hello_world}.to raise_error NoMethodError
665
+ end
666
+ end
667
+ end
668
+
669
+ describe "#respond_to?" do
670
+ it "returns true for its own methods" do
671
+ Decorator.class_eval{def hello_world; end}
672
+ decorator = Decorator.new(Model.new)
673
+
674
+ expect(decorator).to respond_to :hello_world
675
+ end
676
+
677
+ it "returns true for the object's methods" do
678
+ decorator = Decorator.new(double(hello_world: :delegated))
679
+
680
+ expect(decorator).to respond_to :hello_world
681
+ end
682
+
683
+ context "with include_private" do
684
+ it "returns true for its own private methods" do
685
+ Decorator.class_eval{private; def hello_world; end}
686
+ decorator = Decorator.new(Model.new)
687
+
688
+ expect(decorator.respond_to?(:hello_world, true)).to be_truthy
689
+ end
690
+
691
+ it "returns false for the object's private methods" do
692
+ object = Class.new{private; def hello_world; end}.new
693
+ decorator = Decorator.new(object)
694
+
695
+ expect(decorator.respond_to?(:hello_world, true)).to be_falsey
696
+ end
697
+ end
698
+ end
699
+
700
+ describe ".respond_to?" do
701
+ context "without a source class" do
702
+ it "returns true for its own class methods" do
703
+ Decorator.class_eval{def self.hello_world; end}
704
+
705
+ expect(Decorator).to respond_to :hello_world
706
+ end
707
+
708
+ it "returns false for other class methods" do
709
+ expect(Decorator).not_to respond_to :goodnight_moon
710
+ end
711
+ end
712
+
713
+ context "with a source class" do
714
+ it "returns true for its own class methods" do
715
+ Decorator.class_eval{def self.hello_world; end}
716
+ Decorator.stub object_class: Class.new
717
+
718
+ expect(Decorator).to respond_to :hello_world
719
+ end
720
+
721
+ it "returns true for the source's class methods" do
722
+ Decorator.stub object_class: double(hello_world: :delegated)
723
+
724
+ expect(Decorator).to respond_to :hello_world
725
+ end
726
+ end
727
+ end
728
+
729
+ describe "#respond_to_missing?" do
730
+ it "allows #method to be called on delegated methods" do
731
+ object = Class.new{def hello_world; end}.new
732
+ decorator = Decorator.new(object)
733
+
734
+ expect(decorator.method(:hello_world)).not_to be_nil
735
+ end
736
+ end
737
+
738
+ describe ".respond_to_missing?" do
739
+ it "allows .method to be called on delegated class methods" do
740
+ Decorator.stub object_class: double(hello_world: :delegated)
741
+
742
+ expect(Decorator.method(:hello_world)).not_to be_nil
743
+ end
744
+ end
745
+ end
746
+
747
+ describe "class spoofing" do
748
+ it "pretends to be a kind of the source class" do
749
+ decorator = Decorator.new(Model.new)
750
+
751
+ expect(decorator.kind_of?(Model)).to be_truthy
752
+ expect(decorator.is_a?(Model)).to be_truthy
753
+ end
754
+
755
+ it "is still a kind of its own class" do
756
+ decorator = Decorator.new(Model.new)
757
+
758
+ expect(decorator.kind_of?(Decorator)).to be_truthy
759
+ expect(decorator.is_a?(Decorator)).to be_truthy
760
+ end
761
+
762
+ it "pretends to be an instance of the source class" do
763
+ decorator = Decorator.new(Model.new)
764
+
765
+ expect(decorator.instance_of?(Model)).to be_truthy
766
+ end
767
+
768
+ it "is still an instance of its own class" do
769
+ decorator = Decorator.new(Model.new)
770
+
771
+ expect(decorator.instance_of?(Decorator)).to be_truthy
772
+ end
773
+ end
774
+
775
+ describe ".decorates_finders" do
776
+ protect_class Decorator
777
+
778
+ it "extends the Finders module" do
779
+ expect(Decorator).not_to be_a_kind_of Finders
780
+ Decorator.decorates_finders
781
+ expect(Decorator).to be_a_kind_of Finders
782
+ end
783
+ end
784
+
785
+ describe "Enumerable hash and equality functionality" do
786
+ describe "#uniq" do
787
+ it "removes duplicate objects with same decorator" do
788
+ object = Model.new
789
+ array = [Decorator.new(object), Decorator.new(object)]
790
+
791
+ expect(array.uniq.count).to eq(1)
792
+ end
793
+
794
+ it "separates different objects with identical decorators" do
795
+ array = [Decorator.new('foo'), Decorator.new('bar')]
796
+
797
+ expect(array.uniq.count).to eq(2)
798
+ end
799
+
800
+ it "separates identical objects with different decorators" do
801
+ object = Model.new
802
+ array = [Decorator.new(object), OtherDecorator.new(object)]
803
+
804
+ expect(array.uniq.count).to eq(2)
805
+ end
806
+
807
+ it "distinguishes between an objects and its decorated version" do
808
+ object = Model.new
809
+ array = [Decorator.new(object), object]
810
+
811
+ expect(array.uniq.count).to eq(2)
812
+ end
813
+ end
814
+ end
815
+ end
816
+ end