materialist 3.3.0 → 3.4.0

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: f551f35e47a806f9f5e3b475feb35358f4a034b5
4
- data.tar.gz: 8134a56b74346d367618b99b09604c0ded38a815
3
+ metadata.gz: d4b55d54d4aa9644e1fee1055286166521f97120
4
+ data.tar.gz: ed6c12846d2b859f49efdb7b5b9581184cafa4ea
5
5
  SHA512:
6
- metadata.gz: 2a647bde4177b45a090e8a91a1a34b1dde10f1ac38adbe8d1588c161182113cd8f065511edd59f08a82e38721ebaa1260650af9840d4e1f032160a4739561af1
7
- data.tar.gz: c660872899ce6526370260e7852d886ecd34abf2a4c0cdaf7178b11680c524db318ade68d8640ce00aa2a50630158f4242f20eec20f79b49f496b877c6a0c678
6
+ metadata.gz: f3a97dd8a5a618d2600d23d3259979dcf3698860c83794f0314387db616b6ca8635a15dbe7caf5bfd5082426292682241d343e7c9de39a2ab8128facf320d912
7
+ data.tar.gz: 0b61ede028064fcd5e4aa8fc2a4040e2beb8f04e7b3606f723a68fd1e064472eb57faf8f39219a95f7f8b274a666f41045a7630392cc262e8a3bc161680d5249
data/README.md CHANGED
@@ -14,6 +14,9 @@ In your `gemfile`
14
14
 
15
15
  ```ruby
16
16
  gem 'materialist'
17
+
18
+ # or for sandbox features
19
+ gem 'materialist', github: 'deliveroo/materialist', branch: 'sandbox'
17
20
  ```
18
21
 
19
22
  Then do
data/RELEASE_NOTES.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  _description of next release_
4
4
 
5
+ ## 3.4.0
6
+
7
+ - Add support for providing payload into the materializer
8
+ - Enhance linked resource materialization to avoid `:noop` stats
9
+
5
10
  ## 3.3.0
6
11
 
7
12
  Features:
data/lib/materialist.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require_relative './configuration'
2
2
 
3
3
  module Materialist
4
- VERSION = '3.3.0'
4
+ VERSION = '3.4.0'
5
5
  end
@@ -4,9 +4,8 @@ module Materialist
4
4
  module ClassMethods
5
5
  attr_reader :__materialist_options, :__materialist_dsl_mapping_stack
6
6
 
7
- def perform(url, action)
8
- materializer = Materializer.new(url, self)
9
- action == :delete ? materializer.destroy : materializer.upsert
7
+ def perform(url, action, *options)
8
+ Materializer.new(url, self, *options).perform(action)
10
9
  end
11
10
 
12
11
  def _sidekiq_options
@@ -2,13 +2,13 @@ module Materialist
2
2
  module Materializer
3
3
  module Internals
4
4
  class FieldMapping
5
- def initialize(key:, as:)
5
+ def initialize(key:, as: key)
6
6
  @key = key
7
7
  @as = as
8
8
  end
9
9
 
10
10
  def map(resource)
11
- { @as => resource.body[@key] }
11
+ { @as => resource.dig(@key) }
12
12
  end
13
13
  end
14
14
  end
@@ -9,8 +9,8 @@ module Materialist
9
9
  end
10
10
 
11
11
  def map(resource)
12
- return unless link = resource.body._links[@key]
13
- { @as => url_parser.call(link.href) }
12
+ return unless link = resource.dig(:_links, @key)
13
+ { @as => url_parser.call(link[:href]) }
14
14
  end
15
15
 
16
16
  private
@@ -2,25 +2,22 @@ module Materialist
2
2
  module Materializer
3
3
  module Internals
4
4
  class LinkMapping
5
- def initialize(key:, enable_caching: false)
5
+ def initialize(key:, mapping: [], enable_caching: false)
6
6
  @key = key
7
+ @mapping = mapping
7
8
  @enable_caching = enable_caching
8
- @mapping = []
9
9
  end
10
10
 
