materialist 0.0.1
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 +7 -0
- data/.env.test +2 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Appraisals +3 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +165 -0
- data/Guardfile +10 -0
- data/LICENSE.txt +23 -0
- data/README.md +117 -0
- data/Rakefile +1 -0
- data/appraise +28 -0
- data/lib/materialist.rb +3 -0
- data/lib/materialist/event_handler.rb +31 -0
- data/lib/materialist/event_worker.rb +14 -0
- data/lib/materialist/materializer.rb +143 -0
- data/materialist.gemspec +20 -0
- data/spec/materialist/event_handler_spec.rb +68 -0
- data/spec/materialist/event_worker_spec.rb +23 -0
- data/spec/materialist/materializer_spec.rb +148 -0
- data/spec/spec_helper.rb +28 -0
- metadata +83 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: ad0c95193ee887dc776cdb28f54fa1fd7a00212a
|
|
4
|
+
data.tar.gz: 6e3dfb760278e54a275acdc667f5b32fe1809374
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 54a84928a4d988d22741f038d02bc47757b331ff45f45cef201829691b204a239745a89abc9b67fac706f267fda054b0e65094ef1d62ac9836282e7ec608d448
|
|
7
|
+
data.tar.gz: 8758db7954c895a28ac969f4f166b570451f338a1a11d449ee9d9f8bae3e1dcc66cb57d1867af24bb9e3dcee1497d4c557411924a616171fbc2fd6580f2afb16
|
data/.env.test
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.4.0
|
data/Appraisals
ADDED
data/Gemfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
source 'https://rubygems.org'
|
|
2
|
+
|
|
3
|
+
# Specify your gem's dependencies in routemaster_client.gemspec
|
|
4
|
+
gemspec
|
|
5
|
+
|
|
6
|
+
gem 'activesupport'
|
|
7
|
+
gem 'sidekiq'
|
|
8
|
+
gem 'routemaster-drain', '~> 3.0'
|
|
9
|
+
|
|
10
|
+
# Just here to avoid a safety warning
|
|
11
|
+
gem 'psych', require: false
|
|
12
|
+
|
|
13
|
+
# Used in builds and tests
|
|
14
|
+
gem 'bundler', require: false
|
|
15
|
+
gem 'dotenv', require: false
|
|
16
|
+
gem 'simplecov', require: false
|
|
17
|
+
gem 'codecov', require: false
|
|
18
|
+
gem 'webmock', require: false
|
|
19
|
+
|
|
20
|
+
gem 'guard-rspec', require: false
|
|
21
|
+
gem 'pry', require: false
|
|
22
|
+
gem 'byebug', require: false
|
|
23
|
+
gem 'rspec', require: false
|
|
24
|
+
gem 'appraisal', require: false
|
|
25
|
+
gem 'dogstatsd', require: false
|
|
26
|
+
gem 'fork_break', require: false
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
materialist (0.0.1)
|
|
5
|
+
sidekiq
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: https://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
activesupport (5.1.3)
|
|
11
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
12
|
+
i18n (~> 0.7)
|
|
13
|
+
minitest (~> 5.1)
|
|
14
|
+
tzinfo (~> 1.1)
|
|
15
|
+
addressable (2.5.0)
|
|
16
|
+
public_suffix (~> 2.0, >= 2.0.2)
|
|
17
|
+
appraisal (2.2.0)
|
|
18
|
+
bundler
|
|
19
|
+
rake
|
|
20
|
+
thor (>= 0.14.0)
|
|
21
|
+
byebug (9.1.0)
|
|
22
|
+
codecov (0.1.10)
|
|
23
|
+
json
|
|
24
|
+
simplecov
|
|
25
|
+
url
|
|
26
|
+
coderay (1.1.1)
|
|
27
|
+
concurrent-ruby (1.0.5)
|
|
28
|
+
connection_pool (2.2.1)
|
|
29
|
+
crack (0.4.3)
|
|
30
|
+
safe_yaml (~> 1.0.0)
|
|
31
|
+
diff-lcs (1.3)
|
|
32
|
+
docile (1.1.5)
|
|
33
|
+
dogstatsd (2.0.0)
|
|
34
|
+
dotenv (2.2.1)
|
|
35
|
+
ethon (0.10.1)
|
|
36
|
+
ffi (>= 1.3.0)
|
|
37
|
+
faraday (0.13.1)
|
|
38
|
+
multipart-post (>= 1.2, < 3)
|
|
39
|
+
faraday_middleware (0.12.2)
|
|
40
|
+
faraday (>= 0.7.4, < 1.0)
|
|
41
|
+
ffi (1.9.18)
|
|
42
|
+
fork (1.0.1)
|
|
43
|
+
fork_break (0.1.4)
|
|
44
|
+
fork (= 1.0.1)
|
|
45
|
+
formatador (0.2.5)
|
|
46
|
+
guard (2.14.1)
|
|
47
|
+
formatador (>= 0.2.4)
|
|
48
|
+
listen (>= 2.7, < 4.0)
|
|
49
|
+
lumberjack (~> 1.0)
|
|
50
|
+
nenv (~> 0.1)
|
|
51
|
+
notiffany (~> 0.0)
|
|
52
|
+
pry (>= 0.9.12)
|
|
53
|
+
shellany (~> 0.0)
|
|
54
|
+
thor (>= 0.18.1)
|
|
55
|
+
guard-compat (1.2.1)
|
|
56
|
+
guard-rspec (4.7.3)
|
|
57
|
+
guard (~> 2.1)
|
|
58
|
+
guard-compat (~> 1.1)
|
|
59
|
+
rspec (>= 2.99.0, < 4.0)
|
|
60
|
+
hashdiff (0.3.2)
|
|
61
|
+
hashie (3.5.6)
|
|
62
|
+
i18n (0.8.6)
|
|
63
|
+
json (2.1.0)
|
|
64
|
+
listen (3.1.5)
|
|
65
|
+
rb-fsevent (~> 0.9, >= 0.9.4)
|
|
66
|
+
rb-inotify (~> 0.9, >= 0.9.7)
|
|
67
|
+
ruby_dep (~> 1.2)
|
|
68
|
+
lumberjack (1.0.12)
|
|
69
|
+
method_source (0.8.2)
|
|
70
|
+
minitest (5.10.3)
|
|
71
|
+
multipart-post (2.0.0)
|
|
72
|
+
nenv (0.3.0)
|
|
73
|
+
notiffany (0.1.1)
|
|
74
|
+
nenv (~> 0.1)
|
|
75
|
+
shellany (~> 0.0)
|
|
76
|
+
pry (0.10.4)
|
|
77
|
+
coderay (~> 1.1.0)
|
|
78
|
+
method_source (~> 0.8.1)
|
|
79
|
+
slop (~> 3.4)
|
|
80
|
+
psych (2.2.4)
|
|
81
|
+
public_suffix (2.0.5)
|
|
82
|
+
rack (2.0.3)
|
|
83
|
+
rack-protection (2.0.0)
|
|
84
|
+
rack
|
|
85
|
+
rake (12.0.0)
|
|
86
|
+
rb-fsevent (0.10.2)
|
|
87
|
+
rb-inotify (0.9.10)
|
|
88
|
+
ffi (>= 0.5.0, < 2)
|
|
89
|
+
redis (3.3.3)
|
|
90
|
+
redis-namespace (1.5.3)
|
|
91
|
+
redis (~> 3.0, >= 3.0.4)
|
|
92
|
+
routemaster-drain (3.0.1)
|
|
93
|
+
addressable
|
|
94
|
+
concurrent-ruby
|
|
95
|
+
faraday (>= 0.9.0)
|
|
96
|
+
faraday_middleware
|
|
97
|
+
hashie
|
|
98
|
+
rack (>= 1.4.5)
|
|
99
|
+
redis-namespace
|
|
100
|
+
typhoeus (~> 1.1)
|
|
101
|
+
wisper (~> 1.6.1)
|
|
102
|
+
rspec (3.6.0)
|
|
103
|
+
rspec-core (~> 3.6.0)
|
|
104
|
+
rspec-expectations (~> 3.6.0)
|
|
105
|
+
rspec-mocks (~> 3.6.0)
|
|
106
|
+
rspec-core (3.6.0)
|
|
107
|
+
rspec-support (~> 3.6.0)
|
|
108
|
+
rspec-expectations (3.6.0)
|
|
109
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
110
|
+
rspec-support (~> 3.6.0)
|
|
111
|
+
rspec-mocks (3.6.0)
|
|
112
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
113
|
+
rspec-support (~> 3.6.0)
|
|
114
|
+
rspec-support (3.6.0)
|
|
115
|
+
ruby_dep (1.5.0)
|
|
116
|
+
safe_yaml (1.0.4)
|
|
117
|
+
shellany (0.0.1)
|
|
118
|
+
sidekiq (5.0.4)
|
|
119
|
+
concurrent-ruby (~> 1.0)
|
|
120
|
+
connection_pool (~> 2.2, >= 2.2.0)
|
|
121
|
+
rack-protection (>= 1.5.0)
|
|
122
|
+
redis (~> 3.3, >= 3.3.3)
|
|
123
|
+
simplecov (0.15.0)
|
|
124
|
+
docile (~> 1.1.0)
|
|
125
|
+
json (>= 1.8, < 3)
|
|
126
|
+
simplecov-html (~> 0.10.0)
|
|
127
|
+
simplecov-html (0.10.2)
|
|
128
|
+
slop (3.6.0)
|
|
129
|
+
thor (0.20.0)
|
|
130
|
+
thread_safe (0.3.6)
|
|
131
|
+
typhoeus (1.3.0)
|
|
132
|
+
ethon (>= 0.9.0)
|
|
133
|
+
tzinfo (1.2.3)
|
|
134
|
+
thread_safe (~> 0.1)
|
|
135
|
+
url (0.3.2)
|
|
136
|
+
webmock (2.3.2)
|
|
137
|
+
addressable (>= 2.3.6)
|
|
138
|
+
crack (>= 0.3.2)
|
|
139
|
+
hashdiff
|
|
140
|
+
wisper (1.6.1)
|
|
141
|
+
|
|
142
|
+
PLATFORMS
|
|
143
|
+
ruby
|
|
144
|
+
|
|
145
|
+
DEPENDENCIES
|
|
146
|
+
activesupport
|
|
147
|
+
appraisal
|
|
148
|
+
bundler
|
|
149
|
+
byebug
|
|
150
|
+
codecov
|
|
151
|
+
dogstatsd
|
|
152
|
+
dotenv
|
|
153
|
+
fork_break
|
|
154
|
+
guard-rspec
|
|
155
|
+
materialist!
|
|
156
|
+
pry
|
|
157
|
+
psych
|
|
158
|
+
routemaster-drain (~> 3.0)
|
|
159
|
+
rspec
|
|
160
|
+
sidekiq
|
|
161
|
+
simplecov
|
|
162
|
+
webmock
|
|
163
|
+
|
|
164
|
+
BUNDLED WITH
|
|
165
|
+
1.15.4
|
data/Guardfile
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# A sample Guardfile
|
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
|
3
|
+
|
|
4
|
+
guard :rspec, cmd: 'bundle exec rspec' do
|
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
|
8
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
|
9
|
+
end
|
|
10
|
+
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Copyright (c) 2014-2016 HouseTrip Ltd.
|
|
2
|
+
Copyright (c) 2016 Deliveroo Ltd.
|
|
3
|
+
|
|
4
|
+
MIT License
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
7
|
+
a copy of this software and associated documentation files (the
|
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
12
|
+
the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be
|
|
15
|
+
included in all copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
21
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
22
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
23
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
## Materialist
|
|
2
|
+
|
|
3
|
+
> _adjective_ `philosophy`: relating to the theory that nothing exists except matter and its movements and modifications.
|
|
4
|
+
|
|
5
|
+
A "materializer" is a ruby class that is responsible for receiving an event and
|
|
6
|
+
materializing the remote resource (described by the event) in database.
|
|
7
|
+
|
|
8
|
+
This library is a set of utilities that provide both the wiring and the DSL to
|
|
9
|
+
painlessly do so.
|
|
10
|
+
|
|
11
|
+
### Configuration
|
|
12
|
+
|
|
13
|
+
First you need an "event handler":
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
handler = Materialist::EventHandler.new({ ...options })
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Where options could be:
|
|
20
|
+
|
|
21
|
+
- `topics` (only when using in `.subscribe`): An array of topics to be used.
|
|
22
|
+
If not provided nothing would be materialized.
|
|
23
|
+
- `queue`: name of the queue to be used by sidekiq worker
|
|
24
|
+
|
|
25
|
+
Then there are two ways to configure materialist in routemaster:
|
|
26
|
+
|
|
27
|
+
1. **If you DON'T need resources to be cached in redis:** use `handler` as siphon:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
handler = Materialist::EventHandler.new
|
|
31
|
+
siphon_events = {
|
|
32
|
+
zones: handler,
|
|
33
|
+
rider_domain_riders: handler
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
app = Routemaster::Drain::Caching.new(siphon_events: siphon_events)
|
|
37
|
+
# ...
|
|
38
|
+
|
|
39
|
+
map '/events' do
|
|
40
|
+
run app
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
2. **You DO need resources cached in redis:** In this case you need to use `handler` to subscribe to the caching pipeline:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
TOPICS = %w(
|
|
48
|
+
zones
|
|
49
|
+
rider_domain_riders
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
handler = Materialist::EventHandler.new({ topics: TOPICS })
|
|
53
|
+
app = Routemaster::Drain::Caching.new # or ::Basic.new
|
|
54
|
+
app.subscribe(handler, prefix: true)
|
|
55
|
+
# ...
|
|
56
|
+
|
|
57
|
+
map '/events' do
|
|
58
|
+
run app
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### DSL
|
|
63
|
+
|
|
64
|
+
Next you would need to define a materializer for each of the topic. The name of
|
|
65
|
+
the materializer class should match the topic name (in singular)
|
|
66
|
+
|
|
67
|
+
These materializers would live in a first-class directory (`/materializers`) in your rails app.
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
require 'materialist/materializer'
|
|
71
|
+
|
|
72
|
+
class ZoneMaterializer
|
|
73
|
+
include Materialist::Materializer
|
|
74
|
+
|
|
75
|
+
use_model :zone
|
|
76
|
+
|
|
77
|
+
materialize :id, as: :orderweb_id
|
|
78
|
+
materialize :code
|
|
79
|
+
materialize :name
|
|
80
|
+
|
|
81
|
+
link :city do
|
|
82
|
+
materialize :tz_name, as: :timezone
|
|
83
|
+
|
|
84
|
+
link :country do
|
|
85
|
+
materialize :name, as: :country_name
|
|
86
|
+
materialize :iso_alpha2_code, as: :country_iso_alpha2_code
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Here is what each part of the DSL mean:
|
|
93
|
+
|
|
94
|
+
#### `use_model <model_name>`
|
|
95
|
+
describes the name of the active record model to be used.
|
|
96
|
+
|
|
97
|
+
#### `materialize <key>, as: <column> (default: key)`
|
|
98
|
+
describes mapping a resource key to database column.
|
|
99
|
+
|
|
100
|
+
#### `link <key>`
|
|
101
|
+
describes materializing from a relation of the resource. This can be nested to any depth as shown above.
|
|
102
|
+
|
|
103
|
+
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.
|
|
104
|
+
|
|
105
|
+
#### `after_upsert <method>`
|
|
106
|
+
describes the name of the instance method to be invoked after a record was materialized.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
class ZoneMaterializer
|
|
110
|
+
include Materialist::Materializer
|
|
111
|
+
|
|
112
|
+
after_upsert :my_method
|
|
113
|
+
|
|
114
|
+
def my_method(record)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
```
|
data/Rakefile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
data/appraise
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#
|
|
3
|
+
# Run all appraisals, with all specified rubies
|
|
4
|
+
#
|
|
5
|
+
require 'yaml'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
RUBIES = YAML.load_file('.travis.yml')['rvm']
|
|
9
|
+
APPRAISALS = `appraisal list`.strip.split(/\s+/)
|
|
10
|
+
|
|
11
|
+
# setup
|
|
12
|
+
RUBIES.each do |ruby|
|
|
13
|
+
ENV['RBENV_VERSION'] = ruby
|
|
14
|
+
system 'rbenv version'
|
|
15
|
+
system 'rbenv exec ruby -v'
|
|
16
|
+
system 'rbenv exec bundle check || rbenv exec bundle install'
|
|
17
|
+
system "rbenv exec appraisal install"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# tests
|
|
21
|
+
RUBIES.each do |ruby|
|
|
22
|
+
ENV['RBENV_VERSION'] = ruby
|
|
23
|
+
system 'rbenv exec ruby -v'
|
|
24
|
+
APPRAISALS.each do |variant|
|
|
25
|
+
puts "*** Ruby #{ruby} / variant #{variant}"
|
|
26
|
+
system "rbenv exec appraisal #{variant} rspec"
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/materialist.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'active_support/inflector'
|
|
2
|
+
require_relative './event_worker'
|
|
3
|
+
|
|
4
|
+
module Materialist
|
|
5
|
+
class EventHandler
|
|
6
|
+
|
|
7
|
+
def initialize(options={})
|
|
8
|
+
@options = options
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def on_events_received(batch)
|
|
12
|
+
batch.each { |event| call(event) if topics.include?(event['topic'].to_s) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(event)
|
|
16
|
+
worker.perform_async(event)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
attr_reader :options
|
|
22
|
+
|
|
23
|
+
def topics
|
|
24
|
+
@_topics ||= options.fetch(:topics, []).map(&:to_s)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def worker
|
|
28
|
+
@_worker ||= Materialist::EventWorker.set(options.slice(:queue))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'sidekiq'
|
|
2
|
+
require 'active_support/inflector'
|
|
3
|
+
|
|
4
|
+
module Materialist
|
|
5
|
+
class EventWorker
|
|
6
|
+
include Sidekiq::Worker
|
|
7
|
+
|
|
8
|
+
def perform(event)
|
|
9
|
+
topic = event['topic']
|
|
10
|
+
materializer = "#{topic.to_s.singularize.classify}Materializer".constantize
|
|
11
|
+
materializer.perform(event['url'], event['type'].to_sym)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
require 'routemaster/api_client'
|
|
2
|
+
|
|
3
|
+
module Materialist
|
|
4
|
+
module Materializer
|
|
5
|
+
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(Internals::ClassMethods)
|
|
8
|
+
base.extend(Internals::DSL)
|
|
9
|
+
|
|
10
|
+
root_mapping = []
|
|
11
|
+
base.instance_variable_set(:@materialist_options, { mapping: root_mapping })
|
|
12
|
+
base.instance_variable_set(:@__materialist_dsl_mapping_stack, [root_mapping])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module Internals
|
|
16
|
+
class FieldMapping
|
|
17
|
+
def initialize(key:, as:)
|
|
18
|
+
@key = key
|
|
19
|
+
@as = as
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :key, :as
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class LinkMapping
|
|
26
|
+
def initialize(key:)
|
|
27
|
+
@key = key
|
|
28
|
+
@mapping = []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
attr_reader :key, :mapping
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module ClassMethods
|
|
35
|
+
attr_reader :materialist_options, :__materialist_dsl_mapping_stack
|
|
36
|
+
|
|
37
|
+
def perform(url, action)
|
|
38
|
+
materializer = Materializer.new(url, self)
|
|
39
|
+
action == :delete ? materializer.destroy : materializer.upsert
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
module DSL
|
|
44
|
+
|
|
45
|
+
def materialize(key, as: key)
|
|
46
|
+
__materialist_dsl_mapping_stack.last << FieldMapping.new(key: key, as: as)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def link(key)
|
|
50
|
+
link_mapping = LinkMapping.new(key: key)
|
|
51
|
+
__materialist_dsl_mapping_stack.last << link_mapping
|
|
52
|
+
__materialist_dsl_mapping_stack << link_mapping.mapping
|
|
53
|
+
yield
|
|
54
|
+
__materialist_dsl_mapping_stack.pop
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def use_model(klass)
|
|
58
|
+
materialist_options[:model_class] = klass
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def after_upsert(method_name)
|
|
62
|
+
materialist_options[:after_upsert] = method_name
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class Materializer
|
|
68
|
+
|
|
69
|
+
def initialize(url, klass)
|
|
70
|
+
@url = url
|
|
71
|
+
@instance = klass.new
|
|
72
|
+
@options = klass.materialist_options
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def upsert
|
|
76
|
+
upsert_record.tap do |entity|
|
|
77
|
+
instance.send(after_upsert, entity) if after_upsert
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def destroy
|
|
82
|
+
model_class.where(source_url: url).destroy_all
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
attr_reader :url, :instance, :options
|
|
88
|
+
|
|
89
|
+
def upsert_record
|
|
90
|
+
model_class.find_or_initialize_by(source_url: url).tap do |entity|
|
|
91
|
+
entity.update_attributes attributes
|
|
92
|
+
entity.save!
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def mapping
|
|
97
|
+
options.fetch :mapping
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def after_upsert
|
|
101
|
+
options[:after_upsert]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def model_class
|
|
105
|
+
options.fetch(:model_class).to_s.classify.constantize
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def attributes
|
|
109
|
+
build_attributes resource_at(url), mapping
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def build_attributes(resource, mapping)
|
|
113
|
+
return {} unless resource
|
|
114
|
+
|
|
115
|
+
mapping.inject({}) do |result, m|
|
|
116
|
+
case m
|
|
117
|
+
when FieldMapping
|
|
118
|
+
result.tap { |r| r[m.as] = resource.body[m.key] }
|
|
119
|
+
when LinkMapping
|
|
120
|
+
resource.body._links.include?(m.key) ?
|
|
121
|
+
result.merge(build_attributes(resource_at(resource.send(m.key).url, allow_nil: true), m.mapping || [])) :
|
|
122
|
+
result
|
|
123
|
+
else
|
|
124
|
+
result
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def resource_at(url, allow_nil: false)
|
|
130
|
+
api_client.get(url, options: { enable_caching: false })
|
|
131
|
+
rescue Routemaster::Errors::ResourceNotFound
|
|
132
|
+
raise unless allow_nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def api_client
|
|
136
|
+
@_api_client ||= Routemaster::APIClient.new(
|
|
137
|
+
response_class: Routemaster::Responses::HateoasResponse
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
data/materialist.gemspec
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'materialist'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = 'materialist'
|
|
8
|
+
spec.version = Materialist::VERSION
|
|
9
|
+
spec.authors = ['Mo Valipour']
|
|
10
|
+
spec.email = ['valipour@gmail.com']
|
|
11
|
+
spec.summary = %q{Utilities to materialize routemaster topics}
|
|
12
|
+
spec.homepage = 'http://github.com/deliveroo/materialist'
|
|
13
|
+
spec.license = 'MIT'
|
|
14
|
+
|
|
15
|
+
spec.files = `git ls-files`.split($/)
|
|
16
|
+
spec.test_files = spec.files.grep(%r{^spec/})
|
|
17
|
+
spec.require_paths = %w(lib)
|
|
18
|
+
|
|
19
|
+
spec.add_runtime_dependency 'sidekiq'
|
|
20
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'materialist/event_handler'
|
|
3
|
+
require 'materialist/event_worker'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Materialist::EventHandler do
|
|
6
|
+
let(:options) {{}}
|
|
7
|
+
subject { described_class.new options }
|
|
8
|
+
|
|
9
|
+
let(:worker_double) { double() }
|
|
10
|
+
before do
|
|
11
|
+
allow(Materialist::EventWorker).to receive(:set)
|
|
12
|
+
.and_return worker_double
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe "#on_events_received" do
|
|
16
|
+
let(:events) {[{ "topic" => :topic_a }, { "topic" => :topic_b }]}
|
|
17
|
+
let(:perform) { subject.on_events_received events.map() }
|
|
18
|
+
|
|
19
|
+
context "when no topic is specified" do
|
|
20
|
+
let(:options) {{ topics: [] }}
|
|
21
|
+
|
|
22
|
+
it "doesn't enqueue any event" do
|
|
23
|
+
expect(worker_double).to_not receive(:perform_async)
|
|
24
|
+
perform
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
context "when a topic is specified" do
|
|
29
|
+
let(:options) {{ topics: [:topic_a] }}
|
|
30
|
+
|
|
31
|
+
it "enqueues event of that topic" do
|
|
32
|
+
expect(worker_double).to receive(:perform_async).with(events[0])
|
|
33
|
+
perform
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context "when both topics are specified" do
|
|
38
|
+
let(:options) {{ topics: [:topic_a, :topic_b] }}
|
|
39
|
+
|
|
40
|
+
it "enqueues event of both topics" do
|
|
41
|
+
expect(worker_double).to receive(:perform_async).twice
|
|
42
|
+
perform
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe "#call" do
|
|
48
|
+
let(:event) { { "topic" => :foobar } }
|
|
49
|
+
let(:perform) { subject.call event }
|
|
50
|
+
|
|
51
|
+
it "enqueues the event" do
|
|
52
|
+
expect(worker_double).to receive(:perform_async).with(event)
|
|
53
|
+
perform
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
context "if queue name is privided" do
|
|
57
|
+
let(:queue_name) { :some_queue_name }
|
|
58
|
+
let(:options) {{ queue: queue_name }}
|
|
59
|
+
|
|
60
|
+
it "enqueues the event in the given queue" do
|
|
61
|
+
expect(Materialist::EventWorker).to receive(:set)
|
|
62
|
+
.with(queue: queue_name)
|
|
63
|
+
expect(worker_double).to receive(:perform_async).with(event)
|
|
64
|
+
perform
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'materialist/event_worker'
|
|
3
|
+
|
|
4
|
+
RSpec.describe Materialist::EventWorker do
|
|
5
|
+
describe "#perform" do
|
|
6
|
+
let(:source_url) { 'https://service.dev/foobars/1' }
|
|
7
|
+
let(:event) {{ 'topic' => :foobar, 'url' => source_url, 'type' => 'noop' }}
|
|
8
|
+
let!(:materializer_class) { class FoobarMaterializer; end }
|
|
9
|
+
|
|
10
|
+
before do
|
|
11
|
+
allow(FoobarMaterializer).to receive(:perform)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
context "when run synchronously" do
|
|
15
|
+
let(:perform) { subject.perform(event) }
|
|
16
|
+
|
|
17
|
+
it "calls the relevant materializer" do
|
|
18
|
+
expect(FoobarMaterializer).to receive(:perform).with(source_url, :noop)
|
|
19
|
+
perform
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'materialist/materializer'
|
|
3
|
+
|
|
4
|
+
RSpec.describe Materialist::Materializer do
|
|
5
|
+
describe "#perform" do
|
|
6
|
+
let!(:materializer_class) do
|
|
7
|
+
class FoobarMaterializer
|
|
8
|
+
include Materialist::Materializer
|
|
9
|
+
|
|
10
|
+
use_model :foobar
|
|
11
|
+
materialize :name
|
|
12
|
+
materialize :age, as: :how_old
|
|
13
|
+
|
|
14
|
+
link :city do
|
|
15
|
+
materialize :timezone
|
|
16
|
+
|
|
17
|
+
link :country do
|
|
18
|
+
materialize :tld, as: :country_tld
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
let!(:foobar_class) { class Foobar; end }
|
|
25
|
+
let(:country_url) { 'https://service.dev/countries/1' }
|
|
26
|
+
let(:country_body) {{ tld: 'fr' }}
|
|
27
|
+
let(:city_url) { 'https://service.dev/cities/1' }
|
|
28
|
+
let(:city_body) {{ _links: { country: { href: country_url }}, timezone: 'Europe/Paris' }}
|
|
29
|
+
let(:source_url) { 'https://service.dev/foobars/1' }
|
|
30
|
+
let(:source_body) {{ _links: { city: { href: city_url }}, name: 'jack', age: 30 }}
|
|
31
|
+
before do
|
|
32
|
+
stub_request(:get, source_url).to_return(
|
|
33
|
+
status: 200,
|
|
34
|
+
body: source_body.to_json,
|
|
35
|
+
headers: { 'Content-Type' => 'application/json' }
|
|
36
|
+
)
|
|
37
|
+
stub_request(:get, country_url).to_return(
|
|
38
|
+
status: 200,
|
|
39
|
+
body: country_body.to_json,
|
|
40
|
+
headers: { 'Content-Type' => 'application/json' }
|
|
41
|
+
)
|
|
42
|
+
stub_request(:get, city_url).to_return(
|
|
43
|
+
status: 200,
|
|
44
|
+
body: city_body.to_json,
|
|
45
|
+
headers: { 'Content-Type' => 'application/json' }
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
let(:expected_attributes) do
|
|
50
|
+
{ name: 'jack', how_old: 30, country_tld: 'fr', timezone: 'Europe/Paris' }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
let(:record_double) { double() }
|
|
54
|
+
before do
|
|
55
|
+
allow(Foobar).to receive(:find_or_initialize_by).and_return record_double
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
let(:action) { :create }
|
|
59
|
+
let(:perform) { FoobarMaterializer.perform(source_url, action) }
|
|
60
|
+
|
|
61
|
+
def performs_upsert
|
|
62
|
+
expect(Foobar).to receive(:find_or_initialize_by)
|
|
63
|
+
.with(source_url: source_url)
|
|
64
|
+
expect(record_double).to receive(:update_attributes).with(expected_attributes)
|
|
65
|
+
expect(record_double).to receive(:save!)
|
|
66
|
+
perform
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def performs_destroy
|
|
70
|
+
expect(Foobar).to receive(:where)
|
|
71
|
+
.with(source_url: source_url)
|
|
72
|
+
.and_return record_double
|
|
73
|
+
expect(record_double).to receive(:destroy_all)
|
|
74
|
+
perform
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it { performs_upsert }
|
|
78
|
+
|
|
79
|
+
%i(create update noop).each do |action_name|
|
|
80
|
+
context "when action is :#{action_name}" do
|
|
81
|
+
let(:action) { action_name }
|
|
82
|
+
it { performs_upsert }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
context "when action is :delete" do
|
|
87
|
+
let(:action) { :delete }
|
|
88
|
+
|
|
89
|
+
it { performs_destroy }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
context "if resource returns 404" do
|
|
93
|
+
before { stub_request(:get, source_url).to_return(status: 404) }
|
|
94
|
+
|
|
95
|
+
it "bubbles up routemaster not found error" do
|
|
96
|
+
expect { perform }.to raise_error Routemaster::Errors::ResourceNotFound
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
context "if a linked resource returns 404" do
|
|
101
|
+
before { stub_request(:get, city_url).to_return(status: 404) }
|
|
102
|
+
|
|
103
|
+
let(:expected_attributes) do
|
|
104
|
+
{ name: 'jack', how_old: 30 }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "ignores keys from the relation" do
|
|
108
|
+
performs_upsert
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
context "when after_upsert is configured" do
|
|
113
|
+
let(:expected_attributes) {{}}
|
|
114
|
+
let!(:materializer_class) do
|
|
115
|
+
class FoobarMaterializer
|
|
116
|
+
include Materialist::Materializer
|
|
117
|
+
|
|
118
|
+
use_model :foobar
|
|
119
|
+
after_upsert :my_method
|
|
120
|
+
|
|
121
|
+
def my_method(entity)
|
|
122
|
+
entity.after_upsert_action
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
%i(create update noop).each do |action_name|
|
|
128
|
+
context "when action is :#{action_name}" do
|
|
129
|
+
let(:action) { action_name }
|
|
130
|
+
it "calls after_upsert method" do
|
|
131
|
+
expect(record_double).to receive(:after_upsert_action)
|
|
132
|
+
performs_upsert
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
context "when action is :delete" do
|
|
138
|
+
let(:action) { :delete }
|
|
139
|
+
|
|
140
|
+
it "does not call after_upsert method" do
|
|
141
|
+
expect(record_double).to_not receive(:after_upsert_action)
|
|
142
|
+
performs_destroy
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'simplecov'
|
|
2
|
+
SimpleCov.start
|
|
3
|
+
|
|
4
|
+
require 'codecov'
|
|
5
|
+
SimpleCov.formatter = SimpleCov::Formatter::Codecov if ENV['CI']
|
|
6
|
+
|
|
7
|
+
require 'webmock/rspec'
|
|
8
|
+
require 'dotenv'
|
|
9
|
+
require 'pry'
|
|
10
|
+
require 'byebug'
|
|
11
|
+
Dotenv.overload('.env.test')
|
|
12
|
+
|
|
13
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
|
14
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
|
15
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
|
16
|
+
# loaded once.
|
|
17
|
+
#
|
|
18
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
|
19
|
+
RSpec.configure do |config|
|
|
20
|
+
config.run_all_when_everything_filtered = true
|
|
21
|
+
config.raise_errors_for_deprecations!
|
|
22
|
+
|
|
23
|
+
# Run specs in random order to surface order dependencies. If you find an
|
|
24
|
+
# order dependency and want to debug it, you can fix the order by providing
|
|
25
|
+
# the seed, which is printed after each run.
|
|
26
|
+
# --seed 1234
|
|
27
|
+
config.order = 'random'
|
|
28
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: materialist
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mo Valipour
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2017-08-31 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: sidekiq
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
description:
|
|
28
|
+
email:
|
|
29
|
+
- valipour@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- ".env.test"
|
|
35
|
+
- ".gitignore"
|
|
36
|
+
- ".rspec"
|
|
37
|
+
- ".ruby-version"
|
|
38
|
+
- Appraisals
|
|
39
|
+
- Gemfile
|
|
40
|
+
- Gemfile.lock
|
|
41
|
+
- Guardfile
|
|
42
|
+
- LICENSE.txt
|
|
43
|
+
- README.md
|
|
44
|
+
- Rakefile
|
|
45
|
+
- appraise
|
|
46
|
+
- lib/materialist.rb
|
|
47
|
+
- lib/materialist/event_handler.rb
|
|
48
|
+
- lib/materialist/event_worker.rb
|
|
49
|
+
- lib/materialist/materializer.rb
|
|
50
|
+
- materialist.gemspec
|
|
51
|
+
- spec/materialist/event_handler_spec.rb
|
|
52
|
+
- spec/materialist/event_worker_spec.rb
|
|
53
|
+
- spec/materialist/materializer_spec.rb
|
|
54
|
+
- spec/spec_helper.rb
|
|
55
|
+
homepage: http://github.com/deliveroo/materialist
|
|
56
|
+
licenses:
|
|
57
|
+
- MIT
|
|
58
|
+
metadata: {}
|
|
59
|
+
post_install_message:
|
|
60
|
+
rdoc_options: []
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0'
|
|
73
|
+
requirements: []
|
|
74
|
+
rubyforge_project:
|
|
75
|
+
rubygems_version: 2.6.8
|
|
76
|
+
signing_key:
|
|
77
|
+
specification_version: 4
|
|
78
|
+
summary: Utilities to materialize routemaster topics
|
|
79
|
+
test_files:
|
|
80
|
+
- spec/materialist/event_handler_spec.rb
|
|
81
|
+
- spec/materialist/event_worker_spec.rb
|
|
82
|
+
- spec/materialist/materializer_spec.rb
|
|
83
|
+
- spec/spec_helper.rb
|