materialist 2.3.1 → 3.0.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.
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