11
+ attr_reader :mapping
12
+
11
13
  def map(resource)
12
14
  return unless linked_resource = linked_resource(resource)
13
15
  mapping.map{ |m| m.map(linked_resource) }.compact.reduce(&:merge)
14
16
  end
15
17
 
16
- attr_reader :mapping
17
-
18
- private
19
-
20
18
  def linked_resource(resource)
21
- return unless resource.body._links.include?(@key)
22
- return unless sub_resource = resource.send(@key)
23
- sub_resource.show(enable_caching: @enable_caching)
19
+ return unless href = resource.dig(:_links, @key, :href)
20
+ resource.client.get(href, options: { enable_caching: @enable_caching })
24
21
  rescue Routemaster::Errors::ResourceNotFound
25
22
  nil
26
23
  end
@@ -1,18 +1,29 @@
1
1
  require 'routemaster/api_client'
2
2
  require_relative '../../workers/event'
3
+ require_relative './resources'
3
4
 
4
5
  module Materialist
5
6
  module Materializer
6
7
  module Internals
7
8
  class Materializer
8
- def initialize(url, klass)
9
+ def initialize(url, klass, resource_payload: nil, api_client: nil)
9
10
  @url = url
10
11
  @instance = klass.new
11
12
  @options = klass.__materialist_options
13
+ @api_client = api_client || Routemaster::APIClient.new(response_class: HateoasResource)
14
+ if resource_payload
15
+ @resource = PayloadResource.new(resource_payload, client: @api_client)
16
+ end
17
+ end
18
+
19
+ def perform(action)
20
+ action.to_sym == :delete ? destroy : upsert
12
21
  end
13
22
 
23
+ private
24
+
14
25
  def upsert(retry_on_race_condition: true)
15
- return unless root_resource
26
+ return unless resource
16
27
 
17
28
  if materialize_self?
18
29
  upsert_record.tap do |entity|
@@ -42,9 +53,7 @@ module Materialist
42
53
  end
43
54
  end
44
55
 
45
- private
46
-
47
- attr_reader :url, :instance, :options
56
+ attr_reader :url, :instance, :options, :api_client
48
57
 
49
58
  def materialize_self?
50
59
  options.include? :model_class
@@ -53,7 +62,7 @@ module Materialist
53
62
  def upsert_record
54
63
  model_class.find_or_initialize_by(source_lookup(url)).tap do |entity|
55
64
  send_messages(before_upsert, entity) unless before_upsert.nil?
56
- entity.update_attributes! attributes
65
+ entity.update_attributes!(attributes)
57
66
  end
58
67
  end
59
68
 
@@ -63,16 +72,11 @@ module Materialist
63
72
  end
64
73
 
65
74
  def materialize_link(key, opts)
66
- return unless root_resource.body._links.include?(key)
75
+ return unless link = resource.dig(:_links, key)
76
+ return unless materializer_class = MaterializerFactory.class_from_topic(opts.fetch(:topic))
67
77
 
68
- # this can't happen asynchronously
69
- # because the handler options are unavailable in this context
70
- # :(
71
- ::Materialist::Workers::Event.new.perform({
72
- 'topic' => opts[:topic],
73
- 'url' => root_resource.body._links[key].href,
74
- 'type' => 'noop'
75
- })
78
+ # TODO: perhaps consider doing this asynchronously some how?
79
+ materializer_class.perform(link[:href], :noop)
76
80
  end
77
81
 
78
82
  def mappings
@@ -112,21 +116,17 @@ module Materialist
112
116
  end
113
117
 
114
118
  def attributes
115
- mappings.map{ |m| m.map(root_resource) }.compact.reduce(&:merge) || {}
119
+ mappings.map{ |m| m.map(resource) }.compact.reduce(&:merge) || {}
116
120
  end
117
121
 
118
- def root_resource
119
- @_root_resource ||= begin
120
- api_client.get(url, options: { enable_caching: false })
121
- rescue Routemaster::Errors::ResourceNotFound
122
- nil
123
- end
122
+ def resource
123
+ @resource ||= fetch_resource
124
124
  end
