materialist 0.0.4 → 0.0.5
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/README.md +8 -1
- data/lib/materialist/materializer.rb +56 -12
- data/lib/materialist.rb +1 -1
- data/spec/materialist/materializer_spec.rb +77 -26
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e31feaccf5642f29d194ad98a70c05c1866b95f7
|
4
|
+
data.tar.gz: c167c306353e46a47954a53b8e52d88d467c8297
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4c34a1cf91ef09e4cced09aeec849fa6078f3cc9ac96400461aa553039f3a90d83a328ac10335a8af35961f3222305dd9e139b2cc2ee2df0e805a0d549462d82
|
7
|
+
data.tar.gz: 44fd0116938583b3876c0cd6e4e4ab13bf3d40fa5b71b820e46ce93f61683f127889c1fbd8df26ee210273283e96d02a132fb4cae4c5e09817a687a963fdb7fb
|
data/README.md
CHANGED
@@ -136,7 +136,9 @@ end
|
|
136
136
|
Here is what each part of the DSL mean:
|
137
137
|
|
138
138
|
#### `use_model <model_name>`
|
139
|
-
describes the name of the active record model to be used.
|
139
|
+
describes the name of the active record model to be used.
|
140
|
+
If missing, materialist skips materialising the resource itself, but will continue
|
141
|
+
with any other functionality -- such as `materialize_link`.
|
140
142
|
|
141
143
|
#### `materialize <key>, as: <column> (default: key)`
|
142
144
|
describes mapping a resource key to database column.
|
@@ -146,6 +148,11 @@ describes materializing from a relation of the resource. This can be nested to a
|
|
146
148
|
|
147
149
|
When inside the block of a `link` any other part of DSL can be used and will be evaluated in the context of the relation resource.
|
148
150
|
|
151
|
+
### `materialize_link <key>, topic: <topic> (default: key)`
|
152
|
+
describes materializing the linked entity.
|
153
|
+
This simulates a `:noop` event on the given topic and the `url` if the
|
154
|
+
liked resource `<key>` as it appears on the response (`_links`)
|
155
|
+
|
149
156
|
#### `after_upsert <method>` -- also `after_destroy`
|
150
157
|
describes the name of the instance method to be invoked after a record was materialized.
|
151
158
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'active_support/inflector'
|
2
2
|
require 'routemaster/api_client'
|
3
|
+
require_relative './event_worker'
|
3
4
|
|
4
5
|
module Materialist
|
5
6
|
module Materializer
|
@@ -9,7 +10,10 @@ module Materialist
|
|
9
10
|
base.extend(Internals::DSL)
|
10
11
|
|
11
12
|
root_mapping = []
|
12
|
-
base.instance_variable_set(:@
|
13
|
+
base.instance_variable_set(:@__materialist_options, {
|
14
|
+
mapping: root_mapping,
|
15
|
+
links_to_materialize: {}
|
16
|
+
})
|
13
17
|
base.instance_variable_set(:@__materialist_dsl_mapping_stack, [root_mapping])
|
14
18
|
end
|
15
19
|
|
@@ -33,7 +37,7 @@ module Materialist
|
|
33
37
|
end
|
34
38
|
|
35
39
|
module ClassMethods
|
36
|
-
attr_reader :
|
40
|
+
attr_reader :__materialist_options, :__materialist_dsl_mapping_stack
|
37
41
|
|
38
42
|
def perform(url, action)
|
39
43
|
materializer = Materializer.new(url, self)
|
@@ -43,6 +47,10 @@ module Materialist
|
|
43
47
|
|
44
48
|
module DSL
|
45
49
|
|
50
|
+
def materialize_link(key, topic: key)
|
51
|
+
__materialist_options[:links_to_materialize][key] = { topic: topic }
|
52
|
+
end
|
53
|
+
|
46
54
|
def materialize(key, as: key)
|
47
55
|
__materialist_dsl_mapping_stack.last << FieldMapping.new(key: key, as: as)
|
48
56
|
end
|
@@ -56,15 +64,15 @@ module Materialist
|
|
56
64
|
end
|
57
65
|
|
58
66
|
def use_model(klass)
|
59
|
-
|
67
|
+
__materialist_options[:model_class] = klass
|
60
68
|
end
|
61
69
|
|
62
70
|
def after_upsert(method_name)
|
63
|
-
|
71
|
+
__materialist_options[:after_upsert] = method_name
|
64
72
|
end
|
65
73
|
|
66
74
|
def after_destroy(method_name)
|
67
|
-
|
75
|
+
__materialist_options[:after_destroy] = method_name
|
68
76
|
end
|
69
77
|
end
|
70
78
|
|
@@ -73,13 +81,19 @@ module Materialist
|
|
73
81
|
def initialize(url, klass)
|
74
82
|
@url = url
|
75
83
|
@instance = klass.new
|
76
|
-
@options = klass.
|
84
|
+
@options = klass.__materialist_options
|
77
85
|
end
|
78
86
|
|
79
87
|
def upsert(retry_on_race_condition: true)
|
80
|
-
|
81
|
-
|
88
|
+
return unless root_resource
|
89
|
+
|
90
|
+
if materialize_self?
|
91
|
+
upsert_record.tap do |entity|
|
92
|
+
instance.send(after_upsert, entity) if after_upsert
|
93
|
+
end
|
82
94
|
end
|
95
|
+
|
96
|
+
materialize_links
|
83
97
|
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
|
84
98
|
# when there is a race condition and uniqueness of :source_url
|
85
99
|
# is enforced by database index, this error is raised
|
@@ -92,6 +106,7 @@ module Materialist
|
|
92
106
|
end
|
93
107
|
|
94
108
|
def destroy
|
109
|
+
return unless materialize_self?
|
95
110
|
model_class.find_by(source_url: url).tap do |entity|
|
96
111
|
entity.destroy!.tap do |entity|
|
97
112
|
instance.send(after_destroy, entity) if after_destroy
|
@@ -103,6 +118,10 @@ module Materialist
|
|
103
118
|
|
104
119
|
attr_reader :url, :instance, :options
|
105
120
|
|
121
|
+
def materialize_self?
|
122
|
+
options.include? :model_class
|
123
|
+
end
|
124
|
+
|
106
125
|
def upsert_record
|
107
126
|
model_class.find_or_initialize_by(source_url: url).tap do |entity|
|
108
127
|
entity.update_attributes attributes
|
@@ -110,6 +129,24 @@ module Materialist
|
|
110
129
|
end
|
111
130
|
end
|
112
131
|
|
132
|
+
def materialize_links
|
133
|
+
(options[:links_to_materialize] || [])
|
134
|
+
.each { |key, opts| materialize_link(key, opts) }
|
135
|
+
end
|
136
|
+
|
137
|
+
def materialize_link(key, opts)
|
138
|
+
return unless root_resource.body._links.include?(key)
|
139
|
+
|
140
|
+
# this can't happen asynchronously
|
141
|
+
# because the handler options are unavailable in this context
|
142
|
+
# :(
|
143
|
+
::Materialist::EventWorker.new.perform({
|
144
|
+
'topic' => opts[:topic],
|
145
|
+
'url' => root_resource.body._links[key].href,
|
146
|
+
'type' => 'noop'
|
147
|
+
})
|
148
|
+
end
|
149
|
+
|
113
150
|
def mapping
|
114
151
|
options.fetch :mapping
|
115
152
|
end
|
@@ -127,7 +164,11 @@ module Materialist
|
|
127
164
|
end
|
128
165
|
|
129
166
|
def attributes
|
130
|
-
build_attributes
|
167
|
+
build_attributes root_resource, mapping
|
168
|
+
end
|
169
|
+
|
170
|
+
def root_resource
|
171
|
+
@_root_resource ||= resource_at(url)
|
131
172
|
end
|
132
173
|
|
133
174
|
def build_attributes(resource, mapping)
|
@@ -139,7 +180,7 @@ module Materialist
|
|
139
180
|
result.tap { |r| r[m.as] = resource.body[m.key] }
|
140
181
|
when LinkMapping
|
141
182
|
resource.body._links.include?(m.key) ?
|
142
|
-
result.merge(build_attributes(resource_at(resource.send(m.key).url
|
183
|
+
result.merge(build_attributes(resource_at(resource.send(m.key).url), m.mapping || [])) :
|
143
184
|
result
|
144
185
|
else
|
145
186
|
result
|
@@ -147,10 +188,13 @@ module Materialist
|
|
147
188
|
end
|
148
189
|
end
|
149
190
|
|
150
|
-
def resource_at(url
|
191
|
+
def resource_at(url)
|
151
192
|
api_client.get(url, options: { enable_caching: false })
|
152
193
|
rescue Routemaster::Errors::ResourceNotFound
|
153
|
-
|
194
|
+
# this is due to a race condition between an upsert event
|
195
|
+
# and a :delete event
|
196
|
+
# when this happens we should silently ignore the case
|
197
|
+
nil
|
154
198
|
end
|
155
199
|
|
156
200
|
def api_client
|
data/lib/materialist.rb
CHANGED
@@ -14,6 +14,8 @@ RSpec.describe Materialist::Materializer do
|
|
14
14
|
materialize :name
|
15
15
|
materialize :age, as: :how_old
|
16
16
|
|
17
|
+
materialize_link :city
|
18
|
+
|
17
19
|
link :city do
|
18
20
|
materialize :timezone
|
19
21
|
|
@@ -22,12 +24,17 @@ RSpec.describe Materialist::Materializer do
|
|
22
24
|
end
|
23
25
|
end
|
24
26
|
end
|
27
|
+
|
28
|
+
class CityMaterializer
|
29
|
+
include Materialist::Materializer
|
30
|
+
|
31
|
+
use_model :city
|
32
|
+
materialize :name
|
33
|
+
end
|
25
34
|
end
|
26
35
|
|
27
36
|
# this class mocks active record behaviour
|
28
|
-
class
|
29
|
-
attr_accessor :source_url, :name, :how_old, :age, :timezone, :country_tld
|
30
|
-
|
37
|
+
class BaseModel
|
31
38
|
def update_attributes(attrs)
|
32
39
|
attrs.each { |k, v| send("#{k}=", v) }
|
33
40
|
end
|
@@ -57,7 +64,7 @@ RSpec.describe Materialist::Materializer do
|
|
57
64
|
raise err
|
58
65
|
end
|
59
66
|
|
60
|
-
(all[source_url] ||
|
67
|
+
(all[source_url] || self.new).tap do |record|
|
61
68
|
record.source_url = source_url
|
62
69
|
end
|
63
70
|
end
|
@@ -74,11 +81,15 @@ RSpec.describe Materialist::Materializer do
|
|
74
81
|
end
|
75
82
|
|
76
83
|
def all
|
77
|
-
|
84
|
+
store[self.name] ||= {}
|
78
85
|
end
|
79
86
|
|
80
|
-
def
|
81
|
-
|
87
|
+
def destroy_all
|
88
|
+
store[self.name] = {}
|
89
|
+
end
|
90
|
+
|
91
|
+
def store
|
92
|
+
@@_store ||= {}
|
82
93
|
end
|
83
94
|
|
84
95
|
def count
|
@@ -87,6 +98,14 @@ RSpec.describe Materialist::Materializer do
|
|
87
98
|
end
|
88
99
|
end
|
89
100
|
|
101
|
+
class Foobar < BaseModel
|
102
|
+
attr_accessor :source_url, :name, :how_old, :age, :timezone, :country_tld
|
103
|
+
end
|
104
|
+
|
105
|
+
class City < BaseModel
|
106
|
+
attr_accessor :source_url, :name
|
107
|
+
end
|
108
|
+
|
90
109
|
module ActiveRecord
|
91
110
|
class RecordNotUnique < StandardError; end
|
92
111
|
class RecordInvalid < StandardError; end
|
@@ -95,32 +114,31 @@ RSpec.describe Materialist::Materializer do
|
|
95
114
|
let(:country_url) { 'https://service.dev/countries/1' }
|
96
115
|
let(:country_body) {{ tld: 'fr' }}
|
97
116
|
let(:city_url) { 'https://service.dev/cities/1' }
|
98
|
-
let(:city_body) {{ _links: { country: { href: country_url }}, timezone: 'Europe/Paris' }}
|
117
|
+
let(:city_body) {{ _links: { country: { href: country_url }}, name: 'paris', timezone: 'Europe/Paris' }}
|
99
118
|
let(:source_url) { 'https://service.dev/foobars/1' }
|
100
119
|
let(:source_body) {{ _links: { city: { href: city_url }}, name: 'jack', age: 30 }}
|
101
|
-
|
102
|
-
|
103
|
-
stub_request(:get,
|
104
|
-
status: 200,
|
105
|
-
body: source_body.to_json,
|
106
|
-
headers: { 'Content-Type' => 'application/json' }
|
107
|
-
)
|
108
|
-
stub_request(:get, country_url).to_return(
|
109
|
-
status: 200,
|
110
|
-
body: country_body.to_json,
|
111
|
-
headers: { 'Content-Type' => 'application/json' }
|
112
|
-
)
|
113
|
-
stub_request(:get, city_url).to_return(
|
120
|
+
|
121
|
+
def stub_resource(url, body)
|
122
|
+
stub_request(:get, url).to_return(
|
114
123
|
status: 200,
|
115
|
-
body:
|
124
|
+
body: body.to_json,
|
116
125
|
headers: { 'Content-Type' => 'application/json' }
|
117
126
|
)
|
118
127
|
end
|
119
128
|
|
129
|
+
before do
|
130
|
+
Foobar.destroy_all
|
131
|
+
City.destroy_all
|
132
|
+
|
133
|
+
stub_resource source_url, source_body
|
134
|
+
stub_resource country_url, country_body
|
135
|
+
stub_resource city_url, city_body
|
136
|
+
end
|
137
|
+
|
120
138
|
let(:action) { :create }
|
121
139
|
let(:perform) { FoobarMaterializer.perform(source_url, action) }
|
122
140
|
|
123
|
-
it "
|
141
|
+
it "materializes record in db" do
|
124
142
|
expect{perform}.to change{Foobar.count}.by 1
|
125
143
|
inserted = Foobar.find_by(source_url: source_url)
|
126
144
|
expect(inserted.name).to eq source_body[:name]
|
@@ -129,6 +147,13 @@ RSpec.describe Materialist::Materializer do
|
|
129
147
|
expect(inserted.country_tld).to eq country_body[:tld]
|
130
148
|
end
|
131
149
|
|
150
|
+
it "materializes linked record separately in db" do
|
151
|
+
expect{perform}.to change{City.count}.by 1
|
152
|
+
|
153
|
+
inserted = City.find_by(source_url: city_url)
|
154
|
+
expect(inserted.name).to eq city_body[:name]
|
155
|
+
end
|
156
|
+
|
132
157
|
context "when record already exists" do
|
133
158
|
let!(:record) { Foobar.create!(source_url: source_url, name: 'mo') }
|
134
159
|
|
@@ -194,8 +219,8 @@ RSpec.describe Materialist::Materializer do
|
|
194
219
|
context "if resource returns 404" do
|
195
220
|
before { stub_request(:get, source_url).to_return(status: 404) }
|
196
221
|
|
197
|
-
it "
|
198
|
-
expect
|
222
|
+
it "does not add anything to db" do
|
223
|
+
expect{perform}.to change{Foobar.count}.by 0
|
199
224
|
end
|
200
225
|
end
|
201
226
|
|
@@ -276,12 +301,38 @@ RSpec.describe Materialist::Materializer do
|
|
276
301
|
|
277
302
|
context "when resource doesn't exist locally" do
|
278
303
|
it "does not raise error" do
|
279
|
-
Foobar.
|
304
|
+
Foobar.destroy_all
|
280
305
|
expect{ perform }.to_not raise_error
|
281
306
|
end
|
282
307
|
end
|
283
308
|
end
|
284
309
|
|
285
310
|
end
|
311
|
+
|
312
|
+
context "when not materializing self but materializing linked parent" do
|
313
|
+
class CitySettingsMaterializer
|
314
|
+
include Materialist::Materializer
|
315
|
+
|
316
|
+
materialize_link :city
|
317
|
+
end
|
318
|
+
|
319
|
+
let(:city_settings_url) { 'https://service.dev/city_settings/1' }
|
320
|
+
let(:city_settings_body) {{ _links: { city: { href: city_url }}}}
|
321
|
+
before { stub_resource city_settings_url, city_settings_body }
|
322
|
+
|
323
|
+
let(:perform) { CitySettingsMaterializer.perform(city_settings_url, action) }
|
324
|
+
|
325
|
+
it "materializes linked parent" do
|
326
|
+
expect{perform}.to change{City.count}.by 1
|
327
|
+
end
|
328
|
+
|
329
|
+
context "when action is :delete" do
|
330
|
+
let(:action) { :delete }
|
331
|
+
|
332
|
+
it "does not materialize linked parent" do
|
333
|
+
expect{perform}.to_not change{City.count}
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
286
337
|
end
|
287
338
|
end
|
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.5
|
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-09-
|
11
|
+
date: 2017-09-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sidekiq
|