tantot 0.1.8 → 0.1.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6a523888d6f676374dba88050d7cb0026383aefa
4
- data.tar.gz: db6d802ec982feae01b1bcf0f4926f1cc46d3141
3
+ metadata.gz: f2858d3a360d204db3c1755fecc65a5e86b3fb8e
4
+ data.tar.gz: 54d6490ddd105d5ff50eebae905eb91ace85c4e8
5
5
  SHA512:
6
- metadata.gz: 3f53c127a4a7f8f51fcb9667bf6e58d6ff5e0c5f51746dcb3c14f11527805183cce3ed409083db36b3077c23c6aa531490ee5126bca29f33f7044d631fe437ee
7
- data.tar.gz: 75dcd526791dd3f77b760c1f12afab72b73a94bfe9f5c2b4094024757243ac21ecbd50053c3e838da05979bfcff03bec6cc5bbd110a8c2bb2830cc63cb651d29
6
+ metadata.gz: 6db43766e1c9dd8e2a56ecd81386bc01a46b6c300fc91d823063fe5418df2e0d74910edc4053eb7e20216ea6316383e03d57ed35bbe0e6f9bffeee3d247bbbcf
7
+ data.tar.gz: 64a89f3516777115416ce38f48d92317f30390c3acfbb8f770d41bbf12e6cdafd07877fc75e9d65391865099f1308fad5f2fc17792e8c6119ec8967394714aff
@@ -1,14 +1,20 @@
1
+ require 'cityhash'
2
+
1
3
  module Tantot
2
4
  module Agent
3
5
  class Block < Base
4
6
  def self.identify(watch)
5
7
  if watch.block.present?
6
- "#{watch.model.to_s}|#{watch.options.inspect}"
8
+ CityHash.hash64("#{watch.model.to_s}|#{watch.attributes.inspect}|#{watch.options.inspect}")
7
9
  else
8
10
  nil
9
11
  end
10
12
  end
11
13
 
14
+ def setup_watch(watch)
15
+ raise Tantot::MultipleWatchesProhibited.new("Can't have multiple block watches per model with the same attributes and options: #{debug_block(watch.block)}") if @watches.any?
16
+ end
17
+
12
18
  def perform(changes_by_model)
13
19
  # Block agent always has only one watch
14
20
  block = watches.first.block
data/lib/tantot/agent.rb CHANGED
@@ -10,8 +10,8 @@ module Tantot
10
10
 
11
11
  def self.resolve!(watch)
12
12
  agent_classes = AGENT_CLASSES.collect {|klass| [klass, klass.identify(watch)]}.reject {|_klass, id| id.nil?}
13
- raise UnresolvableAgent("Can't resolve agent for watch: #{watch.inspect}") unless agent_classes.any?
14
- raise UnresolvableAgent("More than one agent manages watch: #{watch.inspect}") if agent_classes.many?
13
+ raise Tantot::UnresolvableAgent.new("Can't resolve agent for watch: #{watch.inspect}. Specify either a watcher class or define a block.") unless agent_classes.any?
14
+ raise Tantot::UnresolvableAgent.new("More than one agent manages watch: #{watch.inspect}") if agent_classes.many?
15
15
  agent_classes.first
16
16
  end
17
17
 
data/lib/tantot/errors.rb CHANGED
@@ -7,4 +7,7 @@ module Tantot
7
7
 
8
8
  class AgentNotFound < StandardError
9
9
  end
10
+
11
+ class MultipleWatchesProhibited < StandardError
12
+ end
10
13
  end
@@ -31,16 +31,16 @@ module Tantot
31
31
  end
32
32
  end
33
33
 
34
- watch('tantot/extensions/chewy/chewy', *args, watch_options)
35
34
  Tantot::Extensions::Chewy.register_watch(self, type_name, options, block)
35
+ watch('tantot/extensions/chewy/chewy', *args, watch_options)
36
36
  end
37
37
  end
38
38
 
39
39
  def self.register_watch(model, type_name, options, block)
40
40
  method = options.delete(:method)
41
41
  model._tantot_chewy_callbacks ||= {}
42
- model._tantot_chewy_callbacks[type_name] ||= []
43
- model._tantot_chewy_callbacks[type_name].push({method: method, options: options, block: block})
42
+ raise MultipleWatchesProhibited.new("Cannot register an index watch on the same type more than once: #{type_name}") if model._tantot_chewy_callbacks.key?(type_name)
43
+ model._tantot_chewy_callbacks[type_name] = [method, options, block]
44
44
  end
45
45
 
46
46
  class ChewyWatcher
@@ -51,84 +51,87 @@ module Tantot
51
51
  def perform(changes_by_model)
