draper 3.0.0 → 4.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +55 -0
  3. data/CHANGELOG.md +46 -0
  4. data/Gemfile +11 -2
  5. data/README.md +43 -5
  6. data/bin/bundle +114 -0
  7. data/bin/rake +29 -0
  8. data/draper.gemspec +9 -8
  9. data/lib/draper.rb +3 -0
  10. data/lib/draper/automatic_delegation.rb +4 -2
  11. data/lib/draper/collection_decorator.rb +4 -2
  12. data/lib/draper/compatibility/global_id.rb +22 -0
  13. data/lib/draper/configuration.rb +8 -0
  14. data/lib/draper/decoratable.rb +6 -7
  15. data/lib/draper/decorated_association.rb +0 -2
  16. data/lib/draper/decorator.rb +19 -13
  17. data/lib/draper/delegation.rb +1 -1
  18. data/lib/draper/finders.rb +0 -1
  19. data/lib/draper/helper_proxy.rb +2 -2
  20. data/lib/draper/lazy_helpers.rb +1 -3
  21. data/lib/draper/query_methods.rb +23 -0
  22. data/lib/draper/query_methods/load_strategy.rb +21 -0
  23. data/lib/draper/test_case.rb +3 -3
  24. data/lib/draper/undecorate.rb +1 -1
  25. data/lib/draper/version.rb +1 -1
  26. data/lib/draper/view_context/build_strategy.rb +1 -3
  27. data/lib/draper/view_helpers.rb +5 -5
  28. data/lib/generators/draper/install_generator.rb +1 -1
  29. data/lib/generators/mini_test/decorator_generator.rb +1 -1
  30. data/lib/generators/rails/decorator_generator.rb +1 -1
  31. data/lib/generators/rspec/decorator_generator.rb +6 -4
  32. data/lib/generators/test_unit/decorator_generator.rb +7 -4
  33. data/spec/draper/collection_decorator_spec.rb +5 -6
  34. data/spec/draper/configuration_spec.rb +32 -8
  35. data/spec/draper/decoratable_spec.rb +24 -4
  36. data/spec/draper/decorated_association_spec.rb +0 -2
  37. data/spec/draper/decorator_spec.rb +36 -3
  38. data/spec/draper/draper_spec.rb +1 -0
  39. data/spec/draper/factory_spec.rb +0 -4
  40. data/spec/draper/query_methods/load_strategy_spec.rb +26 -0
  41. data/spec/draper/query_methods_spec.rb +70 -0
  42. data/spec/dummy/app/assets/config/manifest.js +3 -0
  43. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  44. data/spec/dummy/app/jobs/publish_post_job.rb +7 -0
  45. data/spec/dummy/app/models/application_record.rb +3 -0
  46. data/spec/dummy/app/models/post.rb +1 -1
  47. data/spec/dummy/config/application.rb +0 -3
  48. data/spec/dummy/config/environments/development.rb +2 -0
  49. data/spec/dummy/config/environments/production.rb +2 -0
  50. data/spec/dummy/config/environments/test.rb +4 -0
  51. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  52. data/spec/dummy/config/storage.yml +7 -0
  53. data/spec/dummy/db/migrate/20121019115657_create_posts.rb +1 -1
  54. data/spec/dummy/spec/jobs/publish_post_job_spec.rb +9 -0
  55. data/spec/dummy/spec/models/application_spec.rb +7 -0
  56. data/spec/dummy/spec/models/post_spec.rb +11 -2
  57. data/spec/generators/controller/controller_generator_spec.rb +1 -0
  58. data/spec/generators/decorator/decorator_generator_spec.rb +1 -1
  59. data/spec/integration/integration_spec.rb +1 -0
  60. data/spec/spec_helper.rb +9 -0
  61. metadata +72 -20
  62. data/.travis.yml +0 -9
@@ -1,9 +1,12 @@
1
1
  module TestUnit
