pub_sub_model_sync 0.2.2 → 0.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.
data/README.md CHANGED
@@ -67,7 +67,7 @@ And then execute: $ bundle install
67
67
  # attributes: name email age
68
68
  class User < ActiveRecord::Base
69
69
  include PubSubModelSync::PublisherConcern
70
- ps_publish(%i[name email])
70
+ ps_publish(%i[id name email])
71
71
  end
72
72
 
73
73
  # App 2 (Subscriber)
@@ -86,7 +86,7 @@ User.create(name: 'test user', email: 'sample@gmail.com') # Review your App 2 to
86
86
  User.new(name: 'test user').ps_perform_sync(:create) # similar to above to perform sync on demand
87
87
 
88
88
  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
89
+ PubSubModelSync::MessagePublisher.publish_data(User, { msg: 'Hello' }, :greeting) # similar to above when not included publisher concern
90
90
  ```
91
91
 
92
92
  ## Advanced Example
@@ -95,60 +95,117 @@ PubSubModelSync::Publisher.new.publish_data(User, { msg: 'Hello' }, :greeting) #
95
95
  class User < ActiveRecord::Base
96
96
  self.table_name = 'publisher_users'
97
97
  include PubSubModelSync::PublisherConcern
98
- ps_publish(%i[name:full_name email], actions: %i[update], as_klass: 'Client', id: :client_id)
98
+ ps_publish(%i[id:client_id name:full_name email], actions: %i[update], as_klass: 'Client')
99
99
 
100
- def ps_skip_for?(_action)
100
+ def ps_skip_callback?(_action)
101
101
  false # here logic with action to skip push message
102
102
  end
103
+
104
+ def ps_skip_sync?(_action)
105
+ false # here logic with action to skip push message
106
+ end
103
107
  end
104
108
 
105
109
  # App 2 (Subscriber)
106
110
  class User < ActiveRecord::Base
107
111
  self.table_name = 'subscriber_users'
108
112
  include PubSubModelSync::SubscriberConcern
109
- ps_subscribe(%i[name], actions: %i[update], as_klass: 'Client', id: :custom_id)
110
- ps_class_subscribe(:greeting, as_action: :custom_greeting, as_klass: 'CustomUser')
113
+ ps_subscribe(%i[name], actions: %i[update], from_klass: 'Client', id: %i[client_id email])
114
+ ps_class_subscribe(:greeting, from_action: :custom_greeting, from_klass: 'CustomUser')
115
+ alias_attribute :full_name, :name
111
116
 
112
117
  def self.greeting(data)
113
118
  puts 'Class message called through custom_greeting'
114
119
  end
120
+
121
+ # def self.ps_find_model(data, settings)
122
+ # where(email: data[:email], ...).first_or_initialize
123
+ # end
115
124
  end
116
125
  ```
117
126
 
127
+ Note: Be careful with collision of names
128
+ ```
129
+ class User
130
+ # ps_publish %i[name_data:name name:key] # key will be replaced with name_data
131
+ ps_publish %i[name_data:name key_data:key] # use alias to avoid collision
132
+
133
+ def key_data
134
+ name
135
+ end
136
+ end
137
+ ```
138
+
118
139
  ## API
119
- - To perform a callback before publishing message (CRUD)
140
+ ### Subscribers
141
+ - Permit to configure class level listeners
142
+ ```ps_class_subscribe(action_name, from_action: nil, from_klass: nil)```
143
+ * from_action: (Optional) Source method name
144
+ * from_klass: (Optional) Source class name
145
+
146
+ - Permit to configure instance level listeners (CRUD)
147
+ ```ps_subscribe(attrs, from_klass: nil, actions: nil, id: nil)```
148
+ * attrs: (Array/Required) Array of all attributes to be synced
149
+ * from_klass: (String/Optional) Source class name (Instead of the model class name, will use this value)
150
+ * actions: (Array/Optional, default: create/update/destroy) permit to customize action names
151
+ * id: (Sym|Array/Optional, default: id) Attr identifier(s) to find the corresponding model
152
+
153
+ - Permit to configure a custom model finder
154
+ ```ps_find_model(data, settings)```
155
+ * data: (Hash) Data received from sync
156
+ * settings: (Hash(:klass, :action)) Class and action name from sync
157
+ Must return an existent or a new model object
158
+
159
+ - Get crud subscription configured for the class
160
+ ```User.ps_subscriber(action_name)```
161
+ * action_name (default :create, :sym): can be :create, :update, :destroy
162
+
163
+ - Inspect all configured listeners
164
+ ```PubSubModelSync::Config.listeners```
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
+
173
+ - Permit to cancel sync called after create/update/destroy (Before initializing sync service)
174
+ ```model.ps_skip_callback?(action)```
175
+ Note: Return true to cancel sync
176
+
177
+ - Callback called before preparing data for sync (Permit to stop sync)
178
+ ```model.ps_skip_sync?(action)```
179
+ Note: return true to cancel sync
180
+
181
+ - Callback called before sync (After preparing data)
120
182
  ```model.ps_before_sync(action, data_to_deliver)```
