materialist 3.3.0 → 3.4.0

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