2
- class DecoratorGenerator < ::Rails::Generators::NamedBase
3
- source_root File.expand_path('../templates', __FILE__)
2
+ module Generators
3
+ class DecoratorGenerator < ::Rails::Generators::NamedBase
4
+ source_root File.expand_path("templates", __dir__)
5
+ check_class_collision suffix: "DecoratorTest"
4
6
 
5
- def create_test_file
6
- template 'decorator_test.rb', File.join('test/decorators', class_path, "#{singular_name}_decorator_test.rb")
7
+ def create_test_file
8
+ template 'decorator_test.rb', File.join('test/decorators', class_path, "#{singular_name}_decorator_test.rb")
9
+ end
7
10
  end
8
11
  end
9
12
  end
@@ -7,7 +7,6 @@ module Draper
7
7
 
8
8
  describe "#initialize" do
9
9
  describe "options validation" do
10
-
11
10
  it "does not raise error on valid options" do
12
11
  valid_options = {with: Decorator, context: {}}
13
12
  expect{CollectionDecorator.new([], valid_options)}.not_to raise_error
@@ -61,10 +60,11 @@ module Draper
61
60
 
62
61
  context "when the collection has not yet been decorated" do
63
62
  it "does not trigger decoration" do
64
- decorator = CollectionDecorator.new([])
63
+ decorated = CollectionDecorator.new([]).tap(&:to_a)
64
+ undecorated = CollectionDecorator.new([])
65
65
 
66
- expect(decorator).not_to receive(:decorated_collection)
67
- decorator.context = {other: "context"}
66
+ expect(decorated.instance_variable_defined?(:@decorated_collection)).to be_truthy
67
+ expect(undecorated.instance_variable_defined?(:@decorated_collection)).to be_falsy
68
68
  end
69
69
 
70
70
  it "sets context after decoration is triggered" do
@@ -218,7 +218,7 @@ module Draper
218
218
  it "uses the custom class name" do
219
219
  decorator = ProductsDecorator.new([])
220
220
 
221
- expect(decorator.to_s).to match /ProductsDecorator/
221
+ expect(decorator.to_s).to match(/ProductsDecorator/)
222
222
  end
223
223
  end
224
224
  end
@@ -287,6 +287,5 @@ module Draper
287
287
  expect(decorator.replace([:foo, :bar])).to be decorator
288
288
  end
289
289
  end
290
-
291
290
  end
292
291
  end
@@ -6,20 +6,44 @@ module Draper
6
6
  Draper.configure { |config| expect(config).to be Draper }
7
7
  end
8
8
 
9
- it 'defaults default_controller to ApplicationController' do
10
- expect(Draper.default_controller).to be ApplicationController
9
+ describe '#default_controller' do
10
+ it 'defaults default_controller to ApplicationController' do
11
+ expect(Draper.default_controller).to be ApplicationController
12
+ end
13
+
14
+ it 'allows customizing default_controller through configure' do
15
+ default = Draper.default_controller
16
+
17
+ Draper.configure do |config|
18
+ config.default_controller = CustomController
19
+ end
20
+
21
+ expect(Draper.default_controller).to be CustomController
22
+
23
+ Draper.default_controller = default
24
+ end
11
25
  end
12
26
 
13
- it 'allows customizing default_controller through configure' do
14
- default = Draper.default_controller
27
+ describe '#default_query_methods_strategy' do
28
+ let!(:default) { Draper.default_query_methods_strategy }
29
+
30
+ subject { Draper.default_query_methods_strategy }
15
31
 
16
- Draper.configure do |config|
17
- config.default_controller = CustomController
32
+ context 'when there is no custom strategy' do
33
+ it { is_expected.to eq(:active_record) }
18
34
  end
19
35
 
20
- expect(Draper.default_controller).to be CustomController
36
+ context 'when using a custom strategy' do
37
+ before do
38
+ Draper.configure do |config|
39
+ config.default_query_methods_strategy = :mongoid
40
+ end
41
+ end
21
42
 
22
- Draper.default_controller = default
43
+ after { Draper.default_query_methods_strategy = default }
44
+
45
+ it { is_expected.to eq(:mongoid) }
46
+ end
23
47
  end