125
125
 
126
- def api_client
127
- @_api_client ||= Routemaster::APIClient.new(
128
- response_class: Routemaster::Responses::HateoasResponse
129
- )
126
+ def fetch_resource
127
+ api_client.get(url, options: { enable_caching: false })
128
+ rescue Routemaster::Errors::ResourceNotFound
129
+ nil
130
130
  end
131
131
 
132
132
  def send_messages(messages, arguments)
@@ -0,0 +1,22 @@
1
+ module Materialist
2
+ module Materializer
3
+ module Internals
4
+ class PayloadResource
5
+ attr_reader :client
6
+
7
+ delegate :dig, to: :@payload
8
+
9
+ def initialize(payload, client:)
10
+ @payload = payload
11
+ @client = client
12
+ end
13
+ end
14
+
15
+ class HateoasResource < PayloadResource
16
+ def initialize(response, client:)
17
+ super(response.body, client: client)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,9 +1,7 @@
1
1
  module Materialist
2
2
  class MaterializerFactory
3
3
  def self.class_from_topic(topic)
4
- "#{topic.to_s.singularize.classify}Materializer".constantize
5
- rescue NameError
6
- nil
4
+ "#{topic.to_s.singularize.classify}Materializer".safe_constantize
7
5
  end
8
6
  end
9
7
  end
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+ require 'materialist/materializer/internals'
3
+
4
+ include Materialist::Materializer::Internals
5
+
6
+ RSpec.describe Materialist::Materializer::Internals::LinkMapping, type: :internals do
7
+ let(:key) { :details }
8
+ subject(:instance) { described_class.new(key: key, mapping: mappings) }
9
+ let(:mappings) { [ FieldMapping.new(key: :name) ] }
10
+
11
+ let(:url_sub) { 'https://deliveroo.co.uk/details/1001' }
12
+ let(:payload) { { _links: { details: { href: url_sub } } } }
13
+ let(:payload_sub) { { name: "jack", age: 20 } }
14
+
15
+ let(:client) { double }
16
+ let(:resource_class) { Materialist::Materializer::Internals::PayloadResource }
17
+ let(:resource) { resource_class.new(payload, client: client) }
18
+ let(:resource_sub) { resource_class.new(payload_sub, client: client) }
19
+
20
+ before do
21
+ allow(client).to receive(:get).with(url_sub, anything).and_return resource_sub
22
+ end
23
+
24
+ describe '#map' do
25
+ subject(:perform) { instance.map resource }
26
+
27
+ it 'returns a hash corresponding to the mapping' do
28
+ is_expected.to eq({ name: 'jack' })
29
+ end
30
+
31
+ context 'when multiple mappings given' do
32
+ let(:mappings) do
33
+ [
34
+ FieldMapping.new(key: :age),
35
+ LinkMapping.new(key: :wont_find_me),
36
+ FieldMapping.new(key: :name)
37
+ ]
38
+ end
39
+
40
+ it 'returns a hash corresponding to the mapping' do
41
+ is_expected.to eq({ age: 20, name: 'jack' })
42
+ end
43
+ end
44
+
45
+ describe 'missing sub resource' do
46
+ context 'when given link is not present' do
47
+ let(:key) { :foo }
48
+
49
+ it { is_expected.to be nil }
50
+ end
51
+
52
+ context 'when given link is malformed' do
53
+ let(:payload) { { _links: { details: { foo: url_sub } } } }
54
+
55
+ it { is_expected.to be nil }
56
+ end
57
+
58
+ context 'when client returns nil' do
59
+ let(:resource_sub) { nil }
60
+
61
+ it { is_expected.to be nil }
62
+ end
63
+
64
+ context 'when client throws not-found error' do
65
+ before do
66
+ allow(client).to receive(:get)
67
+ .with(url_sub, anything)
68
+ .and_raise Routemaster::Errors::ResourceNotFound.new(double(body: nil))
69
+ end
70
+
71
+ it { is_expected.to be nil }
72
+ end
73
+
74
+ context 'when caching enabled' do
75
+ let(:instance) { described_class.new(key: key, mapping: mappings, enable_caching: true) }
76
+
77
+ it 'passes on the option to client' do
78
+ expect(client).to receive(:get)
79
+ .with(url_sub, options: { enable_caching: true})
80
+ perform
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ end
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
  require 'support/uses_redis'
