pub_sub_model_sync 0.2.4 → 0.4.2

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
  SHA256:
3
- metadata.gz: 7655730b3b89716b76be52db708b7c93ec64176cc158fc84323d4e811f6dcdf1
4
- data.tar.gz: 874aa3f5c5c679fd3e7ec02550426f0824481194ebd99b1a0494180df3a22cc6
3
+ metadata.gz: c0bd383488f8214ca409871adf231319b905f41833b8a9db9c2bc58a9d5a2691
4
+ data.tar.gz: c05ca888a6c085280407f9a1e26fe7552efa1c33f6be7735129ff2f8addd77fc
5
5
  SHA512:
6
- metadata.gz: 9e8406293a7528fa5d697c2a6a30b4cb00cee900b349ef834fad5fd7771ce864a8338ac95bc1bcbe41568d9efa1737111720607cb0f002e684ab532e835ec651
7
- data.tar.gz: 0f995774ddf1b916db02fed9051c26b106e549261877c72a718e9e23c45544dff6c9757231e094201c9d466b2c4922d8e6364f8167697820db5833d78ee85c85
6
+ metadata.gz: f8769aee9d08936fd9fd4707e1dd5664c30ccbb6b2b59e386182ac4a0c173b46813df9ca5ccc3a3e3139af4297c160ca5ddb05a555d8f2c1fbb719e7a54384f4
7
+ data.tar.gz: bc25309e3894fd5905d0e3fdc180d93a98cfced77ff006f24d0044f1db717f5b0fa475722126901ec18381d3e4890a2a0ff00f7ecb234405cdad4a1e7049a5fb
@@ -6,28 +6,45 @@ on:
6
6
  - master
7
7
  pull_request:
8
8
 
9
-
10
9
  jobs:
11
10
  build:
12
11
  name: Tests and Code Style
13
12
  runs-on: ubuntu-latest
13
+ strategy:
14
+ matrix:
15
+ ruby: [2.4, 2.5, 2.6]
16
+ rails: [4, 5, 6]
17
+ include:
18
+ - ruby: 2.7
19
+ rails: 6
20
+ exclude: # rails 6 requires ruby >= 2.5
21
+ - ruby: 2.4
22
+ rails: 6
14
23
 
15
24
  steps:
16
25
  - uses: actions/checkout@v2
17
- - name: Set up Ruby 2.6
26
+ - name: Set up Ruby
18
27
  uses: actions/setup-ruby@v1
19
28
  with:
20
- ruby-version: 2.6.x
21
-
29
+ ruby-version: ${{ matrix.ruby }}
22
30
  - name: Install sqlite3
23
31
  run: sudo apt-get install libsqlite3-dev
24
32
 
25
- - name: Bundle install
33
+ - name: Install bundler
34
+ env:
35
+ GEMFILE_PATH: gemfiles/Gemfile_${{ matrix.rails }}
36
+ RAILS_V: ${{ matrix.rails }}
26
37
  run: |
27
- gem install bundler
28
- bundle install --jobs 4 --retry 3
38
+ rm -f Gemfile.lock && rm -f Gemfile
39
+ cp $GEMFILE_PATH ./Gemfile
40
+ bundler_v='2.1.4'
41
+ if [ $RAILS_V = "4" ]; then bundler_v="1.16.6"; fi
42
+ gem install bundler -v "~> $bundler_v"
43
+ bundle _${bundler_v}_ install --jobs 4 --retry 3
44
+
29
45
  - name: Tests (rspec)
30
46
  run: |
31
47
  bundle exec rspec
48
+
32
49
  - name: Code style (Rubocop)
33
50
  run: bundle exec rubocop
@@ -6,13 +6,11 @@ AllCops:
6
6
  - 'Gemfile'
7
7
  - 'Rakefile'
8
8
  - 'bin/*'
9
- TargetRubyVersion: 2.3
10
9
 
11
10
  Metrics/BlockLength:
12
11
  Exclude:
13
12
  - 'spec/**/*.rb'
14
13
 
15
-
16
14
  Style/SymbolArray:
17
15
  Exclude:
18
16
  - 'Gemfile'
@@ -1,5 +1,26 @@
1
1
  # Change Log
2
2
 
3
+ # 0.4.1 (May 12, 2020)
4
+ - chore: improve log messages
5
+ - feat: do not update model if no changes
6
+ - feat: skip publisher after updating if no changes
7
+
8
+
9
+ # 0.4.0 (May 06, 2020)
10
+ - rename as_klass to from_klass and as_action to from_action for subscribers
11
+ - refactor subscribers to be independent
12
+ - refactor message_publisher to use publisher
13
+ - rename publisher into message_publisher
14
+ - reformat publisher to reuse connector
15
+
16
+ # 0.3.1 (May 05, 2020)
17
+ - improve rabbit service to use sleep instead of block ("Block is not recommended for production")
18
+ - improve message ID
19
+
20
+ # 0.3.0 (April 29, 2020)
21
+ - Support for multiple identifiers when syncing
22
+ - Add klass.ps_find_model method for a custom model finder
23
+
3
24
  # 0.2.4 (April 28, 2020)
4
25
  - Delegate .publish to the .publisher for better understanding
5
26
 
data/Gemfile CHANGED
@@ -5,5 +5,9 @@ gem 'bunny' # rabbit-mq
5
5
  gem 'google-cloud-pubsub' # google pub/sub
6
6
  gem 'ruby-kafka' # kafka pub/sub
7
7
 