24
48
  end
25
49
  end
@@ -3,7 +3,6 @@ require 'support/shared_examples/decoratable_equality'
3
3
 
4
4
  module Draper
5
5
  describe Decoratable do
6
-
7
6
  describe "#decorate" do
8
7
  it "returns a decorator for self" do
9
8
  product = Product.new
@@ -11,7 +10,7 @@ module Draper
11
10
 
12
11
  expect(decorator).to be_a ProductDecorator
13
12
  expect(decorator.object).to be product
14
- end
13
+ end
15
14
 
16
15
  it "accepts context" do
17
16
  context = {some: "context"}
@@ -73,6 +72,16 @@ module Draper
73
72
  expect(Product).to receive(:decorator_class).and_return(:some_decorator)
74
73
  expect(product.decorator_class).to be :some_decorator
75
74
  end
75
+
76
+ it "specifies the class that #decorator_class was first called on (superclass)" do
77
+ person = Person.new
78
+ expect { person.decorator_class }.to raise_error(Draper::UninferrableDecoratorError, 'Could not infer a decorator for Person.')
79
+ end
80
+
81
+ it "specifies the class that #decorator_class was first called on (subclass)" do
82
+ child = Child.new
83
+ expect { child.decorator_class }.to raise_error(Draper::UninferrableDecoratorError, 'Could not infer a decorator for Child.')
84
+ end
76
85
  end
77
86
 
78
87
  describe "#==" do
@@ -205,11 +214,22 @@ module Draper
205
214
 
206
215
  context "when an unrelated NameError is thrown" do
207
216
  it "re-raises that error" do
208
- allow_any_instance_of(String).to receive(:constantize) { Draper::Base }
217
+ # Not related to safe_constantize behavior, we just want to raise a NameError inside the function
218
+ allow_any_instance_of(String).to receive(:safe_constantize) { Draper::Base }
209
219
  expect{Product.decorator_class}.to raise_error NameError, /Draper::Base/
210
220
  end
211
221
  end
212
- end
213
222
 
223
+ context "when an anonymous class is given" do
224
+ it "infers the decorator from a superclass" do
225
+ anonymous_class = Class.new(Product) do
226
+ def self.name
227
+ to_s
228
+ end
229
+ end
230
+ expect(anonymous_class.decorator_class).to be ProductDecorator
231
+ end
232
+ end
233
+ end
214
234
  end
215
235
  end
@@ -2,7 +2,6 @@ require 'spec_helper'
2
2
 
3
3
  module Draper
4
4
  describe DecoratedAssociation do
5
-
6
5
  describe "#initialize" do
7
6
  it "accepts valid options" do
8
7
  valid_options = {with: Decorator, scope: :foo, context: {}}
@@ -79,6 +78,5 @@ module Draper
79
78
  end
80
79
  end
81
80
  end
82
-
83
81
  end
84
82
  end
@@ -202,7 +202,8 @@ module Draper
202
202
 
203
203
  context "when an unrelated NameError is thrown" do
204
204
  it "re-raises that error" do
205
- allow_any_instance_of(String).to receive(:constantize) { SomethingThatDoesntExist }
205
+ # Not related to safe_constantize behavior, we just want to raise a NameError inside the function
206
+ allow_any_instance_of(String).to receive(:safe_constantize) { SomethingThatDoesntExist }
206
207
  expect{ProductDecorator.object_class}.to raise_error NameError, /SomethingThatDoesntExist/
207
208
  end
208
209
  end
@@ -225,7 +226,7 @@ module Draper
225
226
 
226
227
  describe '.collection_decorator_class' do
227
228
  it 'defaults to CollectionDecorator' do
228
- allow_any_instance_of(String).to receive(:constantize) { SomethingThatDoesntExist }
229
+ allow_any_instance_of(String).to receive(:safe_constantize) { nil }
229
230
  expect(ProductDecorator.collection_decorator_class).to be Draper::CollectionDecorator
