materialist 2.3.1 → 3.0.0

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: 4a5fb544fa815aa0d48535edb7ebb25bc6763477
4
- data.tar.gz: d3d505b4e193b8286a8a5f9b889d768a2aab197f
3
+ metadata.gz: 6c126eb6aa8d3e7fd91c9628f1a9a4a16e6476e8
4
+ data.tar.gz: 3d20acfd6c6ae59358dcfffb0b6270d438d6f7e4
5
5
  SHA512:
6
- metadata.gz: c4725e268051a7e8a95e45922d9c6b55c332ab362d8bdd6ffaa9fd5656bb19dd8996ab563b8093a028cff268ff302dac41ec8697bc83a22005416ee40e4327d1
7
- data.tar.gz: c8d683122d2671a8667283eacd95c806d31be3136d793a7d131ca7c222c0ad244235f8d8c6c03c322ccb6cc21393b842d3c0d06452a97f904f817c4c360fcce3
6
+ metadata.gz: 39310ca655d95cd3b51b6765da39f5e7ccb6479ca8df8680c5b6f27100cd7fd5479458d36b5baca80d80b1c8b29dec266453b2e2bd26c98dd4b65e01044b5351
7
+ data.tar.gz: df9c7c8f9c59a6a1b640f011deb7e848ac2cb9e705b288bbb6d8238c1c08e3db5a4cca8014ddfa061236d52a13d60d6a1d239758a414e352adc09e9b1a6d8b64
data/.travis.yml CHANGED
@@ -4,6 +4,6 @@ services:
4
4
  - redis-server
5
5
  rvm:
6
6
  - 2.4.0
7
- - ruby-head
7
+ - 2.5.0
8
8
  script:
9
9
  - bundle exec rspec
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in routemaster_client.gemspec
3
+ # Specify your gem's dependencies in materialist.gemspec
4
4
  gemspec
data/README.md CHANGED
@@ -52,27 +52,47 @@ class Zone < ApplicationRecord
52
52
  end
53
53
  ```
54
54
 
55
+ ### Materialist Configuration
56
+
57
+ If you need to override any of the materialist configurations,
58
+ you can do so in an `configure/initializers/materialist.rb` file:
59
+
60
+
61
+ ```ruby
62
+ Materialist.configure do |config|
63
+ # Configure materialist here. For example:
64
+ #
65
+ # config.topics = %w(topic_a topic_b)
66
+ #
67
+ # config.sidekiq_options = {
68
+ # queue: :routemaster_index,
69
+ # retry: 3
70
+ # }
71
+ #
72
+ # config.metrics_client = STATSD
73
+ end
74
+ ```
75
+
76
+ - `topics` (only when using in `.subscribe`): A string array of topics to be used.
77
+ If not provided nothing would be materialized.
78
+ - `sidekiq_options` (optional, default: `{ retry: 10 }`) -- See [Sidekiq docs](https://github.com/mperham/sidekiq/wiki/Advanced-Options#workers) for list of optiosn
79
+ - `metrics_client` (optional) -- You can pass your `STATSD` instance
80
+
55
81
  ### Routemaster Configuration
56
82
 
57
83
  First you need an "event handler":
58
84
 
59
85
  ```ruby
60
- handler = Materialist::EventHandler.new({ ...options })
86
+ handler = Materialist::EventHandler.new
61
87
  ```
62
88
 
63
89
  Where options could be:
64
90
 
65
- - `topics` (only when using in `.subscribe`): An array of topics to be used.
66
- If not provided nothing would be materialized.
67
- - `queue` (optional): name of the queue to be used by sidekiq worker
68
- - `retry` (default: `10`): sidekiq retry policy to be used.
69
-
70
91
  Then there are two ways to configure materialist in routemaster:
71
92
 
72
93
  1. **If you DON'T need resources to be cached in redis:** use `handler` as siphon:
73
94
 
74
95
  ```ruby
