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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 30db9921aa96b1a36a26893c7307a0101f251fcc
4
- data.tar.gz: 8d18da5698d852c385bc37e9884c096ac0792620
3
+ metadata.gz: e31feaccf5642f29d194ad98a70c05c1866b95f7
4
+ data.tar.gz: c167c306353e46a47954a53b8e52d88d467c8297
5
5
  SHA512:
6
- metadata.gz: eff72d3f1c95a140244848e98f3024e4565b30b909b61e535acb9d5b0d59f4e531b81525f3b50fbc1123e2e072971ac506528870c2f001eaf8c69de647faf969
7
- data.tar.gz: 97283f7df24d20f69d2a0b5720d493a188ff1fdb1f0ec4994e70650f0710a65ff695ad68eee34f6c1d99541847f21e5557dba6b7af9d119ef86ad33442cce73f
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(:@materialist_options, { mapping: root_mapping })
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 :materialist_options, :__materialist_dsl_mapping_stack
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
- materialist_options[:model_class] = klass
67
+ __materialist_options[:model_class] = klass
60
68
  end
61
69
 
62
70
  def after_upsert(method_name)
63
- materialist_options[:after_upsert] = method_name
71
+ __materialist_options[:after_upsert] = method_name
64
72
  end
65
73
 
66
74
  def after_destroy(method_name)
67
- materialist_options[:after_destroy] = method_name
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.materialist_options
84
+ @options = klass.__materialist_options
77
85
  end
78
86
 
79
87
  def upsert(retry_on_race_condition: true)
80
- upsert_record.tap do |entity|
81
- instance.send(after_upsert, entity) if after_upsert
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 resource_at(url), mapping
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, allow_nil: true), m.mapping || [])) :
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, allow_nil: false)
191
+ def resource_at(url)
151
192
  api_client.get(url, options: { enable_caching: false })
152
193
  rescue Routemaster::Errors::ResourceNotFound
153
- raise unless allow_nil
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
@@ -1,3 +1,3 @@
1
1
  module Materialist
2
- VERSION = '0.0.4'
2
+ VERSION = '0.0.5'
3
3
  end
@@ -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 Foobar
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] || Foobar.new).tap do |record|
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
- @@_store ||= {}
84
+ store[self.name] ||= {}
78
85
  end
79
86
 
80
- def flush_all
81
- @@_store = {}
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
- before do
102
- Foobar.flush_all
103
- stub_request(:get, source_url).to_return(
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: city_body.to_json,
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 "inserts record in db" do
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 "bubbles up routemaster not found error" do
198
- expect { perform }.to raise_error Routemaster::Errors::ResourceNotFound
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.flush_all
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
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-08 00:00:00.000000000 Z
11
+ date: 2017-09-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sidekiq