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