75
- handler = Materialist::EventHandler.new
76
96
  siphon_events = {
77
97
  zones: handler,
78
98
  rider_domain_riders: handler
@@ -86,15 +106,9 @@ map '/events' do
86
106
  end
87
107
  ```
88
108
 
89
- 2. **You DO need resources cached in redis:** In this case you need to use `handler` to subscribe to the caching pipeline:
109
+ 2. **You DO need resources cached in redis:** In this case you need to provide `topics` in `Materialist.configure` and use `handler` to subscribe to routemaster caching pipeline:
90
110
 
91
111
  ```ruby
92
- TOPICS = %w(
93
- zones
94
- rider_domain_riders
95
- )
96
-
97
- handler = Materialist::EventHandler.new({ topics: TOPICS })
98
112
  app = Routemaster::Drain::Caching.new # or ::Basic.new
99
113
  app.subscribe(handler, prefix: true)
100
114
  # ...
data/RELEASE_NOTES.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 3.0.0
2
+
3
+ Breaking change:
4
+ - `topics`, `retry` and `queue` options are now configured via `Materialist.configure` block.
5
+
6
+ Features:
7
+ - Ability to provide metrics client e.g. STATSD
8
+
1
9
  ## 2.3.1
2
10
 
3
11
  Fixes:
@@ -0,0 +1,30 @@
1
+ module Materialist
2
+ class << self
3
+ def configuration
4
+ @configuration ||= Configuration.new
5
+ end
6
+
7
+ def reset_configuration!
8
+ @configuration = Configuration.new
9
+ end
10
+
11
+ def configure
12
+ yield(self.configuration)
13
+ end
14
+ end
15
+
16
+ class Configuration
17
+ attr_accessor :topics, :sidekiq_options, :metrics_client
18
+
19
+ def initialize
20
+ @topics = []
21
+ @sidekiq_options = {}
22
+ @metrics_client = NullMetricsClient
23
+ end
24
+
25
+ class NullMetricsClient
26
+ def self.increment(_, tags:); end
27
+ def self.histogram(_, _v, tags:); end
28
+ end
29
+ end
30
+ end
data/lib/materialist.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require_relative './configuration'
2
+
1
3
  module Materialist
2
- VERSION = '2.3.1'
4
+ VERSION = '3.0.0'
3
5
  end
@@ -4,15 +4,13 @@ require_relative './event_worker'
4
4
  module Materialist
5
5
  class EventHandler
6
6
 
7
- # 10 retries takes approximately 6 hours
8
- DEFAULT_OPTIONS = { retry: 10 }
7
+ DEFAULT_SIDEKIQ_OPTIONS = { retry: 10 }.freeze
9
8
 
10
- def initialize(options={})
11
- @options = DEFAULT_OPTIONS.merge(options)
9
+ def initialize
12
10
  end
13
11
 
14
12
  def on_events_received(batch)
15
- batch.each { |event| call(event) if topics.include?(event['topic'].to_s) }
13
+ batch.each { |event| call(event) if should_materialize?(event['topic']) }
16
14
  end
17
15
 
18
16
  def call(event)
@@ -21,14 +19,18 @@ module Materialist
21
19
 
22
20
  private
23
21
 
24
- attr_reader :options
22
+ attr_reader :topics
25
23
 
26
- def topics
27
- @_topics ||= options.fetch(:topics, []).map(&:to_s)
24
+ def should_materialize?(topic)
25
+ Materialist.configuration.topics.include?(topic.to_s)
26
+ end
27
+
28
+ def sidekiq_options
29
+ DEFAULT_SIDEKIQ_OPTIONS.merge(Materialist.configuration.sidekiq_options)
28
30
  end
29
31
 
30
32
  def worker
31
- Materialist::EventWorker.set(options.slice(:queue, :retry))
33
+ Materialist::EventWorker.set(sidekiq_options)
32
34
  end
33
35
  end
34
36
  end
@@ -7,8 +7,35 @@ module Materialist
7
7
 
8
8
  def perform(event)
9
9
  topic = event['topic']
10
+ action = event['type'].to_sym
11
+ timestamp = event['t']
12
+
10
13
  materializer = "#{topic.to_s.singularize.classify}Materializer".constantize
11
- materializer.perform(event['url'], event['type'].to_sym)
14
+ materializer.perform(event['url'], action)
15
+
16
+ report_latency(topic, timestamp) if timestamp
17
+ report_stats(topic, action, :success)
18
+ rescue
19
+ report_stats(topic, action, :failure)
20
+ raise
21
+ end
22
+
23
+ private
24
+
25
+ def report_latency(topic, timestamp)
26
+ t = (Time.now.to_f - (timestamp.to_i / 1e3)).round(1)
27
+ Materialist.configuration.metrics_client.histogram(
28
+ "materialist.event_latency",
29
+ t,
30
+ tags: ["topic:#{topic}"]
31
+ )
32
+ end
33
+
34
+ def report_stats(topic, action, kind)
35
+ Materialist.configuration.metrics_client.increment(
36
+ "materialist.event_worker.#{kind}",
37
+ tags: ["action:#{action}", "topic:#{topic}"]
38
+ )
12
39
  end
13
40
  end
14
41
  end
data/materialist.gemspec CHANGED
@@ -20,6 +20,8 @@ Gem::Specification.new do |spec|
20
20
  spec.add_runtime_dependency 'activesupport'
21
21
  spec.add_runtime_dependency 'routemaster-drain', '>= 3.0'
22
22
 
23
+ spec.add_development_dependency 'activerecord'
24
+ spec.add_development_dependency 'sqlite3'
23
25
  spec.add_development_dependency 'dotenv'
24
26
  spec.add_development_dependency 'simplecov'
25
27
  spec.add_development_dependency 'codecov'
@@ -3,11 +3,16 @@ require 'materialist/event_handler'
3
3
  require 'materialist/event_worker'
4
4
 
5
5
  RSpec.describe Materialist::EventHandler do
6
- let(:options) {{}}
7
- subject { described_class.new options }
8
-
6
+ let(:topics) {[]}
7
+ let(:sidekiq_options) {{}}
9
8
  let(:worker_double) { double() }
9
+
10
10
  before do
11
+ Materialist.configure do |config|
12
+ config.sidekiq_options = sidekiq_options
13
+ config.topics = topics
14
+ end
15
+
11
16
  allow(Materialist::EventWorker).to receive(:set)
12
17
  .and_return worker_double
13
18
  end
@@ -17,7 +22,7 @@ RSpec.describe Materialist::EventHandler do
17
22
  let(:perform) { subject.on_events_received events.map() }
18
23
 
19
24
  context "when no topic is specified" do
20
- let(:options) {{ topics: [] }}
25
+ let(:topics) {[]}
21
26
 
22
27
  it "doesn't enqueue any event" do
23
28
  expect(worker_double).to_not receive(:perform_async)
@@ -26,7 +31,7 @@ RSpec.describe Materialist::EventHandler do
26
31
  end
27
32
 
28
33
  context "when a topic is specified" do
29
- let(:options) {{ topics: [:topic_a] }}
34
+ let(:topics) { %w(topic_a) }
30
35
 
31
36
  it "enqueues event of that topic" do
32
37
  expect(worker_double).to receive(:perform_async).with(events[0])
@@ -35,7 +40,7 @@ RSpec.describe Materialist::EventHandler do
35
40
  end
36
41
 
37
42
  context "when both topics are specified" do
38
- let(:options) {{ topics: [:topic_a, :topic_b] }}
43
+ let(:topics) { %w(topic_a topic_b) }
39
44
 
40
45
  it "enqueues event of both topics" do
41
46
  expect(worker_double).to receive(:perform_async).twice
@@ -55,7 +60,7 @@ RSpec.describe Materialist::EventHandler do
55
60
 
56
61
  context "if queue name is privided" do
57
62
  let(:queue_name) { :some_queue_name }
58
- let(:options) {{ queue: queue_name }}
63
+ let(:sidekiq_options) {{ queue: queue_name }}
59
64
 
60
65
  it "enqueues the event in the given queue" do
61
66
  expect(Materialist::EventWorker).to receive(:set)
@@ -66,7 +71,7 @@ RSpec.describe Materialist::EventHandler do
66
71
  end
67
72
 
68
73
  context "when a retry is specified in options" do
69
- let(:options) {{ retry: false }}
74
+ let(:sidekiq_options) {{ retry: false }}
70
75
 
71
76
  it "uses the given retry option for sidekiq" do
72
77
  expect(Materialist::EventWorker).to receive(:set)
@@ -5,17 +5,60 @@ RSpec.describe Materialist::EventWorker do
5
5
  describe "#perform" do
6
6
  let(:source_url) { 'https://service.dev/foobars/1' }
7
7
  let(:event) {{ 'topic' => :foobar, 'url' => source_url, 'type' => 'noop' }}
8
- let!(:materializer_class) { class FoobarMaterializer; end }
8
+ let!(:materializer_class) { FoobarMaterializer = Class.new }
9
+ let(:metrics_client) { double(increment: true) }
9
10
 
10
11
  before do
11
12
  allow(FoobarMaterializer).to receive(:perform)
13
+ Materialist.configure { |c| c.metrics_client = metrics_client }
12
14
  end
13
15
 
14
- context "when run synchronously" do
15
- let(:perform) { subject.perform(event) }
16
+ after { Object.send(:remove_const, :FoobarMaterializer) }
16
17
 
17
- it "calls the relevant materializer" do
18
- expect(FoobarMaterializer).to receive(:perform).with(source_url, :noop)
18
+ let(:perform) { subject.perform(event) }
19
+
20
+ it "calls the relevant materializer" do
21
+ expect(FoobarMaterializer).to receive(:perform).with(source_url, :noop)
22
+ perform
23
+ end
24
+
25
+ it 'logs success to metrics' do
26
+ expect(metrics_client).to receive(:increment).with(
27
+ "materialist.event_worker.success",
28
+ tags: ["action:noop", "topic:foobar"]
29
+ )
30
+ perform
31
+ end
32
+
33
+ it 'does not log latency' do
34
+ expect(metrics_client).to_not receive(:histogram)
35
+ perform
36
+ end
37
+
38
+ context 'when there is an error' do
39
+ let(:error){ StandardError.new }
40
+ before do
41
+ expect(FoobarMaterializer).to receive(:perform).and_raise error
42
+ end
43
+
44
+ it 'logs failure to metrics and re-raises the error' do
45
+ expect(metrics_client).to receive(:increment).with(
46
+ "materialist.event_worker.failure",
47
+ tags: ["action:noop", "topic:foobar"]
48
+ )
49
+ expect{ perform }.to raise_error error
50
+ end
51
+ end
52
+
53
+ context 'when event has a timestamp' do
54
+ let(:event) {{ 'topic' => :foobar, 'url' => source_url, 'type' => 'noop', 't' => '1519659773842' }}
55
+
56
+ it 'logs latency to metrics' do
57
+ expect(metrics_client).to receive(:histogram).with(
58
+ "materialist.event_latency",
59
+ instance_of(Float),
60
+ tags: ["topic:foobar"]
61
+ )
19
62
  perform
20
63
  end
21
64
  end
@@ -6,7 +6,7 @@ RSpec.describe Materialist::MaterializedRecord do
6
6
  uses_redis
7
7
 
8
8
  let!(:materialized_type) do
9
- class Foobar
9
+ Class.new do
10
10
  include Materialist::MaterializedRecord
11
11
 
12
12
  attr_accessor :source_url
@@ -19,7 +19,7 @@ RSpec.describe Materialist::MaterializedRecord do
19
19
  end
20
20
 
21
21
  let(:record) do
22
- Foobar.new.tap { |r| r.source_url = source_url }
22
+ materialized_type.new.tap { |r| r.source_url = source_url }
23
23
  end
24
24
 
25
25
  let(:country_url) { 'https://service.dev/countries/1' }
@@ -7,7 +7,7 @@ RSpec.describe Materialist::Materializer do
7
7
 
8
8
  describe "#perform" do
9
9
  let!(:materializer_class) do
10
- class FoobarMaterializer
10
+ FoobarMaterializer = Class.new do
11
11
  include Materialist::Materializer
12
12
 
13
13
  persist_to :foobar
@@ -27,122 +27,16 @@ RSpec.describe Materialist::Materializer do
27
27
  end
28
28
  end
29
29
  end
30
+ end
30
31
 
31
- class CityMaterializer
32
+ let!(:city_materializer) do
33
+ CityMaterializer = Class.new do
32
34
  include Materialist::Materializer
33
35
 
34
36
  persist_to :city
35
37
  source_key :source_url
36
38
  capture :name
37
39
  end
38
-
39
- class DefinedSourceMaterializer
40
- include Materialist::Materializer
41
-
42
- persist_to :defined_source
43
-
44
- source_key :source_id do |url|
45
- url.split('/').last.to_i
46
- end
47
-
48
- capture :name
49
- end
50
- end
51
-
52
- # this class mocks active record behaviour
53
- class BaseModel
54
- def update_attributes!(attrs)
55
- attrs.each { |k, v| send("#{k}=", v) }
56
- save!
57
- end
58
-
59
- def save!
60
- self.class.all[source_key_value] = self
61
- end
62
-
63
- def destroy!
64
- self.class.all.delete source_key_value
65
- end
66
-
67
- def reload
68
- self.class.all[source_key_value]
69
- end
70
-
71
- def actions_called
72
- @_actions_called ||= {}
73
- end
74
-
75
- private def source_key_value
76
- send(self.class.source_key_column)
77
- end
78
-
79
- class << self
80
- attr_accessor :error_to_throw_once_in_find_or_initialize_by,
81
- :source_key_column
82
-
83
- def find_or_initialize_by(kv_hash)
84
- if(err = error_to_throw_once_in_find_or_initialize_by)
85
- self.error_to_throw_once_in_find_or_initialize_by = nil
86
- raise err
87
- end
88
-
89
- key_value = kv_hash[source_key_column]
90
-
91
- (all[key_value] || self.new).tap do |record|
92
- record.send("#{source_key_column}=", key_value)
93
- end
94
- end
95
-
96
- def source_key_column
97
- @source_key_column || :source_url
98
- end
99
-
100
- def find_by(kv_hash)
101
- key_value = kv_hash[source_key_column]
102
- all[key_value]
103
- end
104
-
105
- def create!(attrs)
106
- new.tap do |record|
107
- record.update_attributes! attrs
108
- end
109
- end
110
-
111
- def all
112
- store[self.name] ||= {}
113
- end
114
-
115
- def destroy_all
116
- store[self.name] = {}
117
- end
118
-
119
- def store
120
- @@_store ||= {}
121
- end
122
-
123
- def count
124
- all.keys.size
125
- end
126
- end
127
- end
128
-
129
- class Foobar < BaseModel
130
- attr_accessor :source_url, :name, :how_old, :age, :timezone,
131
- :country_tld, :city_url, :account_url
132
- end
133
-
134
- class City < BaseModel
135
- attr_accessor :source_url, :name
136
- end
137
-
138
- class DefinedSource < BaseModel
139
- attr_accessor :source_id, :name
140
- self.source_key_column = :source_id
141
- end
142
-
143
- module ActiveRecord
144
- class RecordNotUnique < StandardError; end
145
- class RecordInvalid < StandardError; end
146
40
  end
147
41
 
148
42
  let(:country_url) { 'https://service.dev/countries/1' }
@@ -164,18 +58,19 @@ RSpec.describe Materialist::Materializer do
164
58
  end
165
59
 
166
60
  before do
167
- Foobar.destroy_all
168
- City.destroy_all
169
- DefinedSource.destroy_all
170
-
171
61
  stub_resource source_url, source_body
172
62
  stub_resource country_url, country_body
173
63
  stub_resource city_url, city_body
174
64
  stub_resource defined_source_url, defined_source_body
175
65
  end
176
66
 
67
+ after do
68
+ Object.send(:remove_const, :FoobarMaterializer)
69
+ Object.send(:remove_const, :CityMaterializer)
70
+ end
71
+
177
72
  let(:action) { :create }
178
- let(:perform) { FoobarMaterializer.perform(source_url, action) }
73
+ let(:perform) { materializer_class.perform(source_url, action) }
179
74
 
180
75
  it "materializes record in db" do
181
76
  expect{perform}.to change{Foobar.count}.by 1
@@ -212,31 +107,28 @@ RSpec.describe Materialist::Materializer do
212
107
  end
213
108
 
214
109
  context "when there is a race condition between a create and update" do
215
- let(:error) { }
110
+ let(:error) { nil }
216
111
  let!(:record) { Foobar.create!(source_url: source_url, name: 'mo') }
217
112
 
218
- before { Foobar.error_to_throw_once_in_find_or_initialize_by = error }
219
-
220
- [ ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid ].each do |error_type|
113
+ [ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid].each do |error_type|
221
114
  context "when error of type #{error_type.name} is thrown" do
222
115
  let(:error) { error_type }
223
116
 
224
117
  it "still updates the record" do
118
+ expect(Foobar).to receive(:find_or_initialize_by).ordered.and_raise(error)
119
+ expect(Foobar).to receive(:find_or_initialize_by).ordered.and_call_original
225
120
  expect{ perform }.to change { record.reload.name }
226
121
  .from('mo').to('jack')
227
122
  end
228
123
 
229
124
  context "if error was thrown second time" do
230
- before { allow(Foobar).to receive(:find_or_initialize_by).and_raise error }
231
-
232
125
  it "bubbles up the error" do
126
+ expect(Foobar).to receive(:find_or_initialize_by).and_raise(error).twice
233
127
  expect{ perform }.to raise_error error
234
128
  end
235
129
  end
236
-
237
130
  end
238
131
  end
239
-
240
132
  end
241
133
 
242
134
  %i(create update noop).each do |action_name|
@@ -276,21 +168,18 @@ RSpec.describe Materialist::Materializer do
276
168
 
277
169
  context "when {after, before}_upsert is configured" do
278
170
  let!(:record) { Foobar.create!(source_url: source_url, name: 'mo') }
171
+ let(:actions_called) { materializer_class.class_variable_get(:@@actions_called) }
279
172
  let!(:materializer_class) do
280
- class FoobarMaterializer
173
+ FoobarMaterializer = Class.new do
281
174
  include Materialist::Materializer
175
+ @@actions_called = {}
282
176
 
283
177
  persist_to :foobar
284
- before_upsert :my_before_method
285
- after_upsert :my_method
286
-
287
- def my_method(entity)
288
- entity.actions_called[:after_upsert] = true
289
- end
178
+ before_upsert :before_hook
179
+ after_upsert :after_hook
290
180
 
291
- def my_before_method(entity)
292
- entity.actions_called[:before_upsert] = true
293
- end
181
+ def before_hook(entity); @@actions_called[:before_hook] = true; end
182
+ def after_hook(entity); @@actions_called[:after_hook] = true; end
294
183
  end
295
184
  end
296
185
 
@@ -298,30 +187,36 @@ RSpec.describe Materialist::Materializer do
298
187
  context "when action is :#{action_name}" do
299
188
  let(:action) { action_name }
300
189
  it "calls before_upsert method" do
301
- expect{ perform }.to change { record.actions_called[:before_upsert] }
190
+ expect{ perform }.to change { actions_called[:before_hook] }
302
191
  end
303
192
 
304
193
  it "calls after_upsert method" do
305
- expect{ perform }.to change { record.actions_called[:after_upsert] }
194
+ expect{ perform }.to change { actions_called[:after_hook] }
306
195
  end
307
196
 
308
- it "calls more than one method" do
309
- class FoobarMaterializer
310
- before_upsert :my_before_method, :my_before_method2
311
- after_upsert :my_method, :my_method2
197
+ context "when configured with more than one hook" do
198
+ let(:materializer_class) do
199
+ FoobarMaterializer = Class.new do
200
+ include Materialist::Materializer
201
+ @@actions_called = {}
312
202
 
313
- def my_method2(entity)
314
- entity.actions_called[:after_upsert2] = true
315
- end
203
+ persist_to :foobar
204
+ before_upsert :before_hook, :before_hook2
205
+ after_upsert :after_hook, :after_hook2
316
206
 
317
- def my_before_method2(entity)
318
- entity.actions_called[:before_upsert2] = true
207
+ def before_hook(entity); @@actions_called[:before_hook] = true; end
208
+ def before_hook2(entity); @@actions_called[:before_hook2] = true; end
209
+ def after_hook(entity); @@actions_called[:after_hook] = true; end
210
+ def after_hook2(entity); @@actions_called[:after_hook2] = true; end
319
211
  end
320
212
  end
321
- expect{ perform }.to change { record.actions_called[:after_upsert] }
322
- .and change { record.actions_called[:after_upsert2] }
323
- .and change { record.actions_called[:before_upsert] }
324
- .and change { record.actions_called[:before_upsert2] }
213
+
214
+ it "calls more than one method" do
215
+ expect{ perform }.to change { actions_called[:before_hook] }
216
+ .and change { actions_called[:before_hook2] }
217
+ .and change { actions_called[:after_hook] }
218
+ .and change { actions_called[:after_hook2] }
219
+ end
325
220
  end
326
221
  end
327
222
  end
@@ -330,11 +225,11 @@ RSpec.describe Materialist::Materializer do
330
225
  let(:action) { :delete }
331
226
 
332
227
  it "does not call after_upsert method" do
333
- expect{ perform }.to_not change { record.actions_called[:after_upsert] }
228
+ expect{ perform }.to_not change { actions_called[:after_hook] }
334
229
  end
335
230
 
336
231
  it "does call after_upsert method" do
337
- expect{ perform }.to_not change { record.actions_called[:before_upsert] }
232
+ expect{ perform }.to_not change { actions_called[:before_hook] }
338
233
  end
339
234
  end
340
235
 
@@ -342,21 +237,18 @@ RSpec.describe Materialist::Materializer do
342
237
 
343
238
  context "when {before, after}_destroy is configured" do
344
239
  let!(:record) { Foobar.create!(source_url: source_url, name: 'mo') }
240
+ let(:actions_called) { materializer_class.class_variable_get(:@@actions_called) }
345
241
  let!(:materializer_class) do
346
- class FoobarMaterializer
242
+ FoobarMaterializer = Class.new do
347
243
  include Materialist::Materializer
244
+ @@actions_called = {}
348
245
 
349
246
  persist_to :foobar
350
- before_destroy :my_before_method
351
- after_destroy :my_method
352
-
353
- def my_method(entity)
354
- entity.actions_called[:after_destroy] = true
355
- end
247
+ before_destroy :before_hook
248
+ after_destroy :after_hook
356
249
 
357
- def my_before_method(entity)
358
- entity.actions_called[:before_destroy] = true unless entity.nil?
359
- end
250
+ def before_hook(entity); @@actions_called[:before_hook] = true; end
251
+ def after_hook(entity); @@actions_called[:after_hook] = true; end
360
252
  end
361
253
  end
362
254
 
@@ -364,11 +256,11 @@ RSpec.describe Materialist::Materializer do
364
256
  context "when action is :#{action_name}" do
365
257
  let(:action) { action_name }
366
258
  it "does not call after_destroy method" do
367
- expect{ perform }.to_not change { record.actions_called[:after_destroy] }
259
+ expect{ perform }.to_not change { actions_called[:after_hook] }
368
260
  end
369
261
 
370
262
  it "does not call before_destroy method" do
371
- expect{ perform }.to_not change { record.actions_called[:before_destroy] }
263
+ expect{ perform }.to_not change { actions_called[:before_hook] }
372
264
  end
373
265
  end
374
266
  end
@@ -377,54 +269,60 @@ RSpec.describe Materialist::Materializer do
377
269
  let(:action) { :delete }
378
270
 
379
271
  it "calls after_destroy method" do
380
- expect{ perform }.to change { record.actions_called[:after_destroy] }
272
+ expect{ perform }.to change { actions_called[:after_hook] }
381
273
  end
382
274
 
383
275
  it "calls before_destroy method" do
384
- expect{ perform }.to change { record.actions_called[:before_destroy] }
276
+ expect{ perform }.to change { actions_called[:before_hook] }
385
277
  end
386
278
 
387
- it "calls more than one method" do
388
- class FoobarMaterializer
389
- before_destroy :my_before_method, :my_before_method2
390
- after_destroy :my_method, :my_method2
279
+ context "when configured with more than one hook" do
280
+ let(:materializer_class) do
281
+ FoobarMaterializer = Class.new do
282
+ include Materialist::Materializer
283
+ @@actions_called = {}
391
284
 
392
- def my_method2(entity)
393
- entity.actions_called[:after_destroy2] = true
394
- end
285
+ persist_to :foobar
286
+ before_destroy :before_hook, :before_hook2
287
+ after_destroy :after_hook, :after_hook2
395
288
 
396
- def my_before_method2(entity)
397
- entity.actions_called[:before_destroy2] = true unless entity.nil?
289
+ def before_hook(entity); @@actions_called[:before_hook] = true; end
290
+ def before_hook2(entity); @@actions_called[:before_hook2] = true; end
291
+ def after_hook(entity); @@actions_called[:after_hook] = true; end
292
+ def after_hook2(entity); @@actions_called[:after_hook2] = true; end
398
293
  end
399
294
  end
400
- expect{ perform }.to change { record.actions_called[:after_destroy] }
401
- .and change { record.actions_called[:after_destroy2] }
402
- .and change { record.actions_called[:before_destroy] }
403
- .and change { record.actions_called[:before_destroy2] }
295
+
296
+ it "calls more than one method" do
297
+ expect{ perform }.to change { actions_called[:before_hook] }
298
+ .and change { actions_called[:before_hook2] }
299
+ .and change { actions_called[:after_hook] }
300
+ .and change { actions_called[:after_hook2] }
301
+ end
404
302
  end
405
303
 
406
304
  context "when resource doesn't exist locally" do
407
305
  it "does not raise error" do
408
- Foobar.destroy_all
306
+ Foobar.delete_all
409
307
  expect{ perform }.to_not raise_error
410
308
  end
411
309
  end
412
310
  end
413
-
414
311
  end
415
312
 
416
313
  context "when not materializing self but materializing linked parent" do
417
- class CitySettingsMaterializer
418
- include Materialist::Materializer
314
+ subject do
315
+ Class.new do
316
+ include Materialist::Materializer
419
317
 
420
- materialize_link :city
318
+ materialize_link :city
319
+ end
421
320
  end
422
-
423
321
  let(:city_settings_url) { 'https://service.dev/city_settings/1' }
424
322
  let(:city_settings_body) {{ _links: { city: { href: city_url }}}}
425
323
  before { stub_resource city_settings_url, city_settings_body }
426
324
 
427
- let(:perform) { CitySettingsMaterializer.perform(city_settings_url, action) }
325
+ let(:perform) { subject.perform(city_settings_url, action) }
428
326
 
429
327
  it "materializes linked parent" do
430
328
  expect{perform}.to change{City.count}.by 1
@@ -439,47 +337,63 @@ RSpec.describe Materialist::Materializer do
439
337
  end
440
338
  end
441
339
 
442
- context "when creating a new entity based on the source_key column" do
443
- let(:perform) { DefinedSourceMaterializer.perform(defined_source_url, action) }
340
+ context "entity based on the source_key column" do
341
+ subject do
342
+ Class.new do
343
+ include Materialist::Materializer
444
344
 
445
- it "creates based on source_key" do
446
- expect{perform}.to change{DefinedSource.count}.by 1
447
- end
345
+ persist_to :defined_source
346
+
347
+ source_key :source_id do |url|
348
+ url.split('/').last.to_i
349
+ end
448
350
 
449
- it "sets the correct source key" do
450
- perform
451
- inserted = DefinedSource.find_by(source_id: defined_source_id)
452
- expect(inserted.source_id).to eq defined_source_id
453
- expect(inserted.name).to eq defined_source_body[:name]
351
+ capture :name
352
+ end
454
353
  end
455
- end
456
354
 
457
- context "when updating a new entity based on the source_key column" do
458
- let(:action) { :update }
459
- let!(:record) { DefinedSource.create!(source_id: defined_source_id, name: 'mo') }
460
- let(:perform) { DefinedSourceMaterializer.perform(defined_source_url, action) }
355
+ context "when creating" do
356
+ let(:perform) { subject.perform(defined_source_url, action) }
461
357
 
462
- it "updates based on source_key" do
463
- perform
464
- expect(DefinedSource.count).to eq 1
358
+ it "creates based on source_key" do
359
+ expect{perform}.to change{DefinedSource.count}.by 1
360
+ end
361
+
362
+ it "sets the correct source key" do
363
+ perform
364
+ inserted = DefinedSource.find_by(source_id: defined_source_id)
365
+ expect(inserted.source_id).to eq defined_source_id
366
+ expect(inserted.name).to eq defined_source_body[:name]
367
+ end
465
368
  end
466
369
 
467
- it "updates the existing record" do
468
- perform
469
- inserted = DefinedSource.find_by(source_id: defined_source_id)
470
- expect(inserted.source_id).to eq defined_source_id
471
- expect(inserted.name).to eq defined_source_body[:name]
370
+ context "when updating" do
371
+ let(:action) { :update }
372
+ let!(:record) { DefinedSource.create!(source_id: defined_source_id, name: 'mo') }
373
+ let(:perform) { subject.perform(defined_source_url, action) }
374
+
375
+ it "updates based on source_key" do
376
+ perform
377
+ expect(DefinedSource.count).to eq 1
378
+ end
379
+
380
+ it "updates the existing record" do
381
+ perform
382
+ inserted = DefinedSource.find_by(source_id: defined_source_id)
383
+ expect(inserted.source_id).to eq defined_source_id
384
+ expect(inserted.name).to eq defined_source_body[:name]
385
+ end
472
386
  end
473
- end
474
387
 
475
- context "when deleting an entity based on the source_key column" do
476
- let(:action) { :delete }
477
- let!(:record) { DefinedSource.create!(source_id: defined_source_id, name: 'mo') }
478
- let(:perform) { DefinedSourceMaterializer.perform(defined_source_url, action) }
388
+ context "when deleting" do
389
+ let(:action) { :delete }
390
+ let!(:record) { DefinedSource.create!(source_id: defined_source_id, name: 'mo') }
391
+ let(:perform) { subject.perform(defined_source_url, action) }
479
392
 
480
- it "deletes based on source_key" do
481
- perform
482
- expect(DefinedSource.count).to eq 0
393
+ it "deletes based on source_key" do
394
+ perform
395
+ expect(DefinedSource.count).to eq 0
396
+ end
483
397
  end
484
398
  end
485
399
  end
data/spec/models.rb ADDED
@@ -0,0 +1,8 @@
1
+ class Foobar < ActiveRecord::Base
2
+ end
3
+
4
+ class City < ActiveRecord::Base
5
+ end
6
+
7
+ class DefinedSource < ActiveRecord::Base
8
+ end
data/spec/schema.rb ADDED
@@ -0,0 +1,31 @@
1
+ ActiveRecord::Schema.define do
2
+ self.verbose = false
3
+
4
+ create_table :foobars, :force => true do |t|
5
+ t.string :source_url, null: false
6
+ t.string :name
7
+ t.integer :how_old
8
+ t.integer :age
9
+ t.string :timezone
10
+ t.string :country_tld
11
+ t.string :city_url
12
+ t.string :account_url
13
+
14
+ t.timestamps
15
+ end
16
+
17
+ create_table :cities, :force => true do |t|
18
+ t.string :source_url, null: false
19
+ t.string :name
20
+
21
+ t.timestamps
22
+ end
23
+
24
+ create_table :defined_sources, :force => true do |t|
25
+ t.integer :source_id, null: false
26
+ t.string :name
27
+
28
+ t.timestamps
29
+ end
30
+
31
+ end
data/spec/spec_helper.rb CHANGED
@@ -19,9 +19,24 @@ RSpec.configure do |config|
19
19
  config.run_all_when_everything_filtered = true
20
20
  config.raise_errors_for_deprecations!
21
21
 
22
+ config.before(:each) do
23
+ Materialist.reset_configuration!
24
+
25
+ # clear database
26
+ ActiveRecord::Base.descendants.each(&:delete_all)
27
+ end
28
+
22
29
  # Run specs in random order to surface order dependencies. If you find an
23
30
  # order dependency and want to debug it, you can fix the order by providing
24
31
  # the seed, which is printed after each run.
25
32
  # --seed 1234
26
33
  config.order = 'random'
27
34
  end
35
+
36
+ # active record
37
+ require 'active_record'
38
+
39
+ ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
40
+
41
+ load File.dirname(__FILE__) + '/schema.rb'
42
+ load File.dirname(__FILE__) + '/models.rb'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: materialist
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mo Valipour
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-01-31 00:00:00.000000000 Z
11
+ date: 2018-02-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sidekiq
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activerecord
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: sqlite3
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'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: dotenv
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -170,6 +198,7 @@ files:
170
198
  - RELEASE_NOTES.md
171
199
  - Rakefile
172
200
  - appraise
201
+ - lib/configuration.rb
173
202
  - lib/materialist.rb
174
203
  - lib/materialist/errors.rb
175
204
  - lib/materialist/event_handler.rb
@@ -181,6 +210,8 @@ files:
181
210
  - spec/materialist/event_worker_spec.rb
182
211
  - spec/materialist/materialized_record_spec.rb
183
212
  - spec/materialist/materializer_spec.rb
213
+ - spec/models.rb
214
+ - spec/schema.rb
184
215
  - spec/spec_helper.rb
185
216
  - spec/support/uses_redis.rb
186
217
  homepage: http://github.com/deliveroo/materialist
@@ -212,5 +243,7 @@ test_files:
212
243
  - spec/materialist/event_worker_spec.rb
213
244
  - spec/materialist/materialized_record_spec.rb
214
245
  - spec/materialist/materializer_spec.rb
246
+ - spec/models.rb
247
+ - spec/schema.rb
215
248
  - spec/spec_helper.rb
216
249
  - spec/support/uses_redis.rb