121
- Note: If the method returns ```false```, the message will not be published
183
+ Note: If the method returns ```:cancel```, the sync will be stopped (message will not be published)
122
184
 
123
- - To perform a callback after publishing message (CRUD)
124
- ```model.ps_after_sync(action, data_to_deliver)```
125
- Note: If the method returns ```false```, the message will not be published
185
+ - Callback called after sync
186
+ ```model.ps_after_sync(action, data_delivered)```
126
187
 
127
188
  - Perform sync on demand (:create, :update, :destroy):
128
189
  The target model will receive a notification to perform the indicated action
129
- ```my_model.ps_perform_sync(action_name)```
190
+ ```my_model.ps_perform_sync(action_name, custom_settings = {})```
191
+ * custom_settings: override default settings defined for action_name ({ attrs: [], as_klass: nil })
130
192
 
131
- - Class level notification:
193
+ - Publish a class level notification:
132
194
  ```User.ps_class_publish(data, action: action_name, as_klass: custom_klass_name)```
133
195
  Target class ```User.action_name``` will be called when message is received
134
196
  * data: (required, :hash) message value to deliver
135
- * action_name: (required, :sim) same action name as defined in ps_class_subscribe(...)
136
- * as_klass: (optional, :string) same class name as defined in ps_class_subscribe(...)
197
+ * action_name: (required, :sim) Action name
198
+ * as_klass: (optional, :string) Custom class name (Default current model name)
137
199
 
138
- - Class level notification (Same as above: on demand call)
139
- ```PubSubModelSync::Publisher.new.publish_data(Klass_name, data, action_name)```
200
+ - Publish a class level notification (Same as above: on demand call)
201
+ ```PubSubModelSync::MessagePublisher.publish_data(Klass_name, data, action_name)```
140
202
  * klass_name: (required, Class) same class name as defined in ps_class_subscribe(...)
141
203
  * data: (required, :hash) message value to deliver
142
204
  * action_name: (required, :sim) same action name as defined in ps_class_subscribe(...)
143
205
 
144
- - Get crud subscription configured for the class
145
- ```User.ps_subscriber(action_name)```
146
- * action_name (default :create, :sym): can be :create, :update, :destroy
147
206
  - Get crud publisher configured for the class
148
207
  ```User.ps_publisher(action_name)```
149
208
  * action_name (default :create, :sym): can be :create, :update, :destroy
150
- - Inspect all configured listeners
151
- ```PubSubModelSync::Config.listeners```
152
209
 
153
210
  ## Testing with RSpec
154
211
  - Config: (spec/rails_helper.rb)
@@ -181,12 +238,10 @@ end
181
238
  # Subscriber
182
239
  it 'receive model message' do
183
240
  action = :create
184
- data = { name: 'name' }
185
- user_id = 999
186
- attrs = PubSubModelSync::Publisher.build_attrs('User', action, user_id)
187
- publisher = PubSubModelSync::MessageProcessor.new(data, 'User', action, id: user_id)
241
+ data = { name: 'name', id: 999 }
242
+ publisher = PubSubModelSync::MessageProcessor.new(data, 'User', action)
188
243
  publisher.process
189
- expect(User.where(id: user_id).any?).to be_truth
244
+ expect(User.where(id: data[:id]).any?).to be_truth
190
245
  end
191
246
 
192
247
  it 'receive class message' do
@@ -199,20 +254,20 @@ end
199
254
 
200
255
  # Publisher
201
256
  it 'publish model action' do
202
- publisher = PubSubModelSync::Publisher
257
+ publisher = PubSubModelSync::MessagePublisher
203
258
  data = { name: 'hello'}
204
259
  action = :create
205
260
  User.ps_class_publish(data, action: action)
206
261
  user = User.create(name: 'name', email: 'email')
207
- expect_any_instance_of(publisher).to receive(:publish_model).with(user, :create, anything)
262
+ expect(publisher).to receive(:publish_model).with(user, :create, anything)
208
263
  end
209
264
 
210
265
  it 'publish class message' do
211
- publisher = PubSubModelSync::Publisher
266
+ publisher = PubSubModelSync::MessagePublisher
212
267
  data = {msg: 'hello'}
213
268
  action = :greeting
214
269
  User.ps_class_publish(data, action: action)