8
+ group :test do
9
+ gem 'database_cleaner-active_record'
10
+ end
11
+
8
12
  # Specify your gem's dependencies in pub_sub_model_sync.gemspec
9
13
  gemspec
@@ -1,8 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pub_sub_model_sync (0.2.4)
5
- activesupport
4
+ pub_sub_model_sync (0.4.2)
6
5
  rails
7
6
 
8
7
  GEM
@@ -72,6 +71,10 @@ GEM
72
71
  amq-protocol (~> 2.3, >= 2.3.0)
73
72
  concurrent-ruby (1.1.6)
74
73
  crass (1.0.6)
74
+ database_cleaner (1.8.4)
75
+ database_cleaner-active_record (1.8.0)
76
+ activerecord
77
+ database_cleaner (~> 1.8.0)
75
78
  diff-lcs (1.3)
76
79
  digest-crc (0.5.1)
77
80
  erubi (1.9.0)
@@ -119,7 +122,7 @@ GEM
119
122
  concurrent-ruby (~> 1.0)
120
123
  jaro_winkler (1.5.4)
121
124
  jwt (2.2.1)
122
- loofah (2.5.0)
125
+ loofah (2.6.0)
123
126
  crass (~> 1.0.2)
124
127
  nokogiri (>= 1.5.9)
125
128
  mail (2.7.1)
@@ -128,21 +131,21 @@ GEM
128
131
  mimemagic (~> 0.3.2)
129
132
  memoist (0.16.2)
130
133
  method_source (1.0.0)
131
- mimemagic (0.3.4)
134
+ mimemagic (0.3.5)
132
135
  mini_mime (1.0.2)
133
136
  mini_portile2 (2.4.0)
134
137
  minitest (5.14.0)
135
138
  multi_json (1.14.1)
136
139
  multipart-post (2.1.1)
137
140
  nio4r (2.5.2)
138
- nokogiri (1.10.9)
141
+ nokogiri (1.10.10)
139
142
  mini_portile2 (~> 2.4.0)
140
143
  os (1.0.1)
141
144
  parallel (1.19.1)
142
145
  parser (2.7.0.4)
143
146
  ast (~> 2.4.0)
144
147
  public_suffix (4.0.3)
145
- rack (2.2.2)
148
+ rack (2.2.3)
146
149
  rack-test (1.1.0)
147
150
  rack (>= 1.0, < 3)
148
151
  rails (6.0.2.2)
@@ -204,7 +207,7 @@ GEM
204
207
  faraday (~> 0.9)
205
208
  jwt (>= 1.5, < 3.0)
206
209
  multi_json (~> 1.10)
207
- sprockets (4.0.0)
210
+ sprockets (4.0.2)
208
211
  concurrent-ruby (~> 1.0)
209
212
  rack (> 1, < 3)
210
213
  sprockets-rails (3.2.1)
@@ -217,9 +220,9 @@ GEM
217
220
  tzinfo (1.2.7)
218
221
  thread_safe (~> 0.1)
219
222
  unicode-display_width (1.6.1)
220
- websocket-driver (0.7.1)
223
+ websocket-driver (0.7.3)
221
224
  websocket-extensions (>= 0.1.0)
222
- websocket-extensions (0.1.4)
225
+ websocket-extensions (0.1.5)
223
226
  zeitwerk (2.3.0)
224
227
 
225
228
  PLATFORMS
@@ -228,6 +231,7 @@ PLATFORMS
228
231
  DEPENDENCIES
229
232
  bundler
230
233
  bunny
234
+ database_cleaner-active_record
231
235
  google-cloud-pubsub
232
236
  pub_sub_model_sync!
233
237
  rake
data/README.md CHANGED
@@ -61,13 +61,16 @@ And then execute: $ bundle install
61
61
  ```
62
62
  Note: Publishers do not need todo this
63
63
 
64
+ - Check the service status with:
65
+ ```PubSubModelSync::MessagePublisher.publish_data('Test message', {sample_value: 10}, :create)```
66
+
64
67
  ## Examples
65
68
  ```ruby
66
69
  # App 1 (Publisher)
67
70
  # attributes: name email age
68
71
  class User < ActiveRecord::Base
69
72
  include PubSubModelSync::PublisherConcern
70
- ps_publish(%i[name email])
73
+ ps_publish(%i[id name email])
71
74
  end
72
75
 
73
76
  # App 2 (Subscriber)
@@ -86,7 +89,7 @@ User.create(name: 'test user', email: 'sample@gmail.com') # Review your App 2 to
86
89
  User.new(name: 'test user').ps_perform_sync(:create) # similar to above to perform sync on demand
87
90
 
88
91
  User.ps_class_publish({ msg: 'Hello' }, action: :greeting) # User.greeting method (Class method) will be called in App2
