materialist 0.0.1 → 0.0.2

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: 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