lifesaver 0.0.1 → 0.1.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 (43) hide show
  1. data/.rspec +1 -0
  2. data/.travis.yml +3 -1
  3. data/CHANGELOG.md +7 -0
  4. data/Gemfile +4 -0
  5. data/README.md +33 -13
  6. data/lib/lifesaver.rb +34 -11
  7. data/lib/lifesaver/config.rb +16 -0
  8. data/lib/lifesaver/index_worker.rb +10 -10
  9. data/lib/lifesaver/indexing/enqueuer.rb +37 -0
  10. data/lib/lifesaver/indexing/indexer.rb +40 -0
  11. data/lib/lifesaver/indexing/model_additions.rb +55 -0
  12. data/lib/lifesaver/notification/eager_loader.rb +48 -0
  13. data/lib/lifesaver/notification/enqueuer.rb +24 -0
  14. data/lib/lifesaver/notification/indexing_graph.rb +91 -0
  15. data/lib/lifesaver/notification/model_additions.rb +104 -0
  16. data/lib/lifesaver/notification/notifiable_associations.rb +49 -0
  17. data/lib/lifesaver/notification/traversal_queue.rb +48 -0
  18. data/lib/lifesaver/railtie.rb +4 -3
  19. data/lib/lifesaver/serialized_model.rb +3 -0
  20. data/lib/lifesaver/version.rb +1 -1
  21. data/lib/lifesaver/visitor_worker.rb +8 -4
  22. data/spec/integration/lifesaver_spec.rb +78 -0
  23. data/spec/spec_helper.rb +12 -10
  24. data/spec/support/active_record.rb +3 -3
  25. data/spec/support/test_models.rb +8 -6
  26. data/spec/support/tire_helper.rb +37 -0
  27. data/spec/unit/config_spec.rb +17 -0
  28. data/spec/unit/indexing/indexer_spec.rb +51 -0
  29. data/spec/unit/indexing/model_addtions_spec.rb +53 -0
  30. data/spec/unit/notification/eager_loader_spec.rb +64 -0
  31. data/spec/unit/notification/enqueuer_spec.rb +39 -0
  32. data/spec/unit/notification/indexing_graph_spec.rb +73 -0
  33. data/spec/unit/notification/model_additions_spec.rb +69 -0
  34. data/spec/unit/notification/notifiable_associations_spec.rb +75 -0
  35. data/spec/unit/notification/traversal_queue_spec.rb +67 -0
  36. metadata +40 -14
  37. data/lib/lifesaver/index_graph.rb +0 -53
  38. data/lib/lifesaver/marshal.rb +0 -43
  39. data/lib/lifesaver/model_additions.rb +0 -133
  40. data/spec/lifesaver/index_graph_spec.rb +0 -64
  41. data/spec/lifesaver/marshal_spec.rb +0 -80
  42. data/spec/lifesaver/model_additions_spec.rb +0 -91
  43. data/spec/lifesaver_spec.rb +0 -111
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe Lifesaver::Config do
4
+ describe '#initialize' do
5
+ context 'with no options' do
6
+ let(:config) { Lifesaver::Config.new }
7
+
8
+ it 'has the default indexing_queue' do
9
+ expect(config.indexing_queue).to eql(:lifesaver_indexing)
10
+ end
11
+
12
+ it 'has the default notification_queue' do
13
+ expect(config.notification_queue).to eql(:lifesaver_notification)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe Lifesaver::Indexing::Indexer do
4
+ context '#perform' do
5
+ let(:index) { double('index', remove: nil, store: nil) }
6
+ let(:index_name) { 'test-index-name' }
7
+ before do
8
+ Model.stub(:index_name).and_return(index_name)
9
+ Tire.stub(:index).with(index_name).and_return(index)
10
+ end
11
+
12
+ context 'when operation is update' do
13
+ let(:model) { Model.new(1) }
14
+ before do
15
+ Model.stub(:exists?).with('1').and_return(true)
16
+ Model.stub(:find).with('1').and_return(model)
17
+ end
18
+
19
+ it 'calls tire with the correct arguments' do
20
+ indexer = Lifesaver::Indexing::Indexer.new(
21
+ class_name: 'model',
22
+ model_id: '1',
23
+ operation: 'update'
24
+ )
25
+
26
+ expect(index).to receive(:store).with(model)
27
+
28
+ indexer.perform
29
+ end
30
+ end
31
+
32
+ context 'when operation is destroy' do
33
+ before do
34
+ Model.stub(:document_type).and_return('class')
35
+ end
36
+
37
+ it 'calls tire with the correct arguments' do
38
+ indexer = Lifesaver::Indexing::Indexer.new(
39
+ class_name: 'model',
40
+ model_id: '1',
41
+ operation: 'destroy'
42
+ )
43
+ correct_arguments = { type: 'class', id: '1' }
44
+
45
+ expect(index).to receive(:remove).with(correct_arguments)
46
+
47
+ indexer.perform
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+
3
+ describe Lifesaver::Indexing::ModelAdditions do
4
+ let(:post) { Post.new(title: 'Test Post') }
5
+
6
+ describe '.enqueues_indexing' do
7
+ before do
8
+ post.stub(:enqueue_indexing)
9
+ end
10
+
11
+ it 'calls enqueue_indexing on save' do
12
+ expect(post).to receive(:enqueue_indexing)
13
+
14
+ post.save!
15
+ end
16
+
17
+ it 'calls enqueue_indexing on destroy' do
18
+ expect(post).to receive(:enqueue_indexing)
19
+
20
+ post.destroy
21
+ end
22
+ end
23
+
24
+ describe '#indexing_suppressed?' do
25
+ it 'is false by default' do
26
+ expect(post.send(:suppress_indexing?)).to eql(false)
27
+ end
28
+
29
+ it 'is true if overridden locally' do
30
+ Lifesaver.suppress_indexing
31
+
32
+ expect(post.send(:suppress_indexing?)).to eql(true)
33
+ end
34
+
35
+ it 'is false if override is cancelled' do
36
+ Lifesaver.unsuppress_indexing
37
+
38
+ expect(post.send(:suppress_indexing?)).to eql(false)
39
+ end
40
+
41
+ it 'is true if set individually' do
42
+ post.suppress_indexing
43
+
44
+ expect(post.send(:suppress_indexing?)).to eql(true)
45
+ end
46
+
47
+ it 'is false if unset individually' do
48
+ post.unsuppress_indexing
49
+
50
+ expect(post.send(:suppress_indexing?)).to eql(false)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe Lifesaver::Notification::EagerLoader do
4
+
5
+ let(:eager_loader) { Lifesaver::Notification::EagerLoader.new }
6
+
7
+ describe '#add_model' do
8
+
9
+ it 'adds when empty' do
10
+ eager_loader.add_model('Post', 34)
11
+ expected_result = { 'Post' => [34] }
12
+
13
+ expect(eager_loader.send(:models_to_load)).to eql(expected_result)
14
+ end
15
+
16
+ it 'adds to when other model ids exist' do
17
+ eager_loader.add_model('Post', 34)
18
+ eager_loader.add_model('Post', 38)
19
+ eager_loader.add_model('Article', 3)
20
+ expected_result = { 'Post' => [34, 38], 'Article' => [3] }
21
+
22
+ expect(eager_loader.send(:models_to_load)).to eql(expected_result)
23
+ end
24
+
25
+ it 'does not add a previously added model' do
26
+ eager_loader.add_model('Post', 38)
27
+ eager_loader.add_model('Post', 38)
28
+ expected_result = { 'Post' => [38] }
29
+
30
+ expect(eager_loader.send(:models_to_load)).to eql(expected_result)
31
+ end
32
+
33
+ end
34
+
35
+ describe '#load' do
36
+ before do
37
+ Post.stub(:load_with_notifiable_associations).and_return([])
38
+ eager_loader.add_model('Post', 34)
39
+ eager_loader.add_model('Post', 38)
40
+ end
41
+
42
+ it 'empties the models_to_load' do
43
+ eager_loader.load
44
+
45
+ expect(eager_loader.send(:models_to_load)).to be_empty
46
+ end
47
+
48
+ it 'returns an array' do
49
+ expect(eager_loader.load).to eq([])
50
+ end
51
+ end
52
+
53
+ describe '#empty?' do
54
+ it 'is true when empty' do
55
+ expect(eager_loader.empty?).to be_true
56
+ end
57
+
58
+ it 'is false when not empty' do
59
+ eager_loader.add_model('Post', 38)
60
+
61
+ expect(eager_loader.empty?).to be_false
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe Lifesaver::Notification::Enqueuer do
4
+ let(:models) { [Lifesaver::SerializedModel.new('TestClass', '3')] }
5
+ let(:enqueuer) { Lifesaver::Notification::Enqueuer.new(models) }
6
+ before { ::Resque.stub(:enqueue) }
7
+
8
+ describe '#enqueue' do
9
+ context 'when indexing is not suppressed' do
10
+ context 'and there are models' do
11
+ it 'calls Resque.enqueue with the correct parameters' do
12
+ expect(::Resque).to receive(:enqueue)
13
+ .with(Lifesaver::VisitorWorker, models)
14
+
15
+ enqueuer.enqueue
16
+ end
17
+ end
18
+ context 'and there are no models' do
19
+ it 'does not call Resque.enqueue' do
20
+ empty_enqueuer = Lifesaver::Notification::Enqueuer.new([])
21
+
22
+ expect(::Resque).to_not receive(:enqueue)
23
+
24
+ empty_enqueuer.enqueue
25
+ end
26
+ end
27
+ end
28
+
29
+ context 'when indexing is suppressed' do
30
+ it 'does not call Resque.enqueue' do
31
+ Lifesaver.suppress_indexing
32
+
33
+ expect(::Resque).to_not receive(:enqueue)
34
+
35
+ enqueuer.enqueue
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ describe Lifesaver::Notification::IndexingGraph do
4
+ let(:indexing_graph) { Lifesaver::Notification::IndexingGraph.new }
5
+
6
+ describe '#initalize_models' do
7
+ it 'should load serialized models into the loader' do
8
+ indexing_graph.stub(:add_model_to_loader)
9
+ serialized_models = []
10
+ serialized_models << Lifesaver::SerializedModel.new('Post', 14)
11
+
12
+ expect(indexing_graph).to receive(:add_model_to_loader).with('Post', 14)
13
+
14
+ indexing_graph.initialize_models(serialized_models)
15
+ end
16
+ end
17
+
18
+ describe '#generate' do
19
+ context 'when queue is empty' do
20
+ before do
21
+ indexing_graph.stub(:queue_full?).and_return(false)
22
+ end
23
+
24
+ context 'and loader is empty' do
25
+ it 'exits and returns models_to_index' do
26
+ indexing_graph.stub(:loader_full?).and_return(false)
27
+ indexing_graph.stub(:models_to_index).and_return([Model.new(15)])
28
+
29
+ expect(indexing_graph.generate).to eql([Model.new(15)])
30
+ end
31
+ end
32
+
33
+ context 'and loader is not empty' do
34
+ it 'calls load_into_queue' do
35
+ indexing_graph.stub(:loader_full?).and_return(true, false)
36
+ indexing_graph.stub(:load_into_queue)
37
+
38
+ expect(indexing_graph).to receive(:load_into_queue)
39
+
40
+ indexing_graph.generate
41
+ end
42
+ end
43
+ end
44
+
45
+ context 'when queue is not empty' do
46
+ before do
47
+ indexing_graph.stub(:queue_full?).and_return(true, false)
48
+ indexing_graph.stub(:pop_model).and_return(Model.new(1))
49
+ indexing_graph.stub(:add_unvisited_associations)
50
+ end
51
+
52
+ it 'adds model if should be indexed' do
53
+ indexing_graph.stub(:model_should_be_indexed?).and_return(true)
54
+
55
+ expect(indexing_graph.generate).to eql([Model.new(1)])
56
+ end
57
+
58
+ it 'does not add a model if it should not be indexed' do
59
+ indexing_graph.stub(:model_should_be_indexed?).and_return(false)
60
+
61
+ expect(indexing_graph.generate).to eql([])
62
+ end
63
+
64
+ it 'adds unvisited associations to the graph' do
65
+ indexing_graph.stub(:model_should_be_indexed?).and_return(false)
66
+
67
+ expect(indexing_graph).to receive(:add_unvisited_associations)
68
+
69
+ indexing_graph.generate
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+ describe Lifesaver::Notification::ModelAdditions do
4
+ let(:post) { Post.create!(title: 'Test', content: 'Lorem', tags: %w(tests)) }
5
+ let(:affiliate) { Affiliate.create(name: 'Some place') }
6
+ let(:author) { Author.create!(name: 'Paul Sorensen', affiliate: affiliate) }
7
+ let(:authorship) { Authorship.create!(post: post, author: author) }
8
+ before do
9
+ post.reload
10
+ author.reload
11
+ authorship.reload
12
+ end
13
+
14
+ describe '.notifies_for_indexing' do
15
+ before do
16
+ post.stub(:update_associations)
17
+ end
18
+
19
+ it 'calls update_associations on save' do
20
+ expect(post).to receive(:update_associations)
21
+
22
+ post.save!
23
+ end
24
+
25
+ it 'calls update_associations on destroy' do
26
+ expect(post).to receive(:update_associations)
27
+
28
+ post.destroy
29
+ end
30
+ end
31
+
32
+ describe '.load_with_notifiable_associations' do
33
+ it 'returns the correct eager-loaded models' do
34
+ model = Authorship.load_with_notifiable_associations(authorship.id).first
35
+
36
+ expect(model.association(:author).loaded?).to be_true
37
+ end
38
+ end
39
+
40
+ describe '#associations_to_notify' do
41
+ it 'returns the correct models' do
42
+ expect(author.associations_to_notify).to eql([authorship])
43
+ end
44
+ end
45
+
46
+ describe '#needs_to_notify?' do
47
+ it 'is false if there are no notifiable associations' do
48
+ expect(post.needs_to_notify?).to be_false
49
+ end
50
+
51
+ it 'is true if there are notifiable associations' do
52
+ expect(author.needs_to_notify?).to be_true
53
+ end
54
+ end
55
+
56
+ describe '#models_for_association' do
57
+ it 'should return an array of models for multiple association' do
58
+ models = post.models_for_association(:authorships)
59
+
60
+ expect(models.first).to be_a_kind_of(Authorship)
61
+ end
62
+
63
+ it 'should return an array of one model for a singular association' do
64
+ models = author.models_for_association(:affiliate)
65
+
66
+ expect(models.first).to be_a_kind_of(Affiliate)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ describe Lifesaver::Notification::NotifiableAssociations do
4
+ let(:notifiable_associations) do
5
+ Lifesaver::Notification::NotifiableAssociations.new
6
+ end
7
+
8
+ describe '#populate' do
9
+ it 'mirrors arguments for both keys' do
10
+ notifiable_associations.populate(:foo)
11
+ association_keys = notifiable_associations.send(:association_keys)
12
+
13
+ expect(association_keys.on_change).to eql(association_keys.on_notify)
14
+ end
15
+
16
+ it 'accepts arguments that are singular' do
17
+ notifiable_associations.populate(:foo)
18
+ association_keys = notifiable_associations.send(:association_keys)
19
+
20
+ expect(association_keys.on_change).to eql([:foo])
21
+ end
22
+
23
+ it 'accepts arguments that are arrays' do
24
+ notifiable_associations.populate([:foo, :bar])
25
+ association_keys = notifiable_associations.send(:association_keys)
26
+
27
+ expect(association_keys.on_notify).to eql([:foo, :bar])
28
+ end
29
+
30
+ it 'accepts an options hash for :only_on_notify' do
31
+ notifiable_associations.populate(:foo, only_on_notify: [:baz])
32
+ association_keys = notifiable_associations.send(:association_keys)
33
+
34
+ expect(association_keys.on_notify).to eql([:foo, :baz])
35
+ end
36
+
37
+ it 'accepts an options hash for :only_on_change' do
38
+ notifiable_associations.populate(:foo, only_on_change: [:baz])
39
+ association_keys = notifiable_associations.send(:association_keys)
40
+
41
+ expect(association_keys.on_change).to eql([:foo, :baz])
42
+ end
43
+ end
44
+
45
+ describe '#any_to_notify?' do
46
+ it 'is true if the :on_notify array is not empty' do
47
+ notifiable_associations.populate([], only_on_notify: [:baz])
48
+
49
+ expect(notifiable_associations.any_to_notify?).to be_true
50
+ end
51
+
52
+ it 'is false if the :on_notify array is empty' do
53
+ notifiable_associations.populate([], only_on_change: [:baz])
54
+
55
+ expect(notifiable_associations.any_to_notify?).to be_false
56
+ end
57
+ end
58
+
59
+ describe '#on_notify' do
60
+ it 'reads the :on_notify key' do
61
+ notifiable_associations.populate([], only_on_notify: [:baz])
62
+ keys = notifiable_associations.send(:association_keys)
63
+
64
+ expect(notifiable_associations.on_notify).to eql(keys.on_notify)
65
+ end
66
+ end
67
+ describe '#on_change' do
68
+ it 'reads the :on_change key' do
69
+ notifiable_associations.populate([], only_on_change: [:baz])
70
+ keys = notifiable_associations.send(:association_keys)
71
+
72
+ expect(notifiable_associations.on_change).to eql(keys.on_change)
73
+ end
74
+ end
75
+ end