89
- PubSubModelSync::Publisher.new.publish_data(User, { msg: 'Hello' }, :greeting) # similar to above when not included publisher concern
92
+ PubSubModelSync::MessagePublisher.publish_data(User, { msg: 'Hello' }, :greeting) # similar to above when not included publisher concern
90
93
  ```
91
94
 
92
95
  ## Advanced Example
@@ -95,7 +98,7 @@ PubSubModelSync::Publisher.new.publish_data(User, { msg: 'Hello' }, :greeting) #
95
98
  class User < ActiveRecord::Base
96
99
  self.table_name = 'publisher_users'
97
100
  include PubSubModelSync::PublisherConcern
98
- ps_publish(%i[name:full_name email], actions: %i[update], as_klass: 'Client', id: :client_id)
101
+ ps_publish(%i[id:client_id name:full_name email], actions: %i[update], as_klass: 'Client')
99
102
 
100
103
  def ps_skip_callback?(_action)
101
104
  false # here logic with action to skip push message
@@ -110,30 +113,66 @@ end
110
113
  class User < ActiveRecord::Base
111
114
  self.table_name = 'subscriber_users'
112
115
  include PubSubModelSync::SubscriberConcern
113
- ps_subscribe(%i[name], actions: %i[update], as_klass: 'Client', id: :custom_id)
114
- ps_class_subscribe(:greeting, as_action: :custom_greeting, as_klass: 'CustomUser')
116
+ ps_subscribe(%i[name], actions: %i[update], from_klass: 'Client', id: %i[client_id email])
117
+ ps_class_subscribe(:greeting, from_action: :custom_greeting, from_klass: 'CustomUser')
118
+ alias_attribute :full_name, :name
115
119
 
116
120
  def self.greeting(data)
117
121
  puts 'Class message called through custom_greeting'
118
122
  end
123
+
124
+ # def self.ps_find_model(data)
125
+ # where(email: data[:email], ...).first_or_initialize
126
+ # end
119
127
  end
120
128
  ```
121
129
 
122
130
  Note: Be careful with collision of names
123
131
  ```
124
- class User
125
- # ps_publish %i[name_data:name name:key] # key will be replaced with name_data
126
- ps_publish %i[name_data:name key_data:key] # use alias to avoid collision
127
-
128
- def key_data
129
- name
130
- end
131
- end
132
+ # ps_publish %i[name_data:name name:key] # key will be replaced with name_data
133
+ ps_publish %i[name_data:name key_data:key] # use alias to avoid collision
132
134
  ```
133
135
 
134
136
  ## API
137
+ ### Subscribers
138
+ - Permit to configure class level subscriptions
139
+ ```ps_class_subscribe(action_name, from_action: nil, from_klass: nil)```
140
+ * from_action: (Optional) Source method name
141
+ * from_klass: (Optional) Source class name
142
+
143
+ - Permit to configure instance level subscriptions (CRUD)
144
+ ```ps_subscribe(attrs, from_klass: nil, actions: nil, id: nil)```
145
+ * attrs: (Array/Required) Array of all attributes to be synced
146
+ * from_klass: (String/Optional) Source class name (Instead of the model class name, will use this value)
147
+ * actions: (Array/Optional, default: create/update/destroy) permit to customize action names
148
+ * id: (Sym|Array/Optional, default: id) Attr identifier(s) to find the corresponding model
149
+
150
+ - Permit to configure a custom model finder
151
+ ```ps_find_model(data)```
152
+ * data: (Hash) Data received from sync
153
+ Must return an existent or a new model object
154
+
155
+ - Get crud subscription configured for the class
156
+ ```User.ps_subscriber(action_name)```
157
+ * action_name (default :create, :sym): can be :create, :update, :destroy
158
+
159
+ - Inspect all configured subscribers
160
+ ```PubSubModelSync::Config.subscribers```
161
+
162
+ - Permit to customize the way to detect if the subscribed model was changed (Only for update action).
163
+ ```.ps_subscriber_changed?(data)```
164
+ By default: ```model.changed?```
165
+
166
+ ### Publishers
167
+ - Permit to configure crud publishers
168
+ ```ps_publish(attrs, actions: nil, as_klass: nil)```
169
+ * attrs: (Array/Required) Array of attributes to be published
170
+ * actions: (Array/Optional, default: create/update/destroy) permit to customize action names
171
+ * as_klass: (String/Optional) Output class name (Instead of the model class name, will use this value)
172
+
135
173
  - Permit to cancel sync called after create/update/destroy (Before initializing sync service)
136
174
  ```model.ps_skip_callback?(action)```
175
+ Default: False
137
176
  Note: Return true to cancel sync
138
177
 
139
178
  - Callback called before preparing data for sync (Permit to stop sync)
@@ -150,29 +189,24 @@ end
150
189
  - Perform sync on demand (:create, :update, :destroy):
151
190
  The target model will receive a notification to perform the indicated action
152
191
  ```my_model.ps_perform_sync(action_name, custom_settings = {})```
153
- * custom_settings: override default settings defined for action_name ({ attrs: [], as_klass: nil, id: nil })
192
+ * custom_settings: override default settings defined for action_name ({ attrs: [], as_klass: nil })
154
193
 
155
- - Class level notification:
194
+ - Publish a class level notification:
156
195
  ```User.ps_class_publish(data, action: action_name, as_klass: custom_klass_name)```
157
196
  Target class ```User.action_name``` will be called when message is received
158
197
  * data: (required, :hash) message value to deliver
159
- * action_name: (required, :sim) same action name as defined in ps_class_subscribe(...)
160
- * as_klass: (optional, :string) same class name as defined in ps_class_subscribe(...)
198
+ * action_name: (required, :sim) Action name
199
+ * as_klass: (optional, :string) Custom class name (Default current model name)
161
200
 
162
- - Class level notification (Same as above: on demand call)
163
- ```PubSubModelSync::Publisher.new.publish_data(Klass_name, data, action_name)```
201
+ - Publish a class level notification (Same as above: on demand call)
202
+ ```PubSubModelSync::MessagePublisher.publish_data(Klass_name, data, action_name)```
164
203
  * klass_name: (required, Class) same class name as defined in ps_class_subscribe(...)
165
204
  * data: (required, :hash) message value to deliver
166
205
  * action_name: (required, :sim) same action name as defined in ps_class_subscribe(...)
167
206
 
