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