materialist 0.0.1 → 0.0.2

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: ad0c95193ee887dc776cdb28f54fa1fd7a00212a
4
- data.tar.gz: 6e3dfb760278e54a275acdc667f5b32fe1809374
3
+ metadata.gz: 3e1d3d42e45eb0a9130ca412078f6df11e29da53
4
+ data.tar.gz: 0041f0349dfce34d055ff59a5a4f2d4fc694bb26
5
5
  SHA512:
6
- metadata.gz: 54a84928a4d988d22741f038d02bc47757b331ff45f45cef201829691b204a239745a89abc9b67fac706f267fda054b0e65094ef1d62ac9836282e7ec608d448
7
- data.tar.gz: 8758db7954c895a28ac969f4f166b570451f338a1a11d449ee9d9f8bae3e1dcc66cb57d1867af24bb9e3dcee1497d4c557411924a616171fbc2fd6580f2afb16
6
+ metadata.gz: 0df7a1d5dab2b2d6f4fe9769e73b67028b2398dca4b309081d172e7b97c3b8729088957ffb8fcd24c1e045f6eba287cd1af96f81d9811fb2fa5470f36800e6e5
7
+ data.tar.gz: bfaf53768945dc8b98cad7a6191aa9ef588b680898546353d5a756f4e4286a867ae3f00067c69961fe2efb9c5b820457ac620df2cf74f5ca1db5bb2983090ed9
data/.gitignore CHANGED
@@ -9,3 +9,4 @@ tags
9
9
  *.swo
10
10
  coverage/
11
11
  .byebug_history
12
+ *.gem
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ cache: bundler
3
+ services:
4
+ - redis-server
5
+ rvm:
6
+ - 2.4.0
7
+ - ruby-head
8
+ script:
9
+ - bundle exec rspec
data/README.md CHANGED
@@ -115,3 +115,28 @@ class ZoneMaterializer
115
115
  end
116
116
  end