230
231
  end
231
232
 
@@ -438,7 +439,7 @@ module Draper
438
439
  it "returns a detailed description of the decorator" do
439
440
  decorator = ProductDecorator.new(double)
440
441
 
441
- expect(decorator.inspect).to match /#<ProductDecorator:0x\h+ .+>/
442
+ expect(decorator.inspect).to match(/#<ProductDecorator:0x\h+ .+>/)
442
443
  end
443
444
 
444
445
  it "includes the object" do
@@ -663,6 +664,38 @@ module Draper
663
664
  expect{decorator.hello_world}.to raise_error NoMethodError
664
665
  expect(decorator.methods).not_to include :hello_world
665
666
  end
667
+
668
+ context 'when decorator overrides a public method defined on the object with a private' do
669
+ let(:decorator_class) do
670
+ Class.new(Decorator) do
671
+ private
672
+
673
+ def hello_world
674
+ 'hello world'
675
+ end
676
+ end
677
+ end
678
+
679
+ let(:object) { Class.new { def hello_world; end }.new }
680
+
681
+ it 'does not delegate the public method defined on the object' do
682
+ decorator = decorator_class.new(object)
683
+
684
+ expect{ decorator.hello_world }.to raise_error NoMethodError
685
+ end
686
+ end
687
+
688
+ context 'when delegated method has the same name as private method defined on another object' do
689
+ let(:decorator_class) { Class.new(Decorator) }
690
+ let(:object) { Class.new { def print; end }.new }
691
+
692
+ it 'delegates the public method defined on the object' do
693
+ decorator = decorator_class.new(object)
694
+
695
+ # `print` private method is defined on `Object`
696
+ expect{ decorator.print }.not_to raise_error
697
+ end
698
+ end
666
699
  end
667
700
 
668
701
  context ".method_missing" do
@@ -1,5 +1,6 @@
1
1
  require 'spec_helper'
2
2
  require 'support/shared_examples/view_helpers'
3
+ SimpleCov.command_name 'test:unit'
3
4
 
4
5
  module Draper
5
6
  describe Draper do
@@ -2,7 +2,6 @@ require 'spec_helper'
2
2
 
3
3
  module Draper
4
4
  describe Factory do
5
-
6
5
  describe "#initialize" do
7
6
  it "accepts valid options" do
8
7
  valid_options = {with: Decorator, context: {foo: "bar"}}
@@ -88,11 +87,9 @@ module Draper
88
87
  end
89
88
  end
90
89
  end
91
-
92
90
  end
93
91
 
94
92
  describe Factory::Worker do
95
-
96
93
  describe "#call" do
97
94
  it "calls the decorator method" do
98
95
  object = double
@@ -246,6 +243,5 @@ module Draper
246
243
  end
247
244
  end
248
245
  end
249
-
250
246
  end
251
247
  end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+ require 'active_record'
3
+
4
+ module Draper
5
+ module QueryMethods
6
+ describe LoadStrategy do
7
+ describe '#new' do
8
+ subject { described_class.new(:active_record) }
9
+
10
+ it { is_expected.to be_an_instance_of(LoadStrategy::ActiveRecord) }
11
+ end
12
+ end
13
+
14
+ describe LoadStrategy::ActiveRecord do
15
+ describe '#allowed?' do
16
+ it 'checks whether or not ActiveRecord::Relation::VALUE_METHODS has the given method' do
17
+ allow(::ActiveRecord::Relation::VALUE_METHODS).to receive(:include?)
18
+
19
+ described_class.new.allowed? :foo
20
+
21
+ expect(::ActiveRecord::Relation::VALUE_METHODS).to have_received(:include?).with(:foo)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+ require_relative '../dummy/app/decorators/post_decorator'
3
+
4
+ Post = Struct.new(:id) { }
5
+
6
+ module Draper
7
+ describe QueryMethods do
8
+ let(:fake_strategy) { instance_double(QueryMethods::LoadStrategy::ActiveRecord) }
9
+
10
+ before { allow(QueryMethods::LoadStrategy).to receive(:new).and_return(fake_strategy) }
11
+
12
+ describe '#method_missing' do
13
+ let(:collection) { [ Post.new, Post.new ] }
14
+ let(:collection_context) { { user: 'foo' } }
15
+ let(:collection_decorator) { PostDecorator.decorate_collection(collection, context: collection_context) }
16
+
17
+ context 'when strategy allows collection to call the method' do
18
+ let(:results) { spy(:results) }
19
+
20
+ before do
21
+ allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(true)
22
+ allow(collection).to receive(:send).with(:some_query_method).and_return(results)
23
+ end
24
+
25
+ it 'calls the method on the collection and decorate it results' do
26
+ collection_decorator.some_query_method
27
+
28
+ expect(results).to have_received(:decorate)
29
+ end
30
+
31
+ it 'calls the method on the collection and keeps the decoration options' do
32
+ collection_decorator.some_query_method
33
+
34
+ expect(results).to have_received(:decorate).with({ context: collection_context, with: PostDecorator })
35
+ end
36
+ end
37
+
38
+ context 'when strategy does not allow collection to call the method' do
39
+ before { allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(false) }
40
+
41
+ it 'raises NoMethodError' do
42
+ expect { collection_decorator.some_query_method }.to raise_exception(NoMethodError)
43
+ end
44
+ end
45
+ end
46
+
47
+ describe "#respond_to?" do
48
+ let(:collection) { [ Post.new, Post.new ] }
49
+ let(:collection_decorator) { PostDecorator.decorate_collection(collection) }
50
+
51
+ subject { collection_decorator.respond_to?(:some_query_method) }
52
+
53
+ context 'when strategy allows collection to call the method' do
54
+ before do
55
+ allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(true)
56
+ end
57
+
58
+ it { is_expected.to eq(true) }
59
+ end
60
+
61
+ context 'when strategy does not allow collection to call the method' do
62
+ before do
63
+ allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(false)
64
+ end
65
+
66
+ it { is_expected.to eq(false) }
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,3 @@
1
+ //= link_tree ../images
2
+ //= link_directory ../javascripts .js
3
+ //= link_directory ../stylesheets .css
@@ -0,0 +1,2 @@
1
+ class ApplicationController < ActionController::Base
2
+ end
@@ -0,0 +1,7 @@
1
+ class PublishPostJob < ActiveJob::Base
2
+ queue_as :default
3
+
4
+ def perform(post)
5
+ post.save!
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
@@ -1,3 +1,3 @@
1
- class Post < ActiveRecord::Base
1
+ class Post < ApplicationRecord
2
2
  # attr_accessible :title, :body
3
3
  end
@@ -38,9 +38,6 @@ module Dummy
38
38
  # Configure the default encoding used in templates for Ruby 1.9.
39
39
  config.encoding = "utf-8"
40
40
 
41
- # Configure sensitive parameters which will be filtered from the log file.
42
- config.filter_parameters += [:password]
43
-
44
41
  # Enable escaping HTML in JSON.
45
42
  config.active_support.escape_html_entities_in_json = true
46
43
 
@@ -30,4 +30,6 @@ Dummy::Application.configure do
30
30
 
31
31
  # Expands the lines which load the assets
32
32
  # config.assets.debug = true
33
+
34
+ config.active_storage.service = :local
33
35
  end
@@ -54,4 +54,6 @@ Dummy::Application.configure do
54
54
  # Log the query plan for queries taking more than this (works
55
55
  # with SQLite, MySQL, and PostgreSQL)
56
56
  # config.active_record.auto_explain_threshold_in_seconds = 0.5
57
+
58
+ config.active_storage.service = :local
57
59
  end
@@ -28,4 +28,8 @@ Dummy::Application.configure do
28
28
  config.active_support.deprecation = :stderr
29
29
 
30
30
  config.eager_load = false
31
+
32
+ config.active_job.queue_adapter = :test
33
+
34
+ config.active_storage.service = :test
31
35
  end