168
- - Get crud subscription configured for the class
169
- ```User.ps_subscriber(action_name)```
170
- * action_name (default :create, :sym): can be :create, :update, :destroy
171
207
  - Get crud publisher configured for the class
172
208
  ```User.ps_publisher(action_name)```
173
209
  * action_name (default :create, :sym): can be :create, :update, :destroy
174
- - Inspect all configured listeners
175
- ```PubSubModelSync::Config.listeners```
176
210
 
177
211
  ## Testing with RSpec
178
212
  - Config: (spec/rails_helper.rb)
@@ -205,12 +239,10 @@ end
205
239
  # Subscriber
206
240
  it 'receive model message' do
207
241
  action = :create
208
- data = { name: 'name' }
209
- user_id = 999
210
- attrs = PubSubModelSync::Publisher.build_attrs('User', action, user_id)
211
- publisher = PubSubModelSync::MessageProcessor.new(data, 'User', action, id: user_id)
242
+ data = { name: 'name', id: 999 }
243
+ publisher = PubSubModelSync::MessageProcessor.new(data, 'User', action)
212
244
  publisher.process
213
- expect(User.where(id: user_id).any?).to be_truth
245
+ expect(User.where(id: data[:id]).any?).to be_truth
214
246
  end
215
247
 
216
248
  it 'receive class message' do
@@ -223,20 +255,20 @@ end
223
255
 
224
256
  # Publisher
225
257
  it 'publish model action' do
226
- publisher = PubSubModelSync::Publisher
258
+ publisher = PubSubModelSync::MessagePublisher
227
259
  data = { name: 'hello'}
228
260
  action = :create
229
261
  User.ps_class_publish(data, action: action)
230
262
  user = User.create(name: 'name', email: 'email')
231
- expect_any_instance_of(publisher).to receive(:publish_model).with(user, :create, anything)
263
+ expect(publisher).to receive(:publish_model).with(user, :create, anything)
232
264
  end
233
265
 
234
266
  it 'publish class message' do
235
- publisher = PubSubModelSync::Publisher
267
+ publisher = PubSubModelSync::MessagePublisher
236
268
  data = {msg: 'hello'}
237
269
  action = :greeting
238
270
  User.ps_class_publish(data, action: action)
239
- expect_any_instance_of(publisher).to receive(:publish_data).with('User', data, action)
271
+ expect(publisher).to receive(:publish_data).with('User', data, action)
240
272
  end
