materialist 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|