garner 0.4.5 → 0.5.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 (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