garner 0.4.5 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +35 -0
  5. data/.travis.yml +13 -0
  6. data/CHANGELOG.md +130 -0
  7. data/CONTRIBUTING.md +118 -0
  8. data/Gemfile +3 -0
  9. data/README.md +1 -0
  10. data/Rakefile +39 -0
  11. data/UPGRADING.md +118 -0
  12. data/garner.gemspec +44 -0
  13. data/lib/garner.rb +21 -21
  14. data/lib/garner/cache.rb +13 -6
  15. data/lib/garner/cache/binding.rb +6 -14
  16. data/lib/garner/cache/context.rb +11 -12
  17. data/lib/garner/cache/identity.rb +1 -1
  18. data/lib/garner/config.rb +12 -7
  19. data/lib/garner/mixins/active_record.rb +3 -3
  20. data/lib/garner/mixins/active_record/base.rb +2 -2
  21. data/lib/garner/mixins/mongoid.rb +4 -4
  22. data/lib/garner/mixins/mongoid/document.rb +8 -12
  23. data/lib/garner/mixins/mongoid/identity.rb +5 -6
  24. data/lib/garner/mixins/rack.rb +1 -2
  25. data/lib/garner/strategies/binding/invalidation/base.rb +2 -4
  26. data/lib/garner/strategies/binding/invalidation/binding_index.rb +1 -3
  27. data/lib/garner/strategies/binding/invalidation/touch.rb +0 -2
  28. data/lib/garner/strategies/binding/key/base.rb +1 -3
  29. data/lib/garner/strategies/binding/key/binding_index.rb +3 -4
  30. data/lib/garner/strategies/binding/key/cache_key.rb +0 -2
  31. data/lib/garner/strategies/binding/key/safe_cache_key.rb +2 -3
  32. data/lib/garner/strategies/context/key/base.rb +1 -3
  33. data/lib/garner/strategies/context/key/caller.rb +9 -12
  34. data/lib/garner/strategies/context/key/jsonp.rb +3 -6
  35. data/lib/garner/strategies/context/key/request_get.rb +2 -4
  36. data/lib/garner/strategies/context/key/request_path.rb +1 -3
  37. data/lib/garner/strategies/context/key/request_post.rb +2 -4
  38. data/lib/garner/version.rb +1 -1
  39. data/spec/garner/cache/context_spec.rb +38 -0
  40. data/spec/garner/cache/identity_spec.rb +68 -0
  41. data/spec/garner/cache_spec.rb +49 -0
  42. data/spec/garner/config_spec.rb +17 -0
  43. data/spec/garner/mixins/mongoid/document_spec.rb +80 -0
  44. data/spec/garner/mixins/mongoid/identity_spec.rb +140 -0
  45. data/spec/garner/mixins/rack_spec.rb +48 -0
  46. data/spec/garner/strategies/binding/invalidation/binding_index_spec.rb +14 -0
  47. data/spec/garner/strategies/binding/invalidation/touch_spec.rb +23 -0
  48. data/spec/garner/strategies/binding/key/binding_index_spec.rb +245 -0
  49. data/spec/garner/strategies/binding/key/cache_key_spec.rb +29 -0
  50. data/spec/garner/strategies/binding/key/safe_cache_key_spec.rb +61 -0
  51. data/spec/garner/strategies/context/key/caller_spec.rb +106 -0
  52. data/spec/garner/strategies/context/key/jsonp_spec.rb +22 -0
  53. data/spec/garner/strategies/context/key/request_get_spec.rb +33 -0
  54. data/spec/garner/strategies/context/key/request_path_spec.rb +28 -0
  55. data/spec/garner/strategies/context/key/request_post_spec.rb +34 -0
  56. data/spec/garner/version_spec.rb +11 -0
  57. data/spec/integration/active_record_spec.rb +43 -0
  58. data/spec/integration/grape_spec.rb +33 -0
  59. data/spec/integration/mongoid_spec.rb +355 -0
  60. data/spec/integration/rack_spec.rb +77 -0
  61. data/spec/integration/sinatra_spec.rb +29 -0
  62. data/spec/performance/strategy_benchmark.rb +59 -0
  63. data/spec/performance/support/benchmark_context.rb +31 -0
  64. data/spec/performance/support/benchmark_context_wrapper.rb +67 -0
  65. data/spec/shared/binding_invalidation_strategy.rb +17 -0
  66. data/spec/shared/binding_key_strategy.rb +35 -0
  67. data/spec/shared/conditional_get.rb +48 -0
  68. data/spec/shared/context_key_strategy.rb +24 -0
  69. data/spec/spec_helper.rb +24 -0
  70. data/spec/spec_support.rb +5 -0
  71. data/spec/support/active_record.rb +36 -0
  72. data/spec/support/cache.rb +15 -0
  73. data/spec/support/garner.rb +5 -0
  74. data/spec/support/mongoid.rb +71 -0
  75. metadata +155 -157
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe Garner::Strategies::Binding::Key::CacheKey do
4
+
5
+ before(:each) do
6
+ @mock = double('model')
7
+ @mock.stub(:cache_key) { 'mocks/4' }
8
+ end
9
+
10
+ subject { Garner::Strategies::Binding::Key::CacheKey }
11
+
12
+ it_behaves_like 'Garner::Strategies::Binding::Key strategy' do
13
+ let(:known_bindings) { [@mock] }
14
+ let(:unknown_bindings) { [@mock.class] }
15
+ end
16
+
17
+ describe 'apply' do
18
+ it "returns the object's cache key, or nil" do
19
+ subject.apply(@mock).should eq 'mocks/4'
20
+ end
21
+ end
22
+
23
+ context 'with real objects' do
24
+ it_behaves_like 'Garner::Strategies::Binding::Key strategy' do
25
+ let(:known_bindings) { [Activist.create, Monger.create] }
26
+ let(:unknown_bindings) { [Monger.identify('m1'), Monger] }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe Garner::Strategies::Binding::Key::SafeCacheKey do
4
+
5
+ before(:each) do
6
+ @new_mock = double('model')
7
+ @new_mock.stub(:cache_key) { 'mocks/4' }
8
+ @persisted_mock = double('model')
9
+ @time_dot_now = Time.now
10
+ @persisted_mock.stub(:cache_key) { "mocks/4-#{@time_dot_now.utc.to_s(:number)}" }
11
+ @persisted_mock.stub(:updated_at) { @time_dot_now }
12
+ end
13
+
14
+ subject { Garner::Strategies::Binding::Key::SafeCacheKey }
15
+
16
+ it_behaves_like 'Garner::Strategies::Binding::Key strategy' do
17
+ let(:known_bindings) { [@persisted_mock] }
18
+ let(:unknown_bindings) { [@new_mock] }
19
+ end
20
+
21
+ describe 'apply' do
22
+ it "returns the object's cache key + milliseconds if defined" do
23
+ timestamp = @time_dot_now.utc.to_s(:number)
24
+ subject.apply(@persisted_mock).should =~ /^mocks\/4-#{timestamp}.[0-9]{10}$/
25
+ end
26
+
27
+ it 'returns nil if :cache_key is undefined or nil' do
28
+ @persisted_mock.unstub(:cache_key)
29
+ subject.apply(@persisted_mock).should be_nil
30
+ @persisted_mock.stub(:cache_key) { nil }
31
+ subject.apply(@persisted_mock).should be_nil
32
+ end
33
+
34
+ it 'returns nil if :updated_at is undefined or nil' do
35
+ @persisted_mock.unstub(:updated_at)
36
+ subject.apply(@persisted_mock).should be_nil
37
+ @persisted_mock.stub(:updated_at) { nil }
38
+ subject.apply(@persisted_mock).should be_nil
39
+ end
40
+ end
41
+
42
+ context 'with real objects' do
43
+ before(:each) do
44
+ Garner.configure do |config|
45
+ config.mongoid_identity_fields = [:_id, :_slugs]
46
+ end
47
+
48
+ @monger = Monger.create(name: 'M1')
49
+ @food = Food.create(name: 'F1')
50
+ end
51
+
52
+ it_behaves_like 'Garner::Strategies::Binding::Key strategy' do
53
+ let(:known_bindings) do
54
+ [Activist.create, @monger, Monger.identify(@monger.id), Monger.identify('m1'), Monger]
55
+ end
56
+ let(:unknown_bindings) do
57
+ [Monger.identify('m2'), Food.identify(nil), Monger.new, Activist.new]
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,106 @@
1
+ require 'spec_helper'
2
+
3
+ describe Garner::Strategies::Context::Key::Caller do
4
+ before(:each) do
5
+ @cache_identity = Garner::Cache::Identity.new
6
+ @mock_context = double('object')
7
+ end
8
+
9
+ subject { Garner::Strategies::Context::Key::Caller }
10
+
11
+ it_behaves_like 'Garner::Strategies::Context::Key strategy'
12
+
13
+ it 'ignores nil caller' do
14
+ @mock_context.stub(:caller) { nil }
15
+ subject.apply(@cache_identity, @mock_context)
16
+ @cache_identity.key_hash[:caller].should be_nil
17
+ end
18
+
19
+ it 'ignores nil caller location' do
20
+ @mock_context.stub(:caller) { [nil] }
21
+ subject.apply(@cache_identity, @mock_context)
22
+ @cache_identity.key_hash[:caller].should be_nil
23
+ end
24
+
25
+ it 'ignores blank caller location' do
26
+ @mock_context.stub(:caller) { [''] }
27
+ subject.apply(@cache_identity, @mock_context)
28
+ @cache_identity.key_hash[:caller].should be_nil
29
+ end
30
+
31
+ context 'with default Garner.config.caller_root' do
32
+ before(:each) do
33
+ raw_gemfile_parent = File.join(__FILE__, '..', '..', '..', '..', '..', '..')
34
+ @gemfile_root = Pathname.new(raw_gemfile_parent).realpath.to_s
35
+ end
36
+
37
+ it 'sets default_root to the nearest ancestor with a Gemfile' do
38
+ subject.default_root.should eq @gemfile_root
39
+ end
40
+
41
+ it 'sets Garner.config.caller_root to the nearest ancestor with a Gemfile' do
42
+ Garner.config.caller_root.should eq @gemfile_root
43
+ end
44
+
45
+ it 'sets an appropriate value for :caller' do
46
+ truncated = __FILE__.gsub(@gemfile_root + File::SEPARATOR, '')
47
+ subject.apply(@cache_identity, self)
48
+ @cache_identity.key_hash[:caller].should eq "#{truncated}:#{__LINE__ - 1}"
49
+ end
50
+ end
51
+
52
+ context 'with Rails.root defined' do
53
+ before(:each) do
54
+ class Rails
55
+ end
56
+ Rails.stub(:root) { Pathname.new(File.dirname(__FILE__)) }
57
+ end
58
+
59
+ it 'sets default_root to Rails.root' do
60
+ subject.default_root.should eq ::Rails.root.realpath.to_s
61
+ end
62
+
63
+ it 'sets Garner.config.caller_root to Rails.root' do
64
+ Garner.config.caller_root.should eq ::Rails.root.realpath.to_s
65
+ end
66
+
67
+ it 'sets an appropriate value for :caller' do
68
+ truncated = File.basename(__FILE__)
69
+ subject.apply(@cache_identity, self)
70
+ @cache_identity.key_hash[:caller].should eq "#{truncated}:#{__LINE__ - 1}"
71
+ end
72
+ end
73
+
74
+ context 'with custom Garner.config.caller_root' do
75
+ before(:each) do
76
+ Garner.configure do |config|
77
+ config.caller_root = File.dirname(__FILE__)
78
+ end
79
+ end
80
+
81
+ it 'sets an appropriate value for :caller' do
82
+ truncated = File.basename(__FILE__)
83
+ subject.apply(@cache_identity, self)
84
+ @cache_identity.key_hash[:caller].should eq "#{truncated}:#{__LINE__ - 1}"
85
+ end
86
+ end
87
+
88
+ context 'with Garner.config.caller_root unset' do
89
+ before(:each) do
90
+ Garner.configure do |config|
91
+ config.caller_root = nil
92
+ end
93
+ end
94
+
95
+ it 'sets an appropriate value for :caller' do
96
+ subject.apply(@cache_identity, self)
97
+ @cache_identity.key_hash[:caller].should eq "#{__FILE__}:#{__LINE__ - 1}"
98
+ end
99
+
100
+ it "doesn't require ActiveSupport" do
101
+ String.any_instance.stub(:blank?) { fail NoMethodError.new }
102
+ subject.apply(@cache_identity, self)
103
+ @cache_identity.key_hash[:caller].should eq "#{__FILE__}:#{__LINE__ - 1}"
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Garner::Strategies::Context::Key::Jsonp do
4
+ before(:each) do
5
+ @cache_identity = Garner::Cache::Identity.new
6
+ @request = Rack::Request.new('REQUEST_METHOD' => 'GET', 'QUERY_STRING' => 'callback=jQuery21435&_=34234')
7
+
8
+ @mock_context = double('object')
9
+ @mock_context.stub(:request) { @request }
10
+ end
11
+
12
+ subject { Garner::Strategies::Context::Key::Jsonp }
13
+
14
+ it_behaves_like 'Garner::Strategies::Context::Key strategy'
15
+
16
+ it 'removes JSONP params from the key hash' do
17
+ request_get = Garner::Strategies::Context::Key::RequestGet
18
+ applied_identity = request_get.apply(@cache_identity, @mock_context)
19
+ subject.apply(applied_identity, @mock_context)
20
+ @cache_identity.key_hash[:request_params].should eq({})
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ describe Garner::Strategies::Context::Key::RequestGet do
4
+ %w(GET HEAD).each do |method|
5
+ context method do
6
+
7
+ before(:each) do
8
+ @cache_identity = Garner::Cache::Identity.new
9
+ @request = Rack::Request.new('REQUEST_METHOD' => method, 'QUERY_STRING' => 'foo=bar')
10
+
11
+ @mock_context = double('object')
12
+ @mock_context.stub(:request) { @request }
13
+ end
14
+
15
+ subject { Garner::Strategies::Context::Key::RequestGet }
16
+
17
+ it_behaves_like 'Garner::Strategies::Context::Key strategy'
18
+
19
+ it 'adds :request_params to the key' do
20
+ subject.apply(@cache_identity, @mock_context)
21
+ @cache_identity.key_hash[:request_params].should eq('foo' => 'bar')
22
+ end
23
+
24
+ it 'appends to an existing key hash' do
25
+ @cache_identity.key(x: :y)
26
+ subject.apply(@cache_identity, @mock_context).key_hash.should eq(
27
+ x: :y,
28
+ request_params: { 'foo' => 'bar' }
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe Garner::Strategies::Context::Key::RequestPath do
4
+ before(:each) do
5
+ @cache_identity = Garner::Cache::Identity.new
6
+ @request = Rack::Request.new('PATH_INFO' => '/foo')
7
+
8
+ @mock_context = double('object')
9
+ @mock_context.stub(:request) { @request }
10
+ end
11
+
12
+ subject { Garner::Strategies::Context::Key::RequestPath }
13
+
14
+ it_behaves_like 'Garner::Strategies::Context::Key strategy'
15
+
16
+ it 'adds :request_params to the key' do
17
+ subject.apply(@cache_identity, @mock_context)
18
+ @cache_identity.key_hash[:request_path].should eq '/foo'
19
+ end
20
+
21
+ it 'appends to an existing key hash' do
22
+ @cache_identity.key(x: :y)
23
+ subject.apply(@cache_identity, @mock_context).key_hash.should eq(
24
+ x: :y,
25
+ request_path: '/foo'
26
+ )
27
+ end
28
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe Garner::Strategies::Context::Key::RequestPost do
4
+ before(:each) do
5
+ @cache_identity = Garner::Cache::Identity.new
6
+ @request = Rack::Request.new(
7
+ Rack::MockRequest.env_for(
8
+ '/?foo=quux',
9
+ 'REQUEST_METHOD' => 'POST',
10
+ :input => 'foo=bar'
11
+ )
12
+ )
13
+
14
+ @mock_context = double('object')
15
+ @mock_context.stub(:request) { @request }
16
+ end
17
+
18
+ subject { Garner::Strategies::Context::Key::RequestPost }
19
+
20
+ it_behaves_like 'Garner::Strategies::Context::Key strategy'
21
+
22
+ it 'adds :request_params to the key' do
23
+ subject.apply(@cache_identity, @mock_context)
24
+ @cache_identity.key_hash[:request_params].should eq('foo' => 'bar')
25
+ end
26
+
27
+ it 'appends to an existing key hash' do
28
+ @cache_identity.key(x: :y)
29
+ subject.apply(@cache_identity, @mock_context).key_hash.should eq(
30
+ x: :y,
31
+ request_params: { 'foo' => 'bar' }
32
+ )
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe Garner::VERSION do
4
+ subject do
5
+ Garner::VERSION
6
+ end
7
+ it 'is valid' do
8
+ subject.should_not be_nil
9
+ (!!Gem::Version.correct?(subject)).should be_truthy
10
+ end
11
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'ActiveRecord integration' do
4
+ context 'using the Garner::Strategies::Binding::Key::CacheKey strategy' do
5
+
6
+ describe 'cache key generation' do
7
+ subject { Garner::Strategies::Binding::Key::CacheKey }
8
+
9
+ it_behaves_like 'Garner::Strategies::Binding::Key strategy' do
10
+ let(:known_bindings) { [Activist.create, Activist.new] }
11
+ let(:unknown_bindings) { [Activist] }
12
+ end
13
+
14
+ it "returns the object's cache key, or nil" do
15
+ new_activist = Activist.new
16
+ subject.apply(new_activist).should eq 'activists/new'
17
+
18
+ persisted_activist = Activist.create
19
+ timestamp = persisted_activist.updated_at.utc.to_s(persisted_activist.cache_timestamp_format)
20
+ expected_key = "activists/#{persisted_activist.id}-#{timestamp}"
21
+ subject.apply(persisted_activist).should eq expected_key
22
+ end
23
+ end
24
+
25
+ describe 'garner_cache_key' do
26
+ context 'instance' do
27
+ subject { Activist.create }
28
+
29
+ it 'returns a non-nil cache_key' do
30
+ subject.garner_cache_key.should_not be_nil
31
+ end
32
+ end
33
+
34
+ context 'class' do
35
+ subject { Activist }
36
+
37
+ it 'should not ' do
38
+ expect { subject.garner_cache_key }.not_to raise_error
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require 'garner/mixins/rack'
3
+ require 'grape'
4
+
5
+ describe 'Grape integration' do
6
+ class TestCachebuster < Grape::Middleware::Base
7
+ def after
8
+ @app_response[1]['Expires'] = Time.at(0).utc.to_s
9
+ @app_response
10
+ end
11
+ end
12
+
13
+ let(:app) do
14
+ class TestGrapeApp < Grape::API
15
+ helpers Garner::Mixins::Rack
16
+ use Rack::ConditionalGet
17
+ use Rack::ETag
18
+ use TestCachebuster
19
+
20
+ format :json
21
+
22
+ get '/' do
23
+ garner do
24
+ { meaning_of_life: 42 }.to_json
25
+ end
26
+ end
27
+ end
28
+
29
+ TestGrapeApp.new
30
+ end
31
+
32
+ it_behaves_like 'Rack::ConditionalGet server'
33
+ end
@@ -0,0 +1,355 @@
1
+ require 'spec_helper'
2
+ require 'garner/mixins/mongoid'
3
+
4
+ describe 'Mongoid integration' do
5
+ before(:each) do
6
+ @app = Class.new.tap do |app|
7
+ app.send(:extend, Garner::Cache::Context)
8
+ end
9
+ end
10
+
11
+ {
12
+ # Garner::Strategies::Binding::Key::SafeCacheKey => Garner::Strategies::Binding::Invalidation::Touch,
13
+ Garner::Strategies::Binding::Key::BindingIndex => Garner::Strategies::Binding::Invalidation::BindingIndex
14
+ }.each do |key_strategy, invalidation_strategy|
15
+ context "using #{key_strategy} with #{invalidation_strategy}" do
16
+ before(:each) do
17
+ Garner.configure do |config|
18
+ config.binding_key_strategy = key_strategy
19
+ config.binding_invalidation_strategy = invalidation_strategy
20
+ end
21
+ end
22
+
23
+ describe 'end-to-end caching and invalidation' do
24
+ context 'binding at the instance level' do
25
+ before(:each) do
26
+ @object = Monger.create!(name: 'M1')
27
+ end
28
+
29
+ describe 'garnered_find' do
30
+ before(:each) do
31
+ Garner.configure do |config|
32
+ config.mongoid_identity_fields = [:_id, :_slugs]
33
+ end
34
+ end
35
+
36
+ it 'caches one copy across all callers' do
37
+ Monger.stub(:find) { @object }
38
+ Monger.should_receive(:find).once
39
+ 2.times { Monger.garnered_find('m1') }
40
+ end
41
+
42
+ it 'returns the instance requested' do
43
+ Monger.garnered_find('m1').should eq @object
44
+ end
45
+
46
+ it 'can be called with an array of one object, and will return an array' do
47
+ Monger.garnered_find(['m1']).should eq [@object]
48
+ end
49
+
50
+ it 'is invalidated on changing identity field' do
51
+ Monger.garnered_find('m1').name.should eq 'M1'
52
+ @object.update_attributes!(name: 'M2')
53
+ Monger.garnered_find('m1').name.should eq 'M2'
54
+ end
55
+
56
+ it 'is invalidated on destruction' do
57
+ Monger.garnered_find('m1').name.should eq 'M1'
58
+ @object.destroy
59
+ Monger.garnered_find('m1').should be_nil
60
+ end
61
+
62
+ context 'with case-insensitive find' do
63
+ before(:each) do
64
+ find_method = Monger.method(:find)
65
+ Monger.stub(:find) do |param|
66
+ find_method.call(param.to_s.downcase)
67
+ end
68
+ end
69
+
70
+ it 'does not cache a nil identity' do
71
+ Monger.garnered_find('M1').should eq @object
72
+ Monger.garnered_find('foobar').should be_nil
73
+ end
74
+ end
75
+
76
+ context 'on finding multiple objects' do
77
+ before(:each) do
78
+ @object2 = Monger.create!(name: 'M2')
79
+ end
80
+
81
+ it 'returns the instances requested' do
82
+ Monger.garnered_find('m1', 'm2').should eq [@object, @object2]
83
+ end
84
+
85
+ it 'can take an array' do
86
+ Monger.garnered_find(%w(m1 m2)).should eq [@object, @object2]
87
+ end
88
+
89
+ it 'is invalidated when one of the objects is changed' do
90
+ Monger.garnered_find('m1', 'm2').should eq [@object, @object2]
91
+ @object2.update_attributes!(name: 'M3')
92
+ Monger.garnered_find('m1', 'm2').last.name.should eq 'M3'
93
+ end
94
+
95
+ it 'is invalidated when one of the objects is changed, passed as an array' do
96
+ Monger.garnered_find(%w(m1 m2)).should eq [@object, @object2]
97
+ @object2.update_attributes!(name: 'M3')
98
+ Monger.garnered_find(%w(m1 m2)).last.name.should eq 'M3'
99
+ end
100
+
101
+ it 'does not return a match when the objects cannot be found' do
102
+ Monger.garnered_find('m3').should be_nil
103
+ Monger.garnered_find('m3', 'm4').should eq []
104
+ Monger.garnered_find(%w(m3 m4)).should eq []
105
+ end
106
+
107
+ it 'does not return a match when some of the objects cannot be found, and returns those that can' do
108
+ Monger.garnered_find('m1', 'm2').should eq [@object, @object2]
109
+ @object2.destroy
110
+ Monger.garnered_find('m1', 'm2').should eq [@object]
111
+ end
112
+
113
+ it 'correctly returns a single object when first asked for a single object as an array' do
114
+ Monger.garnered_find(['m1']).should eq [@object]
115
+ Monger.garnered_find('m1').should eq @object
116
+ end
117
+
118
+ it 'correctly returns an array of a single object, when first asked for a single object' do
119
+ Monger.garnered_find('m1').should eq @object
120
+ Monger.garnered_find(['m1']).should eq [@object]
121
+ end
122
+
123
+ it 'caches properly when called with an array' do
124
+ Monger.stub(:find) { @object }
125
+ Monger.should_receive(:find).once
126
+ 2.times { Monger.garnered_find(%w(m1 m2)) }
127
+ end
128
+
129
+ end
130
+ end
131
+
132
+ [:find, :identify].each do |selection_method|
133
+ context "binding via #{selection_method}" do
134
+ let(:cached_object_namer) do
135
+ lambda do
136
+ binding = Monger.send(selection_method, @object.id)
137
+ object = Monger.find(@object.id)
138
+ @app.garner.bind(binding) { object.name }
139
+ end
140
+ end
141
+
142
+ it 'invalidates on update' do
143
+ cached_object_namer.call.should eq 'M1'
144
+ @object.update_attributes!(name: 'M2')
145
+ cached_object_namer.call.should eq 'M2'
146
+ end
147
+
148
+ it 'invalidates on destroy' do
149
+ cached_object_namer.call.should eq 'M1'
150
+ @object.destroy
151
+ cached_object_namer.should raise_error
152
+ end
153
+
154
+ it 'invalidates by explicit call to invalidate_garner_caches' do
155
+ cached_object_namer.call.should eq 'M1'
156
+ if Mongoid.mongoid3?
157
+ @object.set(:name, 'M2')
158
+ else
159
+ @object.set(name: 'M2')
160
+ end
161
+ @object.invalidate_garner_caches
162
+ cached_object_namer.call.should eq 'M2'
163
+ end
164
+
165
+ it 'does not invalidate results for other like-classed objects' do
166
+ cached_object_namer.call.should eq 'M1'
167
+ if Mongoid.mongoid3?
168
+ @object.set(:name, 'M2')
169
+ else
170
+ @object.set(name: 'M2')
171
+ end
172
+ new_monger = Monger.create!(name: 'M3')
173
+ new_monger.update_attributes!(name: 'M4')
174
+ new_monger.destroy
175
+
176
+ cached_object_namer.call.should eq 'M1'
177
+ end
178
+
179
+ context 'with racing destruction' do
180
+ before(:each) do
181
+ # Define two Mongoid objects for the race
182
+ @monger1, @monger2 = 2.times.map { Monger.find(@object.id) }
183
+ end
184
+
185
+ it 'invalidates caches properly (Type I)' do
186
+ cached_object_namer.call
187
+ @monger1.remove
188
+ if Mongoid.mongoid3?
189
+ @monger2.set(:name, 'M2')
190
+ else
191
+ @monger2.set(name: 'M2')
192
+ end
193
+ @monger1.destroy
194
+ @monger2.save
195
+ cached_object_namer.should raise_error
196
+ end
197
+
198
+ it 'invalidates caches properly (Type II)' do
199
+ cached_object_namer.call
200
+ if Mongoid.mongoid3?
201
+ @monger2.set(:name, 'M2')
202
+ else
203
+ @monger2.set(name: 'M2')
204
+ end
205
+ @monger1.remove
206
+ @monger2.save
207
+ @monger1.destroy
208
+ cached_object_namer.should raise_error
209
+ end
210
+ end
211
+
212
+ context 'with inheritance' do
213
+ before(:each) do
214
+ @monger = Monger.create!(name: 'M1')
215
+ @object = Cheese.create!(name: 'Swiss', monger: @monger)
216
+ end
217
+
218
+ let(:cached_object_namer) do
219
+ lambda do
220
+ binding = Food.send(selection_method, @object.id)
221
+ object = Food.find(@object.id)
222
+ @app.garner.bind(binding) { object.name }
223
+ end
224
+ end
225
+
226
+ it 'binds to the correct object' do
227
+ cached_object_namer.call.should eq 'Swiss'
228
+ @object.update_attributes!(name: 'Havarti')
229
+ cached_object_namer.call.should eq 'Havarti'
230
+ end
231
+ end
232
+
233
+ context 'with an embedded document' do
234
+ before(:each) do
235
+ @monger = Monger.create!(name: 'M1')
236
+ @fish = @monger.create_fish(name: 'Trout')
237
+ end
238
+
239
+ let(:cached_object_namer) do
240
+ lambda do
241
+ found = Monger.where('fish._id' => @fish.id).first.fish
242
+ @app.garner.bind(found) { found.name }
243
+ end
244
+ end
245
+
246
+ it 'binds to the correct object' do
247
+ cached_object_namer.call.should eq 'Trout'
248
+ @fish.update_attributes!(name: 'Sockeye')
249
+ cached_object_namer.call.should eq 'Sockeye'
250
+ end
251
+
252
+ context 'with :invalidate_mongoid_root = true' do
253
+ before(:each) do
254
+ Garner.configure do |config|
255
+ config.invalidate_mongoid_root = true
256
+ end
257
+ end
258
+
259
+ let(:root_cached_object_namer) do
260
+ lambda do
261
+ binding = Monger.send(selection_method, @monger.id)
262
+ @app.garner.bind(binding) { @monger.fish.name }
263
+ end
264
+ end
265
+
266
+ it 'invalidates the root document' do
267
+ root_cached_object_namer.call.should eq 'Trout'
268
+ @fish.update_attributes!(name: 'Sockeye')
269
+ root_cached_object_namer.call.should eq 'Sockeye'
270
+ end
271
+ end
272
+ end
273
+
274
+ context 'with multiple identities' do
275
+ before(:each) do
276
+ Garner.configure do |config|
277
+ config.mongoid_identity_fields = [:_id, :_slugs]
278
+ end
279
+ end
280
+
281
+ let(:cached_object_namer_by_slug) do
282
+ lambda do |slug|
283
+ binding = Monger.send(selection_method, slug)
284
+ object = Monger.find(slug)
285
+ @app.garner.bind(binding) { object.name }
286
+ end
287
+ end
288
+
289
+ it 'invalidates all identities' do
290
+ cached_object_namer.call.should eq 'M1'
291
+ cached_object_namer_by_slug.call('m1').should eq 'M1'
292
+ @object.update_attributes!(name: 'M2')
293
+ cached_object_namer.call.should eq 'M2'
294
+ cached_object_namer_by_slug.call('m1').should eq 'M2'
295
+ end
296
+ end
297
+ end
298
+ end
299
+ end
300
+
301
+ context 'binding at the class level' do
302
+ ['top-level class', 'subclass'].each do |level|
303
+ context "to a #{level}" do
304
+ let(:klass) { level == 'subclass' ? Cheese : Food }
305
+
306
+ let(:cached_object_name_concatenator) do
307
+ lambda do
308
+ @app.garner.bind(klass) do
309
+ klass.all.order_by(_id: :asc).map(&:name).join(', ')
310
+ end
311
+ end
312
+ end
313
+
314
+ it 'invalidates on create' do
315
+ Cheese.create(name: 'M1')
316
+ cached_object_name_concatenator.call.should eq 'M1'
317
+ Cheese.create(name: 'M3')
318
+ cached_object_name_concatenator.call.should eq 'M1, M3'
319
+ end
320
+
321
+ it 'invalidates on update' do
322
+ m1 = Cheese.create(name: 'M1')
323
+ Cheese.create(name: 'M3')
324
+ cached_object_name_concatenator.call.should eq 'M1, M3'
325
+ m1.update_attributes(name: 'M2')
326
+ cached_object_name_concatenator.call.should eq 'M2, M3'
327
+ end
328
+
329
+ it 'invalidates on destroy' do
330
+ m1 = Cheese.create(name: 'M1')
331
+ Cheese.create(name: 'M3')
332
+ cached_object_name_concatenator.call.should eq 'M1, M3'
333
+ m1.destroy
334
+ cached_object_name_concatenator.call.should eq 'M3'
335
+ end
336
+
337
+ it 'invalidates by explicit call to invalidate_garner_caches' do
338
+ m1 = Cheese.create(name: 'M1')
339
+ Cheese.create(name: 'M3')
340
+ cached_object_name_concatenator.call.should eq 'M1, M3'
341
+ if Mongoid.mongoid3?
342
+ m1.set(:name, 'M2')
343
+ else
344
+ m1.set(name: 'M2')
345
+ end
346
+ klass.invalidate_garner_caches
347
+ cached_object_name_concatenator.call.should eq 'M2, M3'
348
+ end
349
+ end
350
+ end
351
+ end
352
+ end
353
+ end
354
+ end
355
+ end