52
52
  changes_by_model.each do |model, changes_by_id|
53
53
  model_watches = model._tantot_chewy_callbacks
54
- model_watches.each do |type_name, watch_args_array|
54
+ model_watches.each do |type_name, (method, options, block)|
55
55
  # Find type
56
- reference =
57
- if type_name.is_a?(Proc)
58
- if type_name.arity.zero?
59
- instance_exec(&type_name)
60
- else
61
- type_name.call(self)
62
- end
63
- else
64
- type_name
65
- end
56
+ reference = get_chewy_type(type_name)
57
+ backreference = get_ids_to_update(model, changes_by_id, method, options, block)
66
58
 
67
- watch_args_array.each do |watch_args|
68
- method = watch_args[:method]
69
- block = watch_args[:block]
70
- options = watch_args[:options]
71
- association = options[:association]
59
+ if backreference
60
+ backreference.compact!
72
61
 
73
- # Find ids to update
74
- backreference =
75
- if association
76
- reflection = model.reflect_on_association(association)
77
- reflection.check_validity!
78
- case reflection.macro
79
- when :belongs_to
80
- changes_by_id.for_attribute(reflection.foreign_key)
81
- when :has_one, :has_many
82
- if reflection.options[:through]
83
- through_query =
84
- case reflection.through_reflection.macro
85
- when :belongs_to
86
- reflection.through_reflection.klass.where(reflection.through_reflection.klass.primary_key => changes_by_id.for_attribute(reflection.through_reflection.foreign_key))
87
- when :has_many, :has_one
88
- reflection.through_reflection.klass.where(reflection.through_reflection.foreign_key => changes_by_id.ids)
89
- end
90
- case reflection.source_reflection.macro
91
- when :belongs_to
92
- through_query.pluck(reflection.source_reflection.foreign_key)
93
- when :has_many
94
- reflection.source_reflection.klass.where(reflection.source_reflection.foreign_key => (through_query.ids)).ids
95
- end
96
- else
97
- reflection.klass.where(reflection.foreign_key => changes_by_id.ids).ids
98
- end
99
- end
100
- else
101
- if (method && method.to_sym == :self) || (!method && !block)
102
- # Simply extract keys from changes
103
- changes_by_id.keys
104
- elsif method
105
- # We need to call `method`.
106
- # Try to find it on the class. If so, call it once with all changes.
107
- # There is no API to call per-instance since objects can be already destroyed
108
- # when using the sidekiq performer
109
- model.send(method, changes_by_id)
110
- elsif block
111
- # Since we can be post-destruction of the model, we can't load models here
112
- # Thus, the signature of the block callback is |changes| which are all
113
- # the changes to all the models
114
- model.instance_exec(changes_by_id, &block)
115
- end
116
- end
62
+ # Make sure there are any backreferences
63
+ if backreference.any?
64
+ Tantot.logger.debug { "[Tantot] [Chewy] [update_index] #{reference} (#{backreference.count} objects): #{backreference.inspect}" }
65
+ ::Chewy.derive_type(reference).update_index(backreference, {})
66
+ end
67
+ else
68
+ # nothing to update
69
+ end
70
+ end
71
+ end
72
+ end
117
73
 
118
- if backreference
119
- backreference.compact!
74
+ def get_chewy_type(type_name)
75
+ if type_name.is_a?(Proc)
76
+ if type_name.arity.zero?
77
+ instance_exec(&type_name)
78
+ else
79
+ type_name.call(self)
80
+ end
81
+ else
82
+ type_name
83
+ end
84
+ end
120
85
 
121
- # Make sure there are any backreferences
122
- if backreference.any?
123
- Tantot.logger.debug { "[Tantot] [Chewy] [update_index] #{reference} (#{backreference.count} objects): #{backreference.inspect}" }
124
- ::Chewy.derive_type(reference).update_index(backreference, {})
125
- end
126
- end
86
+ def get_ids_to_update(model, changes_by_id, method, options, block)
87
+ if options.key?(:association)
88
+ resolve_association(model, options[:association], changes_by_id)
89
+ else
90
+ if (method && method.to_sym == :self) || (!method && !block)
91
+ # Simply extract keys from changes
92
+ changes_by_id.keys
93
+ elsif method
94
+ # We need to call `method`.
95
+ # Try to find it on the class. If so, call it once with all changes.
96
+ # There is no API to call per-instance since objects can be already destroyed
97
+ # when using the sidekiq performer
98
+ model.send(method, changes_by_id)
99
+ elsif block
100
+ # Since we can be post-destruction of the model, we can't load models here
101
+ # Thus, the signature of the block callback is |changes| which are all
102
+ # the changes to all the models
103
+ model.instance_exec(changes_by_id, &block)
104
+ end
105
+ end
106
+ end
127
107
 
