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 +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +9 -0
- data/README.md +25 -0
- data/lib/materialist/materialized_record.rb +36 -0
- data/lib/materialist/materializer.rb +13 -1
- data/lib/materialist.rb +1 -1
- data/spec/materialist/materialized_record_spec.rb +64 -0
- data/spec/materialist/materializer_spec.rb +137 -38
- data/spec/support/uses_redis.rb +11 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e1d3d42e45eb0a9130ca412078f6df11e29da53
|
4
|
+
data.tar.gz: 0041f0349dfce34d055ff59a5a4f2d4fc694bb26
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0df7a1d5dab2b2d6f4fe9769e73b67028b2398dca4b309081d172e7b97c3b8729088957ffb8fcd24c1e045f6eba287cd1af96f81d9811fb2fa5470f36800e6e5
|
7
|
+
data.tar.gz: bfaf53768945dc8b98cad7a6191aa9ef588b680898546353d5a756f4e4286a867ae3f00067c69961fe2efb9c5b820457ac620df2cf74f5ca1db5bb2983090ed9
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
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.
|
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
@@ -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
|
-
|
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
|
-
|
62
|
-
expect
|
63
|
-
|
64
|
-
expect(
|
65
|
-
expect(
|
66
|
-
|
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
|
-
|
70
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
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(:
|
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.
|
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
|
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
|
142
|
-
|
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
|
|
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.
|
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-
|
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
|