tantot 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,299 @@
1
+ require "spec_helper"
2
+
3
+ describe Tantot do
4
+ it "has a version number" do
5
+ expect(Tantot::VERSION).not_to be nil
6
+ end
7
+
8
+ describe '.derive_watcher' do
9
+ class TestWatcher
10
+ include Tantot::Watcher
11
+ end
12
+
13
+ class WrongWatcher
14
+ end
15
+
16
+ module Foo
17
+ class BarWatcher
18
+ include Tantot::Watcher
19
+ end
20
+ end
21
+
22
+ specify { expect { described_class.derive_watcher('foo') }.to raise_error(Tantot::UnderivableWatcher) }
23
+ specify { expect { described_class.derive_watcher(WrongWatcher) }.to raise_error(Tantot::UnderivableWatcher) }
24
+ specify { expect(described_class.derive_watcher(TestWatcher)).to eq(TestWatcher) }
25
+ specify { expect(described_class.derive_watcher(Foo::BarWatcher)).to eq(Foo::BarWatcher) }
26
+ specify { expect(described_class.derive_watcher('test')).to eq(TestWatcher) }
27
+ specify { expect(described_class.derive_watcher('foo/bar')).to eq(Foo::BarWatcher) }
28
+ end
29
+
30
+ describe '.watch' do
31
+
32
+ let(:watcher_instance) { double }
33
+
34
+ before do
35
+ stub_class("TestWatcher") { include Tantot::Watcher }
36
+ allow(TestWatcher).to receive(:new).and_return(watcher_instance)
37
+ end
38
+
39
+ [true, false].each do |use_after_commit_callbacks|
40
+ context "using after_commit hooks: #{use_after_commit_callbacks}" do
41
+ before { allow(Tantot.config).to receive(:use_after_commit_callbacks).and_return(use_after_commit_callbacks) }
42
+
43
+ context "watching an attribute" do
44
+ before do
45
+ stub_model(:city) do
46
+ watch TestWatcher, :name
47
+ end
48
+ end
49
+
50
+ it "doesn't call back when the attribute doesn't change" do
51
+ Tantot.collector.run do
52
+ City.create
53
+ expect(watcher_instance).not_to receive(:perform)
54
+ end
55
+ end
56
+
57
+ it "calls back when the attribute changes (on creation)" do
58
+ Tantot.collector.run do
59
+ city = City.create name: 'foo'
60
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => [nil, 'foo']}}}))
61
+ end
62
+ end
63
+
64
+ it "calls back on model update" do
65
+ city = City.create!
66
+ city.reload
67
+ Tantot.collector.sweep(performer: :bypass)
68
+
69
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => [nil, 'foo']}}}))
70
+ Tantot.collector.run do
71
+ city.name = "foo"
72
+ city.save
73
+ end
74
+ end
75
+
76
+ it "calls back on model destroy" do
77
+ city = City.create!(name: 'foo')
78
+ city.reload
79
+ Tantot.collector.sweep(performer: :bypass)
80
+
81
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => ['foo']}}}))
82
+ Tantot.collector.run do
83
+ city.destroy
84
+ end
85
+ end
86
+
87
+ it "calls back once per model even when updated more than once" do
88
+ Tantot.collector.run do
89
+ city = City.create! name: 'foo'
90
+ city.name = 'bar'
91
+ city.save
92
+ city.name = 'baz'
93
+ city.save
94
+ expect(watcher_instance).to receive(:perform).once.with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => [nil, 'foo', 'bar', 'baz']}}}))
95
+ end
96
+ end
97
+
98
+ it "allows to call a watcher mid-stream" do
99
+ Tantot.collector.run do
100
+ city = City.create name: 'foo'
101
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => [nil, 'foo']}}}))
102
+ Tantot.collector.sweep(performer: :inline, watcher: TestWatcher)
103
+ city.name = 'bar'
104
+ city.save
105
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => ['foo', 'bar']}}}))
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ context "detailed format" do
113
+ let(:watcher_instance) { double }
114
+ before do
115
+ stub_class("DetailedTestWatcher") do
116
+ include Tantot::Watcher
117
+
118
+ watcher_options format: :detailed
119
+ end
120
+ allow(DetailedTestWatcher).to receive(:new).and_return(watcher_instance)
121
+ stub_model(:city) do
122
+ watch 'detailed_test', :name
123
+ end
124
+ end
125
+
126
+ it "should output a detailed array of changes" do
127
+ Tantot.collector.run do
128
+ city = City.create! name: 'foo'
129
+ city.name = 'bar'
130
+ city.save
131
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => [[nil, 'foo'], ['foo', 'bar']]}}}))
132
+ end
133
+ end
134
+ end
135
+
136
+ context "on multiple models" do
137
+ before do
138
+ stub_model(:city) do
139
+ watch TestWatcher, :name, :country_id
140
+ end
141
+ stub_model(:country) do
142
+ watch TestWatcher, :country_code
143
+ end
144
+ end
145
+
146
+ it "calls back once per watch when multiple watched models change" do
147
+ country = Country.create!(country_code: "CDN")
148
+ city = City.create!(name: "Quebec", country_id: country.id)
149
+ country.reload
150
+ city.reload
151
+ Tantot.collector.sweep(performer: :bypass)
152
+
153
+ expect(watcher_instance).to receive(:perform).once.with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => ['Quebec', 'foo', 'bar'], "country_id" => [country.id, nil]}}, Country => {country.id => {"country_code" => ['CDN', 'US']}}}))
154
+ Tantot.collector.run do
155
+ city.name = "foo"
156
+ city.save
157
+ city.name = "bar"
158
+ city.save
159
+ city.country_id = nil
160
+ city.save
161
+ country.country_code = 'US'
162
+ country.save
163
+ city.destroy
164
+ end
165
+ end
166
+ end
167
+
168
+ context "with multiple watchers" do
169
+ let(:watchA_instance) { double }
170
+ let(:watchB_instance) { double }
171
+ before do
172
+ stub_class("TestWatcherA") { include Tantot::Watcher }
173
+ stub_class("TestWatcherB") { include Tantot::Watcher }
174
+ allow(TestWatcherA).to receive(:new).and_return(watchA_instance)
175
+ allow(TestWatcherB).to receive(:new).and_return(watchB_instance)
176
+ stub_model(:city) do
177
+ watch TestWatcherA, :name, :country_id
178
+ watch TestWatcherB, :rating
179
+ end
180
+ stub_model(:country) do
181
+ watch TestWatcherA, :country_code
182
+ watch TestWatcherB, :name, :rating
183
+ end
184
+ end
185
+
186
+ it "calls each watcher once for multiple models" do
187
+ country = Country.create!(country_code: "CDN")
188
+ city = City.create!(name: "Quebec", country_id: country.id, rating: 12)
189
+ country.reload
190
+ city.reload
191
+ expect(watchA_instance).to receive(:perform).once.with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => ['Quebec', 'foo', 'bar'], "country_id" => [country.id, nil]}}, Country => {country.id => {"country_code" => ['CDN', 'US']}}}))
192
+ # WatchB receives the last value of rating since it has been destroyed
193
+ expect(watchB_instance).to receive(:perform).once.with(Tantot::Changes::ByModel.new({City => {city.id => {"rating" => [12]}}}))
194
+ Tantot.collector.sweep(performer: :bypass)
195
+
196
+ Tantot.collector.run do
197
+ city.name = "foo"
198
+ city.save
199
+ city.name = "bar"
200
+ city.save
201
+ city.country_id = nil
202
+ city.save
203
+ country.country_code = 'US'
204
+ country.save
205
+ city.destroy
206
+ end
207
+ end
208
+ end
209
+
210
+ context 'watching all attributes' do
211
+ before do
212
+ stub_model(:city) do
213
+ watch TestWatcher
214
+ end
215
+ end
216
+
217
+ it "should watch all changes" do
218
+ Tantot.collector.run do
219
+ city = City.create name: 'foo'
220
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"id" => [nil, city.id], "name" => [nil, "foo"]}}}))
221
+ end
222
+ end
223
+
224
+ it "should also watch on destroy, but when watching all attributes, change hash is empty" do
225
+ city = City.create!(name: 'foo')
226
+ city.reload
227
+ Tantot.collector.sweep(performer: :bypass)
228
+
229
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {}}}))
230
+ Tantot.collector.run do
231
+ city.destroy
232
+ end
233
+ end
234
+ end
235
+
236
+ context 'with an additional `if` statement' do
237
+
238
+ [:no, :some].each do |attribute_opt|
239
+ context "with #{attribute_opt.to_s} attributes" do
240
+ let(:condition) { double }
241
+ before do
242
+ c = condition
243
+ watch_params = [TestWatcher]
244
+ watch_params << :id if attribute_opt == :some
245
+ watch_params << {if: -> { c.passed? }}
246
+ stub_model(:city) do
247
+ watch(*watch_params)
248
+ end
249
+ end
250
+
251
+ it "should fail if the condition is false" do
252
+ Tantot.collector.run do
253
+ expect(condition).to receive(:passed?).once.and_return(false)
254
+ City.create!
255
+ expect(watcher_instance).not_to receive(:perform)
256
+ end
257
+ end
258
+
259
+ it "should pass if the condition is true" do
260
+ Tantot.collector.run do
261
+ expect(condition).to receive(:passed?).once.and_return(true)
262
+ City.create!
263
+ expect(watcher_instance).to receive(:perform)
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
269
+
270
+ context 'using a block' do
271
+ let(:value) { {changes: 0} }
272
+ let(:changes) { {obj: nil} }
273
+ before do
274
+ v = value
275
+ c = changes
276
+ stub_model(:city) do
277
+ watch {|changes| v[:changes] += 1; c[:obj] = changes}
278
+ end
279
+ end
280
+
281
+ it "should call the block" do
282
+ city = nil
283
+ Tantot.collector.run do
284
+ city = City.create!
285
+ end
286
+ expect(value[:changes]).to eq(1)
287
+ expect(changes[:obj]).to eq(Tantot::Changes::ById.new({city.id => {"id" => [nil, 1]}}))
288
+ end
289
+
290
+ it "call a single time if multiple changes occur" do
291
+ Tantot.collector.run do
292
+ 3.times { City.create! }
293
+ end
294
+ expect(value[:changes]).to eq(1)
295
+ expect(changes[:obj]).to eq(Tantot::Changes::ById.new({1=>{"id"=>[nil, 1]}, 2=>{"id"=>[nil, 2]}, 3=>{"id"=>[nil, 3]}}))
296
+ end
297
+ end
298
+ end # describe '.watch'
299
+ end
data/tantot.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'tantot/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "tantot"
8
+ spec.version = Tantot::VERSION
9
+ spec.authors = ["François-Pierre Bouchard"]
10
+ spec.email = ["fpbouchard@gmail.com"]
11
+
12
+ spec.summary = %q{Delayed, grouped and compact ActiveRecord callbacks}
13
+ spec.description = %q{Centralize and delay changes to multiple ActiveRecord models to offload processing of complex calculations caused by model mutations.}
14
+ spec.homepage = "https://github.com/petalmd/tantot"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files`.split($RS)
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.13"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec", "~> 3.0"
25
+ spec.add_development_dependency 'sqlite3'
26
+ spec.add_development_dependency 'database_cleaner'
27
+
28
+ spec.add_dependency 'activesupport', '>= 3.2'
29
+ spec.add_dependency 'activerecord', '>= 3.2'
30
+ spec.add_dependency 'cityhash', '>= 0.8'
31
+ end
metadata ADDED
@@ -0,0 +1,202 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tantot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - François-Pierre Bouchard
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-12-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.13'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sqlite3
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: database_cleaner
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activesupport
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '3.2'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '3.2'
97
+ - !ruby/object:Gem::Dependency
98
+ name: activerecord
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '3.2'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '3.2'
111
+ - !ruby/object:Gem::Dependency
112
+ name: cityhash
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0.8'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0.8'
125
+ description: Centralize and delay changes to multiple ActiveRecord models to offload
126
+ processing of complex calculations caused by model mutations.
127
+ email:
128
+ - fpbouchard@gmail.com
129
+ executables:
130
+ - console
131
+ - setup
132
+ extensions: []
133
+ extra_rdoc_files: []
134
+ files:
135
+ - ".gitignore"
136
+ - ".rspec"
137
+ - ".travis.yml"
138
+ - CODE_OF_CONDUCT.md
139
+ - Gemfile
140
+ - LICENSE.txt
141
+ - README.md
142
+ - Rakefile
143
+ - bin/console
144
+ - bin/setup
145
+ - lib/tantot.rb
146
+ - lib/tantot/changes.rb
147
+ - lib/tantot/collector.rb
148
+ - lib/tantot/collector/base.rb
149
+ - lib/tantot/collector/block.rb
150
+ - lib/tantot/collector/watcher.rb
151
+ - lib/tantot/config.rb
152
+ - lib/tantot/errors.rb
153
+ - lib/tantot/extensions/chewy.rb
154
+ - lib/tantot/extensions/grape/middleware.rb
155
+ - lib/tantot/formatter.rb
156
+ - lib/tantot/formatter/compact.rb
157
+ - lib/tantot/formatter/detailed.rb
158
+ - lib/tantot/observe.rb
159
+ - lib/tantot/performer.rb
160
+ - lib/tantot/performer/bypass.rb
161
+ - lib/tantot/performer/inline.rb
162
+ - lib/tantot/performer/sidekiq.rb
163
+ - lib/tantot/railtie.rb
164
+ - lib/tantot/registry.rb
165
+ - lib/tantot/version.rb
166
+ - lib/tantot/watcher.rb
167
+ - spec/changes_spec.rb
168
+ - spec/extensions/chewy_spec.rb
169
+ - spec/sidekiq_spec.rb
170
+ - spec/spec_helper.rb
171
+ - spec/tantot_spec.rb
172
+ - tantot.gemspec
173
+ homepage: https://github.com/petalmd/tantot
174
+ licenses:
175
+ - MIT
176
+ metadata: {}
177
+ post_install_message:
178
+ rdoc_options: []
179
+ require_paths:
180
+ - lib
181
+ required_ruby_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
186
+ required_rubygems_version: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: '0'
191
+ requirements: []
192
+ rubyforge_project:
193
+ rubygems_version: 2.5.1
194
+ signing_key:
195
+ specification_version: 4
196
+ summary: Delayed, grouped and compact ActiveRecord callbacks
197
+ test_files:
198
+ - spec/changes_spec.rb
199
+ - spec/extensions/chewy_spec.rb
200
+ - spec/sidekiq_spec.rb
201
+ - spec/spec_helper.rb
202
+ - spec/tantot_spec.rb