117
117
  ```
118
+
119
+ ### Materialized record
120
+
121
+ Imagine you have materialized rider from a routemaster topic and you need to access a key from the remote source that you HAVEN'T materialized locally.
122
+
123
+ > NOTE that doing such thing is only acceptable if you use `caching` drain, otherwise every time the remote source is fetched a fresh http call is made which will result in hammering of the remote service.
124
+
125
+ > Also it is unacceptable to iterate through a large set of records and call on remote sources. Any such data should be materialised because database (compared to redis cache) is more optimised to perform scan operations.
126
+
127
+ ```ruby
128
+ class Rider
129
+ include Materialist::MaterializedRecord
130
+
131
+ source_link_reader :city
132
+ source_link_reader :country, via: :city
133
+ end
134
+ ```
135
+
136
+ Above will give you `.source`, `.city` and `.country` on any instances of `Rider`, allowing you to access remote keys.
137
+
138
+ e.g.
139
+
140
+ ```ruby
141
+ Rider.last.country.created_at
142
+ ```
@@ -0,0 +1,36 @@
1
+ require 'routemaster/api_client'
2
+
3
+ module Materialist
4
+ module MaterializedRecord
5
+
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def source_link_reader(*keys, via: nil)
12
+ keys.each do |key|
13
+ define_method(key) do
14
+ raw = source_raw
15
+ raw = raw.send(via).show if via
16
+ raw.send(key).show.body
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ def source
23
+ source_raw.body
24
+ end
25
+
26
+ def source_raw
27
+ api_client.get(source_url)
28
+ end
29
+
30
+ def api_client
31
+ @_api_client ||= Routemaster::APIClient.new(
32
+ response_class: Routemaster::Responses::HateoasResponse
33
+ )
34
+ end
35
+ end
36
+ end
@@ -1,3 +1,4 @@
1
+ require 'active_support/inflector'
1
2
  require 'routemaster/api_client'
2
3
 
3
4
  module Materialist
@@ -62,6 +63,9 @@ module Materialist
62
63
  materialist_options[:after_upsert] = method_name
63
64
  end
64
65
 
66
+ def after_destroy(method_name)
67
+ materialist_options[:after_destroy] = method_name
68
+ end
65
69
  end
66
70
 
67
71
  class Materializer
@@ -79,7 +83,11 @@ module Materialist
79
83
  end
80
84
 
81
85
  def destroy
82
- model_class.where(source_url: url).destroy_all
86
+ model_class.find_by(source_url: url).tap do |entity|
87
+ entity.destroy!.tap do |entity|
88
+ instance.send(after_destroy, entity) if after_destroy
89
+ end if entity
90
+ end
83
91
  end
84
92
 
85
93
  private
@@ -101,6 +109,10 @@ module Materialist
101
109
  options[:after_upsert]
102
110
  end
103
111
 
112
+ def after_destroy
113
+ options[:after_destroy]
114
+ end
115
+
104
116
  def model_class
105
117
  options.fetch(:model_class).to_s.classify.constantize
106
118
  end
data/lib/materialist.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Materialist
2
- VERSION = '0.0.1'
2
+ VERSION = '0.0.2'
3
3
  end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+ require 'support/uses_redis'
3
+ require 'materialist/materialized_record'
4
+
5
+ RSpec.describe Materialist::MaterializedRecord do
6
+ uses_redis
7
+
8
+ let!(:materialized_type) do
9
+ class Foobar
10
+ include Materialist::MaterializedRecord
11
+
12
+ attr_accessor :source_url
13
+ source_link_reader :city
14
+ source_link_reader :country, via: :city
15
+ end
16
+ end
17
+
18
+ let(:record) do
19
+ Foobar.new.tap { |r| r.source_url = source_url }
20
+ end
21
+
22
+ let(:country_url) { 'https://service.dev/countries/1' }
23
+ let(:country_body) {{ tld: 'fr' }}
24
+ let(:city_url) { 'https://service.dev/cities/1' }
25
+ let(:city_body) {{ _links: { country: { href: country_url }}, timezone: 'Europe/Paris' }}
26
+ let(:source_url) { 'https://service.dev/foobars/1' }
27
+ let(:source_body) {{ _links: { city: { href: city_url }}, name: 'jack', age: 30 }}
28
+ before do
29
+ stub_request(:get, source_url).to_return(
30
+ status: 200,
31
+ body: source_body.to_json,
32
+ headers: { 'Content-Type' => 'application/json' }
33
+ )
34
+ stub_request(:get, country_url).to_return(
35
+ status: 200,
36
+ body: country_body.to_json,
37
+ headers: { 'Content-Type' => 'application/json' }
38
+ )
39
+ stub_request(:get, city_url).to_return(
40
+ status: 200,
41
+ body: city_body.to_json,
42
+ headers: { 'Content-Type' => 'application/json' }
43
+ )
44
+ end
45
+
46
+ describe "#source" do
47
+ it "returns the representation of the source" do
48
+ expect(record.source.name).to eq 'jack'
49
+ expect(record.source.age).to eq 30
50
+ end
51
+ end
52
+
53
+ describe "simple link reader" do
54
+ it "returns the representation of the link source" do
55
+ expect(record.city.timezone).to eq 'Europe/Paris'
56
+ end
57
+ end
58
+
59
+ describe "simple link reader via another link" do
60
+ it "returns the representation of the link source" do
61
+ expect(record.country.tld).to eq 'fr'
62
+ end
63
+ end
64
+ end
@@ -1,7 +1,10 @@
1
1
  require 'spec_helper'
2
+ require 'support/uses_redis'
2
3
  require 'materialist/materializer'
3
4
 
4
5
  RSpec.describe Materialist::Materializer do
6
+ uses_redis
7
+
5
8
  describe "#perform" do
6
9
  let!(:materializer_class) do
7
10
  class FoobarMaterializer
@@ -21,7 +24,62 @@ RSpec.describe Materialist::Materializer do
21
24
  end
22
25
  end
23
26
 
24
- let!(:foobar_class) { class Foobar; end }
27
+ # this class mocks active record behaviour
28
+ class Foobar
29
+ attr_accessor :source_url, :name, :how_old, :age, :timezone, :country_tld
30
+
31
+ def update_attributes(attrs)
32
+ attrs.each { |k, v| send("#{k}=", v) }
33
+ end
34
+
35
+ def save!
36
+ self.class.all[source_url] = self
37
+ end
38
+
39
+ def destroy!
40
+ self.class.all.delete source_url
41
+ end
42
+
43
+ def reload
44
+ self.class.all[source_url]
45
+ end
46
+
47
+ def actions_called
48
+ @_actions_called ||= {}
49
+ end
50
+
51
+ class << self
52
+ def find_or_initialize_by(source_url:)
53
+ (all[source_url] || Foobar.new).tap do |record|
54
+ record.source_url = source_url
55
+ end
56
+ end
57
+
58
+ def find_by(source_url:)
59
+ all[source_url]
60
+ end
61
+
62
+ def create!(attrs)
63
+ new.tap do |record|
64
+ record.update_attributes attrs
65
+ record.save!
66
+ end
67
+ end
68
+
69
+ def all
70
+ @@_store ||= {}
71
+ end
72
+
73
+ def flush_all
74
+ @@_store = {}
75
+ end
76
+
77
+ def count
78
+ all.keys.size
79
+ end
80
+ end
81
+ end
82
+
25
83
  let(:country_url) { 'https://service.dev/countries/1' }
26
84
  let(:country_body) {{ tld: 'fr' }}
27
85
  let(:city_url) { 'https://service.dev/cities/1' }
@@ -29,6 +87,7 @@ RSpec.describe Materialist::Materializer do
29
87
  let(:source_url) { 'https://service.dev/foobars/1' }
30
88
  let(:source_body) {{ _links: { city: { href: city_url }}, name: 'jack', age: 30 }}
31
89
  before do
90
+ Foobar.flush_all
32
91
  stub_request(:get, source_url).to_return(
33
92
  status: 200,
34
93
  body: source_body.to_json,
@@ -46,47 +105,50 @@ RSpec.describe Materialist::Materializer do
46
105
  )
47
106
  end
48
107
 
49
- let(:expected_attributes) do
50
- { name: 'jack', how_old: 30, country_tld: 'fr', timezone: 'Europe/Paris' }
51
- end
52
-
53
- let(:record_double) { double() }
54
- before do
55
- allow(Foobar).to receive(:find_or_initialize_by).and_return record_double
56
- end
57
-
58
108
  let(:action) { :create }
59
109
  let(:perform) { FoobarMaterializer.perform(source_url, action) }
60
110
 
61
- def performs_upsert
62
- expect(Foobar).to receive(:find_or_initialize_by)
63
- .with(source_url: source_url)
64
- expect(record_double).to receive(:update_attributes).with(expected_attributes)
65
- expect(record_double).to receive(:save!)
66
- perform
111
+ it "inserts record in db" do
112
+ expect{perform}.to change{Foobar.count}.by 1
113
+ inserted = Foobar.find_by(source_url: source_url)
114
+ expect(inserted.name).to eq source_body[:name]
115
+ expect(inserted.how_old).to eq source_body[:age]
116
+ expect(inserted.timezone).to eq city_body[:timezone]
117
+ expect(inserted.country_tld).to eq country_body[:tld]
67
118
  end
68
119
 
69
- def performs_destroy
70
- expect(Foobar).to receive(:where)
71
- .with(source_url: source_url)
72
- .and_return record_double
73
- expect(record_double).to receive(:destroy_all)
74
- perform
75
- end
120
+ context "when record already exists" do
121
+ let!(:record) { Foobar.create!(source_url: source_url, name: 'mo') }
76
122
 
77
- it { performs_upsert }
123
+ it "updates the existing record" do
124
+ expect{ perform }.to change { record.reload.name }
125
+ .from('mo').to('jack')
126
+ end
127
+
128
+ context "when action is :delete" do
129
+ let(:action) { :delete }
130
+
131
+ it "removes record from db" do
132
+ expect{perform}.to change{Foobar.count}.by -1
133
+ end
134
+ end
135
+ end
78
136
 
79
137
  %i(create update noop).each do |action_name|
80
138
  context "when action is :#{action_name}" do
81
139
  let(:action) { action_name }
82
- it { performs_upsert }
140
+ it "inserts record in db" do
141
+ expect{perform}.to change{Foobar.count}.by 1
142
+ end
83
143
  end
84
144
  end
85
145
 
86
- context "when action is :delete" do
146
+ context "when action is :delete and no existing record in db" do
87
147
  let(:action) { :delete }
88
148
 
89
- it { performs_destroy }
149
+ it "does not remove anything from db" do
150
+ expect{perform}.to change{Foobar.count}.by 0
151
+ end
90
152
  end
91
153
 
92
154
  context "if resource returns 404" do
@@ -100,17 +162,15 @@ RSpec.describe Materialist::Materializer do
100
162
  context "if a linked resource returns 404" do
101
163
  before { stub_request(:get, city_url).to_return(status: 404) }
102
164
 
103
- let(:expected_attributes) do
104
- { name: 'jack', how_old: 30 }
105
- end
106
-
107
165
  it "ignores keys from the relation" do
108
- performs_upsert
166
+ expect{perform}.to change{Foobar.count}.by 1
167
+ inserted = Foobar.find_by(source_url: source_url)
168
+ expect(inserted.country_tld).to eq nil
109
169
  end
110
170
  end
111
171
 
112
172
  context "when after_upsert is configured" do
113
- let(:expected_attributes) {{}}
173
+ let!(:record) { Foobar.create!(source_url: source_url, name: 'mo') }
114
174
  let!(:materializer_class) do
115
175
  class FoobarMaterializer
116
176
  include Materialist::Materializer
@@ -119,7 +179,7 @@ RSpec.describe Materialist::Materializer do
119
179
  after_upsert :my_method
120
180
 
121
181
  def my_method(entity)
122
- entity.after_upsert_action
182
+ entity.actions_called[:after_upsert] = true
123
183
  end
124
184
  end
125
185
  end
@@ -128,8 +188,7 @@ RSpec.describe Materialist::Materializer do
128
188
  context "when action is :#{action_name}" do
129
189
  let(:action) { action_name }
130
190
  it "calls after_upsert method" do
131
- expect(record_double).to receive(:after_upsert_action)
132
- performs_upsert
191
+ expect{ perform }.to change { record.actions_called[:after_upsert] }
133
192
  end
134
193
  end
135
194
  end
@@ -138,8 +197,48 @@ RSpec.describe Materialist::Materializer do
138
197
  let(:action) { :delete }
139
198
 
140
199
  it "does not call after_upsert method" do
141
- expect(record_double).to_not receive(:after_upsert_action)
142
- performs_destroy
200
+ expect{ perform }.to_not change { record.actions_called[:after_upsert] }
201
+ end
202
+ end
203
+
204
+ end
205
+
206
+ context "when after_destroy is configured" do
207
+ let!(:record) { Foobar.create!(source_url: source_url, name: 'mo') }
208
+ let!(:materializer_class) do
209
+ class FoobarMaterializer
210
+ include Materialist::Materializer
211
+
212
+ use_model :foobar
213
+ after_destroy :my_method
214
+
215
+ def my_method(entity)
216
+ entity.actions_called[:after_destroy] = true
217
+ end
218
+ end
219
+ end
220
+
221
+ %i(create update noop).each do |action_name|
222
+ context "when action is :#{action_name}" do
223
+ let(:action) { action_name }
224
+ it "does not call after_destroy method" do
225
+ expect{ perform }.to_not change { record.actions_called[:after_destroy] }
226
+ end
227
+ end
228
+ end
229
+
230
+ context "when action is :delete" do
231
+ let(:action) { :delete }
232
+
233
+ it "calls after_destroy method" do
234
+ expect{ perform }.to change { record.actions_called[:after_destroy] }
235
+ end
236
+
237
+ context "when resource doesn't exist locally" do
238
+ it "does not raise error" do
239
+ Foobar.flush_all
240
+ expect{ perform }.to_not raise_error
241
+ end
143
242
  end
144
243
  end
145
244
 
@@ -0,0 +1,11 @@
1
+ module RspecSupportUsesRedis
2
+ def uses_redis
3
+ let(:redis) { ::Redis.new }
4
+
5
+ before do
6
+ redis.flushall
7
+ end
8
+ end
9
+ end
10
+
11
+ RSpec.configure { |c| c.extend RspecSupportUsesRedis }
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: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mo Valipour
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-08-31 00:00:00.000000000 Z
11
+ date: 2017-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sidekiq
@@ -35,6 +35,7 @@ files:
35
35
  - ".gitignore"
36
36
  - ".rspec"
37
37
  - ".ruby-version"
38
+ - ".travis.yml"
38
39
  - Appraisals
39
40
  - Gemfile
40
41
  - Gemfile.lock
@@ -46,12 +47,15 @@ files:
46
47
  - lib/materialist.rb
47
48
  - lib/materialist/event_handler.rb
48
49
  - lib/materialist/event_worker.rb
50
+ - lib/materialist/materialized_record.rb
49
51
  - lib/materialist/materializer.rb
50
52
  - materialist.gemspec
51
53
  - spec/materialist/event_handler_spec.rb
52
54
  - spec/materialist/event_worker_spec.rb
55
+ - spec/materialist/materialized_record_spec.rb
53
56
  - spec/materialist/materializer_spec.rb
54
57
  - spec/spec_helper.rb
58
+ - spec/support/uses_redis.rb
55
59
  homepage: http://github.com/deliveroo/materialist
56
60
  licenses:
57
61
  - MIT
@@ -79,5 +83,7 @@ summary: Utilities to materialize routemaster topics
79
83
  test_files:
80
84
  - spec/materialist/event_handler_spec.rb
81
85
  - spec/materialist/event_worker_spec.rb
86
+ - spec/materialist/materialized_record_spec.rb
82
87
  - spec/materialist/materializer_spec.rb
83
88
  - spec/spec_helper.rb
89
+ - spec/support/uses_redis.rb