108
+ def resolve_association(model, association, changes_by_id)
109
+ reflection = model.reflect_on_association(association)
110
+ reflection.check_validity!
111
+ case reflection.macro
112
+ when :belongs_to
113
+ changes_by_id.for_attribute(reflection.foreign_key)
114
+ when :has_one, :has_many
115
+ if reflection.options[:through]
116
+ through_query =
117
+ case reflection.through_reflection.macro
118
+ when :belongs_to
119
+ reflection.through_reflection.klass.where(reflection.through_reflection.klass.primary_key => changes_by_id.for_attribute(reflection.through_reflection.foreign_key))
120
+ when :has_many, :has_one
121
+ reflection.through_reflection.klass.where(reflection.through_reflection.foreign_key => changes_by_id.ids)
122
+ end
123
+ case reflection.source_reflection.macro
124
+ when :belongs_to
125
+ through_query.pluck(reflection.source_reflection.foreign_key)
126
+ when :has_many
127
+ reflection.source_reflection.klass.where(reflection.source_reflection.foreign_key => (through_query.ids)).ids
128
128
  end
129
+ else
130
+ reflection.klass.where(reflection.foreign_key => changes_by_id.ids).ids
129
131
  end
130
132
  end
131
133
  end
134
+
132
135
  end
133
136
  end
134
137
  end
@@ -57,7 +57,10 @@ module Tantot
57
57
  # it if it is a string or class
58
58
  watcher = args.first.is_a?(String) || args.first.is_a?(Class) ? args.shift : nil
59
59
 
60
- only_attributes = Array.wrap(options.fetch(:only, [])).collect(&:to_s)
60
+ raise ArgumentError.new("Only symbols are allowed as attribute filters") unless args.all? {|arg| arg.is_a?(Symbol)}
61
+ raise ArgumentError.new("Only one of arguments or :only option are valid attribute filters") if args.any? && options.key?(:only)
62
+
63
+ only_attributes = Array.wrap(options.fetch(:only, args)).collect(&:to_s)
61
64
  always_attributes = Array.wrap(options.fetch(:always, [])).collect(&:to_s)
62
65
 
63
66
  # Setup watch
@@ -72,7 +75,7 @@ module Tantot
72
75
  watch.block = block
73
76
  watch.watcher = watcher
74
77
 
75
- Tantot.agent_registry.register(watch)
78
+ agent = Tantot.agent_registry.register(watch)
76
79
 
77
80
  # Setup and register callbacks
78
81
  callback_options = {}.tap do |opts|
@@ -86,6 +89,8 @@ module Tantot
86
89
  after_save(callback_options, &update_proc)
87
90
  after_destroy(callback_options, &update_proc)
88
91
  end
92
+
93
+ agent
89
94
  end
90
95
  end
91
96
  end
@@ -1,7 +1,7 @@
1
1
  module Tantot
2
2
  module Strategy
3
3
  class Bypass
4
- def run(id, changes_by_model)
4
+ def run(agent, changes_by_model)
5
5
  # nop
6
6
  end
7
7
  end
@@ -9,8 +9,7 @@ module Tantot
9
9
  raise AgentNotFound.new("No registered agent with id #{id}") unless agent
10
10
 
11
11
  ::Chewy.strategy(chewy_strategy) do
12
- agent.peform(Tantot::Strategy::Sidekiq.unmarshal(changes_by_model))
13
- Tantot.manager.perform(context, changes)
12
+ agent.perform(Tantot::Strategy::Sidekiq.unmarshal(changes_by_model))
14
13
  end
15
14
  end
16
15
  end
@@ -1,3 +1,3 @@
1
1
  module Tantot
2
- VERSION = "0.1.8"
2
+ VERSION = "0.1.9"
3
3
  end
@@ -1,8 +1,7 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Tantot::Agent::Block do
4
-
5
- context 'using a block' do
4
+ context "normal usage" do
6
5
  let(:value) { {changes: 0} }
7
6
  let(:changes) { {obj: nil} }
8
7
  before do
@@ -22,7 +21,7 @@ describe Tantot::Agent::Block do
22
21
  expect(changes[:obj]).to eq(Tantot::Changes::ById.new({city.id => {"id" => [nil, 1]}}))
23
22
  end
24
23
 
25
- it "call a single time if multiple changes occur" do
24
+ it "should call a single time if multiple changes occur" do
26
25
  Tantot.manager.run do
27
26
  3.times { City.create! }
28
27
  end
@@ -30,4 +29,24 @@ describe Tantot::Agent::Block do
30
29
  expect(changes[:obj]).to eq(Tantot::Changes::ById.new({1=>{"id"=>[nil, 1]}, 2=>{"id"=>[nil, 2]}, 3=>{"id"=>[nil, 3]}}))