215
- expect_any_instance_of(publisher).to receive(:publish_data).with('User', data, action)
270
+ expect(publisher).to receive(:publish_data).with('User', data, action)
216
271
  end
217
272
  ```
218
273
 
@@ -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,79 +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
- log 'processing message'
17
- listeners = filter_listeners
18
- eval_message(listeners) if listeners.any?
19
- log 'processed message'
15
+ subscribers = filter_subscribers
16
+ subscribers.each { |subscriber| run_subscriber(subscriber) }
20
17
  end
21
18
 
22
19
  private
23
20
 
24
- def eval_message(listeners)
25
- listeners.each do |listener|
26
- if listener[:direct_mode]
27
- call_class_listener(listener)
28
- else
29
- call_listener(listener)
30
- end
31
- end
32
- end
33
-
34
- def call_class_listener(listener)
35
- model_class = listener[:klass].constantize
36
- model_class.send(listener[:action], data)
37
- rescue => e
38
- log("Error listener (#{listener}): #{e.message}", :error)
39
- end
40
-
41
- # support for: create, update, destroy
42
- def call_listener(listener)
43
- model = find_model(listener)
44
- if attrs[:action].to_sym == :destroy
45
- model.destroy!
46
- else
47
- populate_model(model, listener)
48
- model.save!
49
- end
21
+ def run_subscriber(subscriber)
22
+ subscriber.eval_message(data)
23
+ log "processed message with: #{[klass, action, data]}"
50
24
  rescue => e
51
- log("Error listener (#{listener}): #{e.message}", :error)
52
- end
53
-
54
- def find_model(listener)
55
- model_class = listener[:klass].constantize
56
- identifier = listener[:settings][:id] || :id
57
- model_class.where(identifier => attrs[:id]).first ||
58
- model_class.new(identifier => attrs[:id])
59
- end
60
-
61
- def populate_model(model, listener)
62
- values = data.slice(*listener[:settings][:attrs])
63
- values.each do |attr, value|
64
- model.send("#{attr}=", value)
65
- end
25
+ info = [klass, action, data, e.message, e.backtrace]
26
+ log("error processing message: #{info}", :error)
66
27
  end
67
28
 
68
- def filter_listeners
69
- listeners = PubSubModelSync::Config.listeners
70
- listeners.select do |listener|
71
- listener[:as_klass].to_s == attrs[:klass].to_s &&
72
- 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
73
33
  end
74
34
  end
75
35
 
76
36
  def log(message, kind = :info)
77
- PubSubModelSync::Config.log "#{message} ==> #{[data, attrs]}", kind
37
+ PubSubModelSync::Config.log message, kind
78
38
  end
79
39
  end
80
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
@@ -30,6 +30,10 @@ module PubSubModelSync
30
30
  def topic(*_args)
31
31
  @topic ||= MockTopic.new
32
32
  end
33
+
34
+ def close
35
+ true
36
+ end
33
37
  end
34
38
 
35
39
  def create_channel(*_args)
@@ -2,43 +2,24 @@
2
2
 
3
3
  module PubSubModelSync
4
4
  class Publisher
5
- attr_accessor :connector
6
- def initialize
7
- @connector = PubSubModelSync::Connector.new
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
8
11
  end
9
12
 
10
- def publish_data(klass, data, action)
11
- attributes = self.class.build_attrs(klass, action)
12
- connector.publish(data, attributes)
13
- end
14
-
15
- # @param settings (Hash): { attrs: [], as_klass: nil, id: nil }
16
- def publish_model(model, action, settings = nil)
17
- settings ||= model.class.ps_publisher_info(action)
18
- attributes = build_model_attrs(model, action, settings)
19
- data = {}
20
- data = build_model_data(model, settings[:attrs]) if action != :destroy
21
- res_before = model.ps_before_sync(action, data)
22
- return if res_before == false
23
-
24
- connector.publish(data.symbolize_keys, attributes)
25
- model.ps_after_sync(action, data)
26
- end
27
-
28
- def self.build_attrs(klass, action, id = nil)
29
- {
30
- klass: klass.to_s,
31
- action: action.to_sym,
32
- id: id
33
- }
13
+ def payload(model, action)
14
+ { data: payload_data(model), attrs: payload_attrs(model, action) }
34
15
  end
35
16
 
36
17
  private
37
18
 
38
- def build_model_data(model, model_props)
39
- 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 }
40
21
  data = model.as_json(only: source_props, methods: source_props)
41
- aliased_props = model_props.select { |prop| prop.to_s.include?(':') }
22
+ aliased_props = @attrs.select { |prop| prop.to_s.include?(':') }
42
23
  aliased_props.each do |prop|
43
24
  source, target = prop.to_s.split(':')
44
25
  data[target] = data.delete(source)
@@ -46,14 +27,8 @@ module PubSubModelSync
46
27
  data.symbolize_keys
47
28
  end
48
29
 
49
- def build_model_attrs(model, action, settings)
50
- as_klass = (settings[:as_klass] || model.class.name).to_s
51
- id_val = model.send(settings[:id] || :id)
52
- self.class.build_attrs(as_klass, action, id_val)
53
- end
54
-
55
- def log(msg)
56
- PubSubModelSync::Config.log(msg)
30
+ def payload_attrs(model, action)
31
+ { klass: (as_klass || model.class.name).to_s, action: action.to_sym }
57
32
  end
58
33
  end
59
34
  end
@@ -6,54 +6,66 @@ module PubSubModelSync
6
6
  base.extend(ClassMethods)
7
7
  end
8
8
 
9
- # Permit to skip a publish callback
10
- def ps_skip_for?(_action)
9
+ # Before initializing sync service (callbacks: after create/update/destroy)
10
+ def ps_skip_callback?(_action)
11
11
  false
12
12
  end
13
13
 
14
+ # before preparing data to sync
15
+ def ps_skip_sync?(_action)
16
+ false
17
+ end
18
+
19
+ # before delivering data
14
20
  def ps_before_sync(_action, _data); end
15
21
 
22
+ # after delivering data
16
23
  def ps_after_sync(_action, _data); end
17
24
 
18
- def ps_perform_sync(action = :create)
19
- service = self.class.ps_publisher_service
20
- service.publish_model(self, action, self.class.ps_publisher_info(action))
25
+ # To perform sync on demand
26
+ # @param attrs (Array, optional): custom attrs to be used
27
+ # @param as_klass (Array, optional): custom klass name to be used
28
+ # @param publisher (Publisher, optional): custom publisher object
29
+ def ps_perform_sync(action = :create, attrs: nil, as_klass: nil,
30
+ publisher: nil)
31
+ publisher ||= self.class.ps_publisher(action).dup
32
+ publisher.attrs = attrs if attrs
33
+ publisher.as_klass = as_klass if as_klass
34
+ PubSubModelSync::MessagePublisher.publish_model(self, action, publisher)
21
35
  end
22
36
 
23
37
  module ClassMethods
24
- # Permit to publish crud actions (:create, :update, :destroy)
25
- # @param settings (Hash): { actions: nil, as_klass: nil, id: nil }
26
- def ps_publish(attrs, settings = {})
27
- actions = settings.delete(:actions) || %i[create update destroy]
38
+ # Permit to configure to publish crud actions (:create, :update, :destroy)
39
+ def ps_publish(attrs, actions: %i[create update destroy], as_klass: nil)
40
+ klass = PubSubModelSync::Publisher
41
+ publisher = klass.new(attrs, name, actions, as_klass)
42
+ PubSubModelSync::Config.publishers << publisher
28
43
  actions.each do |action|
29
- info = settings.merge(klass: name, action: action, attrs: attrs)
30
- PubSubModelSync::Config.publishers << info
31
- ps_register_callback(action.to_sym, info)
44
+ ps_register_callback(action.to_sym, publisher)
32
45
  end
33
46
  end
34
47
 
35
- def ps_publisher_info(action = :create)
36
- PubSubModelSync::Config.publishers.select do |listener|
37
- listener[:klass] == name && listener[:action] == action
38
- end.last
39
- end
40
-
48
+ # On demand class level publisher
41
49
  def ps_class_publish(data, action:, as_klass: nil)
42
50
  as_klass = (as_klass || name).to_s
43
- ps_publisher_service.publish_data(as_klass, data, action.to_sym)
51
+ klass = PubSubModelSync::MessagePublisher
52
+ klass.publish_data(as_klass, data, action.to_sym)
44
53
  end
45
54
 
46
- def ps_publisher_service
47
- PubSubModelSync::Publisher.new
55
+ # Publisher info for specific action
56
+ def ps_publisher(action = :create)
57
+ PubSubModelSync::Config.publishers.find do |publisher|
58
+ publisher.klass == name && publisher.actions.include?(action)
59
+ end
48
60
  end
49
61
 
50
62
  private
51
63
 
52
- def ps_register_callback(action, info)
64
+ def ps_register_callback(action, publisher)
53
65
  after_commit(on: action) do |model|
54
- unless model.ps_skip_for?(action)
55
- service = model.class.ps_publisher_service
56
- service.publish_model(model, action.to_sym, info)
66
+ unless model.ps_skip_callback?(action)
67
+ klass = PubSubModelSync::MessagePublisher
68
+ klass.publish_model(model, action.to_sym, publisher)
57
69
  end
58
70
  end
59
71
  end