241
273
  ```
242
274
 
@@ -0,0 +1,16 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rubocop'
4
+ gem 'bunny' # rabbit-mq
5
+ gem 'google-cloud-pubsub' # google pub/sub
6
+ gem 'ruby-kafka' # kafka pub/sub
7
+ gem 'rails', '~> 4'
8
+ gem 'bundler'
9
+ gem 'sqlite3', '1.3.13'
10
+
11
+ group :test do
12
+ gem 'database_cleaner-active_record'
13
+ end
14
+
15
+ # Specify your gem's dependencies in pub_sub_model_sync.gemspec
16
+ gemspec
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rubocop'
4
+ gem 'bunny' # rabbit-mq
5
+ gem 'google-cloud-pubsub' # google pub/sub
6
+ gem 'ruby-kafka' # kafka pub/sub
7
+ gem 'rails', '~> 5'
8
+
9
+ group :test do
10
+ gem 'database_cleaner-active_record'
11
+ end
12
+
13
+ # Specify your gem's dependencies in pub_sub_model_sync.gemspec
14
+ gemspec
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rubocop'
4
+ gem 'bunny' # rabbit-mq
5
+ gem 'google-cloud-pubsub' # google pub/sub
6
+ gem 'ruby-kafka' # kafka pub/sub
7
+ gem 'rails', '~> 6'
8
+
9
+ group :test do
10
+ gem 'database_cleaner-active_record'
11
+ end
12
+
13
+ # Specify your gem's dependencies in pub_sub_model_sync.gemspec
14
+ gemspec
@@ -6,12 +6,15 @@ require 'active_support'
6
6
  require 'pub_sub_model_sync/railtie'
7
7
  require 'pub_sub_model_sync/config'
8
8
  require 'pub_sub_model_sync/subscriber_concern'
9
- require 'pub_sub_model_sync/publisher'
9
+ require 'pub_sub_model_sync/message_publisher'
10
10
  require 'pub_sub_model_sync/publisher_concern'
11
11
  require 'pub_sub_model_sync/runner'
12
12
  require 'pub_sub_model_sync/connector'
13
13
  require 'pub_sub_model_sync/message_processor'
14
14
 
15
+ require 'pub_sub_model_sync/publisher'
16
+ require 'pub_sub_model_sync/subscriber'
17
+
15
18
  require 'pub_sub_model_sync/service_base'
16
19
  require 'pub_sub_model_sync/service_google'
17
20
  require 'pub_sub_model_sync/service_rabbit'
@@ -2,7 +2,7 @@
2
2
 
3
3
  module PubSubModelSync
4
4
  class Config
5
- cattr_accessor(:listeners) { [] }
5
+ cattr_accessor(:subscribers) { [] }
6
6
  cattr_accessor(:publishers) { [] }
7
7
  cattr_accessor(:service_name) { :google }
8
8
  cattr_accessor :logger
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'google/cloud/pubsub'
4
3
  module PubSubModelSync
5
4
  class Connector
6
5
  attr_accessor :service
@@ -2,83 +2,39 @@
2
2
 
3
3
  module PubSubModelSync
4
4
  class MessageProcessor
5
- attr_accessor :data, :attrs, :settings
5
+ attr_accessor :data, :klass, :action
6
6
 
7
7
  # @param data (Hash): any hash value to deliver
8
- # @param settings (optional): { id: id_val }
9
- def initialize(data, klass, action, settings = {})
8
+ def initialize(data, klass, action)
10
9
  @data = data
11
- @settings = settings
12
- @attrs = settings.merge(klass: klass, action: action)
10
+ @klass = klass
11
+ @action = action
13
12
  end
14
13
 
15
14
  def process
16
- @failed = false
17
- log 'processing message'
18
- listeners = filter_listeners
19
- return log 'Skipped: No listeners' unless listeners.any?
20
-
21
- eval_message(listeners)
22
- log 'processed message' unless @failed
15
+ subscribers = filter_subscribers
16
+ subscribers.each { |subscriber| run_subscriber(subscriber) }
23
17
  end
24
18
 
25
19
  private
26
20
 
27
- def eval_message(listeners)
28
- listeners.each do |listener|
29
- if listener[:direct_mode]
30
- call_class_listener(listener)
31
- else
32
- call_listener(listener)
33
- end
34
- end
35
- end
36
-
37
- def call_class_listener(listener)
38
- model_class = listener[:klass].constantize
39
- model_class.send(listener[:action], data)
21
+ def run_subscriber(subscriber)
22
+ subscriber.eval_message(data)
23
+ log "processed message with: #{[klass, action, data]}"
40
24
  rescue => e
41
- log("Error listener (#{listener}): #{e.message}", :error)
42
- @failed = true
43
- end
44
-
45
- # support for: create, update, destroy
46
- def call_listener(listener)
47
- model = find_model(listener)
48
- if attrs[:action].to_sym == :destroy
49
- model.destroy!
50
- else
51
- populate_model(model, listener)
52
- model.save!
53
- end
54
- rescue => e
55
- log("Error listener (#{listener}): #{e.message}", :error)
56
- @failed = true
57
- end
58
-
59
- def find_model(listener)
60
- model_class = listener[:klass].constantize
61
- identifier = listener[:settings][:id] || :id
62
- model_class.where(identifier => attrs[:id]).first_or_initialize
63
- end
64
-
65
- def populate_model(model, listener)
66
- values = data.slice(*listener[:settings][:attrs])
67
- values.each do |attr, value|
68
- model.send("#{attr}=", value)
69
- end
25
+ info = [klass, action, data, e.message, e.backtrace]
26
+ log("error processing message: #{info}", :error)
70
27
  end
71
28
 
72
- def filter_listeners
73
- listeners = PubSubModelSync::Config.listeners
74
- listeners.select do |listener|
75
- listener[:as_klass].to_s == attrs[:klass].to_s &&
76
- listener[:as_action].to_s == attrs[:action].to_s
29
+ def filter_subscribers
30
+ PubSubModelSync::Config.subscribers.select do |subscriber|
31
+ subscriber.settings[:from_klass].to_s == klass.to_s &&
32
+ subscriber.settings[:from_action].to_s == action.to_s
77
33
  end
78
34
  end
79
35
 
80
36
  def log(message, kind = :info)
81
- PubSubModelSync::Config.log "#{message} ==> #{[data, attrs]}", kind
37
+ PubSubModelSync::Config.log message, kind
82
38
  end
83
39
  end
84
40
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class MessagePublisher
5
+ class << self
6
+ delegate :publish, to: :connector
7
+
8
+ def connector
9
+ @connector ||= PubSubModelSync::Connector.new
10
+ end
11
+
12
+ def publish_data(klass, data, action)
13
+ attrs = { klass: klass.to_s, action: action.to_sym }
14
+ publish(data, attrs)
15
+ end
16
+
17
+ # @param model: ActiveRecord model
18
+ # @param action: (Sym) Action name
19
+ # @param publisher: (Publisher, optional) Publisher to be used
20
+ def publish_model(model, action, publisher = nil)
21
+ return if model.ps_skip_sync?(action)
22
+
23
+ publisher ||= model.class.ps_publisher(action)
24
+ payload = publisher.payload(model, action)
25
+ res_before = model.ps_before_sync(action, payload[:data])
26
+ return if res_before == :cancel
27
+
28
+ publish(payload[:data], payload[:attrs])
29
+ model.ps_after_sync(action, payload[:data])
30
+ end
31
+ end
32
+ end
33
+ end
@@ -2,47 +2,24 @@
2
2
 
3
3
  module PubSubModelSync
4
4
  class Publisher
5
- attr_accessor :connector
6
- delegate :publish, to: :connector
7
-
8
- def initialize
9
- @connector = PubSubModelSync::Connector.new
10
- end
11
-
12
- def publish_data(klass, data, action)
13
- attributes = self.class.build_attrs(klass, action)
14
- publish(data, attributes)
5
+ attr_accessor :attrs, :actions, :klass, :as_klass
6
+ def initialize(attrs, klass, actions = nil, as_klass = nil)
7
+ @attrs = attrs
8
+ @klass = klass
9
+ @actions = actions || %i[create update destroy]
10
+ @as_klass = as_klass || klass
15
11
  end
16
12
 
17
- # @param settings (Hash): { attrs: [], as_klass: nil, id: nil }
18
- def publish_model(model, action, settings = nil)
19
- return if model.ps_skip_sync?(action)
20
-
21
- settings ||= model.class.ps_publisher_info(action)
22
- attributes = build_model_attrs(model, action, settings)
23
- data = {}
24
- data = build_model_data(model, settings[:attrs]) if action != :destroy
25
- res_before = model.ps_before_sync(action, data)
26
- return if res_before == :cancel
27
-
28
- publish(data.symbolize_keys, attributes)
29
- model.ps_after_sync(action, data)
30
- end
31
-
32
- def self.build_attrs(klass, action, id = nil)
33
- {
34
- klass: klass.to_s,
35
- action: action.to_sym,
36
- id: id
37
- }
13
+ def payload(model, action)
14
+ { data: payload_data(model), attrs: payload_attrs(model, action) }
38
15
  end
39
16
 
40
17
  private
41
18
 
42
- def build_model_data(model, model_props)
43
- source_props = model_props.map { |prop| prop.to_s.split(':').first }
19
+ def payload_data(model)
20
+ source_props = @attrs.map { |prop| prop.to_s.split(':').first }
44
21
  data = model.as_json(only: source_props, methods: source_props)
45
- aliased_props = model_props.select { |prop| prop.to_s.include?(':') }
22
+ aliased_props = @attrs.select { |prop| prop.to_s.include?(':') }
46
23
  aliased_props.each do |prop|
47
24
  source, target = prop.to_s.split(':')
48
25
  data[target] = data.delete(source)
@@ -50,14 +27,8 @@ module PubSubModelSync
50
27
  data.symbolize_keys
51
28
  end
52
29
 
53
- def build_model_attrs(model, action, settings)
54
- as_klass = (settings[:as_klass] || model.class.name).to_s
55
- id_val = model.send(settings[:id] || :id)
56
- self.class.build_attrs(as_klass, action, id_val)
57
- end
58
-
59
- def log(msg)
60
- PubSubModelSync::Config.log(msg)
30
+ def payload_attrs(model, action)
31
+ { klass: (as_klass || model.class.name).to_s, action: action.to_sym }
61
32
  end
62
33
  end
63
34
  end
@@ -11,6 +11,7 @@ module PubSubModelSync
11
11
  false
12
12
  end
13
13
 
14
+ # TODO: make it using respond_to?(:ps_skip_sync?)
14
15
  # before preparing data to sync
15
16
  def ps_skip_sync?(_action)
16
17
  false
@@ -23,49 +24,49 @@ module PubSubModelSync
23
24
  def ps_after_sync(_action, _data); end
24
25
 
25
26
  # To perform sync on demand
26
- # @param custom_settings (Hash): { attrs: [], as_klass: nil, id: nil }
27
- def ps_perform_sync(action = :create, custom_settings = {})
28
- service = self.class.ps_publisher_service
29
- model_settings = self.class.ps_publisher_info(action) || {}
30
- service.publish_model(self, action, model_settings.merge(custom_settings))
27
+ # @param attrs (Array, optional): custom attrs to be used
28
+ # @param as_klass (Array, optional): custom klass name to be used
29
+ # @param publisher (Publisher, optional): custom publisher object
30
+ def ps_perform_sync(action = :create, attrs: nil, as_klass: nil,
31
+ publisher: nil)
32
+ publisher ||= self.class.ps_publisher(action).dup
33
+ publisher.attrs = attrs if attrs
34
+ publisher.as_klass = as_klass if as_klass
35
+ PubSubModelSync::MessagePublisher.publish_model(self, action, publisher)
31
36
  end
32
37
 
33
38
  module ClassMethods
34
39
  # Permit to configure to publish crud actions (:create, :update, :destroy)
35
- # @param settings (Hash): { actions: nil, as_klass: nil, id: nil }
36
- def ps_publish(attrs, settings = {})
37
- actions = settings.delete(:actions) || %i[create update destroy]
40
+ def ps_publish(attrs, actions: %i[create update destroy], as_klass: nil)
41
+ klass = PubSubModelSync::Publisher
42
+ publisher = klass.new(attrs, name, actions, as_klass)
43
+ PubSubModelSync::Config.publishers << publisher
38
44
  actions.each do |action|
39
- info = settings.merge(klass: name, action: action, attrs: attrs)
40
- PubSubModelSync::Config.publishers << info
41
- ps_register_callback(action.to_sym, info)
45
+ ps_register_callback(action.to_sym, publisher)
42
46
  end
43
47
  end
44
48
 
45
49
  # On demand class level publisher
46
50
  def ps_class_publish(data, action:, as_klass: nil)
47
51
  as_klass = (as_klass || name).to_s
48
- ps_publisher_service.publish_data(as_klass, data, action.to_sym)
52
+ klass = PubSubModelSync::MessagePublisher
53
+ klass.publish_data(as_klass, data, action.to_sym)
49
54
  end
50
55
 
51
56
  # Publisher info for specific action
52
- def ps_publisher_info(action = :create)
53
- PubSubModelSync::Config.publishers.select do |listener|
54
- listener[:klass] == name && listener[:action] == action
55
- end.last
56
- end
57
-
58
- def ps_publisher_service
59
- PubSubModelSync::Publisher.new
57
+ def ps_publisher(action = :create)
58
+ PubSubModelSync::Config.publishers.find do |publisher|
59
+ publisher.klass == name && publisher.actions.include?(action)
60
+ end
60
61
  end
61
62
 
62
63
  private
63
64
 
64
- def ps_register_callback(action, info)
65
+ def ps_register_callback(action, publisher)
65
66
  after_commit(on: action) do |model|
66
67
  unless model.ps_skip_callback?(action)
67
- service = model.class.ps_publisher_service
68
- service.publish_model(model, action.to_sym, info)
68
+ klass = PubSubModelSync::MessagePublisher
69
+ klass.publish_model(model, action.to_sym, publisher)
69
70
  end
70
71
  end
71
72
  end
@@ -18,11 +18,11 @@ module PubSubModelSync
18
18
 
19
19
  private
20
20
 
21
- # @param payload (String JSON): '{"data":{},"attributes":{..}}'
22
- # refer: PubSubModelSync::Publisher (.publish_model | .publish_data)
21
+ # @param payload (String JSON): '{"data":{}, "attributes":{..}}'
22
+ # refer: PubSubModelSync::MessagePublisher(.publish_model | .publish_data)
23
23
  def perform_message(payload)
24
24
  data, attrs = parse_message_payload(payload)
25
- args = [data, attrs[:klass], attrs[:action], attrs]
25
+ args = [data, attrs[:klass], attrs[:action]]
26
26
  PubSubModelSync::MessageProcessor.new(*args).process
27
27
  end
28
28
 
@@ -29,10 +29,12 @@ module PubSubModelSync
29
29
  end
30
30
 
31
31
  def publish(data, attributes)
32
- log("Publishing message: #{[data, attributes]}")
33
-
32
+ log("Publishing message: #{[attributes, data]}")
34
33
  payload = { data: data, attributes: attributes }.to_json
35
34
  topic.publish(payload, { SERVICE_KEY => true })
35
+ rescue => e
36
+ info = [attributes, data, e.message, e.backtrace]
37
+ log("Error publishing: #{info}", :error)
36
38
  end
37
39
 
38
40
  def stop
@@ -29,12 +29,12 @@ module PubSubModelSync
29
29
  end
30
30
 
31
31
  def publish(data, attributes)
32
- log("Publishing: #{[data, attributes]}")
32
+ log("Publishing: #{[attributes, data]}")
33
33
  payload = { data: data, attributes: attributes }
34
34
  producer.produce(payload.to_json, message_settings)
35
35
  producer.deliver_messages
36
36
  rescue => e
37
- info = [data, attributes, e.message, e.backtrace]
37
+ info = [attributes, data, e.message, e.backtrace]
38
38
  log("Error publishing: #{info}", :error)
39
39
  end
40
40
 
@@ -19,7 +19,8 @@ module PubSubModelSync
19
19
  log('Listener starting...')
20
20
  subscribe_to_queue
21
21
  log('Listener started')
22
- queue.subscribe(block: true, manual_ack: false, &method(:process_message))
22
+ queue.subscribe(subscribe_settings, &method(:process_message))
23
+ loop { sleep 5 }
23
24
  rescue PubSubModelSync::Runner::ShutDown
24
25
  raise
25
26
  rescue => e
@@ -27,7 +28,7 @@ module PubSubModelSync
27
28
  end
28
29
 
29
30
  def publish(data, attributes)
30
- log("Publishing: #{[data, attributes]}")
31
+ log("Publishing: #{[attributes, data]}")
31
32
  deliver_data(data, attributes)
32
33
  # TODO: max retry
33
34
  rescue Timeout::Error => e
@@ -35,7 +36,7 @@ module PubSubModelSync
35
36
  initialize
36
37
  retry
37
38
  rescue => e
38
- info = [data, attributes, e.message, e.backtrace]
39
+ info = [attributes, data, e.message, e.backtrace]
39
40
  log("Error publishing: #{info}", :error)
40
41
  end
41
42
 
@@ -50,6 +51,10 @@ module PubSubModelSync
50
51
  { routing_key: queue.name, type: SERVICE_KEY }
51
52
  end
52
53
 
54
+ def subscribe_settings
55
+ { manual_ack: false }
56
+ end
57
+
53
58
  def process_message(_delivery_info, meta_info, payload)
54
59
  return unless meta_info[:type] == SERVICE_KEY
55
60
 
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class Subscriber
5
+ attr_accessor :klass, :action, :attrs, :settings
6
+
7
+ # @param settings: (Hash) { id: :id, direct_mode: false,
8
+ # from_klass: klass, from_action: action }
9
+ def initialize(klass, action, attrs: nil, settings: {})
10
+ def_settings = { id: :id, direct_mode: false,
11
+ from_klass: klass, from_action: action }
12
+ @klass = klass
13
+ @action = action
14
+ @attrs = attrs
15
+ @settings = def_settings.merge(settings)
16
+ end
17
+
18
+ def eval_message(message)
19
+ if settings[:direct_mode]
20
+ run_class_message(message)
21
+ else
22
+ run_model_message(message)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def run_class_message(message)
29
+ model_class = klass.constantize
30
+ model_class.send(action, message)
31
+ end
32
+
33
+ # support for: create, update, destroy
34
+ def run_model_message(message)
35
+ model = find_model(message)
36
+ if action == :destroy
37
+ model.destroy!
38
+ else
39
+ populate_model(model, message)
40
+ return if action == :update && !model.ps_subscriber_changed?(message)
41
+
42
+ model.save!
43
+ end
44
+ end
45
+
46
+ def find_model(message)
47
+ model_class = klass.constantize
48
+ if model_class.respond_to?(:ps_find_model)
49
+ return model_class.ps_find_model(message)
50
+ end
51
+
52
+ model_class.where(model_identifiers(message)).first_or_initialize
53
+ end
54
+
55
+ def model_identifiers(message)
56
+ identifiers = Array(settings[:id])
57
+ identifiers.map { |key| [key, message[key.to_sym]] }.to_h
58
+ end
59
+
60
+ def populate_model(model, message)
61
+ values = message.slice(*attrs)
62
+ values.each do |attr, value|
63
+ model.send("#{attr}=", value)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -6,39 +6,40 @@ module PubSubModelSync
6
6
  base.extend(ClassMethods)
7
7
  end
8
8
 
9
+ # check if model was changed to .save!
10
+ def ps_subscriber_changed?(_data)
11
+ changed?
12
+ end
13
+
9
14
  module ClassMethods
10
- # @param settings (Hash): { as_klass: nil, actions: nil, id: nil }
11
- def ps_subscribe(attrs, settings = {})
12
- as_klass = (settings[:as_klass] || name).to_s
13
- actions = settings.delete(:actions) || %i[create update destroy]
14
- settings = settings.merge(attrs: attrs)
15
+ def ps_subscribe(attrs, actions: nil, from_klass: name, id: :id)
16
+ settings = { id: id, from_klass: from_klass }
17
+ actions ||= %i[create update destroy]
15
18
  actions.each do |action|
16
- add_ps_subscriber(as_klass, action, action, false, settings)
19
+ add_ps_subscriber(action, attrs, settings)
17
20
  end
18
21
  end
19
22
 
20
- def ps_class_subscribe(action, as_action: nil, as_klass: nil)
21
- add_ps_subscriber(as_klass, action, as_action, true, {})
23
+ def ps_class_subscribe(action, from_action: nil, from_klass: nil)
24
+ settings = { direct_mode: true }
25
+ settings[:from_action] = from_action if from_action
26
+ settings[:from_klass] = from_klass if from_klass
27
+ add_ps_subscriber(action, nil, settings)
22
28
  end
23
29
 
24
30
  def ps_subscriber(action = :create)
25
- PubSubModelSync::Config.listeners.select do |listener|
26
- listener[:klass] == name && listener[:action] == action
27
- end.last
31
+ PubSubModelSync::Config.subscribers.find do |subscriber|
32
+ subscriber.klass == name && subscriber.action == action
33
+ end
28
34
  end
29
35
 
30
36
  private
31
37
 
32
- def add_ps_subscriber(as_klass, action, as_action, direct_mode, settings)
33
- listener = {
34
- klass: name,
35
- as_klass: (as_klass || name).to_s,
36
- action: action.to_sym,
37
- as_action: (as_action || action).to_sym,
38
- direct_mode: direct_mode,
39
- settings: settings
40
- }
41
- PubSubModelSync::Config.listeners << listener
38
+ # @param settings (Hash): refer to PubSubModelSync::Subscriber.settings
39
+ def add_ps_subscriber(action, attrs, settings = {})
40
+ klass = PubSubModelSync::Subscriber
41
+ subscriber = klass.new(name, action, attrs: attrs, settings: settings)
42
+ PubSubModelSync::Config.subscribers.push(subscriber) && subscriber
42
43
  end
43
44
  end
44
45
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- VERSION = '0.2.4'
4
+ VERSION = '0.4.2'
5
5
  end
@@ -32,7 +32,6 @@ Gem::Specification.new do |spec|
32
32
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
33
  spec.require_paths = ['lib']
34
34
 
35
- spec.add_dependency 'activesupport'
36
35
  spec.add_dependency 'rails'
37
36
 
38
37
  spec.add_development_dependency 'bundler'
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pub_sub_model_sync
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Owen
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-04-28 00:00:00.000000000 Z
11
+ date: 2020-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: activesupport
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
13
  - !ruby/object:Gem::Dependency
28
14
  name: rails
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -114,10 +100,14 @@ files:
114
100
  - Rakefile
115
101
  - bin/console
116
102
  - bin/setup
103
+ - gemfiles/Gemfile_4
104
+ - gemfiles/Gemfile_5
105
+ - gemfiles/Gemfile_6
117
106
  - lib/pub_sub_model_sync.rb
118
107
  - lib/pub_sub_model_sync/config.rb
119
108
  - lib/pub_sub_model_sync/connector.rb
120
109
  - lib/pub_sub_model_sync/message_processor.rb
110
+ - lib/pub_sub_model_sync/message_publisher.rb
121
111
  - lib/pub_sub_model_sync/mock_google_service.rb
122
112
  - lib/pub_sub_model_sync/mock_kafka_service.rb
123
113
  - lib/pub_sub_model_sync/mock_rabbit_service.rb
@@ -129,6 +119,7 @@ files:
129
119
  - lib/pub_sub_model_sync/service_google.rb
130
120
  - lib/pub_sub_model_sync/service_kafka.rb
131
121
  - lib/pub_sub_model_sync/service_rabbit.rb
122
+ - lib/pub_sub_model_sync/subscriber.rb
132
123
  - lib/pub_sub_model_sync/subscriber_concern.rb
133
124
  - lib/pub_sub_model_sync/tasks/worker.rake
134
125
  - lib/pub_sub_model_sync/version.rb
@@ -140,7 +131,7 @@ metadata:
140
131
  homepage_uri: https://github.com/owen2345/pub_sub_model_sync
141
132
  source_code_uri: https://github.com/owen2345/pub_sub_model_sync
142
133
  changelog_uri: https://github.com/owen2345/pub_sub_model_sync/blob/master/CHANGELOG.md
143
- post_install_message:
134
+ post_install_message:
144
135
  rdoc_options: []
145
136
  require_paths:
146
137
  - lib
@@ -156,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
147
  version: '0'
157
148
  requirements: []
158
149
  rubygems_version: 3.0.8
159
- signing_key:
150
+ signing_key:
160
151
  specification_version: 4
161
152
  summary: Permit to sync models between apps through pub/sub
162
153
  test_files: []