31
30
  end
32
31
  end
32
+
33
+ context "validations" do
34
+ it "should prevent registering twice with the same options" do
35
+ expect do
36
+ stub_model(:city) do
37
+ watch {}
38
+ watch {}
39
+ end
40
+ end.to raise_error(Tantot::MultipleWatchesProhibited, /Can't have multiple/)
41
+ end
42
+
43
+ it "should allow registering twice with different options" do
44
+ expect do
45
+ stub_model(:city) do
46
+ watch {}
47
+ watch(:name) {}
48
+ end
49
+ end.not_to raise_error
50
+ end
51
+ end
33
52
  end
@@ -4,6 +4,15 @@ if defined?(::Chewy)
4
4
 
5
5
  describe Tantot::Extensions::Chewy do
6
6
 
7
+ context "validations" do
8
+ it "should prevent registering on the same type more than once" do
9
+ stub_model(:city) do
10
+ watch_index 'foo'
11
+ end
12
+ expect { City.watch_index 'foo' }.to raise_error(Tantot::MultipleWatchesProhibited, /same type more than once/)
13
+ end
14
+ end
15
+
7
16
  [nil, :self, :class_method, :block].product([:some, :all]).each do |backreference_opt, attribute_opt|
8
17
  context "should update indexes using backreference: #{backreference_opt.inspect}, attributes: #{attribute_opt}" do
9
18
  let(:chewy_type) { double }
@@ -0,0 +1,59 @@
1
+ require "spec_helper"
2
+
3
+ describe Tantot::Observe::ActiveRecordMethods do
4
+
5
+ describe ".watch" do
6
+ before do
7
+ stub_class(:some_watcher) do
8
+ include Tantot::Watcher
9
+ end
10
+ end
11
+
12
+ it "should validate that at least a watcher or a block is defined" do
13
+ expect do
14
+ stub_model(:no_block_no_watcher_model) do
15
+ watch
16
+ end
17
+ end.to raise_error(Tantot::UnresolvableAgent, /Can't resolve/)
18
+ end
19
+
20
+ it "should validate that no more than one agent can be specified" do
21
+ expect do
22
+ stub_model(:block_and_watcher_model) do
23
+ watch(SomeWatcher) {}
24
+ end
25
+ end.to raise_error(Tantot::UnresolvableAgent, /More than one/)
26
+ end
27
+
28
+ it "should allow registering a simple block watch" do
29
+ stub_model(:block_model) do
30
+ watch {}
31
+ end
32
+ end
33
+
34
+ it "should allow registering a simple watcher watch" do
35
+ stub_model(:watcher_model) do
36
+ watch SomeWatcher
37
+ end
38
+ end
39
+
40
+ it "should treat additional arguments as the :only option" do
41
+ stub_model(:block_model)
42
+
43
+ agent = BlockModel.watch(:foo, :bar) {}
44
+
45
+ expect(agent.watches.first.attributes[:only]).to eq(['foo', 'bar'])
46
+ end
47
+
48
+ it "prevent bad arguments" do
49
+ stub_model(:block_model)
50
+
51
+ expect { BlockModel.watch(SomeWatcher, SomeWatcher, :foo, :bar) }.to raise_error(ArgumentError, /symbol/)
52
+ expect { BlockModel.watch(:foo, "string", :bar) {} }.to raise_error(ArgumentError, /symbol/)
53
+ expect { BlockModel.watch(:foo, SomeWatcher, :bar) {} }.to raise_error(ArgumentError, /symbol/)
54
+ expect { BlockModel.watch(:foo, :bar, only: :baz) {} }.to raise_error(ArgumentError, /Only one of/)
55
+ end
56
+ end
57
+
58
+ end
59
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tantot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - François-Pierre Bouchard
@@ -183,6 +183,7 @@ files:
183
183
  - spec/collector/options_spec.rb
184
184
  - spec/collector/watcher_spec.rb
185
185
  - spec/extensions/chewy_spec.rb
186
+ - spec/observe_spec.rb
186
187
  - spec/sidekiq_spec.rb
187
188
  - spec/spec_helper.rb
188
189
  - spec/tantot_spec.rb
@@ -217,6 +218,7 @@ test_files:
217
218
  - spec/collector/options_spec.rb
218
219
  - spec/collector/watcher_spec.rb
219
220
  - spec/extensions/chewy_spec.rb
221
+ - spec/observe_spec.rb
220
222
  - spec/sidekiq_spec.rb
221
223
  - spec/spec_helper.rb
222
224
  - spec/tantot_spec.rb