3
3
  require 'materialist/materializer'
4
4
 
5
- RSpec.describe Materialist::Materializer do
5
+ RSpec.describe Materialist::Materializer::Internals::Materializer do
6
6
  uses_redis
7
7
 
8
8
  describe ".perform" do
@@ -60,8 +60,9 @@ RSpec.describe Materialist::Materializer do
60
60
  )
61
61
  end
62
62
 
63
+ let!(:source_stub) { stub_resource source_url, source_body }
64
+
63
65
  before do
64
- stub_resource source_url, source_body
65
66
  stub_resource country_url, country_body
66
67
  stub_resource city_url, city_body
67
68
  stub_resource defined_source_url, defined_source_body
@@ -77,7 +78,7 @@ RSpec.describe Materialist::Materializer do
77
78
  let(:actions_called) { materializer_class.class_variable_get(:@@actions_called) }
78
79
 
79
80
  it "materializes record in db" do
80
- expect{perform}.to change{Foobar.count}.by 1
81
+ expect{ perform }.to change{ Foobar.count }.by 1
81
82
  inserted = Foobar.find_by(source_url: source_url)
82
83
  expect(inserted.name).to eq source_body[:name]
83
84
  expect(inserted.how_old).to eq source_body[:age]
@@ -95,6 +96,23 @@ RSpec.describe Materialist::Materializer do
95
96
  expect(inserted.name).to eq city_body[:name]
96
97
  end
97
98
 
99
+ context 'when materializing using payload' do
100
+ let(:perform) { materializer_class.perform(source_url, action, resource_payload: source_body) }
101
+
102
+ it 'does not fetch resource' do
103
+ perform
104
+ expect(source_stub).to_not have_been_requested
105
+ end
106
+
107
+ it "materializes record in db" do
108
+ expect{ perform }.to change{ Foobar.count }.by 1
109
+ end
110
+
111
+ it "materializes linked record in db" do
112
+ expect{ perform }.to change{ City.count }.by 1
113
+ end
114
+ end
115
+
98
116
  context "when record already exists" do
99
117
  let!(:record) { Foobar.create!(source_url: source_url, name: 'mo') }
100
118
 
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: 3.3.0
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mo Valipour
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-02 00:00:00.000000000 Z
11
+ date: 2018-05-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sidekiq
@@ -212,11 +212,13 @@ files:
212
212
  - lib/materialist/materializer/internals/link_href_mapping.rb
213
213
  - lib/materialist/materializer/internals/link_mapping.rb
214
214
  - lib/materialist/materializer/internals/materializer.rb
215
+ - lib/materialist/materializer/internals/resources.rb
215
216
  - lib/materialist/materializer_factory.rb
216
217
  - lib/materialist/workers/event.rb
217
218
  - materialist.gemspec
218
219
  - spec/materialist/event_handler_spec.rb
219
220
  - spec/materialist/materialized_record_spec.rb
221
+ - spec/materialist/materializer/internals/link_mapping_spec.rb
220
222
  - spec/materialist/materializer_factory_spec.rb
221
223
  - spec/materialist/materializer_spec.rb
222
224
  - spec/materialist/workers/event_spec.rb
@@ -251,6 +253,7 @@ summary: Utilities to materialize routemaster topics
251
253
  test_files:
252
254
  - spec/materialist/event_handler_spec.rb
253
255
  - spec/materialist/materialized_record_spec.rb
256
+ - spec/materialist/materializer/internals/link_mapping_spec.rb
254
257
  - spec/materialist/materializer_factory_spec.rb
255
258
  - spec/materialist/materializer_spec.rb
256
259
  - spec/materialist/workers/event_spec.rb