tantot 0.1.8 → 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
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